Java工程师知识树 / Java基础
Lock锁
java.util.concurrent.locks
包定义了Lock锁的使用与规范
常用的类或接口主要有ReentrantLock,ReentrantReadWriteLock,Condition
ReentrantReadWriteLock
所处位置:
java.util.concurrent.locks.ReentrantReadWriteLock
类结构:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable
类内部结构图
ReadWriteLock
一个ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。
ReadWriteLock读写锁允许多个线程同时读共享变量,但是只允许一个线程写共享变量,当写共享变量的时候也会阻塞读的操作。这样在读的时候就不会互斥,提高读的效率。
常用业务场景:多读少写。比如服务缓存等。
构造函数
ReentrantReadWriteLock类中带有两个构造函数,一个是默认的构造函数,不带任何参数;一个是带有 fair 参数的构造函数。与ReentrantLock一致。
public ReentrantReadWriteLock() {this(false);}//创建一个新的 ReentrantReadWriteLock与默认(非公平锁)
public ReentrantReadWriteLock(boolean fair) {//创建一个新的 ReentrantReadWriteLock与给定的公平政策。
sync = fair ? new FairSync() : new NonfairSync();//fair 为true是为公平锁,false时为非公平锁
readerLock = new ReadLock(this);//读锁
writerLock = new WriteLock(this);//写锁
}
结合类图结构分析:
读写锁ReentrantReadWriteLock的内部维护了一个ReadLock和对应的WriteLock,它们依赖Sync实现具体的功能。
而Sync继承自AQS,并且也提供了公平和非公平的实现。
AQS维护了一个state状态,而ReentrantReadWriteLock需要维护读状态与写状态。
一个state如何表示写和读两种状态呢?
ReentrantLock使用的是AQS中的一个同步状态state表示当前共享资源是否被其他线程锁占用。如果为0则表示未被占用,其他值表示该锁被重入的次数。
而ReentrantReadWriteLock巧妙地使用state的高16位表示读状态,也就是获取到读锁的次数;使用低16位表示写状态,也就是获取到写锁的线程的可重入次数。
当前状态表示一个线程已经获取了写锁,且重入了两次,同时也获取了两次读锁。那么读写锁是如何迅速确定读和写各自的状态那?答案就是”位运算” 。
如何通过位运算计算得出是读还是写获取到锁了那?
如果当前同步状态state不为0,那么先计算低16位写状态,如果低16为为0,也就是写状态为0则表示高16为不为0,也就是读状态不为0,则读获取到锁;如果此时低16为不为0则抹去高16位得出低16位的值,判断是否与state值相同,如果相同则表示写获取到锁。同样如果state不为0,低16为不为0,且低16位值不等于state,也可以通过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; }
Sync定义的一些其他变量
/** 用来保存持有持有共享锁的线程个数及线程id。*/
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/** ThreadLocalHoldCounter继承了ThreadLocal,因而initialValue方法返回一个Holdcounter对象。*/
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/** 是ThreadLocal变量,用来存放除去第一个获取读锁线程外的其他线程获取读锁的可重入次数。*/
private transient ThreadLocalHoldCounter readHolds;
/** 记录最后一个获取读锁的线程获取读锁的可重入次数*/
private transient HoldCounter cachedHoldCounter;
/** 记录第一个获取到读锁的线程*/
private transient Thread firstReader = null;
/** 记录第一个获取到读锁的线程获取读锁的可重入次数*/
private transient int firstReaderHoldCount;
主要方法源码分析
写锁的获取与释放
写锁获取
// WriteLock
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && //独占锁(写锁)获取
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))////排队竞争阶段
selfInterrupt();//Thread.currentThread().interrupt(); 当前线程中断
}
/**
* 独占锁(写锁)获取
*/
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c); // 因为读写锁最大支持 2的15次方-1;同时读写锁还有分段,对低十六位逻辑与,高16位置0
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
/**
* c!=0 && w==0表示有线程持有共享锁,就算是自己持有共享锁,也会阻塞
* 所以ReentrantReadWrLock不支持锁升级
* current != getExclusiveOwnerThread()表示,有其他线程持有独占锁
* 这两种情况都需要阻塞自己
*/
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//到这里说明是线程进行了重复获取锁,所以进行可重入处理,直接返回
setState(c + acquires);
return true;
}
/**
* 程序执行到这里说明是这个锁是首次被获取,所以要进行并发处理,使用CAS.
* 1.如果是公平锁,那么writerShouldBlock只允许队列头的线程获取锁
* 2.如果是非公平锁,不做限制,writerShouldBlock直接返回false
*/
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
tips:因为读写锁是将一个字节的高低16位分割开进行判断的,即state 大于0的时候,表明当前处在锁状态,那么这个锁可能是写锁,可能是读锁,需要分别判断读写锁的状态(位操作)
writerShouldBlock()这个函数很特别,即写锁是否被阻塞,实现根据FairSync与NonFaireSync分为两种,可以先提前猜想,公平模式下,写锁是否被阻塞应该怎样实现?对既然是公平模式,那么写锁一定要根据head后面是否有阶段来判断是否被阻塞,符合FIFO的结构;那么非公平模式呢?既然是非公平模式,那么写锁一定是不会被阻塞的;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
//非公平
final boolean writerShouldBlock() {
return false; // writers can always barge
}
写锁释放
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {//独占锁释放
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);// 唤醒下一个节点,触发 sun.misc.Unsafe#unpark
return true;
}
return false;
}
/**
* 独占锁释放
*/
protected final boolean tryRelease(int releases) {
// 非当前线程不能进行释放
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
//当state为0时,清除当前执行线程
if (free) //因为可重入,只有当exclusiveCount(nextc) == 0时候,才会真正释放锁
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
读锁的获取与释放
读锁获取
/**
* 读锁(共享锁)获取
*/
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
/**
* 已经有其他的线程持有了独占锁,让自己阻塞
* 但是如果是自己已经持有了独占锁,那么允许自己再获取共享锁
* ReentrantReadWriteLock的支持锁降级,但是不支持锁升级
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) {
return -1;
}
int r = sharedCount(c);
/**
* 1.通过公平/非公平的排队规则限制
* 2.读锁状态没有溢出
* 3.CAS设置同步状态成功 (去设置高16位)
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) { //第一次被线程获取
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { //更新首次获取锁线程HoldCount
firstReaderHoldCount++;
} else { //更新非首次获取锁线程的HoldCount
HoldCounter rh = cachedHoldCounter; //先查缓存
if (rh == null || rh.tid != getThreadId(current))
//缓存没有命中,从ThreadLocal中获取 且更新缓存
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//一次获取读锁失败后,尝试循环获取
return fullTryAcquireShared(current);
}
FairSync的readerShouldBlock
实现:
/**
* 对于公平锁来说,如果有前驱(也就是非头结点),都会进行等待,不允许竞争锁
*/
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
NoNofairSync的readerShouldBlock
实现:
final boolean readerShouldBlock() {
/**
* 由于非公平的竞争,并且读锁可以共享,所以可能会出现源源不断的读
* 使得写锁永远竞争不到,然后出现饿死的现象
* 所以通过这个策略,当一个写锁申请线程出现在头结点后面的时候
* 会立刻阻塞所有还未获取读锁的其他线程,让步给写线程先执行
*/
return apparentlyFirstQueuedIsExclusive();
}
在tryAcquireShared中经行了一次快速锁获取,但是由于CAS只能允许一个线程获取锁成功,且读锁是共享的,可能存在其他仍然可以获取锁的线程,所以在函数末尾调用函数fullTryAcquireShared来进行死循环的获取锁,这个函数很关键,代码分析如下:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
/**
* 如果是其他线程获取了写锁,那么把当前线程阻塞
* 如果是当前线程获取了写锁,不阻塞,否则会造成死锁
* 从这里可以看到ReentrantReadWriteLock允许锁降级
*/
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {
/**
* 进入这里说明,同步队列的头结点的后继有一个竞争写锁的线程
* 所以这里有一个锁让步的操作,即让写锁先获取
* 如果是firstReader必然是重入,或者rh.count>0也必然是重入
* 对于读锁重入是允许死循环直到获取锁成功的,不然会导致死锁
* 但是如果rh.count = 0就说明,这个线程是第一次获取读锁
* 为了防止写饥饿,直接将他们重新扔会同步队列,而且这些阻塞不会导致死锁
*/
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;
}
}
}
}
读锁释放
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//共享锁(读锁)释放
doReleaseShared();// 唤醒下一个节点,触发 sun.misc.Unsafe#unpark
return true;
}
return false;
}
/**
* 共享锁(读锁)释放
*/
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { //更新第一个获取到读锁线程的HoldCount
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else { //更新其他线程的HoldCount
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) { //如果count <= 1就清除这个key,value
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) { //死循环CAS更新state
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
读写锁降级
package com.thread.study;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 更新缓存演示锁降级
*/
public class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
//在获取写锁之前,必须首先释放读锁。
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 这里需要再次判断数据的有效性
// 因为在我们释放读锁和获取写锁的空隙之内,可能有其他线程修改了数据。
if (!cacheValid) {
data = new Object();
cacheValid = true;
}
//在不释放写锁的情况下,直接获取读锁,这就是读写锁的降级。
rwl.readLock().lock();
} finally {
//释放了写锁,但是依然持有读锁
rwl.writeLock().unlock();
}
}
try {
System.out.println(data);
} finally {
//释放读锁
rwl.readLock().unlock();
}
}
}
「线程在不释放写锁的情况下,获取读锁(这就是锁的降级)」
「PS:由于写锁是独占锁,当前线程获取写锁之后,其它线程就既不能获取写锁也不能获取读锁了,但是当前已经获取写锁的线程仍然可以获取读锁」
为什么需要锁的降级?
针对读多,写非常少的任务,使用锁的降级可以提高效率,与资源利用率
只支持降级,不支持升级
final static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
upgrade();
}
public static void upgrade() {
rwl.readLock().lock();
System.out.println("获取到了读锁");
rwl.writeLock().lock();
System.out.println("成功升级");
}
运行上面代码,通过结果可以得知:「在不释放读锁的情况下直接尝试获取写锁,也就是锁的升级,会让线程直接阻塞,程序是无法运行的」。
为什么不支持锁的升级?
读写锁的特点是如果线程都申请读锁,是可以多个线程同时持有的,可是如果是写锁,只能有一个线程持有,并且不可能存在读锁和写锁同时持有的情况。
举个栗子:假设线程 A 和 B 都想升级到写锁,那么对于线程 A 而言,它需要等待其他所有线程,包括线程 B 在内释放读锁。而线程 B 也需要等待所有的线程,包括线程 A 释放读锁。这就是一种非常典型的死锁的情况。谁都愿不愿意率先释放掉自己手中的锁。
一句话总结就是「多个线程同时发生锁升级的时候,会发生死锁,因为发生锁升级的线程会等待其它线程释放读锁」。
升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁。
ReentrantReadWriteLock使用实例
package com.thread.study;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
private ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
public void read() {
try {
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取读锁");
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放读锁");
readLock.unlock();
}
}
public void write() {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取写锁");
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 释放写锁");
writeLock.unlock();
}
}
public static void main(String[] args) {
ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
Thread t1 = new Thread(readWriteLockTest::read);
Thread t2 = new Thread(readWriteLockTest::read);
Thread t3 = new Thread(readWriteLockTest::read);
Thread t11 = new Thread(readWriteLockTest::write);
Thread t21 = new Thread(readWriteLockTest::write);
Thread t31 = new Thread(readWriteLockTest::write);
t11.start();
t1.start();
t2.start();
t3.start();
t21.start();
t31.start();
}
}
打印结果:
Thread-3 获取写锁 // -----------写锁
Thread-3 释放写锁 \\ -----------写锁
Thread-0 获取读锁 // ***********读锁
Thread-1 获取读锁 // ***********读锁
Thread-2 获取读锁 // ***********读锁
Thread-2 释放读锁 \\ ***********读锁
Thread-0 释放读锁 \\ ***********读锁
Thread-1 释放读锁 \\ ***********读锁
Thread-4 获取写锁 // -----------写锁
Thread-4 释放写锁 \\ -----------写锁
Thread-5 获取写锁 // -----------写锁
Thread-5 释放写锁 \\ -----------写锁
执行结果分析:
- 允许多个线程同时读共享变量
- 只允许一个线程写共享变量
- 如果写线程正在执行写操作,此时则禁止其他读线程读共享变量
执行结果总结:
「读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)」
总结
ReentrantReadWriteLock读写锁底层使用了AQS实现,使用AQS的状态值的高16位表示获取到读锁的个数,低16位表示获取写锁的线程的可重入次数,并通过CAS对其进行操作实现了读写分离。
ReentrantReadWriteLock读写锁适用于读多写少的场景。
读写特点:「读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)」