06-多线程笔记-2-锁-3-Lock
1 AQS
AbstractQueuedSynchronizer简称AQS,它是java.util.concurrent包下CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore实现的基础。
AQS通过内部实现的FIFO等待队列来完成资源获取线程的等待工作,如果当前线程获取资源失败,AQS则会将当前线程以及等待状态等信息构造成一个Node结构的节点,并将其加入等待队列中,同时会阻塞当前线程;当其它获取到资源的线程释放持有的资源时,则会把等待队列节点中的线程唤醒,使其再次尝试获取对应资源。
AQS是一个抽象类,当我们继承AQS去实现自己的同步器时,要做的仅仅是根据自己同步器需要满足的性质实现线程获取和释放资源的方式(修改同步状态变量的方式)即可,至于具体线程等待队列的维护(如获取资源失败入队、唤醒出队、以及线程在队列中行为的管理等),AQS在其顶层已经帮我们实现好了,AQS的这种设计使用的正是模板方法模式。
参考JDK11
AbstractOwnableSynchronizer
线程专有同步器,此类为创建锁和可能需要所有权概念的相关同步器提供了基础。这个类是
AbstractQueuedSynchronizer
的父类;
此类只有一个变量
/**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread;
和对应的get/set函数;在java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryRelease
、java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
等函数中用于制定拥有锁的线程;
实例属性
AQS使用一个int成员变量state去表征当前资源的同步状态。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;
AQS持有head指针和tail指针,头结点是抢占锁成功而持有锁的线程对应的结点,若有线程抢锁失败,AQS会创建新结点并用CAS操作使其成为新的尾结点
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
Node
AQS通过维护一个等待获取锁的线程队列来管理(获取资源失败入队/唤醒出队)抢占锁的线程,这个队列是一种 CLH介绍的变体。
AQS把对某线程的一些控制信息放到了其前驱中维护,当某结点的前驱释放锁或被取消时会唤醒其后继,而其后继会在获取锁成功后将自己设为新的头结点,AQS对这个维护等待线程队列的操作都是非阻塞的,也是线程安全的。队列中的每个结点都是类Node的一个实例。
-
共享
AQS支持线程抢占两种锁——独占锁和共享锁:
- 独占锁:同一个时刻只能被一个线程占有,如ReentrantLock,ReentrantWriteLock等;
- 共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadLock,Semaphore等;
在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;
-
等待状态
Node节点中,维护
waitStatus
属性标识线程的等待状态;waitStatus
初始默认为0,Condition队列中初始默认为-2;它有以下几种取值://结点已被取消,表示线程放弃抢锁,结点状态以后不再变直到GC回收它 static final int CANCELLED = 1; //结点的后继已经或很快就阻塞,在结点释放锁或被取消时要唤醒其后面第1个非CANCELLED结点 static final int SIGNAL = -1; /** Condition队列中结点的状态,CLH队列中结点没有该状态,当Condition的signal方法被调用, Condition队列中的结点被转移进CLH队列并且状态变为0 **/ static final int CONDITION = -2; //与共享模式相关,当线程以共享模式去获取或释放锁时,对后续线程的释放动作需要不断往后传播 static final int PROGAGATE = -3; 0:新结点入队时的默认状态。
负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
模板方法
AQS提供了针对锁操作的一些目标方法,这些方法需要在子类中实现,像
tryReleaseShared/tryAcquireShared,tryAcquire/tryRelease
,
独占锁
-
获取
如果成功获取锁,则返回;如果获取锁失败,则将将当前线程加入到等待队列中,并在队列中执行自旋获取锁,直到成功获取锁或者线程满足阻塞条件而阻塞;当前线程只有被前一个有效等待节点记录后才能进入阻塞状态。前一个有效节点释放锁后,会通知处于阻塞状态的当前线程,然后当前线程重新自旋获取锁;
如果在获取锁的过程中出现异常,会将当前节点从等待队列中删除。acquire(int arg)方法是独占模式下线程获取共享资源的顶层入口。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
函数流程如下:
(1)tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
(2)addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
(3)acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
(4)如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。加入CLH队列的过程不再描述,主要对
acquireQueued()
进行说明-
acquireQueued(Node, int)
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ final boolean acquireQueued(final Node node, int arg) { // 当前线程是否被中断 boolean interrupted = false; try { // 自旋获取资源 for (;;) { // 获取当前线程对应节点的前驱结点 final Node p = node.predecessor(); // 如果前驱结点是头节点,当前线程有获取资源的资格,然后获取资源 if (p == head && tryAcquire(arg)) { // 如果获取资源成功,修改队列头指针指向自己 setHead(node); // 释放当前节点的头节点示例 p.next = null; // help GC // 返回中断状态 return interrupted; } // 没有获取资源资格,或者获取资源失败,需要执行shouldParkAfterFailedAcquire方法,用于确定当前结点对应的线程是否可以进入阻塞状态 if (shouldParkAfterFailedAcquire(p, node)) // 如果可以进入阻塞状态,执行parkAndCheckInterrupt方法以阻塞当前线程 interrupted |= parkAndCheckInterrupt(); } // 如果执行tryAcquire失败,取消失败节点 } catch (Throwable t) { // 删除当前节点 cancelAcquire(node); if (interrupted) selfInterrupt(); throw t; } }
-
-
释放
释放锁分两步,第一步释放资源,第二步唤醒当前节点下一个可用的节点对应的线程;
共享锁
-
获取
共享资源的获取,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。
public final void acquireShared(int arg) { // 如果成功获取共享锁,直接返回;否则,进入等待队列 if (tryAcquireShared(arg) < 0) // 进入等待队列,自旋获取锁或阻塞 doAcquireShared(arg); } /** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ private void doAcquireShared(int arg) { // 加入到等待队列 final Node node = addWaiter(Node.SHARED); // 是否中断标识,此处也是在获取锁结束后响应中断,只有在获取中断锁(acquireSharedInterruptibly)时,才会在获取锁的过程中响应中断 boolean interrupted = false; try { for (;;) { // 获取前继节点 final Node p = node.predecessor(); // 如果前继节点为头节点(成功获取了锁的节点),则此阶段有资格获取锁 if (p == head) { //尝试获取锁 int r = tryAcquireShared(arg); // 获取锁成功 if (r >= 0) { // 成功获取锁后,将自身设置为等待队列的头节点(之前的头节点会出队,被GC回收),并唤醒所有后续的共享锁申请节点 setHeadAndPropagate(node, r); p.next = null; // help GC return; } } // 如果没有获取锁资格,判断是否可以进入阻塞状态;如果可以,线程进入阻塞状态 if (shouldParkAfterFailedAcquire(p, node)) interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { // 如果请求共享锁发生异常,将当前节点从等待队列中删除,取消申请锁流程 cancelAcquire(node); throw t; } finally { // 响应中断 if (interrupted) selfInterrupt(); } } /** * Sets head of queue, and checks if successor may be waiting * in shared mode, if so propagating if either propagate > 0 or * PROPAGATE status was set. * * @param node the node * @param propagate the return value from a tryAcquireShared */ private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below // 修改等待队列的头节点,之前的头节点出队,等待GC回收 setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ // 如果资源有剩余量,并且头节点等待状态小于0(小于0表示头节点后续有阻塞节点),则继续处理后续节点 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; // 如果后续节点是申请共享锁的,则唤醒后续节点 if (s == null || s.isShared()) doReleaseShared(); } } /** * Release action for shared mode -- signals successor and ensures * propagation. (Note: For exclusive mode, release just amounts * to calling unparkSuccessor of head if it needs signal.) */ private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { //记录当前头节点,后面会唤醒后继结点,如果后继结点成功获取共享锁,会修改等待队列的头节点; Node h = head; // if (h != null && h != tail) { int ws = h.waitStatus; // 如果节点等待状态为SIGNAL,后续肯定有阻塞节点,则唤起后续节点 if (ws == Node.SIGNAL) { // 将当前节点的等待状态置为0(初始化状态) if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) continue; // loop to recheck cases // 唤醒后续节点 unparkSuccessor(h); } // 如果当前节点的等待状态为0(节点初始化,无后续节点),将节点状态修改为PRORAGATE,表示一旦出现一个新的共享结点连接在该结点后,该结点的共享锁将传播下去。 else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) continue; // loop on failed CAS } // 如果后继结点未能成功获取共享锁(获取成功后会修改头节点),结束循环 if (h == head) // loop if head changed break; } }
-
释放
共享锁的释放后,会唤醒后续节点。
public final void acquireShared(int arg) { // 如果成功获取共享锁,直接返回;否则,进入等待队列 if (tryAcquireShared(arg) < 0) // 进入等待队列,自旋获取锁或阻塞 doAcquireShared(arg); } /** * Acquires in shared uninterruptible mode. * @param arg the acquire argument */ private void doAcquireShared(int arg) { // 加入到等待队列 final Node node = addWaiter(Node.SHARED); // 是否中断标识,此处也是在获取锁结束后响应中断,只有在获取中断锁(acquireSharedInterruptibly)时,才会在获取锁的过程中响应中断 boolean interrupted = false; try { for (;;) { // 获取前继节点 final Node p = node.predecessor(); // 如果前继节点为头节点(成功获取了锁的节点),则此阶段有资格获取锁 if (p == head) { //尝试获取锁 int r = tryAcquireShared(arg); // 获取锁成功 if (r >= 0) { // 成功获取锁后,将自身设置为等待队列的头节点(之前的头节点会出队,被GC回收),并唤醒所有后续的共享锁申请节点 setHeadAndPropagate(node, r); p.next = null; // help GC return; } } // 如果没有获取锁资格,判断是否可以进入阻塞状态;如果可以,线程进入阻塞状态 if (shouldParkAfterFailedAcquire(p, node)) interrupted |= parkAndCheckInterrupt(); } } catch (Throwable t) { // 如果请求共享锁发生异常,将当前节点从等待队列中删除,取消申请锁流程 cancelAcquire(node); throw t; } finally { // 响应中断 if (interrupted) selfInterrupt(); } } /** * Sets head of queue, and checks if successor may be waiting * in shared mode, if so propagating if either propagate > 0 or * PROPAGATE status was set. * * @param node the node * @param propagate the return value from a tryAcquireShared */ private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below // 修改等待队列的头节点,之前的头节点出队,等待GC回收 setHead(node); /* * Try to signal next queued node if: * Propagation was indicated by caller, * or was recorded (as h.waitStatus either before * or after setHead) by a previous operation * (note: this uses sign-check of waitStatus because * PROPAGATE status may transition to SIGNAL.) * and * The next node is waiting in shared mode, * or we don't know, because it appears null * * The conservatism in both of these checks may cause * unnecessary wake-ups, but only when there are multiple * racing acquires/releases, so most need signals now or soon * anyway. */ // 如果资源有剩余量,并且头节点等待状态小于0(小于0表示头节点后续有阻塞节点),则继续处理后续节点 if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { Node s = node.next; // 如果后续节点为空,重新检查等待队列,或是申请共享锁的,则唤醒后续节点 if (s == null || s.isShared()) doReleaseShared(); } } /** * Release action for shared mode -- signals successor and ensures * propagation. (Note: For exclusive mode, release just amounts * to calling unparkSuccessor of head if it needs signal.) */ private void doReleaseShared() { /* * Ensure that a release propagates, even if there are other * in-progress acquires/releases. This proceeds in the usual * way of trying to unparkSuccessor of head if it needs * signal. But if it does not, status is set to PROPAGATE to * ensure that upon release, propagation continues. * Additionally, we must loop in case a new node is added * while we are doing this. Also, unlike other uses of * unparkSuccessor, we need to know if CAS to reset status * fails, if so rechecking. */ for (;;) { //记录当前头节点,后面会唤醒后继结点,如果后继结点成功获取共享锁,会修改等待队列的头节点; Node h = head; // if (h != null && h != tail) { int ws = h.waitStatus; // 如果节点等待状态为SIGNAL,后续肯定有阻塞节点,则唤起后续节点 if (ws == Node.SIGNAL) { // 将当前节点的等待状态置为0(初始化状态) if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0)) continue; // loop to recheck cases // 唤醒后续节点 unparkSuccessor(h); } // 如果当前节点的等待状态为0(节点初始化,无后续节点),将节点状态修改为PRORAGATE,表示一旦出现一个新的共享结点连接在该结点后,该结点的共享锁将传播下去。 else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE)) continue; // loop on failed CAS } // 如果后继结点成功获取共享锁(获取成功后会修改头节点),结束循环 if (h == head) // loop if head changed break; } } /** * Wakes up node's successor, if one exists. * * @param node the node */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) node.compareAndSetWaitStatus(ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node p = tail; p != node && p != null; p = p.prev) if (p.waitStatus <= 0) s = p; } if (s != null) LockSupport.unpark(s.thread); }
Condition/ConditionObject
在没有Lock之前,我们使用synchronized来控制同步,配合Object的wait()、notify()系列方法可以实现等待/通知模式。在Java SE5后,Java提供了Lock接口,相对于Synchronized而言,Lock提供了条件Condition,对线程的等待、唤醒操作更加详细和灵活。
Condition是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。
ConditionObject是AQS中的内部类,提供了条件锁的同步实现,实现了Condition接口,并且实现了其中的await(),signal(),signalALL()等方法。在一个AQS同步器中,可以定义多个Condition,只需要多次lock.newCondition(),每次都会返回一个新的ConditionObject对象。在ConditionObject中,通过一个条件队列来维护条线等待的线程。所以在一个同步器中可以有多个等待队列,他们等待的条件是不一样的。
-
条件队列
条件队列是一个FIFO的队列,在队列的每个节点都包含了一个线程引用。该线程就是在Condition对象上等待的线程。这里的节点和AQS中的同步队列中的节点一样,使用的都是AbstractQueuedSynchronizer.Node类。每个调用了condition.await()的线程都会进入到 条件队列中去。
在Condition中包含了firstWaiter和lastWaiter,每次加入到 条件队列中的线程都会加入到 条件队列的尾部,来构成一个FIFO的 条件队列。 -
await()
JDK11中ConditionObject中的实现源码
public final void await() throws InterruptedException { // 如果当前线程中断,抛出异常 if (Thread.interrupted()) throw new InterruptedException(); // 将当前线程加入到等待队列的尾部 Node node = addConditionWaiter(); // 释放线程占用的锁,如果释放出现异常,标记当前节点为CANCELLED,后续会删除此节点 int savedState = fullyRelease(node); int interruptMode = 0; // 如果当前线程不在同步队列中 while (!isOnSyncQueue(node)) { // 阻塞当前线程,直到被唤醒或线程中断(调用await方法后,上面完成了加入条件队列,释放锁的过程,阻塞到此处,后续是被唤醒后的流程) LockSupport.park(this); // 如果线程中断,结束循环 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 被唤醒后,会尝试去获取资源(资源数据量由之前保存) if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; // 如果当前节点后续还有等待线程,清理条件队列(将当前节点清理出条件队列) if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); // 对中断进行处理 if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
文档
- https://www.cnblogs.com/waterystone/p/4920797.html
- https://www.cnblogs.com/awakedreaming/p/9510021.html
- https://zhuanlan.zhihu.com/p/41456147
- https://www.cnblogs.com/gunduzi/p/13614429.html
- https://www.cnblogs.com/zerotomax/p/8969416.html
2 Lock
与synchronized的区别
在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的
常用方法
-
lock()
lock()方法用来获取锁。如果锁已被其他线程获取,则进行等待。在前面已经讲到,如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
Lock lock = ...; lock.lock(); try{ //处理任务 }catch(Exception ex){ }finally{ lock.unlock(); //释放锁 }
-
tryLock() & tryLock(long time, TimeUnit unit)
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。 -
void lockInterruptibly() throws InterruptedException;
lockInterruptibly()方法能够中断等待获取锁的线程。当两个线程同时通过lock.lockInterruptibly()获取某个锁时,假若此时线程A获取到了锁,而线程B只有等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
-
Condition newCondition();
条件变量(java.util.concurrent.Condition),如果说ReentrantLock是synchronized的替代选择,Condition则是将wait、notify、notifyAll等操作转化为相应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为。
详细介绍在AQS小节中进行说明
ReentrantLock
ReentrantLock,即可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法.
可重入锁是指在同一个线程在一个临界区获取锁后,再进入该线程的其他临界区会自动获取锁(前提锁对象是同一个对象或者类对象)。ReentrantLock和synchronized都是可重入锁,可重入锁可以避免线程持有锁但又请求锁造成的死锁问题。
这里有一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的。ReentrantLock提供了一个构造函数,能够控制这个锁是否是公平的。
事实上公平的锁机制往往没有非公平的效率高,因为公平的获取锁没有考虑到操作系统对线程的调度因素,这样造成JVM对于等待中的线程调度次序和操作系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程中,连续获取的概率是非常高的,而公平锁会压制这种情况,虽然公平性得以保障,但是响应比却下降了,但是并不是任何场景都是以TPS作为唯一指标的,因为公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足
-
NonfairSync
非公平锁继承了ReetrantLock内部类Sync。在Sync中实现了非公屏锁获取的关键方法(JDK11):
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
当锁可用时,直接获取并返回获取结果;当线程相同时,直接重入,否则返回获取锁失败;
-
FairSync
公平锁也是集成自ReetrantLock的内部类Sync。在FairSync中实现了公平锁获取方法:
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); 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; }
与非公平锁明显区别在于,当锁可用时,会使用
hasQueuedPredecessors
判断当前线程是否有获取锁的资格。public final boolean hasQueuedPredecessors() { Node h, s; if ((h = head) != null) { if ((s = h.next) == null || s.waitStatus > 0) { s = null; // traverse in case of concurrent cancellation for (Node p = tail; p != h && p != null; p = p.prev) { if (p.waitStatus <= 0) s = p; } } if (s != null && s.thread != Thread.currentThread()) return true; } return false; }
在
hasQueuedPredecessors
函数中,通过遍历AQS(队列同步器,下节进行详细说明)中等待队列,判断是否有比当前线程等待时间更长的线程,如果有,那么当前线程不具有获取锁的资格,返回true;如果当前线程是等待时间最长的线程,当前线程有获取锁的资格,返回false;
3 ReadWriteLock
现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。
ReetranctLock和Synchronized能够允许一个线程进入多个临界区(可重入),但是不能运行不同线程进入同一个临界区,也就是无法实现多个线程同时读取共享资源;
使用
ReadWriteLock
可以解决这个问题,它保证:
- 只允许一个线程写入(其他线程既不能写入也不能读取);
- 没有写入时,多个线程允许同时读(提高性能)。
ReadWriteLock
有以下三个重要的特性:
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- 重进入:读锁和写锁都支持线程重进入。
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
ReentrantReadWriteLock
JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁.
- 线程进入读锁的前提条件
没有其他线程的写锁;
没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。
- 线程进入写锁的前提条件
没有其他线程的读锁;
没有其他线程的写锁
-
Sync
Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。
Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象。
-
读写状态的设计
在
ReentrantReadWriteLock
的内部类Sync
中,定义了若干变量,将同步器状态值state
拆分成两部分,高16位标识读锁,低16位标识写锁;abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 6317671515068378041L; /* * Read vs write count extraction constants and functions. * Lock state is logically divided into two unsigned shorts: * The lower one representing the exclusive (writer) lock hold count, * and the upper the shared (reader) hold count. */ // 将int类型的state(32位),拆分成unsigned shorts(16位) static final int SHARED_SHIFT = 16; // 读锁基本单位(高16位的1) static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 读写锁最大数量 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 写锁的子码,用于从state中计算写锁值 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; // 省略其他剩余代码 }
-
-
ReadLock
-
获取锁
protected final int tryAcquireShared(int unused) { /* * Walkthrough: * 1. If write lock held by another thread, fail. * 2. Otherwise, this thread is eligible for * lock wrt state, so ask if it should block * because of queue policy. If not, try * to grant by CASing state and updating count. * Note that step does not check for reentrant * acquires, which is postponed to full version * to avoid having to check hold count in * the more typical non-reentrant case. * 3. If step 2 fails either because thread * apparently not eligible or CAS fails or count * saturated, chain to version with full retry loop. */ // 获取当前线程 Thread current = Thread.currentThread(); // 获取锁状态 int c = getState(); // 如果有写锁并且写锁不是当前线程,获取读锁失败 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 获取读锁数量 int r = sharedCount(c); // 1. 如果当前线程有获取锁的资格(公平锁只有等待最久的线程可以获取,非公平锁没有限制) // 2. 读锁的数量小于最大数量 // 3. 更新读锁数据,(c+SHARED_UNIT)=(sharedCount(c) + 1) << 16 = (c >>> 16 + 1) << 16,相当于在高16位加1, if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // 如果读锁没有被线程获取,直接设置第一个拥有读锁的线程为当前线程(第一个拥有读锁的参数设置是为了优化性能) if (r == 0) { firstReader = current; firstReaderHoldCount = 1; // 如果第一个读锁拥有线程为当前线程,锁重入数加1 } else if (firstReader == current) { firstReaderHoldCount++; } else { // 获取上一个拥有锁的线程的线程数计数器(性能优化) HoldCounter rh = cachedHoldCounter; // 如果没有线程数计数器,或者线程数计数器对应的线程不是当前线程,获取线程空间中保持的线程计数器,并修改锁对象中线程计数器; if (rh == null || rh.tid != LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); // 如果上一个拥有锁的线程是当前线程,并且计数器为0(上一个线程释放了读锁),将线程计数器对象保存到当前线程的空间中。 else if (rh.count == 0) readHolds.set(rh); // 线程计数器加1 rh.count++; } return 1; } // 如果当前线程不具有获取锁资格或比较交换操作失败,则重新获取读锁 return fullTryAcquireShared(current); }
-
释放锁
protected final boolean tryReleaseShared(int unused) { // 获取当前线程 Thread current = Thread.currentThread(); // 如果第一个获取读锁的线程是当前线程 if (firstReader == current) { // assert firstReaderHoldCount > 0; // 如果只获取了一次读锁,丢弃锁对象中缓存的第一个获取读锁线程信息 if (firstReaderHoldCount == 1) firstReader = null; // 如果第一个获取读锁的线程多次获取读锁,获取读锁数减1 else firstReaderHoldCount--; // 如果当前线程不是第一个获取读锁的线程 } else { // 获取上一个拥有读锁的线程对应的线程计数器 HoldCounter rh = cachedHoldCounter; // 如果没有缓存的线程计数器(上一个拥有读锁的线程释放了读锁),或者上一个拥有读锁的线程不是当前线程,从当前线程空间中获取线程计数器 if (rh == null || rh.tid != LockSupport.getThreadId(current)) rh = readHolds.get(); int count = rh.count; // 如果当前线程拥有读锁数不超过1 if (count <= 1) { // 移除当前线程中对应的线程计数器(会造成锁对象中缓存的上一个拥有读锁的线程计数器为null,也就是上一个判断条件) readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } // 如果当前线程多次申请读锁,线程计数器值减1 --rh.count; } // 自旋更新锁状态值 for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; } }
-
-
WriteLock
-
获取锁
@ReservedStackAccess protected final boolean tryAcquire(int acquires) { /* * Walkthrough: * 1. If read count nonzero or write count nonzero * and owner is a different thread, fail. * 2. If count would saturate, fail. (This can only * happen if count is already nonzero.) * 3. Otherwise, this thread is eligible for lock if * it is either a reentrant acquire or * queue policy allows it. If so, update state * and set owner. */ Thread current = Thread.currentThread(); int c = getState(); // 获取写锁状态 int w = exclusiveCount(c); if (c != 0) { // (Note: if c != 0 and w == 0 then shared count != 0) // 如果写锁未被获取(有线程获取了读锁),或者获取写锁的不是当前线程,则获取写锁失败 if (w == 0 || current != getExclusiveOwnerThread()) return false; // 如果获取写锁后的数量超长最大值,抛出异常 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire // 修改锁状态值 setState(c + acquires); return true; } // 如果锁未被获取(写锁,读锁均未被获取), // 1. 如果当前线程不具有获取写锁的资格(公平锁会有限制),获取比较交换操作失败,则获取写锁失败 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; // 修改锁拥有线程 setExclusiveOwnerThread(current); return true; }
-
释放锁
protected final boolean tryRelease(int releases) { // 如果当前线程不是拥有写锁的线程,抛出异常 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; // 如果写锁只被请求一次(未被写锁拥有线程多次申请),则修改写锁拥有线程为null,修改写锁请求值为0(释放写锁),否则写锁释放失败 if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }
-
-
参考文档
4 StampedLock
StampedLock是并发包里面jdk8版本新增的一个锁,该锁提供了三种模式的读写控制,三种模式分别如下:
写锁writeLock,是个排它锁或者叫独占锁,同时只有一个线程可以获取该锁,当一个线程获取该锁后,其它请求的线程必须等待,当目前没有线程持有读锁或者写锁的时候才可以获取到该锁,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockWrite并传递参数stamp。
悲观读锁readLock,是个共享锁,在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁,如果已经有线程持有写锁,其他线程请求获取该读锁会被阻塞。这里讲的悲观其实是参考数据库中的乐观悲观锁的,这里说的悲观是说在具体操作数据前悲观的认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑,请求该锁成功后会返回一个stamp票据变量用来表示该锁的版本,当释放该锁时候需要unlockRead并传递参数stamp。
乐观读锁tryOptimisticRead,是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,如果当前没有线程持有写锁,则简单的返回一个非0的stamp版本信息,获取该stamp后在具体操作数据前还需要调用validate验证下该stamp是否已经不可用,也就是看当调用tryOptimisticRead返回stamp后到到当前时间间是否有其他线程持有了写锁,如果是那么validate会返回0,否者就可以使用该stamp版本的锁对数据进行操作。由于tryOptimisticRead并没有使用CAS设置锁状态所以不需要显示的释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用与或操作进行检验,不涉及CAS操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
5 一些工具类
LockSupport
LockSupport 工具类是 JUC 的基础组件,主要作用是用来阻塞和唤醒线程,底层依赖于 Unsafe 类实现。
LockSupport 主要定义类 2 类方法:park 和 unpark,其中 park 方法用于阻塞当前线程,而 unpark(Thread) 方法用于唤醒处于阻塞状态的指定线程。
CountDownLatch
CountDownLatch是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集。
CountDownLatch使用给定的计数值(count)初始化。await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,在这之后(即,count为0之后)所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回。这是一个一次性现象 ———— count不会被重置。如果你需要一个重置count的版本,那么请考虑使用CyclicBarrier。
-
java.util.concurrent.CountDownLatch.Sync
CountDownLatch依赖AQS实现线程的阻塞与唤醒,其内部类Sync是AQL类的子类,实现了共享锁的获取与释放方法;
private static final class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 4982264981922014374L; // 此处用于指定等待线程的个数 Sync(int count) { setState(count); } int getCount() { return getState(); } // 获取共享锁逻辑,是CountDownLatch的await方法的核心代码 protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1; } // 将资源数减1 // 只有将资源数减为0时,才会释放所有阻塞线程,其他情况都不做特殊处理 protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); if (c == 0) return false; int nextc = c - 1; if (compareAndSetState(c, nextc)) return nextc == 0; } } }
-
java.util.concurrent.CountDownLatch#await()/ await(long timeout, TimeUnit unit)
await() 阻塞调用线程,除非达到以下两种情况:
- 通过调用countDown函数,将计数器值减少到0;
- 其他线程中断了此线程;
await(long timeout, TimeUnit unit)阻塞调用线程,满足以下三种条件后,会被唤醒:
- 通过调用countDown函数,将计数器值减少到0;
- 等待时间超过设定值,超时后自动返回,如果计数器为0返回true,否则返回false;
- 其他线程中断了此线程;
JDK11
-
await()
// 阻塞调用线程,除非达到以下两种情况 // 1. 通过调用countDown函数,将计数器值减少到0; // 2. 其他线程中断了此线程 public void await() throws InterruptedException { // 底层依赖的是AQS的可中断共享锁 sync.acquireSharedInterruptibly(1); } // AQS中获取可中断共享锁方法 public final void acquireSharedInterruptibly(int arg) throws InterruptedException { // 如果线程中断,抛出异常 if (Thread.interrupted()) throw new InterruptedException(); // 如果成功获取共享锁(计数器值已经被减少到0)直接返回,否则加入到阻塞队列 if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); } // 获取共享锁逻辑,是CountDownLatch的await方法的核心代码 protected int tryAcquireShared(int acquires) { // 如果计数器值为0,调用wait不会阻塞线程;否则将当前线程加入到阻塞队列 return (getState() == 0) ? 1 : -1; }
-
boolean await(long timeout, TimeUnit unit)
// 阻塞调用线程,满足以下三种条件后,会被唤醒 // 1. 通过调用countDown函数,将计数器值减少到0; // 2. 等待时间超过设定值,超时后自动返回,如果计数器为0返回true,否则返回false; // 3. 其他线程中断了此线程 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); } // 获取定时可中断共享锁 public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException { // 线程中断,抛出异常 if (Thread.interrupted()) throw new InterruptedException(); // 如果成功获取共享锁(计数器值已经被减少到0)直接返回true,否则加入到阻塞队列 return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout); } // 获取共享锁逻辑,是CountDownLatch的await方法的核心代码 protected int tryAcquireShared(int acquires) { // 如果计数器值为0,调用wait不会阻塞线程;否则将当前线程加入到阻塞队列 return (getState() == 0) ? 1 : -1; }
-
java.util.concurrent.CountDownLatch#countDown
减少计数器的值(调用一次该方法,计数器值减1),如果计数器值减为0,唤醒所有阻塞线程;
- 如果计数器的值大于0,计数器的值减1,如果新值等于零,唤醒所有阻塞线程;
- 如果计数器的值等于0,什么都不做;
IN JDK11 /** * 减少计数器的值(调用一次该方法,计数器值减1),如果计数器值减为0,唤醒所有阻塞线程 * 1. 如果计数器的值大于0,计数器的值减1,如果新值等于零,唤醒所有阻塞线程 * 2. 如果计数器的值等于0,什么都不做 */ public void countDown() { sync.releaseShared(1); } // AQS类中的释放共享锁方法 public final boolean releaseShared(int arg) { // 如果计数器值减1后,新值为0 if (tryReleaseShared(arg)) { doReleaseShared(); return true; } // 如果计数器值为0,或者新值不为0 return false; } // java.util.concurrent.CountDownLatch.Sync#tryReleaseShared protected boolean tryReleaseShared(int releases) { // Decrement count; signal when transition to zero for (;;) { int c = getState(); // 如果计数器为0,什么都不做 if (c == 0) return false; int nextc = c - 1; // 设置新值 if (compareAndSetState(c, nextc)) // 如果新值为0返回true return nextc == 0; } }
CyclicBarrier
一个同步辅助工具,允许一组线程在到达同一个状态前彼此等待;适用于一组固定数量的线程需要彼此等待的情况;之所以叫循环(cyclic)栅栏,是因为当一组线程释放后,可以重用此工具;
CyclicBarrier支持一个可选的Runnable命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作很有用。
CyclicBarrier通过ReetratLock保证多线程安全,所有阻塞线程有ReetranntLock中的等待队列(Condition)维护;
如果一个线程被中断,则所有的线程都将被唤醒
CyclicBarrier与CountDownLatch比较
1)CountDownLatch:一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行;CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
2)CountDownLatch:一次性的;CyclicBarrier:可以重复使用。
3)CountDownLatch基于AQS;CyclicBarrier基于锁和Condition。本质上都是依赖于volatile和CAS实现的。
-
await()
当前线程等待直到其他线程到达屏障状态或超长指定的等待时间;
如果当前线程不是最后一个到达屏障状态的线程,线程会阻塞直到以下几种情况发生:- 最后一个线程到达屏障状态;
- 超出指定的等待时间;
- 其他线程中断此线程;
- 所有等待线程中,有一个被中断(等待线程中任意一个被中断,都将唤醒其他等待线程)
- 任意其他线程等待超时;
- 任意线程重置屏障;
如果当前线程在调用此方法时被中断或在等待队列时被中断,会抛出InterruptedException异常
如果屏障被重置时(调用reset方法),有等待线程,线程组的其他线程会抛出BrokenBarrierException异常,并且屏障会被置为broken状态;
/** * 当前线程等待直到其他线程到达屏障状态或超长指定的等待时间 * 如果当前线程不是最后一个到达屏障状态的线程,线程会阻塞直到以下几种情况发生: * 1. 最后一个线程到达屏障状态; * 2. 超出指定的等待时间; * 3. 其他线程中断此线程; * 4. 所有等待线程中,有一个被中断(等待线程中任意一个被中断,都将唤醒其他等待线程) * 5. 任意其他线程等待超时; * 6. 任意线程重置屏障; * 如果当前线程在调用此方法时被中断或在等待队列时被中断,会抛出InterruptedException异常 * 如果屏障被重置时(调用reset方法)有等待线程,线程组的其他线程会抛出BrokenBarrierException异常,并且屏障会被置为broken状态 */ public int await() throws InterruptedException, BrokenBarrierException { try { // 加入到等待队列 return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } } private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException { final ReentrantLock lock = this.lock; // 加锁 lock.lock(); try { final Generation g = generation; // 如果当前屏障被其他线程置为异常,抛出异常 if (g.broken) throw new BrokenBarrierException(); // 如果线程被中断,将屏障状态置为broken,释放所有等待线程,抛出异常 if (Thread.interrupted()) { //将屏障状态置为broken,释放所有等待线程 breakBarrier(); throw new InterruptedException(); } // 计数器减1 int index = --count; // 如果当前线程是最后一个到达屏障状态的 if (index == 0) { // tripped boolean ranAction = false; try { // 获取CyclicBarrier创建时指定的执行命令并执行 final Runnable command = barrierCommand; if (command != null) command.run(); ranAction = true; // 释放所有等待线程,将计数器重置(创建时指定的线程组大小),重置屏障; nextGeneration(); return 0; } finally { // 如果没有成功执行命令,将屏障状态置为broken,释放所有等待线程 if (!ranAction) //将屏障状态置为broken,释放所有等待线程 breakBarrier(); } } // loop until tripped, broken, interrupted, or timed out for (;;) { try { // 如果没有设置等待超时,直接将当前线程加入到等待队列,线程会阻塞在此处直到被唤醒或被中断 if (!timed) trip.await(); // 否则,将线程加入到等待队列并指定超时时间,线程会阻塞在此处直到被唤醒或被中断 else if (nanos > 0L) // 返回剩余等待时间,如果不大于0,等待超时 nanos = trip.awaitNanos(nanos); // 如果在加入到队列过程中,当前线程被中断 } catch (InterruptedException ie) { // 如果当前屏障状态不为borken if (g == generation && ! g.broken) { //将屏障状态置为broken,释放所有等待线程 breakBarrier(); // 抛出中断异常 throw ie; // 如果屏障状态为broken,中断当前线程 } else { // We're about to finish waiting even if we had not // been interrupted, so this interrupt is deemed to // "belong" to subsequent execution. Thread.currentThread().interrupt(); } } // 如果屏障状态为broken(其他线程修改的),抛出BrokenBarrierException异常 if (g.broken) throw new BrokenBarrierException(); // 如果屏障被重置了,返回未到达屏障状态的线程数(此线程如果是被最后一个线程唤醒的,会返回记录的数据) if (g != generation) return index; // 如果等待超时0,抛出异常 if (timed && nanos <= 0L) { //将屏障状态置为broken,释放所有等待线程 breakBarrier(); throw new TimeoutException(); } } } finally { // 释放锁 lock.unlock(); } } private void breakBarrier() { // 将屏障状态置为broken(此值初始化时为false) generation.broken = true; // 将计数器置为初始值(CyclicBarrier创建时指定的线程组大小) count = parties; // 释放等待队列中所有的线程 trip.signalAll(); }
Semaphores
Semaphores(信号量),代表一组许可证,调用acquire方法,会获取一个许可证或阻塞直到获取一个许可证;调用release会归还一个许可证;Semaphores不代表实际资源,只代表可用资源数量;
Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,默认情况下使用非公平策略。当初始值为1时,可以用作互斥锁,并具备不可重入的加锁语义。Semaphore使用AQS的同步状态用保存当前可用许可的数量。
参考文章
- https://www.cnblogs.com/zaizhoumo/p/7787064.html
- https://www.cnblogs.com/mengchunchen/p/9890076.html