多线程之内存模型

JMM模型

1、java内存模型

java内存模型并非是真是存在的,而是一种抽象的概念,也可以理解成是一个接口规范,它定义了程序中的变量(实例字段、静态字段和构成数组对象的元素)的访问方式。

JVM中运行程序的实体是线程,而每个线程在被创建的时候,JVM都会为其单独分配一个工作内存(通常被理解成栈空间),用于存储线程私有的数据。而java内存模型规定了所有的变量都需要存储在主内存,主内存是共享内存区域,所有的线程都可以来进行访问。但是线程对变量的操作(读取操作)都必须要在自己的工作内存中完成。首先要将主内存中的变量拷贝到自己的工作内存中来,然后将操作完成的变量写回到主内存中去,线程是无法直接操作主内存中的变量的,每个线程的工作内存中都会保存到主内存中共享变量的副本。

线程的工作内存是每个线程自己的私有区域,其他的线程无法来进行访问。要想实现线程之间的通信,必须要通过主内存完成。

看下图来进行总结:

2、JMM和JVM的区别

JMM和JVM内存的划分是不同的概念,JMM是用来描述通过这组规则控制程序中各个变量在共享区域和私有区域的访问方式。

JMM是围绕着原子性、有序性、可见性展开的。JMM和java中的内存区域唯一相似的一点在于,都存在于共享区域和私有区域,在JMM中主内存属于共享内存区域,包括了堆和栈空间,而线程的私有工作区域,也就是线程的工作空间,从某种程度上来说包含了程序计数器、虚拟栈和本地方法栈。

3、主内存和工作内存

主内存:主要存储的是java实例对象,所有线程创建的实例对象都放在了主内存中,不管实例对象是成员变量还是局部变量,还包括了类的共享信息、常量和静态变量。由于主内存是共享区域,所以多条线程对同一个变量访问可能会造成线程安全问题。

工作内存:主要存储的是当前方法中的所有本地变量信息(这里包括了从主内存中拷贝过来的共享变量的副本),每个线程只能够访问自己工作内存中的信息,而不能够访问到其他线程的工作内存中的信息,因为当前的工作内存中的本地变量对于其他的线程来说是不可见的。

不管两个线程空间的代码和数据究竟是不是相同的,每个线程依然会在每个线程空间中创建本地变量信息,因为每个线程的空间是属于每个线程私有的,其他的线程无法访问的到,那么说明每个线程空间的数据是安全的,即不存在线程安全问题。

根据JVM虚拟机规范主内存和工作内存的数据存储类型以及操作方式,对于一个实例对象中的成员方法来说,如果方法中包含的变量是基本类型数据类型:boolean,byte,short,char,int,long,float,double,那么将直接存储到工作内存中的栈帧结构中;但是如果局部变量是引用数据类型,那么该变量的引用将会存储在功能内存中的栈帧中,而对象实例将会存储在主内存(共享数据区域)堆中。

但是对于实例对象的成员变量,不管是基本类型还是引用类型,那么都将会存储到堆区。至于static关键字修饰的成员和类信息都将会存储到主内存中来。需要注意的是,因为实例对象是存储在主内存中的,所以可能被其它的线程访问到。如果多个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中来,执行完操作之后,会刷新到主内存中去。

从这里可以得到一个信息,那就是如果多个线程操作同一个实例对象的方法,在操作完成之后需要写回到主内存中去。

那么多个线程之间的最终操作的结果之间达到的效果是覆盖。

4、Java内存模型与硬件内存架构的关系

通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应

该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬

件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没

有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划

分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存

在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内

存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算

机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注

意对于Java内存区域划分也是同样的道理)

5、JMM的必要性

在明白了Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型

的具体关系后,接着来谈谈Java内存模型存在的必要性。由于JVM运行程序的实体是线

程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线

程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量

从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量

写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱

发线程安全问题。

假设主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作,

A/B线程各自的工作内存中存在共享变量副本x。假设现在A线程想要修改x的值为2,而B线

程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案

是,不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值

2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,首先是将变量从主内

存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而

对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假

如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到

自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后,

B线程才开始读取的话,那么此时B线程读取到的就是x=2,但到底是哪种情况先发生呢?

如以下示例图所示案例:

以上关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作

内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来

完成。

数据同步八大原子操作

(1)lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态

(2)unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后

的变量才可以被其他线程锁定

(3)read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存

中,以便随后的load动作使用

(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工

作内存的变量副本中

(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎

(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内

存的变量

(7)store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存

中,以便随后的write的操作

(8)write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值

传送到主内存的变量中

如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操

作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

6、同步规则分析

1)不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中

2)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化

(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行 assign和load操作。

3)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重

复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。

4)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个

变量之前需要重新执行load或assign操作初始化变量的值。

5)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去

unlock一个被其他线程锁定的变量。

6)对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write

操作)

7、并发编程的可见性,原子性与有序性问题

7.1、 原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

在java中,对基本数据类型的变量的读取和赋值操作是原子性操作有点要注意的是,对

于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说

如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因

为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,

这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取

到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能

是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为

读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数

据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。

 X=10; //原子性(简单的读取、将数字赋值给变量) 

 Y = x; //变量之间的相互赋值,不是原子操作 

 X++; //对变量进行计算操作 

 X = x+1; 

7.2 、可见性

理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量

的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,

因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且

是修改过的新值。

但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线

程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了

共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操

作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步

延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过

前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确

实会导致程序轮序执行的问题,从而也就导致可见性问题。

7.3、有序性

有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这

样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序

现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺

序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如

果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单

线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟

现象

7.4、指令重排序

java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的 重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处 理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

下图为从源码到最终执行的指令序列示意图:

从上面的规则中可以理解是,只要最终的结果是正确的,那么针对每种对应的实现来说,是不需要来管的。指令重排可以更大的发挥机器性能就可以了。举个例子说明一下:

int i = 10;
int j= 20;
int k = i;

我们看到的是上面的顺序,但是对于指令重排之后的可能就是:

int i = 10;
int k = i;
int j= 20;

我们可以从缓冲行中来进行理解,对于64字节的缓存行来说,当第一行代码操作完成之后,可能对于缓存行中的变量i来说,还没有过期,我们可以直接操作缓存行来进行操作。如果按照我们上面的写法,可能把j变量读进缓存行中去了之后,原来的缓存失效了,就无法再从缓存行中来进行获取得到对应的值了,这个时候又需要从主内存中来读取对应的值。按照这里理论猜想,那么速度就降下来了。

那么就无法充分利用到CPU的效率了。综上所述,主要是为了压榨CPU而已。

7.5、代码实现

代码实现看下:

public class JMMDemoOne {
    private static boolean initFlag = false;

    private /*volatile*/ static int counter = 0;

    public static void refresh(){
        System.out.println("refresh data.......");
        initFlag = true;
        System.out.println("refresh data success.......");
    }

    public static void main(String[] args){
        Thread threadA = new Thread(()->{
            while (!initFlag){
                //System.out.println("runing");
                counter++;
            }
            System.out.println("线程:" + Thread.currentThread().getName()
                    + "当前线程嗅探到initFlag的状态的改变");
        },"threadA");
        threadA.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread threadB = new Thread(()->{
            refresh();
        },"threadB");
        threadB.start();
    }
}

控制台打印:

refresh data.......
refresh data success.......

上面介绍的一种场景:两个线程去操作一个共享变量initFlag,线程A启动之后,一直调用while循环;线程B启动之后,调用refresh方法,想要去修改initFlag变量。

但是在控制台一直在进行检测的时候发现,发现线程A一直处于while循环中,而线程B执行完成结束之后,已经修改过了initFlag变量的值。但是按照我们想的是应该是线程B执行完成了之后initFlag变量就已经发生了修改,线程A中的变量就会发生变化,然后就会执行线程A中的while中的代码。但是实际上并没有

那么从上面的代码来进行分析,线程A和线程B分别要想访问主存中的initFlag变量,两个线程会各自拷贝initFlag变量到自己的工作内存中来,然后操作完成之后,会刷新到主内存中去。但是线程B改变了initFlag变量之后,线程A因为需要来进行访问initFlag变量,但是这和我们实际看到的不同。原因在哪里?

我觉得是因为线程A在执行while循环的时候过快,一直访问的是数据是Cache中的数据,因为Cache一直在进行改变,此时此刻并没有从主内中来进行查询,导致了改变的一致是线程A一直是在改变Cache中的值,而和主存中的数据来不及交流。

那么将上面的数据进行修改,也就是把对volatile关键字的注解放开

public class JMMDemoOne {
    private static boolean initFlag = false;

    private volatile static int counter = 0;

    public static void refresh(){
        System.out.println("refresh data.......");
        initFlag = true;
        System.out.println("refresh data success.......");
    }

    public static void main(String[] args){
        Thread threadA = new Thread(()->{
            while (!initFlag){
                //System.out.println("runing");
                counter++;
            }
            System.out.println("线程:" + Thread.currentThread().getName()
                    + "当前线程嗅探到initFlag的状态的改变");
        },"threadA");
        threadA.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread threadB = new Thread(()->{
            refresh();
        },"threadB");
        threadB.start();
    }
}

查看控制台:

refresh data.......
refresh data success.......
线程:threadA当前线程嗅探到initFlag的状态的改变

可以看到很快的就进行了修改。这是因为volatile具备着特别的作用,就是能够让修改的共享变量即使能够让其他的线程可见

但是真的这样的一种方式吗?

再写一种方式来进行说明:

public class JMMDemoOne {
    private static boolean initFlag = false;

    private /*volatile*/ static int counter = 0;

    public static void refresh(){
        System.out.println("refresh data.......");
        initFlag = true;
        System.out.println("refresh data success.......");
    }

    public static void main(String[] args){
        Thread threadA = new Thread(()->{
            while (!initFlag){
                System.out.println("runing");
                //counter++;
            }
            System.out.println("线程:" + Thread.currentThread().getName()
                    + "当前线程嗅探到initFlag的状态的改变");
        },"threadA");
        threadA.start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread threadB = new Thread(()->{
            refresh();
        },"threadB");
        threadB.start();
    }
}

控制台:

....
runing
refresh data.......
refresh data success.......
线程:threadA当前线程嗅探到initFlag的状态的改变

可以看到尽管没有加volatile关键字,也可以使得这种方式来进行结束。

那么这种方式又该来如何解释?

线程B修改的共享变量initFlag的值,刷新回了主内存之后,对于线程A来说,无法及时可以看到线程B修改过后的值,所以在刷新了很多次running之后,又会重新看到

对于这里来说,很多人又会说:

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

因为这里有一个同步块来进行操作。但是从上面的结果中看来,线程A是打印了很多次running之后,才看到while执行结束的结果。

对于这种来说,我统一总结成对于操作共享变量的线程来说无法及时可见主内存空间的共享数据。

其实还有很多种情况可以实现上面的效果,但是问题就是能够保证"共享变量一旦发生修改,其他的也在操作共享变量的线程立马感知到"。但是从实验中可以得到,无法保证。而java提供的volatile关键字可以来保证共享变量一旦发生修改,其他操作共享变量的线程就可以立马感知到的效果。

这里描述的是JMM中的可见性问题

下面再来介绍一种情况:

public class JMMDemoTwo {
    private volatile static int counter = 0;
    static Object object = new Object();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 100000; j++) {
                        counter++;//分三步- 读,自加,写回
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

查看控制台:

479874

这里需要设计到CPU的事情,因为java采用的是KLT线程模型,对应的是操作系统的内核级线程来进行操作。

而线程又是CPU执行的最小调度单位,而CPU又不是总会在一个线程上执行,通过时间片来进行调度(其实这种说法也是不太正确的,因为有时候时间片没有用完,又优先级高的过来,也会被中断当前的线程执行,这里情况很多),CPU发生了上下文切换,需要使用寄存器来保存好当前线程的场景。

把上面的counter++分为三步:

// var可以理解成cpu自己使用的寄存器来保存的变量
int var = counter;
var = var+1;
counter = var;

线程的执行并非是一步到位的,也就是说并不是一次性全部都执行完。而是会伴随着CPU的切换,当其中的一个线程执行到counter++的第一步的时候,线程发生切换,然后另外一个线程执行完成之后会将主内存中的counter更新,因为counter变量使用volatile关键字修饰的,所以最开始的线程会读取到最新的数据,重新执行counter++代码,那么就意味着这里已经少加了一次,从而损失了一次。

那么对于这种线程频繁发生切换的场景下,这种场景非常常见,正是因为这种频繁的上下文切换,造成了有些数据少加了。

尽管使用了volatile关键字修饰,也无法避免原子性问题。

这个是原子性,那么想要解决原子性问题,这种方式其实也很简单。常见的两种方式:syncronized关键字和lock锁来进行保证

下面写一个例子来进行说明:

public class JMMDemoTwo {
    private volatile static int counter = 0;
    static Object object = new Object();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 100000; j++) {
                    synchronized (object){
                        counter++;//分三步- 读,自加,写回
                    }
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter);

    }
}

使用了这种方式,无论使用多少次,最终都将只会获取得到相同的值,也就是最终执行的效果和单线程下执行的效果是一样的。

也可以使用纯java实现的lock锁来进行同步机制

下面需要用代码来解决最后一个问题,有序性问题。

public class JMMDemoThree {
    private  static int x = 0, y = 0;
    private  static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    shortWait(10000);
                    a = 1;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }

    }

    /**
     * 等待一段时间,时间单位纳秒
     * @param interval
     */
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

现在需要做的就是来验证指令重排带来的效果。

从上面的程序中,我们可以看到,主内存中定义了四个变量

    private  static int x = 0, y = 0;
    private  static int a = 0, b = 0;

在for循环中不断的给x,y,a,b来赋值为初始值,可以看到有两个线程来对x,y,a,b来进行赋值,但是我们最终想要的是x和y的值,只要这两个值符合条件,那么就会跳出循环并打印出来对应的值

首先分析下两个线程中的执行情况,这里可以用图解的方式来进行说明(这里先说明,没有指令重排下的情况):

加入说,线程A先执行完,然后再执行线程B

这种情况下得到的结果应该是:

x=0,y=1

那么再看第二种:

线程B先执行,然后执行线程A

这种情况下得到的结果是:

x=1,y=0

那么再看一种交叉执行的情况:

这里是线程B执行到b=1这不操作之后,线程上下文切换,然后线程A执行a=1,然后线程上下文切换,执行y=a,执行完成之后,上下文切换,x=b,最终能够得到的结果就是:

x=1,y=1

那么目前就只能够获取得到这么多的信息了,也就只有这三种情况了,不可能再次出现其他的情况。

那么如果说,有了指令重排的参与了,又会有什么情况的产生呢?

如果说指令重排了,也就是说每个线程之间的变量的定义的位置发生了改变,那么就有可能会导致了这种情况的发生,最终导致的x和y的结果就是

x=0,y=0

那么为了验证这种结果,那么我们可以用程序来进行实现:

public class JMMDemoThree {
    private  static  int x = 0, y = 0;
    private  static  int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    shortWait(10000);
                    a = 1;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }

    }

    /**
     * 等待一段时间,时间单位纳秒
     * @param interval
     */
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

控制台输出:

第66346次 (0,0)

那么通过这个结果可以看到,指令重排的现象发生了。但是指令重排的效果在这个地方出现了错误,因为不再符合我们的预期结果,可能我们预期的结果就只能够接收前三种,而无法接收到第四种结果。如果接受到第四种,那么我们认为这种结果是错误的,那么又该如何来避免这种情况的发生呢?

还是上面的讲的解决可见性的关键字volatile关键字

public class JMMDemoThree {
    private  static volatile int x = 0, y = 0;
    private  static volatile int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    shortWait(10000);
                    a = 1;
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }

    }

    /**
     * 等待一段时间,时间单位纳秒
     * @param interval
     */
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

那么在控制台等待着结束,但是可以看到无论等待多久,都没有出现上面的情况。

但是对上面的变量加上了volatile关键字之后,禁止指令重排之后,就可以导致上面的指令重排,从而导致了第四种情况不会出现。

从而避免了第四种情况的发生,那么就不会导致这种问题的出现。

除了上面的这种方式,还有另外的一种方式来进行解决,那么就是利用UnSafe类中的方法来进行解决。但是又不能直接来进行使用,因为可能会造成内存泄漏,这种不推荐来进行使用。

如果需要用的话需要使用到

public class UnsafeInstance {

    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

然后对应的代码是:

public class JMMDemoThree {
    private  static  int x = 0, y = 0;
    private  static  int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    shortWait(10000);
                    a = 1;
                    UnsafeInstance.reflectGetUnsafe().fullFence();
                    x = b;
                }
            });

            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    UnsafeInstance.reflectGetUnsafe().fullFence();
                    y = a;
                }
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }

    /**
     * 等待一段时间,时间单位纳秒
     * @param interval
     */
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

这种方式解决的局部变量之间的放置指令重排的问题;而适用于成员变量的这种来说,推荐使用volatile

因为volatile关键字无法对局部变量来进行使用。

7.6、单例模式中的问题

public class Singleton {
    private  static Singleton myinstance;

    /**
     * 双重锁机制保证单例安全
     * @return
     */
    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();
                }
            }
        }
        return myinstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

如同上述的代码案例,这个如果在多线程中,如果线程竞争不是激烈,那么是不会出现问题的。但是如果说线程竞争激烈,那么依然可能存在着线程安全问题。

可能出现的点在:

if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();
                }
            }
        }

如果两个线程同时进入到了synchronized关键字这里后,只有一个线程进入了,另外一个线程只能够等待,然后进入后的线程开始来创建对象,但是对于一个创建的创建来说可以分为三个步骤:

memory = allocate();//1.分配对象内存空间 
instance(memory);//2.初始化对象
instance = memory;//3.设置instance指向刚分配的内存地址,此时instanc e!=null

但是因为指令重排的存在,那么就可能会导致出现下面的结果:

memory=allocate();//1.分配对象内存空间 
instance=memory;//3.设置instance指向刚分配的内存地址,此时instanc e!=null,但是对象还没有初始化完成! 
instance(memory);//2.初始化对象

这样子就会导致未来得及初始化,然后就被别的线程使用了该对象,调用过程了发生了类似空指针异常这种,所以也是可能会存在的。

所以正确的方式,也是万无一失的方式就是:

public class Singleton {

    private volatile static Singleton myinstance;

    /**
     * 双重锁机制保证单例安全
     * @return
     */
    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();
                }
            }
        }
        return myinstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

8、总结

posted @ 2021-10-17 01:58  雩娄的木子  阅读(185)  评论(0编辑  收藏  举报