共计 7555 个字符,预计需要花费 19 分钟才能阅读完成。
开篇闲扯
后面几篇写了无关 Java 对象的内存布局、Java 的内存模型、多线程锁的分类、Synchronized、Volatile、以及并发场景下呈现问题的三大罪魁祸首。看起来写了五篇文章,实际上也仅仅是写了个皮毛,用来应酬应酬局部公司“八股文”式的面试还行,然而在真正的在理论开发中会遇到各种稀奇古怪的问题。这时候就要通过线上的一些监测伎俩,获取零碎的运行日志进行剖析后再隔靴搔痒,比方 JDK 的 jstack、jmap、命令行工具 vmstat、JMeter 等等,肯定要在正当的剖析根底上优化,否则可能就是零碎小“感冒”,后果做了个阑尾炎手术。
又扯远了,老样子,还是先说一下本文次要讲点啥,而后再一点点解释。本文次要讲并发包 JUC 中的三个类:ReentrantLock、ReentrantReadWriteLock 和 StampedLock 以及 AQS(AbstractQueuedSynchronizer)的一些基本概念。
先来个脑图:
Lock 接口
public interface Lock {
// 加锁操作,加锁失败就进入阻塞状态并期待锁开释
void lock();
// 与 lock()办法始终,只是该办法容许阻塞的线程中断
void lockInterruptibly() throws InterruptedException;
// 非阻塞获取锁
boolean tryLock();
// 带参数的非阻塞获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 对立的解锁办法
void unlock();}
下面的源码展现了作为顶层接口 Lock 定义的一些根底办法。
lock 只是个显示的加锁接口,对应不同的实现类,能够供开发人员进行自定义扩大。比方一些定时的可轮询的获取锁模式,偏心锁与非偏心锁,读写锁,以及可重入锁等,都可能很轻松的实现。Lock 的锁是基于 Java 代码实现的,加解锁都是通过 lock()和 unlock()办法实现的。从性能上来说,Synchronized 的性能(吞吐量)以及稳定性是略差于 Lock 锁的。然而,在 Doug Lee 参加编写的《Java 并发编程实际》一书中又特别强调了,如果不是对 Lock 锁中提供的高级个性有相对的依赖,倡议还是应用 Synchronized 来作为并发同步的工具。因为它更简洁易用,不会因为在应用 Lock 接口时遗记在 Finally 中解锁而出 bug。说到底,还是为了升高编程门槛,让 Java 语言更加好用。
其实常见的几个实现类有:ReentrantLock、ReentrantReadWriteLock、StampedLock
接下来将具体解说一下。
ReentrantLock
先简略举个应用的例子:
/**
* FileName: TestLock
* Author: RollerRunning
* Date: 2020/12/7 9:34 PM
* Description:
*/
public class TestLock {
private static int count=0;
private static Lock lock=new ReentrantLock();
public static void add(){
// 加锁
lock.lock();
try {
count++;
Thread.sleep(1);
} catch (InterruptedException e) {e.printStackTrace();
}finally{
// 在 finally 中解锁,加解锁必须成对呈现
lock.unlock();}
}
}
ReentrantLock 只反对独占式的获取偏心锁或者是非偏心锁(都是基于 Sync 外部类实现,而 Sync 又继承自 AQS),在它的外部类 Sync 继承了 AbstractQueuedSynchronizer,并同时实现了 tryAcquire()、tryRelease()和 isHeldExclusively()办法等。同时,在 ReentrantLock 中还有其余两个外部类,一个是实现了偏心锁一个实现了非偏心锁,上面是 ReentrantLock 的局部源码:
/**
* 非偏心锁
*/
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);
}
}
/**
* 偏心锁
*/
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();
// 获取父类 AQS 中的 int 型 state
int c = getState();
// 判断锁是否被占用
if (c == 0) {
// 这个 if 判断中,先判断队列是否为空,如果为空则阐明锁能够失常获取,而后进行 CAS 操作并批改 state 标记位的信息
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//CAS 操作胜利,设置 AQS 中变量 exclusiveOwnerThread 的值为以后线程,示意获取锁胜利
setExclusiveOwnerThread(current);
// 返回获取锁胜利
return true;
}
}
// 而当 state 的值不为 0 时,阐明锁曾经被拿走了,此时判断锁是不是本人拿走的,因为他是个可重入锁。else if (current == getExclusiveOwnerThread()) {
// 如果是以后线程在占用锁,则再次获取锁,并批改 state 的值
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 当标记位不为 0,且占用锁的线程也不是本人时,返回获取锁失败
return false;
}
}
/**
* AQS 中排队的办法
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {if (failed)
cancelAcquire(node);
}
}
下面是以偏心锁为例对源码进行了简略的正文,能够依据这个思路,看一看非偏心锁的源码实现,再敞开源码试着画一下整个流程图,理解其外部实现的真谛。我先画为敬了:
这里涵盖了 ReentrantLock 的加锁根本流程,观众老爷是不是能够试着画一下解锁的流程,还有就是这个例子是 独占式偏心锁,独占式非偏心锁的总体流程大差不差,这里就不赘述了。
ReentrantReadWriteLock
一个简略的应用示例,大家能够本人运行感受一下:
/**
* FileName: ReentrantReadWriteLockTest
* Author: RollerRunning
* Date: 2020/12/8 6:48 PM
* Description: ReentrantReadWriteLock 的简略应用示例
*/
public class ReentrantReadWriteLockTest {private static ReentrantReadWriteLock READWRITELOCK = new ReentrantReadWriteLock();
// 取得读锁
private static ReentrantReadWriteLock.ReadLock READLOCK = READWRITELOCK.readLock();
// 取得写锁
private static ReentrantReadWriteLock.WriteLock WRITELOCK = READWRITELOCK.writeLock();
public static void main(String[] args) {ReentrantReadWriteLockTest lock = new ReentrantReadWriteLockTest();
// 别离启动两个读线程和一个写线程
Thread readThread1 = new Thread(new Runnable() {
@Override
public void run() {lock.read();
}
},"read1");
Thread readThread2 = new Thread(new Runnable() {
@Override
public void run() {lock.read();
}
},"read2");
Thread writeThread = new Thread(new Runnable() {
@Override
public void run() {lock.write();
}
},"write");
readThread1.start();
readThread2.start();
writeThread.start();}
public void read() {READLOCK.lock();
try {System.out.println("线程" + Thread.currentThread().getName() + "获取读锁。。。");
Thread.sleep(2000);
System.out.println("线程" + Thread.currentThread().getName() + "开释读锁。。。");
} catch (Exception e) {e.printStackTrace();
} finally {READLOCK.unlock();
}
}
public void write() {WRITELOCK.lock();
try {System.out.println("线程" + Thread.currentThread().getName() + "获取写锁。。。");
Thread.sleep(2000);
System.out.println("线程" + Thread.currentThread().getName() + "开释写锁。。。");
} catch (Exception e) {e.printStackTrace();
} finally {WRITELOCK.unlock();
}
}
}
后面说了 ReentrantLock 是一个独占锁,即不管线程对数据执行读还是写操作,同一时刻只容许一个线程持有锁。然而在一些读多写少的场景下,这种不分青红皂白就无脑加锁对的做法不够极客也很影响效率。因而,基于 ReentrantLock 优化而来的 ReentrantReadWriteLock 就呈现了。这种锁的思维是“读写锁拆散”,多个线程能够同时持有读锁,然而不容许多个线程持有雷同写锁或者同时持有读写锁。要害源码解读:
// 加共享锁
protected final int tryAcquireShared(int unused) {
// 获取以后加锁的线程
Thread current = Thread.currentThread();
// 获取锁状态信息
int c = getState();
// 判断以后锁是否可用,并判断以后线程是否独占资源
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 获取读锁的数量
int r = sharedCount(c);
// 这里做了三个判断:是否阻塞即是否为偏心锁、持有该共享锁的线程是否超过最大值、CAS 加共享读锁是否胜利
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 中,这里是基于 ThreadLocal 实现的,每个线程都有本人的 readHolds 用于记录本人重入的次数
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
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;
}
}
}
在 ReentrantReadWriteLock 中,也是基于 AQS 来实现的,在它的外部应用了一个 int 型(4 字节 32 位)的 stat 来示意读写锁,其中高 16 位示意读锁,低 16 位示意写锁,而对于读写锁的判断通常是对 int 值以及高下 16 位进行判断。接下来用一张图展现一下获取共享的读锁过程:
至此,别离展现了获取 ReentrantLock 独占锁 和ReentrantReadWriteLock 共享读锁 的过程,心愿可能帮忙大家跟面试官 PK。
总结一下后面说的两种锁:
当线程持有读锁时,那么就不能再获取写锁。当 A 线程在获取写锁的时候,如果以后读锁被占用,立刻返回失败失败。
当线程持有写锁时,该线程是能够持续获取读锁的。当 A 线程获取读锁时如果发现写锁被占用,判断以后写锁持有者是不是本人,如果是本人就能够持续获取读锁,否则返回失败。
StampedLock
StampedLock 其实是对 ReentrantReadWriteLock 进行了进一步的降级,试想一下,当有很多读线程,然而只有一个写线程,最蹩脚的状况是写线程始终竞争不到锁,写线程就会始终处于期待状态,也就是线程饥饿问题。StampedLock 的外部实现也是基于队列和 state 状态实现的,然而它引入了 stamp(标记)的概念,因而在获取锁时会返回一个惟一标识 stamp 作为以后锁的版本,而在开释锁时,须要传递这个 stamp 作为标识来解锁。
从概念上来说 StampedLock 比 RRW 多引入了一种乐观锁的思维,从应用层面来说,加锁生成 stamp,解锁须要传同样的 stamp 作为参数。
最初贴一张我整顿的这部分脑图:
最初,感激各位观众老爷,还请三连!!!
点赞关注不迷路,再次感激!