《Java并发编程之美》读书笔记
独占锁ReentrantLock的原理
类图结构
ReentrantLock是可重入的独占锁,同时只能有一个线程可以获取到该锁,其他获取该锁的线程会被阻塞返给到AQS阻塞队里面。
从类图看到,ReentrantLock最终还是基于AQS来实现的,并且能够根据参数来决定其内部是一个公平锁还是非公平锁,默认是非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
其中Sync类直接继承自AQS,它的子类NonfairSync和FairSync分别实现了获取锁的非公平策略。
在这里,AQS的状态值state表示该线程获取锁的可重入次数,在默认情况下,state的指表示当前锁没有被任何线程持有,当一个线程第一次获取该锁时会尝试使用CAS设置state的状态值为1,如果CAS成功则当前线程获取了该锁,单后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2,这就是可重入次数,在该线程释放该所时,会尝试使用CAS让状态值为1,如果减1后状态值为0,则当前线程释放该锁。
获取锁
1.void lock()方法
当一个线程调用该方法时候,说明该线程希望获取该锁,如果锁当前没有被其他线程占用并且当前线程之前没有获取过该锁,则当前线程会获取该锁,然后设置当前锁的拥有者为当前线程,并且AQS的状态值为1,然后直接返回,如当前线程之前已经获取过该锁,则这次简单的把AQS的状态值加1.如果该锁已经被其他线程所拥有的,则调用该方法的线程会被放入到AQS阻塞队列阻塞挂起。
public void lock() {
sync.acquire(1);
}
在如上的代码中,ReentrantLock的lock()委托给了sync类,根据创建的ReentrantLock构造函数选择实现的是NonfairSync还是FairSync,这个锁是一个非公平锁还是公平锁。这里先看sync子类NonfairSync的情况也就是非公平锁
非公平锁
final void lock(){
//1.CAS设置当前值
if(compareAndSet(0,1)){
setExclusiveOwnerThread(Thread.currentThread);
}else{
//2.调用AQS的acquire方法
acquire(1);
}
}
在代码1中,因为默认AQS的状态值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread设置该锁的拥有者为当前线程。
如果这时候有其他线程调用lock方法企图获取该锁,CAS会失败,然后会调用AQS的acquire方法。注意,传递参数为1。
public final void acquire(int arg) {
//调用ReentrantLock重写的tryAcquire方法。
if (!tryAcquire(arg) &&
//tryAcquire返回false会把当前线程放入AQS阻塞队列里面
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化,所以这里代码3会调用ReentrantLock自己重写的tryAcquire方法。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
@ReservedStackAccess
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;
}
}
//5当前线程是否是该锁的持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}//6
return false;
}
首先代码4会查看当前锁的状态是否为0,为0则说明当前锁空闲,那么就尝试CAS获取该锁,将AQS的状态值从0设置为1,并设置当前锁的持有者为当前线程然后但会true,如果当前状态不为0则说明该锁已经被某个线程所持有,所以代码首先判断当前线程是否为该锁的持有者,如果是则状态值加1,然后返回true,这里需要注意,nextc<0代表可重入次数溢出了。如果当前线程不是锁的持有者则返回false,然后其会被放入AQS阻塞队列。
介绍完了非公平锁的实现代码,再来关注非公平在这里是如何实现的。
首先非公平锁是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁
这里假设线程A调用了lock方法执行到nonfairTryAcquire的代码4,发现当前状态值不为0则执行代码5,发现当前线程不是线程的持有者,则执行代码6返回false,然后线程A被放入AQS阻塞队列。
这时候线程B也调用了lock()方法执行到nonfairTryAcquire的代码4方法,发现当前状态值为0了(假设占有该锁的其他线程释放了该锁),所以通过CAS设置获取到了该锁。明明是线程A先请求获取该锁的啊?这就是非公平的体现。
这里的线程B,在获取锁之前并没有查看当前AQS队列里面是否有比自己更早请求该锁的线程,而是采用了抢夺策略。
公平锁
@ReservedStackAccess
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;
}
}
//9.当前线程是该锁持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如上的代码所示,公平锁tryAcquire策略与非公平锁类似,不同之处策略,代码8在设置CAS之前添加了hasQueuedPredecessors方法,该方法是实现公平性的核心代码
public final boolean hasQueuedPredecessors() {
Node t=tail;
Node h=head;
Node s;
return h!=t&&((s=h.text)==null||s.thread!=Thread.currentThread());
}
在如上的代码中,如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空或者当前线程节点是AQS第一个节点则返回false。如果h==t则说明当前AQS队列为空,直接返回false;如果h!=t并且s==null则说明有一个元素将要作为AQS的第一个节点加入队列(enq函数的第一个元素入队列是两步操作:首先常见一个哨兵头结点,然后将第一个元素插入哨兵节点后面),那么返回true,如果h!=t和s.thread!=Thread.currentThread()则说明队列里面的第一个元素不是当前线程,则返回true。
void lockInterruptibly()方法
这个方法与lock()方法类似,不同在于,它对中断进行响应,就是当前线程在调用该方法时,如果其他线程调用了当前线程的interrupt()方法,则当前线程会抛出中断异常InterruptedException异常,然后返回。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果当前线程被中断,则直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
boolean tryLock()方法
尝试获取锁,如果当前锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。这方法不会引起线程阻塞
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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);
return true;
}
return false;
}
boolean tryLock(long timeout,TimeUnit unit)
尝试获取锁,与tryLock()的不同之处在于,它设置了超时时间,如果超时时间没有获取到了该锁则返回false
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
释放锁 void unlock()方法
尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对持有AQS state状态值减1,如果减去1之后状态值变为1状态值为0,则当前线程会释放该锁,否则仅仅减1而已。如果当前线程没有持有该锁则会抛出IllegalMonitorStateException异常
public void unlock() {
sync.release(1);
}
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
//11.如果不是锁持有者调用unlock则抛出Unlock异常
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//12.如果当前的可重入次数为0,则从孔该锁的持有线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//13.设置可重入次数为原始值-1
setState(c);
return free;
}
如上代码所示,如果当前线程不是该锁持有者则直接抛出异常,否则查看状态值是否为0,为0则说明当前线程要放弃对该锁的持有权,则执行代码12把锁的持有者为null,如果状态值不为0,则仅仅让当前线程对该锁的可重入次数减1.
使用ReentrantLock实现一个简单的线程安全的list;
public static class ReentrantLockList {
//线程不安全的List
private ArrayList array=new ArrayList<>();
//独占锁
private volatile ReentrantLock lock=new ReentrantLock();
//添加元素
public void add(String e){
lock.lock();
try{
array.add(e);
}finally {
lock.unlock();
}
}
//删除元素
public void remove(String e){
lock.lock();
try{
array.remove(e);
}finally {
lock.unlock();
}
}
//获取数据
public String get(int index){
lock.lock();
try{
return array.get(index);
}finally {
lock.unlock();
}
}
}
如上代码通过操作array元素前进行加锁保证了同一时间只有一个线程可以对arry进行修改,但是也只能有一个线程对array元素进行访问。
如图所示,假如线程Thread1,Thread2,Thread3同时尝试获取独占锁ReentrantLock,假如Thread1获取到了,那么Thread2和Thread3就会被转换为Node 节点被放入ReentrantLock的AQS阻塞队列,而后被阻塞挂起。
如图所示,假设Thread1获取该锁了之后调用了对应锁创建的条件变量1 await()方法,那么Thread1就会释放获取到的锁,然后当前线程就会被转换为Node节点插入条件变量1的条件队列,由于Thread1释放了锁,锁以阻塞到AQS队列里面的Thread2和Thread3就有机会获取到所=锁,假如使用的公平策略,那么这时候Thread2会获取到该锁,从而从AQS队列里面一出Thread2对应的Node节点。
读写锁ReentrantReadWriteLock原理
类图结构
解决线程安全问题其实使用ReentrantLock就可以,但是ReentrantLock是独占锁,某时只有一个线程就可以获取该锁,而实际上会有写少读多的场景,显然ReentrantLock满足不了需求,所以ReentrantReadWriteLock应运而生。ReentrantReadWriteLock采用读写分离的策略,允许读个线程可以同时获取读锁。
读写锁的内部维护了一个ReadLock和WriteLock,他们依赖Sync实现具体的功能,而Sync继承自AQS,并且也提供了公平锁和非公平锁的实现。我们知道AQS中值维护了一个state状态,一个state怎么表示读和写两种状态呢?ReentrantReadWriteLock巧妙的运用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示获取到写锁的线程的可重入次数。
static final int SHARED_SHIFT = 16;
//读锁(共享锁)状态单位指65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁(共享锁)线程最大的个数65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//写锁(排它锁)掩码,二进制 15个1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//返回读锁线程数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//返回写锁可重入个数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
写锁的获取与释放
在ReentrantReadWriteLock中写锁使用writeLock来实现
void lock()
写锁是一个独占锁,只有一个线程可以获取资源。如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前已有线程获取读锁或者写锁,则当前请求写锁的线程就会被阻塞挂起。另外,写锁是可重入锁,如果当前线程已经获取了该锁,再次获取知识简单的把可重入次数加1然后直接返回。
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
//syn重写tryAcquire方法
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
非公平写锁的lock内部调用了AQS的acquire的方法,其中tryAcquire是ReentrantReadWriteLock内部类sync类重写的。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
//1.说明读锁或者写锁已经被谋线程获取
if (c != 0) {
//2 w=0说明已经有线程获取了读锁,w!=0表示当前线程不是写锁拥有者
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//3.说明当前线程获取了写锁,判断可重入次数
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 4.设置可重入次数+1
setState(c + acquires);
return true;
}
//5.第一个写线程获取写锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
在代码1中,如果当前AQS的状态值不为0则说明当前已经有线程获取到了读锁或者写锁。在代码2中,如果w==0说明状态值的低16位为0,而AQS状态值不为0,则说明高16位不为0,这暗示已经有线程获取到读锁,所以直接返回false;
而如果w!=0则说明已有线程获取了写锁,在看当前线程是不是该锁的持有者,如果过不是就返回false;
执行到代码3说明当前线程之前已经获取了读写锁,所以判断该线程的可重入数是不是超过了最大值,是则抛出异常,否则执行代码4增加当前线程的可重入次数,然后返回true。
如果AQS的状态值为0则表示目前没有线程获取到读锁和写锁,所以执行代码5,抢占式执行CAS尝试获取写锁,获取成功则设置当前锁的持有者为当前线程并返回true,否则返回false;
公平锁的实现为:
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
这里还是使用hasQueuedPredecessors判断当前线程节点是否有前驱节点,如果有则当前线程放弃获取写锁的权利,直接返回false;
void lockInterruptibly()
类似于lock()方法,它的不同之处在于,他会对中断做出响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,其会抛出InterruptedException
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
Boolean trylock()
尝试获取写锁,如果当前没有其他线程持有写锁或者读锁,则当前线程获取写锁会成功,然后返回true,如果有返回false,但是当前线程并不会阻塞。如果当前线程已经持有了读写锁后则简单增加AQS的状态值后直接返回true。
public boolean tryLock() {
return sync.tryWriteLock();
}
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;
}
boolean tryLock(long timeout, TimeUnit unit)
与tryAcquire的不同之处在于,多了超时时间参数,如果尝试获取写锁失败则会把当前线程挂起指定的时间,带到超时时间到后当前线程被激活,如果还是没有获取到写锁则返回false。另外,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,其会抛出InterruptedException
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
void unlock()
尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的AQS状态值减1,如果减去1后当前状态值为0则当前线程会释放该锁,否则仅仅是减1而已。如果当前线程没有持有该锁而调用了这个方法则会抛出IllegalMonitor
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//调用ReentrantReadWriteLock中sync实现的tryRelease方法
if (tryRelease(arg)) {
//激活阻塞队列里面的一个线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);//LockSupport里面的Unpark方法
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//看是否是写锁拥有者调用的unlock
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//获取可重入值,这里没有高16位,因为获取写锁时读锁的状态肯定为0
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
//如果写锁可重入值为0则释放锁,否则只是简单的更新状态值。
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁的获取与释放
ReentrantReadWriteLock中的读锁时使用ReadLock来实现的
void lock()
获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS状态指的高16位的值会增加1,然后方法返回,否则如果其他一个线程持有写锁,则当前线程就会被阻塞。
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
//调用ReentrantReadWriteLock中sync实现的tryAcquireShared方法。
if (tryAcquireShared(arg) < 0)
//调用AQS的doAcquireShared方法
doAcquireShared(arg);
}
在如上的代码中,读锁的lock方法调用了AQS的acquireShared方法,在其内部调用了ReentrantReadWriteLock中的sync重写的tryAcquireShared方法。
protected final int tryAcquireShared(int unused) {
//1.获取当前线程的状态值
Thread current = Thread.currentThread();
int c = getState();
//2.判断是否被写锁占用
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//3获取读锁计数
int r = sharedCount(c);
//4.尝试获取锁,读个读线程只有一个会成功,不成功的会进入fullTryAcquireShared进行重试
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.记录最后一个获取读锁的线程获记录其他线程读锁的可重入数。
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//类似于tryAcquireShared
return fullTryAcquireShared(current);
}
如上的代码首先获取了当前AQS的状态值,然后代码2查看是否有其他线程获取到了写锁,如果是则直接返回-1,而后调用AQS的doAcquireShared方法把当前线程放入AQS阻塞队列。
如果当前要获取读锁的线程之前已经持有了写锁,则也可以获取读锁,但是需要注意,一个线程先获取了写锁,然后获取了读锁处理事情完毕后,要记得把读锁和写锁一起释放掉,不能只释放写锁。
否则执行代码3,得到获取到的读锁的个数,到这里就说明没有线程获取到写锁,但是可能有线程持有读锁,然后执行代码4,其中非公平锁的readerShouldBlock实现代码
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
如上代码的作用是,如果队列里面存在一个元素,则判断第一个元素是不是正在尝试获取写锁,若不是,则当前线程判断当前获取读锁的线程是否已经达到了最大值,最后执行CAS操作将AQS状态值的高16位加1.
代码5,6记录第一个获取读锁的线程并统计该线程获取读锁的可重入数。代码7cachedHoldCounter记录最后一个获取到读锁的线程和该线程获取读锁的可重入数,readHolds记录了当前线程获取读锁的可重入数。
如果readerShouldBlock返回true则代表有线程正在获取写锁,所以执行代码8,fullTryAcquireShared和tryAcquireShared类似,但是fullTryAcquireShared是通过自选获取。
void lockInterruptibly()
类似于lock()方法,它的不同之处在于,他会对中断做出响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,其会抛出InterruptedException
boolean tryLock()
尝试获取写锁,如果当前没有其他线程持有写锁,则当前线程获取读锁会成功,然后返回true,如果有返回false,但是当前线程并不会阻塞。如果当前线程已经持有了读锁后则简单增加AQS的状态值高16位后直接返回true。
boolean tryLock(long timeout,TimeUnit unit)
与tryLock()的不同之处在于,多了超时时间参数,如果尝试获取读锁失败则会把当前线程挂起指定的时间,带到超时时间到后当前线程被激活,如果还是没有获取到读锁则返回false。另外,该方法会对中断进行响应,也就是当其他线程调用了该线程的interrupt()方法中断了当前线程时,其会抛出InterruptedException
void unlock()
public void unlock() {
sync.releaseShared(1);
}
如上代码具体释放锁的操作是委托给Sync类来做的
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//循环知道自己的读计数-1,CAS更新成功。
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
如以上代码所示,在无线循环里面,首先会获取当前的AQS状态值并将其保存到变量c,然后变量c被减去一个读计数单位后使用CAS去更新AQS的状态值,如果更新成功则查看当前的AQS的状态值是否为0,为0则说明当前已经没有读线程占用该锁,则tryReleaseShared返回try。然后会调用doReleaseShared方法释放一个由于获取写锁而被阻塞的线程,如果当前AQS的状态值不为0,则说明还有其他线程持有了读锁,所以tryReleaseShared返回false。如果tryReleaseShared中的CAS更新AQS状态值失败,则自旋重试直到成功。
基于ReentrantReadWriteLock实现线程安全的list
之前使用ReentrantLock实现线程安全的list,但是由于ReentrantLock是独占锁,所以在读多写少的情况下性能很差。
public class ReentrantReadWriteLockList {
//线程不安全的List
private ArrayList array=new ArrayList<>();
//独占锁
private volatile ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
private final Lock readLock=lock.readLock();
private final Lock writeLock=lock.writeLock();
//添加元素
public void add(String e){
writeLock.lock();
try{
array.add(e);
}finally {
writeLock.unlock();
}
}
//删除元素
public void remove(String e){
writeLock.lock();
try{
array.remove(e);
}finally {
writeLock.unlock();
}
}
//获取数据
public String get(int index){
readLock.lock();
try{
return array.get(index);
}finally {
readLock.unlock();
}
}
}
以上代码调用get的时候用的是读锁,这样运行多个读线程来同时访问list的元素,这在读多写少的情况下性能会更好。
参考资料:
《Java并发编程之美》