6.1 LockSupport工具类
JDK中的rt.jar包里面的LockSupport是个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础。LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的。下面介绍LockSupport中的几个主要函数:
/**
* 如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被
* 禁止参与线程的调度,也就是会被阻塞挂起。在其他线程调用unpark(Thread thread)方法并且将当前线程作为参数时,调用park
* 方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者线程被虚假唤醒,
* 则阻塞线程也会返回。所以在调用park方法时最好也使用循环条件判断方式。需要注意的是,因调用park()方法而被阻塞的线程
* 被其他线程中断而返回时并不会抛出InterruptedException异常。
*
* 注意:
* 如果thread之前没有调用park,则调用unpark方法后调用park方法,其会立刻返回,再次调用park方法时会被阻塞
* 如果thread之前没有调用park,在调用interrupt()方法后调用park方法会立刻返回不进行阻塞,再次调用park方法时依然不会被阻塞
* 该方法底调用了UNSAFE.park(false, 0L),UNSAFE.park(false, 0L)方法底层C语言实现代码中进行了判断,如果当前线程设置了中断标志,调用park则直接返回,所以如果在park之前调用了interrupt就会直接返回
*/
void park();
/**
* 当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联的许可证,则让thread线程持有。如果thread
* 之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。
*
* @param thread
*/
void unpark(Thread thread);
6.2 抽象同步队列AQS概述
6.2.1 AQS——锁的底层支持
AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。
AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node。其中Node中的thread变量用来存放进入AQS队列里面的线程;Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点); prev记录当前节点的前驱节点,next记录当前节点的后继节点。
在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数;对于semaphore来说,state用来表示当前可用信号的个数;对于CountDownlatch来说,state用来表示计数器当前的值。
AQS有个内部类ConditionObject,用来结合锁实现线程同步。 ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程,如类图所示,这个条件队列的头、尾元素分别为firstWaiter和lastWaiter。
对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。
-
在独占方式下获取和释放资源使用的方法为:
void acquire(int arg)
void acquireInterruptibly(int arg)
boolean release(int arg)
使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。比如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现它就是锁的持有者,则会把状态值从1变为2,也就是设置可重入次数,而当另外一个线程获取锁时发现自己并不是该锁的持有者就会被放入AQS阻塞队列后挂起。
在独占方式下,获取与释放资源的流程如下:
(1)当一个线程调用acquire(int arg)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。
// AbstractQueuedSynchronizer.java public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
(2)当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。
// AbstractQueuedSynchronizer.java public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
需要注意的是,AQS类并没有提供可用的tryAcquire和tryRelease方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquire和tryRelease需要由具体的子类来实现。子类在实现tryAcquire和tryRelease时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。子类还需要定义,在调用acquire和release方法时state状态值的增减代表什么含义。 比如继承自AQS实现的独占锁ReentrantLock,定义当status为0时表示锁空闲,为1时表示锁已经被占用。在重写tryAcquire时,在内部需要使用CAS算法查看当前state是否为0,如果为0则使用CAS设置为1,并设置当前锁的持有者为当前线程,而后返回true,如果CAS失败则返回false。比如继承自AQS实现的独占锁ReentrantLock在实现tryRelease时,在内部需要使用CAS算法把当前state的值从1修改为0,并设置当前锁的持有者为null,然后返回true,如果CAS失败则返回false。
-
在共享方式下获取和释放资源的方法为:
void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
- ``boolean releaseShared(int arg)`
共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。比如Semaphore信号量,当一个线程通过acquire()方法获取信号量时,会首先看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。
在共享方式下,获取与释放资源的流程如下:
(1)当线程调用acquireShared(int arg)获取共享资源时,会首先使用tryAcquireShared尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.SHARED的Node节点后插入到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己。
// AbstractQueuedSynchronizer.java public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
(2)当一个线程调用releaseShared(int arg)时会尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用LockSupport.unpark(thread)激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquireShared查看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。
// AbstractQueuedSynchronizer.java public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
同样需要注意的是,AQS类并没有提供可用的tryAcquireShared和tryReleaseShared方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquireShared和tryReleaseShared需要由具体的子类来实现。子类在实现tryAcquireShared和tryReleaseShared时要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。 比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryAcquireShared时,首先查看写锁是否被其他线程持有,如果是则直接返回false,否则使用CAS递增state的高16位(在ReentrantReadWriteLock中,state的高16位为获取读锁的次数)。比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryReleaseShared时,在内部需要使用CAS算法把当前state值的高16位减1,然后返回true,如果CAS失败则返回false。
6.2.2 AQS——条件变量的支持
notify和wait,是配合synchronized内置锁实现线程间同步的基础设施一样,条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。 它们的不同在于,synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。 在调用共享变量的notify和wait方法前必须先获取该共享变量的内置锁,同理,在调用条件变量的signal和await方法前也必须先获取条件变量对应的锁。
-
await()方法
当线程调用条件变量的await()方法时(必须先调用锁的lock()方法获取锁),在内部会构造一个类型为Node.CONDITION的node节点,然后将该节点插入条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的state变量的值),并被阻塞挂起。这时候如果有其他线程调用lock.lock()尝试获取锁,就会有一个线程获取到锁,如果获取到锁的线程调用了条件变量的await()方法,则该线程也会被放入条件变量的阻塞队列,然后释放获取到的锁,在await()方法处阻塞。
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 创建新的node节点,并插入到条件队列末尾 Node node = addConditionWaiter(); // 释放当前线程获取的锁 int savedState = fullyRelease(node); int interruptMode = 0; // 调用park方法阻塞挂起当前线程 while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; if (node.nextWaiter != null) // clean up if cancelled unlinkCancelledWaiters(); if (interruptMode != 0) reportInterruptAfterWait(interruptMode); }
-
signal()方法
当另外一个线程调用条件变量的signal方法时(必须先调用锁的lock()方法获取锁),在内部会把条件队列里面队头的一个线程节点从条件队列里面移除并放入AQS的阻塞队列里面,然后激活这个线程。
public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) // 将条件队列投元素移动到AQS队列 doSignal(first); }
需要注意的是,AQS只提供了ConditionObject的实现,并没有提供newCondition函数,该函数用来new一个ConditionObject对象。需要由AQS的子类来提供newCondition函数。
当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列(双向链表)里面,并做自旋CAS尝试获取锁。如果获取到锁的线程又调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面(单向队列)。
当另外一个线程调用条件变量的signal()或者signalAll()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。
一个锁对应一个AQS阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
6.2.3 基于AQS实现自定义同步器
package com.ww.useAqs;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 基于AQS实现的不可重入的独占锁
*
* @author: Sun
* @create: 2020-05-12 15:51
* @version: v1.0
*/
public class NonReentrantLock implements Lock, Serializable {
// 内部帮助类
private static class Sync extends AbstractQueuedSynchronizer {
// 是否锁已经被持有
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 如果state为0 则尝试获取锁
@Override
protected boolean tryAcquire(int acquires) {
assert acquires == 1;
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 尝试释放锁 设置state为0
@Override
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
Condition newCondition() {
return new ConditionObject();
}
}
// 创建一个Sync来做具体的工作
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
public boolean isLocked() {
return sync.isHeldExclusively();
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
package com.ww.useAqs;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Condition;
/**
* 使用自定锁实现生产-消费模型
* @author: Sun
* @create: 2020-05-12 15:48
* @version: v1.0
*/
public class UseNonReentrantLock {
final static NonReentrantLock lock = new NonReentrantLock();
final static Condition full = lock.newCondition();
final static Condition empty = lock.newCondition();
final static Queue<Integer> queue = new LinkedBlockingQueue<>();
final static int queueSize = 10;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
// 获取独占锁
lock.lock();
try {
Integer count = 0;
while (true) {
if (queue.size() == queueSize) {
// 如果队列满了,唤醒消费线程后自身阻塞挂起
System.out.println("队列满,唤醒消费线程后自身阻塞挂起...queue count:" + queue.size());
empty.signalAll();
full.await();
} else {
// 添加元素到队列
queue.add(count++);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
});
Thread consumer = new Thread(() -> {
// 获取独占锁
lock.lock();
try {
while (true) {
if (0 == queue.size()) {
// 队列为空 唤醒生产线程自身阻塞挂起
System.out.println("队列空,唤醒生产线程自身阻塞挂起...queue count:" + queue.size());
full.signalAll();
empty.await();
} else {
// 消费一个元素
Integer count = queue.poll();
System.out.println(count);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
}
});
producer.start();
consumer.start();
}
}
6.3 独占锁ReentrantLock的原理
6.3.1 类图结构
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取该锁,其他获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列里面。
从类图可以看到,ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁。
Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平与公平策略。
在ReentrantLock,AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数。在该线程释放该锁时,会尝试使用CAS让状态值减1,如果减1后状态值为0,则当前线程释放该锁。
6.3.2 获取锁
-
非公平锁
// 非公平锁代码 // ReentrantLock.lock() public void lock() { sync.lock(); } // ReentrantLock.NonfairSync.lock() final void lock() { // (1)CAS设置状态值 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // (2)调用AQS的acquire方法 acquire(1); } // AbstractQueuedSynchronizer.acquire() public final void acquire(int arg) { // (3)调用ReentrantLock重写的tryAcquire方法,如果返回false则会把当前线程放入AQS阻塞队列 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // ReentrantLock.NonfairSync.tryAcquire() protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } // ReentrantLock.NonfairSync.nonfairTryAcquire() final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // (4)当前AQS状态值为0 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // (5) 当前线程是该锁持有者 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
在代码(1)中,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1, CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread设置该锁持有者是当前线程。如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。注意,传递参数为1,这里再贴下AQS的acquire的核心代码。
AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化,所以这里代码(3)会调用ReentrantLock重写的tryAcquire方法。我们先看下非公平锁的代码。
首先代码(4)会查看当前锁的状态值是否为0,为0则说明当前该锁空闲,那么就尝试CAS获取该锁,将AQS的状态值从0设置为1,并设置当前锁的持有者为当前线程然后返回,true。如果当前状态值不为0则说明该锁已经被某个线程持有,所以代码(5)查看当前线程是否是该锁的持有者,如果当前线程是该锁的持有者,则状态值加1,然后返回true,这里需要注意,nextc<0说明可重入次数溢出了。如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。
非公平锁的非公平是如何体现的?
首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁。这里假设线程A调用lock()方法时执行到nonfairTryAcquire的代码(4),发现当前状态值不为0,所以执行代码(5),发现当前线程不是线程持有者,则执行代码(6)返回false,然后当前线程被放入AQS阻塞队列。这时候线程B也调用了lock()方法执行到nonfairTryAcquire的代码(4),发现当前状态值为0了(假设占有该锁的其他线程释放了该锁),所以通过CAS设置获取到了该锁。明明是线程A先请求获取该锁呀,这就是非公平的体现。这里线程B在获取锁前并没有查看当前AQS队列里面是否有比自己更早请求该锁的线程,而是使用了抢夺策略。
-
公平锁
// ReentrantLock.FairSync.tryAcquire() protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // (7)当前AQS状态值为0 if (c == 0) { // (8)公平性策略 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // (9) 当前线程是该锁持有者 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
公平的tryAcquire策略与非公平的类似,不同之处在于,代码(8)在设置CAS前添加了hasQueuedPredecessors方法,该方法是实现公平性的核心代码。
6.3.3 释放锁
尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。释放锁之后会将AQS阻塞队列中的首个阻塞线程唤醒。
// ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
// AbstractQueuedSynchronizer.release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// (11) 如果不是锁持有者调用unlock()则直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// (12)如果当前可重入次数为0,则清空锁持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 重置可重入次数(原可重入次数 - 1)
setState(c);
return free;
}
6.3.4 案例介绍
package com.ww.useAqs;
import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
/**
* 使用ReentrantLock来实现一个简单的线程安全的list
* 在操作array元素前进行加锁保证同一时间只有一个线程可以对array数组进行修改,但是也只能有一个线程对
* array元素进行访问。
*
* @author: Sun
* @create: 2020-05-14 14:07
* @version: v1.0
*/
public class UseReentrantLock {
private ArrayList<String> stringArrayList = new ArrayList<>();
private volatile ReentrantLock lock = new ReentrantLock();
// 添加元素
public void add(String e) {
lock.lock();
try {
stringArrayList.add(e);
} finally {
lock.unlock();
}
}
// 删除元素
public void remove(String e) {
lock.lock();
try {
stringArrayList.remove(e);
} finally {
lock.unlock();
}
}
// 获取数据
public String get(int index) {
lock.lock();
try {
return stringArrayList.get(index);
} finally {
lock.unlock();
}
}
}
假如线程Thread1、Thread2和Thread3同时尝试获取独占锁ReentrantLock,假设Thread1获取到了,则Thread2和Thread3就会被转换为Node节点并被放入ReentrantLock对应的AQS阻塞队列,而后被阻塞挂起。
假设Thread1获取锁后调用了对应的锁创建的条件变量1,那么Thread1就会释放获取到的锁,然后当前线程就会被转换为Node节点插入条件变量1的条件队列。由于Thread1释放了锁,所以阻塞到AQS队列里面的Thread2和Thread3就有机会获取到该锁,假如使用的是公平策略,那么这时候Thread2会获取到该锁,从而从AQS队列里面移除Thread2对应的Node节点。
6.3.5 小结
ReentrantLock的底层是使用AQS实现的可重入独占锁。在这里AQS状态值为0表示当前锁空闲,为大于等于1的值则说明该锁已经被占用。该锁内部有公平与非公平实现,默认情况下是非公平的实现。另外,由于该锁是独占锁,所以某时只有一个线程可以获取该锁。
6.4 读写锁ReentrantReadWriteLock的原理
6.4.1 类图结构
解决线程安全问题使用ReentrantLock就可以,但是ReentrantLock是独占锁,某时只有一个线程可以获取该锁,而实际中会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁。
读写锁的内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。ReentrantReadWriteLock巧妙地使用state的高16位表示获取到读锁的次数
;使用低16位表示获取到写锁的线程的可重入次数
。
ReentrantReadWriteLock属性
Thread firstReader
用来记录第一个获取到读锁的线程int firstReaderHoldCount
则记录第一个获取到读锁的线程获取读锁的可重入次数HoldCounter cachedHoldCounter
用来记录最后一个获取读锁的线程获取读锁的可重入次数ThreadLocalHoldCounter readHolds
用来存放除去第一个获取读锁线程外的其他线程获取读锁的可重入次数
6.4.2 写锁的获取与释放
在ReentrantReadWriteLock中写锁使用WriteLock来实现。
-
void lock()
,写锁是个独占锁,某时只有一个线程可以获取该锁。如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前已经有线程获取到读锁和写锁,则当前请求写锁的线程会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取只是简单地把可重入次数加1后直接返回。// ReentrantReadWriteLock.WriteLock.lock() public void lock() { sync.acquire(1); } // AbstractQueuedSynchronizer.acquire() public final void acquire(int arg) { if (!tryAcquire(arg) && // 添加节点到阻塞队列中 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // ReentrantReadWriteLock.Sync.tryAcquire() protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); // (1)c != 0说明读锁或者写锁已经被某线程获取 if (c != 0) { // (2) w == 0说明已经有线程获取了读锁,w != 0并且当前线程不是写锁拥有者,则返回false if (w == 0 || current != getExclusiveOwnerThread()) return false; // (3) 说明当前线程获取到了写锁 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); // Reentrant acquire // (4) 设置写锁可重入次数 setState(c + acquires); return true; } // (5) 如果AQS的状态值等于0说明当前没有线程获取到读锁或写锁,所以直接获取写锁 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; }
-
void lockInterruptibly()
,类似于lock()方法,它的不同之处在于,它会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出异常InterruptedException异常。// ReentrantReadWriteLock.WriteLock.lockInterruptibly() public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
-
boolean tryLock()
,尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功,然后返回true。如果当前已经有其他线程持有写锁或者读锁则该方法直接返回false,且当前线程并不会被阻塞。如果当前线程已经持有了该写锁则简单增加AQS的状态值后直接返回true。// ReentrantReadWriteLock.WriteLock.tryLock() public boolean tryLock( ) { return sync.tryWriteLock(); } // ReentrantReadWriteLock.WriteLock.tryWriteLock(),与tryAcquire方法类似 final boolean tryWriteLock() { Thread current = Thread.currentThread(); int c = getState(); if (c != 0) { int w = exclusiveCount(c); if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w == MAX_COUNT) throw new Error("Maximum lock count exceeded"); } if (!compareAndSetState(c, c + 1)) return false; setExclusiveOwnerThread(current); return true; }
-
void unlock()
,尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁而调用了该方法则会抛出IllegalMonitorStateException异常。// ReentrantReadWriteLock.WriteLock.unlock() public void unlock() { sync.release(1); } // AbstractQueuedSynchronizer.release() public final boolean release(int arg) { // 调用ReentrantReadWriteLock中sync实现的tryRelease方法 if (tryRelease(arg)) { // 激活阻塞队列里面的一个线程 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } // ReentrantReadWriteLock.Sync.tryRelease() protected final boolean tryRelease(int releases) { // (6) 判断是否为写锁拥有者线程调用的unlock if (!isHeldExclusively()) throw new IllegalMonitorStateException(); // (7) 获取写锁可重入值 int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; // (8) 如果写锁可重入值为0则释放锁,否则只是简单的更新状态值 if (free) setExclusiveOwnerThread(null); setState(nextc); return free; }
6.4.3 读锁的获取与释放
ReentrantReadWriteLock中的读锁是使用ReadLock来实现的。
-
void lock()
,获取读锁。如果当前没有其他线程持有写锁,则当前线程可以获取读锁(当前线程持有写锁后还可以持有读锁)。AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,则当前线程会被阻塞。// ReentrantReadWriteLock.ReadLock.lock() public void lock() { sync.acquireShared(1); } // AbstractQueuedSynchronizer.acquireShared() public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) // 添加节点到阻塞队列中 doAcquireShared(arg); } // ReentrantReadWriteLock.Sync.tryAcquireShared() protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); // (1) 获取当前状态值 int c = getState(); // (2) 判断写锁是否被占用 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // (3) 获取读锁可重入数量 int r = sharedCount(c); // (4) 尝试获取锁,多个线程只有一个会成功,不成功的进入fullTryAcquireShared进行重试。如果readerShouldBlock返回true则说明有线程正在获取写锁,所以执行代码(8)。fullTryAcquireShared的代码与tryAcquireShared类似,它们的不同之处在于:前者通过循环自旋获取。 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // (5) 记录第一个获取读锁的线程并统计该线程获取读锁的可重入数 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; // (6) 如果当前线程是第一个获取读锁的线程 } else if (firstReader == current) { firstReaderHoldCount++; } else { // (7) 记录最后一个获取读锁的线程和该线程获取读锁的可重入数,readHolds记录了当前线程获取读锁的可重入数。 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; } return 1; // (8) 如果readerShouldBlock()返回true则说明有线程正在获取写锁。类似tryAcquireShared,但是是自旋获取 return fullTryAcquireShared(current); }
-
void lockInterruptibly()
,类似于lock()方法,不同之处在于,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,当前线程会抛出InterruptedException异常。 -
boolean tryLock()
,尝试获取读锁,如果当前没有其他线程持有写锁,则当前线程获取读锁会成功,然后返回true。如果当前已经有其他线程持有写锁则该方法直接返回false,但当前线程并不会被阻塞。如果当前线程已经持有了该读锁则简单增加AQS的状态值高16位后直接返回true。 -
void unlock()
,释放锁。// ReentrantReadWriteLock.ReadLock.unlock() public void unlock() { sync.releaseShared(1); } // AbstractQueuedSynchronizer.releaseShared() public final boolean releaseShared(int arg) { // tryReleaseShared()返回true表示没有线程占用读锁,doReleaseShared()释放一个由于获取写锁而被阻塞的线程 if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } // ReentrantReadWriteLock.Sync.tryReleaseShared() protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } // 循环知道自己的读计数-1,CAS更新成功 for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // Releasing the read lock has no effect on readers, // but it may allow waiting writers to proceed if // both read and write locks are now free. return nextc == 0; } } // ReentrantReadWriteLock.ReadLock.fullTryAcquireShared() final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; // else we hold the exclusive lock; blocking here // would cause deadlock. } else if (readerShouldBlock()) { // Make sure we're not acquiring read lock reentrantly if (firstReader == current) { // assert firstReaderHoldCount > 0; } else { if (rh == null) { rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) return -1; } } if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) { if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; cachedHoldCounter = rh; // cache for release } return 1; } } }
6.5 JDK 8中新增的StampedLock锁探究
6.5.1 概述
StampedLock
是并发包里面JDK8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数时,会返回一个long型的变量,我们称之为戳记(stamp),这个戳记代表了锁的状态。其中try系列获取锁的函数,当获取锁失败后会返回为0的stamp值。当调用释放锁和转换锁的方法时需要传入获取锁时返回的stamp值。
StampedLock提供的三种读写模式的锁分别如下:
- 写锁writeLock: 是一个排它锁或者独占锁,某时只有一个线程可以获取该锁,当一个线程获取该锁后,其他请求读锁和写锁的线程必须等待,这类似于ReentrantReadWriteLock的写锁(不同的是这里的写锁是不可重入锁);当目前没有线程持有读锁或者写锁时才可以获取到该锁。**请求该锁成功后会返回一个stamp变量用来表示该锁的版本,当释放该锁时需要调用unlockWrite方法并传递获取锁时的stamp参数。并且它提供了非阻塞的tryWriteLock方法。
- 悲观读锁readLock: 是一个共享锁,在没有线程获取独占写锁的情况下,多个线程可以同时获取该锁。如果已经有线程持有写锁,则其他线程请求获取该读锁会被阻塞,这类似于ReentrantReadWriteLock的读锁(不同的是这里的读锁是不可重入锁)。这里说的悲观是指在具体操作数据前其会悲观地认为其他线程可能要对自己操作的数据进行修改,所以需要先对数据加锁,这是在读少写多的情况下的一种考虑。请求该锁成功后会返回一个stamp变量用来表示该锁的版本,当释放该锁时需要调用unlockRead方法并传递stamp参数。并且它提供了非阻塞的tryReadLock方法。
- 乐观读锁tryOptimisticRead: 它是相对于悲观锁来说的,在操作数据前并没有通过CAS设置锁的状态,仅仅通过位运算测试。如果当前没有线程持有写锁,则简单地返回一个非0的stamp版本信息。获取该stamp后在具体操作数据前还需要调用validate方法验证该stamp是否已经不可用,也就是看当调用tryOptimisticRead返回stamp后到当前时间期间是否有其他线程持有了写锁,如果是则validate会返回0,否则就可以使用该stamp版本的锁对数据进行操作。由于tryOptimisticRead并没有使用CAS设置锁状态,所以不需要显式地释放该锁。该锁的一个特点是适用于读多写少的场景,因为获取读锁只是使用位操作进行检验,不涉及CAS操作,所以效率会高很多,但是同时由于没有使用真正的锁,在保证数据一致性上需要复制一份要操作的变量到方法栈,并且在操作数据时可能其他写线程已经修改了数据,而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
锁之前的互斥关系:
- 写锁(独占锁):当没有线程获取写锁、悲观读锁时可获取;获取后其他线程无法再获取任何锁。
- 悲观读锁(共享锁):当没有线程获取写锁时可获取;获取后其他线程依旧可以获得悲观读锁、乐观读锁,但无法获取写锁。
- 乐观读锁(共享锁):当前没有线程获取写锁时可获取,获取后其他线程可以获得写锁、悲观读锁。
6.5.3 小结
StampedLock提供的读写锁与ReentrantReadWriteLock类似,只是前者提供的是不可重入锁。但是前者通过提供乐观读锁在多线程多读的情况下提供了更好的性能,这是因为获取乐观读锁时不需要进行CAS操作设置锁的状态,而只是简单地测试状态。