重点及完成情况
- 线程池
- volatile内存屏障,storeload等四种规则
- final多线程重排序与实现原理
- DCL
- ThreadLocalMap源码
- condition
- mutex
- GC
- ConcurrentHashMap
- ReentrantLock。。
- AQS同步器组件
- 原子类操作,浮点型的原子操作
- 循环打印1a2b3c….LockSupport;synchronized;condition;transfreeQueue实现
- 循环打印3次ABC实现。
- 两种和三种线程顺序执行的区别
JUC
Unsafe
通过unsafe可以直接操作内存区。该类没有办法通过GC回收。
并且unsafe不用使用提供的getUnsafe方法进行实例化,因为对于jvm来说这是不安全的,
会抛出securitException。
但是可以通过反射进行实例化。
sun.misc.Unsafe提供了可以随意查看及修改JVM中运行时的数据结构的方法。
原子类
为了解决操作的原子性问题,如i++,可以分为三个操作,仅仅使用volatile会存在线程安全问题。
- 悲观锁的解决方式
直接加锁,同一时间只有一个线程可以操作i,如synchronized
但是线程的频繁挂起唤醒会造成严重资源消耗。 - 乐观锁的解决方式
默认其他线程不会参与竞争修改,在同步时使用给冲突检测机制,不阻塞线程,自旋反复重试。
分类
基本数据类型:AtomicInteger, AtomicLong, AtomicBoolean
数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
引用类型:AtomicReference,AtomicStampedRerence,AtomicMarkableReference
对象属性修改类型:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
基本数据类型
使用volatile修饰value,将非原子操作分开执行。
如:
int i = 0;
i++;
使用原子类,会先取得i的初始值O,然后,执行+1操作等到新值N,最后通过cas进行赋值把N值赋给i,再次过程中会进行比较初始值与现在i的初始值,一样才会执行赋值操作,否则自旋。
AQS
AbstractQueuedSynchronizer是用来构建同步组件的基础框架。
主要依靠一个int成员变量(state)来表示同步状态以及通过FIFO(先进先出)队列构成等待队列来实现。
他的子类必须重写AQS的几个protected修饰的用来改变同步状态的方法。
除此之外的其他方法主要实现了排队和阻塞机制。
基本原理
AQS通过维护一个CLH队列来实现资源线程的排队工作。
该队列使用双端双向链表实现。
线程获取资源失败后,会被构造成一个node结点添加到CLH队列中,当前线程会被阻塞在队列中(LockSupport的park方法),当持有资源的线程释放同步状态时,会唤醒后继结点,然后后继结点加入同步状态的竞争中。
两种同步方式:
- 独占式
- ReentrantLock
- 共享式
- Semaphore
- CountDownLatch
- 组合式
- ReentantWriteLock
同步器的实现基于模板方法模式(除此之外的模板设计模式应用还有servlet与其实现类)
- 重写AQS的指定方法(主要是对共享资源state的获取和释放)
- 将AQS组合在自定义同步组件的实现中,并调用重写后的模板方法
两种同步方式及其实现
两种同步方式:
- 独占式
- ReentrantLock
- 共享式
- Semaphore
- CountDownLatch
- 组合式
- ReentantWriteLock
Node结点
node结点为AQS中的一个静态内部类。
负值表示结点处于有效等待状态,正值表示结点已被取消。
- CANCELLED(1)
表示当前节点已经取消调度,当超时或被中断(相应中断标志时),会触发变更为此状态,进入该状态后的结点将不再变化。 - SIGNAL(-1)
表示后继结点在等待当前结点唤醒。后继结点在入队时,会将前驱结点的状态更新为singnal - CONDITION(-2)
表示结点等待在condition上,当其他线程调用了condition的signal方法后,condition的状态结点将从等待对垒转移到同步队列中,等待获取同步锁。 - PROPAGATE(-3)
共享模式下,前驱结点不仅会唤醒其后继节点,同时也可能唤醒后继的后继结点。 - 0: 新入队的默认状态。
方法
可重写的方法:
以下方法不需要全部重写,只需要根据需求重写相应的方法,如独占锁,不用实现共享锁的方法。开发者系需要完成ixie简单的资源状态的获取和释放操作,其余的问题交给AQS完成。tryAcquire
protected boolean tryAcquire(int arg)
//独占式获取同步状态。成功返回true,否则返回false
tryRelease
protected boolean tryRelease(int arg)
//独占式释放同步状态,等待中的其他线程此时将有机会获取到同步状态。
tryAcquireShared
protected int tryAcquireShared(int arg)
//共享式获取同步状态,返回值大于0代表成功,反之失败
tryReleaseShared
protected boolean tryReleaseShared(int arg);
//共享式释放同步状态,成功为true,失败为false
isHeldExclusively
protected boolean isHeldExclusively()
//是否在独占模式下被线程占用。
独占式
acquire
acquire方法是独占模式下获取共享资源的顶层入口,用来获取同步锁
获取到资源,线程直接返回,否则进入等待队列,直到获取到资源位为止。
整个过程中忽略中断的影响。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- final修饰,不能被重写。
- 开始尝试直接获取资源,成功直接返回
- 不成功,将创造一个结点,通过自旋加入到队尾
- 通过自旋判断是否前驱结点为队头,是则尝试获取同步状态,
- 否则设置前驱结点的状态为singal后,判断当前线程是否可以休息
- 可以则休息等待唤醒,不可以则进行自旋。
- 获取到同步状态后,将head指向自己。返回整个过程中是否中断过。
该线程在整个过程中如果被中断是不响应的,只是返回是否被执行了中断,在获取到了资源后在对中断操作进行相应。
addwaiter方法添加新节点到队尾
将获取资源失败的线程创建一个node对象,然后将该node对象加入到队列的尾部。
插入过程中可能会存在多个线程同时插入到尾部的情况,所以使用cas首先尝试直接插入,插入失败后使用cas自旋插入。
enq方法自旋插入队尾
保证能够线程安全的添加到队尾。
acquireQueued方法处理插入队列中的线程
该方法同样使用了自旋,通过自旋的方式不断判断是否为队头,
队头尝试直接插入,插入成功则返回过程中是否出现中断。
不是队头则判断当前线程能否阻塞休息。
shouldParkAfterFailedAcquire方法 判断该线程是否能够休息
首先获取前驱结点的等待状态。
状态为singal,直接返回true,可以休息。
状态>0(说明线程已经处于cannel状态,已经无效),从后往前遍历,找到状态非cannel的结点,将自己设置为该节点的后续。
找到正常状态的节点后,通过cas将该节点的状态设置为singal。
parkAndCheckInterrupt方法使线程进入阻塞状态,并且返回线程是否中断过。
该方法使用park进入等待状态,并执行中断操作等待unpark唤醒。后续将中断状态改为true;
release
public final boolean release(int arg) {
if (tryRelease(arg)) {//调用使用者重写的tryRelease方法,若成功,唤醒其后继结点,失败则返回false
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//唤醒后继结点
return true;
}
return false;
}
释放独占式锁的同步状态。
调用同步器重写实现的tryRelease方法来释放锁,成功唤醒后继结点,失败返回false。
自定义同步器对于tryRelease的实现,一般来说直接剪掉相应量的资源即可,state-=arg,该过程不需要考虑线程安全问题,但是注意返回值是boolean,所以应判断state==0.
unparkSuccessor方法
获取当前结点的状态,小于0则置为0
寻找正常的后继结点(不为空并且状态<0),后继结点不为空则执行唤醒操作(unpark)。
共享式
共享式获取同步状态,同一时刻可以有多个线程同时获取到同步状态。
tryAcquireShared
尝试获取同步资源,需要同步器来重写实现。
其返回结果:
1. 返回值大于0,表明获取同步状态成功,且还有剩余同步状态可用。
2. 返回值等于0,表明获取同步状态成功,且没有剩余同步状态了
3. 返回值小于0,表明获取同步状态失败。
acquireShared
共享式获取同步资源方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)//返回值小于0,获取同步状态失败,排队去;获取同步状态成功,直接返回去干自己的事儿。
doAcquireShared(arg);
}
获取资源失败,执行doAcquireShared,进行排队,获取成功直接放回。
doAcquireShared方法cas获取资源
构造一个共享节点,添加到同步队列尾部,与独占模式一样,cas实现
判断前驱结点是否是头节点,是则尝试获取资源,否则判断时候能够休息方式同独占模式相同。
不同的是在获取资源的时候,独占模式返回的结果是true或false
共享模式返回int类型判断是否可以获取到同步。获取成功就当前结点设置为头结点,如果还有可用资源,传播下去,继续唤醒后继结点。
setHeadAndPropagate方法设置头节点并且唤醒后续结点。
取出队列的头节点后,将自己设置为头节点。判断如果可获取的资源>0或者取出的头为空或者状态正常,那么当前节点的后继结点,如果为空或者是共享状态,则执行释放操作。
releaseShared
共享模式释放同步状态。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//释放同步状态
return true;
}
return false;
}
尝试直接释放锁,失败返回false,成功后唤醒后继结点。
doReleaseShared方法唤醒后继节点。
使用cas,如果头节点不为空且不是队尾,尝试将头节点的状态设置为0,如果不成功,则进行自旋,成功的话将唤醒后继节点。
共享与独占的实现区别
自旋的实现
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
通过不断循环的方式进行自旋。插入不成功就不断取得最后一个结点尝试插入,直到成功。
模板方法设计模式
父类中实现一个适用于所有情况的模板,在其子类中针对各个类的实际情况,实现自己的不同
公平锁与非公平锁
公平锁:按照线程在队列中的顺序获取锁的资源,先到先得。
非公平锁:线程想要获取资源时,无视队列顺序直接获取,抢到就是得到。
非公平方式如:AQS在实现获取资源时首先直接进行获取资源,而不是直接插入队列,直接获取不到后才进行插入队尾操作。
而公平方式:对应的时候插入队列的线程取得资源的方式。
互斥锁
LockSupport
locakSupport是一个线程阻塞工具类,主要为了实现阻塞和唤醒线程使用。
其所有方法都是静态方法。可以让线程在任意位置阻塞,任意位置唤醒。
与此不同wait和notify只能在同步代码块中执行。
park与unpark方法
park方法实现线程阻塞,unpark实现线程唤醒。
他们之间通过二元信号量(0,1)做的阻塞,可以理解为许可证。
初始状态是0
unpark方法会释放许可证,park会消耗一个许可证。
线程在获取资源时如果不存在许可证,线程会进入等待状态,
如果存在许可证,则消耗一个许可证而获取到资源。
无论执行多少次unpark,许可的数量只能是1
unpark可以先于park调用。
与wait/notify的区别
park、unpark比wait、notify要灵活。解耦了之前的线程之间的同步。同样是阻塞和唤醒操作,但是两者没有交集,park阻塞的线程,notify不能唤醒。
- park不需要获取对象的monitor。所以不用必须在同步代码块中调用
- notify只能随机唤醒一个线程,而且不能准确唤醒。unpark可以准确唤醒。
- park/unpark不会引发死锁。
- wait方法会强制让出锁,而park不会
Lock接口
Lock提供比Synchronized更加详细灵活的操作。
Condition
Codition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待通知模式。
线程在调用await方法后执行挂起操作。直到线程等待的某个条件为真时才会被唤醒。不同的条件队列,根据不同的条件阻塞、唤醒队列中的线程。
例如:生产者消费者模式中,多线程对同一个阻塞队列进行操作时,可以创建两个条件队列分别对生产者和消费者进行加锁。当阻塞队列满时,唤醒消费者条件队列中的线程来消费,为空时唤醒生产者条件队列来生产。
与wait/notify最大区别可以根据不同的条件精准的唤醒不同类型的线程。
Condition必须与锁一起使用,Condition实例必须与一个lock绑定。所以Condition一般是作为lock的内部实现。
获取一个condition必须要通过lock的newCondition方法实现。返回一个绑定到此lock下的condition对象。
condition是一个接口,其下只有一个实现类ConditionObject,由于Condition的操作需要获取相关的锁,而AQS是同步锁的实现基础,所以ConditionObject定义为AQS的内部类。
Condition的方法必须要在lock中执行,类似于wait和notify要在synchronized中执行。
- Condition接口可以支持多个等待队列,在前面已经提到一个Lock实例可以绑定多个Condition,所以自然可以支持多个等待队列了
- Condition接口支持响应中断,前面已经提到过
- Condition接口支持当前线程释放锁并进入等待状态到将来的某个时间,也就是支持定时功能
wait/notify与await/signal的比较
Condtion中的await对应Object的wait;
Condition中的signal对应object的notify;
condition中的notifyAll对应object的notifyAll;
ConditionObject类
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
//头节点
private transient Node firstWaiter;
//尾节点
private transient Node lastWaiter;
public ConditionObject() {
}
/** 省略方法 **/
}
每一个ConditionObject象都包含一个FIFO队列。
FIFO队列由多个Node结点组成,每个结点都包含一个线程引用。
await
调用await方法会使当前线程进入等待状态,同时会将当前线程加入到condition等待队列中并且释放锁,当从await方法返回时,当前线程一定是取得了condition相关联的锁。
signal
唤醒被阻塞的线程,唤醒时从对应的condition队列中唤醒队头获取资源。
Lock方法
尝试获取锁。成功返回,否则阻塞当前线程。
lockInterruptibly() 尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常。
tryLock() tryLock(long time , TimeUnit unit)
尝试获取锁,成功返回true,否则返回false
规定时间内获取锁,返回true,否则返回false,期间被中断抛出异常。
ReentrantLock
reentrnatLock为可重入互斥锁,能够对共享资源重复加锁。
支持公平锁和非公平锁两种方式。
其内部主要有三个静态内部类实现。
Sync,NonFairSync,FairSync
Sync是公用的同步组件,继承了AQS。
NonFairSync,FairSync都继承自Sync,分别实现了公平与非公平逻辑。
使用公平锁与非公平锁可以通过创建对象时的传参决定。true使用公平锁,false使用非公平锁,默认false。
- lock方法和lockInterruptibly方法
线程使用lock方法获取锁时,如果该线程被执行了中断操作,该线程依然会获取锁,在取得锁之后,首先判断是否被中断,然后执行其他操作。而lockInterruptibly方法,如果在获取锁的过程中被执行了中断操作,那么该线程将不再参与获取锁的操作。
NonFairSync
非公平可重入锁
- 获取锁
- 获取同步资源的state状态值,若为0,意味着此时没有线程获取到资源,CAS将其设置为1,设置成功则代表获取到排他锁了;
- 若state大于0,肯定有线程已经抢占到资源了,此时再去判断是否就是自己抢占的,是的话,state累加,返回true,重入成功,state的值即是线程重入的次数;
- 其他情况下,获取锁失败,
- 获取锁失败会返回到AQS中获取锁失败的逻辑,创建一个新的等待结点,判断是否能够进行休息。。。
- 释放锁
释放锁的流程,公平锁与非公平锁相同。都是使用的Sync中的方法。两个实现类并没有重写。- 将state状态值减一,如果state==0,将资源持有线程设为null,返回释放成功。AQS中会处理进行唤醒后续结点。
- 如果不为0,返回释放失败,
FairSync
公平可重入锁
- 获取锁
- 获取资源状态,如果为0,进行判断是否存在排在自己之前的未在执行线程。没有则尝试获取资源。
- state不为0的情况和非公平锁一致。
- 释放锁
循环打印3次ABC
ReentrantReadWriteLock
可重入读写锁。
读写锁其中包括读锁和写锁。
读锁为共享锁,写锁为独占锁。
在读多写少的情况下,读写锁可以很好的提高效率。
Smaphore
信号量是一个共享锁,可以允许多个线程同时访问资源。
同样支持公平与非公平锁。
创建对象时需要传入信号量大小,最多可以允许多少个线程访问同一资源。
acquire方法获取许可证,release方法驶方资源,增加许可证。smaphore主要来维护许可证的数量。
用于限制某种资源的数量。
阻塞队列
- 阻塞队列的几个方法 | 方法类型 | 抛出异常 | 返回布尔 | 阻塞 | 超时 | | —- | —- | —- | —- | —- | | 插入 | add(E e) | offer(E e) | put(E e) | offer(E e,Time,TimeUnit) | | 取出 | remove() | poll() | take() | poll(Time,TimeUnit) | | 队首 | element() | peek() | 无 | 无 |
offer方法和put方法通过enqueue方法来实现。
区别在于,offer方法插入失败会返回false,put方法在队列为满时插入会调用condition的await方法进入阻塞状态。put方法在插入时调用的加锁方法是可被中断的。
两个方法在插入之前都会进行加锁。使用的是可重入锁,ReentrantLock。
而add方法调用了put方法进行插入,返回false时会抛出异常。
取出操作同理。
阻塞之后的唤醒使用的是condition条件队列的signal方法。该方法是notify的升级版。
三种常用的阻塞队列
- SynchronousQueue : 单个元素的阻塞队列,不存储元素,生产一个消费一个
- ArrayBlockingQueue :底层是数组,有界阻塞队列
- LinkedBlockingQueue :底层是链表,无界阻塞队列
LinkedBlockingQueue 和ArrayBlockingQueue 的区别
- ArrayBlockingQueue,初始化时需要传入数组大小,LinkedBlockingQueue,可以传值,默认为int最大值。
- ArrayBlock只有一个锁,LinkedBlock有两个锁,分别插入和移除操作。linkedblock性能会高
- array是固定内存,linkedblock是动态内存。