volatile关键字剖析

183 阅读4分钟

JMM(Java内存模型)

  • JMM是指Java内存模型不是Java内存布局,不是所谓的栈、堆、方法区。
  • 每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。
  • JMM可能带来可见性原子性有序性问题。所谓可见性,就是某个线程对主内存内容的更改,应该立刻通知到其它线程。原子性是指一个操作是不可分割的,不能执行到一半,就不执行了。所谓有序性,就是指令是有序的,不会被重排。

volatile关键字

  • volatile关键字是Java提供的一种轻量级同步机制。它能够保证可见性和有序性,但是不能保证原子性
  • 可见性测试Demo
public class Volatile可见性测试 {
    //线程操纵资源类
    static class MyData {
        //普通变量
        int number = 0;
        //原子包装类
        AtomicInteger atomicInteger = new AtomicInteger();
        public void setTo60() {
            this.number = 60;
        }
        public void addPlusPlus() {
            this.number++;
        }
        public void addAtomic() {
            atomicInteger.getAndIncrement();
        }
    }

    public static void main(String[] args) {
        System.out.println("可见性测试");
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                TimeUnit.SECONDS.sleep(3);
                myData.setTo60();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t更新后的值是:" + myData.number);
        }, "t1").start();
        int i = 0;
        while (myData.number == 0) {
           
        }
        System.out.println(Thread.currentThread().getName() + "\t main线程获得的number值" + myData.number);
    }
}
  • 测试结果:没有加volatile之前main线程无法立即获得更新后的number值会在while循环阻塞住。加了volatile之后的是第二张图。

原子性

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

getfield        //读
iconst_1	//++常量1
iadd		//加操作
putfield	//写操作

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

解决的方式就是:

  • 1.对addPlusPlus()方法加锁。
  • 2.使用java.util.concurrent.AtomicInteger类。
public class Volatile原子性测试 {
    //线程操纵资源类
    static class MyData {
        //普通变量
        volatile int number = 0;
        //原子包装类
        AtomicInteger atomicInteger = new AtomicInteger();
        public void setTo60() {
            this.number = 60;
        }
        public void addPlusPlus() {
            this.number++;
        }
        public void addAtomic() {
            atomicInteger.getAndIncrement();
        }
    }

    public static void main(String[] args) {
        System.out.println("原子性测试");
        MyData myData = new MyData();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 200; j++) {
                    myData.addPlusPlus();
                    myData.addAtomic();
                }
            }, String.valueOf(i)).start();
        }
        //有两个线程,1.GC线程 2.main线程
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("addPlusPlus:" + myData.number);
        System.out.println("addAtomic:" + myData.atomicInteger);
    }
}

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

有序性

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

int x = 11; //语句1
int y = 12; //语句2
x = x + 5;  //语句3
y = x * x;  //语句4

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

volatile底层是用CPU的内存屏障(MemoryBarrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性是保证变量的可见性在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。

哪些地方用到过volatile?

  • 1.单例模式的安全问题

常见的DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。

public class DCL单例模式 {
    static class SingleTon {
        private static volatile SingleTon instance = null;
        private SingleTon() {
            System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
        }
        public static SingleTon getInstance() {
            if (instance == null) {
                synchronized (SingleTon.class) {
                    if (instance == null) {
                        instance = new SingleTon();
                    }
                }
            }
            return instance;
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
        	new Thread(SingleTon::getInstance, String.valueOf(i+1)).start();
        }
    }
}

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

memory = allocate();     //1.分配内存
instance(memory);	 //2.初始化对象
instance = memory;	 //3.设置引用地址

其中2、3没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配那么instance=memory不为null。如果此时线程挂起,instance(memory)还未执行,对象还未初始化。由于instance!=null,所以两次判断都跳过,最后返回的instance没有任何内容,还没初始化。

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