Effective Java 第三版——81. 优先使用并发实用程序替代wait和notify

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java 第三版——81. 优先使用并发实用程序替代wait和notify
  1. 优先使用并发实用程序替代wait和notify

本书的第一版专门用一个条目来介绍正确使用wait和notify方法[Bloch01,Item 50]。 它的建议仍然有效,并在本条目末尾进行了总结,但这个建议远不如以前那么重要了。 这是因为没有太多理由再使用wait和notify了。 从Java 5开始,该平台提供了更高级别的并发实用程序,可以执行以前必须在wait和notify时手动编写代码的各种操作。 鉴于正确使用wait和notify的困难,应该使用更高级别的并发实用程序

java.util.concurrent包中的高级实用程序分为三类:Executor Framework,在条目 80中简要介绍了它;并发集合(concurrent collections) 和同步器(synchronizers)。 本条目简要介绍后两者。

并发集合是标准集合接口(如List,Queue和Map)的高性能并发实现。 为了提供高并发性,这些实现在内部管理自己的同步(条目 79)。 因此, 不可能从并发集合中排除并发活动; 锁定它只会使程序变慢

因为不能排除并发集合上的并发活动,所以也不能以原子方式组合对它们的方法调用。 因此,并发集合接口配备了依赖于状态的修改操作,这些操作将几个基本操作组合成单个原子操作。 事实证明,这些操作对并发集合非常有用,它们使用默认方法(条目 21)添加到Java 8中相应的集合接口中。

例如,Map的 putIfAbsent(key, value)方法插入键的映射(如果不存在)并返回与键关联的之前的值,如果没有则返回null。
这样可以轻松实现线程安全的规范化Map 。 此方法模拟String.intern`方法的行为:

// Concurrent canonicalizing map atop ConcurrentMap - not optimal
private static final ConcurrentMap<string, string> map =
        new ConcurrentHashMap<>();

public static String intern(String s) {
    String previousValue = map.putIfAbsent(s, s);
    return previousValue == null ? s : previousValue;
}
</string,>

事实上,你可以做得更好。ConcurrentHashMap针对get等检索操作进行了优化。因此,只有在get表明有必要时,才首先调用get并调用putIfAbsent方法:

// Concurrent canonicalizing map atop ConcurrentMap - faster!

public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        result = map.putIfAbsent(s, s);
        if (result == null)
            result = s;
    }
    return result;
}

除了提供出色的并发性外,ConcurrentHashMap非常快。 在我的机器上,上面的intern方法比String.intern快6倍(但请记住,String.intern必须采用一些策略来防止在长期运行的应用程序中泄漏内存)。 并发集合使基于同步的集合在很大程度上已经过时了。 例如,使用ConcurrentHashMap优先于 Collections.synchronizedMap。 简单地用并发Map替换同步Map以显着提高并发应用程序的性能。

一些集合接口使用阻塞操作进行扩展,这些操作等待(或阻塞)直到可以成功执行。 例如,BlockingQueue扩展了Queue并添加了几个方法,包括take,它从队列中删除并返回head元素,等待队列为空。 这允许阻塞队列用于工作队列(也称为生产者——消费者队列),一个或多个生产者线程将工作项入队,并且一个或多个消费者线程从哪个队列变为可用时出队并处理项目。 正如所期望的那样,大多数ExecutorService实现(包括ThreadPoolExecutor)都使用BlockingQueue(条目 80)。

同步器是使线程能够彼此等待的对象,允许它们协调各自的活动。 最常用的同步器是CountDownLatch和Semaphore。 不太常用的是CyclicBarrier和Exchanger。 最强大的同步器是Phaser。

倒计时锁存器(CountDownLatch)是一次性使用的屏障,允许一个或多个线程等待一个或多个其他线程执行某些操作。 CountDownLatch的唯一构造方法接受一个int类型的参数,它是在允许所有等待的线程继续之前,必须在latch上调用countDown方法的次数。

在这个简单的原语上构建有用的东西非常容易。例如,假设想要构建一个简单的框架来为一个操作的并发执行计时。这个框架由一个方法组成,该方法使用一个执行器executor来执行操作,一个表示要并发执行的操作数量并发级别,以及一个表示该操作的runnable组成。所有工作线程都准备在计时器线程启动时钟之前运行操作。当最后一个工作线程准备好运行该操作时,计时器线程”发号施令(fires the starting gun)”,允许工作线程执行该操作。一旦最后一个工作线程完成该操作,计时器线程就停止计时。在wait和notify的基础上直接实现这种逻辑至少会有点麻烦,但是在CountDownLatch的基础上实现起来却非常简单:

// Simple framework for timing concurrent execution
public static long time(Executor executor, int concurrency,
            Runnable action) throws InterruptedException {
    CountDownLatch ready = new CountDownLatch(concurrency);
    CountDownLatch start = new CountDownLatch(1);
    CountDownLatch done  = new CountDownLatch(concurrency);

    for (int i = 0; i < concurrency; i++) {
        executor.execute(() -> {
            ready.countDown(); // Tell timer we're ready
            try {
                start.await(); // Wait till peers are ready
                action.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                done.countDown();  // Tell timer we're done
            }
        });
    }
    ready.await();     // Wait for all workers to be ready
    long startNanos = System.nanoTime();
    start.countDown(); // And they're off!

    done.await();      // Wait for all workers to finish
    return System.nanoTime() - startNanos;
}

请注意,该方法使用三个倒计时锁存器。 第一个 ready,由工作线程来告诉计时器线程何时准备就绪。 工作线程然后等待第二个锁存器,即 start。 当最后一个工作线程调用 ready.countDown时,计时器线程记录开始时间并调用 start.countDown,允许所有工作线程继续。 然后,计时器线程等待第三个锁存器完成,直到最后一个工作线程完成运行并调用 done.countDown。 一旦发生这种情况,计时器线程就会唤醒并记录结束时间。

还有一些细节值得注意。传递给time方法的executor必须允许创建至少与给定并发级别相同数量的线程,否则测试将永远不会结束。这被称为线程饥饿死锁(thread starvation deadlock)[Goetz06, 8.1.1]。如果工作线程捕捉到InterruptedException异常,它使用习惯用法 thread.currentthread ().interrupt()重新断言中断,并从它的run方法返回。这允许执行程序按照它认为合适的方式处理中断。System.nanoTime用于计算活动的时间。**对于间隔计时,请始终使用 System.nanoTime而不是 System.currentTimeMillisSystem.nanoTime更准确,更精确,不受系统实时时钟调整的影响。最后,请注意,本例中的代码不会产生准确的计时,除非action做了相当多的工作,比如一秒钟或更长时间。准确的微基准测试是非常困难的,最好是借助诸如jmh [JMH]这样的专业框架来完成。

这个条目只涉及使用并发实用程序做一些皮毛的事情。 例如,前一个示例中的三个倒计时锁存器可以由单个CyclicBarrier或Phaser实例替换。 结果代码会更简洁,但可能更难理解。

虽然应该始终优先使用并发实用程序来等替换wait和notify方法,但你可能必须维护使用wait和notify的旧代码。 wait方法用于使线程等待某些条件。 必须在同步区域内调用它,该区域锁定调用它的对象。 下面是使用wait方法的标准习惯用法:

// The standard idiom for using the wait method
synchronized (obj) {
    while (<condition does not hold>)
        obj.wait(); // (Releases lock, and reacquires on wakeup)
    ... // Perform action appropriate to condition
}
</condition>

始终要在循环中调用wait方法;永远不要在循环之外调用它。循环用于测试wait前后的条件。

如果条件已经存在,则在wait之前测试条件并跳过等待以确保活性(liveness)。 如果条件已经存在并且在线程等待之前已经调用了notify(或notifyAll)方法,则无法保证线程将从等待中唤醒。

为了确保安全,需要在等待之后再测试条件,如果条件不成立,则再次等待。如果线程在条件不成立的情况下继续执行该操作,它可能会破坏由锁保护的不变式(invariant)。当条件不成立时,以下几个原因可以把线程唤醒:

  • 另一个线程可以获得锁并在线程调用notify和等待线程醒来之间改变了保护状态。
  • 当条件不成立时,另一个线程可能意外地或恶意地调用notify方法。类通过等待公共可访问的对象来暴露自己。公共可访问对象的同步方法中的任何wait方法都容易受到这个问题的影响。
  • 通知线程在唤醒等待线程时可能过于”慷慨”。例如,即使只有一些等待线程的满足条件,通知线程也可能调用notifyAll。
  • 在没有通知的情况下,等待的线程可能(很少)被唤醒。这被称为虚假的唤醒(spurious wakeup)[POSIX, 11.4.3.6.1;Java9-api]。

一个相关的问题是,为了唤醒等待的线程,是使用notify还是notifyAll。(回想一下notify唤醒一个等待线程,假设存在这样一个线程,notifyAll唤醒所有等待线程)。有时人们会说,应该始终使用notifyAll。这是合理的、保守的建议。它总是会产生正确的结果,因为它保证唤醒所有需要被唤醒的线程。可能还会唤醒其他一些线程,但这不会影响程序的正确性。这些线程将检查它们正在等待的条件,如果发现为false,将继续等待。

作为一种优化,如果所有线程都在等待相同的条件,并且每次只有一个线程可以从条件变为true中唤醒,那么可以选择调用notify而不是notifyAll。

即使满足了这些先决条件,也可能有理由使用notifyAll来代替notify。正如将wait方法调用放在循环中可以防止公共访问对象上的意外或恶意通知一样,使用notifyAll代替notify可以防止不相关线程的意外或恶意等待。否则,这样的等待可能会”吞下”一个关键通知,让预期的接收者无限期地等待。

总之,与java.util.concurrent提供的高级语言相比,直接使用wait和notify就像在”并发汇编语言”中编程一样。 在新代码中基本上不存在使用wait和notify的理由。 如果正在维护使用wait和notify的代码,请确保它始终使用标准惯用法在while循环内调用wait方法。 通常应优先使用notifyAll方法进行通知。 如果使用notify,必须非常小心以确保程序的活性。

Original: https://www.cnblogs.com/IcanFixIt/p/10640314.html
Author: 林本托
Title: Effective Java 第三版——81. 优先使用并发实用程序替代wait和notify

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/537086/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SpringBoot定时任务-开箱即用分布式任务框架xxl-job

    除了前文介绍的ElasticJob,xxl-job在很多中小公司有着应用(虽然其代码和设计等质量并不太高,License不够开放,有着个人主义色彩,但是其具体开箱使用的便捷性和功能…

    Java 2023年6月6日
    070
  • arthas 使用指导

    arthas 阿尔萨斯 这种命令行的东西首先得知道 如何使用帮助,帮助文档最先开始用的,应该是可以在网上找到的官方文档 文档一:https://alibaba.github.io/…

    Java 2023年6月5日
    074
  • Go学习第一天:有关环境变量及结构的解释

    环境变量 有三个变量 GOPATH、 PATH、 GOROOT: GOROOT 就是 go 的安装路径; GOPATH 就是go的项目目录; PATH是go安装路径下的bin目录。…

    Java 2023年6月7日
    081
  • crm遇错记录

    bug thymeleaf 共享域对象 如果需要将对象的数据显示到前台页面我们可以通过使用thymeleaf实现这一点,Thymeleaf是一个动态渲染页面用的,他简单易懂,不像j…

    Java 2023年6月8日
    059
  • Spring Boot下的一种导出Excel文件的代码框架

    1、前言 ​ 在Spring Boot项目中,将数据导出成Excel格式文件是常见的功能。与Excel文件导入类似,此处也用代码框架式的方式实现Excel文件导出,使得代码具有可重…

    Java 2023年6月14日
    073
  • SpringCloud之Seata

    1.Seata是什么? 1.1 概念:Seata 是一款开源的分布式事务解决方案,提供高性能和简单易用的分布式事务服务。1.2 术语(1)TC: 事务协调者维护全局和分支事务的状态…

    Java 2023年6月13日
    048
  • Mybatis源码4 Cache的实现和其原理

    Mybatis CachingExecutor, 二级缓存,缓存的实现 一丶二级缓存概述 上一章节,我们知道mybaits在构造SqlSession的时候,需要让SqlSessio…

    Java 2023年6月14日
    061
  • jackson实体转json时 为NULL不参加序列化的汇总

    首先加入依赖 方法一、实体上使用 @JsonInclude(JsonInclude.Include.NON_NULL) 1、如果放在属性上,如果该属性为NULL则不参与序列化 ;2…

    Java 2023年6月13日
    055
  • IBM MQ Explorer 示例操作

    此示例为双向传输 建立队列管理器 建立【test01】【test02】两个队列管理器,一直下一步即可,端口号不能一致(需要记住设置的端口号,后面会用到) 【test01】端口号 1…

    Java 2023年5月29日
    065
  • Spring中Resource和Autowired的区别

    有时候可以相互交换使用,但有时候不能使用 不同点: Resource是Java源代码里面的,而Autowired是Spring里的注解。在使用的时候SpringIOC在装载的时候会…

    Java 2023年6月8日
    059
  • [学习笔记] Java常用包装类

    在实际开发过程中,可能需要用到内置数据类型的对象。Java为每个内置数据类型提供对应的包装类。 Number类的子类 所有的数值类型的包装类都是抽象类Number的子类; Inte…

    Java 2023年6月5日
    069
  • 反射加缓存,解决不同的业务下调用不同的实现

    根据前端传入不同的支付code,动态找到对应的支付方法,发起支付。我们先定义一个注解。 @Retention(RetentionPolicy.RUNTIME) @Target(El…

    Java 2023年6月14日
    061
  • Spring Cloud Alibaba 使用Seata解决分布式事务

    为什么会产生分布式事务? 随着业务的快速发展,网站系统往往由单体架构逐渐演变为分布式、微服务架构,而对于数据库则由单机数据库架构向分布式数据库架构转变。此时,我们会将一个大的应用系…

    Java 2023年6月5日
    079
  • uniapp原生tabbar设置并添加数字角标或小红点提示

    uniapp配置并设置原生tabbar,原生tabbar基本够用,没必要去用一些比较难配置的插件 //原生tabbar设置在pages.json里面添加如下配置 “ta…

    Java 2023年6月14日
    071
  • mybatis报错:java.io.IOException: Could not find resource /resources/mybatis-config.xml

    原因:这个图标的resources目录是根目录,在此目录下的文件直接写文件名即可 Original: https://www.cnblogs.com/CounterX/p/1645…

    Java 2023年6月9日
    087
  • SpringMVC

    MVCModel 业务封装事务逻辑处理View 数据展示Controller 分发指派工作 SpringMVC开发步骤 导入Spring-MVC包 配置Servlet 编写POJO…

    Java 2023年6月5日
    081
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球