目录
Lock接口是JUC的lock包中的一个接口,ReentrantLock可重入独占锁和ReadWriteLock读写锁都实现了此接口,
两者都支持公平与非公平锁,与可重入。
由CAS一文知这些锁在内部需要实现具体的资源获取释放的策略。
ReentrantLock资源获取时,如果资源没有被获取,那么CAS修改state值;如果资源已被当前线程获取过,那么setState值+1;
释放锁就是将state值-1。
ReadWriteLock接口为读写锁,针对于读多写少的情景。
原则就是允许多个线程读,只允许一个线程写;读写混合的话只允许同一个线程同时进行。
原理是state资源的按位拆分,然后在资源获取时,额外判断写锁读锁的数量+获取锁的是否就是当前线程,判断是否允许获取资源或者重入。
【Lock】接口
锁是用来控制多个线程访问共享资源的方式。
在Lock接口出现之前,Java是通过synchronized关键字来实现锁功能的。Lock接口提供了与synchronized关键字类似的同步功能,只是需要再使用时 显式地获取和释放锁 。
Lock提供的synchronized关键字所不具备的特性:
- 尝试 非阻塞 地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁;
- 能 被中断 地获取锁:与synchronized不同,获取到锁的线程能够响应中断,当获取到锁的线程被中断,中断异常将被抛出,同时锁会被释放;
- 超时 获取锁:在指定的截止日期之前获取锁,如果截止时间到了仍旧无法获取锁,则返回;
Lock接口:
public interface Lock {
//获取锁
void lock();
//可中断地获取锁,即在锁的获取中可以中断当前线程
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞地获取锁,调用该方法后立刻返回,如果能够获取返回true,否则返回false
boolean tryLock();
//超时的获取锁,当前线程再以下情况会返回:
//1.当前线程再超时时间内获得了锁
//2.当前线程再超时时间内被中断
//3.超时时间结束,返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//获取等待通知组件,该组件和当前锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,调用后,当前线程将释放锁
Condition newCondition();
}
【ReentrantLock】可重入锁(Lock接口实现类)
ReentrantLock类实现了Lock接口,是一种 可重入的独占锁 。它与synchronized的区别在于:
ReentrantLock | Synchronized | |
---|---|---|
锁实现 | 基于AQS | 基于Monitor监视器 |
锁释放 | 需显式unlock | 无需手动释放锁 |
锁类型 | 公平、非公平 | 非公平 |
可重入 | 可重入 | 可重入 |
使用方式:
ReentrantLock lock = new ReentrantLock(true);
lock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
存储结构与构造函数
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
}
ReentrantLock的构造函数有两种:指定公平锁、非公平锁(默认非公平)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
锁类别 | |
---|---|
公平策略 | 在多个线程争用锁的情况下,倾向于将锁授予等待时间最长的线程,也就是相当于有一个等待队列,先进入等待队列的线程会先获取锁; |
非公平策略 | 多个线程争用锁的情况下,获取锁的线程是随机的; |
1.可重入性
- ReentrantLock的可重入性:锁可以被单个线程多次获取
实现可重入的特性,需要解决两个问题:
- 判断获取锁的线程是否就是当前持有锁的线程,如果是,再次获取成功;
- 某个线程获取了n次锁,那么需要释放n次之后,才能被其他线程获取;
由此可知,ReentrantLock内部维护了一个计数器,也就是AQS提供的 state 。
2. 公平锁与非公平锁
2.1【公平锁获取锁】tryAcquire
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
由AQS原理分析一文知,tryAcquire线程尝试获取锁,是由ReentrantLock自己实现的。
- 首先根据AQS提供的getState方法获取资源状态
- 如果state=0,表示资源没有被任何线程获取:
- hasQueuedPredecessors,这个方法来自于父类AQS:如果当前线程是CLH队列的队头则返回false,否则返回true;
- 如果返回true,表示等待队列前面还有其他节点,直接返回获取锁失败
- 如果返回false,表示当前线程是第一个节点,尝试CAS修改state值
- CAS成功,setExclusiveOwnerThread设置独占线程为当前线程;
- CAS失败,返回获取锁失败
- 如果state!=0,表示资源已经被获取
- getExclusiveOwnerThread得到当前获取锁的线程
- 如果不是当前线程,直接返回获取锁失败
- 如果就是当前线程获取了锁,通过调研父类AQS提供的setState方法将state+1
- hasQueuedPredecessors 判断当前节点在队列中是否有前驱节点 ,这也是公平锁的特性;
- 这里的独占线程设置:
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
2.2【非公平锁获取锁】tryAcquire
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
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;
}
对比于上边的公平锁,非公平锁的区别在于:
- 如果state=0,表示锁没有被任何线程获取,这时 不再判断当前线程是否是等待队列的第一个节点 ,而是直接去CAS尝试修改state获取锁。
获取锁流程:
2.3【释放锁】tryRelease
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;
}
- 如果获取锁的线程不是当前线程,则抛出异常
- 将getState获取的state-1
- 如果此时state=0,表示重入的线程已全部释放:
- setExclusiveOwnerThread设置获取锁的线程为null
- setState设置state=0
- 返回true
- 此时state!=0,表示重入的此线程还没释放完:
- setState设置state
- 返回false
【ReadWriteLock】接口
Synchronized关键字和ReentrantLock都是基于排他锁的实现,也就是同一个时刻只允许一个进程对共享资源进行获取。
对于一种 读多写少 的场景,就需要使用读写锁提升性能。
- 读写锁,即有两把锁:读锁和写锁
- 同一个时刻允许多个线程对共享资源进行读操作;
- 同一个时刻只允许一个线程对共享资源进行写操作;
- 当进行读操作时,同一时刻的其他写操作会被阻塞;
- 当进行写操作时,同一时刻的其他读操作会被阻塞;
ReadWriteLock接口方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
【ReentrantReadWriteLock】读写锁(ReadWriteLock接口实现类)
ReentrantReadWriteLock使用方式:
ReadWriteLock lock = new ReentrantReadWriteLock();
private int data = 0;
// 读操作
public void read() {
lock.readLock().lock();
try {
System.out.println("read data begin");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("read data end." + data);
} catch (Exception e) {
//ignore
} finally {
lock.readLock().unlock();
}
}
// 写操作
public void write(int data) {
// 加写锁
lock.writeLock().lock();
try {
System.out.println("write data begin");
Thread.sleep((long) (Math.random() * 1000));
this.data = data;
System.out.println("write data end." + data);
} catch (Exception e) {
//ignore
} finally {
lock.writeLock().unlock();
}
}
存储结构与构造函数
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
}
ReentrantReadWriteLock有两个内部类:ReadLock和WriteLock:
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
}
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
}
同样的,ReentrantReadWriteLock的构造函数也区分公平锁与非公平锁(默认是非公平):
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
特点:
- 支持 公平锁、非公平锁 ;
- 可重入 :读线程可重复获取读锁、写线程可再次获取读锁或写锁;
- 锁降级 :允许从写锁降级为读锁,即写线程可以获取读锁,然后释放写锁;
读写锁实现原理【state按位拆分】
在AQS中是通过一个int类型的state来表示锁的,但是在ReentrantReadWriteLock中,需要使用一个state来表示两个锁。实现的方式即:按位拆分。
- 将int类型的state拆分为高16位和低16位
- 高16位表示读锁,低16位表示写锁
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
方法中的参数c即通过getState获取的state值:
- sharedCount方法返回值即读锁数量 ;
- exclusiveCount方法返回值即写锁数量 ;
1.【写锁获取锁】tryAcquire
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
- 首先根据AQS提供的getState方法获取资源状态
- 如果state!=0,表示资源已经被某个线程获取了:
- 首先 exclusiveCount, 计算写锁数量
- 如果写锁为0,(那么此时一定是读锁),当进行读操作时,写锁获取会失败,返回false;
- 如果写锁!=0,那么判断是否是当前线程持有写锁,如果不是,返回false;
- 如果是当前线程持有写锁,那么判断写锁数量是否超限(因为是可重入的)
- setState设置写锁状态
- 如果state = 0,表示当前资源没有锁:
- writerShouldBlock判断当前线程是否应该阻塞,公平锁与非公平锁的实现不同。
- 公平锁:hasQueuedPredecessors判断等待队列中是否有其他线程排在前面
- 非公平锁:直接返回false,表示不需要排队
与ReentrantLock中的公平非公平锁逻辑相同
- compareAndSetState修改state同步变量,表示获取锁成功;
- setExclusiveOwnerThread设置获取锁的线程为当前线程;
- writerShouldBlock判断当前线程是否应该阻塞,公平锁与非公平锁的实现不同。
写锁获取流程:
2.【写锁释放锁】tryRelease
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);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 判断当前线程是否持有锁,如果没有,抛出异常
- getState获取state值,并-release, 计算写锁数量
- 如果写锁数量=0,设置持有锁线程为null
- setState设置锁状态
3.【读锁获取锁】tryAcquireShared
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
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;
}
return fullTryAcquireShared(current);
}
- getState获取state值,然后exclusiveCount 获取写锁状态 ;
- 如果写锁!=0,且持有锁线程不为当前线程,那么返回false;
- 如果写锁!=0,且就是当前线程持有写锁,向下执行(写线程可以获取读锁);
- readerShouldBlock判断读锁是否需要排队(与写锁的判断一样的)
- 公平锁:hasQueuedPredecessors判断等待队列中是否有其他线程排在前面
- 非公平锁:直接返回false,表示不需要排队
- sharedCount 获取读锁数量,判断是否超出最大值,且compareAndSetState尝试修改状态,CAS成功:
- 如果读锁数量=0,那么将当前线程设为firstReader;
- 如果读锁!=0,且 firstReader就是当前线程,那么将firstReaderHoldCount++;
- 如果读锁!=0,且firstReader不是当前线程,通过ThreadLocal获取当前线程的获取读锁次数,并保存到readHolds;
- 如果2、或3的条件不满足,则进入fullTryAcquireShared 循环重试 获取锁
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
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;
}
}
}
读锁获取流程:
4.【读锁释放锁】tryReleaseShared
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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;
}
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;
}
}
- 将当前线程持有读锁数量-1,分为是否firstReader两种情况;
- 循环尝试compareAndSetState设置读锁状态,返回释放后的读锁数量是否为0;
总结Q&A
- 为什么在获取锁的流程中,有时是通过compareAndSetState设置state值,有时是直接通过setState设置?
如当获取写锁时,如果当前锁没有被任何线程获取,那么此时要通过CAS设置state状态,因为可能会有多个线程抢占;
而如果锁已被当前线程获取,那么其他线程将不会有机会抢占,所以直接setState设置写锁值+1; - 为什么写锁加锁的CAS失败直接返回false,而读锁加锁会进入循环CAS尝试?
一个线程通过CAS对写锁加锁失败,则另外的线程获取了锁,这个线程将不能再获取到写锁;
但是一个线程获取到读锁,另一个线程可以继续获取读锁,所以可以通过循环尝试直到成功; - 写锁可以降级为读锁,而读锁不可以升级为写锁