码迷,mamicode.com
首页 > 其他好文 > 详细

14、volatile(轻量级的同步机制)

时间:2020-06-01 23:40:03      阅读:70      评论:0      收藏:0      [点我收藏+]

标签:图片   atomic   完整   内存屏障   vol   通过   使用命令   nat   查看   

引用学习(狂神说)

谈谈你对 volatile 的理解

Volatile 是 Java 虚拟机提供的轻量级的同步机制

它的3个特性:

1、保证可见性

2、不保证原子性(原子性就是任务要么完整执行,要么都不执行)

3、禁止指令重排

深刻理解volatile的3个特性

1、保证可见性

  • 上面代码中程序不是死循环了吗?因为线程A并不知道num的值已经被修改。

  • 如何解决呢?

  • 因为volatile关键字保证了可见性,所以主存中的值发生修改后,其他线程可以清晰地看到。

package com.zxh.testValidate;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    // volatile 保证可见性,但主存的值发生修改,线程可以看见
    private volatile static int num = 0; // 内存的变量

    public static void main(String[] args) {
        // 线程A不断对num值进行访问
        new Thread(()->{
            while(num == 0){

            }
        }).start();

        // 为了保证线程A先启动,进行延迟1s,否则会导致num直接=1
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // main线程在另一线程还在运行时,将num变为1
        num = 1;
        System.out.println(num);    // 输出是否被修改
    }
}

技术图片

2、不保证原子性

原子性:不可分割

比如:当线程A在执行时,不能被打扰,要求操作全部都完成,不能被分割。要么同时完成,要么同时失败;

举例

  • 下面:开了20个线程,每个线程执行了1000次对 num + 1操作。
package com.zxh.testValidate;

public class Demo02 {

    private static int num = 0; // 定义一个变量

    public static void add(){
        num++;  // 对num进行+1操作
    }

    public static void main(String[] args) {

        // 理论情况,num应该变成了20000
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行
        while(Thread.activeCount() > 2){
            Thread.yield(); // 礼让其他线程先执行    
        }
        // 当其他线程执行完add()操作后,再输出结果
        System.out.println("num:" + num);

    }

}

技术图片

 

 但是发现 num 并没有加到 20000,为什么呢?

分析问题的原因

分析线程的执行操作,解答这个问题

1、打开生成的 target 目录下的,这个执行文件所在的位置

技术图片

2、看到有对应生成的class文件,进入命令行,使用命令javap -c Demo02.class反编译查看技术图片

技术图片

 正如我们所见,add方法执行分成了好几步,所有多个线程操作,可能会插队,比如:导致获取到同一个num=1000 值,而最后两个线程修改的结果为同一个num=1001,就会少增加1

volatile可以解决吗

  • 答案是不可以,所以volatile有不保证原子性的特点。

  • 现在我们在变量上加入关键字 volatile

 现在我们在变量上加入关键字 volatile

package com.zxh.testValidate;

public class Demo02 {
    // volatile 不保证原子性
    private volatile static int num = 0; // 定义一个变量

    public static void add(){
        num++;  // 对num进行+1操作
    }

    public static void main(String[] args) {

        // 理论情况,num应该变成了20000
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行
        while(Thread.activeCount() > 2){
            Thread.yield(); // 礼让其他线程先执行
        }
        // 当其他线程执行完add()操作后,再输出结果
        System.out.println("num:" + num);

    }

}

技术图片

 并不能解决

问题解决

到底要如何保证原子性呢?

首先我们通过反编译class文件知道了,add()方法被拆分成了好几步执行,虽然只有一句num++操作。

就因为被分成了几个步骤,所以当一个线程执行add()方法的时候,被打扰,也就不能保证原子性的操作,导致出修改的结果重复。

其他JUC已经提出了解决方法

查看官方文档

  • 我们已经了解了concurrent和locks包下的大部分类和接口了。

  • 只剩下atomic包,其实这个包就是JUC提供的保证变量原子性的包。技术图片 

  • 我们点进去看一下

  • 其实是一些对基本类型封装的类,它们保证了原子

技术图片

代码:使用原子类看一下结果

package com.zxh.testValidate;

import java.util.concurrent.atomic.AtomicInteger;

public class Demo02 {
    // volatile 不保证原子性
    // 使用原子类 保证 原子性
    private volatile static AtomicInteger num = new AtomicInteger(); // 定义一个变量

    public static void add(){
        num.getAndIncrement();  // 对num进行+1操作
    }

    public static void main(String[] args) {

        // 理论情况,num应该变成了20000
        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        // 判断当处理gc和main线程之外,还有其他线程在执行,就让它们先执行
        while(Thread.activeCount() > 2){
            Thread.yield(); // 礼让其他线程先执行
        }
        // 当其他线程执行完add()操作后,再输出结果
        System.out.println("num:" + num);

    }

}

技术图片

成功解决!但是我们需要知道是怎么解决的,底层是怎么运行的?

千万不要以为,这个getAndIncrement()方法只是做了+1操作。

原子类的源码分析

1、进入这个 + 1方法

技术图片

2、发现通过unsafe这个变量进行了 + 1操作

技术图片

3、查看这个变量,发现是一个Unsafe

技术图片

4、点进去这个类,发现很多方法都是native的本地方法

技术图片

所以总结:这些类的底层都直接和操作系统挂钩!直接在内存中修改值!Unsafe类是一个很特殊的存在!

3、指令重排

什么是指令重排?

你写的程序,计算机并不是按照你写的那样去执行的。

它会通过:源代码-->编译器优化的重排-->指令并行也重排-->内存系统也会重排-->执行

举个指令重排的栗子

 1、指令重排不会影响结果

int x = 1;    //1
int y = 2;    //2    
x = x + 5;    //3
y = x * x;    //4

我们所期望的运行顺序:1234
但是可能执行的时候经过指令重排变成 2134 1324的顺序

有没有可能顺序为:4123!不可能
因为处理器进行指令重排的时候,会考虑:数据之间的依赖,比如:第3条依赖第1条

2、指令重排会影响结果

假设 x y a b默认为0

线程A线程B
x=a y=b
b=1 a=2

正常的结果:x = 0,y = 0

但是经过指令重排,因为线程A和线程B里的指令各自没有依赖关系,所以可能变成:

线程A线程B
b=1 a=2
x=a y=b

指令重排导致诡异的结果:x = 2,y = 1

volatile为什么可以避免指令重排?

在内存中会有个内存屏障,阻挡CPU指令。作用:

1、保证特定的操作的执行顺序!

2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)

加上volatile,就是在上下加入了屏障

技术图片

Volatile 是可以保持 可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

 

那个地方会使用volatie?

那就是下面要讲的单例模式,其中DCL懒汉式用到了

 

14、volatile(轻量级的同步机制)

标签:图片   atomic   完整   内存屏障   vol   通过   使用命令   nat   查看   

原文地址:https://www.cnblogs.com/zxhbk/p/13028037.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!