0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Lock与Condition接口条件变量方式

科技绿洲 来源:Java技术指北 作者:Java技术指北 2023-10-13 11:21 次阅读

我们知道,并发领域中有两大核心问题:互斥与同步问题,Java在1.5版本之前,是提供了synchronized来实现的。synchronized是内置锁,虽然在大部分情况下它都能很好的工作,但是依然还是会存在一些局限性,除了当时1.5版本的性能问题外(1.6版本后,synchronized的性能已经得到了很大的优化),还有如下两个问题:

  1. 无法解决死锁问题
  2. 最多使用一个条件变量

所以针对这些问题,Doug Lea在并发包中增加了两个接口Lock和Condition来解决这两个问题,所以今天就说说这两个接口是如何解决synchronized中的这两个问题的。

一. Lock接口

1.1 介绍

在我们分析Lock接口是如何解决死锁问题之前,我们先看看死锁是如何产生的。死锁的产生需要满足下面四个条件:

  1. 互斥 :共享资源同一时间只能被一个线程占用
  2. 不可抢占 :其他线程不能强行占有另一个线程的资源
  3. 占有且等待 :线程在等待其他资源时,不释放自己已占有的资源
  4. 循环等待 :线程1和线程2互相占有对方的资源并相互等待

所以,我们只需要破坏上面条件中的任意一个,即可打破死锁。但需要注意的是,互斥条件是不能破坏的,因为使用锁的目的就是为了互斥。所以Lock接口通过破坏掉 "不可抢占"这个条件来解决死锁,具体如下:

  1. 非阻塞获取锁 :尝试获取锁,如果失败了就立刻返回失败,这样就可以释放已经持有的其他锁
  2. 响应中断 :如果发生死锁后,此线程被其他线程中断,则会释放锁,解除死锁
  3. 支持超时 :一段时间内获取不到锁,就返回失败,这样就可以释放之前已经持有的锁

接下来我们具体看看接口代码吧。

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 < 50) {
            try {
                // 加锁
                lock.lock();
                // 打印B
                System.out.println("C");
                count ++;
                // 唤醒打印A的线程
                conditionA.signal();
                // 将自己放入ConditionC的容器中,等待其他线程的唤醒
                conditionC.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Test test = new Test();
        // 建立打印ABC的三个线程
        Thread theadA = new Thread(() - > {
            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接口通过一把锁可以创建多个条件变量的方式来解决。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    8568

    浏览量

    150996
  • JAVA
    +关注

    关注

    19

    文章

    2964

    浏览量

    104688
  • Lock
    +关注

    关注

    0

    文章

    10

    浏览量

    7767
  • 线程
    +关注

    关注

    0

    文章

    504

    浏览量

    19674
收藏 人收藏

    评论

    相关推荐

    Linux下线程间通讯---读写锁和条件变量

    读写锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。件变量是线程可用的一种同步机制,条件变量给多个线程提供了一个回合的场所,条件
    的头像 发表于 08-26 20:44 1463次阅读
    Linux下线程间通讯---读写锁和<b class='flag-5'>条件</b><b class='flag-5'>变量</b>

    Linux系统中线程同步方式中的条件变量方法

    今天主要和大家聊一聊,如何使用Linux中线程同步方式中的条件变量
    发表于 11-08 09:16 528次阅读

    labview枚举变量条件结构的结合的问题

    如图:程序运行时,为什么在前面板上改变枚举变量的值,条件结构的条件中对应的没有改变呢?
    发表于 12-06 15:21

    Raw condition msg 篇

    调用这个函数会立即block 在condition msg上,直到其他任务调用raw_cond_msg_set,满足条件后才会醒过来并接收到一个消息。Wait_option 可以设置为
    发表于 02-27 14:08

    条件结构的布尔变量问题?

    本帖最后由 dsl7410 于 2016-1-13 00:00 编辑 这个顺序结构里面的条件结构一个是把value值+1,一个是-1,+1可以理解,因为分支选择器接了加的布尔变量,但是减的那个分支怎么实现呢?分支选择器没有连接减啊,那个减的布尔控件删除了,还能实现-
    发表于 01-12 23:57

    Linux C 多线程编程之互斥锁与条件变量实例详解

    。这时线程挂起,不占用 CPU 时间,直到条件变量被触发。因此,全过程可以描述为:(1)pthread_mutex_lock()上锁,(2)pthread_cond_wait()等待,等待过程分解为为
    发表于 06-03 17:13

    浅析linux下的条件变量

    变量中常用的API:      1).条件变量类型为:pthread_cond_t ,类似互斥变量条件
    发表于 07-12 08:10

    Lock体系结构和读写锁机制解析

    排它性,即同一个时刻只有一个线程进入任务。Condition接口Condition接口描述可能会与锁有关联的条件
    发表于 01-05 17:53

    【随笔记】C++ condition_variable 陷阱

    ; lock(mutex_data_); cond_.notify_all(); } 改进方案一(使用 select 方式实现):缺点是一个对象会浪费两个文件描述符资源 DelayControl
    发表于 11-24 10:41

    FIDIC合同条件体系及应用方式

    介绍了FIDIC组织、FIDIC合同条件体系及其最新发展,讨论了FIDIC合同条件的应用方式,强调了学习FIDIC合同条件的意义和重要性。
    发表于 01-08 15:32 2次下载

    linux设置环境变量的三种方式

     linux设置环境变量有以下三种方式
    发表于 06-15 09:05 1414次阅读
    linux设置环境<b class='flag-5'>变量</b>的三种<b class='flag-5'>方式</b>

    详谈Linux操作系统编程的条件变量

    条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用。条件变量之所以要和互斥锁一起使
    的头像 发表于 09-27 15:23 1988次阅读
    详谈Linux操作系统编程的<b class='flag-5'>条件</b><b class='flag-5'>变量</b>

    TensorRT条件用于实现网络子图的条件执行

    IIfConditional实现了一个 if-then-else 流控制结构,该结构提供基于动态布尔输入的网络子图的条件执行。它由一个布尔标量predicate condition和两个分支子图定义
    的头像 发表于 05-18 10:02 1167次阅读

    Linux线程条件变量是什么意思

    条件变量 条件变量用于自动阻塞线程,直到某个特定事件发生或某个条件满足为止,通常情况下,条件
    的头像 发表于 07-21 11:18 513次阅读

    case怎么使用多个条件

    在编写代码时,我们经常需要根据不同的条件来执行不同的操作。在Python中,我们可以使用 if 语句来实现这一目的。 if 语句允许我们设置多个条件,并且根据不同的条件执行不同的代码块。 语法结构
    的头像 发表于 11-30 14:34 1207次阅读