Java线程安全问题(原子性、可见性、有序性)

培训教学 潘老师 6个月前 (11-09) 120 ℃ (0) 扫码查看

本文重点介绍Java多线程的中的线程安全问题有哪些,以及Java如何解决多线程安全问题。

1.线程安全问题

非线程安全:主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的问题。

线程安全:原子性、可见性、有序性

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作(Atomic、CAS算法、synchronized、Lock)
  • 可见性:一个主内存的线程如果进行了修改,可以及时被其他线程观察到(synchronized、volatile)
  • 有序性:如果两个线程不能从 happens-before原则 观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序,导致其观察观察结果杂乱无序(happens-before原则)

2.原子性

原子(Atomic)就是不可分割的意思。一条线程在执行一系列程序指令操作时,该线程不可中断。一旦出现中断,那么就可能会导致程序执行前后的结果不一致。与数据库中的原子性(事务管理体现)是相同的

概括:一段程序只能由一条线程去完整的执行,不能被多个线程干扰执行。

原子操作的不可分割有两层含义:

  • 1)访问(读、写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕,要么尚未发生。即其他线程看不到当前操作的中间结果。
  • 2)访问同一组共享变量的原子操作,是不能够交叉的。

Java有两种方式保证原子性:

  • 使用锁:锁具有排它性,保证共享变量在某一时刻只能被一个线程访问。
  • 利用处理器的CAS(Compare and Swap)指令:CAS指令直接在硬件(处理器和内存)层次上实现原子性,可以看作是硬件锁。

以下这段代码因为没有考虑原子性,导致这两个线程读取的num值有时候是一样的(因为num++其实是分步执行的)。

public class Test01 {

    public static void main(String[] args) {
        //启动两个线程,不断调用getNum()方法
        MyInt myInt = new MyInt();

        for(int i = 1; i <= 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(true) {
                        System.out.println(Thread.currentThread().getName() + "->" + myInt.getNum());
                        try {
                            TimeUnit.MILLISECONDS.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    }

    static class MyInt{
        int num;
        public int getNum() {
            return num++;
        }
    }
}

3.可见性

简介:一个线程对主内存的修改可以及时被其他线程观察到。

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

如果一个线程对共享变量更新后,后续访问该变量的其他线程可以马上读到更新的结果,称这个线程对共享变量的更新对其他线程具有可见性;反之称为没有可见性。

多线程程序可能因为可见性,导致其他线程读取到了旧数据(脏数据)。

导致共享变量在线程间不可见的原因:

  • 1.线程交叉执行
  • 2.重新排序结合线程交叉执行
  • 3.共享变量更新后的值没有在工作内存中与主内存间及时更新

下面这段代码可能出现这种情况:在main线程中调用了myTask的cancel()方法修改toCancel为true,但是myTask线程看不到。

//测试线程的可见性
public class Test02 {

    public static void main(String[] args) throws InterruptedException {
        MyTask myTask = new MyTask();
        new Thread(myTask).start();
        TimeUnit.MILLISECONDS.sleep(1000);
        myTask.cancel();
    }

    static class MyTask implements Runnable {
        private boolean toCancel = false;

        @Override
        public void run() {
            while(!toCancel) {
                doSomething();
            }
            if(toCancel) System.out.println("任务被取消");
            else System.out.println("任务正常结束");
        }

        private boolean doSomething() {
            System.out.println("执行某个任务");
            try {
                TimeUnit.MILLISECONDS.sleep(1000); //模拟任务执行需要的时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return true;
        }

        public void cancel() {
            toCancel = true;
        }
    }
}

原因: 1、 JIT及时编译器可能对while循环进行优化:

if(!toCancel) {
    while(true) {
        doSomething();
    }
}

2、 可能与计算机的存储系统有关假设main线程和myTask线程分别运行在两个cpu上,而一个cpu不能立即读取到另一个cpu中的数据;

4.有序性

有序性(Ordering):在某种情况下,一个处理器上某个线程执行的内存访问操作,在另一个处理器上的线程看来是乱序的。

  • 在JMM中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
  • 通过volatile、synchronized、lock保证有序性

与内存操作顺序相关的概念:

  • 源代码顺序:源码中指定的操作顺序
  • 程序顺序:处理器上目标代码的顺序
  • 执行顺序:内存访问操作在处理器上的实际操作顺序
  • 感知顺序:处理器感受到该处理器和其他处理器的内存操作顺序

重排序可以分为指令重排序和存储子系统重排序:

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

指令重排序:

当源代码顺序和程序顺序不一致,或者程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)。

指令重排是一种动作,确实对指令进行了调整,重排序的对象是指令。Java编译器一般不会进行指令重排,但是JIT可能会执行这个操作。

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

指令重排不会对单线程程序的结果产生影响,但是可能对多线程程序的结果产生影响。

存储子系统重排序:

存储子系统指的是高速缓存和写缓冲器:

  • 高速缓存指的是CPU为了弥补其与主存储器处理速度不一致的问题而设置的,目的是提高CPU读取数据的速度。
  • 写缓冲器,用来提高写高速缓存的效率。

即使严格按照程序顺序的两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序可能不一样,即这两个操作的顺序看起来像是发生了变化,这就是存储子系统重排序。

存储子系统重排序并没有对指令执行顺序产生影响,而是造成指令执行顺序被调整的假象。

存储子系统操作的对象是内存操作的结果。

5.线程安全性总结

  • 原子性:Atomic包、CAS算法、synchronized、Lock 原子性做了互斥方法,同一个线程只能有一个进行操作
  • 可见性:synchronized、volatile 一个主内存的线程如果进行了修改,可以及时被其他线程观察到,介绍了volatile如何被观察到的
  • 有序性:happens-before原则。happens-before原则,观察结果,如果两个线程不能偶从happens-before原则观察出来,那么就不能观察他们的有序性,虚拟机可以随意的对他们进行重排序

版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/teach/10905.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】