第16章 Java内存模型

什么是内存模型,为什么需要它

aVariable = 3;

内存模型需要解决这个问题:这什么条件下,读取aVariable的线程看到这个值为3

JMM 规定了 JVM 必须遵循一组最小保证,这组保证规定了对变量的写入操作在何时将对于其他线程可见。 JMM 在设计时就在可预测性和程序的易于开发性之间进行了权衡,从而旨在各种主流的处理器体系架构上能实现高性能的 JVM

平台的内存模型

在共享内存的多处理器体系架构中,每个处理器都拥有自己的缓存,并且定期地与主内存进行协调。在不同的处理器架构中提供了不同级别的缓存一致性(Cache Coherenc的,其中一部分只提供最小的保证,即允许不同的处理器在任意时刻从同一个存储位置上看到不同的值。

重排序

同步将限制编译器、运行时和硬件对内存操作重排序的方式,从而实施重排序时不会破坏 JMM 提供的可见性保证。

Java内存模型简介

JMM 为程序中所有的操作定义了一个偏序关系 ,称之Happens-Before.

  • 程序顺序规则
  • 监视器锁规则
  • volatile 变量规则
  • 线程启动规则
  • 中断规则
  • 终结器规则
  • 传递性

借助同步

由于 Happens-Before 的排序功能很强大,因此有时候可以“借助( Piggyback )"现有同步机制的可见性属性。

程序清单 16-2 说明如何借助同步的 FutureTask 的内部类

// FutureTask 内部类
private final class Sync extends AbstractQueuedSynchronizer {
  private static final int RUNNING = 1, RAN = 2, CANCELLED = 4;
  private V result;
  private Exception exception;
  
  void innerSet(V v) {
    while (true) {
      int s = getState();
      if (ranOrCancelled(s)) {
        return;
      }
      if (compareAndSetState(s, RAN)) {
        break;
      }
    }
    result = v;
    releaseShared(0);
    done();
  }
  
  V innerGet() throws InterruptedException, ExecutionException {
    acquireSharedInterruptibly(0);
    if (getState() == CANCELLED) {
      throw new CancellationException();
    }
    if (exception != null) {
      throw new ExecutionException(exception);
    }
    return result;
  }
}

发布

不安全的发布

除了不可变对象以外,使用被另一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行

安全的发布

Happens-Before 排序是在内存访问级别上操作的,它是一种“并发级汇编语言“,而安全发布的运行级别更接近程序设计。

安全初始化模式

线程安全的延迟初始化

@ThreadSafe
public class SafeLazyInitialization {
  private static Resource resource;
  
  public synchronized static Resource getInstance() {
    if (resource == null) {
      resource = new Resource();
    }
    return source();
  }
}

程序清单 16-5 提前初始化

@ThreadSafe
public class EagerInitialization {
  private static Resource resource = new Resource();
  
  public static Resource getResource() {
    return resource;
  }
}

程序清单 16-6 延长初始化占位类模式

@ThreadSafe
public class ResourceFactory {
  private static class ResourceHolder {
    public static Resource resource = new Resource();
  }
  public static Resource getResource() {
    return ResourceHolder.resource;
  }
}

双重检查加锁

DCL 的这种使用方法已经被广泛地废弃了,促使该模式出现的驱动力〈无竞争同步的执行速度很慢,以及 JVM 启动时很慢)己经

不复存在,因而它不是一种高效的优化措施。延迟初始化占位类模式能带来同样的优势,并且更容易理解。

初始化过程中的安全性

初始化安全性将确保,对于被正确构造的对象,所有线程都能看到由构造函数为对象给各个 final 域设置的正确值,而不管采用何种方式来发布对象。而且,对于可以通过被正确构造对象中某个 final 域到达的任意变量(例如某个 final 数组中的元素,或者由一个 final 域引用的 HashMap 的内容)将同样对于其他线程是可见的。

程序清单 16-8 不可变对象的初始化安全性

@ThreadSafe
public class SafeStates {
  private final Map<String, String> states;
  
  public SafeStates() {
    states = new HashMap<String, String>();
    states.put("alaska", "AK");
    states.put("alabama", "AL");
    
  }
  public String getAbbreviation(String s) {
    return states.get(s);
  }
}

初始化安全性只能保证通过 final 域可达的值从构造过程完成时开始的可见性。对于通过非final域可达的值,或者在构成过程完成后可能改变的值,必须采用同步来确保可见性。