《java并发编程实战》读书笔记2–对象的共享,可见性,安全发布,线程封闭,不变性

这章的主要内容是:如何共享和发布对象,从而使它们能够安全地由多个线程同时访问。

内存的可见性

确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

上面的程序中NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。一种更奇怪的现象是NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序”。多线程之指令重排序

失效数据

简而言之就是在缺乏同步的程序中可能会读取到过期的数据,也就是失效数据,就像上面的例子一样,当度线程查看ready变量时可能会的得到一个失效的值。

非原子的64位操作

虽然得到的可能是一个失效值,但至少这个值是由之前每个线程设置的,而不是一个随机值。这种安全性保证也被称为最低安全性。最低安全性适用于绝大多数变量,但是对于非volatile类型的64位数值变量(double和long)并非如此。java变量的读操作和写操作都是原子的,但是JVM允许将64位的读操作和写操作分解为2个32为的操作。这样当读取一个非volatile的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32为和另一个值的低32位。

加锁与可见性

Volatile变量

确保将变量的更新操作通知到其他线程,当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。在读取volatile类型的变量时总会返回最新的写入值。下面的程序给出了volatile变量的一种典型用法:检查每个状态标记以判断是否退出循环。

volatile的语义不足以确保递增操作(count++)的原子性。当且仅当满足一下所有条件是,才应该使用volatile变量:

1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值

2. 该变量不会与其他状态变量一起纳入不变性条件中

3. 在访问变量时不需要加锁

Java中Volatile关键字详解&&Java并发编程:volatile关键字解析

发布与逸出

“发布”一个对象是指:使对象能够在当前作用域之外的代码中使用。当某个不该发布的对象被发布时,这种情况就被称为逸出。发布对象的最简单方法是将对象的引用保存到一个公有的静态变量中;

安全的对象构造过程

不要在构造过程中使this引用逸出。在构造过程中使this引用逸出的一个常见的错误是:在构造函数中启动一个线程。当对象在其构造函数中创建一个线程时,无论是显示创建还是隐士创建,this引用都会被新创建的线程共享。(简而言之在对象的构造函数中的别的对象能够拿到当前对象的this引用从而造成逸出)。

线程封闭

不共享数据,仅在单线程内访问数据,避免使用同步的方式。这种技术被大量使用喻Swing和JDBC的Connection对象。

Ad-hoc线程封闭

指维护线程封闭性的职责完全由程序实现来承担。很脆弱,尽量使用更强的线程封闭技术(如栈封闭或ThreadLocal类)

栈封闭

只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中。他们位于执行线程的栈中,其他线程无法访问这个栈。

ThreadLocal类

这个类能使线程中的某个值与保存值的对象关联起来。ThisLocal提供了get和set等访问接口或方法,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal对象通常用于防止对可变的单实例或全局变量进行共享。通过将JDBC的连接保存到ThreadLocal对象中(因为JDBC的连接对象不一定是线程安全的),每个线程都会有属于自己的连接。

当某个频繁执行的操作需要一个临时对象,例如缓冲区,同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。当某个线程初次调用ThreadLocal.get方法时,救会调用initialValue来获取初始值。

不变性

满足同步需求的另一种方法是使用不可变对象,即对象创建以后状态不能修改,它的所有域都是final类型,并且对象是正确创建的(this引用没有逸出)。在不可变对象的内部仍可以使用可变对象来管理它们的状态,如

Final域

final类型的域是不能修改的,但如果final域所引用的对象是可变的(如上例),那么这些被引用的对象是可以修改的。

示例:使用volatile类型来发布不可变对象

继续前面因式分解的例子。因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以创建一个不可变的类来包含这些数据,如:

对数值及其因数分解结果进行缓存的不可变容器类

@Immutableclass OneValueCache {  private final BigInteger lastNumber;  private final BigInteger[] lastFactors;  /**   * 如果在构造函数中没有使用 Arrays.copyOf()方法,那么域内不可变对象 lastFactors却能被域外代码改变   * 那么 OneValueCache 就不是不可变的。   */  public OneValueCache(BigInteger i,             BigInteger[] factors) {    lastNumber  = i;    lastFactors = Arrays.copyOf(factors, factors.length);  }  public BigInteger[] getFactors(BigInteger i) {    if (lastNumber == null || !lastNumber.equals(i))      return null;    else      return Arrays.copyOf(lastFactors, lastFactors.length);  }}

安全发布

不正确的发布,正确的对象被破坏

不可变对象与初始化安全性

任何线程都可以子在不需要额外同步的情况下安全地访问不可变对象(状态不可变,域都是final类型,正确的构造过程)

安全发布的常用模式

可变对象必须通过安全的方式来发布。

事实不可变对象

本身可变但对象在发布后不会被修改。在没有额外的同步情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

可变对象

不仅需要安全发布,而且需要额外的同步和线程安全来保护

安全地共享对象