Java线程安全-可见性问题

Java内存模型(JMM) 与 JVM运行时数据区

  • Java内存模型是《Java语言规范》中,描述对java语言的一个特性,在执行多线程的一种形态,描述了允许多个线程程序的合法行为

  • JVM运行时数据区是《Java虚拟机规范》中,描述Java虚拟机的一个实现

【Java语言和虚拟机规范】
The Java Language Specification
The Java Virtual Machine Specification
访问地址:https://docs.oracle.com/javase/specs/


多线程中的问题

  1. 所见非所得
  2. 无法肉眼检测程序的准确性
  3. 不同的运行平台有不同的表现
  4. 错误很难重现

问题解析

public class Demo {
    int i = 0;
    public boolean isRunning = true;
    
    public static void main(String args[])  throws InterruptedException {
        Demo demo = new Demo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("here i am ...");
                while(demo.isRunning){
                    demo.i ++;
                }
                System.out.println("i=" + demo.i);
            }
        }).start(); 

        Thread.sleep(3000L);
        demo.isRunning = false;
        System.out.println("shutdown...");
     }
}
  • 输出结果
启动参数 32位JDK 64位JDK
-server 不打印i的值 不打印i的值
-client 打印i的值 不打印i的值
  • 为什么不会打印i的值?

    理解代码中的逻辑,i值没打印是不是高速缓存的问题?

  • 可见性问题分析

    1. 高速缓存虽然有高速缓存协议约定,但是也可能导致急短的时间内出现可见性问题
    2. 不打印出i值,只有isRunning一直等于true才会出现,所以并不是高速缓存的问题
    3. 只有子线程无法读到主线程对数据操作后的变化
  • CPU指令重排序
    令重排序只保证单个线程的结果最终一致性,但不保证多个线程的结果,这里就会引发代码无法打印i的值

JIT编译器(Just In Time Compiler)

  • 脚本语言和编译语言的区别
    解释执行:脚本在执行时,有语言解释器将其一条条翻译成机器可识别的指令
    编译执行:直接将编译成机器可以识别的指令码(批量编译)

  • Java语言是脚本语言还是编译语言? 如图

    Java即是编译语言又是脚本语言

  • JIT编译后的结果

 bool f = isRunning

 if(f) {
    while(true) {
        i++
    }
 }
实际的编译结果-引发无法打印i的结果

volatile关键字

Java内存模型规定:对volatile变量的写入,与所有其他线程后续对变量的读同步
可见性问题:让一个线程对共享变量的修改,能及时的被其他线程看到

  • 禁止缓存,volatile变量的访问控制符会加上ACC_VOLATILE
  • 不指令重排,对volatile变量相关的指令不做重排序
public class Demo {
    int i = 0;
    public volatile boolean isRunning = true;
    
    public static void main(String args[])  throws InterruptedException {
        Demo demo = new Demo();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("here i am ...");
                while(demo.isRunning){
                    demo.i ++;
                }
                System.out.println("i=" + demo.i);
            }
        }).start(); 

        Thread.sleep(3000L);
        demo.isRunning = false;
        System.out.println("shutdown...");
     }
}

对变量加入volatile关键字修饰后,能打印i的结果

volatile 只会对变量禁止缓存和不指令重排,但是JIT还是可以批量编译,只是不会出现这种“激进”的编译结果


线程理论

共享变量 (Shared Variables定义)

  • 可以在线程之间共享的内存称为共享内存或者堆内存
  • 实例字段、静态字段和数组元素都是存储在堆内存中,这些都属于共享变量
  • 能够被多个线程访问的共享变量是内存模型规范的对象

线程间操作定义(约束)

  1. 线程间操作指:一个程序执行的操作可被其他线程感知或者被其他线程直接影响
  2. Java内存模型只描述线程间操作,不描述线程内操作,线程内操作按照线程内语义执行,如局部变量

对同步的规则定义

  • 对volatile变量v的写入,与所有其他线程后续对变量v的读的同步
  • 对监视器m的解锁与所有后续操作对于m的加锁同步
  • 对每个属性写入默认值(0、flase、null)与每个线程对其进行的操作同步

Happens-before先行发生原则

我们无法就所有场景来规定某个线程修改的变量何时对其他线程可见,但是我们可以指定某些规则,这规则就是happens-before,从JDK 5 开始,JMM就使用happens-before的概念来阐述多线程之间的内存可见性。

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

JVM需要实现如下规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

final在JMM中的处理

  • final在该对象的构造函数中设置对象的字段,当线程看到该对象时,将始终看到该对象的final字段的正确构造版本
  • 如果在构造函数中设置字段后发生读取,则会看到该final字段分配的值,否则将看到默认值
public class Demo{
  final int x;
  int y;
  public Demo(){
     x = 3; //能正确的读到 x = 3的构造版本
     y = 4; //看到 y=0 的构造版本
  }
}
  • 被final修饰的变量要在对象创建之前完成赋值
    定义变量,静态代码块,构造方法赋值一次
public class Demo{
  final int x;
  int y;
  public Demo(){
     x = 3; //能正确的读到 x = 3的构造版本
     y = 4; //也等读到y=3的构造版本
  }
}
  • 读取该共享对象的final成员变量之前,先要读取共享对象。
    伪代码示例:r=new Demo(); k = r.f; 这两个操作不能重排序
  • 通常被static final修饰的字段,不能被修改。然而System.in、System.out、System.err被修饰也可以改。

你可能感兴趣的:(Java线程安全-可见性问题)