内存模型之可见性

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

1 Java内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等.

体现方面:

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

2 可见性

退不出循环

因main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程永远无法停止:

static boolean run = true;
public static void main(String[] args) throws InterruptedException {
 Thread t = new Thread(()->{
 while(run){
  // ....
 }
 });
 t.start();
 sleep(1);
 // 状态改变 线程t不会如预想的停下来
 run = false; 
}

说明:

1 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存

image.png

2 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率

image.png

3 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值

image.png

解决办法:

volatile (英文意为 多变,易变)

主要用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存

可见性VS原子性

可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见, 不能保证原子性,仅用在一个写线程,多个读线程的情况.

从字节码分析:

getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
getstatic run // 线程 t 获取 run true 
putstatic run // 线程 main 修改 run 为 false, 仅此一次
getstatic run // 线程 t 获取 run false 

引出指令交错问题:

两个线程一个 i++ 一个 i-- ,只能保证看到最新值,并不能保证结果为0

// 假设i的初始值为0 
getstatic i // 线程2-获取静态变量i的值 线程内i=0 
getstatic i // 线程1-获取静态变量i的值 线程内i=0 
iconst_1 // 线程1-准备常量1 
iadd // 线程1-自增 线程内i=1 
putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 
iconst_1 // 线程2-准备常量1 
isub // 线程2-自减 线程内i=-1 
putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1 

注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低

附加:

上面死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,为什么?

	public void println(String x) {
        synchronized (this) {
            print(x);
            newLine();
        }
    }

从System.out.println() 方法可以看到, 里面添加了synchronized关键字,而且锁住的是当前对象.

3 同步模式之Balking (犹豫模式)

定义:

Balking (犹豫)模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事,那么本线程就无需再做 了,直接结束返回

实现:

public class MonitorService {
     // 用来表示是否已经有线程已经在执行启动了
     private volatile boolean starting;
    
     public void start() {
         log.info("尝试启动监控线程...");
         synchronized (this) {
             if (starting) {
                 return;
             }
             starting = true;
         }
         // 启动线程 ...
     }
}

/*
多次调用start方法:
该监控线程已启动?(false)
监控线程已启动...
该监控线程已启动?(true)
该监控线程已启动?(true)
该监控线程已启动?(true)
*/

类似之前设计模式中的线程安全单例,即

public final class Singleton {
     private Singleton() {
     }
    
     private static Singleton INSTANCE = null;
     public static synchronized Singleton getInstance() {
         if (INSTANCE != null) {
             return INSTANCE;
         }

         INSTANCE = new Singleton();
         return INSTANCE;
     }
}

与保护性暂停模式相比:

  • 区别是保护性暂停模式用在一个线程等待另一个线程的执行结果,当条件不满足时线程等待.

猜你喜欢

转载自juejin.im/post/7126697783389585416