Java并发编程|第六篇:ReentrantLock 重入锁
系列文章
前言
JUC
>>>>>>>>java.util.concurrent.*
下面的工具类
重入锁源码中的很多知识点(AQS
,CAS
…),调用的方法(acquire()
,tryAcquire()
,enq(node)
…),在JUC
系列里面的其他工具类中都有多处使用;因此本文的学习非常重要,有助于更好的理解和学习JUC
后面的源码分析系列。
场景
例子1
经典例子count++1000次
创建1000个线程,每个线程操作变量 count++
count 打印结果不确定的原因可以参考Java并发编程|第三篇:synchronized锁
public class ReentrantLockDemo implements Runnable {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new ReentrantLockDemo()).start();
}
Thread.sleep(3000);
System.out.println("count:" + count);
}
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
执行结果:957(每次结果不确定)
使用synchronized
关键字解决
当然有多种使用方法,本文用的是new object()对象作为lock的方式。
public class ReentrantLockDemo32 implements Runnable {
private Object lock;
public ReentrantLockDemo32(Object lock) {
this.lock = lock;
}
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
for (int i = 0; i < 1000; i++) {
new Thread(new ReentrantLockDemo32(lock)).start();
}
Thread.sleep(3000);
System.out.println("count:" + count);
}
@Override
public void run() {
synchronized (lock) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}
执行结果:1000
使用ReentrantLock
重入锁解决
public class ReentrantLockDemo3 implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new ReentrantLockDemo3()).start();
}
Thread.sleep(3000);
System.out.println("count:" + count);
}
@Override
public void run() {
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
}
执行结果:1000
例子2
什么是重入?
同一个线程
- 在demo()中获得lock锁
- 在demo2()中再次获得同一把lock锁
重入锁的设计目的
调用 demo 方法获得了lock对象锁
,然后在这个方法中再去调用demo2,demo2 中的存在同一把lock对象锁
,这个时候如果当前线程因为无法获得 demo2 的lock对象锁
而阻塞,就会产生死锁;重入锁的设计目的是避免线程的死锁。
public class ReentrantLockDemo {
private Object lock;
public ReentrantLockDemo(Object lock) {
this.lock = lock;
}
public void demo() {//获取对象锁
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ":demo");
demo2();
}
}
public void demo2() {
synchronized (lock) {//再次获取对象锁
System.out.println(Thread.currentThread().getName() + ":demo2");
}
}
public static void main(String[] args) {
final Object lock = new Object();
for (int i = 0; i < 10; i++) {
new Thread(() ->
new ReentrantLockDemo(lock).demo()
, "Thread" + i).start();
}
}
}
使用ReentrantLock
重入锁改写上面的例子
重入锁可以有效的防止死锁的产生
public class ReentrantLockDemo4 {
private Lock lock;
public ReentrantLockDemo4(Lock lock) {
this.lock = lock;
}
public void demo() {//获取对象锁
lock.lock();
demo2();
System.out.println(Thread.currentThread().getName() + ":demo");
lock.unlock();
}
public void demo2() {
lock.lock();//再次获取对象锁
System.out.println(Thread.currentThread().getName() + ":demo2");
lock.unlock();
}
public static void main(String[] args) {
final ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 10; i++) {
new Thread(() ->
new ReentrantLockDemo4(lock).demo()
, "Thread" + i).start();
}
}
}
1.类图分析
ReentrantLock
实现了Lock
接口
Lock
本质上是个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意味着它定义了锁的个标准规范,也同时意味着锁的不同实现public interface Lock { ... void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
在
Lock
接口出现之前,Java 中的应用程序对于多线程的并发安全处理只能基于synchronized
关键字来解决。但是synchronized
在有些场景中会存在一些短板,也就是它并不适合于所有的并发场景。但是在 Java5 以后,Lock
的出现可以解决synchronized
在某些场景中的短板,它比synchronized
更加灵活。
-
Sync
实际上是ReentrantLock
中的一个抽象的静态内部类,它继承了AQS(AbstractQueuedSynchronizer)
来实现重入锁的逻辑 -
Sync
有两个具体的实现- NonfairSync(非公平锁):表示可以存着抢占锁的功能,不管当前线程是有存着其他线程等待,新线程都有机会抢占锁
- FairSync(公平锁):表示所有线程严格按照 FIFO(先进先出) 来获取锁
2.预备知识
2.1 什么是AQS
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它是一个同步工具也是 Lock 用来实现线程同步的核心组件。
AQS 的两种功能
从使用层面来说,AQS 的功能分为两种:独占和共享
-
独占锁,每次只能有一个线程持有锁,ReentrantLock 就是以独占方式实现的互斥锁
-
共享锁,允许多个线程同时获取锁,并发访问共享资源 , 比如ReentrantReadWriteLock
AQS内部实现
AQS
队列内部维护的是一个 FIFO
的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向上一个节pre
节点和下一个next
节点;从链表的任意一个节点开始访问都很方便,每个Node
节点其实都是由线程封装,当线程竞争锁失败之后会封装成Node
节点 加入到 AQS
的队列中;当线程释放锁会从队列中唤醒一个阻塞的节点(线程)。
2.2 CAS实现原理
-
C:compare 比较
-
A:and
-
S:swap 替换
我们以java.util.concurrent.locks.AbstractQueuedSynchronizer#compareAndSetState
来分析
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
unsafe
中的compareAndSwapInt
方法是一个native
的方法,有兴趣可以去研究openjdk
的c++
源码
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
这段代码的意思是,通过cas
乐观锁的方式来做比较并替换,如果当前内存中state
的值和预期值expect
相等,则替换为update
值。如果更新成功返回true
,否则返回false
。
这个操作是原子的,不会出现线程安全问题,这里面涉及到unsafe
这个类的操作,以及state
属性的一样。
-
state
是 AQS 中的一个属性,它在不同的实现中所表述的含义是不一样的,对于重入锁的实现来说,它表述锁的状态。- state=0 ,表示无锁状态
- state>=1,表示有锁状态,比如state=1,但是因为ReentrantLock允许重入,所有同一个线程多次获得同步锁时,state会递增,比如state=5;而在释放锁的时候,同样需要依次释放锁直到state=0时,其他线程才能获取锁。
-
Unsafe 类
Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、Hadoop、Kafka 等。
Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、线程的挂起和恢复、CAS、线程同步、内存屏障。
而 CAS 就是 Unsafe 类中提的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false。
3.ReentrantLock 源码分析
3.1 加锁流程
lock.lock() 时序图
lock()入口
ReentrantLock.lock() 方法是锁的入口方法
这里以NonfairSync
的实现为例
final void lock() {
//通过cas来实现原子性,返回true表示,获取lock成功
//如果第一个线程A访问 state默认状态为0 cas操作后变为1
//state锁标记 0:无锁状态 >=1:有锁状态
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
线程A访问,CAS
成功获取锁
CAS:compareAndSetState(0, 1)
成功返回true
- 设置 state=0 -> state=1
- 设置setExclusiveOwnerThread = 当前线程A
acquire()
线程B再访问时,CAS
失败会走else
中的逻辑acquire(1);
acquire
逻辑如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()
第一:tryAcquire(arg),这里的nonfairTryAcquire
是非公平锁的实现
- 尝试获取独占锁,如果成功返回 true,失败返回 false
- 如果当前执行线程和之前获取锁的线程是同一个线程,增加重入次数
final boolean nonfairTryAcquire(int acquires) {
//假设当前线程是线程B
final Thread current = Thread.currentThread();
//再次获取lock的状态
int c = getState();
//如果为0 表示无锁 继续走之前的获取锁的逻辑
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);//设置当前获得锁的线程
return true;
}
}
//如果线程A的lock还没有释放 而且当前线程B=线程A 表示是同一个线程
//重入锁的概念,增加重入次数
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;
}
如果第一方法tryAcquire
返回false
表示是不同线程,继续走后面的逻辑acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
addWaiter ()
addWaiter 方法将当前线程封装成 Node节点 添加到 AQS 队列尾部
- addWaiter(Node.EXCLUSIVE)
- Node 是 AQS 中的一个双向链表结构
EXCLUSIVE
表示该node
为独占节点
private Node addWaiter(Node mode) {
//将当前线程封装成Node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
//第二次 线程B进来不走这段逻辑 因为tail尾节点 为null
if (pred != null) {//如果尾节点(为线程B)不为空 线程C进来
node.prev = pred;//设置当前节点的上一个节点为尾节点(线程B节点)
if (compareAndSetTail(pred, node)) {//设置尾节点为线程C
pred.next = node;//设置线程B节点的下一个节点为线程C节点
return node;
}
}
enq(node);
return node;
}
enq(node)
竞争锁失败的Node(线程)添加到 AQS 链表中
private Node enq(final Node node) {
for (;;) {//自旋
Node t = tail;
//如果tail节点为空
if (t == null) { // Must initialize
//初始化一个head节点,设置tail节点 = head节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//表示AQS队列中已经存在节点
//设置当前节点的上一个节点为tail节点
node.prev = t;
if (compareAndSetTail(t, node)) {//cas设置node节点为tail节点
t.next = node;//cas成功 设计tail节点的下一个节点为node
return t;
}
}
}
}
线程B进来,cas
失败,第一步 初始化一个head
节点 并设置 head = tail = new Node()
第二步:线程B自旋没有获取到锁 addWaiter 添加到链表的尾部
第三步:线程C进来,cas
失败,自旋没有获取到锁 addWaiter 添加到链表的尾部
通过 addWaiter 将线程加到 链表 之后,继续走acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
把 Node 作为参数传递给acquireQueued 方法,去竞争锁
Node 的一些状态
static final class Node {
...
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
...
AQS.acquireQueued()
通过 addWaiter 方法把线程添加到链表后,会接着把 Node 作为参数传递给
acquireQueued 方法,去竞争锁
-
获取当前节点的 prev 节点
-
如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
-
抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head节点
-
如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
-
最后,通过 cancelAcquire 取消获得锁的操作
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋
//获取当前节点的上一个节点
final Node p = node.predecessor();
//如果上一个节点是头节点,继续争抢锁
if (p == head && tryAcquire(arg)) {
//线程A 已经释放锁
//线程B 抢占锁成功以后,把获得锁的线程B节点设置为 head
setHead(node);
//移除原来的初始化 head节点
p.next = null; // help GC
failed = false;
return interrupted;
}
//线程A 可能还没释放锁,使得 线程B 在执行 tryAcquire 时会返回 false
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;//并且返回当前线程在等待过程中有没有中断过。
}
} finally {
//取消获得锁的操作
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire()
如果在争抢锁失败之后,是否要挂起线程。SIGNAL = -1
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果上一个节点的状态为-1 但是ws此时的状态为0
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
//如果ws>0 状态表示 CANCELLED 1
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
//无限循环去移除掉该节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//所以走最后一个逻辑 将节点的pred节点 状态 修改为 SIGNAL
/*
* 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;
}
经过两次循环,线程B进入和线程C进入,都将pred节点的状态修改为SIGNAL(-1),最后的状态如下
parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
//挂起当前线程
LockSupport.park(this);
return Thread.interrupted();//返回当前线程的中断状态,并复位当前线程
}
java.util.concurrent.locks.LockSupport#park(java.lang.Object)
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
3.2 锁的释放流程
lock.unlock() 时序图
线程A执行完,执行lock.unlock()
方法释放锁
public void unlock() {
//释放
sync.release(1);
}
release()
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
//获取AQS中的head节点
Node h = head;
//如果此时的头节点为初始化的节点而且waitStatus = -1(SIGNAL)
if (h != null && h.waitStatus != 0)//如果 head 节点不为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease()
这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值(releases是 1),直到state状态为0,才返回true ; 因为在tryAcquire()
方法中(如果同一个线程多次lock.lock() 获取锁)state增加重入次数,所以只有线程对应的lock.unlock(),当state依次递减为0时,才会将对应的Owner线程设置为null,最后才会返回true。
protected final boolean tryRelease(int releases) {
//每次释放 重入次数-1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//当重入次数为0 表示无锁状态
if (c == 0) {
free = true;
//设置独占线程为null
setExclusiveOwnerThread(null);
}
//重新设置锁的状态
setState(c);
return free;
}
unparkSuccessor()
private void unparkSuccessor(Node node) {
//此时传入的头节点的状态为-1
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//将节点的状态替换为0 无锁状态
//拿到头节点的next节点 为 线程B 节点
Node s = node.next;
//如果节点为空或者状态>0(>0表示cancelled)
if (s == null || s.waitStatus > 0) {
s = null;
//从尾部遍历
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//找到状态小于等于0的节点 single状态
s = t;
}
if (s != null)//线程B节点 不为null 唤醒线程B
LockSupport.unpark(s.thread);
}
唤醒之后,线程B 会接着执行之前被阻塞的位置执行。
为何要从尾部遍历?
原因是enq()中的else后面的这一段代码
private Node enq(final Node node) {
for (;;) {//自旋
Node t = tail;
//如果tail节点为空
if (t == null) { // Must initialize
//初始化一个head节点,设置tail节点 = head节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
//表示AQS队列中已经存在节点
//设置当前节点的上一个节点为tail节点
node.prev = t;
if (compareAndSetTail(t, node)) {//cas设置node节点为tail节点
t.next = node;//cas成功 设计tail节点的下一个节点为node
return t;
}
}
}
}
新加入AQS队列的节点先执行node.prev = t;
后执行compareAndSetTail(t, node)
成功之后才会设置原tail节点的next节点为node节点;如果在这个中间有其他线程释放锁,从前往后遍历,由于tail节点的next = node
这一步还没有执行,会漏洞这一个节点,但是从后往前遍历就没有这个问题。
原来阻塞的线程重新开始执行
从AQS.acquireQueued()
这个方法重新开始执行,因为程序在这个方法里面的parkAndCheckInterrupt()
方法中阻塞线程,所以唤醒此方法的自旋开始。
...此处省略N行代码
for (;;) {//自旋
//获取当前节点的上一个节点
final Node p = node.predecessor();
//如果上一个节点是头节点,继续争抢锁
if (p == head && tryAcquire(arg)) {
//线程A 已经释放锁
//线程B 抢占锁成功以后,把获得锁的线程B节点设置为 head
setHead(node);
//移除原来的初始化 head节点
p.next = null; // help GC
failed = false;
return interrupted;
}
-
自旋直到当前Node的上一个节点为head节点
-
tryAcquire(arg) 竞争锁成功
-
设置新的头节点为当前Node 线程B
private void setHead(Node node) {
head = node;//设置AQS的head节点为node
node.thread = null;//设置node的线程为null
node.prev = null;//设置node节点的上一个节点为null
}
- 设置原来的head节点的next节点为null
通过这种方式将原来的头节点从AQS队列中剔除
3.3 公平锁和非公平锁的区别
lock方法
公平锁 FairSync
final void lock() {
acquire(1);
}
非公平锁 NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平锁上来,先通过CAS
来抢占锁
tryAcquire方法
公平锁 FairSync
公平锁先进行了!hasQueuedPredecessors()
的判断,判断AQS的前驱节点(第一个线程封装的节点)是否是当前线程
而非公平锁 NonfairSync 直接通过CAS
来抢占锁
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()
判断在AQS队列中head节点的next节点是否为null
如果不为null,这个节点是否等于当前线程节点
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
4.参考
腾讯课堂->咕泡学院->mic老师->并发编程基础