前言:
要真正搞懂volatile的特性需要与JMM对比来看
JMM(线程安全的保证)
JMM:JAVA内存模型(java memory model) 是一种抽象概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定
- 线程解锁前,必须把共享变量的值刷新回主内存;
- 线程加锁前,必须读取主内存的最新值到自己的工作内存;
- 枷锁解锁是同一把锁。
由于JVM运行程序的实体是线程。而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域。
JMM中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读写)必须在工作内存中进行。具体步骤:首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存存储着主内存中的变量副本拷贝,因此不同线程间无法访问对方的工作内存,线程的通信必须通过主内存来完成
如图所示 可以说cache缓存 就是 这种JMM内存模型的硬件抽象
JMM的特性:可见性,原子性,有序性。
说回到volatile
volatile
volatile 是java虚拟机提供的轻量级同步机制
导致并发问题的源头是 : 多核 CPU 缓存导致程序的可见性问题、多线程间切换带来的原子性问题以及编译优化带来的顺序性问题。
下面是三个volatile特性
可见性
用代码证明volatile的可见性
class MyDate {
//共享变量 1.1 首先不加volatile关键字
int number = 0;
public void change() {
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyDate myDate = new MyDate();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myDate.change();
System.out.println(Thread.currentThread().getName() + "\t update number value=" + myDate.number);
}, "A线程").start();
//main线程读到的数据
while (myDate.number == 0) {
//main 线程一直循环等待直到number值不等于0
}
System.out.println(Thread.currentThread().getName() + "\t over number value="+myDate.number);
}
}
首先new了一个名字叫A线程的线程。先让线程sleep三秒钟,此时A线程和main线程都拿到了number的值。然后A线程修改了number的值为60。但是main线程因为没有可见性不知道值发生了修改。还是继续循环等待,证明了此时线程之间没有可见性。
1.2 证明volatile可见性 ,我们在 int number前面加上volatile关键字
volatile int number = 0;
此时main线程由可见性及时知道了主内存中的number值发生了修改,跳出了循环。
不保证原子性
代码证明不保证原子性
class MyDate {
volatile int number = 0;
public void add() {
number++;
}
}
//2.1证明原子性
//原子性是指 完整性,不可分割,就是说某个线程执行时不可分割或被插队。
public class VolatileDemo {
public static void main(String[] args) {
MyDate myDate = new MyDate();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myDate.add();
}
}, String.valueOf(i)).start();
}
// 等待线程计算完成后 由main线程取最终结果 10*1000 = 1万
while (Thread.activeCount()>2){ //mian线程和GC后台线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t sum value="+myDate.number);
}
代码for出10个线程,每个线程做1000次add操作。正常来说10个线程操作完成之后number应该等于10000
但是程序没次运行都会得到不同的结果,而且小于10000.
是由于number++这个操作在java中是线程不安全的。
n++这个操作在字节码中分为了三个步骤
getfield ---- 拿到原始的值n
iadd -------- 进行+1操作
putfield -----把累加后的值写回
当在多线程环境中,如果多个线程都拿到了原始的n,运算完成后在写回数值之前线程被挂起或阻塞,线程恢复后来不及收到可见性通知就向主内存写下了数值,就会发生写覆盖的情况,丢失数据。
导致了在本程序中无法达到10000.
如何解决呢
public synchronized void add() {
number++;
}
首先可能想到的就是synchronized同步方法,但是synchronized太重了,一个方法里面就一个++操作如果用synchronized就好比杀鸡用牛刀,大炮打蚊子。需要功能和性能的同时考虑。
使用java.util.concurrent.atomic下的AtomicInteger类
原子性的增加1 相当于++操作
或者getAndAdd(int delta) 参数填1
//2.3 原子性int
AtomicInteger atomicInteger = new AtomicInteger();
// 原子性方法
public void addWithAtomic(){
atomicInteger.getAndIncrement();
}
至于为什么AtomicInteger能保证原子性,后面有机会继续详解。
禁止指令重排
指令重排:计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为编译器优化重排,指令并行的重排,内存系统的重排。
在多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的共享变量的一致性时无法保证的。所以结果无法预测。
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //语句1
flag = true; //语句2
}
public void reader() {
if (flag) { //语句3
a = a + 1; //语句4
System.out.println("value:" + a);
}
}
volatile 实现了禁止指令重排序优化,从而避免了多线程环境下程序出现乱序执行的现象
实现:在对volatile变量进行写操作时,会在写操作后面加入一条Memory Barriier(内存屏障)告诉内存和CPU,禁止在内存屏障前后的执行指令重排优化
写
读
volatile 使用场景
DCL(双重检查锁)+ volatile的单例模式
public class Singleton {
private static Singleton instance = null;
// + volatile
private Singleton() {
System.out.println(Thread.currentThread().getName() + "\t Singleton的构造方法");
}
//DCL Double check lock
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
如果不加volatile
由于
instance = new Singleton();
要分为3步骤执行
1.分配对象内存空间
2.初始化对象
3.完成对象的引用
步骤2和步骤3不存在数据依赖关系,所以可能发生重排序,1->3->2 此时的引用对象可能还没有完成初始化
当一条线程访问instance不为null时,由于instance实例还没有初始化完成,就造成了线程安全问题。