JUC 中的重入锁-ReentrantLock
作者:沈自在
1 Lock API
1.1 Lock API 有哪些提供方法?
位置: java.util.locks.Lock
Lock接口的作用其实与 synchronized类似,均是为了达到线程安全所提出的解决方案,在Lock中有以下几个方法:
public interface Lock {
void lock();
// 获取锁过程中可被中断 中断抛出 InterruptedException
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
// 返回绑定当前锁的 Condition 实例
Condition newCondition();
}
1.2 有哪些实现?
主要有以下几个:
ReentrantLock可重入锁的典范,支持公平与非公平锁(默认非公平),内部核心类Sync继承自AQSReentrantReadWriteLock可重入的读写锁,该类并没有实现Lock,其内部的ReadLock和WriteLock才是实现源(默认非公平)StampedLock对ReentrantReadWriteLock的增强,在原先读写锁的基础上新增了乐观读模式,因此提供了更高的性能
2 ReentrantLock
2.1 简单实践一下
2.1.1 可重入锁的使用范式
public class ReentrantLockTest {
static Lock lock = new ReentrantLock();
public void testLock(){
lock.lock();
try {
// omitted code..
}finally {
lock.unlock();
}
}
}
2.1.2 Java自增原子吗?
众所周知,Java中的自增操作并不是原子操作
public void getNext();
Code:
: aload_0
: dup // 将当前栈顶的对象引用复制一份
: getfield #2; // 获取id的值,并将其值压入栈顶
: iconst_1 // 将int型的值1压入栈顶
: iadd // 将栈顶两个int类型的元素相加,并将其值压入栈顶
: putfield #2; // 将栈顶的值赋值给id
: return
所以Java的自增操作其实是分为三步的:取值 -> 加一 ->赋值 ,因此并非原子
2.1.3 一个一定出错的案例
那么这里就要引入一个老生常谈的案例——多线程自增出错,这个案例在后续的文章中也会频繁出现。
// 开启 1000 个线程,每个线程自增1000次, 理论值 1000000
public class ReentrantLockTest {
int value;
int getValue() {
return value;
}
void increment() {
++value;
}
@Test
void reentrant_lock_test() {
ReentrantLockTest reentrantLockTest = new ReentrantLockTest();
for (int i = 0; i < 1000; ++i) {
new Thread(() -> {
for (int j = 0; j < 1000; ++j) {
reentrantLockTest.increment();
}
}).start();
}
// 等待 7s
LockSupport.parkNanos(7000000000L);
System.out.println("reentrantLockTest.getValue() = "
+ reentrantLockTest.getValue());
}
}
上面这个简单的案例的结果不用多说肯定是小于理论值的,接下来我们就用ReentrantLock来实践去解决这个问题
2.1.4 可重入锁改造代码
我们对increment()方法进行简单的加锁改造(套用2.1.1的使用范式)
void increment() {
lock.lock();
try {
++value;
}finally {
lock.unlock();
}
}
我们不难得到一个稳如老狗的值
2.2 这玩意咋实现的?
2.1.1 结构起手
简单介绍一下:
可重入锁ReentrantLock其核心点在于内部的Sync(其实是AbstractQueuedSynchronizer的派生),同时基于Sync 提供了公平和非公平俩种实现。
所以从这里可以知道该锁的核心点其实就是 AQS,接下来,我们就接着探讨一下什么是AQS
2.1.2 啥玩意儿是AQS呢?
AbstractQueuedSynchronizer(简称AQS)是ReentrantLock实现锁同步的核心类,实际上在J.U.C中大部分组件都依赖于AbstractQueuedSynchronizer。
来自何方?
AbstractQueuedSynchronizer 继承自AbstractOwnableSynchronizer,在该类中提供了基础的信息维护操作。
如:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
那么对于AQS,它究竟提供了什么?其实是俩种锁的实现:
- 独占锁:同一时刻只允许有一个线程获得锁。
- 共享锁,同一时刻允许多个线程同时获得锁。
这里比较关键的一点是 state的值。其中 state = 0 表示当前锁资源空闲。如果state > 0 表示已有锁占用。需要注意的是:
线程重入了多少次就需要释放多少次,这样才能 保证state的值最终为0
当然对于共享锁和独占锁这里也有着不同的实现:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
那么AQS是如何完成这样一个锁机制的呢?
2.1.3 Sync 的具体工作
Sync具有以下俩种实现
• NonfairSync,非公平锁,非公平锁的特点是允许在不排队的情况下直接尝试抢占锁, 默认使用非公平锁。
• FairSync,公平锁,必须按照FIFO的规则来访问锁资源。
// 下面是公平锁的获取方法,必须满足等待队列中没有线程才能获取锁
protected final boolean tryAcquire(int acquires) {
if (getState() == 0 && !hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
2.1.3.1 上锁
下面是一段 ReentrantLock 上锁代码:
隐含: state初始为0
@ReservedStackAccess
final boolean tryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}
可以简单的看出来,如果当前锁为空闲状态,那么直接会利用CAS进行赋值上锁,如果已经被上锁了,但是上锁的线程是自己就对state 加1赋值。
2.1.3.2 释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (getExclusiveOwnerThread() != Thread.currentThread())
throw new IllegalMonitorStateException();
boolean free = (c == 0);
if (free)
setExclusiveOwnerThread(null);
setState(c);
return free;
}
其实这个理念都一样啦,如果state = 0释放,否则减去一个release放回去。
2.1.4 整体来看 ReentrantLock
2.1.4.1 ReentrantLock中的lock()
public void lock() {
sync.lock();
}
2.1.4.2 如何给State赋值
这里用到的是Unsafe类的CAS操作 :
// AQS 中
// 这段代码的意思是,通过CAS乐观锁的方式来做比较并替换,如果当前内存中state的值 和预期值expect相等,则更新为update。如果更新成功则返回true,否则返回false。
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSetInt(this, STATE, expect, update);
}
// Unsafe 中
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
2.1.5 核心的上锁方法是什么?
最核心的上锁方法是 AQS中的
final int acquire(Node node, int arg, boolean shared,boolean interruptible, boolean timed, long time)方法
它,非常的长!!
参数解释:
- node:除非有重新获取条件,否则为null
- arg:获取参数
- shared:是否为共享模式
- timed:是否超时等待
- time:如果超市等待,那么超时时间是?
jdk8
2.1.5.1 获取?入队?
// acquire失败后尝试入队
public final void acquire(int arg) {
// Sync实现的tryAcquire
if (!tryAcquire(arg) &&
// 入队后反复阻塞和恢复进行tryAcquire直到成功 (EXCLUSIVE为独占状态)
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//入队
private Node addWaiter(Node mode) {
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) {
// 当前节点只想tail
node.prev = pred;
// cas 入队
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 初始化队列
enq(node);
return node;
}
2.1.5.2 入队之后怎么办?
// 通过自旋去抢占锁
// 当抢不到锁时,不能让线程一直自旋重试,如果竞争失败就调用 parkAndCheckInterrupt()方法阻塞当前线程。
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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.1.5.3 抢不到怎么办?
Node 具有五种状态:
- CANCELLED(1)如果在同步队列中等待的线程等待超时或被中断,那么需要从同步队 列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后 的结点将不会再变化。
- SIGNAL(-1)只要前置节点释放锁,就会通知标识为SIGNAL状态的后续节点的线程
- CONDITION(-2)
- PROPAGATE(-3) 在共享模式下,PROPAGATE状态的线程处于可运行状态。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFiledAcquired()方法的作用是检查当前节点的前置节点状态,如果是 SIGNAL,则表示可以放心地阻塞,否则需要通过compareAndSetWaitStatus修改前置节点的状态为SIGNAL。这么做的目的是确保在同步队列中每个等待的线程状态是正常的,否则就需要把非正常状态的节点移除。
如果shouldParkAfterFailedAcquire()方法返回true,则调用parkAndCheckInterrupt()方法挂 起当前线程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
2.1.6 一个完成的释放锁的流程
// 1. 你所调用的释放锁 API
public void unlock() {
sync.release(1);
}
// 2. 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;
}
// 3. ReentrantLock的释放操作
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;
}
// 4. 唤醒
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 如果下一个节点被取消则找最近的 waitStatus <= 0 的节点
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);
}
那么为什么这里一定要从队尾开始检索呢?
这和enq()方法有关系,在enq()方法的逻辑中,把一个新节点添加到链表中的逻辑如 下。
- 将新节点的prev指向tail。
- 通过CAS将tail设置为新节点,因为CAS是原子操作,所以能够保证线程的安全性。
- t.next=node,目的是设置原tail的next节点指向新节点。
如果在CAS操作之后、t.next=node操作之前,存在其他线程调用unlock()方法从head开始往后遍历,由于t.next=node还没执行,所以链表的关系还没有建立完整,就会导致遍历到t节点的时候被中断。而如果从tail往前遍历,就一定不会出现这个问题。