《Effective Java》-并发

Chapter 11. Concurrency

Item 78: Synchronize access to shared mutable data

同步访问可变的数据

synchronized关键字保证了同一时间,只能够有单个线程去访问同步方法或者同步代码块。

同步,不仅避免了多个线程并发访问导致数据不一致的情况,同时也保证了每个进入同步块的线程,能够感知到之前线程对同步块的改变。

有很多人认为,在对一个原子变量(atomic variable)进行读写的时候,应该避免使用synchronize以提升性能。但是这种说法真的是无稽之谈,因为,对原子变量的读写根本无法保证修改的可变性(visible)。而synchronize可以保证线程之间的正确通信和互斥。

需要注意的是,当且仅当读写方法都有synchronize修饰时,其可见性才会生效。

当然,除了使用synchronized关键字之外,还有可以使用volatile关键字来保证可见性。但是,volatile关键字并不保证互斥性,也不保证原子性,它只是保证任何线程在访问被其修饰的变量时,都能够获取其最新的版本。

如何去保证原子性呢?第一种方法是使用synchronize关键字,另一种方法是使用java.util.concurrent.atomic包下原子类。这个包提供了无锁的、线程安全的基本数据类型。

Item 79: Avoidc excessive synchronization

避免过度同步

过度同步可能会导致性能衰退、死锁、甚至非确定性的行为。

首先,在同步区域里头,永远不要去调用一个不确定性的方法,例如用于被实现的抽象方法、或者由客户端控制的函数式方法等。

在同步区域之外调用外域方法被称为开放调用(open call)。开放调用不仅可以避免失败,还能提高系统的性能,因为你无法预测外域方法的性能和返回值。

总之,不要在同步代码块中去干太多的事情:

  1. 获得锁
  2. 检测共享数据
  3. 更新共享数据
  4. 释放锁

Item 80: Prefer executors, tasks, and Streams to threads

优先executors, tasks和stream而非threads

Executors FrameWork是一个很灵活的基于接口的任务执行工具。可以使用静态工厂类java.util.concurrent.Executors创建各种类型的线程池。

对于线程池的选择是比较有技巧的,如果是小程序或者是轻量级的应用,则可使用CachedThreadPool,因为其开箱即用,不需要配置。但是如果是重量级的应用,用这个线程池就捉襟见肘了,因为其会无限制地创建新的线程。最好用固定数目的线程池FixedThreadPool

尽量不要自己编写自己的工作队列,尽量不要直接使用线程。抽象的工作单元,即任务(task ),有两种形式,第一种是runnable和其远方表亲callable(callable可以返回值,并且抛出任意异常)

Item 81: Prefer concurrency utilities to wait and notify

并发工具优先于wait | notify

自从Java 5以来,平台提供了各种各样的并发工具类。鉴于想要正确使用wait | notify的困难性,没有理由不使用这些优秀的工具类来替代wait | notify

java.util.concurrrent并发工具包主要分为三大类,第一大类是执行器框架,其二是concurrent collections,其三是synchronizer。

需要在非并发场景下使用并发工具类,因为并发工具类中的锁(Lock)只会拖慢系统的运行效率。此外,数个方法组成的操作也是非原子的。因此,并发接口装配了一些状态独立修改操作,这些操作是通过合并几个初步的动作而得到的原子性的操作。

并发工具类的出现,直接让同步集合变得过时了。无脑使用并发集合替代同步集合即可提高并发应用的性能。

同步类的作用是协调线程之间的活动。最常用的同步器就是CountDownLatch

若要维护一下使用wait的历史遗留代码,可以使用如下的方式,使用wait方法:

java 复制代码
synchronized(obj) {
    while(condition dose not hold) {
        obj.wait(); // realease lock, and requires on wakeup
    }
}

需要注意,一定要将wait方法写在循环里头,不要在循环外面去引用wait方法。

关于唤醒线程的方法notifynotifyAll(),最保险的方法就是使用notifyAll方法去唤醒所有的线程。

Item 82: Document thread safety

线程安全的文档化。

为了保证安全并发使用,一个类必须清晰地在文档中说明其支持的线程安全的等级:

  • 不可变的
  • 无条件的线程安全
  • 有条件的线程安全
  • 非线程安全
  • 线程对立:非同步情况下修改静态数据

Item 83: Use Lazy initialization judiciously

谨慎地使用懒加载

懒加载是直到某个属性第一次被使用时才加载其值的行为。当某个字段的值永远没有被使用,则其永远也不会被初始化。

懒加载是一把双刃剑。当一个类的属性值只在类的部分实例中使用,并且其初始化代价比较大的时候,使用懒加载是一个比较好的优化方式。

但是,并发环境下,懒加载的使用需要注意技巧。需要使用同步锁对初始化过程进行同步,否则会产生严重的BUG。

谨记,大多数情况下,正常初始化要比延迟加载合适。

如果你非要对静态字段做懒加载性能优化的时候,一定要使用静态内部类Holder方式初始化静态字段。其利用了"类在其使用的时候才会初始化"的特性。我们在单例模式的懒加载实现形式中也是使用了这种方法。

java 复制代码
private static class FieldHolder {
    static final FieldType field = cmomputeFieldValue();
}
private static FieldType getField() { return FieldHolder.field; }

如果想要使用懒加载实例化去提升性能,在并发环境下一定要去使用双重检查锁(Double Check Lock)的形式。这种形式下,需要变量声明为volatile

总而言之,如果你非要去使用懒加载优化性能,对于不同类型的字段,选择合适的懒加载方式:

  • 实例字段:双重检查锁
  • 静态字段:Holder
  • 基本数据类型:能够忍受重复初始化的情况下,使用单重检查

Item 84: Don't depend on the thread scheduler

不要依赖于线程调度器

多线程情况下,线程调度器用来决定那个线程可以运行,运行多久。

任何依赖于线程调度器以达到正确性或者高性能目的的程序,很可能都是不可移植的。

最佳实践是,保证正在运行的线程数量小于处理器的数量。

线程不应该处于忙-等(busy-wait) 状态,即自旋状态。

相关推荐
追逐时光者4 分钟前
推荐 12 款开源美观、简单易用的 WPF UI 控件库,让 WPF 应用界面焕然一新!
后端·.net
Jagger_4 分钟前
敏捷开发流程-精简版
前端·后端
苏打水com1 小时前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧2 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧2 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧2 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧2 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧2 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng3 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6013 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring