时下最火的教程:高并发场景Volatile你必须得懂

开课吧IT霈哥2021-02-17 11:56

前言:

应广大读者的需要,今天为给大家带来新一期的干货啦!「面试必问」Java高并发编程

谈谈对 Volatile 的理解

代码

volatile关键字是Java提供的一种轻量级同步机制。

它能够保证可见性有序性

但是不能保证原子性

禁止指令重排

可见性

class MyData {

    int number = 0;

    //volatile int number = 0;

 

    public void setTo60() {

        this.number = 60;

    }

 

}

 

public class VolatileDemo {

    public static void main(String[] args) {

        volatileVisibilityDemo();

    }

 

    //volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改

    private static void volatileVisibilityDemo() {

        System.out.println("可见性测试");

        MyData myData = new MyData();//资源类

        //启动一个线程操作共享数据

        new Thread(() -> {

            System.out.println(Thread.currentThread().getName() + "\t 执行");

            try {

                TimeUnit.SECONDS.sleep(3);

                myData.setTo60();

                System.out.println(Thread.currentThread().getName() + "\t 更新number值: " + myData.number);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }, "ThreadA").start();

        while (myData.number == 0) {

            //main线程持有共享数据的拷贝,一直为0

        }

        System.out.println(Thread.currentThread().getName() + "\t main获取number值: " + myData.number);

    }

}

MyData类是资源类,一开始number变量没有用volatile修饰,所以程序运行的结果是:

可见性测试

ThreadA 执行

ThreadA 更新number值: 60

虽然一个线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。

如果对number添加了volatile修饰,运行结果是:

可见性测试

ThreadA 执行

ThreadA 更新number值: 60

main main获取number值: 60

可见某个线程对number的修改,会立刻反映到主内存上。

原子性

原子性指的是什么意思?

不和分割,完整性,也即某个线程正则做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整,要么同时成功,要么同时失败。

class MyData{

    //int number=0;

    volatile int number=0;

    //此时number前面已经加了volatile,但是不保证原子性

    public void addPlusPlus(){

        number++;

    }

}

public class VolatileDemo {

    public static void main(String[] args) {

        //volatileVisibilityDemo();

        atomicDemo();

    }

    private static void atomicDemo() {

        System.out.println("原子性测试");

        MyData myData=new MyData();

        for (int i = 1; i <= 20; i++) {

            new Thread(()->{

                for (int j = 0; j <1000 ; j++) {

                    myData.addPlusPlus();

                }

            },String.valueOf(i)).start();

        }

        while (Thread.activeCount()>2){

            Thread.yield();

        }

        System.out.println(Thread.currentThread().getName()+"\t int类型最终number值: "+myData.number);

    }

}

volatile并不能保证操作的原子性。这是因为,比如一条number++的操作,会形成3条指令。

javap -c 包名.类名

javap -c MyData

public void addPlusPlus();

  Code:

     0: aload_0

     1: dup

     2: getfield      #2                  // Field number:I  //读

     5: iconst_1 //++常量1

     6: iadd //加操作

     7: putfield      #2                  // Field number:I  //写操作

    10: return  

假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,也立刻通知到了其它线程,但是此时线程1、2已经拿到了number=0,所以结果就是写覆盖,线程1、2将number变成1。

解决的方式就是:

1、对addPlusPlus()方法加锁。

2、使用java.util.concurrent.AtomicInteger类。

代码

class MyData{

    //int number=0;

    volatile int number=0;

    AtomicInteger atomicInteger=new AtomicInteger();

    

    public void setTo60(){

        this.number=60;

    }

    //此时number前面已经加了volatile,但是不保证原子性

    public void addPlusPlus(){

        number++;

    }

    public void addAtomic(){

        atomicInteger.getAndIncrement();

    }

}

public class VolatileDemo {

    public static void main(String[] args) {

        //volatileVisibilityDemo();

        atomicDemo();

    }

    private static void atomicDemo() {

        System.out.println("原子性测试");

        MyData myData=new MyData();

        for (int i = 1; i <= 20; i++) {

            new Thread(()->{

                for (int j = 0; j <1000 ; j++) {

                    myData.addPlusPlus();

                    myData.addAtomic();

                }

            },String.valueOf(i)).start();

        }

        while (Thread.activeCount()>2){

            Thread.yield();

        }

        System.out.println(Thread.currentThread().getName()+"\t int类型最终number值: "+myData.number);

        System.out.println(Thread.currentThread().getName()+"\t AtomicInteger类型最终number值: "+myData.atomicInteger);

    }

}

结果:可见,由于volatile不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而AtomicInteger可以保证原子性。

原子性测试

main int类型最终number值: 17751

main AtomicInteger类型最终number值: 20000

有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种:

代码

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致;处理器在进行重排序时必须要考虑指令之间的数据依赖性;多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

volatile可以保证有序性,也就是防止指令重排序

所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空。

int x = 11; //语句1

int y = 12; //语句2

x = x + 5;  //语句3

y = y * x;  //语句4

以上例子,可能出现的执行顺序有1234、2134,这二个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。

观看下面代码,在多线程场景下,说出最终值a的结果是多少?

我们采用 volatile 可实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

public class ResortSeqDemo {

    int a=0;

    boolean flag=false;

    /*

    多线程下flag=true可能先执行,还没走到a=1就被挂起。

    其它线程进入method02的判断,修改a的值=5,而不是6。

     */

    public void method01(){

        a=1;

        flag=true;

    }

    public void method02(){

        if (flag){

            a+=5;

            System.out.println("*****最终值a: "+a);

        }

    }

    public static void main(String[] args) {

        ResortSeqDemo resortSeq = new ResortSeqDemo();

        new Thread(()->{resortSeq.method01();},"ThreadA").start();

        new Thread(()->{resortSeq.method02();},"ThreadB").start();

    }

}

为什么volatile 可实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象?说说它的原理

我们先来了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,volatile底层就是用CPU的内存屏障(Memory Barrier)指令来实现的,它有两个作用

一个是保证特定操作的顺序性

二是保证变量的可见性。

代码

代码

由于编译器和处理器都能够执行指令重排优化。所以,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障可以禁止在内存屏障前后的指令进行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读到这些数据的最新版本。

哪些地方用到过volatile?

单例模式的安全问题

传统

public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo() {

        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造方法执行了");

    }

    public static SingletonDemo getInstance(){

        if (instance == null) {

            instance = new SingletonDemo();

        }

        return instance;

    }

    public static void main(String[] args) {

        //main线程操作

        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());

    }

}

代码

改为多线程操作测试

public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo() {

        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造方法执行了");

    }

    public static SingletonDemo getInstance(){

        if (instance == null) {

            instance = new SingletonDemo();

        }

        return instance;

    }

    public static void main(String[] args) {

        //多线程操作

        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                SingletonDemo.getInstance();

            },Thread.currentThread().getName()).start();

        }

    }

}

代码

调整后,采用常见的DCL(Double Check Lock)双端检查模式加了同步,但是在多线程下依然会有线程安全问题。

public class SingletonDemo {

    private static SingletonDemo instance = null;

    private SingletonDemo() {

        System.out.println(Thread.currentThread().getName() +"\t SingletonDemo构造方法执行了");

    }

    public static SingletonDemo getInstance(){

        if (instance == null) {

            synchronized (SingletonDemo.class){

                if (instance == null) {

                    instance = new SingletonDemo();

                }

            }

        }

        return instance;

    }

    public static void main(String[] args) {

        //多线程操作

        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                SingletonDemo.getInstance();

            },Thread.currentThread().getName()).start();

        }

    }

}

代码

这个漏洞比较tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();可以大致分为三步

instance = new SingletonDemo();

public static thread.SingletonDemo getInstance();

  Code:

     0: getstatic     #11 // Field instance:Lthread/SingletonDemo;

     3: ifnonnull     37

     6: ldc           #12       // class thread/SingletonDemo

     8: dup

     9: astore_0

    10: monitorenter

    11: getstatic     #11       // Field instance:Lthread/SingletonDemo;

    14: ifnonnull     27

    17: new           #12       // class thread/SingletonDemo 步骤1

    20: dup

    21: invokespecial #13       // Method "<init>":()V 步骤2

    24: putstatic     #11       // Field instance:Lthread/SingletonDemo;步骤3

底层Java Native Interface中的C语言代码内容,开辟空间的步骤

memory = allocate();    //步骤1.分配对象内存空间

instance(memory); //步骤2.初始化对象

instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null

剖析:

在多线程的环境下,由于有指令重排序的存在,DCL(双端检锁)机制不一定线程安全,我们可以加入volatile可以禁止指令重排。

原因在与某一个线程执行到第一次检测,读取到的instance不为null时,==instance的引用对象可能没有完成初始化。==

memory = allocate();     //步骤1. 分配对象内存空间

instance(memory); //步骤2.初始化对象

instance = memory;   //步骤3.设置instance指向刚分配的内存地址,此时instance != null

==步骤2和步骤3不存在数据依赖关系==,而且无论重排前还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。

memory = allocate();     //步骤1. 分配对象内存空间

instance = memory; //步骤3.设置instance指向刚分配的内存地址,此时instance != null,但是对象还没有初始化完成!

instance(memory);   //步骤2.初始化对象

但是指令重排只会保证串行语义的执行一致性(单线程),并不关心多线程的语义一致性。==所以,当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。==

public static SingletonDemo getInstance(){

    if (instance == null) {

        synchronized (SingletonDemo.class){

            if (instance == null) {

                instance = new SingletonDemo(); //多线程情况下,可能发生指令重排

            }

        }

    }

    return instance;

}

如果发生指定重排,那么,

1、此时内存已经分配,那么instance=memory不为null。

2、碰巧,若遇到线程此时挂起,那么instance(memory)还未执行,对象还未初始化。

3、导致了 instance!=null,所以两次判断都跳过,最后返回的instance`没有任何内容,还没初始化。

解决的方法就是对SingletonDemo对象instance添加上volatile关键字,禁止指令重排。

private static volatile SingletonDemo instance=null;

以上就是为大家准备的Java高并发编程,希望对你们的技术提升有所帮助,新年的假期已经慢慢接近尾声,新一年的工作马上就要开始了。很多人,在这个时候内心都在躁动,想着新年开始,有个新的开始,那么今天除了为大家准备了以上的教程,还有Java面试题免费领取。

面试

原价9980的课程,今天免费送给大家,点击图片免费领取,希望对您想要换个新工作的心,有所帮助。

上一篇:解惑jLambda表达式、函数式接口,Stream流、方法引用下一篇:Java数组简介:数组是什么?

最新文章

文章图0

Java多线程与JUC—死锁的出现与解决(二)

.固定锁顺序避免死锁(针对锁顺序死锁) 上面transferMoney()发生死锁的原因是因为加锁顺序不一致而出现的~ 如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题!

2021-02-26 10:20:15

文章图1

Java多线程与JUC—死锁的出现与解决(一)

在Java中使用多线程,就会有可能导致死锁问题。死锁会让程序一直卡住,程序不再往下执行。 我们只能通过中止并重启的方式来让程序重新执行。 这是我们非常不愿意看到的一种现象,我们要尽可能避免死锁的情况发生!

2021-02-26 10:11:15

文章图2

Java教程:MySQL高可用方案对比(三)

MySQLcluster是官方集群的部署方案,通过使用NDB存储引擎实时备份冗余数据,实现数据库的高可用性和数据一致性。

2021-02-26 09:52:06

文章图3

Java面试题:dispatchServlet怎样分发任务的?

用户发请求-->DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制。

2021-02-25 18:03:02

文章图4

Java教程:MySQL高可用方案对比(二)

由于半同步复制,存在接收到一个从机的成功应答即认为半同步复制成功的特性,所以多从半同步复制的可靠性要优于单从半同步复制的可靠性。并且多节点同时宕机的几率也要小于单节点宕机的几率,所以多节点架构在一定程度上可以认为高可用性是好于双节点架构。

2021-02-25 16:52:40