1.概念

  • volatile是java虚拟机提供的轻量级的同步机制,有三大特性:可见性,不保证原子性,禁止指令重排。

1.可见性

在了解可见性之前,你首先要知道JMM(java内存模型)。

JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。

在这里插入图片描述

可见性是指:在并发编程中,当A线程修改了工作内存中的值并写回主内存时,及时通知其他线程,重新去主内存去取最新的值。

package com.sk.Multithreading;

class Data {

    int temp = 0;

    void add() {
        this.temp = 1;
    }
}

public class Test {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {//修改temp的线程
            System.out.println(Thread.currentThread().getName()+"\t 准备修改");
            data.add();
            System.out.println(Thread.currentThread().getName()+"\t 修改完成");
            System.out.println(data.temp);
        },"玉无双").start();

        //第二个线程是main线程
        while (data.temp==0){
            //一直循环,直到temp的值变为1
        }
        System.out.println(Thread.currentThread().getName()+"\t 程序结束");
    }

}

在这里插入图片描述
    看运行结果,我们明显看出temp的值被修改,但是程序并没有结束,main线程中的值还是0,程序还在循环里,并没有结束。
    当我们加上volatile关键字,接下来就是见证奇迹的时刻!

package com.sk.Multithreading;

class Data {

    volatile int temp = 0;

    void add() {
        this.temp = 1;
    }
}

public class Test {

    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {//修改temp的线程
            System.out.println(Thread.currentThread().getName()+"\t 准备修改");
            data.add();
            System.out.println(Thread.currentThread().getName()+"\t 修改完成");
            System.out.println(data.temp);
        },"玉无双").start();

        //第二个线程是main线程
        while (data.temp==0){
            //一直循环,直到temp的值变为1
        }
        System.out.println(Thread.currentThread().getName()+"\t 程序结束");
    }

}

在这里插入图片描述
在这里插入图片描述
这里明显看出,无双线程修改temp值完成后,main线程能拿到修改后temp的值,这就是可见性!

2.不保证原子性

2.1 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

2.2先看例子

package com.sk.Multithreading;

class Data {
    volatile int num = 0;

    void add() {
        num++;
    }
}

public class Multithreading {
    public static void main(String[] args) {
        Data data = new Data();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    data.add();
                }
            }, "thread" + i).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(data.num);
    }
}

计算结果(可以将循环次数增加效果更明显)
在这里插入图片描述

2.3原因
在num++,这个自增操作,可以分成三步,获取num的值,加1,再赋值给num,在多线程情况下,a线程先取到num值为0,做自增操作,num值变为1,正要写回主内存时被挂起;b线程也取到num值为0(此时a线程还未将值更新回主物理内存),b线程也做自增操作,b工作内存中的num值变为1,b线程将值更新回主内存,突然啪的一下啊很快,a线程被唤醒,a线程没来得及接收到主内存的值已经更新,就将自己工作内存的值写回主内存,导致了写覆盖。

2.4解决原子性问题
1.加synchronized(不建议)

package com.sk.Multithreading;

class Data {
    volatile int num = 0;

    void add() {
        num++;
    }
}

public class Multithreading {
    public static void main(String[] args) {
        Data data = new Data();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    synchronized (data) {
                        data.add();
                    }
                }
            }, "thread" + i).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        System.out.println(data.num);
    }
}

2.原子类(AtomicInteger),效率高于synchronized

package com.sk.Multithreading;

import java.util.concurrent.atomic.AtomicInteger;

class Data {
    volatile AtomicInteger num = new AtomicInteger();

    void add() {
        num.getAndIncrement();
    }
}

public class Multithreading {
    public static void main(String[] args) {
        Data data = new Data();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 2000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    data.add();
                }
            }, "thread" + i).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        System.out.println(data.num);
    }
}

3.有序性(禁止指令重排)

3.1什么是指令重排?
    指令重排是指编译器和处理器在不影响代码单线程执行结果的前提下,对源代码的指令进行重新排序执行。这种重排序执行是一种优化手段,目的是为了处理器内部的运算单元能尽量被充分利用,提升程序的整体运行效率。
  例子

package com.sk.Multithreading;

public class test {
    int x = 0;//1
    int y = 1;//2
    int s = x + y;//3
}

多线程的情况下执行的顺序可能是2,1,3

3.2怎么保证有序性
    volatile的底层是使用内存屏障来保证有序性的。写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。

    int x = 0;//1
    int y = 1;//2
    volatile int s = 3;//3
    int k = 4;//4

4不会在3前面,1,2也不会在3后面执行(1和2顺序不保证)

    volatile变量进行写操作时,写操作加入store屏障指令,将共享变量写回主内存;读操作时,会在读操作之加一个load屏障指令,从主内存中读取共享变量的值。
    观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
    lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),内存屏障会提供3个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

2)它会强制将对缓存的修改操作立即写入主存;

3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

5. 实际应用
    在双重检查锁(double-checked locking)(在并发场景下)存在延迟初始化的优化
问题隐患(可参考 The “Double-Checked Locking is Broken” Declaration)

public class LazyInitDemo {
        private volatile Helper helper = null;

        public Helper getHelper() {
            if (helper == null) {
                synchronized (this) {
                    if (helper == null) {
                        helper = new Helper();1.分配内存空间 2.初始化对象 3.将对象指向刚分配的内存空间   第二步与第三步极小情况下会发生指令重排 导致返回null
                    }
                }
            }
            return helper;
        }

    }

6. 解决多线程下long和double赋值问题(java对long和double的赋值操作是非原子操作)

使用volatile修饰后的long和double类型的读写操作是原子性的

注:对其引用类型(Long/Double)的读写操作总是原子的,尽管他们的实现可能被分为两次32-bit或者一个64-bit

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐