本文简要说下Java中的各种锁和类锁机制,还有一些相关的如sleep/yield join等,分析其实现原理,做简单比较。
请点击右侧目录,挑选感兴趣的章节观看。
注意:最近发现本文所讲偏向锁和轻量级锁的代码分析章节有误,请大家移驾参阅死磕Synchronized底层实现–概论系列文章,查看源码分析。待后续有时间我会改正本文内容。
Thread 相关方法,是锁和类锁代码中大量使用的一些基本方法。第一张简单提一下。
sleep
方法如其名,就是让线程休息下,直到指定时间耗尽。pthread_cond_timedwait
方法实现。请点击这里
请点击这里
join方法主要用来等待其他线程运行结束,再继续运行自己的线程代码。
请点击这里
可参考java线程阻塞中断和LockSupport的常见问题
park()
方法时,如果拥有许可就立刻返回;否则也许会阻塞unpark()
会使得本来不可用的许可变为可用状态,解除线程阻塞parkNanos
可指定超时时长、parkUntil
可指定截止时间戳java.util.concurrent.Semaphore
中许可的概念不同,LockSupport的许可每个线程最多能拥有1个park
可因中断、timeout或unpark甚至是毫无理由的返回,所以一般是通过循环检查附加条件是否满足InterruptedException
。此时可通过interrupted(会清除中断标记位)或isInterrupted方法判断是否发生中断UNSAFE.park
和UNSAFE.unpark
实现阻塞和解除阻塞的。请查阅源码解读:Java-并发-锁-LockSupport
LockSupport中的阻塞和唤醒操作是直接作用于Thread对象的,更符合我们队线程阻塞这个语义的理解,使用起来也更方便;
而wait/notify的调用是面向Object
的,线程的阻塞/唤醒对Thread本身来说是被动的。而且notify是随机唤醒的,无法精确地控制唤醒的线程以及唤醒的时机。代码上来说也很麻烦,稍不注意就会写错。
Java中Lock和LockSupport的区别到底是什么?
Java中LockSupport.parkNanos与Sleep的区别是什么?
import java.util.concurrent.locks.LockSupport;
public class LockParkDemo1 {
private static Thread mainThread;
public static void main(String[] args) {
InnerThread it = new LockParkDemo1().new InnerThread();
Thread td = new Thread(it);
mainThread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + " start it");
td.start();
System.out.println(Thread.currentThread().getName() + " block");
// LockSupport.park(Thread.currentThread());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " continue");
}
class InnerThread implements Runnable{
@Override
public void run() {
int count = 5;
while(count>0){
System.out.println("count=" + count);
count--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" wakup others");
LockSupport.unpark(mainThread);
}
}
}
程序输出结果如下:
main start it
main block
Thread-0 wakup others
main continue
import java.util.concurrent.locks.LockSupport;
/**
* Created by chengc on 2018/12/15.
*/
public class BlockerTest
{
public static void main(String[] args)
{
Thread.currentThread().setName("Messi");
LockSupport.park("YangGuang");
}
}
jps
查看该进程pid:$ jps
73900 BlockerTest
jstack
:$ jstack -l 73900
"Messi" #1 prio=5 os_prio=31 tid=0x00007fe34c822000 nid=0x1b03 waiting on condition [0x0000700006470000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ac8fcc0> (a java.lang.String)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at demos.concurrent.lock.park.BlockerTest.main(BlockerTest.java:13)
Locked ownable synchronizers:
- None
可以看到我们的主线程Messi
处于WAITING
状态,而且原因是parking
。Blocker
对象时个java.lang.String
。
AQS(AbstractQueuedSynchronizer)
就是利用了LockSupport的相关方法来控制线程阻塞或者唤醒。
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
synchronized
区域的线程可以拿到对象的Object-Monitor。具体有3种用法,作用域不同,在后面例子中介绍。synchronized
具有可重入性关于synchronized的实现原理可以查看这篇文章: Java-并发-锁-synchronized
ReentrantLock和synchronized对比如下:
可重入 | 等待可中断 | 公平性 | 绑定对象数 | 性能优化 | |
---|---|---|---|---|---|
synchronized | 支持 | 不支持 | 非公平 | 只能1个 | 较多 |
ReentrantLock | 支持 | 支持 | 非公平/公平 | 可以多个 | - |
从JDK 1.6
开始HotSpot虚拟机团队花了很多精力实现各种锁的优化技术,主要目的很明显就是为了更少的阻塞、更少的竞争、更高效的获取锁和释放锁,说白了就是提高多线程需要访问共享区间的执行效率。
JavaHeap
中的对象主要包括三部分:
这部分是对象真正存储的有效信息,各种类型字段内容,还包括父类继承过来的信息。
只是占位符,因为HotSpot内存管理要求对象起始地址必须是8字节整数倍,即对象长度必是8字节整数倍。而对象头一般来说已经是整数倍,所以字节填充主要是为实例数据填充。
Mark Word
,即对象运行时数据。他的内部字节长度分布与含义非固定,节约空间。存有如hashCode、分代年龄、锁标志信息等。Klass Pointer
,即类型指针(用于确定对象属于的类),指向方法区中的该对象Class类型对象。下面是一个32位的HotSpot虚拟机中 MarkWord示意图:
Mark Word
中的最后2bit就是锁状态的标志位,用来记录当前对象的锁状态:
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁) | 10 | 执行重量级锁定的指针 |
GC标记 | 11 | 空,不需要记录信息 |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
注意上图中的后方1bit还会在无锁和偏向锁时不同以区分两种锁状态,因为他们的最后2bit锁标志位都是01。
jdk8/hotspot/src/share/vm/oops/markOop.hpp
描述了对象头部信息,有兴趣的读者可以看看。
使用互斥锁的时候,往往阻塞时间其实很短,但线程阻塞和唤醒操作由用户态转为内核态,性能开销大。
这个时候自旋锁产生了,他的思想很朴素,前提是有多于1个CPU:
-XX:PreBlockSpin
修改自旋次数,默认为10次。这个自适应自旋锁思想也很朴素,相当于基于HBO(历史)的优化:
锁消除,顾名思义,就是JVM在编译器运行时会扫描代码,当检查到那些不可能存在共享区竞争但却有互斥同步的代码,直接将这样的多此一举的锁消除。
除了那些经验不足的编程人员会写无意义的同步代码,还有很多是JVM帮程序加上的,比如以下代码:
public String connectStrs(String str1, String str2, String str3){
return str1 + str2 + str3;
}
会因为String
是不可变类,反复产生新对象,所以被JVM自动优化成以下形式(JDK1.5之前版本,1.5之后是StringBuilder了):
public String connectStrs(String str1, String str2, String str3){
StringBuffer sb = new StringBuffer;
sb.append(str1);
sb.append(str2);
sb.append(str3);
}
此时,StringBuffer
是带锁的了。
锁消除的主要依据是逃逸分析,详见Java-JVM-逃逸分析。这里简单说下,就是指代码中的位于JavaHeap的所有数据都不会逃逸导致被其他线程访问,那就将可将他们作为栈内数据,作为线程私有。这样一来同步锁就没有意义了,可以消除。
这个名字有点诡异,其实说白了就是扩大锁的范围。
什么?不是说好了要尽量减小同步锁的适用范围,缩短占有锁的时间吗?!
其实,JVM是会在反复在段代码中对同一对象加锁的情况进行锁粗化优化的。
比如
public String optimizedConnectStrs(String str1, String str2, String str3){
StringBuffer sb = new StringBuffer;
sb.append(str1);
sb.append(str2);
sb.append(str3);
}
这种情况每个append
都会执行如下代码:
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
也就是说会反复对sb
这个对象监视器加synchronized同步锁。
此时,JVM就会进行优化,将锁包住多次append操作的起始,只需加锁一次。这就是所谓锁粗化。
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
重量级锁就是前面提到过的传统的基于ObjectMonitor
的锁synchronized
,底层使用MutexLock
。使用这类互斥锁的时候,往往阻塞时间其实很短,但线程阻塞和唤醒操作会有用户态和内核态转换,性能开销大。
上述的monitorLock底层采用MutexLock实现,他和自旋锁SpinLock对比如下:
MutexLock | SpinLock | |
---|---|---|
原理 | 尝试获取锁,若可得到就占有;若不能,就阻塞等待 | 尝试获取锁,若可得到就占有。若不能,空转并持续尝试直到获取 |
使用场景 | 当线程进入阻塞没有很大问题,或需要等待一段足够长的时间才能获取锁 | 当线程不应该进入睡眠如中断处理等或只需等待非常短的时间就能获取锁 |
缺点 | 引起线程切换和线程调度开销大 | 线程空跑CPU等待,浪费资源 |
JDK1.6后引入
。
该轻量级锁的名字是相对于传统的那些锁来说,认为传统同步锁(重量级锁)开销极大,大部分锁其实在同步期间并没有竞争,没必要使用重量级锁导致不必要开销。
Lock Record
空间,他是一个锁对象头的Mark Word
的内容拷贝,名为Displaced Mark Word
。Lock Record
的指针MarkWord
最后两bit
标记为 00,表示轻量级锁状态。MarkWord
是否指向当前线程的栈帧。如果是,就说明当前线程拥有了该对象锁,这是锁重入,可以开始执行同步块内代码;否则说明被其他线程拥有锁就膨胀为重量级锁,此时会标记为10。在膨胀过程中,其他线程全部阻塞等待,而当前线程会使用4.1章节中提到的自旋锁等待膨胀完成,避免阻塞。待膨胀为重量级锁完成后重新竞争同步锁。轻量级锁膨胀为重量级锁的过程可以在jdk8/hotspot/src/share/vm/runtime/synchronizer.cpp
的ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object)
方法代码中看到,这里不再展开。
解锁过程如下:
轻量级锁的依据是大部分锁在同步期间没有竞争,从而用CAS方式避免了使用互斥量开销。
但如果线程竞争锁激烈的场景,就会额外加上CAS的开销。此时反而效率低于所谓的重量级锁了。
JDK1.6后引入
。
相对于轻量级锁是消除无竞争时用CAS
消除同步原语,偏向锁是直接在无竞争时消除所有同步。
当开启了偏向锁配置(-XX:+UserBiasedLocking
)时,偏向锁加锁过程如下:
Mark Word
中线程ID是否是当前线程ID。如果是,那么可以直接执行同步块代码,JVM可以不用再进行加解锁、更新偏向信息等同步操作,效率提高很多SafePoint
)。此时需撤销偏向锁,会导致stop the word
暂停拥有偏向锁线程,判断是否处于被锁定状态:
偏向锁的释放:
偏向锁释放锁的动作是被动的,如加锁过程中第三步即在其他线程尝试获取竞争偏向锁时才会触发偏向锁释放过程。上面说的安全点指在该时间点上没有代码运行。
初始分配对象时分为开启/不开启偏向锁模式。注意,初始时,偏向锁模式开启,但是拥有锁线程ID为0,代表未锁定。
在学习了前面几种类别的锁后,再把synchronized加锁过程串起来讲一下,前提已经打开偏向锁:
ownerId
设为自己Mark Word
为Displaced Mark Word
,且CAS(锁对象, MarkWord, DisplacedMarkWord)wait
notify
还有个notifyAll
都是线程通信的常用手段。
有一个先导概念就是对象锁和类锁,他们其实都是对象监视器Object Monitor
,只不过类锁是类对象的监视器,可以看另一篇文章:
Java-并发-锁-synchronized之对象锁和类锁
interrupted
方法,会抛出InterruptedException
,且中断标记会被自动清理。notify
的线程拥有,直到退出synchronized
块。IllegalMonitorStateException
。notifyAll
的线程拥有。IllegalMonitorStateException
。请参考文档Java-多线程-wait/notify
经常面试会问这个问题,往往我们都是网上查资料死记硬背。现在我们都看完了源码(sleep源码点这里),可以得出以下结论
等待 | 唤醒 | 唤醒全部 | |
---|---|---|---|
Object | wait | notify | notifyAll |
Condition | await | signal | signalAll |
中断 | 超时精确 | Deadline | |
---|---|---|---|
wait | 可中断 | 可为纳秒 | 不支持 |
await | 支持可中断/不可中断 | 可为纳秒 | 支持 |
全部唤醒 | 唤醒顺序 | 执行前提 | 逻辑 | |
---|---|---|---|---|
notify | 支持,notifyAll | 随机(jdk写的,其实cpp源码是一个wait_queue,FIFO) | 拥有锁 | 从wait_list取出,放入entry_list,重新竞争锁 |
signal | 支持,signalAll | 顺序唤醒 | 拥有锁 | 从condition_queue取出,放入wait_queue,重新竞争锁 |
active
状态。调用wait方法后,线程对象被放入wait_queue。而notify会按FIFO方法从wait_queue中取得一个对象并放回entry_list,这样该线程可以重新竞争synchronized同步锁了。private static ReentrantLock lock = new ReentrantLock();
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();
// 生产者
public void produce(E item) {
lock.lock();
try {
while(isFull()) {
// 数据满了,生产者就阻塞,等待消费者消费完后唤醒
notFull.await();
}
// ...生产数据代码
// 唤醒消费者线程,告知有数据了,可以消费
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 消费者
public E consume() {
lock.lock();
try {
while(isEmpty()) {
// 数据空了,消费者就阻塞,等待生产者生产数据后唤醒
notEmpty.await();
}
// ...消费数据代码
// 唤醒生产者者线程,告知有数据了,可以消费
notFull.signal();
return item;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return null;
}
这样好处就很明显了。如果使用Object,那么唤醒的时候也许就唤醒了同类的角色线程。而使用condition可以在只有一个锁的情况下,实现我们想要的只唤醒对方角色线程的功能。
JDK中大量代码使用了CAS,底层是调用的sun.misc.Unsafe
,如Unsafe.compareAndSwapInt
方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
该方法第一个参数为对象,第二个参数为指定field
在对象中的偏移量,第三个为期望值,最后一个是要更新的目标值。
CAS的基本思想就是原子性的执行以下两个操作:
那么,java是怎么实现这个操作的原子性的呢?我们接着往下看
透过前面的代码,可以看到compareAndSwapInt
是一个JNI
调用。
在jdk8/hotspot/src/share/vm/prims/unsafe.cpp
中可以找到以下内容:
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 获取该filed内存地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 调用Atomic.cmpxchg方法
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
鉴于本人能力有限,就不再继续向下了。有兴趣的读者可以研究下jdk8/hotspot/src/share/vm/runtime/atomic.cpp
也可参考文章:
JVM的CAS实现原理解析
Java并发编程-无锁CAS与Unsafe类及其并发包Atomic
ReentrantLock
是使用最广的、最出名的AQS(AbstractQueuedSynchronizer)
系列的可重入锁。
它属于是高层API。和synchronized对比如下:
可重入性 | 等待可中断 | 公平性 | 绑定对象数 | 性能优化 | |
---|---|---|---|---|---|
synchronized | 支持 | 不支持 | 非公平 | 只能1个 | 较多 |
ReentrantLock | 支持 | 支持 | 非公平/公平 | 可以多个 | - |
synchronized
绑定一个Object用来wait
, notify
等操作;而ReentrantLock可以newCondition多次等到多个Condition实例,执行await
, signal
等方法。限于篇幅,这里可以大概说下其原理。
AQS全称AbstractQueuedSynchronizer
,他是ReentrantLock
内部类NonfairSync
和FairSync
的父类Sync
的父类,其核心组件如下:
该Node
就是AQS的内部类,这里可以简单看看Node定义:
static final class Node {
// 表明等待的节点处于共享锁模式,如Semaphore:addWaiter(Node.SHARED)
static final Node SHARED = new Node();
// 表明等待的节点处于排他锁模式,如ReentranLock:addWaiter(Node.EXCLUSIVE)
static final Node EXCLUSIVE = null;
// 线程已撤销状态
static final int CANCELLED = 1;
// 后继节点需要unpark
static final int SIGNAL = -1;
// 线程wait在condition上
static final int CONDITION = -2;
// 使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播
static final int PROPAGATE = -3;
// 这个waitStatus就是存放以上int状态的变量,默认为0
// 用volatile修饰保证多线程时的可见性和顺序性
volatile int waitStatus;
// 指向前一个Node的指针
volatile Node prev;
// 指向后一个Node的指针
volatile Node next;
// 指向等待的线程
volatile Thread thread;
// condition_queue中使用,指向下一个conditionNode的指针
Node nextWaiter;
// 判断是否共享锁模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前驱结点,当前驱结点为null时抛出NullPointerException
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 用来初始化wait队列的构造方法;也被用来做共享锁模式
Node() {
}
// 在addWaiter方法时,将指定Thread以指定模式放置
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// Condition使用的构造方法
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
默认采用非公平的实现NonFairSync
。
lock()
方法流程如下图:
可以看到,lock()
方法最核心的部分就是可重入获取许可(state),以及拿不到许可时放入一个AQS实现的双向链表中,调用LockSupport.park(this)
将自己阻塞。就算阻塞过程被中断唤醒,还是需要去拿锁,直到拿到为止,注意,此时在拿到锁之后还会调用selfInterrupt()
方法对自己发起中断请求。
他的实现和非公平锁有少许区别:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 这里不再有非公平锁的
// if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());
// 也就是说,公平锁中,必须按规矩办事,不能抢占
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) {
// 这里多了一个 !hasQueuedPredecessors(),也就是不需要考虑wait链表
// 否则就老实按流程走acquireQueued方法
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;
}
}
下面看看的hasQueuedPredecessors
实现
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// h != t 代表wait链表不为空状态
// (s = h.next) == null代表wait链表已经初始化
// s.thread != Thread.currentThread()代表当前线程不是第一个在wait链表排队的线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
compareAndSetState(0, acquires)
进行抢占,而公平锁必须老老实实FIFO形式排队;但unlock
唤醒的时候是没有区别的。volatile
,保证有序性和可见性unsafe.compareAndSwapInt(this, stateOffset, expect, update);
此类的CAS操作,保证原子性,同时在竞争小的时候效率胜过synchronized
LockSupport.park(this)
阻塞ReentrantLock和synchronized对比如下:
可重入 | 等待可中断 | 公平性 | 绑定对象数 | 性能优化 | |
---|---|---|---|---|---|
synchronized | 支持 | 不支持 | 非公平 | 只能1个 | 较多 |
ReentrantLock | 支持 | 支持 | 非公平/公平 | 可以多个 | - |
Condition
类其实是位于java.util.concurrent.locks
的一个接口类。他的一个常用实现类是AQS的非静态内部类ConditionObject
:
public class ConditionObject implements Condition, java.io.Serializable
虽说ConditionObject是public
修饰,但不能直接使用,因为他是非静态内部类,必须先实例化AQS的实例。而AQS定义如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable
很明显,他是一个抽象类,不能直接实例化。也就是说必须使用继承他的子类才能实例化,从而使用ConditionObject
。
我们最常使用的是配套ReentrantLock和Condition使用:
ReentrantLock lock = new ReentrantLock(true);
Condition condition = lock.newCondition();
condition.await();
condition.signal();
下面简单分析下后面3步代码实现:
具体请查阅
Java-并发-Condition
这里只给出流程总结:
await
方法,将当前线程加入AQS.condition_queue
,且会顺便清理其中不为CONDITION
状态的结点await
方法,让当前线程释放所有的锁许可(state归0)await
方法,将当前线程阻塞,直到该线程被放入了AQS.wait_queue
signal
方法,将从AQS.condition_queue
队列的头结点开始往后遍历,从AQS.condition_queue
中将该线程结点移除,并放回AQS.wait_queue
,并根据前驱结点是否已经撤销或异常按需唤醒当前结点。注意,此过程只要成功移动一个节点,遍历就结束了,也就是说每次signal
方法最多只能从AQS.condition_queue
中移动一个结点到AQS.wait_queue
。signal
方法,在上述遍历移动节点过程中会顺便清理掉AQS.condition_queue
中那些状态不为CONDITION
的结点await
方法,阻塞的线程因为被signal
方法重新放入AQS.wait_queue
而被其他前驱结点唤醒,此时有几种情况:
AQS.wait_queue
。wait_queue
中该结点的前驱结点执行unlock
方法时唤醒。处理同情况1signal
方法前调用中断方法唤醒,需要重设interruptMode
signal
方法后调用中断方法唤醒,需要重设interruptMode
await
方法,该节点调用acquireQueued
走申请锁许可流程。注意,如果此时申请不到锁,线程又会被LockSupport.park
阻塞。await
方法,会又一次顺便清理其中不为CONDITION
状态的结点await
方法,按阻塞前后收到中断请求的情况按需发起中断await
方法返回,可继续执行用户代码上面wait/notify章节已经比较过了 ,请点击这里查看
Condition特点如下:
lock
的。lock维护了一个wait_queue
,用于存放等待锁的线程。而Condition也维护了一个condition_queue
。当拥有锁的线程调用await
方法,就会被放入condition_queue
;当调用signal方法,会从condition_queue
选头一个满足要求的节点移除然后放入wait_queue
,重新竞争lock。现在大家开发程序,大多是在多线程场景,就会用到各种锁。但其实往往读和读之间是不冲突的,是无状态无修改的,不应该互相互斥。我们往往只需在读写或者写与写之间互斥即可。在JDK中就直接提供了一个ReadWriteLock
,互斥关系如下表:
读 | 写 | |
---|---|---|
读 | 不互斥 | 互斥 |
写 | 互斥 | 互斥 |
ReadWriteLock的其他重要知识点如下:
共享-独占锁
lock
就必须配套调用几次unlcok
使用时一般是这几个api:
// 获得读写锁实例
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 读锁加锁,可与其他线程读锁共享,阻塞其他写锁申请,可重入
lock.readLock().lock()
// 读锁解锁,调用次数必须和lock相同
lock.readLock().unlock();
// 写锁加锁,阻塞其他线程读写锁申请,可重入
lock.writeLock().lock();
// 写锁解锁,调用次数必须和lock相同
lock.writeLock().unlock();
ReadWriteLock场景应用
见Java-并发-锁-ReadWriteLock
已经在第二章的park cpp分析一节中分析过了。
LockSupport.park底层就是用的Unsafe.park,而JDK中很多地方使用了Unsafe。这两个锁偏底层,建议用基于他们或AQS的高级锁如ReentrantLock
、CountDownLatch
、CyclicBarrier
等。
Java 8 StampedLock,ReadWriteLock以及synchronized的比较
Java 8 并发篇 - 冷静分析 Synchronized(下)
Synchronized的原理及自旋锁,偏向锁,轻量级锁,重量级锁的区别
浅谈Mutex (Lock)
jdk源码剖析二: 对象内存布局、synchronized终极原理
JVM源码分析之synchronized实现
Java线程源码解析之yield和sleep
java并发编程之LockSupport