《Java核心技术卷》读书笔记-并发(七) - 监视器&volatile&原子操作类&线程局部变量

87 阅读3分钟

监视器介绍

监视器解决的问题是:不需要程序员考虑如何加锁的情况下,就可以保证线程的安全性。用Java的术语来讲,监视器具有如下特性:

  • 监视器是只包含私有域的类。

  • 每个监视器类的对象有一个相关的锁。

  • 使用该锁对所有的方法进行加锁。

  • 该锁可以有任意多个相关条件。

Java的监视器实现

要知道,Java中的每一个对象有一个内部的锁和内部的条件。如果一个方法用synchronized关键字声明,那么,它表现的就像是一个监视器方法。通过调用wait/notifyAll/notify来访问条件变量。但是,Java的实现方式不太安全,与监视器的特性是有区别的:

  • 域不要求必须是private

  • 方法不要求必须是synchronized

  • 内部锁对用户是可用的

Volatile

在介绍volatile之前,我们来了解两个现代计算机和编译器的特性:

  • 多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。

  • 编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显示的修改指令时才会改变。这就意味着,顺序改变之后,就有可能被另一个线程改变这个内存中的值。

要解决这两个问题,可以使用同步,但开销太大。volatile修饰符就是用于这个场景。

volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。

但是要注意,volatile保证了该实例域指令执行顺序不变和内存可见性,但没有保证原子性。也就是说,无法保证对该域进行赋值能保证线程安全。因此该修饰符适合对共享变量除了赋值之外并不完成其他操作。

final

final修饰符也可以保证安全地访问一个共享域。因为当一个域被声明为final,代表它不可再被修改,不会被修改地变量自然可以随意读取。

原子性

java.util.concurrent.atomic包中有很多类使用了很高效地机器级指令来保证其他操作的原子性。

Atomicxxxxx

提供了方法incrementAndGet和decrementAndGet,分别以原子方式使一个数自增自减。如果要进行更复杂的更新,使用compareAndSet方法。在java8中,我们可以使用


// 传入lamba函数

largest.updateAndGet(x -> Math.max(x, other));

// 或

largest.accumlateAndGet(other, Math::max);

LongAdder&LongAccumulator

如果有大量线程要访问相同的原子量,性能会大幅下降,因为乐观更新需要太多次重试。xxxAdder和xxxAccumulator的出现解决了这个问题。

xxxAdder包括多个变量,其总和为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。通常情况下,只有当所有工作都完成之后才需要总和的值,所以就会很高效。

线程局部变量

因为在线程间共享变量是有风险的,使用ThreadLocal辅助类为各个线程提供各自的实例,就可以避免共享变量。要为每个线程构造一个实例,可以类似以下代码使用,这里以SimpleDateFormat为例:


public static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInital(() -> new SimpleDateFormat("yyyy-MM-dd"));

在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。