ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 它是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
代码示例
public class MyService {
private Lock lock = new ReentrantLock();
//Lock lock=new ReentrantLock(true);//公平锁
//Lock lock=new ReentrantLock(false);//非公平锁
private Condition condition=lock.newCondition();//创建 Condition
public void testMethod() {
try {
lock.lock();//lock 加锁
//1: wait 方法等待:
//System.out.println("开始 wait");
condition.await();
//通过创建 Condition 对象来使线程 wait,必须先执行 lock.lock 方法获得锁
//:2: signal 方法唤醒
condition.signal();//condition 对象的 signal 方法可以唤醒 wait 线程
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName=" +
Thread.currentThread().getName()+ (" " + (i + 1)));
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
ReentrantLock源码分析
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronizer(AQS)。AQS使用一个整型的volatile变量(命名为state)来维护同步状态。它支持公平锁和非公平锁,两者的实现类似。AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus。
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。
参考并发编程——详解 AQS CLH 锁
非公平锁NonfairSync lock()的过程:final void lock() {
if (compareAndSetState(0, 1))//CAS操作,若state为0则将其设为1
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
获取锁失败进入acquire(1):
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg):第一步:尝试去获取锁。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();//获取state变量值
if (c == 0) { //没有线程占用锁 :非公平锁的特点
if (compareAndSetState(0, acquires)) {//占用锁成功
setExclusiveOwnerThread(current);//设置独占线程为当前线程
return true;
}
} else if (current == getExclusiveOwnerThread()) { //当前线程已经占用该锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新state值为新的重入次数
return true;
}
return false; //获取锁失败
}
非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 第二步:获取锁失败则入队。addWaiter(Node.EXCLUSIVE)将新节点和当前线程关联并且入队列:
private Node addWaiter(Node mode) {
//初始化节点,设置关联线程和模式(独占 or 共享)
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail; // 获取尾节点引用
if (pred != null) {// 尾节点不为空,说明队列已经初始化过
node.prev = pred;
if (compareAndSetTail(pred, node)) {//CAS,设置新节点为尾节点
pred.next = node;
return node;
}
}
enq(node); // 尾节点为空,说明队列还未初始化
return node;
}
private Node enq(final Node node) {
for (;;) {//开始自旋
Node t = tail;
if (t == null) { // 如果tail为空
if (compareAndSetHead(new Node()))//新建一个head节点
tail = head; //tail指向head
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {// tail不为空
t.next = node; //将新节点入队
return t;
}
}
}
}
acquireQueued(final Node node, int arg) 已经入队的线程尝试获取锁,若失败则会被挂起。
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; // 原head节点出队,在某个时间点被GC
failed = false; //获取成功
return interrupted; //返回是否被中断过
}
// 判断获取失败后是否可以挂起,若可以则挂起
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 线程若被中断,设置interrupted为true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//前驱节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前驱节点状态为signal,返回true
return true;
// 前驱节点状态为CANCELLED
if (ws > 0) {
// 从队尾向前寻找第一个状态不为CANCELLED的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// 挂起当前线程,返回线程中断状态并重置
return Thread.interrupted();
}
线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
非公平锁NonfairSync unlock()的过程:public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {//尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)//若头结点的状态是SIGNAL
unparkSuccessor(h);//唤醒头结点下一个节点的关联线程
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 计算释放后state值
// 如果不是当前线程占用锁,那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true; // 锁被重入次数为0,表示释放成功
setExclusiveOwnerThread(null); // 清空独占线程
}
setState(c); // 更新state值
return free;
}
tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。
公平锁和非公平锁公平锁和非公平锁释放时,最后都要写一个volatile变量state
公平锁获取时,首先会去读volatile变量,若为0,按队列顺序获取锁
非公平锁获取时,首先会用CAS更新volatile变量,若为0,当前线程可直接抢占
tryLock():线程获取锁失败后,先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试。
ReentrantReadWriteLock 源码分析ReentrantReadWriteLock包含两个内部类: ReadLock和WriteLock,获取锁和释放锁都是通过AQS来实现的。AQS的状态state是32位的,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁低16位,表示写锁的重入次数(exclusiveCount)。
线程进入读锁的前提条件:(共享锁) 没有其他线程的拥有写锁, 没有写请求或者有写请求,但调用线程和持有读锁的线程是同一个。 线程进入写锁的前提条件:(排他锁/独占锁) 没有其他线程的读锁 没有其他线程的写锁 读写锁有以下三个重要的特性: 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。 重进入:读锁和写锁都支持线程重进入。 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。 获取写锁的步骤:(1)判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取锁,执行(2);否则执行(5)。
(2)若读锁此时被其他线程占用,或其他线程获取写锁,则返回false,当前线程不能获取写锁。
(3)若当前线程获取写锁超过最大次数,抛异常,否则更新同步状态,返回true。
(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
释放写锁的步骤:(1)查看当前线程是否为写锁的持有者,如果不是抛出异常。
(2)检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
获取读锁的步骤:(1)若写锁线程数 != 0 ,且独占锁不是当前线程,则返回失败;
(2)否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且CAS设置状态;
(3)若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程就是第一个读线程,则为重入,增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。
释放读锁的步骤:(1)判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;
(2)若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。
总结:在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
写锁可以“降级”为读锁;读锁不能“升级”为写锁。
下一篇
JUC知识点总结(四)五种单例模式的写法