网站点击率,雏鸟app网站推广,网站模板 手机app展示,投广告哪个平台好大家好#xff0c;我是呼噜噜#xff0c;我们之前聊过Java中以互斥同步的方式保证线程安全#xff1a;Sychronized#xff0c;这次我们来再聊聊另一种互斥同步的方式Lock#xff0c;本文会介绍ReentrantLock及其它的基石AQS的源码解析#xff0c;一个非常重要的同步框架 …大家好我是呼噜噜我们之前聊过Java中以互斥同步的方式保证线程安全Sychronized这次我们来再聊聊另一种互斥同步的方式Lock本文会介绍ReentrantLock及其它的基石AQS的源码解析一个非常重要的同步框架
Lock接口
与Sychronized利用JVM指令级别的monitor锁来实现线程安全详情可见Synchronized关键字详解
不同的是Lock接口实现线程安全则是代码级别实现的,Lock接口是 Java并发编程中很重要的一个接口当程序发生异常时Sychronized可以自动释放锁但Lock必须需要手动解锁。与 Lock 关联密切的锁有 ReetrantLock 和 ReadWriteLock。我们以ReentrantLock切入来看看其底层涉及到的原理。 作者小牛呼噜噜 初识ReentrantLock
ReentrantLock也叫重入锁我们首先得了解ReentrantLock的一般使用方法
Lock lock new ReentrantLock(false);
lock.lock();
try{//临界区执行一些具体操作。。
}finally{lock.unlock();
}代码层次必须要手动解锁。
公平锁和非公平锁
查看ReentrantLock的源码可以发现
public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync();}当参数fair为true表示公平锁创建的是FairSync类;false为非公平锁创建的是NonfairSync类。如果该参数不填则默认是非公平锁调用另一个构造方法。
那什么是公平锁和非公平锁 公平锁每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的最前面的线程总是最先获取到锁,遵循先来先得的规则。非公平锁每个线程获取锁的顺序是随机的并不会遵循先来先得的规则所有线程会竞争获取锁 我们接下来举个例子来看看首先创建公平锁开启6个线程执行分别加锁和释放锁并打印线程名的操作
public class FairReentrantLockTest {static Lock lock new ReentrantLock(true);public static void main(String[] args) throws InterruptedException {for (int i 0; i 6; i) {new Thread(() - {lock.lock();System.out.println(临界区的当前线程名称 Thread.currentThread().getName());lock.unlock();}).start();}}
}结果
临界区的当前线程名称Thread-0
临界区的当前线程名称Thread-1
临界区的当前线程名称Thread-2
临界区的当前线程名称Thread-3
临界区的当前线程名称Thread-4
临界区的当前线程名称Thread-5如果我们把公平锁换成非公平锁的话,static Lock lock new ReentrantLock(false)再执行一遍结果为
临界区的当前线程名称Thread-0
临界区的当前线程名称Thread-5
临界区的当前线程名称Thread-1
临界区的当前线程名称Thread-2
临界区的当前线程名称Thread-3
临界区的当前线程名称Thread-4我们可以发现当使用公平锁线程获取锁的话线程进入等待队列的队尾得排队依次获取锁先到先得。如果使用的是非公平锁那就直接尝试竞争锁竞争得到就获得锁获取锁的顺序是随机的。
公平锁和非公平锁的优缺点
公平锁其优点所有的线程都能得到资源不会饿死在队列中缺点吞吐量会下降很多队列里面除了第一个线程其他的线程都会阻塞线程 每次从阻塞恢复到运行状态 都需要从用户态转换成内核态而这个状态的转换是比较慢的因此公平锁的执行速度会比较慢而且CPU唤醒阻塞线程的开销会很大。非公平锁,其优点不遵守先到先得的原则CPU不必取唤醒所有线程会减少唤起线程的数量可以减少CPU唤醒线程的开销整体的吞吐效率会高点。缺点但这样也可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁导致饿死。
我们这里贴一下公平锁和非公平锁的性能测试结果图来源于《Java并发编程实战》
从上述结果可以看出使用非公平锁的性能(吞吐率)普遍比公平锁高很多。
可重入锁与非可重入锁
ReentrantLock顾名思义叫重入锁是指当同一个线程在获取外层同步方法锁的时候再进入该线程的内层同步方法会自动获取锁其实就是递归调用前提锁对象得是同一个对象或者class对象并不会因为之前已经获取过还没释放而阻塞这样就可以有效避免死锁的产生这个叫可重入锁。Synchronized与本文的ReentrantLock都属于可重入锁
下面我们用几个例子来看看
基于Synchronized实现可重入锁
public class RLockTestBySynchronized {public static void main(String[] args) {new Thread(new Runnable() {Overridepublic void run() {synchronized (RLockTestBySynchronized.class) {System.out.println(第1次获取锁);int index 1;while (true) {synchronized (RLockTestBySynchronized.class) {System.out.println(第 (index) 次获取该锁);}if (index 6) {break;}}}}}).start();}}结果
第1次获取锁
第2次获取该锁
第3次获取该锁
第4次获取该锁
第5次获取该锁
第6次获取该锁基于ReentrantLock实现可重入锁
public class RLockTestByReentrantLock2 {public static void main(String[] args) {ReentrantLock lock new ReentrantLock();new Thread(new Runnable() {Overridepublic void run() {try {lock.lock();System.out.println(第1次获取锁);int index 1;while (true) {try {lock.lock();System.out.println(第 (index) 次获取该锁);try {Thread.sleep(new Random().nextInt(200));} catch (InterruptedException e) {e.printStackTrace();}if (index 6) {break;}} finally {lock.unlock();}}} finally {lock.unlock();}}}).start();}结果
第1次获取锁
第2次获取该锁
第3次获取该锁
第4次获取该锁
第5次获取该锁
第6次获取该锁需要注意的是ReentrantLock的时候一定要手动释放锁并且加锁次数和释放次数要一样不然还是会导致死锁
基于wait/notify 实现不可重入锁
与其经常拿来比较的是不可重入锁与可重入锁相反的是一个线程在获取到外层同步方法锁后再进入该方法的内层同步方法无法获取到锁即使锁是同一个对象这样容易死锁。
public class NoRLockTest {private static boolean isLock false;public synchronized void lock() throws InterruptedException {Thread thread Thread.currentThread();// 判断是否加锁while (isLock){wait();//阻塞}isLock true;System.out.println(thread.getName() 获得了锁);}public synchronized void unLock(){isLock false;notify();//唤醒}public static void main(String[] args) throws Exception {NoRLockTest tt new NoRLockTest();tt.lock();//第1次获取锁tt.lock();//第2次获取锁}}结果
main 获得了锁显然当程序第2次获取锁时由于锁已被占有就发生了死锁
通过上面的例子我们可以发现ReetrantLock锁在使用上还是比较简单的但内部的原理可一点都不简单我们接下来着重解读一下ReetrantLock的内部实现原理
ReentrantLock源码解析
当我们去阅读ReentrantLock的源码时发现有以下3个类 其中 ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类NonfairSync与FairSync类继承自Sync类Sync类继承自AbstractQueuedSynchronizer抽象类当然ReentrantLock类本身继承Lock接口
Lock
lock接口以下定义了并发中常用的5个方法
public interface Lock {// 获取锁void lock();// 获取锁可中断即在拿锁过程中可以中断interrupt不同的是synchronized是不可中断锁。void lockInterruptibly() throws InterruptedException;// 尝试获取锁锁在空闲的才能获取锁未获得锁不会等待boolean tryLock();// 在给定时间内尝试获取锁成功返回true失败返回falseboolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 释放锁void unlock();// 等待与唤醒机制Condition newCondition();
}ReentrantLock继承了lock接口这几个方法也会在ReentrantLock内部进行重写
Sync
abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID -5179523762034025860L;// 获取锁abstract void lock();// 获取非公平锁final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current Thread.currentThread();// 获取状态int c getState();if (c 0) { // 表示没有线程正在竞争该锁//通过CAS尝试拿到锁状态0表示锁没有被占用 !!!if (compareAndSetState(0, acquires)) {// 设置当前线程独占也就是设置当前线程排他锁setExclusiveOwnerThread(current); return true; // 成功}}//如果是已上锁状态就进一步判断当前线程拥有该锁(即可重入锁)else if (current getExclusiveOwnerThread()) { int nextc c acquires; // 增加重入次数if (nextc 0) // overflowthrow new Error(Maximum lock count exceeded);// 设置状态setState(nextc); // 成功return true; }// 失败return false;}// 尝试释放资源protected final boolean tryRelease(int releases) {int c getState() - releases;if (Thread.currentThread() ! getExclusiveOwnerThread()) // 当前线程不为独占线程throw new IllegalMonitorStateException(); // 释放标识boolean free false; if (c 0) {free true;// 已经释放清空独占setExclusiveOwnerThread(null); }// 设置标识setState(c); return free; }// 判断资源是否被当前线程占有protected final boolean isHeldExclusively() {// While we must in general read state before owner,// we dont need to do so to check if current thread is ownerreturn getExclusiveOwnerThread() Thread.currentThread();}// 新生一个条件final ConditionObject newCondition() {return new ConditionObject();}// 获取持有锁的线程final Thread getOwner() { return getState() 0 ? null : getExclusiveOwnerThread();}// 返回状态final int getHoldCount() { return isHeldExclusively() ? getState() : 0;}// 是否上锁状态final boolean isLocked() { return getState() ! 0;}/*** Reconstitutes the instance from a stream (that is, deserializes it).*/// 自定义反序列化逻辑private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException {s.defaultReadObject();setState(0); // reset to unlocked state}
} Sync是一个抽象类那必然有继承它的类。在ReentrantLock中有两个Sync的实现分别为非公平锁NonfairSync与公平锁FairSync
NonfairSync 与 FairSync
static final class NonfairSync extends Sync {private static final long serialVersionUID 7316153563782823691L;/*** Performs lock. Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {// 使用CAS加锁(如果state等于0则设置为1返回true否则返回false)if (compareAndSetState(0, 1))// 加锁成功则设置独占线程为当前线程setExclusiveOwnerThread(Thread.currentThread());else// 加锁失败则调用AbstractQueuedSynchronizer类的acquire方法acquire(1);}protected final boolean tryAcquire(int acquires) {// 调用父类Sync的nonfairTryAcquire方法return nonfairTryAcquire(acquires);}}我们可以看出在非公平锁里如果当前线程锁占用状态为0的话会直接进行CAS尝试获取锁不需要加入队列然后等待队列头线程唤醒再获取锁这一步骤所以效率相比于公平锁会较快。ReentrantLock默认是非公平锁
我们接着看FairSync的源码
static final class FairSync extends Sync {private static final long serialVersionUID -3000897897090466540L;final void lock() {acquire(1);}/*** Fair version of tryAcquire. Dont grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState();if (c 0) {//未上锁状态//先判断没有等待节点时才会开启CAS去拿锁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;}}我们可以看出在公平锁里如果当前线程锁占用状态为0的话会先去同步队列中是否有在等待的线程如果没有才会去进行拿锁操作这样就遵循FIFO的原则先到先得。所以效率相较于非公平锁较慢
在ReentrantLock内部无论NonfairSync、FairSync、Sync类其实归根结底都继承AbstractQueuedSynchronizer这也是非常重要的部分我们下面一起重点来看看
AQS
AbstractQueuedSynchronizer一般简称AQS也叫抽象队列同步器AbstractQueuedSynchronizer是Java并发工具包JUC的基石它是一个同步框架为Java的各种同步器锁等提供了并发抽象是由大名鼎鼎的Doug Lea完成。
CLH队列
我们先来看下它的源码一上来就是CLH队列定义
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {...protected AbstractQueuedSynchronizer() { } static final class Node {...// 记录状态用volatile int waitStatus; // 标识哪个线程volatile Thread thread; // 前驱节点volatile Node prev; // 后继节点volatile Node next;...}// 同步队列的头结点private transient volatile Node head;// 同步队列的尾结点private transient volatile Node tail;// 同步状态private volatile int state;//判断是否为共享模式final boolean isShared() {return nextWaiter SHARED;}...}
CLH队列也叫AQS同步队列模型该队列的设计是构建AQS的关键多个线程对共享资源的竞争以及线程阻塞等待以及被唤醒时锁分配的机制都是基于AQS同步队列
Doug Lea参考了CLH的设计 保留了基本的设计由前驱节点做阻塞与唤醒的控制但是在队列的选择上做出了改变AQS选择双向链表来实现虚拟的双向队列节点中添加了prev和next指针添加prev指针主要是为了实现取消功能而next指针的加入可以方便的实现唤醒后继节点
CLH 锁其实也是对自旋锁的一种改进当多进程竞争资源时无法直接获取不到锁的线程会进入该队列。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点Node来实现锁的分配同时还依赖state来控制同步状态当state0时则说明共享资源未被上锁当state1时则说明该共享资源被上锁了其他线程必须加入同步队列进行等待
AQS同步队列模型
waitStatus
其中waitStatus是表示当前被封装成Node结点的状态默认为0表示初始化状态还有4种状态CANCELLED、SIGNAL、CONDITION、PROPAGATE分别是
CANCELLED 1 表示该节点的线程被取消当同步队列中的线程超时或中断会将此节点取消。该节点永远不会再发生变化需要注意的是当前节点的线程为取消状态时再也不会被阻塞SIGNAL-1 当其prev结点释放了同步锁 或者 被取消后立即通知处于SIGNAL状态的next节点的线程执行CONDITION-2表示节点处于条件队列等待调用了await方法后处于等待状态的线程节点会被标记为此种状态当调用了Condition的singal()方法后CONDITION状态会变为SIGNAL状态并且会在适当的时机从等待队列转移到同步队列中。PROPAGATE:-3这种状态与共享模式有关在共享模式下表示节点处于可运行状态
独占模式和共享模式
AQS作为并发包基石定义两种资源共享方式独占模式和共享模式
共享模式即共享锁锁在同一时刻可以被多个线程共享使用一个线程对资源加了共享锁后其它线程对资源也只能加共享锁。共享锁有着很好的读性能。ReentrantReadWriteLock的读锁就是一种共享锁的实现。在AQS中常量SHARED表示共享模式独占模式即排他锁锁在同一时刻只能有一个线程使用同一时刻不能被多个线程一同占用一个线程占用后其它线程只能等待。ReentrantLock、Synchronized、ReentrantReadWriteLock的写锁等都是排他锁的实现。在AQS中常量EXCLUSIVE表示独占模式无论是共享模式还是独占模式的实现类比如ReentrantLock其内部都是基于AQS实现的也都维持着一个同步队列当请求锁的线程超过现有模式的限制时会将线程包装成Node结点并将线程当前必要的信息存储到node结点中然后加入同步队列等会获取锁而这系列操作都间接调用AQS完成的
volatile关键字
在阅读完AQS的源码后我们可以发现里面充斥着大量volatile关键字
...// 同步队列的头结点
private transient volatile Node head;
// 同步队列的尾结点
private transient volatile Node tail;
// 同步状态
private volatile int state;...那什么是volatile呢又有什么用呢 volatile是Java中用于修饰变量的关键字其可以保证该变量的可见性以及有序性但是无法保证原子性。更准确地说是volatile关键字只能保证单操作的原子性比如 x1但是无法保证复合操作的原子性比如x
其为Java提供了一种轻量级的同步机制保证被volatile修饰的共享变量对所有线程总是可见的也就是当一个线程修改了一个被volatile修饰共享变量的值新值总是可以被其他线程立即得知。
相比于synchronized关键字synchronized通常称为重量级锁volatile更轻量级开销低因为它不会引起线程上下文的切换和调度。
大家感兴趣的可以去看看笔者之前的文章volatile关键字在并发中有哪些作用
CAS
我们知道并发的三大特性除了可见性有序性还有一个原子性很不幸volatile关键字无法保证原子性那么Doug Lea大师写AQS的时候是怎么保证原子性的呢 当我们仔细去阅读源码时会发现出现大量compareAndSwap相关方法也叫CAS顾名思义比较并交换
CAS机制的可以保证一个共享变量的原子操作问题比如对变量i先读后写这2步操作可以封装成一个原子操作这样就能保证并发的安全性那其工作原理是什么呢
在CAS机制中包含三个核心操作数 – 内存位置V、预期原值A和新值(B)
如果内存位置V处的值 与预期原值A,相匹配那么处理器会自动将该位置V处值更新为新值B。那如果不一样。说明A的值已经被别的线程修改过了,所以不会更新内存位置V处的值更新失败后线程会重新获取此时内存位置V处的值。其实这就是一种乐观锁然后就是不断重复这一系列的操作叫做自旋这是高情商的说法其实就是死循环~~
那当然自旋CAS如果长时间不成功会给CPU带来非常大的执行开销
需要注意的是这比较和提交操作都是原子性的来源于底层硬件层的现在的 CPU 中为这两个动作专门提供了一个指令CAH 由 CPU 来保证这两个操作一定是原子的。在Java语言层调用UnSafe类的CAS方法,JVM会帮我实现CAS汇编指令UnSafe类Java无法直接访问需要通过本地(native)方法来访问
//将同步状态值设置为给定值updateCAS,原子性
protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}然后我们接着依次看看AQS里面的重要的方法这里主要是独占模式下的相关一系列方法
AQS独占模式获取锁
acquire与tryAcquire
public final void acquire(int arg) {//尝试获取资源if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}在AQS的acquire方法中首先调用了tryAcquire而AQS中没有实现tryAcquire,而是抛出了一个异常,那么就是由其子类实现
protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}我们本文是以ReentrantLock为例所以我们去找ReentrantLock中tryAcquire的具体实现
//公平锁tryAcquire实现
protected final boolean tryAcquire(int acquires) {final Thread current Thread.currentThread();// 获取锁同步状态int c getState();if (c 0) {// 0表示无锁状态//判断有没有别的线程排在了当前线程的前面,这个方法我们后续再讲if (!hasQueuedPredecessors() //cas竞争锁将state的值改为1compareAndSetState(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实现protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}final boolean nonfairTryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState();if (c 0) {// 0表示无锁状态//cas竞争锁将state的值改为1if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} // 如果是同一个线程来获得锁则直接增加冲入次数else if (current getExclusiveOwnerThread()) {int nextc c acquires;if (nextc 0) // overflowthrow new Error(Maximum lock count exceeded);setState(nextc);return true;}return false;}理想的情况是当前线程直接通过tryAcquire方法直接拿到了锁。但是如果没有拿到锁该怎么办呢
我们回到acquire源码处
public final void acquire(int arg) {//尝试获取资源if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}我们可以发现在tryAcquire返回false的时候会接着又调用了addWaiter方法将其加入到了同步队列。acquireQueued的职责是线程进入队列之后的操作尝试获取锁不然就挂起让线程变成阻塞状态
addWaiter
我们先来看一下addWaiter方法相关的源码
private Node addWaiter(Node mode) {//线程封装成Node,并根据给定的模式独占或者共享Node node new Node(Thread.currentThread(), mode);Node pred tail;//尝试添加尾节点如果是第一个结点加入肯定为空跳过if (pred ! null) {node.prev pred;//CAS设置尾节点if (compareAndSetTail(pred, node)) {pred.next node;return node;}}//没有一次成功的话就会去多次尝试enq(node);return node;
}private Node enq(final Node node) {for (;;) {//自旋也就是死循环Node t tail;if (t null) { // Must initialize//CAS 设置队列头新建一个空的Node节点作为头结点if (compareAndSetHead(new Node()))tail head;} else {node.prev t;//CAS 设置队列尾存储当前线程的节点if (compareAndSetTail(t, node)) {t.next node;return t;}}}
}
需要注意的是head结点本身不存在任何数据是一个虚节点它只是作为一个牵头结点如果队列不为nulltail则永远指向尾部结点
采用虚节点当头结点主要是因为每个节点都需要设置前置节点的 ws 状态这个状态是为了保证数据一致性如果只有一个线程竞争锁时只有一个结点其是没有前置节点的所以需要创建一个虚拟节点这样就能兼容临界情况当只有一个线程竞争锁时无需初始化生成同步队列直接获取同步锁即可
在aquire方法中调用addWaiter方法时会标记模式SHARED表示共享模式EXCLUSIVE表示独占模式
acquireQueued与hasQueuedPredecessors
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))我们阅读看下acquireQueued的源码
final boolean acquireQueued(final Node node, int arg) {boolean failed true;try {boolean interrupted false;//自旋死循环for (;;) {final Node p node.predecessor();获得该node的前置节点// 当前线程的前驱节点是头结点即该节点是第二个节点且获取锁成功if (p head tryAcquire(arg)) {setHead(node);p.next null; // // help GC将前置节点移出队列这样就没有指针指向它可以被gc回收failed false;//返回false表示不能被打断即没有被挂起也就是获得到了锁return interrupted;}//如果node的前驱节点不是头结点那么则调用shouldParkAfterFailedAcquire方法判断是否要将线程挂起。如果是则调用parkAndCheckInterrupt将线程挂起。if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt())interrupted true;//如果等待过程中只要被中断过就将interrupted标记为true}} finally {if (failed)//如果失败就取消尝试获取锁cancelAcquire(node);}}需要注意的是不管是非公平锁还是公平锁只要你没获取到锁就都得去同步队列中排队然后出队抢锁!
这里可能就有小伙伴就要问了**非公平锁怎么还要排队啊那还是非公平锁吗**没得灵魂
公平锁与非公平锁的主要区别主要是tryAcquire()方法我们上面已经贴出ReentrantLock中tryAcquire()公平锁和非公平锁具体实现的实现源码可以发现主要区别就是公平锁多一个hasQueuedPredecessors这个方法
public final boolean hasQueuedPredecessors() {//读取头节点Node t tail; //读取尾节点Node h head;//s是首节点h的后继节点Node s;return h ! t ((s h.next) null || s.thread ! Thread.currentThread());
}hasQueuedPredecessors源码很简短就是判断有没有别的线程排在了当前线程的前面如果有的话返回true表示线程需要排队没有则返回false则表示线程无需排队也就是为公平锁判断线程需不需要排队
换句话说就是ReentrantLock这里公平与非公平锁的区别具体体现在
公平锁会先判断同步队列是否存在结点如果存在必须先执行完同步队列中的线程结点也就是说没入队的线程就不能参与抢锁非公平锁不管同步队列是否存在线程结点直接尝试去抢锁** **这样后到的线程就有可能先抢到锁
这里和公平与非公平锁一般意义上的定义有所区别在一般情况下我们更倾向于效率较高的非公平锁。
上述源码结合AQS同步队列示意图能够更好地理解 上图的具体参数上文已经阐述这里就不再赘述
如果node的前驱节点不是头结点那么则调用shouldParkAfterFailedAcquire方法判断是否要将线程挂起。如果是则调用parkAndCheckInterrupt将线程挂起。我们马上就来看这2部分源码
shouldParkAfterFailedAcquire
shouldParkAfterFailedAcquire主要是判断一个线程是否阻塞这里涉及到Node类中waitStatus的两个状态属性
CANCELLED等于1表示节点被取消即结束状态。SIGNAL等于-1表示当前节点需要去唤醒下一个节点。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {//获取前驱节点的等待状态int ws pred.waitStatus;//如果如果前驱节点处于等待状态SIGNAL则返回trueif (ws Node.SIGNAL)return true;//如果ws0 则说明是结束状态if (ws 0) {//遍历前驱结点直到找到最近一个不是结束状态的node然后插个队排在它的后边do {node.prev pred pred.prev;} while (pred.waitStatus 0);pred.next node;} else {//如果ws小于0又不是SIGNAL状态//则将其设置为SIGNAL状态当前节点被挂起等待唤醒。。compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {//将当前线程挂起.进入等待唤醒状态LockSupport.park(this);//获取线程中断状态也就是检测线程是否被中断同时会清除当前线程的 中断标志return Thread.interrupted();
}需要注意的是park()会让当前线程进入等待唤醒状态waiting一般可以通过unpark或者interrupt去唤醒。
不知道大家有没有对这里感到一丝丝的奇怪LockSupport.park(this)你不是说是将当前线程挂起嘛!!!那怎么还能继续去执行下面的Thread.interrupted()博主你是不是打自己脸啊 嗐首先这代码不是博主写的(先撇清关系)这个是大师Doug Lea写的另外遇到问题我们不能先质疑别人先反思自己
咳咳我们来看下其中的奥秘interrupt是Thread类的的APIpark是Unsafe类的API两者是有区别的
一般情况下如果线程A调用LockSupport.park()后会停在那直到其他线程调用LockSupport.unpark(),线程A才能继续执行
但是我们之前讲了LockSupport.park()还有一种方法唤醒就是interrupt()它的作用就是给线程打一个中断标志也就是说当线程有中断标志时线程A调用LockSupport.park()后不会停会接着执行Thread.interrupted(),检测线程是否被中断同时会清除当前线程的中断标志会返回true。
当返回true后外层代码会执行 interrupted true;再次记录其实当前线程是被中断过的因为在parkAndCheckInterrupt中的Thread.interrupted()已经把当前线程的中断标志给清除了所以当前线程它自己不知道自己已经被中断过了
然后acquireQueued()这个方法会返回true来提醒当前线程被中断过。最后调用selfInterrupt给当前线程补上一个中断标志让当前线程自己知道自己被中断过同时也唤醒当前线程。
如果你需要在线程发生中断时结束获取锁那么可以考虑使用lockInterruptibly()来获取锁。
让我们再次回到acquire源码处
public final void acquire(int arg) {//尝试获取资源if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}static void selfInterrupt() {//给当前线程打一个中断标志唤醒当前线程Thread.currentThread().interrupt();}至此AQS通过lock拿锁的流程结束这同样也是ReentrantLock.lock()拿锁的流程 笔者画了张图让我们把AQS中lock拿锁的流程给串起来
AQS独占模式释放锁
public final boolean release(int arg) {// 尝试释放锁if (tryRelease(arg)) {Node h head;if (h ! null h.waitStatus ! 0)//释放锁成功后,唤醒后继节点unparkSuccessor(h);return true;}return false;}tryRelease
跟tryAcquire()一样这个方法是需要独占模式的ReentrantLock去实现的
protected final boolean tryRelease(int releases) {//针对可重入锁的情况下, c可能大于1,将当前持有锁的线程个数减1int c getState() - releases;// 确保释放锁的线程当前必须是持有锁的线程if (Thread.currentThread() ! getExclusiveOwnerThread())throw new IllegalMonitorStateException();// 是否完全释放锁boolean free false;// 如果c0表明没有嵌套锁了可以释放了不然还不能释放掉if (c 0) {free true;setExclusiveOwnerThread(null);}setState(c);return free;}需要注意的是,与tryAcquire()不同的是这里并没有使用任何CAS操作,因为当前线程已经持有了锁才会去释放锁呀~~所以肯定线程安全
unparkSuccessor private void unparkSuccessor(Node node) {int ws node.waitStatus;if (ws 0)///当前线程所在的结点状态设置为0允许失败compareAndSetWaitStatus(node, ws, 0);//唤醒后继节点的线程若为空从tail往前遍历找一个距离head最近的正常的节点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);}需要注意的是从后向前查找正常的节点是为了兼容在addWaiter方法中刚入队列的节点由于节点入队不是一个原子操作有3步
设置node的前驱节点为当前的尾节点node.prev t(不成功也会自旋直到成功)修改tail属性使它指向当前节点修改原来的尾节点使它的next指向当前节点(这步依赖于第2步第2步不执行这步也不会执行)
当有大量的线程在同时入队的时候同一时刻只有一个线程能完整地完成这三步而其他线程只能完成第1步也就是说会出现t.next的值还没有被设置成node导致next链可能中间断开了的情况而每个线程都能完成第1步也就是node.prev pred保证了prev链是连续且唯一的所以如果从tail往前遍历新加的节点都能遍历到能够将整个队列完整地走一遍
小结
AQS不愧是Doug Lea大神的闭关修炼下的力作其利用CAS 自旋 volatile变量最终实现多个线程访问共享资源的功能写的真的很精妙里面细节满满
AQS的实现中并不是后继节点“监听”前驱节点的状态来决定自身是否持有锁而是通过前驱节点释放锁并主动唤醒后继节点来实现排队的。
本文着重解读了AQS独占锁的获取与释放由于篇幅有限而AQS的细节实在太多呼噜噜后续有空会继续更新共享锁可中断等待队列等很重要的特性~
参考资料 《Java并发编程实战》 《Java并发编程的艺术》 https://www.cnblogs.com/dennyzhangdd/p/7218510.html 作者小牛呼噜噜 首发于公众号 小牛呼噜噜」系列文章还有
计算机硬件的读写速度差异聊聊GPU与CPU的区别聊聊CPU的发展历程之单核、多核、超线程什么是计算机中的高速公路-总线计算机中数值和字符串怎么用二进制表示聊聊开关和CPU之间故事简易加法器的实现减法器的设计与实现并用译码器显示16、10进制漫谈从RS触发器到D触发器的发展历程突破计算机性能瓶颈的利器CPU CacheCPU Cache是如何映射与寻址的CPU Cache是如何保证缓存一致性如何利用缓存让CPU更有效率地执行代码