Java并发_JMM和volatile(4)

JMM

JMM (Java Memory Model) 即 Java 内存模型,是对实际内存的一个抽象,盗一张网上传烂的图

JMM
JMM

主内存被所有线程共享,本地内存则是每个线程自己独有的,不同线程之间无法访问对方的本地内存,都需要通过主内存来进行通信

例如线程 A 与线程 B 之间的通信,必须是线程 A 将其本地内存中共享变量的副本刷新到主内存中,线程 B 要先到主内存中读取对应的共享变量,但后拷贝一份到自己的本地内存中

所以共享变量会出现线程之间可见性的问题,即一个线程修改了某变量的值,新值如果没有立刻同步到主内存中,则其他的线程从主内存中读取该变量读到的将是旧值

volatile

synchronized 可见性、原子性、有序性三个性质都能保证,volatile 不能保证原子性,但能保证可见性和有序性,所以 synchronized 的性能较低,volatile 的同步更弱

volatile 能保证被其修饰的变量对所有线程的可见性,即每次写都会立刻同步到主内存,读都会读取到主内存中最新的值,底层实现的方式是通过内存屏障 (操作系统的内容先暂且跳过)

所以 volatile 不能保证原子性,即对于非原子性操作的中间过程的可见性是不能保证的,例如对 volatile 变量进行自增操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
  public static volatile int count = 0;
  public static void main(String[] args) {
    //开启10个线程
    for (int i = 0; i < 10; i++) {
      new Thread(new Runnable() {
        @Override
        public void run() {
          try {
            //等待主线程开启10个线程执行完,再执行自增操作
            Thread.sleep(500);
            for (int j = 0; j < 1000; j++) {
              count++;
            }
            System.out.println(Thread.currentThread().getName() + " : " + count);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }).start();
    }
    //主线程等待10个线程执行结束
    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.println("最后结果 : " + count); //期望结果为10000,但很可能小于10000
  }
}

自增可以大概分为取值、加 1、更新值三步操作,可以很容易地想象到,如果一个线程执行到加 1 操作时,此时如果有其他的线程修改了 count 的值,但是对于当前线程是不可见的,所以再当前线程执行完以后,之前那个线程修改的值将被覆盖,所以最后结果很可能小于 10000

所以 volatile 适用于仅仅是 get 或 set 的情况,不适用于 getAndOperate 的情况

volatile 用作判断标志也是比较合适的,例如下面的 signal 如果不加 volatile 则可能会造成死循环

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Test {
  public static volatile boolean signal = false;
  public static void main(String[] args) {
    new Thread(new Runnable() {
      @Override
      public void run() {
        signal = true;
      }
    }).start();
    while (!signal) {
    }
    System.out.println("end");
  }
}

有序性问题的产生则是由于指令重排,即 JVM 编译 Java 代码,或 CPU 执行 JVM 字节码时,会对一些指令的顺序进行重新排序,进而优化程序的执行效率

对于单线程无影响,但会影响多线程,可通过 volatile 来阻止指令的重排 (底层是通过内存屏障来实现的),例如双重检查锁 DCL 的单例模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Singleton {
  private static Singleton instance;
  private Singleton() {}
  public static Singleton getInstance() {
    //第一次判断,若已经创建了对象,则不用进入同步代码块,提高性能
    if (instance == null) {
      synchronized (Singleton.class) {
        //可能会有多个线程进入外层if,不加内层if会重复创建对象
        if (instance == null) {
          instance = new Singleton();
        }
      }
    }
    return instance;
  }
}

若同时有两个线程进入了外层 if ,线程 A 进入同步代码块后线程 B 只能等待,等线程 A 创建完对象,由于 synchronized 能保证可见性,instance 引用会被立即更新,等线程 B 再进入同步代码块时,读取到的 instance 已经指向了线程 A 创建的对象,就不会进入内层 if

注意到 instance = new Singleton(); 不是原子操作,可抽象为以下三条 JVM 指令

1
2
3
memory = allocate();    //1 分配对象的内存空间
initInstance(memory);   //2 初始化对象
instance = memory;      //3 将instance引用指向刚分配的内存地址

操作 2 依赖操作 1,但操作 3 不依赖操作 2,所以可以被指令重排序为

1
2
3
memory = allocate();    //1 分配对象的内存空间
instance = memory;      //3 将instance引用指向刚分配的内存地址(此时对象还未初始化)
initInstance(memory);   //2 初始化对象

即若线程 A 执行了 1、3 操作,同时线程 B 执行到外层 if,获取到 instance 不为 null 了,则直接返回 instance 引用,但此时 instance 指向的对象还没有完成初始化

可以通过给引用添加 volatile 来防止指令重排

1
private volatile static Singleton instance;

happens-before

默认存在无需借助同步操作的 happens-before 先行发生原则,有以下 8 条

程序顺序规则,在一个线程内代码顺序执行,即指令重排不会在 Java 代码层面重排,不会改变 Java 源码的顺序

监视器锁规则,一个锁的解锁先发生于,其后续的这把锁的加锁

volatile 规则,volatile 变量的写操作先发生于,其后续的读操作

线程启动规则,Thread 的 start 方法先发生于,该线程的所有操作

线程中断规则,interrupt 方法中断线程的调用先发生于,被中断的线程检测到中断发生

线程终止规则,线程所有的操作都先发生于,线程终止的检测操作 (可以通过 Thread.isAlive 方法检测线程是否已结束)

对象终结规则,一个对象的初始化先行发生于,GC 调用其 finalize 方法

传递规则,如果 A 操作先于 B 操作,B 操作又先于 C 操作,则 A 操作先于 C 操作


以上内容是玉山整理的笔记,如有错误还请指出