《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) 状态,即自旋状态。

相关推荐
小杨40444 分钟前
springboot框架项目实践应用十四(扩展sentinel错误提示)
spring boot·后端·spring cloud
陈大爷(有低保)1 小时前
Spring中都用到了哪些设计模式
java·后端·spring
程序员 小柴1 小时前
SpringCloud概述
后端·spring·spring cloud
喝醉的小喵1 小时前
分布式环境下的主从数据同步
分布式·后端·mysql·etcd·共识算法·主从复制
雷渊2 小时前
深入分析mybatis中#{}和${}的区别
java·后端·面试
我是福福大王2 小时前
前后端SM2加密交互问题解析与解决方案
前端·后端
老友@2 小时前
Kafka 全面解析
服务器·分布式·后端·kafka
Java中文社群2 小时前
超实用!Prompt程序员使用指南,大模型各角色代码实战案例分享
后端·aigc
风象南3 小时前
Spring Boot 实现文件秒传功能
java·spring boot·后端
橘猫云计算机设计3 小时前
基于django优秀少儿图书推荐网(源码+lw+部署文档+讲解),源码可白嫖!
java·spring boot·后端·python·小程序·django·毕业设计