JUC并发编程-volatile详解

volatile详解

三大特性

1.保证可见性

可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题

package TestVolatile;

public class TestVolatile {

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"执行");
           
            data.assign();
            System.out.println("a=》"+data.a);
        },"副线程").start();

     	 System.out.println(Thread.currentThread().getName()+"主线程");
        System.out.println(data.a);

    }
}

class Data{

    public int a=0;//没加volatile
    public void assign(){
        a=99;
    }
    
}
//可能的结果:
main主线程
副线程执行
a=99
0
    //就是因为副线程在工作内存修改了a的值但还没来得及同步到主内存,此时主线程已经读取了还没修改的a

2.禁止指令重排

多线程下的指令重排问题(DCL单例模式)

//懒汉式单例
public class LazyMan {
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "ok");
    }

    private  volatile static LazyMan lazyMan; // volatile 为了避免指令重排

    
    //双重检查模式:提升性能,避免线程不必要的等待,如果不为空直接略过,不用等拿到锁之后才判断是否为空
    public static LazyMan getInstance() {
        //避免不必要的等待,因为别的线程已经创建了单例对象,但是你得先去尝试获取锁等待,拿到锁后判断是否创建了单例对象,在锁外面加一层判断可以提前,不为空直接拿走引用对象,提高并发性能
        if (lazyMan == null) {
            //类对象加锁,只有
            synchronized (LazyMan.class) {
                //保证在多线程下只有一个实例对象
                if (lazyMan == null) {
                    lazyMan = new LazyMan();// 不是一个原子性操作
                }
            }
        }
        return lazyMan;
    }

    // 单线程下确实单例ok,但是多线程并发不行
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();

为什么LazyMan要加volatile保证有序性?

reason:

lazyMan = new LazyMan();包含了3个操作

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置lazyMan指向刚分配的内存地址

此时的正常顺序是123,但是如果发生指令重排变成132会发生什么呢?

如果有两个线程A和B,A在执行lazyMan = new LazyMan();时顺序是132

  1. 分配了对象的内存空间
  2. 设置lazyMan指向刚分配的内存地址

但是现在线程B也进来代码块判断lazyMan不为空,拿走了lazyMan对象,但此时lazyMan对象还没有完成初始化,B线程后面的操作就会有问题

3.不保证原子性

volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。

问题1: i++为什么不能保证原子性?

volatile可以保证变量的单次读/写操作的原子性,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为==本质上i++是读、写两次操作==

现在我们就通过下列程序来演示一下这个问题:

public class TestAtomicity {

    volatile private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }

    public static void main(String[] args) throws InterruptedException {
        TestAtomicity test = new TestAtomicity();

        for (int i = 0; i < 10; i++) {
                new Thread(()->{
                    for (int j = 0; j < 1000; j++) {
                        test.add();
                    }
                }).start();
        }

        while(Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println(test.cnt);

    }
}

按正常逻辑代码运行结果应该是10000,但是几次测试结果都小于10000,说明volatile是无法保证原子性的

原因也很简单,i++其实是一个复合操作,包括三步骤:

  • 读取i的值。
  • i+1
  • 将i的值写回内存

结论

volatile是无法保证这三个操作是具有原子性的,我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性。

我们也可以通过java字节码文件证实观点

public class Test {

    public int n;

    public void add(){
        n++;
    }
    public static void main(String[] args) {

    }
}
{

对应的字节码
JUC并发编程-volatile详解_第1张图片

问题2: 共享的long和double变量的为什么要用volatile?

Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。

volatile 的应用场景

使用 volatile 必须具备的条件

  • 对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。
  • 只有在状态真正独立于程序内其他内容时才能使用 volatile。

你可能感兴趣的:(JUC并发编程,java,单例模式,开发语言)