1 ReentrantLock与synchronized的区别
我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全性的目的。在synchronized 中,我们分析了偏向锁、轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。
ReentrantLock与synchronized的区别如下:
1.原始构成
Synchronized是关键字属于JVM层面
Lock是具体类(java.util.concurrent.locks.Lock) 是api层面的锁,依赖AQS
2.使用方法
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放锁的占用
ReetrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致死锁现象需要Lock() 和unLock()方法配合try/finally语句块完成。
3.等待是否可中断
synchronized不可中断,除非抛出异常或者正常运行完成
ReetrantLock可中断:
- 设置超时方法tryLock(Long timeout, TimeUnit unit)
- lockInterruptibly() 放代码块中,调用interrupt方法中断
4.加锁是否公平
synchronized非公平锁
ReetrantLock两者都可以,默认是非公平锁,构造方法传入Boolean值,true为公平锁,false为非公平锁
5.锁绑定多个条件Condition
synchronized没有
ReetrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像syncronized要么随机一个线程要么唤醒全部。
下面通过伪代码,进行更加直观的比较:
// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
2 AQS原理
2.1 AQS是什么
AQS全称 是AbstractQueuedSynchronizer, 是 JUC 提供的一个用于构建锁和同步容器的基础类,。同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的 方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些 模板方法将会调用使用者重写的方法。JUC 包内许多类都是基于 AQS 构建,例如 ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、FutureTask 等。
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线程争抢锁失败后会封装成 Node 加入到 AQS 队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
2.2 AQS的核心成员
2.2.1 状态标识位
AQS 中维持了一个单一的 volatile 修饰的状态信息 state,AQS 使用 int 类型的 state 标示锁的状态,可以理解为锁的同步状态。
state 因为用 volatile 因此保证了我们操作的可见性,所以任何线程通过 getState()获得状态都是可以得到最新值。AQS 提供了 getState( )、setState( )来获取和设置同步状态,具体如下:
由于 setState()无法保证原子性,因此 AQS 给我们提供了 compareAndSetState 方法利用底层UnSafe 的 CAS 机制来实现原子性。compareAndSetState()函数实际上调用的是 unsafe 成员的compareAndSwapInt 方法。
2.2.2 队列节点类(Node 内部类)
AQS 是一个虚拟队列不存在队列实例,仅存在节点之间的前后关系。节点类型通过内部类Node 定义,其核心的成员如下:
static final class Node {
/**节点等待状态值 1:取消状态*/
static final int CANCELLED = 1;
/**节点等待状态值-1:标识后继线程处于等待状态*/
static final int SIGNAL = -1;
/**节点等待状态值-2:标识当前线程正在进行条件等待*/
static final int CONDITION = -2;
/**节点等待状态值-3:标识下一次共享锁的 acquireShared 操作需要无条件传播*/
static final int PROPAGATE = -3;
//节点状态:值为 SIGNAL、CANCELLED、CONDITION、PROPAGATE、0
//普通的同步节点的初始值为 0,条件等待节点的初始值为 CONDITION (-2)
volatile int waitStatus;
//节点所对应的线程,为抢锁线程或者条件等待线程
volatile Thread thread;
//前驱节点,当前节点会在前驱节点上自旋,循环检查前驱节点的 waitStatus 状态
volatile Node prev;
//后继节点
volatile Node next;
//如果当前 Node 不是普通节点而是条件等待节点,则节点处于某个条件的等待队列上
//此属性指向下一个条件等待节点,即其条件队列上的后继节点。
Node nextWaiter;
...
}
2.2.2.1 waitStatus 属性
每个 Node 节点与等待线程关联,每个节点维护一个状态 waitStatus,waitStatus 的各种值以常量的形式进行定义。waitStatus 的各常量值,具体如下:
(1)static final int CANCELLED = 1
waitStatus 值为 1 时表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞。表示线程因为中断或者等待超时,需要从等待队列中取消等待;由于该节点线程等待超时或者被中断,需要从同步队列中取消等待,则该线程被置 1。节点进入了取消状态,该类型节点不会参与竞争,且会一直保持取消状态。
(2)static final int SIGNAL = -1
waitStatus 为 SIGNAL(-1)时表示其后继的节点处于等待状态,当前节点对应的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行。
(3)static final int CONDITION =-2;
waitStatus 为-2 时,表示该线程在条件队列(Condition queues)中阻塞(Condition 有使用),表示结点在等待队列中(这里指的是等待在某个 lock 的 condition 上),当持有锁的线程调用了 Condition 的 signal()方法之后,结点会从该 condition 的等待队列转
移到该 lock 的同步队列上,去竞争 lock。(注意:这里的同步队列就是我们说的 AQS 维护的 FIFO
队列,等待队列则是每个 condition 关联的队列)节点处于等待队列中,节点线程等待在 Condition 上,当其他线程对 Condition 调用了 signal()方法后,该节点从等待队列中转移到同步队列中,加入到对同步状态的获取中。
(4)static final int PROPAGATE = -3;
waitStatus 为-3 时,表示该下一个线程获取共享锁后,自己的共享状态会被无条件的传播下去,因为共享锁可能出现同时有 N 个锁可以用,这时候直接让后面的 N 个节点都来工作。这种状态在 CountDownLatch 中有使用到。
(5)waiteStatus 为 0
waitStatus 为 0 时,表示当前节点处于初始状态。
Node 节点的 waitStatus 状态,为以上 5 种状态的一种。
2.2.2.2 thread 成员
Node 的 thread 成员用来存放进入 AQS 队列中的线程引用;Node 的 nextWaiter 成员用来指向自己的后继等待节点,此成员只有线程处于条件等待队列中的时候使用。
2.2.2.3 抢占类型常量标识
Node 结点还定义了两个抢占类型常量标识:SHARED、EXCLUSIVE,具体如下:
static final class Node {
//标识节点在抢占共享锁
static final Node SHARED = new Node();
//标识节点在抢占独占锁
static final Node EXCLUSIVE = null;
...
}
SHARED 表示线程是因为获取共享资源时阻塞,而被添加到队列中的;EXCLUSIVE 表示线程因为获取独占资源时阻塞,而被添加到队列中的。
2.2.3 FIFO 双向同步队列
AQS 的内部队列是 CLH 队列变种,每当线程通过 AQS 获取锁失败,线程将被封装成一个Node 节点,通过 CAS 原子操作插入队列尾。当有线程释放锁时,AQS 会尝试让队头的后继节点占用锁。
AQS 是一个通过内置的 FIFO 双向队列来完成线程的排队工作,内部通过结点 head 和 tail 记录队首和队尾元素,元素的结点类型为 Node 类型,具体如下:
/*首节点的引用*/
private transient volatile Node head;
/*尾节点的引用*/
private transient volatile Node tail;
AQS 的首结点和尾节点,都是懒加载的。懒加载的意思是,在需要的时候才真正创建。只有在线程竞争失败的情况下,有新线程加入同步队列时,AQS 才创建一个 Head 结点。Head 结点只能被 setHead 方法修改,并且结点的 waitStatus 不能为 CANCELLED。尾节点只在有新线程阻塞时,才被创建。
2.2.4 JUC 显式锁与 AQS 的关系
java.util.concurrent.locks 包中显示锁如 ReentrantLock、ReentrantReadWriteLock,线程同步工具如 Semaphore,异步回调工具如 FutureTask 等,内部都使用了 AQS 作为等待队列。通过开发工具去进行 AQS 的子类导航,会发现大量的 AQS 子类以内部类的形式使用:
3. ReentrantLock 的源码分析
3.1 ReentrantLock抢占锁
调用 ReentrantLock 中的 lock()方法,源码的调用过程我使用了时序图来展现。
3.1.1 ReentrantLock.lock()
这个是 ReentrantLock获取锁的入口:
public void lock() {
sync.lock();
}
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑,我们前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。
Sync 有两个具体的实现类,分别是:
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
FailSync: 表示所有线程严格按照 FIFO 来获取锁。
3.1.2 非公平锁的同步器子类
ReentrantLock 为非公平锁实现了一个内部的同步器——NonfairSync,其显示锁获取方法 lock的源码如下:
static final class NonfairSync extends Sync {
//非公平锁抢占
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//...省略其他
}
首先用一个 CAS 操作,判断 state 是否是 0(表示当前锁未被占用),如果是 0 则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS 操作只能保证一个线程操作成功,剩下的只能乖乖的去排队。
ReentrantLock“非公平”性即体现在这里:如果占用锁的线程刚释放锁,state 置为 0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。举个例子:当前有三个线程 A、B、C 去竞争锁,假设线程 A、B 在排队,但是后来的 C 直接进行 CAS 操作成功了,拿到了锁开开心心的返回了,那么线程 A、B 只能乖乖看着。
3.1.3 非公平抢占的钩子方法:tryAcquire(arg)
如果非公平抢占没有成功,非公平锁的 lock 会执行模板方法 acquire,首先会调用到钩子方法 tryAcquire(arg)。非公平抢占的钩子方法实现如下:
static final class NonfairSync extends Sync {
//非公平锁抢占的钩子方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//...省略其他
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 先直接获得锁的状态
int c = getState();
if (c == 0) {
// 如果出现了任务队列 首节点线程完工了,将 state 设置为 0,
// 下一步就进行交接仪式了。这个时候,首先抢占,不管不顾
// 发现 state 是空的,那就直接拿来加锁使用。根本不考虑后面继承者的存在。
if (compareAndSetState(0, acquires)) {
// 1、利用 CAS 自旋方式,判断当前 state 确实为 0,然后设置成 acquire(1)
// 这是原子性的操作,可以保证线程安全
setExclusiveOwnerThread(current);
// 设置当前执行的线程,直接返回为 true
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
// 2、当前的线程和执行中的线程是同一个,也就意味着可重入操作
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
// 表示当前锁被 1 个线程重复获取了 nextc 次
return true;
}
// 否则就是返回 false,表示没有尝试成功获取当前锁,进入排队过程
return false;
}
//...省略其他
}
非公平同步器 ReentrantLock.NonfairSync 的核心思想就是当前进程尝试获取锁的时候,如果发现锁的状态位是 0 的话就直接尝试将锁拿过来,然后 setExclusiveOwnerThread,根本不管同步队列中的排队节点。
3.1.4 ReentrantLock 公平锁的抢占流程
ReentrantLock 为公平锁实现了一个内部的同步器——FairSync ,其显示锁获取方法 lock 的源码如下:
static final class FairSync extends Sync {
//公平锁抢占的钩子方法
final void lock() {
acquire(1);
}
//...省略其他
}
公平同步器 ReentrantLock.FairSync 的核心思想:通过 AQS 模板方法去进行队列入队操作。
3.1.5 公平抢占的钩子方法:tryAcquire(arg)
公平锁的 lock 会执行模板方法 acquire,该方法首先会调用到钩子方法 tryAcquire(arg)。公平抢占的钩子方法实现如下:
static final class FairSync extends Sync {
//公平抢占的钩子方法
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;
}
}
公平抢占的钩子方法中,首先判断是否有后继,如果有后继,并且当前线程不是锁的占有线程,那么钩子方法就返回 false,模板方法会进入排队的执行流程,可见,公平锁是真正公平的。
3.1.6 是否有后继节点的判断
FairSync 进行是否有后继节点的判断的代码,具体如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
hasQueuedPredecessors 的执行场景,大致如下:
(1)当 h!=t 不成立的时候,说明 h 头结点、t 尾结点要么是同一个节点,要么都是 null,此时 hasQueuedPredecessors 返回 false 表示没有后继。
(2)当 h!=t 成立的时候,进一步检查 head.next 是否为 null,如果为 null,返回 true。什么情况下 h!=t 同时 h.next==null 呢,有其他线程第一次正在入队时,可能会出现。其他线程执行 AQS的 enq 方法,compareAndSetHead(node)完成,还没执行 tail=head 语句时,此时 t=null,head=new Node(),head.next=null。
(3)如果 h!=t 成立,head.next != null,则判断 head.next 是否是当前线程,如果是返回 false,否则返回 true。
head 节点是获取到锁的节点,但是任意时刻 head 节点可能占用着锁,也可能释放了锁,如果释放了锁则此时 state=0 了,未被阻塞的 head.next 节点对应的线程在任意时刻都是在自旋的在尝试获取锁。
3.2 ReentrantLock释放锁
3.2.1 ReentrantLock.unlock
在 unlock 中,会调用 release 方法来释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功
Node h = head; //得到 aqs 中 head 节点
if (h != null && h.waitStatus != 0)
//如果 head 节点不为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点
unparkSuccessor(h);
return true;
}
return false;
}
3.2.2 ReentrantLock.tryRelease
这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它的线程有机会进行执行。在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock()的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true。
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;
}
为什么在释放锁的时候是从 tail 进行扫描?
在else的代码块中来看一个新的节点是如何加入到链表中的:
- 将新的节点的 prev 指向 tail
- 通过 cas 将 tail 设置为新的节点,因为 cas 是原子操作所以能够保证线程安全性
- t.next=node;设置原 tail 的 next 节点指向新的节点
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;
}
}
}
}
在 cas 操作之后,t.next=node 操作之前。存在其他线程调用 unlock 方法从 head开始往后遍历,由于 t.next=node 还没执行意味着链表的关系还没有建立完整。就会导致遍历到 t 节点的时候被中断。所以从后往前遍历,一定不会存在这个问题。
参考来源:
《Java并发编程的艺术》
《深入理解Java虚拟机》
不可不说的Java“锁”事
从ReentrantLock的实现看AQS的原理及应用