深刻了解 Java 并发锁
1. 并发锁简介
确保线程平安最常见的做法是利用锁机制(Lock
、sychronized
)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程能够执行某个办法或者某个代码块,那么操作必然是原子性的,线程平安的。
在工作、面试中,常常会听到各种形形色色的锁,听的人云里雾里。锁的概念术语很多,它们是针对不同的问题所提出的,通过简略的梳理,也不难理解。
1.1. 可重入锁
可重入锁,顾名思义,指的是线程能够反复获取同一把锁。即同一个线程在外层办法获取了锁,在进入内层办法会主动获取锁。
可重入锁能够在肯定水平上防止死锁。
ReentrantLock
、ReentrantReadWriteLock
是可重入锁。这点,从其命名也不难看出。synchronized
也是一个可重入锁。
【示例】synchronized
的可重入示例
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}
下面的代码就是一个典型场景:如果应用的锁不是可重入锁的话,setB
可能不会被以后线程执行,从而造成死锁。
【示例】ReentrantLock
的可重入示例
class Task {
private int value;
private final Lock lock = new ReentrantLock();
public Task() {
this.value = 0;
}
public int get() {
// 获取锁
lock.lock();
try {
return value;
} finally {
// 保障锁能开释
lock.unlock();
}
}
public void addOne() {
// 获取锁
lock.lock();
try {
// 留神:此处曾经胜利获取锁,进入 get 办法后,又尝试获取锁,
// 如果锁不是可重入的,会导致死锁
value = 1 + get();
} finally {
// 保障锁能开释
lock.unlock();
}
}
}
1.2. 偏心锁与非偏心锁
- 偏心锁 – 偏心锁是指 多线程依照申请锁的程序来获取锁。
- 非偏心锁 – 非偏心锁是指 多线程不依照申请锁的程序来获取锁 。这就可能会呈现优先级反转(后来者居上)或者饥饿景象(某线程总是抢不过别的线程,导致始终无奈执行)。
偏心锁为了保障线程申请程序,势必要付出肯定的性能代价,因而其吞吐量个别低于非偏心锁。
偏心锁与非偏心锁 在 Java 中的典型实现:
synchronized
只反对非偏心锁。ReentrantLock
、ReentrantReadWriteLock
,默认是非偏心锁,但反对偏心锁。
1.3. 独享锁与共享锁
独享锁与共享锁是一种狭义上的说法,从理论用处上来看,也常被称为互斥锁与读写锁。
- 独享锁 – 独享锁是指 锁一次只能被一个线程所持有。
- 共享锁 – 共享锁是指 锁可被多个线程所持有。
独享锁与共享锁在 Java 中的典型实现:
synchronized
、ReentrantLock
只反对独享锁。ReentrantReadWriteLock
其写锁是独享锁,其读锁是共享锁。读锁是共享锁使得并发读是十分高效的,读写,写读 ,写写的过程是互斥的。
1.4. 乐观锁与乐观锁
乐观锁与乐观锁不是指具体的什么类型的锁,而是解决并发同步的策略。
- 乐观锁 – 乐观锁对于并发采取乐观的态度,认为:不加锁的并发操作肯定会出问题。乐观锁适宜写操作频繁的场景。
- 乐观锁 – 乐观锁对于并发采取乐观的态度,认为:不加锁的并发操作也没什么问题。对于同一个数据的并发操作,是不会产生批改的。在更新数据的时候,会采纳一直尝试更新的形式更新数据。乐观锁适宜读多写少的场景。
乐观锁与乐观锁在 Java 中的典型实现:
- 乐观锁在 Java 中的利用就是通过应用
synchronized
和Lock
显示加锁来进行互斥同步,这是一种阻塞同步。 - 乐观锁在 Java 中的利用就是采纳
CAS
机制(CAS
操作通过Unsafe
类提供,但这个类不间接裸露为 API,所以都是间接应用,如各种原子类)。
1.5. 偏差锁、轻量级锁、重量级锁
所谓轻量级锁与重量级锁,指的是锁管制粒度的粗细。显然,管制粒度越细,阻塞开销越小,并发性也就越高。
Java 1.6 以前,重量级锁个别指的是 synchronized
,而轻量级锁指的是 volatile
。
Java 1.6 当前,针对 synchronized
做了大量优化,引入 4 种锁状态: 无锁状态、偏差锁、轻量级锁和重量级锁。锁能够单向的从偏差锁降级到轻量级锁,再从轻量级锁降级到重量级锁 。
- 偏差锁 – 偏差锁是指一段同步代码始终被一个线程所拜访,那么该线程会主动获取锁。升高获取锁的代价。
- 轻量级锁 – 是指当锁是偏差锁的时候,被另一个线程所拜访,偏差锁就会降级为轻量级锁,其余线程会通过自旋的模式尝试获取锁,不会阻塞,进步性能。
- 重量级锁 – 是指当锁为轻量级锁的时候,另一个线程尽管是自旋,但自旋不会始终继续上来,当自旋肯定次数的时候,还没有获取到锁,就会进入阻塞,该锁收缩为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能升高。
1.6. 分段锁
分段锁其实是一种锁的设计,并不是具体的一种锁。所谓分段锁,就是把锁的对象分成多段,每段独立管制,使得锁粒度更细,缩小阻塞开销,从而进步并发性。这其实很好了解,就像高速公路上的收费站,如果只有一个收费口,那所有的车只能排成一条队缴费;如果有多个收费口,就能够分流了。
Hashtable
应用 synchronized
润饰办法来保障线程安全性,那么面对线程的拜访,Hashtable 就会锁住整个对象,所有的其它线程只能期待,这种阻塞形式的吞吐量显然很低。
Java 1.7 以前的 ConcurrentHashMap
就是分段锁的典型案例。ConcurrentHashMap
保护了一个 Segment
数组,个别称为分段桶。
final Segment<K,V>[] segments;
当有线程拜访 ConcurrentHashMap
的数据时,ConcurrentHashMap
会先依据 hashCode 计算出数据在哪个桶(即哪个 Segment),而后锁住这个 Segment
。
1.7. 显示锁和内置锁
Java 1.5 之前,协调对共享对象的拜访时能够应用的机制只有 synchronized
和 volatile
。这两个都属于内置锁,即锁的申请和开释都是由 JVM 所管制。
Java 1.5 之后,减少了新的机制:ReentrantLock
、ReentrantReadWriteLock
,这类锁的申请和开释都能够由程序所管制,所以常被称为显示锁。
留神:如果不须要
ReentrantLock
、ReentrantReadWriteLock
所提供的高级同步个性,应该优先思考应用synchronized
。理由如下:
- Java 1.6 当前,
synchronized
做了大量的优化,其性能曾经与ReentrantLock
、ReentrantReadWriteLock
基本上持平。- 从趋势来看,Java 将来更可能会优化
synchronized
,而不是ReentrantLock
、ReentrantReadWriteLock
,因为synchronized
是 JVM 内置属性,它能执行一些优化。ReentrantLock
、ReentrantReadWriteLock
申请和开释锁都是由程序控制,如果使用不当,可能造成死锁,这是很危险的。
以下比照一下显示锁和内置锁的差别:
-
被动获取锁和开释锁
synchronized
不能被动获取锁和开释锁。获取锁和开释锁都是 JVM 管制的。ReentrantLock
能够被动获取锁和开释锁。(如果遗记开释锁,就可能产生死锁)。
-
响应中断
synchronized
不能响应中断。ReentrantLock
能够响应中断。
-
超时机制
synchronized
没有超时机制。ReentrantLock
有超时机制。ReentrantLock
能够设置超时工夫,超时后主动开释锁,防止始终期待。
-
反对偏心锁
synchronized
只反对非偏心锁。ReentrantLock
反对非偏心锁和偏心锁。
-
是否反对共享
- 被
synchronized
润饰的办法或代码块,只能被一个线程拜访(独享)。如果这个线程被阻塞,其余线程也只能期待 ReentrantLock
能够基于Condition
灵便的管制同步条件。
- 被
-
是否反对读写拆散
synchronized
不反对读写锁拆散;ReentrantReadWriteLock
反对读写锁,从而使阻塞读写的操作离开,无效进步并发性。
2. Lock 和 Condition
2.1. 为何引入 Lock 和 Condition
并发编程畛域,有两大外围问题:一个是互斥,即同一时刻只容许一个线程访问共享资源;另一个是同步,即线程之间如何通信、合作。这两大问题,管程都是可能解决的。Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
synchronized 是管程的一种实现,既然如此,何必再提供 Lock 和 Condition。
JDK 1.6 以前,synchronized 还没有做优化,性能远低于 Lock。然而,性能不是引入 Lock 的最重要因素。真正关键在于:synchronized 使用不当,可能会呈现死锁。
synchronized 无奈通过毁坏不可抢占条件来防止死锁。起因是 synchronized 申请资源的时候,如果申请不到,线程间接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也开释不了线程曾经占有的资源。
与内置锁 synchronized
不同的是,Lock
提供了一组无条件的、可轮询的、定时的以及可中断的锁操作,所有获取锁、开释锁的操作都是显式的操作。
- 可能响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦产生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程可能响应中断信号,也就是说当咱们给阻塞的线程发送中断信号的时候,可能唤醒它,那它就有机会开释已经持有的锁 A。这样就毁坏了不可抢占条件了。
- 反对超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个谬误,那这个线程也有机会开释已经持有的锁。这样也能毁坏不可抢占条件。
- 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是间接返回,那这个线程也有机会开释已经持有的锁。这样也能毁坏不可抢占条件。
2.2. Lock 接口
Lock
的接口定义如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
lock()
– 获取锁。unlock()
– 开释锁。tryLock()
– 尝试获取锁,仅在调用时锁未被另一个线程持有的状况下,才获取该锁。tryLock(long time, TimeUnit unit)
– 和tryLock()
相似,区别仅在于限定工夫,如果限定工夫内未获取到锁,视为失败。lockInterruptibly()
– 锁未被另一个线程持有,且线程没有被中断的状况下,能力获取锁。newCondition()
– 返回一个绑定到Lock
对象上的Condition
实例。
2.3. Condition
Condition 实现了管程模型外面的条件变量。
前文中提过 Lock
接口中 有一个 newCondition()
办法用于返回一个绑定到 Lock
对象上的 Condition
实例。Condition
是什么?有什么作用?本节将一一解说。
在单线程中,一段代码的执行可能依赖于某个状态,如果不满足状态条件,代码就不会被执行(典型的场景,如:if ... else ...
)。在并发环境中,当一个线程判断某个状态条件时,其状态可能是因为其余线程的操作而扭转,这时就须要有肯定的协调机制来确保在同一时刻,数据只能被一个线程锁批改,且批改的数据状态被所有线程所感知。
Java 1.5 之前,次要是利用 Object
类中的 wait
、notify
、notifyAll
配合 synchronized
来进行线程间通信 。
wait
、notify
、notifyAll
须要配合 synchronized
应用,不适用于 Lock
。而应用 Lock
的线程,彼此间通信应该应用 Condition
。这能够了解为,什么样的锁配什么样的钥匙。内置锁(synchronized
)配合内置条件队列(wait
、notify
、notifyAll
),显式锁(Lock
)配合显式条件队列(Condition
)。
Condition 的个性
Condition
接口定义如下:
public interface Condition {
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
}
其中,await
、signal
、signalAll
与 wait
、notify
、notifyAll
绝对应,性能也类似。除此以外,Condition
相比内置条件队列( wait
、notify
、notifyAll
),提供了更为丰盛的性能:
- 每个锁(
Lock
)上能够存在多个Condition
,这意味着锁的状态条件能够有多个。 - 反对偏心的或非偏心的队列操作。
- 反对可中断的条件期待,相干办法:
awaitUninterruptibly()
。 - 反对可定时的期待,相干办法:
awaitNanos(long)
、await(long, TimeUnit)
、awaitUntil(Date)
。
Condition 的用法
这里以 Condition
来实现一个消费者、生产者模式。
产品类
class Message {
private final Lock lock = new ReentrantLock();
private final Condition producedMsg = lock.newCondition();
private final Condition consumedMsg = lock.newCondition();
private String message;
private boolean state;
private boolean end;
public void consume() {
//lock
lock.lock();
try {
// no new message wait for new message
while (!state) { producedMsg.await(); }
System.out.println("consume message : " + message);
state = false;
// message consumed, notify waiting thread
consumedMsg.signal();
} catch (InterruptedException ie) {
System.out.println("Thread interrupted - viewMessage");
} finally {
lock.unlock();
}
}
public void produce(String message) {
lock.lock();
try {
// last message not consumed, wait for it be consumed
while (state) { consumedMsg.await(); }
System.out.println("produce msg: " + message);
this.message = message;
state = true;
// new message added, notify waiting thread
producedMsg.signal();
} catch (InterruptedException ie) {
System.out.println("Thread interrupted - publishMessage");
} finally {
lock.unlock();
}
}
public boolean isEnd() {
return end;
}
public void setEnd(boolean end) {
this.end = end;
}
}
消费者
class MessageConsumer implements Runnable {
private Message message;
public MessageConsumer(Message msg) {
message = msg;
}
@Override
public void run() {
while (!message.isEnd()) { message.consume(); }
}
}
生产者
class MessageProducer implements Runnable {
private Message message;
public MessageProducer(Message msg) {
message = msg;
}
@Override
public void run() {
produce();
}
public void produce() {
List<String> msgs = new ArrayList<>();
msgs.add("Begin");
msgs.add("Msg1");
msgs.add("Msg2");
for (String msg : msgs) {
message.produce(msg);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
message.produce("End");
message.setEnd(true);
}
}
测试
public class LockConditionDemo {
public static void main(String[] args) {
Message msg = new Message();
Thread producer = new Thread(new MessageProducer(msg));
Thread consumer = new Thread(new MessageConsumer(msg));
producer.start();
consumer.start();
}
}
3. ReentrantLock
ReentrantLock
类是 Lock
接口的具体实现,与内置锁 synchronized
雷同的是,它是一个可重入锁。
3.1. ReentrantLock 的个性
ReentrantLock
的个性如下:
ReentrantLock
提供了与synchronized
雷同的互斥性、内存可见性和可重入性。ReentrantLock
反对偏心锁和非偏心锁(默认)两种模式。-
ReentrantLock
实现了Lock
接口,反对了synchronized
所不具备的灵活性。synchronized
无奈中断一个正在期待获取锁的线程synchronized
无奈在申请获取一个锁时无休止地期待
3.2. ReentrantLock 的用法
前文理解了 ReentrantLock
的个性,接下来,咱们要讲述其具体用法。
ReentrantLock 的构造方法
ReentrantLock
有两个构造方法:
public ReentrantLock() {}
public ReentrantLock(boolean fair) {}
ReentrantLock()
– 默认构造方法会初始化一个非偏心锁(NonfairSync);ReentrantLock(boolean)
–new ReentrantLock(true)
会初始化一个偏心锁(FairSync)。
lock 和 unlock 办法
lock()
– 无条件获取锁。如果以后线程无奈获取锁,则以后线程进入休眠状态不可用,直至以后线程获取到锁。如果该锁没有被另一个线程持有,则获取该锁并立刻返回,将锁的持有计数设置为 1。unlock()
– 用于开释锁。
留神:请务必牢记,获取锁操作
lock()
必须在try catch
块外进行,并且将开释锁操作unlock()
放在finally
块中进行,以保障锁肯定被被开释,避免死锁的产生。
示例:ReentrantLock
的基本操作
public class ReentrantLockDemo {
public static void main(String[] args) {
Task task = new Task();
MyThread tA = new MyThread("Thread-A", task);
MyThread tB = new MyThread("Thread-B", task);
MyThread tC = new MyThread("Thread-C", task);
tA.start();
tB.start();
tC.start();
}
static class MyThread extends Thread {
private Task task;
public MyThread(String name, Task task) {
super(name);
this.task = task;
}
@Override
public void run() {
task.execute();
}
}
static class Task {
private ReentrantLock lock = new ReentrantLock();
public void execute() {
lock.lock();
try {
for (int i = 0; i < 3; i++) {
System.out.println(lock.toString());
// 查问以后线程 hold 住此锁的次数
System.out.println("\t holdCount: " + lock.getHoldCount());
// 查问正等待获取此锁的线程数
System.out.println("\t queuedLength: " + lock.getQueueLength());
// 是否为偏心锁
System.out.println("\t isFair: " + lock.isFair());
// 是否被锁住
System.out.println("\t isLocked: " + lock.isLocked());
// 是否被以后线程持有锁
System.out.println("\t isHeldByCurrentThread: " + lock.isHeldByCurrentThread());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
lock.unlock();
}
}
}
}
输入后果:
java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-A]
holdCount: 1
queuedLength: 2
isFair: false
isLocked: true
isHeldByCurrentThread: true
java.util.concurrent.locks.ReentrantLock@64fcd88a[Locked by thread Thread-C]
holdCount: 1
queuedLength: 1
isFair: false
isLocked: true
isHeldByCurrentThread: true
// ...
tryLock 办法
与无条件获取锁相比,tryLock 有更欠缺的容错机制。
tryLock()
– 可轮询获取锁。如果胜利,则返回 true;如果失败,则返回 false。也就是说,这个办法无论成败都会立刻返回,获取不到锁(锁已被其余线程获取)时不会始终期待。tryLock(long, TimeUnit)
– 可定时获取锁。和tryLock()
相似,区别仅在于这个办法在获取不到锁时会期待肯定的工夫,在工夫期限之内如果还获取不到锁,就返回 false。如果如果一开始拿到锁或者在期待期间内拿到了锁,则返回 true。
示例:ReentrantLock
的 tryLock()
操作
批改上个示例中的 execute()
办法
public void execute() {
if (lock.tryLock()) {
try {
for (int i = 0; i < 3; i++) {
// 略...
}
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败");
}
}
示例:ReentrantLock
的 tryLock(long, TimeUnit)
操作
批改上个示例中的 execute()
办法
public void execute() {
try {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
for (int i = 0; i < 3; i++) {
// 略...
}
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败");
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 获取锁超时");
e.printStackTrace();
}
}
lockInterruptibly 办法
-
lockInterruptibly()
– 可中断获取锁。可中断获取锁能够在取得锁的同时放弃对中断的响应。可中断获取锁比其它获取锁的形式略微简单一些,须要两个try-catch
块(如果在获取锁的操作中抛出了InterruptedException
,那么能够应用规范的try-finally
加锁模式)。- 举例来说:假如有两个线程同时通过
lock.lockInterruptibly()
获取某个锁时,若线程 A 获取到了锁,则线程 B 只能期待。若此时对线程 B 调用threadB.interrupt()
办法可能中断线程 B 的期待过程。因为lockInterruptibly()
的申明中抛出了异样,所以lock.lockInterruptibly()
必须放在try
块中或者在调用lockInterruptibly()
的办法外申明抛出InterruptedException
。
- 举例来说:假如有两个线程同时通过
留神:当一个线程获取了锁之后,是不会被
interrupt()
办法中断的。独自调用interrupt()
办法不能中断正在运行状态中的线程,只能中断阻塞状态中的线程。因而当通过lockInterruptibly()
办法获取某个锁时,如果未获取到锁,只有在期待的状态下,才能够响应中断。
示例:ReentrantLock
的 lockInterruptibly()
操作
批改上个示例中的 execute()
办法
public void execute() {
try {
lock.lockInterruptibly();
for (int i = 0; i < 3; i++) {
// 略...
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断");
e.printStackTrace();
} finally {
lock.unlock();
}
}
newCondition 办法
newCondition()
– 返回一个绑定到 Lock
对象上的 Condition
实例。
3.3. ReentrantLock 的原理
ReentrantLock 的可见性
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保障锁能开释
rtl.unlock();
}
}
}
ReentrantLock,外部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码如上面所示)。也就是说,在执行 value+=1 之前,程序先读写了一次 volatile 变量 state,在执行 value+=1 之后,又读写了一次 volatile 变量 state。依据相干的 Happens-Before 规定:
- 程序性规定:对于线程 T1,value+=1 Happens-Before 开释锁的操作 unlock();
- volatile 变量规定:因为 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作;
- 传递性规定:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作。
ReentrantLock 的数据结构
浏览 ReentrantLock
的源码,能够发现它有一个外围字段:
private final Sync sync;
sync
– 外部抽象类ReentrantLock.Sync
对象,Sync
继承自 AQS。它有两个子类:ReentrantLock.FairSync
– 偏心锁。ReentrantLock.NonfairSync
– 非偏心锁。
查看源码能够发现,ReentrantLock
实现 Lock
接口其实是调用 ReentrantLock.FairSync
或 ReentrantLock.NonfairSync
中各自的实现,这里不一一列举。
ReentrantLock 的获取锁和开释锁
ReentrantLock 获取锁和开释锁的接口,从表象看,是调用 ReentrantLock.FairSync
或 ReentrantLock.NonfairSync
中各自的实现;从实质上看,是基于 AQS 的实现。
仔细阅读源码很容易发现:
void lock()
调用 Sync 的 lock() 办法。void lockInterruptibly()
间接调用 AQS 的 获取可中断的独占锁 办法lockInterruptibly()
。boolean tryLock()
调用 Sync 的nonfairTryAcquire()
。boolean tryLock(long time, TimeUnit unit)
间接调用 AQS 的 获取超时期待式的独占锁 办法tryAcquireNanos(int arg, long nanosTimeout)
。void unlock()
间接调用 AQS 的 开释独占锁 办法release(int arg)
。
间接调用 AQS 接口的办法就不再赘述了,其原理在 AQS 的原理 中曾经用很大篇幅进行过解说。
nonfairTryAcquire
办法源码如下:
// 偏心锁和非偏心锁都会用这个办法区尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 如果同步状态为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;
}
解决流程很简略:
- 如果同步状态为 0,设置同步状态设为 acquires,并设置以后线程为排它线程,而后返回 true,获取锁胜利。
- 如果同步状态不为 0 且以后线程为排它线程,设置同步状态为以后状态值+acquires 值,而后返回 true,获取锁胜利。
- 否则,返回 false,获取锁失败。
偏心锁和非偏心锁
ReentrantLock 这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的偏心策略,如果传入 true 就示意须要结构一个偏心锁,反之则示意要结构一个非偏心锁。
锁都对应着一个期待队列,如果一个线程没有取得锁,就会进入期待队列,当有线程开释锁的时候,就须要从期待队列中唤醒一个期待的线程。如果是偏心锁,唤醒的策略就是谁期待的工夫长,就唤醒谁,很偏心;如果是非偏心锁,则不提供这个偏心保障,有可能等待时间短的线程反而先被唤醒。
lock 办法在偏心锁和非偏心锁中的实现:
二者的区别仅在于申请非偏心锁时,如果同步状态为 0,尝试将其设为 1,如果胜利,间接将以后线程置为排它线程;否则和偏心锁一样,调用 AQS 获取独占锁办法 acquire
。
// 非偏心锁实现
final void lock() {
if (compareAndSetState(0, 1))
// 如果同步状态为0,将其设为1,并设置以后线程为排它线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 调用 AQS 获取独占锁办法 acquire
acquire(1);
}
// 偏心锁实现
final void lock() {
// 调用 AQS 获取独占锁办法 acquire
acquire(1);
}
4. ReentrantReadWriteLock
ReadWriteLock
实用于读多写少的场景。
ReentrantReadWriteLock
类是 ReadWriteLock
接口的具体实现,它是一个可重入的读写锁。ReentrantReadWriteLock
保护了一对读写锁,将读写锁离开,有利于进步并发效率。
读写锁,并不是 Java 语言特有的,而是一个广为应用的通用技术,所有的读写锁都恪守以下三条根本准则:
- 容许多个线程同时读共享变量;
- 只容许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
读写锁与互斥锁的一个重要区别就是读写锁容许多个线程同时读共享变量,而互斥锁是不容许的,这是读写锁在读多写少场景下性能优于互斥锁的要害。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不容许其余线程执行写操作和读操作。
4.1. ReentrantReadWriteLock 的个性
ReentrantReadWriteLock 的个性如下:
ReentrantReadWriteLock
实用于读多写少的场景。如果是写多读少的场景,因为ReentrantReadWriteLock
其外部实现比ReentrantLock
简单,性能可能反而要差一些。如果存在这样的问题,须要具体问题具体分析。因为ReentrantReadWriteLock
的读写锁(ReadLock
、WriteLock
)都实现了Lock
接口,所以要替换为ReentrantLock
也较为容易。ReentrantReadWriteLock
实现了ReadWriteLock
接口,反对了ReentrantLock
所不具备的读写锁拆散。ReentrantReadWriteLock
保护了一对读写锁(ReadLock
、WriteLock
)。将读写锁离开,有利于进步并发效率。ReentrantReadWriteLock
的加锁策略是:容许多个读操作并发执行,但每次只容许一个写操作。ReentrantReadWriteLock
为读写锁都提供了可重入的加锁语义。ReentrantReadWriteLock
反对偏心锁和非偏心锁(默认)两种模式。
ReadWriteLock
接口定义如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
readLock
– 返回用于读操作的锁(ReadLock
)。writeLock
– 返回用于写操作的锁(WriteLock
)。
在读写锁和写入锁之间的交互能够采纳多种实现形式,ReadWriteLock
的一些可选实现包含:
- 开释优先 – 当一个写入操作开释写锁,并且队列中同时存在读线程和写线程,那么应该优先选择读线程、写线程,还是最先发出请求的线程?
- 读线程插队 – 如果锁是由读线程持有,但有写线程正在期待,那么新达到的读线程是否立刻取得拜访权,还是应该在写线程前面期待?如果容许读线程插队到写线程之前,那么将进步并发性,但可能造成线程饥饿问题。
- 重入性 – 读锁和写锁是否是可重入的?
- 降级 – 如果一个线程持有写入锁,那么它是否在不开释该锁的状况下取得读锁?这可能会使得写锁被降级为读锁,同时不容许其余写线程批改被爱护的资源。
- 降级 – 读锁是否优先于其余正在期待的读线程和写线程而降级为一个写锁?在大多数的读写锁实现中并不反对降级,因为如果没有显式的降级操作,那么很容易造成死锁。
4.2. ReentrantReadWriteLock 的用法
前文理解了 ReentrantReadWriteLock
的个性,接下来,咱们要讲述其具体用法。
ReentrantReadWriteLock 的构造方法
ReentrantReadWriteLock
和 ReentrantLock
一样,也有两个构造方法,且用法类似。
public ReentrantReadWriteLock() {}
public ReentrantReadWriteLock(boolean fair) {}
ReentrantReadWriteLock()
– 默认构造方法会初始化一个非偏心锁(NonfairSync)。在非偏心的锁中,线程取得锁的程序是不确定的。写线程降级为读线程是能够的,但读线程降级为写线程是不能够的(这样会导致死锁)。ReentrantReadWriteLock(boolean)
–new ReentrantLock(true)
会初始化一个偏心锁(FairSync)。对于偏心锁,等待时间最长的线程将优先取得锁。如果这个锁是读线程持有,则另一个线程申请写锁,那么其余读线程都不能取得读锁,直到写线程开释写锁。
ReentrantReadWriteLock 的应用实例
在 ReentrantReadWriteLock
的个性 中曾经介绍过,ReentrantReadWriteLock
的读写锁(ReadLock
、WriteLock
)都实现了 Lock
接口,所以其各自独立的应用形式与 ReentrantLock
一样,这里不再赘述。
ReentrantReadWriteLock
与 ReentrantLock
用法上的差别,次要在于读写锁的配合应用。本文以一个典型应用场景来进行解说。
【示例】基于 ReadWriteLock
实现一个简略的泛型无界缓存
/**
* 简略的无界缓存实现
* <p>
* 应用 WeakHashMap 存储键值对。WeakHashMap 中存储的对象是弱援用,JVM GC 时会主动革除没有被援用的弱援用对象。
*/
static class UnboundedCache<K, V> {
private final Map<K, V> cacheMap = new WeakHashMap<>();
private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();
public V get(K key) {
cacheLock.readLock().lock();
V value;
try {
value = cacheMap.get(key);
String log = String.format("%s 读数据 %s:%s", Thread.currentThread().getName(), key, value);
System.out.println(log);
} finally {
cacheLock.readLock().unlock();
}
return value;
}
public V put(K key, V value) {
cacheLock.writeLock().lock();
try {
cacheMap.put(key, value);
String log = String.format("%s 写入数据 %s:%s", Thread.currentThread().getName(), key, value);
System.out.println(log);
} finally {
cacheLock.writeLock().unlock();
}
return value;
}
public V remove(K key) {
cacheLock.writeLock().lock();
try {
return cacheMap.remove(key);
} finally {
cacheLock.writeLock().unlock();
}
}
public void clear() {
cacheLock.writeLock().lock();
try {
this.cacheMap.clear();
} finally {
cacheLock.writeLock().unlock();
}
}
}
阐明:
- 应用
WeakHashMap
而不是HashMap
来存储键值对。WeakHashMap
中存储的对象是弱援用,JVM GC 时会主动革除没有被援用的弱援用对象。 - 向
Map
写数据前加写锁,写完后,开释写锁。 - 向
Map
读数据前加读锁,读完后,开释读锁。
测试其线程安全性:
/**
* @author <a href="mailto:forbreak@163.com">Zhang Peng</a>
* @since 2020-01-01
*/
public class ReentrantReadWriteLockDemo {
static UnboundedCache<Integer, Integer> cache = new UnboundedCache<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
executorService.execute(new MyThread());
cache.get(0);
}
executorService.shutdown();
}
/** 线程工作每次向缓存中写入 3 个随机值,key 固定 */
static class MyThread implements Runnable {
@Override
public void run() {
Random random = new Random();
for (int i = 0; i < 3; i++) {
cache.put(i, random.nextInt(100));
}
}
}
}
阐明:示例中,通过线程池启动 20 个并发工作。工作每次向缓存中写入 3 个随机值,key 固定;而后主线程每次固定读取缓存中第一个 key 的值。
输入后果:
main 读数据 0:null
pool-1-thread-1 写入数据 0:16
pool-1-thread-1 写入数据 1:58
pool-1-thread-1 写入数据 2:50
main 读数据 0:16
pool-1-thread-1 写入数据 0:85
pool-1-thread-1 写入数据 1:76
pool-1-thread-1 写入数据 2:46
pool-1-thread-2 写入数据 0:21
pool-1-thread-2 写入数据 1:41
pool-1-thread-2 写入数据 2:63
main 读数据 0:21
main 读数据 0:21
// ...
4.3. ReentrantReadWriteLock 的原理
后面理解了 ReentrantLock
的原理,了解 ReentrantReadWriteLock
就容易多了。
ReentrantReadWriteLock 的数据结构
浏览 ReentrantReadWriteLock 的源码,能够发现它有三个外围字段:
/** 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;
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
sync
– 外部类ReentrantReadWriteLock.Sync
对象。与ReentrantLock
相似,它有两个子类:ReentrantReadWriteLock.FairSync
和ReentrantReadWriteLock.NonfairSync
,别离示意偏心锁和非偏心锁的实现。readerLock
– 外部类ReentrantReadWriteLock.ReadLock
对象,这是一把读锁。writerLock
– 外部类ReentrantReadWriteLock.WriteLock
对象,这是一把写锁。
ReentrantReadWriteLock 的获取锁和开释锁
public static class ReadLock implements Lock, java.io.Serializable {
// 调用 AQS 获取共享锁办法
public void lock() {
sync.acquireShared(1);
}
// 调用 AQS 开释共享锁办法
public void unlock() {
sync.releaseShared(1);
}
}
public static class WriteLock implements Lock, java.io.Serializable {
// 调用 AQS 获取独占锁办法
public void lock() {
sync.acquire(1);
}
// 调用 AQS 开释独占锁办法
public void unlock() {
sync.release(1);
}
}
5. StampedLock
ReadWriteLock 反对两种模式:一种是读锁,一种是写锁。而 StampedLock 反对三种模式,别离是:写锁、乐观读锁和乐观读。其中,写锁、乐观读锁的语义和 ReadWriteLock 的写锁、读锁的语义十分相似,容许多个线程同时获取乐观读锁,然而只容许一个线程获取写锁,写锁和乐观读锁是互斥的。不同的是:StampedLock 里的写锁和乐观读锁加锁胜利之后,都会返回一个 stamp;而后解锁的时候,须要传入这个 stamp。
留神这里,用的是“乐观读”这个词,而不是“乐观读锁”,是要揭示你,乐观读这个操作是无锁的,所以相比拟 ReadWriteLock 的读锁,乐观读的性能更好一些。
StampedLock 的性能之所以比 ReadWriteLock 还要好,其要害是 StampedLock 反对乐观读的形式。
- ReadWriteLock 反对多个线程同时读,然而当多个线程同时读的时候,所有的写操作会被阻塞;
- 而 StampedLock 提供的乐观读,是容许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。
对于读多写少的场景 StampedLock 性能很好,简略的利用场景基本上能够代替 ReadWriteLock,然而StampedLock 的性能仅仅是 ReadWriteLock 的子集,在应用的时候,还是有几个中央须要留神一下。
- StampedLock 不反对重入
- StampedLock 的乐观读锁、写锁都不反对条件变量。
- 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 办法,会导致 CPU 飙升。应用 StampedLock 肯定不要调用中断操作,如果须要反对中断性能,肯定应用可中断的乐观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。
【示例】StampedLock 阻塞时,调用 interrupt() 导致 CPU 飙升
final StampedLock lock
= new StampedLock();
Thread T1 = new Thread(()->{
// 获取写锁
lock.writeLock();
// 永远阻塞在此处,不开释写锁
LockSupport.park();
});
T1.start();
// 保障 T1 获取写锁
Thread.sleep(100);
Thread T2 = new Thread(()->
// 阻塞在乐观读锁
lock.readLock()
);
T2.start();
// 保障 T2 阻塞在读锁
Thread.sleep(100);
// 中断线程 T2
// 会导致线程 T2 所在 CPU 飙升
T2.interrupt();
T2.join();
【示例】StampedLock 读模板:
final StampedLock sl =
new StampedLock();
// 乐观读
long stamp =
sl.tryOptimisticRead();
// 读入办法局部变量
......
// 校验 stamp
if (!sl.validate(stamp)){
// 降级为乐观读锁
stamp = sl.readLock();
try {
// 读入办法局部变量
.....
} finally {
// 开释乐观读锁
sl.unlockRead(stamp);
}
}
// 应用办法局部变量执行业务操作
......
【示例】StampedLock 写模板:
long stamp = sl.writeLock();
try {
// 写共享变量
......
} finally {
sl.unlockWrite(stamp);
}
6. AQS
AbstractQueuedSynchronizer
(简称 AQS)是队列同步器,顾名思义,其次要作用是解决同步。它是并发锁和很多同步工具类的实现基石(如ReentrantLock
、ReentrantReadWriteLock
、CountDownLatch
、Semaphore
、FutureTask
等)。
6.1. AQS 的要点
AQS 提供了对独享锁与共享锁的反对。
在 java.util.concurrent.locks
包中的相干锁(罕用的有 ReentrantLock
、 ReadWriteLock
)都是基于 AQS 来实现。这些锁都没有间接继承 AQS,而是定义了一个 Sync
类去继承 AQS。为什么要这样呢?因为锁面向的是应用用户,而同步器面向的则是线程管制,那么在锁的实现中聚合同步器而不是间接继承 AQS 就能够很好的隔离二者所关注的事件。
6.2. AQS 的利用
AQS 提供了对独享锁与共享锁的反对。
独享锁 API
获取、开释独享锁的次要 API 如下:
public final void acquire(int arg)
public final void acquireInterruptibly(int arg)
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
public final boolean release(int arg)
acquire
– 获取独占锁。acquireInterruptibly
– 获取可中断的独占锁。-
tryAcquireNanos
– 尝试在指定工夫内获取可中断的独占锁。在以下三种状况下回返回:- 在超时工夫内,以后线程胜利获取了锁;
- 以后线程在超时工夫内被中断;
- 超时工夫完结,仍未取得锁返回 false。
release
– 开释独占锁。
共享锁 API
获取、开释共享锁的次要 API 如下:
public final void acquireShared(int arg)
public final void acquireSharedInterruptibly(int arg)
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
public final boolean releaseShared(int arg)
acquireShared
– 获取共享锁。acquireSharedInterruptibly
– 获取可中断的共享锁。tryAcquireSharedNanos
– 尝试在指定工夫内获取可中断的共享锁。release
– 开释共享锁。
6.3. AQS 的原理
ASQ 原理要点:
- AQS 应用一个整型的
volatile
变量来 保护同步状态。状态的意义由子类赋予。- AQS 保护了一个 FIFO 的双链表,用来存储获取锁失败的线程。
AQS 围绕同步状态提供两种基本操作“获取”和“开释”,并提供一系列判断和解决办法,简略说几点:
- state 是独占的,还是共享的;
- state 被获取后,其余线程须要期待;
- state 被开释后,唤醒期待线程;
- 线程等不及时,如何退出期待。
至于线程是否能够取得 state,如何开释 state,就不是 AQS 关怀的了,要由子类具体实现。
AQS 的数据结构
浏览 AQS 的源码,能够发现:AQS 继承自 AbstractOwnableSynchronize
。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
/** 期待队列的队头,懒加载。只能通过 setHead 办法批改。 */
private transient volatile Node head;
/** 期待队列的队尾,懒加载。只能通过 enq 办法增加新的期待节点。*/
private transient volatile Node tail;
/** 同步状态 */
private volatile int state;
}
-
state
– AQS 应用一个整型的volatile
变量来 保护同步状态。- 这个整数状态的意义由子类来赋予,如
ReentrantLock
中该状态值示意所有者线程曾经反复获取该锁的次数,Semaphore
中该状态值示意残余的许可数量。
- 这个整数状态的意义由子类来赋予,如
head
和tail
– AQS 保护了一个Node
类型(AQS 的外部类)的双链表来实现同步状态的治理。这个双链表是一个双向的 FIFO 队列,通过head
和tail
指针进行拜访。当 有线程获取锁失败后,就被增加到队列开端。
再来看一下 Node
的源码
static final class Node {
/** 该期待同步的节点处于共享模式 */
static final Node SHARED = new Node();
/** 该期待同步的节点处于独占模式 */
static final Node EXCLUSIVE = null;
/** 线程期待状态,状态值有: 0、1、-1、-2、-3 */
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
/** 前驱节点 */
volatile Node prev;
/** 后继节点 */
volatile Node next;
/** 期待锁的线程 */
volatile Thread thread;
/** 和节点是否共享无关 */
Node nextWaiter;
}
很显然,Node 是一个双链表构造。
-
waitStatus
–Node
应用一个整型的volatile
变量来 保护 AQS 同步队列中线程节点的状态。waitStatus
有五个状态值:CANCELLED(1)
– 此状态示意:该节点的线程可能因为超时或被中断而 处于被勾销(作废)状态,一旦处于这个状态,示意这个节点应该从期待队列中移除。SIGNAL(-1)
– 此状态示意:后继节点会被挂起,因而在以后节点开释锁或被勾销之后,必须唤醒(unparking
)其后继结点。CONDITION(-2)
– 此状态示意:该节点的线程 处于期待条件状态,不会被当作是同步队列上的节点,直到被唤醒(signal
),设置其值为 0,再从新进入阻塞状态。PROPAGATE(-3)
– 此状态示意:下一个acquireShared
应无条件流传。- 0 – 非以上状态。
独占锁的获取和开释
获取独占锁
AQS 中应用 acquire(int arg)
办法获取独占锁,其大抵流程如下:
- 先尝试获取同步状态,如果获取同步状态胜利,则完结办法,间接返回。
- 如果获取同步状态不胜利,AQS 会一直尝试利用 CAS 操作将以后线程插入期待同步队列的队尾,直到胜利为止。
- 接着,一直尝试为期待队列中的线程节点获取独占锁。
具体流程能够用下图来示意,请联合源码来了解(一图胜千言):
开释独占锁
AQS 中应用 release(int arg)
办法开释独占锁,其大抵流程如下:
- 先尝试获取解锁线程的同步状态,如果获取同步状态不胜利,则完结办法,间接返回。
- 如果获取同步状态胜利,AQS 会尝试唤醒以后线程节点的后继节点。
获取可中断的独占锁
AQS 中应用 acquireInterruptibly(int arg)
办法获取可中断的独占锁。
acquireInterruptibly(int arg)
实现形式相较于获取独占锁办法( acquire
)十分类似,区别仅在于它会通过 Thread.interrupted
检测以后线程是否被中断,如果是,则立刻抛出中断异样(InterruptedException
)。
获取超时期待式的独占锁
AQS 中应用 tryAcquireNanos(int arg)
办法获取超时期待的独占锁。
doAcquireNanos 的实现形式 相较于获取独占锁办法( acquire
)十分类似,区别在于它会依据超时工夫和以后工夫计算出截止工夫。在获取锁的流程中,会一直判断是否超时,如果超时,间接返回 false;如果没超时,则用 LockSupport.parkNanos
来阻塞以后线程。
共享锁的获取和开释
获取共享锁
AQS 中应用 acquireShared(int arg)
办法获取共享锁。
acquireShared
办法和 acquire
办法的逻辑很类似,区别仅在于自旋的条件以及节点出队的操作有所不同。
胜利取得共享锁的条件如下:
tryAcquireShared(arg)
返回值大于等于 0 (这意味着共享锁的 permit 还没有用完)。- 以后节点的前驱节点是头结点。
开释共享锁
AQS 中应用 releaseShared(int arg)
办法开释共享锁。
releaseShared
首先会尝试开释同步状态,如果胜利,则解锁一个或多个后继线程节点。开释共享锁和开释独享锁流程大体类似,区别在于:
对于独享模式,如果须要 SIGNAL,开释仅相当于调用头节点的 unparkSuccessor
。
获取可中断的共享锁
AQS 中应用 acquireSharedInterruptibly(int arg)
办法获取可中断的共享锁。
acquireSharedInterruptibly
办法与 acquireInterruptibly
简直统一,不再赘述。
获取超时期待式的共享锁
AQS 中应用 tryAcquireSharedNanos(int arg)
办法获取超时期待式的共享锁。
tryAcquireSharedNanos
办法与 tryAcquireNanos
简直统一,不再赘述。
7. 死锁
7.1. 什么是死锁
死锁是一种特定的程序状态,在实体之间,因为循环依赖导致彼此始终处于期待之中,没有任何个体能够继续前进。死锁不仅仅是在线程之间会产生,存在资源独占的过程之间同样也
可能呈现死锁。通常来说,咱们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,因为相互持有对方须要的锁,而永恒处于阻塞的状态。
7.2. 如何定位死锁
定位死锁最常见的形式就是利用 jstack 等工具获取线程栈,而后定位相互之间的依赖关系,进而找到死锁。如果是比拟显著的死锁,往往 jstack 等就能间接定位,相似 JConsole 甚至能够在图形界面进行无限的死锁检测。
如果咱们是开发本人的管理工具,须要用更加程序化的形式扫描服务过程、定位死锁,能够思考应用 Java 提供的规范治理 API,ThreadMXBean
,其间接就提供了 findDeadlockedThreads()
办法用于定位。
7.3. 如何防止死锁
基本上死锁的产生是因为:
- 互斥,相似 Java 中 Monitor 都是独占的。
- 长期保持互斥,在应用完结之前,不会开释,也不能被其余线程抢占。
- 循环依赖,多个个体之间呈现了锁的循环依赖,彼此依赖上一环开释锁。
由此,咱们能够剖析出防止死锁的思路和办法。
(1)防止一个线程同时获取多个锁。
防止一个线程在锁内同时占用多个资源,尽量保障每个锁只占用一个资源。
尝试应用定时锁 lock.tryLock(timeout)
,防止锁始终不能开释。
对于数据库锁,加锁和解锁必须在一个数据库连贯中里,否则会呈现解锁失败的状况。
8. 参考资料
- 《Java 并发编程实战》
- 《Java 并发编程的艺术》
- Java 并发编程:Lock
- 深刻学习 java 同步器 AQS
- AbstractQueuedSynchronizer 框架
关注公众号:java宝典
发表回复