Java提高——JUC锁03-公平锁(一)
1、AQS——指AbstractQueuedSynchronizer类
AQS是Java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现的。AQS是独占锁(如ReentrantLock)和共享锁(如Semaphore)的公共父类。
2、AQS锁的类别——分为“独占锁”和“共享锁”
1)独占锁:锁在一个时点只能被一个线程占有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。公平锁,是按照通过CLH等待队列按照先来先得的规则,公平的获取锁;非公平锁,当线程要获取锁的时候,它会无视CLH等待队列而直接获取锁。独占锁的典型例子是ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁。
2)共享锁:能被多个线程同时拥有,能被共享的锁。JUC包中的ReentrantReadWriteLock.ReadLock、CyclicBarrier、CountDownLatch和Semaphre都是共享锁。
3、CLH队列——Craig、Landin、and Hagersten lock queue
CLH队列是AQS中“等待锁”的线程队列。在多个线程中,为了保护竞争资源不被多个线程同时操作出现错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时点只能被一个线程访问,而其他线程则需要等待。CLH就是管理这些这些“等待锁”的线程的队列。
CLH是一个非阻塞的FIFO队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和CAS保证节点插入和移除的原子性。
4、CAS——Compare And Swap
CAS函数,是一个比较并交换的函数,它是原子操作函数;即通过CAS操作的数据都是以原子的方式进行的。例如,compareAndSetHead( ),compareAndSetTail( ),compareAndSetNext( )等函数。它的共同特点是,这些函数所执行的动作是以原子的方式进行的。
ReentrantLock数据结构
ReentrantLock的UML图
可以看出:
1)ReentrantLock实现了Lock接口
2)ReentrantLock与sync是组合关系。ReentrantLock中包含了Sync对象;而且Sync是AQS的子类;更重要的是Sync有两个子类,公平锁(FairSync)和非公平锁(NonfairSync)。ReentrantLock是一个独占锁,至于是公平的还是非公平取决于sync对象是FareSync实例还是NonfairSync实例。
获取公平锁
1、Lock
lock( )在ReentrantLock中的FairSync类中实现,源码:
final void lock() { acquire(1); }
当前线程实际上是通过acquire(1)获取锁的。
这里的“1”是设置锁状态的参数,对于独占锁,锁处于可获取的状态时,它的状态值是0;锁被线程初次获取到了,状态值就是1。
由于ReentrantLock(公平锁/非公平锁)是可重入锁,所以“独占锁”可以被线程多次获取,每次获取就将锁的状态+1。初次获取锁时,通过acquire(1)将锁的状态设为1;再次获取的时候将锁的状态设为2,以此类推----这就是为什么获取锁时传入的参数为1的原因。
可重入是指锁可以被单个线程多次获取。
2、acquire( )
acquire( )在AQS中实现的,源码:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
1)“当前线程”通过acquire( )获取锁,如果成功则直接返回,如果失败则进入到等待队列中排队等待(前面可能有线程在等待锁)
2)“当前线程”获取失败的情况下,先通过addWaiter(Node.EXCLUSIVE)将当前线程加入到CLH队列(非阻塞的FIFO队列)末尾。CLH队列就是线程等待队列。
3)执行完addWaiter之后,会调用acquireQueued( )来获取锁。由于此时ReentrantLock是公平锁,它会根据公平性原则来获取锁。
4)“当前线程”在执行acquireQueued()时,会进入到CLH队列中休眠等待,直到获取锁了才返回!如果“当前线程”在休眠中被中断,acquireQueued()会返回true,此时,“当前线程”会调用selfInterrupt( )来自己给自己产生一个中断
接下来介绍上面源码中的各个方法:
一、tryAcquire( )
1、公平锁的tryAcquire()在ReentrantLock的FairSync类中的实现,源码:
protected final boolean tryAcquire(int acquires) { //获取当前线程 final Thread current = Thread.currentThread(); //获取独占锁的状态 int c = getState(); //若锁没有被任何线程拥有 //则判断,当前线程是不是CLH中的第一个线程 //若是,则获取该锁,设置锁的状态,并且设置锁的拥有者为当前线程 if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //若独占锁的拥有者为当前线程,则跟新锁的状态 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }根据源码得知tryAcquire()只是尝试获取锁,成功返回true,失败返回false,后续再通过其他办法获取锁。
2、hasQueuedPredecessor()在AQS中的实现
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }hasQueuedPredecessor()是判断当前线程在CLH队列是不是队首,返回AQS中是不是比“当前线程”等待更久的线程。
3、Node的源码
//CLH队列的节点 static final class Node { /** Marker to indicate a node is waiting in shared mode */ static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ static final Node EXCLUSIVE = null; /** 线程被取消后waitStatus的值 */ static final int CANCELLED = 1; /** 当前线程的后续线程需要被唤醒时waitStatus的值 一般发生的情况是:当前线程的后续线程处于阻塞状态,而当前线程被release或cancle掉, 因此需要唤醒当前线程的后续线程 */ static final int SIGNAL = -1; /** 线程(处在Condition休眠状态)在等待Condition唤醒,对应的waitStatus的值 */ static final int CONDITION = -2; /** * 其他线程获取到“共享锁”对应的waitStatus的值 * unconditionally propagate */ static final int PROPAGATE = -3; /* * * waitStatus为: * SINGNAL: * CANCELLED: * CONDITION: * PROPAGATE: 时,分别表示不同的状态 * 若waitStatus为0: 则意味着当前线程不属于上面任何一种状态 * */ volatile int waitStatus; /** * 前一节点 */ volatile Node prev; /** * 后一节点 */ volatile Node next; /** * 节点所对应的线程 */ volatile Thread thread; /** * nextWaiter是区别当前锁为独占锁队列还是共享锁队列的标记 * 若nextWaiter=SHARED. 则CLH是独占锁队列 * 若nextWaiter=EXCLUSIVE(即nextWaiter=null),则CLH是共享锁队列 */ Node nextWaiter; /** * 共享锁则返回true,独占锁则返回false */ final boolean isShared() { return nextWaiter == SHARED; } /** * 返回前一节点 */ final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } //构造函数,thread是节点所对应的线程,mode是用来表示thread是独占锁还是共享锁 Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } //构造函数,thread是节点所对应的线程,waitStatus是线程的等待状态 Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
Node是CLH队列的节点,代表等待锁的线程队列。
1)每个Node都会有一个线程对应
2)每个Node都会通过pre和next分别指向上一个节点和下一个节点,这分别代表上一个等待线程和下一个等待线程
3)Node通过waitStatus保存线程的等待状态
4)Node通过nextWaiter来区分是独占锁还是共享锁。如果是独占锁,则nextWaiter的值为EXCLUSIVE;如果是共享锁,则nextWaiter的值为SHARED。
4、compareAndSetState( )
compareAndSetState()在AQS中的实现,源码:
protected final boolean compareAndSetState(int expect, int update) { // See below for intrinsics setup to support this return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
compareAndSetState()是sun.misc.Unsafe类中的一个本地方法。对此我们需要了解compareAndSetState()是以原子的方式操作当前线程;若当前线程的状态是expect,则它设置的状态是update。
5、setExclusiveOwnerThread( )
setExclusiveOwnerThread( )是在AbstractOwnableSynchronizer中实现,源码:
/** * exclusiveOwnerThread是当前拥有独占锁的线程 */ private transient Thread exclusiveOwnerThread; /** * setExclusiveOwnerThread的作用就是设置线程为独占线程 */ protected final void setExclusiveOwnerThread(Thread thread) { exclusiveOwnerThread = thread; }
6、getState()、setState()
/** * 锁的状态 */ private volatile int state; /** * 获取锁的状态 */ protected final int getState() { return state; } /** * 设置锁的状态 */ protected final void setState(int newState) { state = newState; }
state表示锁的状态,对于独占锁,state=0表示锁处于可获取状态(即锁没有被任何线程持有)。由于Java中的独占锁是可重入锁。state的值可以>1。
小结:tryAcquire()的作用就是让当前线程尝试获取锁。获取成功返回true,失败返回false。
二、addWaiter(Node.EXCLUSIVE)
addW(Node.EXCLUSIVE)的作用就是创建“当前线程”的Node节点,且Node中记录“当前线程”对应的锁是独占锁类型,并将该节点添加到CLH队列的末尾。
1、addWaiter()在AQS中实现的源码,源码:
private Node addWaiter(Node mode) { //创建一个节点,节点对应当前线程,线程的锁的模型是mode Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure //若CLH不为空,则将当前线程添加到CLH末尾 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //若CLH为空,则调用enq方法新建CLH队列,然后再将当前线程添加到CLH队列中 enq(node); return node; }
2、compareAndSetTail( )
compareAndSetTail( )在AQS中的实现,源码如下:
private final boolean compareAndSetTail(Node expect, Node update) { return unsafe.compareAndSwapObject(this, tailOffset, expect, update); }
compareAndSetTail( )也属于CAS函数,通过本地方法实现的。compareAndSetTail(expect,update )会以原子的方式操作,他的作用是判断CLH的队尾是不是expect,是的话,就将队尾设置为update。
3、enq( )
enq( )在AQS中的实现,源码:
private Node enq(final Node node) { 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; } } } }
enq的作用,如果CLH队列为空,则新建一个CLH表头;然后将node添加到CLH末尾。否则直接将node添加到CLH末尾。
小结:addWaiter的作用是将当前线程添加到CLH队列中。这就意味着将当前线程添加到等待获取锁的等待线程队列中了。
三、acquireQueued()
1、acquireQueued()在AQS中的实现,源码:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { //interrupted表示在CLH调度中,当前线程在休眠中有没有被中断过 boolean interrupted = false; for (;;) { // 获取上一个节点。node是当前线程对应的节点,这就意味着获取上一个等待锁的线程 final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
acquireQueued()的目的是从队列中获取锁。
2、shouldParkAfterFailedAcquire()
shouldParkAfterFailedAcquire()在AQS中的实现,源码如下:
//返回当前线程是否应该阻塞 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //前继节点的状态 int ws = pred.waitStatus; //如果前继节点是SIGNAL状态,则意味着当前线程需要被unpark唤醒。此时返回true if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /*如果前继节点为取消状态,则设置当前节点的当前前继节点为原前继节点的前继节点。 * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /*如果前继节点为0或者共享状态,则设置前继节点为SIGNAL状态 * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
1)关于waitStatus参考:
CANCELLED[1] -- 当前线程已被取消
SIGNAL[-1] -- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。
CONDITION[-2] -- 当前线程(处在Condition休眠状态)在等待Condition唤醒
PROPAGATE[-3] -- (共享锁)其它线程获取到“共享锁”[0] -- 当前线程不属于上面的任何一种状态。
2)shouldParkAfterFailedAcquire()通过以下规则,判断当前线程是否需要被阻塞:
①如果前继节点状态为SINGNAL,表明当前节点需要被unpark(唤醒),此时则返回true
②如果前继节点状态我CANCELLED(ws>0),说明前继节点被取消,则通过先前回溯找到一个有效的节点,并返回false
③如果前继节点状态为非SINGNAL、非CANCELLED,则设置前继节点的状态为SIGNAL,并返回false
如果“规则1”发生,即“前继节点是SIGNAL”状态,则意味着“当前线程”需要被阻塞。接下来会调用parkAndCheckInterrupt()阻塞当前线程,直到当前先被唤醒才从parkAndCheckInterrupt()中返回。
3、parkAndCheckInterrupt()
parkAndCheckInterrupt()在AQS中的实现,源码:
private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//通过LockSupport的park阻塞当前线程 return Thread.interrupted();//返回线程的中断状态 }
parkAndCheckInterrupt()的作用是阻塞当前线程,并返回当前线程被唤醒后的中断状态。它首先会通过LockSupport.park()阻塞当前线程,然后通过Thread.interrupted返回线程的中断状态。
阻塞之后如何唤醒:
情况1:unpark()唤醒,前继节点对应的线程使用完锁之后,通过unpark方式唤醒线程
情况2:中断唤醒。其他线程通过interrupt中断当前线程。
补充:LockSupport中的park和unpark的作用和Object中的wait、notify作用类似,是阻塞和唤醒。它们用法不同,park和unpark是轻量级的,而wait和notify是必须先通过Synchronized获取同步锁。
4、再次tryAcquire()
final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; }1)通过node.predecessor( )获取前继节点。prodecessor( )就是返回node的前继节点
2)p==head&&tryAcquire( args)
首先判断前继节点是不是CLH表头,如果是则通过tryAcquire尝试获取锁。
其实这么做的原因是为了让当前线程获取锁 。为什么要先判断p==head呢?因为这样做是为了保证公平性:
a、前面我们在shouldParkAfterFailedAcquire()判断当前线程是否需要阻塞
b、接着,,当前线程阻塞的话,会调用parkAndCheckInterrupt( )来阻塞线程。当前线程被解除阻塞的时候,我们会返回线程的中断状态。而线程被解决阻塞可能是由于“线程被中断” ,也可能其他线程调用该线程的unpark函数。
c、再回到p==head这里,如果当前线程因为其他线程调用了unpark函数而被唤醒,那么唤醒它的线程应该是前继节点所对应的线程。
再来理解p==head:当前继节点是CLH队列的头节点的时候,并且释放锁之后;就轮到当前节点获取锁了。然后当前节点通过tryAcquire()获取锁;获取成功则通过setHead(node)设置当前节点为头结点,并返回。
总之,如果前继节点调用unpark函数唤醒了当前线程,并且前继节点是CLH的表头,此时满足p==head,也就符合公平性原则。否则,如果当前线程因为“线程中断”而被唤醒,那么就显得不公平。这就是为什么说p==hend是公平性原型的保证。
小结:acquireQueued()的作用就是“当前线程”会根据公平性原则进行阻塞等待,直到获取锁为止;并返回当前线程在等待过程中有没有 被中断过。
四、selfInterrupt( )
selfInterrupt()在AQS中的源码如下:
static void selfInterrupt() { Thread.currentThread().interrupt(); }
意思就是 “当前线程”自己产生一个中断。原因是什么拉?
必须结合acquireQueued( )进行说明。如果在acquireQueue( )中,当前线程被中断过,则执行selInterrupt();否则不执行。
在acquireQueue()中,即使线程在阻塞状态被中断唤醒而获取到CPU执行权利;但是,如果该线程的前面还有其他等待锁的线程,根据公平性原则该线程依然无法获取到锁。它会在次阻塞,直到该线程被它前面的等待锁的线程唤醒;线程才会获取锁,然后“真正执行起来”!
也就是说,在该线程“成功获取锁真正执行起来”之前,它的中断会被忽略并且中断标记会被清除!因为在parkAndCheckInterrupt ()中,我们的线程中断状态调用了Thread.interrupted()。该函数不同于Thread的isInterrupted( )函数,isInterrupted()仅仅返回中断状态,而interrupted( )在返回当前中断状态之后,还会清除中断状态。正因为之前的中断状态被清除了,所以这里需要调用selInterrupted( )重新产生一个新的中断。
小结:selInterrupted()的作用就是当前线程自己产生一个中断。
总结:
再回头来看,acquire( )函数的最终目的是获取锁!
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
1)、先通过tryAcquire( )尝试获取锁。获取成功的话,直接返回;获取失败的话则通过acquireQueued()获取锁
2)、尝试失败的情况下会先通过addWaiter()来将当前线程“加入到CLH队列”末尾;然后调用acquireQueued(),在CLH队列中排队等待获取锁,在此过程中线程处于休眠状态。直到获取锁才会返回。如果在休眠等待过程中被中断过,则调用isInterrupt()来自己产生一个中断。