一、Lock接口

在Lock接口出现之前,Java中的应用程序对于多线程的并发安全处理只能基于synchronized关键字来解决。但是synchronized在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在Java5以后,Lock的出现可以解决synchronized在某些场景中的短板,它比synchronized更加灵活。

如上图,Lock接口定义了一些方法,需要实现类去实现。

Lock的实现类:

  • ReentrantLock(重入锁)
  • ReentrantReadWriteLock(重入读写锁)
  • StampedLock(jdk1.8,也是读写锁,在ReentrantReadWriteLock基础上进行改造)

二、ReentrantLock

ReentrantLock有三个内部类,分别是Sync,NonfairSync,FairSync,Sync继承于AbstractQueuedSynchronizer。

ReentrantLock 还提供了公平锁非公平锁的选择,通过构造方法接受一个可选的 fair 参数(默认非公平锁):当设置为 true 时,表示公平锁;否则为非公平锁。

公平锁与非公平锁的区别在于,公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock 整体结构如下图:

2.1 ReentrantLock的demo

public class AtomicDemo {private static int count=0;//重入锁(如何实现的?)static Lock lock=new ReentrantLock(true);public static void inc(){lock.lock(); //获得锁(互斥锁) ThreadA 获得了锁try {//退出线程 中断的过程往下传递.  true// sleep/ join/ wait//while()// ...Thread.sleep(1);count++;decr();} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();//释放锁 ThreadA释放锁  state=1-1=0}}
}

这两个线程如果没有嵌套,那么这时候,这两个方法是互斥的,因为他们是同一个锁对象。

2.2 读写锁的特性以及用法

读写锁特性:

  1. 读锁不能阻塞读锁

  2. 读锁需要阻塞写锁,直到所有读锁都释放

  3. 写锁需要阻塞读锁,直到所有写锁都释放

  4. 写锁需要阻塞写锁

demo:

public class Demo {static Map<String,Object> cacheMap=new HashMap<>();static ReentrantReadWriteLock rwl=new ReentrantReadWriteLock();static Lock read=rwl.readLock();static Lock write=rwl.writeLock();public static Object get(String key){read.lock(); //读锁 ThreadA 阻塞try{return cacheMap.get(key);}finally {read.unlock(); //释放读锁}}public static Object write(String key,Object value){write.lock(); //Other Thread 获得了写锁try{return cacheMap.put(key,value);}finally {write.unlock();}}}

2.3 重入锁

其含义是:如果已经拥有锁的线程再次获取锁时会立即响应成功。

demo:

上述inc()方法内部调用了decr()方法,而两个方法都有lock.lock()的操作,因为Reentrant是重入锁,所以这里是可以油inc()方法进入到decr()中并获取到锁的。

假设不允许重入,上述会出现死锁问题

一般来说,锁都是重入的,很少会见到不可重入的锁。

三、思索实现(设计思维)以及实现源码

{互斥}

  • 锁的互斥特性 -> 共享资源()-> 标记 (0 无锁, 1代表有锁)
  • 没有抢占到锁的线程?-> 释放CPU资源 , [等待 -> 唤醒]
  • 等待的线程怎么存储? -> 数据结构去存储一些列等待中的线程,FIFO (等待队列)
  • 公平和非公平(能否插队)
  • 重入的特性(识别是否是同一个人?ThreadID)

{技术方案}

  • volatile state =0 (无锁) , 1代表是持有锁 , >1代表重入
  • wait/notify | condition 需要唤醒指定线程。[LockSupport.park(); ->unpark(thread)] unsafe类中提供的一个方法
  • 双向链表
  • 逻辑层面去实现
  • 在某一个地方存储当前获得锁的线程的ID,判断下次抢占锁的线程是否为同一个。

3.1 AQS源码分析

首先看一下ReentrantLock的类图:

可以看到ReentrantLock是基于AbstactQueuedSynchronizer的。

3.1.1 AQS中的其他数据结构(ReentrantLock的语境下)

AbstractQueuedSynchronizer,简称AQS,为构建不同的同步组件(重入锁,读写锁,CountDownLatch等)提供了可扩展的基础框架,如下图所示。

这里用了什么设计模式?——模板方法设计模式

同步状态变量

/*** The synchronization state.*/
private volatile int state;

如第三章推测,这是一个带volatile前缀的int值,是一个类似计数器的东西。在不同的同步组件中有不同的含义。以ReentrantLock为例,state可以用来表示该锁被线程重入的次数。

  • 当state为0表示该锁不被任何线程持有;
  • 当state为1表示线程恰好持有该锁1次(未重入);
  • 当state大于1则表示锁被线程重入state次。

因为这是一个会被并发访问的量,为了防止出现可见性问题要用volatile进行修饰。

持有同步状态的线程标志

/*** The current owner of exclusive mode synchronization.*/
private transient Thread exclusiveOwnerThread;

如注释所言,这是在独占同步模式下标记持有同步状态线程的。ReentrantLock就是典型的独占同步模式,该变量用来标识锁被哪个线程持有。

waitStatus的枚举


这里对waitStatus 的一些状态有疑惑,我们来看下Node的属性,都是因为,没关系,有道词典翻译一下。

  • CANCELLED: 表示线程已被取消的waitStatus值,即这个node失效了,可能中断、超时等原因。
  • SIGNAL: 后续线程需要阻塞,所以当前node在释放锁时必须启动后续线程,所以这个代表线程启动的信号。
  • CONDITION: 指示线程正处于等待状态,这个node当前是属于条件锁。
  • PROPAGATE: 无条件传播,这个node是共享锁节点,他需要进行唤醒传播。

3.3 非公平锁加锁逻辑

demo:

public class AtomicDemo {private static int count=0;//重入锁(如何实现的?)static Lock lock=new ReentrantLock(false);//创建非公平锁public static void inc(){lock.lock(); //获得锁(互斥锁) ThreadA 获得了锁try {//退出线程 中断的过程往下传递.  true// sleep/ join/ wait//while()// ...Thread.sleep(1);count++;decr();} catch (InterruptedException e) {e.printStackTrace();}finally {lock.unlock();//释放锁 ThreadA释放锁  state=1-1=0}}public static void decr(){lock.lock(); //state=2   //ThreadA再次来抢占锁 : 不需要再次抢占锁,而是只增加重入的次数try{count--;}finally {lock.unlock(); //state=1}}public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->AtomicDemo.inc());t1.start();Thread t2=new Thread(()->AtomicDemo.inc());t2.start();t2.interrupt(); //中断线程。for (int i = 0; i < 1000; i++) {new Thread(()->AtomicDemo.inc()).start();}Thread.sleep(4000);System.out.println("result:"+count);}
}

了解AQS的主要结构后,就可以开始进行ReentrantLock的源码解读了。由于非公平锁在实际开发中用的比较多,故以讲解非公平锁的源码为主。以下面这段对非公平锁使用的代码为例:

1.加锁流程从lock.lock()开始

public void lock() {sync.lock();
}

进入该源码,正确找到sycn的实现类——NonfairSync 后可以看到真正有内容的入口方法

2.加锁流程真正意义上的入口

/*** Performs lock.  Try immediate barge, backing up to normal* acquire on failure.*/
//加锁流程真正意义上的入口
final void lock() {//以cas方式尝试将AQS中的state从0更新为1//乐观锁( true / false) | 只有一个线程能够进入.if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());//获取锁成功则将当前线程标记为持有锁的线程,然后直接返回elseacquire(1);//获取锁失败则执行该方法
}

进入到compareA有序列表dnSetState方法:

stateOffset——偏移量,内部c++代码通过偏移量拿到地址,直接对比。

首先尝试快速获取锁,以cas的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在ReentrantLock的语境下等同于锁被线程重入的次数,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。若获取锁失败,则执行acquire方法——抢占锁的方法。

public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

acquere()方法有三个方法,下面我们逐步分析这三个方法:

  • ! tryAcquire(arg)
  • addWaiter 将未获得锁的线程加入到队列
  • acquireQueued(); 去抢占锁或者阻塞

尝试获取锁的通用方法:tryAcquire()

tryAcquire是AQS中定义的钩子方法,如下所示

protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
}

该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现,非公平模式的实现如下:

protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}

底层调用了nonfairTryAcquire()
从方法名上我们就可以知道这是非公平模式下尝试获取锁的方法,具体方法实现如下:d'f

/*** Performs non-fair tryLock.  tryAcquire is implemented in* subclasses, but both need nonfair try for trylock method.*/
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();//获取当前线程实例int c = getState();//获取state变量的值,即当前锁被重入的次数if (c == 0) {   //state为0,说明当前锁未被任何线程持有if (compareAndSetState(0, acquires)) { //以cas方式获取锁setExclusiveOwnerThread(current);  //将当前线程标记为持有锁的线程return true;//获取锁成功,非重入}}else if (current == getExclusiveOwnerThread()) { //当前线程就是持有锁的线程,说明该锁被重入了int nextc = c + acquires;//计算state变量要更新的值if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);//非同步方式更新state值return true;  //获取锁成功,重入}return false;     //走到这里说明尝试获取锁失败
}

这是非公平模式下获取锁的通用方法。它囊括了当前线程在尝试获取锁时的所有可能情况:

  1. 当state=0,通过cas方式获取,并将当前线程设置到exclusiveOwnerThread(独占线程属性)中。若cas失败,说明在得到state=0和cas获取锁之间有其他线程已经获取了锁,返回失败结果。
  2. state>0,exclusiveOwnerThread为当前线程,当前是该锁被重入了,则将锁的重入次数加1(state+1),然后返回成功结果。因为该线程之前已经获得了锁,所以这个累加操作不用同步。
  3. 若当前锁已经被其他线程持有(state>0,exclusiveOwnerThread不为当前线程),则直接返回失败结果

因为我们用state来统计锁被线程重入的次数,所以当前线程尝试获取锁的操作是否成功可以简化为:state值是否成功累加1,是则尝试获取锁成功,否则尝试获取锁失败。

退回到上层的acquire方法:

public final void acquire(int arg) {if (!tryAcquire(arg) &&  //当前线程尝试获取锁,若获取成功返回true,否则falseacquireQueued(addWaiter(Node.EXCLUSIVE), arg))  //只有当前线程获取锁失败才会执行者这部分代码selfInterrupt();
}

tryAcquire(arg)返回成功,则说明当前线程成功获取了锁(第一次获取或者重入),由取反和&&可知,整个流程到这结束,只有当前线程获取锁失败才会执行后面的判断。

先来看看addWaiter(Node.EXCLUSIVE)部分,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。

获取锁失败的线程如何安全的加入同步队列:addWaiter()

这部分逻辑在addWaiter()方法中:

private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);//首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);//入队的逻辑这里都有return node;
}

首先创建了一个新节点,并将当前线程实例封装在其内部,之后我们直接看enq(node)方法就可以了,中间这部分逻辑在enq(node)中都有,之所以加上这部分“重复代码”和尝试获取锁时的“重复代码”一样,对某些特殊情况进行提前处理,牺牲一定的代码可读性换取性能提升。

/*** Inserts node into queue, initializing if necessary. See picture above.* @param node the node to insert* @return node's predecessor*/
private Node enq(final Node node) {for (;;) {Node t = tail;//t指向当前队列的最后一个节点,队列为空则为nullif (t == null) { // Must initialize  //队列为空if (compareAndSetHead(new Node())) //构造新结点,CAS方式设置为队列首元素,当head==null时更新成功tail = head;//尾指针指向首结点} else {  //队列不为空node.prev = t;if (compareAndSetTail(t, node)) { //CAS将尾指针指向当前结点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功t.next = node;    //原尾结点的next指针指向当前结点return t;}}}
}

这里有两个CAS操作:

  • compareAndSetHead(new Node()),CAS方式更新head指针,仅当原值为null时更新成功
/*** CAS head field. Used only by enq.*/
private final boolean compareAndSetHead(Node update) {return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
  • compareAndSetTail(t, node),CAS方式更新tial指针,仅当原值为t时更新成功
/*** CAS tail field. Used only by enq.*/
private final boolean compareAndSetTail(Node expect, Node update) {return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

外层的for循环保证了所有获取锁失败的线程经过失败重试后最后都能加入同步队列。

从代码可以看出来,该node节点插入的方法用的是尾插法,即后加入的node节点在链表的末尾。

整个入队的过程并不复杂,是典型的CAS加失败重试的乐观锁策略。其中只有更新头指针和更新尾指针这两步进行了CAS同步,可以预见高并发场景下性能是非常好的。但是本着质疑精神我们不禁会思考下这么做真的线程安全吗?

  • 1.队列为空的情况:
    因为队列为空,故head=tail=null,假设线程执行2成功,则在其执行3之前,因为tail=null,其他进入该方法的线程因为head不为null将在2处不停的失败,所以3即使没有同步也不会有线程安全问题。
  • 2.队列不为空的情况:
    假设线程执行5成功,则此时4的操作必然也是正确的(当前结点的prev指针确实指向了队列尾结点,换句话说tail指针没有改变,如若不然5必然执行失败),又因为4执行成功,当前节点在队列中的次序已经确定了,所以6何时执行对线程安全不会有任何影响,比如下面这种情况

 为了确保真的理解了它,可以思考这个问题:把enq方法图中的4放到5之后,整个入队的过程还线程安全吗?

到这为止,获取锁失败的线程加入同步队列的逻辑就结束了。但是线程加入同步队列后会做什么我们并不清楚,这部分在acquireQueued方法中

线程加入同步队列后会做什么:acquireQueued()

先看acquireQueued方法的源码:

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;//死循环,正常情况下线程只有获得锁才能跳出循环for (;;) {final Node p = node.predecessor();//获得当前线程所在结点的前驱结点//第一个if分句if (p == head && tryAcquire(arg)) { setHead(node); //将当前结点设置为队列头结点p.next = null; // help GCfailed = false;return interrupted;//正常情况下死循环唯一的出口}//第二个if分句if (shouldParkAfterFailedAcquire(p, node) &&  //判断是否要阻塞当前线程parkAndCheckInterrupt())      //阻塞当前线程interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

这段代码主要的内容都在for循环中,这是一个死循环,主要有两个if分句构成:

  • 第一个if分句中,当前线程首先会判断前驱结点是否是头结点,如果是则尝试获取锁,获取锁成功则会设置当前结点为头结点(更新头指针)。
  • 第二个分局,判断是否要阻塞当前线程

第一个if分句中为什么必须前驱结点为头结点才尝试去获取锁?

——因为头结点表示当前正占有锁的线程,正常情况下该线程释放锁后会通知后面结点中阻塞的线程,阻塞线程被唤醒后去获取锁,这是我们希望看到的。然而还有一种情况,就是前驱结点取消了等待,此时当前线程也会被唤醒,这时候就不应该去获取锁,而是往前回溯一直找到一个没有取消等待的结点,然后将自身连接在它后面。一旦我们成功获取了锁并成功将自身设置为头结点,就会跳出for循环。否则就会执行第二个if分句确保前驱结点的状态为SIGNAL,然后阻塞当前线程。

为什么调用parkAndCheckInterrupt()方法和selfInterrupt()方法,不是很理解。下面是解释,但不是解答。

parkAndCheckInterrupt()方法会检查是否有中断标识,有的话并且线程是阻塞的,会抛出InterruptedException()异常。

selfInterrupt()方法比较简单,就是调用Thread.currentThread().interrupt();将当前线程挂起。

先来看shouldParkAfterFailedAcquire(p, node),从方法名上我们可以大概猜出这是判断是否要阻塞当前线程的,方法内容如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL) //状态为SIGNAL/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) { //状态为CANCELLED,/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else { //状态为初始化状态(ReentrentLock语境下)/** 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;
}

以看到针对前驱结点(前一个节点)pred的状态会进行不同的处理

  • 1.pred状态为SIGNAL,则返回true,表示要阻塞当前线程。
  • 2.pred状态为CANCELLED,则一直往队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面——总结起来就一句话,清除状态为CANCELLED的节点。
  • 3.pred的状态为初始化状态,此时通过compareAndSetWaitStatus(pred, ws, Node.SIGNAL)方法将pred的状态改为SIGNAL。

其实这个方法的含义很简单,就是确保当前结点的前驱结点的状态为SIGNAL,SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程。毕竟,只有确保能够被唤醒,当前线程才能放心的阻塞。

但是要注意只有在前驱结点已经是SIGNAL状态后才会执行后面的方法立即阻塞,对应上面的第一种情况。其他两种情况则因为返回false而重新执行一遍。

for循环。这种延迟阻塞其实也是一种高并发场景下的优化,试想我如果在重新执行循环的时候成功获取了锁,是不是线程阻塞唤醒的开销就省了呢?

阻塞线程的方法parkAndCheckInterrupt

shouldParkAfterFailedAcquire返回true表示应该阻塞当前线程,则会执行parkAndCheckInterrupt方法,这个方法比较简单,底层调用了LockSupport来阻塞当前线程,源码如下:

private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}

该方法内部通过调用LockSupport的park方法来阻塞当前线程,不清楚LockSupport的可以看看这里。LockSupport功能简介及原理浅析

下面通过一张流程图来说明线程从加入同步队列到成功获取锁的过程

概括的说,线程在同步队列中会尝试获取锁,失败则被阻塞,被唤醒后会不停的重复这个过程,直到线程真正持有了锁,并将自身结点置于队列头部。

3.1.3 加锁流程源码总结

我们可以用一张流程图来说明lock()方法的流程:

网上也有另一张图:

ReentrantLock非公平模式下的加锁流程,贴上以供参考:
3.3 非公平锁释放锁逻辑

3.3.1 解锁流程源码解读

解锁的源码相对简单,源码如下:

public void unlock() {sync.release(1);}
public final boolean release(int arg) {if (tryRelease(arg)) {释放锁(state-1),若释放后锁可被其他线程获取(state=0),返回trueNode h = head;//当前队列不为空且头结点状态不为初始化状态(0)   if (h != null && h.waitStatus != 0)unparkSuccessor(h);//唤醒同步队列中被阻塞的线程return true;}return false;}

正确找到sync的实现类,找到真正的入口方法,主要内容都在一个if语句中,先看下判断条件tryRelease方法:

protected final boolean tryRelease(int releases) {int c = getState() - releases; //计算待更新的state值if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) { //待更新的state值为0,说明持有锁的线程未重入,一旦释放锁其他线程将能获取free = true; setExclusiveOwnerThread(null);//清除锁的持有线程标记}setState(c);//更新state值return free;
}

tryRelease其实只是将线程持有锁的次数减1,即将state值减1,若减少后线程将完全释放锁(state值为0),则该方法将返回true,否则返回false。由于执行该方法的线程必然持有锁,故该方法不需要任何同步操作。
若当前线程已经完全释放锁,即锁可被其他线程使用,则还应该唤醒后续等待线程。不过在此之前需要进行两个条件的判断:

  • h!=null是为了防止队列为空,即没有任何线程处于等待队列中,那么也就不需要进行唤醒的操作
  • h.waitStatus != 0是为了防止队列中虽有线程,但该线程还未阻塞,由前面的分析知,线程在阻塞自己前必须设置前驱结点的状态为SIGNAL,否则它不会阻塞自己。

接下来就是唤醒线程的操作,unparkSuccessor(h)源码如下:

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)compareAndSetWaitStatus(node, 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 t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)LockSupport.unpark(s.thread);}

一般情况下只要唤醒后继结点的线程就行了,但是后继结点可能已经取消等待,所以从队列尾部往前回溯,找到离头结点最近的正常结点,并唤醒其线程。

3.3.2 释放锁流程图、总结

我的疑问:

有个疑问,release中可以看到,尝试释放锁成功了就返回true,失败了返回false。

——失败了后就直接返回了吗?那是不是释放锁失败了,别的线程就一直获取不到锁呢?

四、公平锁和非公平锁的区别

ReentrantLock中有两个内部类,公平锁的实现类是FairSync,非公平锁的实现类是NonfairSync,这两个类只有在加锁流程中有区别,以下是二者的源码和区别。公平锁在线程获取锁的时候,会先判断一下等待队列中是否有线程在排队。而非公平锁则不会判断,而是直接试图去获取锁。根据前面的解锁流程可知,此时有可能正好有线程释放锁,并唤醒等待队列中的第一个线程,因此在非公平锁中,当有线程释放锁时,新加入的线程和等待队列的第一个线程都会去抢锁,如果新加入线程抢锁成功,则不会被挂起,减少了状态切换等带来性能开销,因此非公平锁的整体性能更优

五、总结与疑问

5.1.非公平性体现在哪呢?

——不管队列中有没有排队,新来的线程都不管的,且和队列里的线程竞争。

5.2.疑问,如下图代码:

release中可以看到,尝试释放锁成功了就返回true,失败了返回false。失败了后就直接返回了吗?那是不是释放锁失败了,别的线程就一直获取不到锁呢?

——我只能根据我的理解回答,若是不正确,后面再修改。
根据我的理解,只会有三种情况:

  1. 当前线程不是占据锁的线程,抛出异常
  2. 当state-1==0,表示锁不是重入,此时辉煌true
  3. 当state-1>0,表示锁是重入,此时只是这把锁释放了,但线程依然持有锁,所以返回false,后续依然要显示的调用lock.unlock()。

六、网友的解释

6.1 附上知乎网友对parkAndCheckInterrupt()的解释

在加锁流程中,我们知道如果线程自旋获取锁的过程中满足了阻塞条件,将执行以下代码。因此线程被唤醒后,就是继续执行return Thread.interrupted(); ,这个函数返回的是当前执行线程的中断状态,并清除。

咱们重新回到acquireQueued代码,当parkAndCheckInterrupt返回时,线程继续执行获取锁的for循环,如果这个时候获取锁成功,就会把当前interrupted返回

如果acquireQueued为True,就会执行selfInterrupt方法。这个方法就是中断当前线程。但为什么获取了锁以后还要中断线程呢?简单理解就是在获取锁的过程中,线程因为获取锁失败,进入了阻塞状态,在此期间,线程发生了中断,但是此时线程被阻塞,因此只是把中断标志记录下来了,只有当自己活得CPU执行权时才能执行中断。

6.2 网上扒来的流程图

第六章参考文章 :https://zhuanlan.zhihu.com/p/109799609

理解J.U.C中的ReentrantLock相关推荐

  1. java lock unlock_详解Java中的ReentrantLock锁

    ReentrantLock锁 ReentrantLock是Java中常用的锁,属于乐观锁类型,多线程并发情况下.能保证共享数据安全性,线程间有序性 ReentrantLock通过原子操作和阻塞实现锁原 ...

  2. AQS理解之五—并发编程中AQS的理解

    AQS理解之五-并发编程中AQS的理解 首先看下uml类图: AbstractOwnableSynchronizer 这个类定义是提供一个创建锁的基础,设置一个排它线程,帮助控制和监控访问. 先看下A ...

  3. java阻塞队列作用_简单理解阻塞队列(BlockingQueue)中的take/put方法以及Condition存在的作用...

    简单理解阻塞队列(BlockingQueue)中的take/put方法以及Condition存在的作用 Condition:可以理解成一把锁的一个钥匙,它既可以解锁(通知放行),又可以加锁(阻塞) n ...

  4. 《Understanding the Effective Receptive Field in Deep Convolutional Neural Networks》理解深卷积神经网络中的有效感受野

    前言 论文推荐来自B站UP-启释科技,该up对感受野和卷积有深刻的理解 推荐感受野视频讲解: 深度理解感受野(一) 深度理解感受野(二) 深度理解感受野(三) 深度理解感受野(四) 深度理解感受野(五 ...

  5. 快速理解j=j++ 和 j=++j(新手入门)

    快速理解j = j++ 和 j = ++ j(新手入门): j = j++ ,代码如下: public class TestDemo {/*** @param args*/public static ...

  6. 理解YOLOv2训练过程中输出参数含义

    转载自https://blog.csdn.net/dcrmg/article/details/78565440 原英文地址: https://timebutt.github.io/static/und ...

  7. 理解与使用Javascript中的回调函数

    在Javascript中,函数是第一类对象,这意味着函数可以像对象一样按照第一类管理被使用.既然函数实际上是对象:它们能被"存储"在变量中,能作为函数参数被传递,能在函数中被创建, ...

  8. 深入理解领域驱动设计中的聚合

    简介:聚合模式是 DDD 的模式结构中较为难于理解的一个,也是 DDD 学习曲线中的一个关键障碍.合理地设计聚合,能清晰地表述业务一致性,也更容易带来清晰的实现,设计不合理的聚合,甚至在设计中没有聚合 ...

  9. 【JavaScript】理解与使用Javascript中的回调函数

    在Javascript中,函数是第一类对象,这意味着函数可以像对象一样按照第一类管理被使用.既然函数实际上是对象:它们能被"存储"在变量中,能作为函数参数被传递,能在函数中被创建, ...

  10. J .U.C 中的原子操作类

    由于变量类型的关系,在J.U.C中提供了12个原子操作的类.这12个类可以分为四大类 1. 原子更新基本类型 AtomicBoolean.AtomicInteger.AtomicLong 2. 原子更 ...

最新文章

  1. RedHat使用163源
  2. BZOJ4590 [Shoi2015]自动刷题机
  3. 【python图像处理】】python绘制散点图
  4. nowcoder20C 位数差
  5. 计算机英语句子缩略,根据汉语意思完成英语句子,每空一词(含缩略形式)。 【1】-咋考网...
  6. 法线有接缝_发送带有接缝的活动邀请
  7. 不叫K50 Pro+!Redmi K50系列超大杯或有新名称
  8. 【SSM 1】SpringMVC、Spring和Struts的区别
  9. 转 Linux查看文件编码格式及文件编码转换
  10. UCHOME ajaxmenU()用法
  11. 方舟编译器编译linux,方舟编译器环境配置
  12. python创意动态图片_Python趣味创意编程
  13. DirectX、Directshow介绍
  14. GDevelop 5教程之 如何在不编程的情况下轻松制作手机游戏
  15. Java对接苹果账号授权登录
  16. 关系网络lbs的应用_冒泡网王江:熟人关系将成LBS最重要商业模式
  17. 来自作业本的写给90后
  18. 米开朗基罗在他的画里,可是暗藏了不少'男男kiss'的镜头,要不要来了解下?
  19. 举个栗子~Tableau 技巧(215):简化实现雷达图(Radar Chart)
  20. Share 很喜欢的派大星图片

热门文章

  1. python的objectproperty,python – ObjectProperty类的用法
  2. 极客大学产品经理训练营:数据分析 第八章作业
  3. CDN 的诞生、术语、原理、特征以及应用场景
  4. mysql db for python_Python使用MySQLdb for Python操作数据库教程
  5. 522. 最长特殊序列 II
  6. 反射--获取构造器,获取父类,获取带泛型的父类,获取实现的接口,获取所在的包,获取注解
  7. matlab设计单神经元系统框图,单神经元自适应系统
  8. java算术表达式求值-中缀表达式转后缀表达式
  9. 现代通信原理7.1:模拟角度调制的基本概念
  10. linux常见命令用法之(二)