《Java并发编程实战》读书笔记二:对象的安全共享

前言

线程安全性一章中,JCIP作者指出了编写正确的多线程并发程序的关键问题在于:访问共享的可变状态时需要进行正确的管理,这一节主要研究的就是如何安全地使得对象在多个线程之间共享访问(能够随时读到对象的最新状态 而不是失效值)。

可见性

概述

定义:一个线程对共享变量的修改,另外一个线程能够立刻看到。
实现方式:在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的方式实现,依赖主内存作为传输媒质。
可以保证可见性的关键字

volatile

volatile 变量是用来确保将变量的更新操作通知给其他线程的,即在读取 volatile 变量时,总会返回最新写入的值,是一种无锁实现的、比synchronized更轻量级的同步机制。可以简单理解为对volatile变量的读写操作分别替换为了getset方法(当然这种说法并不严谨)

特点

  • 保证该变量的最新值对所有线程可见
  • 禁止JVM指令重排序优化
  • 只保证可见性,不保证原子性

应用场景 & 使用条件

  • 使用条件:需满足以下所有使用条件
    • 对变量的写入不依赖于变量的当前值 / 或者确保只有单一线程可以更新变量的值
    • 该变量没有包含在含有其他变量的不变式(invariants)中

这里对不变式进行一个注解,在看书的时候由于翻译问题在这里也卡壳了。
我的理解是这样的,不变式属于一种断言(assert),或者类比为数据库中的逻辑一致性,它是人为规定的,例如有start活动开始时间,end活动结束时间,如果我们把他们2个同时声明为volatile变量,易于证明我们不能够保证它们满足强一致性 关于一致性和断言可以参考这里[2]

  • 常见的应用:
    • 某个操作完成的标志位
    • 一写多读的场景
  • 不适用的场景
    • 运算结果依赖于该变量当前的值(比如count++)
    • 运算结果依赖于其他状态变量

发布与逸出

定义

  • 发布(Publish)”一个对象的意思指,使对象可以在当前作用域之外的代码中使用
  • 逸出指的是某个不应该发布的对象被发布的现象

发布方法

  • 最简单的发布方法:public static
  • 将指向该对象的引用保存到其他代码可以访问的地方
  • 在某个非私有的方法中返回该引用
  • 将引用传递到其他类的方法中

逸出的风险

当某个对象逸出后,我们必须假设可能会存在某个类或者线程会误用该对象

这是封装的作用之一:封装可以降低对程序进行正确性分析的难度,并且使无意中破坏设计约束条件变得更难,它隐藏了内部的实现细节,提高了程序安全性。

this引用逸出

public class ThisEscape{
	public ThisEscape(EventSource source){
		// 调用方法的启动线程会持有ThisEscape的隐式引用
		source.registerListener(
			new EventListener(){
				public void onEvent(Event e){
					doSomething(e);
				}
			});
	}
}
  • 产生原因:在一个对象的构造方法中启动了一个线程,并在这个线程的 public 方法中调用了这个对象的方法,相当于将还没构造好的对象的 this 实例泄露了。
  • 解决方法:私有化构造方法,只在构造方法中写新线程的代码但不 start,然后写一个工厂方法 newInstance 来创建实例,在工厂方法中先调用构造函数创建实例,再启动线程,这样就不会把一个还没有构造好的对象发布出去了

Tips:

  • 当我们声明一个非静态内部类的时候,编译器会添加一个对外部类的隐式引用,即非静态的内部类会持有外部类的一个隐式引用

线程封闭

Ad-hoc线程封闭

维基百科上对于Ad hoc这个词有专门的解释,ad-hoc这个短语通常用来形容一些特殊的、不能用于其它方面的的,为一个特定的问题、任务而专门设定的解决方案。因此Ad-hoc线程封闭的意思就是说维护线程封闭性的职责完全由程序实现来承担,例如如果我们能确保只有单个线程对共享的volatile变量进行写入操作,就可以安全地在这些共享变量上进行read-modify-write操作,但这种封闭是相当脆弱的。

栈封闭

使用局部变量,并保证这个局部变量不溢出。可以理解为仅限于线程内部/局部使用。

ThreadLocal

关于ThreadLocal之后会专门写一些文章,这里的话简单说一下思想;ThreadLocal为每个使用该变量的线程存有一份独立的本地副本,实际上是通过内存隔离(打破共享条件)实现的线程安全。

使用不可变对象

定义

满足下面条件的对象叫不可变对象:

  • 对象创建后,其状态不能被修改
  • 对象是正确创建的(无 this 引用逸出)
    当这个对象中的状态需要被改变时,直接废除当前的对象,new一个新对象代替现在的旧对象

final域

final域是我们用来构造不可变对象的一个利器,因为被它修饰的域一旦被初始化后,就是不可修改的了,不过这有一个前提,就是如果 final 修饰的是一个对象引用,必须保证这个对象也是不可变对象才行,否则 final 只能保证这个引用一直指向一个固定的对象,但这个对象自己的状态是可以改变的,所以一个所有域都是 final 的对象也不一定是不可变对象(例如final StringBuffer)。

2 种初始化方式

final i = 42;
final i;  // 之后在每一个构造函数中给i赋值

注意:对于含有 final 域的对象,JVM确保了初始过程化的安全性,它保证了对象的初始引用在构造函数之后执行,不会发生指令重排(也就是说,一旦得到了对象的引用,那么这个对象的 final 域一定是已经完成了初始化的)!
补充说明:对象的创建过程
类加载检查->分配内存->初始化->设置对象头->执行init方法(最后完成初始引用)

安全发布对象

保证发布的对象的初始化构造过程不会受到任何其他线程干扰,就像加了锁一样,被创建它的线程构造好了,在发布给其他线程。

  • 不可变对象可以以任何方式发布
  • 事实不可变对象必须通过安全方式发布
  • 可变对象必须通过安全方式发布 并且必须是线程安全的

常用发布模式

  • 在静态块中初始化一个对象引用
  • 将对象引用保存到 volatile 类型的域或者 AtomicReference 对象中
  • 将对象引用保存到某个正确构造的对象的 final 域中
  • 将对象引用保存到一个被锁保护的域中

最简单和安全的发布方式

public static Holder holder = new Holder(42);
可以保证安全的原因:静态变量的赋值操作在加载类的初始化阶段完成,包含在<clinit>()方法的执行过程中,因此这个过程受到 JVM 内部的的同步机制保护,可以用来安全发布对象。

Java 提供的可以安全发布对象的容器

  • Map

    • HashTable
    • ConcurrentMap
    • Collections.SynchronizedMap
  • List

    • Vector
    • CopyOnWriteArrayList
    • CopyOnWriteSet
    • Collections.SynchronizedSet
  • Queue

    • BlockQueue
    • ConcurrentLinkedQueue

Reference

[1] Volatile趣谈——我是怎么把贝克汉姆的进球弄丢的
[2] Programming With Assertions - Oracle Java SE Documentation
[3] Java内部类持有外部类的引用详细分析与解决方案