线程安全的问题

目录

1 .线程安全则主要体现在三个方面:

1.1 原子性

1.1.1 Java 实现原子性的两种方式:

1.2 可见性

1.3有序性

1.3.1 重排序

1.3.2 指令重排序

1.3.3 存储子系统重排序

1.3.4 貌似串型语义

1.3.5 保证内存的访问顺序


线程安全的问题_第1张图片

1 .线程安全则主要体现在三个方面:

  • 原子性
  • 可见性
  • 有序性

接下来就对这三个特性进一步说明:

线程安全的问题_第2张图片

1.1 原子性

  •  原子:分子组成的最小单位。(不可分割)
  • 原子操作不可分割的两层含义
    • 访问共享变量的原子操作是不能够交错的
    • 访问(读、写):某个共享变量的操作从其他线程来看。
      • 该操作要么执行完毕。
      • 要么尚未发生。
      • 例如生活中的取款,两个人同时操作一张银行卡时,不可能两个人同时成功。

1.1.1 Java 实现原子性的两种方式:

  • 第一种:使用锁  
    • 锁的缺点:
    • 多线程编程中 我们⼀般会使⽤各种锁来确保对共享资源的访问和操作,需要共享的数据要对它的访问串⾏化。
      修改共享数据的操作必须以原⼦操作的形式出现才能保证不被其它线程破坏相应数据。
      锁这种机制⽆法彻底避免以下⼏点问题:

      1、锁引起线程的阻塞,对于没有能占⽤到锁的线程或者进程将会⼀直等待锁的占有者释放资源后才能继续。
      2、申请和释放锁的操作增加了很多访问共享资源的消耗。
      3、锁不能很好的避免编程开发者设计实现的程序出现死锁或者活锁可能
      4、优先级反转和所护送怪现象
      5、难以调试
  • 第二种:利用处理器的CAS(Compare and Swap比较并操作)指令。
    • CAS:⽐较并操作,解决多线程并⾏情况下使⽤锁造成性能损耗的机制。

线程安全的问题_第3张图片  两者比较:

  •  锁具有排它性,保证共享数据在某一时刻只能被以线程执行。
  • CAS 指令直接在硬件(处理器和内存)层次上实现,看做是硬件锁

1.2 可见性

在多线程环境中,一个线程对某一个共享变量进行更新之后,后续其它线程可能无法立即读到这个更新的结果这就是线程安全中的另一种形式:可见性(visibility)。

  •  如果一个线程对共享变量更新后,后续访问该变量的其它线程可以读到这个更新之后的结果,称这个线程对共享变量的更新对其他线程可见,否则称这个线程对共享变量的更新对其他线程不可见。
  • 多线程程序因为可见性问题可能会导致其它线程读到了脏数据(过期的数据)。
package stu.my.cdn.threadsafe;

/**
 * 测试线程的可见性
 */
public class Test02 {
    public static void main(String[] args) throws InterruptedException {
        MyTask myTask = new MyTask();
        new Thread(myTask).start();

        Thread.sleep(6000);
        // 主线程 1 秒后取消执行
        myTask.cancel();
        /*
            可能会出现以下情况:
                在 main 线程中调用了 myTask.cancel() 方法,把myTask的 toCancel 修改为 true
                可能存在子线程看不到 main 线程对 toCancel做的修改,在子线程中 toCancel 变量的值一直为 false
                导致子线程看不到 main 线程对 toCancel 变量更新的原因可能为:
                    1)JIT即时编译器,可能会对 run 方法中的 while循环进行优化为:
                        if(!toCancel){
                             while(true){
                                if(doSomething())
                             }
                        }
                    2) 可能与计算机的存储系统有关,假设分别有两个 cpu 内核运行 main 线程与子线程,
                      一个 cpu 内核无法立即读取另一个cpu 内核中的数据。(运行在子线程的cpu无法立即读取
                      main 线程cpu的数据)
         */
    }

    static class MyTask implements Runnable{
        private boolean toCancel = false;
        @Override
        public void run() {
            while(!toCancel){
                if(doSomething()) break;
            }
            if(toCancel){
                System.out.println("任务被取消");
            }else{
                System.out.println("任务正常执行");
            }
        }

        private boolean doSomething() {
            System.out.println("执行了某个任务");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return true;
        }

        public void cancel(){
            toCancel = true;
            System.out.println("收到 取消线程的消息");
        }
    }
}

总的来说,可见性就是,一个线程修改了数据,能让其共享的线程及时的到新数据。 

线程安全的问题_第4张图片

1.3有序性

  • 有序性(Ordering):是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另一个处理器运行的其它线程看起来是乱序的(Out of Order)
  • 乱序是指内存访问操作的顺序看起来发生了变化。

1.3.1 重排序

  • 在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的:
    • 编译器可能会改变两个操作的先后顺序。
    • 处理器也可能不按照目标代码的顺序执行
    • 这种一个处理器上执行的多个操作,在其它处理器来看的顺序与目标代码指定的顺序可能不一样,这种现象为重排序。
    • 重排序是指编译器处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
    • 可能出现,和可见性一样,不是必然的。

  与内存操作顺序有关的几个概念:

  • 源码顺序:就是源码中指定访问的内存顺序。
  • 程序顺序:处理器上运行的目标代码所执行的内存访问顺序。
  • 执行顺序:内存访问操作在处理器上的实际执行顺序。
  • 感知顺序:给定处理器所感知到的,该处理器即及它处理器的程序访问操作的顺序。

   可以把重排序分为存储子系统重排序子系统重排序 :

  • 指令重排序:主要是由 JIT 编译器,处理器引起的,指程序顺序与执行顺序不一样。
  • 存储子系统重排序:是由高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致。

  JIT 编译器 和 javac 编译器区别:

  • JIT:

    Java 代码的执行过程中,首先进行前端编译,通过编译器将 .java文件 编译成字节码,即 .class文件。
    然后,进行后端编译,将字节码编译成机器码,在此过程中,有一个编译器将热点代码编译成本地平台相关的机器码,并进行优化。 

    javac: javac的任务是将Java源代码语言 (.java文件) 先转化成JVM能够识别的一种语言,然后由JVM将JVM语言再转化成当前这个机器能够识别的机器语言,javac的作用简单来说就是通过一些列的流程之后将.java文件转换为.class文件, javac 编译器位于jdk --> bin -->javac

  • javac:

     在 Java 中提到“编译”,自然很容易想到 javac 编译器将*.java文件编译成为*.class文件的过程,这里的 javac 编译器称为前端编译器,其他的前端编译器还有诸如 Eclipse JDT 中的增量式编译器 ECJ 等。相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的 Java 程序在运行时基本都是解释执行加编译执行),如 HotSpot 虚拟机自带的 JIT(Just In Time Compiler)编译器(分 Client 端和 Server 端)。另外,有时候还有可能会碰到静态提前编译器(AOT,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码,如 GCJ、Excelsior JET 等,这类编译器我们应该比较少遇到。

1.3.2 指令重排序

  • 在源码顺序与程序顺序不一致,或者 程序顺序与执行顺序不一致时的情况下,我们就说发生了指令重排序。

  • 指令重排序是一种动作,确实对指令的顺序做了调整,重排序的对象指令。

  • javac 编译器一般不会执行指令重排序,而 JIT 编译器可能执行指令重排序。

  • 处理器也可以执行指令重排序,使得执行顺序与程序顺序不一致。

  • 指令重排不会对单线程程序的结果正确性产生影响,可能导致多线程程序出现非预测的结果。

1.3.3 存储子系统重排序

  • 存储子系统是指写缓冲器与高速缓存:

  • 高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配设计的一个高速缓冲

  • 写缓冲器(Store buffer,Write buffer)用来提高写高速缓冲操作的效率。

  • 即使处理器严格按照程序执行两个内存访问操作,在存储子系统的作用下,其它处理器对这两个操作的感知顺序和程序顺序一致,即使这两个操作的顺序看起来像是发生了变化,这种现象称为存储子系统重排序。

  • 存储子系统重排序并没有真正的对程序执行顺序进行了调整,而是造成一种指令执行顺序被调整的现象。

  • 存储子系统重排序对象是内存操作的结果。

1.3.4 貌似串型语义

  • JIT 编译器,处理器,存储子系统是按照一定的规则对指令,内存操作的结果进行重排序,给单线程程序造成一种假象----------指令是按照源码的顺序执行的,这种假象称为貌似串型语义。并不能保证多线程环境程序的正确性。

  • 为了保证貌似串型语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的数据才会被重排序。如果两个操作(指令)访问同一个变量,其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data dependency)

    • 如:

      • x=1; y=x+1; 后一条语句的操作数包含前一条语句的执行结果;

      • y = x; x = 1 ; 先读取x变量,在更新 x 变量的值。

      • x = 1; x = 2; 两条语句同时对一个变量进行写操作。

  • 如果不存在数据依赖关系则可能重排序

    • 如:

      • double price = 45.8;

      • int quantity = 10;

      • double sum = price * quantity;

  • 存在控制依赖关系的数据允许重排。一条语句(指令)的执行结果会决定另一条语句(指令)能否执行,这两条数据(指令)存在控制依赖关系(Control Dependency)。如在 if 语句中允许重排,可能存在处理器先执行 if 代码块,在判断 if 条件是否成立。

1.3.5 保证内存的访问顺序

  • 可以使用 volatile 关键字
  • 使用 synchronized 关键字实现有序性。

两者比较:

volatile与sychronized的比较:

1)volatile只能修饰变量sychronized可以修饰变量、方法,以及代码块

2)多线程访问volatile修饰的变量时不会发生阻塞,而访问sychronized修饰的内容时,线程会发生阻塞;

3)volatile只能保证可见性,而sychronized保证了原子性,间接保证了可见性,因为它会将本地缓存和主内存的数据同步。

感谢阅读~~~希望对你有帮助~~~

线程安全的问题_第5张图片

你可能感兴趣的:(多线程安全,JIT,javac)