Java 线程安全问题的本质

原创声明:做者:Arnold.zhao 博客园地址:https://www.cnblogs.com/zh94html

目录:缓存

线程安全问题的本质

出现线程安全的问题本质是由于:安全

主内存和工做内存数据不一致性以及编译器重排序致使。服务器

因此理解上述两个问题的核心,对认知多线程的问题则具备很高的意义;多线程

简单理解CPU

CPU除了控制器、运算器等器件还有一个重要的部件就是寄存器。其中寄存器的做用就是进行数据的临时存储。并发

CPU的运算速度是很是快的,为了性能CPU在内部开辟一小块临时存储区域,并在进行运算时先将数据从内存复制到这一小块临时存储区域中,运算时就在这一小快临时存储区域内进行。咱们称这一小块临时存储区域为寄存器。jvm

CPU读取指令是往内存里面去读取的,读一条指令放到CPU中,CPU去执行,对内存的读取速度比较慢,因此从内存读取的速度去决定了这个CPU的执行速度的。因此不管咱们的CPU怎么去升级,可是若是这方面速度没有解决的话,其的性能也不会获得多大的提高。函数

为了弥补这个缺陷,因此添加了高速缓存的机制,如ARM A11的处理器,它的1级缓存中的容量是64KB,2级缓存中的容量是8M,
经过增长cpu高速缓存的机制,以此弥补服务器内存读写速度的效率问题;工具

JVM虚拟机类比于操做系统

JVM虚拟计算机平台就相似于一个操做系统的角色,因此在具体实现上JVM虚拟机也的确是借鉴了不少操做系统的特色;性能

JAVA中线程的工做空间(working memory)就是CPU的寄存器和高速缓存的抽象描述,cpu在计算的时候,并不老是从内存读取数据,它的数据读取顺序优先级 是:寄存器-高速缓存-内存;
而在JAVA的内存模型中也是同等的,Java内存模型中规定了全部的变量都存储在主内存中,每条线程还有本身的工做内存(相似于CPU的高速缓存),线程的工做内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的全部操做(读取、赋值)都必须在工做内存中进行,而不能直接读写主内存中的变量,操做完成后再将变量写回主内存。不一样线程之间没法直接访问对方工做内存中的变量,线程间变量值的传递均须要在主内存来完成。基本关系以下图:

注意:这里的Java内存模型,主内存、工做内存与Java内存区域模型的Java堆、栈、方法区不是同一层次内存划分,这二者基本上没有关系。

重排序

在执行程序时,为了提升性能,编译器和处理器经常会对指令进行重排序。通常重排序能够分为以下三种:

举例以下:

public class Singleton {
    public static  Singleton singleton;

    /**
     * 构造函数私有,禁止外部实例化
     */
    private Singleton() {};

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

如上,一个简单的单例模式,按照对象的构造过程,实例化一个对象一、能够分为三个步骤(指令):
一、 分配内存空间。
二、 初始化对象。
三、 将内存空间的地址赋值给对应的引用。
可是因为操做系统能够对指令进行重排序,因此上面的过程也可能变为以下的过程:
一、 分配内存空间。
二、 将内存空间的地址赋值给对应的引用。
三、 初始化对象 。

因此,若是出现并发访问getInstance()方法时,则可能会出现,线程二判断singleton是否为空,此时因为当前该singleton已经分配了内存地址,但其实并无初始化对象,则会致使return 一个未初始化的对象引用暴露出来,以此可能会出现一些不可预料的代码异常;

固然,指令重排序的问题并不是每次都会进行,在某些特殊的场景下,编译器和处理器是不会进行重排序的,但上述的举例场景则是大几率会出现指令重排序问题(关于指令重排序的概念后续给出详细的地址)

原创声明:做者:Arnold.zhao 博客园地址:https://www.cnblogs.com/zh94

汇总

因此,如上可知,多线程在执行过程当中,数据的不可见性,原子性,以及重排序所引发的指令有序性 三个问题基本是多线程并发问题的三个重要特性,也就是咱们常说的:

并发的三大特性:原子性,有序性,可见性;

原子性:代码操做是不是原子操做(如:i++ 看似一个代码片断,实际的执行中将会分为三步执行,则必然是非原子化的操做,在多线程的场景中则会出现异常)
有序性:CPU执行代码指令时的有序性;
可见性:因为工做线程的内存与主内存的数据不一样步,而致使的数据可见性问题;

一些解释

可是,问题就真的有那么复杂吗?若是按照上面所说的问题,i++是非原子操做,就会出现并发异常的问题,new Object() 就会出现重排序的并发问题,那么Java开发还能作吗。。我随便写个方法代码,岂不是就会出现并发问题?可是为何我开发了这么久的代码,也没有出现过方法并发致使的异常问题啊?
烧的麻袋;
这里就要说明另一个问题,JVM的线程栈,JVM线程栈中是线程独有的内存空间(如:程序计数器以线程栈帧)而线程栈帧中的局部变量表则用来存储当前所执行方法的基本数据类型(包含 reference, returnAddress等),因此当方法在被线程执行的过程当中,相关的对象引用信息,以及基本类型的数据都是线程独有的,并不会出现多个线程访问时的并发问题,也就是简单来讲:一个方法内的变量定义以及方法内的业务代码,是不会出现并发问题的。多个线程并不会共享一个方法内的变量数据,而是每一个方法内的定义都属于当前该执行线程的独有栈空间中。(因此经过Java线程栈的这一独特特性天然当中则为咱们省了不少事项;)

可是因为咱们的线程的数据操做不可能每次都去访问主存中的数据,对于线程所使用到的变量须要copy至线程内存中以增长咱们的执行速度,因此就引出了咱们上述所提到的并发问题的本质问题,线程工做空间和主内存的数据不一样步而致使的数据共享时的可见性问题;

如:此时定义一个简单的类

class Person{
    int a = 1;
    int b = 2;

    public void change() {
        a = 3;
        b = a;
    }

    public void print() {
        String result = "b=" + b + ";a=" + a;
        System.out.println(result);
    }
    
    public static void main(String[] args) {
        while (true) {
            final Person test = new Person();
            new Thread(() -> {
                Thread.sleep(10);
                test.change();
            }).start();

            new Thread(() -> {
                Thread.sleep(10);
                test.print();
            }).start();
        }
    }
}

如上,假设此时多个线程同时访问change()以及print() 方法,则可能会出现print所输出的结果是:b=2;a=1或者b=3;a=3;这两种都是正常现象,但还有多是会输出结果是:b=2;a=3以及b=3;a=1;

Person类所定义的变量a和b,按照JVM内存区域划分,在对象实例化后则都是存储到数据堆中;
按照咱们上述关于线程工做内存的解释来看,此时线程在执行change()方法和print()方法时,因为两个方法都有关于外部变量的引用,因此须要copy主内存中的这两个变量副本到对应的线程工做内存中进行操做,执行完之后再同步至主内存中。

此时在A线程执行完change()方法后,a=3,b=3;但此时a=3在执行完成后尚未同步到主内存,但b=3此时已经提供至主内存了,那么此时B线程执行print()数据输出后,则获得的是结果是:b=3;a=1;同理也能够获得b=2;a=3的可能性结果;因此此处则因为线程共享变量的可见性问题,而致使了上述的问题;

正是因为存在上述所提到的线程并发所可能引发的种种问题,因此JDK则也有了后续的一系列多线程玩法:ThreadLocal,CountDownLatch,ReentrantLock,Unsafe,synchronized,volatile,Executor,Future 这些供开发者在开发程序时用来对多线程保驾护航的助手类,以及JDK已经自身开发好的支持线程安全的一些工具类,StringBuffer,CopyOnWriteArrayList, ConcurrentHashMap,AtomicInteger等,供开发者开箱即用;后续针对这些JDK自身所提供的一些类的玩法会作进一步说明,顺便系统整理下脑中的信息,造成有效的知识结构;End;

参考连接

写到这里可能你依然会对线程工做内存和主内存的同步机制比较感兴趣,则能够参考这里:

Java内存模型总结

若是对上述所提到的线程栈的局部变量表等概念依然不是很清晰,则能够参考这里:

JVM虚拟机的内存区域分配

原创声明:做者:Arnold.zhao 博客园地址:https://www.cnblogs.com/zh94

相关文章
相关标签/搜索