Skip to main content

第3章 对象的共享

对象的共享

同步synchronized不仅可用于原子性或者确定临界区(Critical Section),还可以保证内存可见性(Memory Visibility)

3.1 可见性

重排序(Reordering)读线程看到的顺序与写入的顺序完全相反

  • 编译器优化的重排序
  • 指令级并行的重排序
  • 内存系统的重排序

Java内存访问重排序的研究

3.1.1 失效数据

3.1.2 非原子的64位操作

3.1.3 加锁与可见性

3.1.4 Volatile变量

用来确保将变量的更新操作通知到其它线程

3.2 发布与逸出

发布对象(Publish):使对象在当前作用域之外的代码中使用

当某个不该发布的对象被发布时,逸出(Escape)

class UnsafeStates {
private String[] states = new String[] {"AL", "AK"};

public String[] getStates() { return states; }
}

隐式使this引用逸出

public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
// 内部类初始化之后,已经拿到父类的this引用,但此时父类还没初始化完成
public void onEvent(Event e) {
doSt(e);
}
});
}
}

安全的对象构造过程,不要在构造过程中使this引用逸出

public class SafeListener {
private final EventListener listener;

private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSt(e);
}
}
}

public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}

3.3 线程封闭

避免同步,不使用共享数据。

单线程内访问数据,线程封闭(Thread Confinement)

3.3.1 Ad-hoc线程封闭

完全由程序实现来承担,尽量少用

3.3.2 栈封闭

只有局部变量才能访问对象

3.3.3 ThreadLocal

通常用于防止对可变的单实例变量(Singleton)和全局变量进行共享

3.4 不变性

不可变对象(Immutable Object)一定是线程安全的,满足同步

  • 对象创建以后其状态就不能修改

  • 对象的所有域都是final类型

  • 对象是正确创建的(在对象创建期间,this引用没有逸出)

    保存在不可变对象中的程序仍然可以更新,即通过将一个保存新状态的实例来替换原有的不可变对象

3.4.1 Final域

JAVA内存模型,final域确保初始化过程中的安全性,从而可以不受限制的访问不可变对象,且共享访问时无需同步。

final域重排序规则:

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用 变量,这两个操作之间不能重排序。

  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能 重排序。

    public class FinalExample {
    // 普通变量
    int i;
    // final变量
    final int j;
    static FinalExample obj;

    // 构造函数
    public FinalExample() {
    // 写普通域,可能重排序到构造函数之外,i = 1 还没写入普通域i
    i = 1;
    // 写final域
    j = 2;
    }

    // 写线程A执行
    public static void writer() {
    obj = new FinalExample();
    }

    // 读线程B执行
    public static void reader() {
    // 读对象引用
    FinalExample object = obj;
    // 读普通域,可能重排序到“读对象引用”之前,线程A还没写入i
    int a = object.i;
    // 读final域
    int b = object.j;
    }
    }

写final域的重排序规则

  • JMM禁止编译器把final域的写重排序到构造函数之外
  • 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外

读final域的重排序规则

  • 在一个线程中,初次读对象引用与初次读该对象包含的final 域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。

final域为引用类型

1是对final域的写入,2是对这个final域引用的对象的成员域的写入,3是把被构造的对象的引用赋值给某个引用变量。这里除了前面提到的1不能和3重排序外,2和3也不能重排序。

/* 
* 假设首先线程A执行writerOne()方法,执行完后线程B执行 writerTwo()方法,执行完后线程C执行reader()方法。
* JMM不保证线程B的写入对读线程C可见,因为写线程B和读线程C之间存在数 据竞争,此时的执行结果不可预知。
* 写线程B和读线程C之间需要使 用同步原语(lock或volatile)来确保内存可见性
*/
public class FinalReferenceExample {
final int[] intArray;
static FinalReferenceExample obj;

public FinalReferenceExample() {
// 1: 写final引用
intArray = new int[1];
// 2: 写final引用对象的成员域
intArray[0] = 1;
}

// 写线程A执行
public static void writeOne() {
// 3: 把构造对象的引用赋值给引用变量obj
obj = new FinalReferenceExample();
}

// 写线程B执行
public static void writeTwo() {
// 4: 写final引用对象的成员域
obj.intArray[0] = 2;
}

// 读线程C执行
public static void reader() {
// 5: 读对象引用obj
if (obj != null) {
// 6: 读final引用的成员域
int temp1 = obj.intArray[0];
}
}
}

为什么final引用不能从构造函数内“溢出”

保证在构造器内部,不能让这个被构造对象的引用为其它线程所见,也就是对象引用不能在构造函数中“逸出”。

public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;

public FinalReferenceEscapeExample() {
// 1:写final域
i = 1;
// 2:this引用在此逸出,1,2可能被重排序
obj = this;
}

public static void writer() {
new FinalReferenceEscapeExample();
}

public static void reader() {
// 读取不为null的对象引用a
if (obj != null) {
// 可能读取到final域初始化之前的值
int temp = obj.i;
}
}
}

3.4.2 Volatile类型发布不可变对象

不可变对象能够提供一种弱形式的原子性

当需要对一组相关数据以原子方式执行某个操作,就可以考虑创建一个不可变的类来包含这些数据

@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;

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);
}
}

https://juejin.cn/post/6844903601068998664

@ThreadSafe
public class VolatileCachedFactorizer extends GenericServlet implements Servlet {
// 可见性
private volatile OneValueCache cache = new OneValueCache(null, null);

public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
// i 和 factors 是一一对应的,保障了缓存一致性
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}

void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}

BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}

BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[]{i};
}
}

3.5 安全发布

3.5.1 不正确的发布:正确的对象被破坏

public Holder holder;

public void initialize() {
// 由于可见性问题,其它线程看到尚未创建完成的对象
holder = new Holder(42);
}

不能指望尚未完全创建的对象拥有完整性。

public class Holder {
private int n;

public Holder(int n) { this.n = n; }

public void assertSanity() {
// 第一次读取到失效值,再次读取得到更新值
if (n != n) {
thrown new AssertionError("This statement is false.")
}
}
}

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

Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。

任何钱程都可以在不需妥额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

3.5.3 安全发布的常用模式

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

线程安全库中的容器类提供了安全发布保证:

  • 将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全的将它发布给任何从这些容器访问它的线程
  • 将某些元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素发布到任何从这些容器中访问该元素的线程
  • 将某个元素放入BlockingQueue或ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

类库中的其它传递机制(Future、Exchanger)同样能实现安全发布

发布静态构造的对象,最简单和最安全的方式是使用静态的初始化器:

public static Holder holder = new Holder(42);

静态初始化器由JVM 在类的初始化阶段执行。由于在 JVM 内部存在着同步机制,因此通过这种方式初始化的任何对象都可以坡安全地发布[JLS 12.4.2]。

3.5.4 事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么提这种对象称为“事实不可变对象( Effectively Immutable Object)”

在没是有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<String, Date>);

保存每位用户的最近登录时间,如果Date对象的值放入Map后就不会改变,那么synchronizedMap中的同步机制足以使Date值被安全发布,并且访问Date值时不需要额外的同步。

3.5.5 可变对象

可变对象在构造后可以修改,不仅在发布对象时需要使用同步,而且在每次对象访问时需要使用同步来确保后续修改操作的可见性。

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布

  • 事实不可变对象必须通过安全方式来发布

  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来

3.5.6 安全的共享对象

  • 线程封闭
  • 只读共享,包括不可变对象和事实不可变对象
  • 线程安全共享,线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步
  • 保护对象,封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象