Lock与Condition接口条件变量方式

我们知道,并发领域中有两大核心问题:互斥与同步问题,java在1.5版本之前,是提供了synchronized来实现的。synchronized是内置锁,虽然在大部分情况下它都能很好的工作,但是依然还是会存在一些局限性,除了当时1.5版本的性能问题外(1.6版本后,synchronized的性能已经得到了很大的优化),还有如下两个问题:
无法解决死锁问题最多使用一个条件变量所以针对这些问题,doug lea在并发包中增加了两个接口lock和condition来解决这两个问题,所以今天就说说这两个接口是如何解决synchronized中的这两个问题的。
一. lock接口1.1 介绍在我们分析lock接口是如何解决死锁问题之前,我们先看看死锁是如何产生的。死锁的产生需要满足下面四个条件:
互斥 :共享资源同一时间只能被一个线程占用不可抢占 :其他线程不能强行占有另一个线程的资源占有且等待 :线程在等待其他资源时,不释放自己已占有的资源循环等待 :线程1和线程2互相占有对方的资源并相互等待所以,我们只需要破坏上面条件中的任意一个,即可打破死锁。但需要注意的是,互斥条件是不能破坏的,因为使用锁的目的就是为了互斥。所以lock接口通过破坏掉 不可抢占这个条件来解决死锁,具体如下:
非阻塞获取锁 :尝试获取锁,如果失败了就立刻返回失败,这样就可以释放已经持有的其他锁响应中断 :如果发生死锁后,此线程被其他线程中断,则会释放锁,解除死锁支持超时 :一段时间内获取不到锁,就返回失败,这样就可以释放之前已经持有的锁接下来我们具体看看接口代码吧。
1.2 源码解读public interface lock { /** 阻塞获取锁,不响应中断,如果获取不到,则当前线程将进入休眠状态,直到获得锁为止。 */ void lock(); /** 阻塞获取锁,响应中断,如果出现以下两种情况将抛出异常 1.调用该方法时,此线程中断标志位被设置为true 2.获取锁的过程中此线程被中断,并且获取锁的实现会响应中断 */ void lockinterruptibly() throws interruptedexception; /** 非阻塞获取锁,不管成功还是失败,都会立刻返回结果,成功了返回true,失败了返回false */ boolean trylock(); /** 带超时时间且响应中断的获取锁,如果获取锁成功,则返回true,获取不到则会休眠,直到下面三个条件满足 1.当前线程获取到锁 2.其他线程中断了当前线程,并且获取锁的实现支持中断 3.设置的超时事件到了 而抛出异常的情况与lockinterruptibly一致 当异常抛出后中断标志位会被清除,且超时时间到了,当前线程还没有获得锁,则会直接返回false */ boolean trylock(long time, timeunit unit) throws interruptedexception; /** 没啥好说,只有拥有锁的线程才能释放锁 */ void unlock(); /** 返回绑定到此lock实例的新condition实例。 在等待该条件之前,该锁必须由当前线程持有。调用condition.await()会在等待之前自动释放锁,并在等待返回之前重新获取该锁。 我们再下一小节再详细说说condition接口 */ condition newcondition();}还需要额外注意的一点,使用synchronized作为锁时,我们是不需要考虑释放锁的,但lock是属于显示锁,是需要我们手动释放锁的。我们一般在finally块中调用lock.unlock()手动释放锁,具体形式如下:
lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }我们最后通过一张图来总结下lock接口:
二. condition接口2.1 介绍针对synchronized最多只能使用一个条件变量的问题,condition接口提供了解决方案。但是为什么多个条件变量就比一个条件变量好呢?我们先来看看synchronized使用一个条件变量时会有什么弊端。
一个synchronized内置锁只对应一个等待容器(wait set),当线程调用wait方法时,会把当前线程放入到同一个等待容器中,当我们需要根据某些特定的条件来唤醒符合条件的线程时,我们只能先从等待容器里唤醒一个线程后,再看是否符合条件。如果不符合条件,则需要将此线程继续wait,然后再去等待容器中获取下一个线程再判断是否满足条件。这样会导致许多无意义的cpu开销。
我们可以看到lock接口中有个newcondition()的方法:
condition newcondition();通过这个方法,一个锁可以建立多个conditiion,每个condtition都有一个容器来保存相应的等待线程,拿到锁的线程根据特定的条件唤醒对应的线程时,只需要去唤醒对应的contition内置容器中的线程即可,这样就可以减少无意义的cpu开销。然后我们具体看看condition接口的源码。
2.2 源码解读public interface condition { /** 使当前线程等待,并响应中断。当当前线程进入休眠状态后,如果发生以下四种情况将会被唤醒: 1.其他一些线程对此条件调用signal方法,而当前线程恰好被选择为要唤醒的线程; 2.其他一些线程对此条件调用signalall方法 3.其他一些线程中断当前线程,并支持中断线程挂起 4.发生“虚假唤醒”。 */ void await() throws interruptedexception; /** 使当前线程等待,并不响应中断。只有以下三种情况才会被唤醒 1.其他一些线程对此条件调用signal方法,而当前线程恰好被选择为要唤醒的线程; 2.其他一些线程对此条件调用signalall方法 3.发生“虚假唤醒”。 */ void awaituninterruptibly(); /** 使当前线程等待,响应中断,且可以指定超时事件。发生以下五种情况之一将会被唤醒: 1.其他一些线程为此条件调用signal方法,而当前线程恰好被选择为要唤醒的线程; 2.其他一些线程为此条件调用signalall方法; 3.其他一些线程中断当前线程,并且支持中断线程挂起; 4.经过指定的等待时间; 5.发生“虚假唤醒”。 */ long awaitnanos(long nanostimeout) throws interruptedexception; /** 与awaitnanos类似,时间单位不同 */ boolean await(long time, timeunit unit) throws interruptedexception; /** 与awaitnanos类似,只不过超时时间是截止时间 */ boolean awaituntil(date deadline) throws interruptedexception; /** 唤醒一个等待线程 */ void signal(); /** 唤醒所有等待线程 */ void signalall();}需要注意的是,object类的等待方法是没有返回值的,但condtition类中的部分等待方法是有返回值的。awaitnanos(long nanostimeout)返回了剩余等待的时间;await(long time, timeunit unit)返回boolean值,如果返回false,则说明是因为超时返回的,否则返回true。为什么增加返回值?为了就是帮助我们弄清楚方法返回的原因。
四. 阿里多线程考题最后我们通过实现了lock和condition接口能力的reentrantlock类来解决阿里多线程面试题。
题目是使用三个线程循环打印abc,一共打印50次。我们直接上答案:
public class test { int count = 0; lock lock = new reentrantlock(); condition conditiona = lock.newcondition(); condition conditionb = lock.newcondition(); condition conditionc = lock.newcondition(); public void printa() { while (count < 50) { try { // 加锁 lock.lock(); // 打印a system.out.println(a); count ++; // 唤醒打印b的线程 conditionb.signal(); // 将自己放入conditiona的容器中,等待其他线程的唤醒 conditiona.await(); } catch (interruptedexception e) { e.printstacktrace(); } finally { // 释放锁 lock.unlock(); } } } public void printb() { while (count < 50) { try { // 加锁 lock.lock(); // 打印b system.out.println(b); count ++; // 唤醒打印c的线程 conditionc.signal(); // 将自己放入conditionb的容器中,等待其他线程的唤醒 conditionb.await(); } catch (interruptedexception e) { e.printstacktrace(); } finally { // 释放锁 lock.unlock(); } } } public void printc() { while (count { test.printa(); }); thread theadb = new thread(() - > { test.printb(); }); thread theadc = new thread(() - > { test.printc(); }); // 启动线程 theada.start(); theadb.start(); theadc.start(); }}五. 总结lock与condition接口就说完了,最后再总结一下:
针对synchronized内置锁无法解决死锁、只有一个条件变量等问题,doug lea在java并发包中增加了lock和condition接口来解决。对于死锁问题,lock接口增加了超时、响应中断、非阻塞三种方式来获取锁,从而避免了死锁。针对一个条件变量问题,condtition接口通过一把锁可以创建多个条件变量的方式来解决。

出现耳鸣?不要紧,缓解耳鸣有绝招
什么是MEMS(微机电系统)传感器?
偏压点分析(Bias Point Detail)和直流扫描分
GPT-4即将实现重大突破 如何打破10000倍规模瓶颈
沉降监测预警装置的自身特点是什么
Lock与Condition接口条件变量方式
苹果A6X处理器深解:ARM GPU性能推至极限
ADS7890的基本外围电路图
aigo国民好物主流及高端固态硬盘产品对比,哪款更适合你
智算中心在AI算法、应用领域的探索和实践
iPhone电池又出问题,iPhone 6电池炸了
通过利用电化学诊断技术分析传感器的健康状况
微软希望通过新的安全功能,消灭恶意软件
内嵌式二维码识别模组,LV3298条码识读模块有什么独特亮点?
混合动力汽车的战术突破口是插电式和锂电池
2020全球平板电脑出货量:苹果第一
阻碍嵌入式CPU性能的两大主要因素
重要进展!本源量子率先发布量子金融衍生品定价库
活动预告 | 2023高端集成电路IP技术研讨会·北京站,芯动邀您共聚!
人工智能为何成为了主角