JMM(可见性-原子性-重排序)

JMM(可见性-原子性-重排序)

Posted by bulingfeng on July 28, 2024

可见性

造成可见性的最根本原因是CPU缓存,并且还是多核CPU下的缓存。换而言之如果是单CPU的缓存则不会存在可见性问题。

重排序

下面的代码有可能先执行ready = true; 然后再执行value=2,从而导致的结果就是t1线程输出的结果为1。

重排序也只会在多线程情况下才有可能发生问题的(请注意是多线程,不用关心CPU是否是多核)。

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
public class Demo30_3 {
  private static boolean ready = false;
  private static int value = 1;
  
  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        while (!ready) {
        }
        System.out.println(value);
      }
    });

    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        value = 2;
        ready = true;
      }
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();
  }
}

重排序分类

  1. 编译优化导致的重排序
  2. CPU指令并行执行导致的重排序
  3. 硬件内存模型导致的重排序

原子性

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
31
public class Demo30_2 {
  private  static volatile int count = 0;

  public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i = 0; i < 10000; ++i) {
          count++;
        }
      }
    });

    Thread t2 = new Thread(new Runnable() {
      @Override
      public void run() {
        for (int i = 0; i < 10000; ++i) {
          count++;
        }
      }
    });

    t1.start();
    t2.start();

    t1.join();
    t2.join();

    System.out.println(count);
  }
}

如何解决多线程中的可见性、原子性和有序性问题?

通过3个关键词分别是:volatile、synchronized、final,1个规则是happens-before规则。

volitile

volitile可以解决可见性问题,重排序问题和部分原子性问题(原子性很少会用到volitile)。

volitile可以让线程主动让值刷新到内存中和主动从内存中获取数据。

使用volitile来创建双重检验的单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Singleton {
  private static Singleton instance;
  private int value;
  
  private Singleton(int value) {
    this.value = value;
  }
  
  public static Singleton getInstance() {
    if (instance == null) {
      synchronized (Singleton.class) {
        if (instance == null) {
          instance = new Singleton(11);
        }
      }
    }
    return instance;
  }
  
  // 省略其他方法,比如value的getter方法
}

instance = new Singleton(11); 不是原子性的,

  • 为对象分配内存空间
  • 初始化对象
  • 将内存地址分配给instance

使用volitle来修饰instance,从容让将内存地址分配给instance不能重排序到初始化对象和为分配对象空间之前。

假如不加volitile会造成将内存地址分配给instance提前,但是对象还没有进行初始化,从而造成这个单例不可用。

一个常见的错误

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
private static  boolean running = true;
    private static  int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (running) {
                    count++;
                }
                System.out.println("count: " + count);
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                running = false;
            }
        });

        t1.start();
        Thread.sleep(1000); // 1s
        t2.start();
        t1.join();
        t2.join();
    }

比如上述代码,很多人用这个代码来证明多线程之间的可见性问题,但是其实是不正确的。如果是是可见性问题,那么当t2线程改变了running=true的时候,t1线程也不会一直永远进行while循环而不停止,但是实际情况确实t1线程一直在运行。

原因是:

在进行JIT编译时,JIT编译器会同步进行一定的编译优化。JIT编译器在编译线程t1中的while循环时,因为探测到running一直为true,所以,对其进行优化,省掉了每次判定running是否为true的逻辑.

认为优化后的代码为:

1
2
3
4
5
6
7
@Override
public void run() {
  while (true) {
    count++;
  }
  System.out.println("count: " + count);
}

可以给running之前加上volitile关键字,来让JIT优化不那么激进可以解决问题。

当然也可以给count来加上volitile,这样因为count++是一个写操作,会强制上面的指令不发生重排序(为了保证之前的读和写正确),然后让running去内存中获取。