???欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
- 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
- 导航
- 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
- 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
- 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
- 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
- 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。??? ?? 欢迎订阅本专栏 ??
博客目录
-
-
- 1.什么是可重入锁?
- 2.ReentrantLock 非公平锁获取与释放?
- 3.ReentrantLock 公平锁获取和释放?
- 4.公平锁和非公平锁总结?
- 5.为什么非公平锁会造成线程饥饿?
- 6.ReentrantLock 使用和理解?
- 7.什么是读写锁?
- 8.ReentrantReadWriteLock 原理?
- 9.ReentrantReadWriteLock 写锁的获取与释放?
- 10.读写锁 c 值的含义
- 11.ReentrantReadWriteLock 读锁的获取与释放?
- 12.ReentrantReadWriteLock 的锁降级问题?
- 13.LockSupport 工具是什么?
- 14.parkNanos 中 blocker 含义
- 15.park unpark 原理
- 16.Synchronized 和 Lock 区别?
- 17.Lock 的 Condition 接口作用?
- 18.Condition 的实现原理?
-
1.什么是可重入锁?
首先明确下 synchronized 和 lock 接口均为可重入锁。
重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。
实现原理如下:
- 线程再次获取锁.锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
- 锁的最终释放.线程重复 n 次获取了锁,随后在第 n 次释放该锁后,其他线程能够获取到该锁.锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于 0 时表示锁已经成功释放。
2.ReentrantLock 非公平锁获取与释放?
使用非公平锁加锁过程:
- ReentrantLock:lock()。
- NonfairSync:lock()。
- AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。
final boolean nonfairTryAcquire(int acquires){
final Thread current = Thread.currentThread();int c = getState();if (c == 0){
if (compareAndSetState(0, acquires)){
setExclusiveOwnerThread(current);//以cas原子操作更新statereturn true;}} else if (current == getExclusiveOwnerThread()){
int nextc = c + acquires;if (nextc < 0)// overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求 ReentrantLock 在释放同步状态时减少同步状态值
在使用非公平锁时,解锁方法 unlock()调用轨迹如下。
- ReentrantLock:unlock()
- AbstractQueuedSynchronizer:release(int arg)。
- Sync:tryRelease(int releases)。
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;
}
3.ReentrantLock 公平锁获取和释放?
公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO 的规则从队列中取到自己。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非公平锁大。
使用公平锁时,加锁方法 lock()调用轨迹如下。
- ReentrantLock:lock()。
- FairSync:lock()
- AbstractQueuedSynchronizer:acquire(int arg)。
- ReentrantLock:tryAcquire(int acquires)。
在第 4 步真正开始加锁,下面是该方法的源代码。
在使用公平锁时,解锁方法 unlock()调用轨迹如下。
- ReentrantLock:unlock()
- AbstractQueuedSynchronizer:release(int arg)。
- Sync:tryRelease(int releases)。
在第 3 步真正开始释放锁,下面是该方法的源代码。
c==0 代表是第一次进入,不是重复获取锁,所以不需要加其他的判断,第一步需要读取 volatile 变量 state
公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。该方法与 nonfairTryAcquire(int acquires)比较,唯一不同的置为判断条件多了 hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;final void lock(){
acquire(1);}protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();int c = getState();//获取锁,读取volatile变量stateif (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;}
}
释放锁的过程和非公平锁一样
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);//释放锁,写volatile变量statereturn free;
}
4.公平锁和非公平锁总结?
ReentrantLock 是 Java 并发包提供的一种重入锁,它可以作为公平锁或非公平锁来使用。下面对 ReentrantLock 的公平锁和非公平锁进行总结:
- 公平锁(Fair Lock):
- 特点:公平锁遵循"先到先得"的原则,即线程按照请求锁的顺序依次获取锁。当有多个线程竞争锁时,等待时间最长的线程会被优先获取锁。
- 实现:公平锁的实现需要维护一个等待队列,当线程请求锁时,如果锁已被其他线程占用,则该线程会进入等待队列,按照 FIFO 的顺序等待。
- 优点:公平锁能够避免饥饿现象,所有线程都有机会获得锁,公平性比较高。
- 缺点:由于涉及到频繁的线程切换和调度,可能会导致性能下降。
- 非公平锁(Non-Fair Lock):
- 特点:非公平锁是一种抢占式的锁策略,线程在请求锁时,不管是否有其他线程在等待,它都会直接尝试获取锁,不会考虑等待队列中的顺序。
- 实现:非公平锁的实现不维护等待队列,当线程请求锁时,直接尝试获取锁,如果锁被其他线程占用,则进入自旋等待或阻塞状态。
- 优点:非公平锁由于省去了等待队列的维护和线程调度的开销,通常比公平锁具有更高的吞吐量和更低的延迟。
- 缺点:非公平锁可能会导致某些线程长时间无法获取锁,产生线程饥饿现象,不具备公平性。
5.为什么非公平锁会造成线程饥饿?
首先说下公平锁.假设目前 AQS 的同步队列中有 A B C 三个线程.线程 A 排在最前边.当线程 A 获取锁的同时,线程 D 也要获取锁.此时 D 线程先会通过 tryAcquire 方法判断是否自己是同步队列的头结点,如果不是,则乖乖的去同步队列中等待.线程 A 处于无竞争状态下获取锁.因此说公平锁完全按照线程先后进行 FIFO 的获取锁。非公平锁不必唤醒所有线程,cpu 开销小.
-
ReentrantLock:lock()。
-
FairSync:lock()。
-
AbstractQueuedSynchronizer:acquire(int arg)。
-
ReentrantLock:tryAcquire(int acquires)
再说非公平锁.假设目前 AQS 的同步队列中有 A B C 三个线程.线程 A 排在最前边.当线程 A 获取锁的同时,线程 D 也要获取锁.此时 D 线程直接进行争夺,虽说 D 是后来的,但是作为非公平锁,会直接进行 cas 的竞争.如果竞争成功,线程 A 继续作为头结点,等待 D 线程释放锁,参与下一轮竞争.如果竞争失败,线程 D 也需要乖乖的到同步队列中排队。从这段话我们可以看到,如果线程 A 一直竞争不到锁,那么就会一直留在同步队列中等待,造成线程饥饿,没事儿可干。
使用非公平锁时,加锁方法 lock()调用轨迹如下。
- ReentrantLock:lock()。
- NonfairSync:lock()。
- AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。
6.ReentrantLock 使用和理解?
在 ReentrantLock 中,调用 lock()方法获取锁;调用 unlock()方法释放锁。
注意:lock()是在 try 的外面,为了防止获取锁失败,还去释放锁.
ReentrantLock 的实现依赖于 Java 同步器框架 AbstractQueuedSynchronizer(本文简称之之为 AQS)。AQS 使用一个整型的 volatile 变量(命名为 state)来维护同步状态,这个 volatile 变量是 ReentrantLock 内存语义实现的关键。
ReentrantLock 默认是非公平锁,如果需要公平锁,参数传入 true
7.什么是读写锁?
读写锁,读读不排它,读写排它,写写排它.。Java 并发包提供读写锁的实现是 ReentrantReadWriteLock,它提供的特性如下所示:
特性 | 说明 |
---|---|
公平性选择 | 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平 |
重进入 | 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁 |
锁降级 | 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁 |
ReentrantReadWriteLock 作为 ReadWriteLock 的子类.ReadWriteLock 仅定义了获取读锁和写锁的两个方法,即 readLock()方法和 writeLock()方法
public interface ReadWriteLock {
/** * Returns the lock used for reading *@return the lock used for reading */Lock readLock();/** * Returns the lock used for writing *@return the lock used for writing */Lock writeLock();
}
而其实现 ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法以及描述如下所示。
方法名称 | 描述 |
---|---|
int getReadLockCount() | 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如,仅一个线程,它连续获取(重进入)了 n 次读锁,那么占据读锁的线程数是 1.但该方法返回 n |
int getReadHoldCount() | 返回当前线程获取读锁的次数。该方法在 Java6 中加入到 ReentrantReadWriteLock 中,使用 ThreadLocal 保存当前线程获取的的次数,这也使得 Java6 的实现变得更加复杂 |
boolean isWriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
如下代码,Cache 组合一个非线程安全的 HashMap 作为缓存的实现,同时使用读写锁的读锁和写锁来保证 Cache 是线程安全的.在读操作 get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作 put(String key,Object value)方法和 clear()方法,在更新 HashMap 必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续.Cache 使用读写锁提升读操作的并发性,也保证每次操作对所有的读写操作的可见性,同时简化了编程方式。
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();static Lock r = rwl.readLock();static Lock w = rwl.writeLock();//获取一个key对应的valuepublic static final Object get(String key){
r.lock();try {
return map.get(key);} finally {
r.unlock();}}//设置key对应的value,并返回旧的valuepublic static final Object put(String key, Object value){
w.lock();try {
return map.put(key, value);} finally {
r.unlock();}return null;}//清空所有的内容public static final void clear(){
w.lock();try {
map.clear();} finally {
w.unlock();}}
}
8.ReentrantReadWriteLock 原理?
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态 state.回想 ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写,划分方式如下图所示:
9.ReentrantReadWriteLock 写锁的获取与释放?
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,**读锁已经被获取(读状态不为 0)**或者当前线程不是已经获取写锁的线程(意思是非重入状态),则当前线程进入等待状态:
protected final boolean tryAcquire(int acquires){
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 acquiresetState(c + acquires);return true;}if (writerShouldBlock()||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;
}
从代码上看,如果 c!= 0,w=0。说明 r!=0。则 return false,当前线程进入同步队列等待。否则进行加锁,更改 state 的值,如果 c==0,这说明读写锁都没有。则进行判断是否写线程需要 block 或者进行更新同步状态失败。否则设置当前线程为 owner 线程,获取锁成功。写锁的释放与 ReentrantLock 的释放过程基本类似,每次释放均减少写状态,当写状态为 0 时表示写锁已被释放.
ReentrantReadWriteLock
是 Java 并发包提供的读写锁,用于实现读写分离的并发控制。在ReentrantReadWriteLock
中,有一个名为"c"的整型变量,用于表示读写状态。
10.读写锁 c 值的含义
c 的值及其含义如下:
- c = 0:
- 表示当前没有任何线程持有读锁或写锁。即没有线程正在读取或写入共享资源。
- c > 0:
- 表示当前有一个或多个线程持有读锁。c 的值等于持有读锁的线程数量。
- c = -1:
- 表示当前有一个线程持有写锁。读写锁中只能有一个线程持有写锁,因此 c 的值只会是 0 或-1。
读写锁允许多个线程同时获取读锁,只要没有线程持有写锁,而写锁是独占的,只能有一个线程获取写锁。读锁和写锁之间是互斥的,即当有线程持有写锁时,其他线程无法获取读锁或写锁,直到写锁被释放。
11.ReentrantReadWriteLock 读锁的获取与释放?
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为 0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。类似于修改文件的时候不能打开查看.
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);//读状态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);
}
在 tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
12.ReentrantReadWriteLock 的锁降级问题?
锁降级指的是写锁降级成为读锁.如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级.锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。state 是包含读写锁的状态,保证安全性.
为什么先获取读锁才能释放写锁:
因为 state 是包含读写锁的状态,如果先释放写锁,则获取读锁的时候,写锁被其他线程获取,不能再次获取到读锁.而拥有写锁的时候可以同时获取读锁,所以需要先获取读锁才能释放写锁.原理上来说只能有一个线程持有写锁.
RentrantReadWriteLock 为什么不支持锁升级:
读锁升级为写锁。首先自己有读锁,之后拿到写锁。如果有两个读锁同时升级为写锁。那么只有一个能升级成功。但是这两个线程同时拥有读锁。其中一个线程还一直在申请写锁。其他线程获取到读锁时,不能获取写锁.这就会造成死锁。但是锁降级是可以的。因为写锁只有一个线程占有。
13.LockSupport 工具是什么?
当需要阻塞或唤醒一个线程的时候,都会使用 LockSupport 工具类来完成相应工作.LockSupport 定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而 LockSupport 也成为构建同步组件的基础工具.(对比使用 synchronized 方法的阻塞唤醒功能.).LockSupport 定义了一组以 park 开头的方法用来阻塞当前线程,以及 unpark(Thread thread)方法来唤醒一个被阻塞的线程。
方法名称 | 描述 |
---|---|
void park() | 阻塞当前线程,如果调用 unpar ik(Thread thread)方法或者当前线程被中断,才能从 park()方法返回 |
void parkNanos(long nanos) | 阻塞当前线程,最长不超过 na anos 纳秒,返回条件在 park(的基础上增加了超时返回 |
void parkUntil(long deadline) | 阻塞当前线程,直到 deadline 日时间(从 1970 年开始到 deadline 时间的毫秒数) |
void unpark(Thread thread) | 唤醒处于阻塞状态的线程 thread |
先唤醒线程,再阻塞线程,线程不会真的阻塞;但是先唤醒线程两次再阻塞两次时就会导致线程真的阻塞。
LockSupport 就是通过控制变量_counter
来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制。
- 当调用
park()
方法时,会将_counter 置为 0,同时判断前值,小于 1 说明前面被unpark
过,则直接退出,否则将使该线程阻塞。 - 当调用
unpark()
方法时,会将_counter 置为 1,同时判断前值,小于 1 会进行线程唤醒,否则直接退出。
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有 1 个。当调用 park 方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而 unpark 则相反,它会增加一个凭证,但凭证最多只能有 1 个。 - 为什么可以先唤醒线程后阻塞线程?
因为 unpark 获得了一个凭证,之后调用 park 因为有凭证消费,故不会阻塞。 - 为什么唤醒两次后阻塞两次会阻塞线程。
因为凭证的数量最多为 1,连续调用两次 unpark 和调用一次 unpark 效果一样,只会增加一个凭证;而调用两次 park 却需要消费两个凭证。
14.parkNanos 中 blocker 含义
parkNanos(long nanos)和 parkNanos(Object blocker,long nanos)异同?
对比 parkNanos(long nanos)方法和 parkNanos(Object blocker,long nanos)方法在使用场景上有什么不同?
LockSupport 增加了 3 个方法
- park(Object blocker)
- parkNanos(Object blocker,long nanos)
- parkUntil(Object blocker,long deadline)
用于实现阻塞当前线程的功能,其中参数 blocker 是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。
从右图的线程 dump 结果可以看出,代码片段的内容都是阻塞当前线程 10 秒,但从线程 dump 结果可以看出,有阻塞对象的 parkNanos 方法能够传递给开发人员更多的现场信息.这是由于在 Java 5 之前,当线程阻塞(使用 synchronized 关键字)在一个对象上时,通过线程 dump 能够查看到该线程的阻塞对象,方便问题定位,而 Java 5 推出的 Lock 等并发工具时却遗漏了这一点,致使在线程 dump 时无法提供阻塞对象的信息.因此,在 Java 6 中,LockSupport 新增了上述 3 个含有阻塞对象的 park 方法,用以替代原有的 park 方法。
15.park unpark 原理
每个线程都有自己的一个 Parker 对象,由三部分组成 _counter
, _cond
和 _mutex
打个比喻线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter
就好比背包中的备用干粮(0 为耗尽,1 为充足)
-
调用 park 就是要看需不需要停下来歇息
-
- 如果备用干粮耗尽,那么钻进帐篷歇息
- 如果备用干粮充足,那么不需停留,继续前进
-
调用 unpark,就好比令干粮充足
-
- 如果这时线程还在帐篷,就唤醒让他继续前进
- 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮
16.Synchronized 和 Lock 区别?
synchronized | Lock | |
---|---|---|
原始构成 | sync 是 JVM 层面的,底层通过monitorenter 和monitorexit 来实现的 |
Lock 是 JDK API 层面的 |
使用方法 | sync 不需要手动释放锁 |
Lock 需要手动释放 |
是否可中断 | sync 不可中断,除非抛出异常或者正常运行完成 |
Lock 是可中断的,通过调用interrupt() 方法 |
是否为公平锁 | sync 只能是非公平锁 |
Lock 既能是公平锁,又能是非公平锁 |
绑定多个条件 | sync 不能,只能随机唤醒 |
Lock 可以通过Condition 来绑定多个条件,精确唤醒 |
synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
Lock 接口提供的 synchronized 关键字不具备的主要特性
特性 | 描述 |
---|---|
尝试非阻塞地获取锁 | 当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁 |
能被中断地获取锁 | 与 synchronized 不同,获取到锁的线程能能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 | 在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回 |
Lock 的 API:
方法名称 | 描述 |
---|---|
void lock() | 获取锁,调用该方法当前线程将会获取锁,当锁获得后,从该方法返回 |
void lockInterruptibly()) throws InterruptedException | 可中断地获取锁,和 lock0 方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程 |
boolean tryLock() | 尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回 true,否则返回 false |
boolean tryLock(long time.TimeUnit unit)throws InterruptedException | 超时的获取锁,当前线程在以下 3 种情况下会返回: ① 当前线程在超时时间内获得了锁 ② 当前线程在超时时间内被中断 ③ 超时时间结束,返回 false |
void unlock() | 释放锁 |
Condition newCondition() | 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的 waitO 方法,而调用后,当前线程将释放锁 |
synchronized和lock区别:
- synchronized 是一个关键词,lock 是一个接口
- synchronized 是隐式调用,lock 是显示调用
- synchronized 可以作用到方法,lock 只能作用到代码块
- Lock 支持非阻塞式加锁,
- Lock 支持超时加锁
- Lock 支持可中断加锁
- synchronized 采用的 monitor 监视器,lock 采用的是 AQS
- synchronized 只有 2 个同步队列,一个等待队列,lock 可以有多个等待队列,一个同步队列
- synchronized 只支持非公平锁,lock 支持公平锁和非公平锁
- synchronized 与 wait 和 notify 和 notifyAll 配合使用,lock 和 condition 和 await 和 signal 和 signalAll 配合使用
- lock 是模板方法模式,可以自定义实现锁机制
- lock 有读写锁,可以支持同时读读
17.Lock 的 Condition 接口作用?
任意一个 Java 对象,都拥有一组监视器方法(定义在 java.lang.Object 上),主要包括 wait()、 wait(long timeout)、 notify()以及 notifyAll()方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式.Condition 接口也提供了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式。
ConditionObject 是 AQS 的一个内部类,Condition 操作需要获取同步状态.节点类型和 AQS 中的节点是一样的.
通过对比 Object 的监视器方法和 Condition 接口,可以更详细地了解 Condition 的特性
Object 的监视器方法与 Condition 接口的对比
对比项 | Obiect Monitor Methods | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用 Locklock 获取锁 调用 LocknewCondition 获取 Condition 对象 |
调用方式 | 直接调用 如:object.wait() |
直接调用 如:condition.await() |
等待队列个数 | 一个 | 多个 |
当前线程释放锁并进人等待状态 | 支持 | 支持 |
当前线程释放锁并进人等待状态,在等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来的某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
Condition 定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition 对象关联的锁.Condition 对象是由 Lock 对象(调用 Lock 对象的 newCondition()方法)创建出来的,换句话说,Condition 是依赖 Lock 对象的。
public class Juc_15_keyword_Condition {
final Lock lock = new ReentrantLock();//定义锁对象//通过锁对象获取Condition实例final Condition notFull = lock.newCondition(); //用于控制put操作final Condition notEmpty = lock.newCondition(); //用于控制take操作final Object[] items = new Object[100];//缓冲队列,初始容量100int putptr, takeptr, count;//分别记录put,take当前的索引,count用于记录当前item的个数/*** 往缓冲队列中添加数据*/public void put(Object x) throws InterruptedException {
//上锁,作用和synchronized一样,保证代码同一时刻只有一个线程可以操作,也保证了和take方法的互斥lock.lock();try {
while (count == items.length) {
notFull.await();//如果队列满了,则put线程等待被唤醒}items[putptr] = x; //队列未满,则添加数据if (++putptr == items.length) putptr = 0;//添完后,如果记录读数据的索引到了最后一个位置,则重置为0++count;//item总数自增notEmpty.signal();//唤醒take线程取数据} finally {
lock.unlock();//put操作完后,释放锁.}}/*** 从缓冲队列中取数据*/public Object take() throws InterruptedException {
lock.lock();try {
while (count == 0) {
notEmpty.await();//如果队列空了,则take线程等待被唤醒}Object x = items[takeptr]; //队列未空,则取数据if (++takeptr == items.length) takeptr = 0;//取完后,如果记录取数据的索引到了最后一个位置,则重置为0--count;//item总数自减notFull.signal();//唤醒put线程添加数据return x;//返回取得的数据} finally {
lock.unlock();//take操作完后,释放锁对象}}
}
当调用 await()方法后,当前线程会释放锁并在此等待,而其他线程调用 Condition 对象的 signal()方法,通知当前线程后,当前线程才从 await()方法返回,并且在返回前已经获取了锁。一个线程进入等待状态----释放锁----线程被唤醒----线程获取锁----等待状态结束。
Condition 相关的方法,Condition 其实就是一个队列,好理解
方法 | 描述 |
---|---|
void await() | 造成当前线程在接到信号或被中断之前一直处于等待状态。 |
boolean await(long time, TimeUnit unit) | 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 |
long awaitNanos(long nanosTimeout) | 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。 |
void awaitUninterruptibly() | 造成当前线程在接到信号之前一直处于等待状态。 |
boolean awaitUntil(Date deadline) | 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。 |
void signal() | 唤醒一个等待线程。 |
void signalAll() | 唤醒所有等待线程。 |
18.Condition 的实现原理?
ConditionObject 是同步器 AbstractQueuedSynchronizer 的内部类,因为 Condition 的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理.每个 Condition 对象都包含着一个队列(以下称为等待队列),该队列是 Condition 对象实现等待/通知功能的关键。
如果一个线程调用了 Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
一个 Condition 包含一个等待队列,Condition 拥有首节点(firstWaiter)和尾节点(lastWaiter).当前线程调用 Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列
Condition 拥有首尾节点的引用,而新增节点只需要将原有的尾节点 nextWaiter 指向它,并且更新尾节点即可.上述节点引用更新的过程并没有使用 CAS 保证,原因在于调用 await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。
在 Object 的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的 Lock (更确切地说是同步器)拥有一个同步队列和多个等待队列
如果从队列(同步队列和等待队列)的角度看 await()方法,当调用 await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到 Condition 的等待队列中。
//AbstractQueuedLongSynchronizer中的内部类ConditionObject,实现了Condition接口
public final void signal() {
if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);
}
调用 Condition 的 signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中.调用该方法的前置条件是当前线程必须获取了锁,可以看到 signal()方法进行了 isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程.接着获取等待队列的首节点,将其移动到同步队列并使用 LockSupport 唤醒节点中的线程。
Condition 的 signalAll()方法,相当于对等待队列中的每个节点均执行一次 signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。唤醒代表着移动到同步队列中.
觉得有用的话点个赞
??
呗。
??????本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!??????如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!? ? ?
???Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!???