锁的基本原则:把多线程的并行任务通过某种机制实现线程的串行化执行达到线程安全的目的。
1、锁的特性
-
共享:所有的线程都有机会获取;
-
互斥性:一旦一个线程获取锁没有释放前,其他的线程只能阻塞等待;
-
重入性:同一个线程可以重入-避免死锁;
-
高效释放:避免发生死锁,超时机制;
2、Lock与Synchronized的区别
JUC(Java.util.concurrent),作者是
Doug lee [USA],是在并发编程中比较常用的工具类,里面包含很多用来在并发场景中使用的组件。比如并发集合、线程池、阻塞队列、计时器、同步器等等。
在 Lock 接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于Synchronized 关键字来解决。但是 Synchronized 在有些场景中会存在一些短板, Synchronized中锁的获取和释放不是我们能控制的;也就是它并不适合于所有的并发场景,所以我们迫切需要自己实现了一个更加灵活可控性强的锁,例如可中断,可设置公平,可自己释放等特性的锁。因此在 Java5 以后,Lock 的出现就解决了Synchronized 在某些场景中的短板,变得更加灵活。
下面看一下Lock和Synchronized的区别与联系:
|
synchronized(主要是不灵活)OS实现-
字节码-moniter
|
lock 显示锁:
基于AQS
|
描述 |
内置语言
关键字的
jvm实现 (重入锁)
|
是一个
接口(重入锁)
|
获取·释放
|
执行完同步代码块 + 发生异常会
自动释放锁,不能自控
|
同步快执行完毕需要
手动释放锁
unlock() ,final()
|
中断
|
不会响应中断,无限等待,不支持超时-
灵活
|
响
应
中断,支持
超时tryLock()
|
死锁
|
不知道有没有成功获取,会一直阻塞等待,
死锁
|
知道获取成功
尝试获取 trylock()
|
公平
|
非公平锁
|
lock()可以设置公平和非公平
|
|
悲观锁-
独占锁
|
悲观锁 + (cas乐观锁)
|
灵活
|
synchronizd的添加多个锁
|
一个lock绑定多个条件Condition
|
3、Lock接口
Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意 味着它定义了锁的一个标准规范,也是典型的模版设计模式,也意味着锁的不同实现。
这也是面向接口编程的好处,
如果想要实
现自己的一个显示锁,只需要实现 lock接口就OK,使用起来非常灵活,易扩展,使用的时候只需要获取接口的不同的实现类就可以使用其功能,其他的代码逻辑不用修改。
//本质是一个接口,意思就是定义了锁的规范
public interface Lock {
//阻塞式加锁 - 如果中断,表现的十分友好
void lock();
//可中断的加锁 - 如果中断直接抛异常
void lockInterruptibly() throws InterruptedException;
//非阻塞式加锁,加一次,不成功就撤
boolean tryLock();
//超时加锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//解锁
void unlock();
//条件式加锁
Condition newCondition();
}
lock接口的类图:
从Lock接口的类图可以看到常见的几个锁的实现:
ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个 类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则 是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥,但是读操作之间不会阻塞线程,提高了程序并发性能。
StampedLock: stampedLock是JDK8引入的新的锁机制,可以简单认为是读写锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。StampedLock是一种乐观的读写策略,使得乐观锁完全不会阻塞写线程。
ReentrantLock:是一种可重入锁,下面将对其详细介绍。
4、认识AQS(AbstractQueueSynchronizer)
这里在学习ReentrantLock之前需要好好了解一下AQS。
什么是AQS: (AbstractQueueSynchronizer) 抽象同步队列。
它只是一个同步工具,也是实现线程同步的核心组件,利用它可以实现共享锁和独占锁;
AQS它本身并没有业务功能,是一个基础组件支撑层,我们可以调用这个组件工具去实现一些锁功能,从使用层面来说,
AQS的同步功能分为两种:
-
独占:只有一个线程得到锁,其他的线程只能阻塞;例如ReentrantLock 的实现;
-
共享:允许多个线程同时获取锁,并发访问共享资源;例如ReentrantReadWriteLock- 读写锁,读与读操作共享;
5、为什么会有AQS?
我们知道锁的基本原理是,基于将多线程并行任务通过某一机制实现线程的串行化运行,从而达到线程安全的目的。要想更好的理解AQS,可以把它和Synchronized类比,在基于OS实现的synchronized中,我们知道有偏向锁/轻量级锁/自旋锁/乐观锁cas的优化。重量级锁阶段,通过线程的阻塞及唤醒来达到线程竞争和同步的目的。那么在ReentrantLock等锁中,也一定会存在某一种机制去解决这个问题。
思考:当多个线程竞争锁的时候,只有一个线程成功,其他的线程怎么办呢?竞争失败的线程是如何实现阻塞以及被唤醒的呢?
这里就用到了同步工具:
AQS
。
-
问题一 : 锁是什么?就是一个标记位。锁用什么存储,类似于对象头中的锁标记位;
-
问题二: 如何解决线程的互斥和同步通信呢?类似Object中的wait()和notify() ;
-
问题三: 在多线程竞争重入锁的时候, 竞争失败的线程是如何管理呢? 下次我将它唤醒从那里开始呢?所以 一定要有一个数据结构来承载它;
-
拿不到锁的线程必须要做点什么,只有两种选择?
-
继续抢:例如利用CAS机制自旋,或者控制次数的自适应自旋锁;
-
park() 阻塞自己,暂时放弃cpu的执行权;
-
5.1、AQS的解决方案
-
锁标记:实现了互斥共享; private volatile int state;
-
线程之间的协作:使用 lockSupport: park()和unpark()来阻塞和唤醒;
-
阻塞线程的管理:使用双向链表数据结构实现了线程的管理和排队,使用Node封装了线程;
-
可重入:记录CurrentThreadid + state计数;
-
阻塞的条件:Condition,获取锁的线程需要等待其他线程的处理结果数据;
-
公平与非公平:看队列里是否有线程排队;
- CAS机制: 保证所有的更新操作的原子性;
AQS的源码封装线程的Node结构及状态:
//抽象同步队列工具
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
protected AbstractQueuedSynchronizer() { }
/**
* Wait queue node class.
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*
//封装线程的节点状态
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 value to indicate thread has cancelled */
//等待的线程发生超时或者中断会将该线程从同步队列中移除,针对发生异常的情况;状态不会再发生变化
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
//只要前置节点获取锁,就会通知状态为signal的后续节点去竞争锁;-aqs队列里的节点
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
//条件锁,一把锁的多个等待条件通知节点 - Condition
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
//共享模式下,propagate的线程处于可运行状态 -countDownLatch
static final int PROPAGATE = -3;
//线程的等待状态
volatile int waitStatus;
//双向链表的前驱指针;
volatile Node prev;
//双向链表的后驱指针;
volatile Node next;
//封装阻塞的线程
volatile Thread thread;
//双向链表的头节点;
private transient volatile Node head;
private transient volatile Node tail;
//互斥变量;
private volatile int state;
Node nextWaiter;
}
//node节点的状态:
* CANCELLED 1: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取 消该 Node的结点, 其结点的 waitStatus为 CANCELLED,即结束状态,进入该状 态后的结点将不会再变化。
* SIGNAL -1: 只要前置节点获取锁,就会通过状态为signal的后续节点去竞争锁;
* CONDITION -2: 条件锁,一把锁的多个等待条件通知节点
* PROPAGATE -3: 共享模式下,propagate的线程处于可运行状态
* 0: 初始状态
AQS的双向链表Node节点中的四个元素:
-
state: 共享变量记录锁的状态;
-
0: 无锁
-
>= 1 有锁可重入
-
-
tail:尾节点
-
head:头节点
-
exclusiveOwerThread:拥有锁的线程id
其数据结构及主要节点示意图:
6、ReentrantLock的实现原理
ReentrantLock重入锁,表示支持同一个线程锁的重新获取,也就是说,如果当前线程 t1 通过调用lock方法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数就行了。Synchronized和ReentrantLock都是可重入锁,防止死锁。
6.1、核心数组结构及思想
-
Node LinkedList:双向链表-保存阻塞的线程—同步队列;
-
否则我怎么知道从那里唤醒一个什么线程呢?
-
阻塞的线程怎么办呢?
-
-
ConditionObject:实现了一个单向链表
-
等待其他线程的数据,因同一个条件阻塞且没有抢锁权限的线程怎么办呢,肯定得有一个数据结构来记录它;
-
-
State: 一个共享的volatile变量,记录互斥锁状态;
-
LockSupport: 阻塞与唤醒:线程之间的通信
-
阻塞park():将线程封装为node加入双向链表;
-
释放unpark():将该线程从链表头删除,唤醒下一个线程;
-
-
CAS机制: CompareAndSet()保证线程竞争锁state和exclusiveOwnerThread的安全性,很重要的思想;
6.2、可重入锁的理解
可重入锁的设计是为例避免死锁的发生,对于同一个线程而言,当获取锁后想要再次进入获取,此时不会被阻塞,而是通过
计数器来实现了可重入,进入的时候+1,退出的时候-1;如果已经获取了锁,再次进入的时候就不要再获取锁了,否则等待一个正在使用的锁就会出现死锁;
以Synchronized的实现原理为例:
我们知道Synchronized会在对象头里专门记录锁的信息,包括
锁的线程,
锁的计数器,
锁的状态。线程在获取锁的时候会检查锁的计数器是不是0,为0说明锁未占用,计数器加1,并记下锁的线程;当再次有线程来请求同步的时候,先看看是不是当前持有锁的线程,如果是就直接访问,计数器+1;如果不是,对不起,你阻塞吧。当退出同步
块的时候,计数器-1,变成0时,释放锁;例如下面实例递归调用,当线程再次想获取this锁的时候,如果不能重入则会出现死锁。
//递归调用如果锁不可重入,就出现了死锁;
public class ReentrantLockTest {
public synchronized void reentrantKing(){ //获取了this锁
System.out.println("i am king enter");
}
public synchronized void reentrantZZ(){
System.out.println("i am zz enter");
synchronized (this){ //需要获取同一把this锁,可重入防治死锁
reentrantKing();
}
}
public static void main(String[] args) {
test.reentrantZZ();
}
而Reentrantlock则是使用AQS的state变量实现重入锁:
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入 锁的实现来说,表示一个同步状态。它有两个含义的表示
-
1. 当 state=0 时,表示无锁状态
-
2. 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增, 比如重入 3次,那么 state=3。而在释放锁的时候,同样需要释放3次直到state=0其他线程才有资格获得锁。
6.3、ReentrantLock的使用
锁的使用范式:
Lock lock = new ReentrantLock(); //创建锁
lock.lock(); //获取锁
try{
//被lock()保护起来的代码块
}cache(Exception e){
}finally {
lock.uulock(); //解锁,finally防止异常
}
6.4、公平与非公平
设置:公平锁与非公平锁在创建的时候可以进行设置。
//todo 创建的时候,可以设置公平锁与非公平锁,注意这个地方
private Lock lock = new ReentrantLock(true);
//根据是否是公平锁调用相应的实现
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
区别:公平锁与非公平锁的区别:
-
公平锁:不允许插队,严格按照FIFO的顺序;
-
非公平锁:允许插队,不管队列中是否有线程阻塞等待,上去直接就cas竞争锁;效率高,节约了挂起和唤醒的状态 ,但是容易引起线程饥渴;
思考:非公平锁的效率高在那里?
原因
:当出现锁冲突的时候,线程的
挂起-唤醒切换时间是无法避免的,而非公平锁可以尽量的避免切换,充分利用了挂起和恢复的时间,进而充分利用了cpu;
例如线程a正在运行,线程b在排队,线程a运行完了线程c刚好到达抢到锁,线程b继续排队,线程c就避免了排队,线程c节约了一次阻塞和唤醒的开销。
源码区别其实就一行代码
:就是通过
hasQueuedPreDecessors()判断
当前节点在同步队列中是否存在前驱节点,是否有线程排队。
7、源码分析
首先了解一下ReentrantLock的函数调用关系时序图,以非公平锁为例:
Sync类
下面主要看一下非公平锁
NonfairSync
的源码实现:
Sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,前面提到AQS是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承AQS来实现对应场景的功能
Sync有两个具体的实现类,分别是:
-
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁;
-
FailSync: 表示只要等待队列里有线程在等待,新加入的线程将严格按照FIFO排队来获取锁;
//ReentrantLock-lock-Sync-AbstractQueuedSynchronizer继承关系图
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
//Sync是一个静态内部类
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
}
}
7.1、lock()
lock()获取锁,就是对一个共享的变量state做状态的变更:
-
1、CAS 成功,state=1,就表示成功获得了锁;
-
2、CAS 失败,说明有线程占用该锁,调用 acquire(1)走锁竞争逻辑;
//获取锁
public void lock() {
sync.lock();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
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实现原子操作的数值更新;(exclusiveOwnerThread = threadId & state=1)
if (compareAndSetState(0, 1)) //如果是重入,state + 1即可;
//设置自身的线程,可重入的线程id
setExclusiveOwnerThread(Thread.currentThread());
else
//请求入队列,继续竞争锁,这里的参数1 表示可重入锁的时候,将state +1
acquire(1);
}
//尝试加锁,失败则返回
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
7.2、acquire()
ac
quire()阻塞获取锁,方法的主要功能:
-
1. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false;
-
2. 如果 tryAcquire 失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部;
-
3. acquireQueued,将Node作为参数,通过自旋去继续尝试获取锁;
public final void acquire(int arg) { //可以看到这里的arg传的值为 1
if (!tryAcquire(arg) && //第一步:获取锁失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //第二步:通过 addWator将当前的线程封装为Node加入到 AQS队列尾部排队;
//第三步:通过acquireQueued(),将node作为参数,头节点通过自旋去尝试获取
selfInterrupt(); //线程唤醒后,响应中断,再中断一次
}
//中断当前线程
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
tryAcquire()获取锁的步骤:
-
1. 获取当前线程,判断当前的锁的状态;
-
2. 如果 state=0 表示当前是无锁状态,通过cas更新state状态的值 ;
-
3. 当前线程是属于重入,则增加重入次数 + acquires;
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//第一步:获取当前线程,获取目前锁的状态
int c = getState();
//第二步:state=0,没有线程占有,直接cas获取,这只线程id + state=1
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}//第三步:如果是同一个线程,则增加state++来记录该线程重入次数,线程池里没有这一步
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;
}
AQS.addWaiter: 当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node.
入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状 态。意味着重入锁用到了 AQS 的独占锁功能
-
1. 将当前线程封装成Node;
-
2. 当前链表中的 tail 节点是否为空,如果不为空,则通过cas操作把当前线程的node 添加到AQS队列;
-
3. 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列;
private Node addWaiter(Node mode) {
//第一步:封装线程为节点node,mode为Node.EXCLUSIVE独占状态
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; //第二步:判断尾节点是否为空
if (pred != null) { //2.1 不为空
node.prev = pred; //将该节点的前驱节点指向tail
if (compareAndSetTail(pred, node)) { //第三步:使用cas来设置尾节点
pred.next = node; //设置之前尾节点指向自己:此处有线程安全问题
return node; //返回该节点
}
}
enq(node); //2.2 如果tail节点为空 || cas失败,调用enq将节点加入到队列中,Condition里用的也是这个enq
return node;
}
enq():调用enq通过自旋将当前节点(封装的线程)加入到队列中:
//通过自旋将该节点加入到同步队列中
private Node enq(final Node node) {
for (;;) { //自旋锁
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) //先初始化空头节点, 然后cas来操作加入队列
tail = head;
} else {
node.prev = t; //第一步:新节点的prv指向tail;
if (compareAndSetTail(t, node)) { //第二步:cas自旋加入队列,tail指向当前的节点,保证线程安全
t.next = node; //第三步:修改节点的next指针;这里存在并发安全的问题,所以释放的时候要从尾到头遍历
return t;
}
}
}
}
AQS.acquireQueued():通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给 acquireQueued 方法,去竞争锁:
-
1. 获取当前节点的prev节点;
-
2. 如果 prev 节点为head节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁;
-
3. 抢占锁成功以后,通过setHead()把获得锁的节点设置为head,并且移除原来的初始化 head节点;
-
4. 如果获得锁失败,则根据waitStatus决定是否需要挂起线程shouldParkAfterFailedAcquire();
-
5. 最后,通过cancelAcquire取消获得锁的操作;
final boolean acquireQueued(final Node node, int arg) { //抢占锁
boolean failed = true;
try {
boolean interrupted = false; //判断是否中断过
for (;;) { //自旋锁
//获取该节点的前驱节点
final Node p = node.predecessor();
//如果该节点的前驱节点为head,才有资格去竞争锁
if (p == head && tryAcquire(arg)) {//如果竞争锁成功
setHead(node); //将head节点指向当前获取锁的节点
p.next = null; // help GC //第四步:释放原头节点
failed = false;
return interrupted;
}//根据节点的状态判断是否应该park(),如果prev是signal,则直接放心的挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //返回该线程有没有被中断过,因为线程阻塞无法响应,等唤醒的时候通过判断是否中断过再做处理
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//将获取锁的当前节点设置为属性值为空的头节点
private void setHead(Node node) {
head = node; //第一步:设置新的头节点
node.thread = null; //第二步:获取锁的线程不需要再保存了 thread= null
node.prev = null; //第三步:前驱节点置为null
}
检查是否挂起当前的线程,如果前驱的状态是-1 signal就安心挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //得到前置线程的状态
if (ws == Node.SIGNAL) //前置状态为signal - -1,直接放心的挂起
return true;
if (ws > 0) { //遍历链表,删除cancel- 1 状态的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//将节点的状态设置为 signal状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire()
如果ThreadA的锁还没有释放的情况下,ThreadB和ThreadC来争抢锁肯定是会失败,那么失败以后会调用shouldParkAfterFailedAcquire方法,判断ThreadA是否应该被挂起:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //如果没有资格去过去,则阻塞线程变成wait()状态;
return Thread.interrupted(); //返回中断标志是否被中断过
}
node节点的状态:
* CANCELLED 1: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取 消该 Node的结点, 其结点的 waitStatus为 CANCELLED,即结束状态,进入该状 态后的结点将不会再变化。
* SIGNAL -1: 只要前置节点获取锁,就会通过状态为signal的后续节点去竞争锁;
* CONDITION -2: 条件锁,一把锁的多个等待条件通知节点
* PROPAGATE -3: 共享模式下,propagate的线程处于可运行状态
* 0: 初始状态
7.3、unlock()-锁的释放
在unlock中,会调用release方法来释放锁;public void unlock() {
sync.release(1); //释放锁成功;
}
//继续调用release():
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head; //得到头节点,如果头节点不为空,且状态 !=0,唤醒后续节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease()释放锁
这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值 (参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为 2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。这里主要做了三件事:
-
(1)state=0;
-
(2)exclusiveOwnerThread=null;
-
(3)唤醒下一个节点的阻塞的线程;
protected final boolean tryRelease(int releases) {
//修改state的值:state=state-release,如果state=0,同时唤醒所有的线程;
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//如果当前没有线程使用锁,则设置为独占线程为exclusiveOwnerThread=null
setExclusiveOwnerThread(null);
}
//如果是可重入锁,则设置新的state的值
setState(c);
return free;
}
当释放锁后 CAS: (exclusiveOwnerThread = null & state=0 ),唤醒后续节点;
private void unparkSuccessor(Node node) {
//获得头节点的状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//如果头节点的下一个为null,或者它的状态为cancel,则从tail节点扫描,找到距离head最近的一个节点,唤醒即可;
//思考,为啥要从尾节点开始扫描呢?因为并发入队和遍历的时候破坏了next指针,但是prev指针没有断;
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);
}
源码中,我们发现AQS里使用了很多的CAS机制以及自旋操作:for( ; ; ),因为根据经验其实线程获得锁以后执行的代码块,其实很快就会释放,所以使用自旋可以避免锁的阻塞,提高了效率,避免了线程的阻塞和唤醒。
8、小结
我们发现AQS是实现Reentrantlock锁的基石,但是它本身没有任何业务功能,是一个基础组件,如果我们想实现一把自己的锁,就直接面向Lock接口编程即可。虽然锁能解决问题,但是一般从性能出发,首先应该想到无锁编程:
-
例如 一写多读场景使用volatile就能解决;
-
如果是 多读少写的场景:考虑基于cas + volatile变量的乐观锁是否能够满足要求;
-
其次过程中设计到 锁性能优化问题,还有偏向锁,自旋锁,自适应自旋,JUC里用的很多,可以参考;粒度锁,例如Mybaties的BlockingCache;
-
最后, 多写少读的情况才考虑重量级阻塞式加锁:Lock;
OK---人生不止眼前的苟且,还有诗和远方。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。