对一个技术人的定义什么,是钢铁直男,还是情商极低,不会说话,或者是专注技术有所产出,充满了认真的魅力,我们说事物总有两面性,其实事物是有多面性,如果不能拥有上帝视角就很难做好一件事,而我们能做的就是尽量去拥有这种视角,全面的看待问题,这就需要很大的知识储备。
从今天开始要写一些并发编程的东西了,并发编程其实也用到过,可是的确很少,工作中大部分时间是crud,都是写大众的东西,只会拿出10%的时间去写类似并发的东西,10%才是精华,现在就开始说说精华之一volatile。
1 volatile的作用和使用场景
volatile 处理线程间变量的可见性的以及防止指令重排,用代码验证一下
/**
* A B两个线程都在跑 他两们对同一个变量做修改 但是由于个线程间变量是不可见的 所以B线程修改了变量不影响
* 导致 这段代码会一直输出你坏 不会输出你好 (不加修饰时)
* 更正下其实也不是不会输出 jvm其实会尽量保证可见性的
* @param args
*/
public static void main(String[] args) {
Optional.empty();
final VT vt = new VT();
Thread Thread01 = new Thread(vt);
Thread Thread02 = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignore) {
}
vt.sign = true;
System.out.println("vt.sign = true 通知 while (!sign) 结束!");
}
});
Thread01.start();
Thread02.start();
}
}
/**
* volatile原理
* 实际上 内存分为一个主内存 然后每增加一个线程 就会开辟一个分内存,两个线程之间的变量都是在各自的分内存上
* 如果不加volatile 那么A工作内存 和B工作内存中 有各自的变量 sign 一个是true 一个是false sign属性的改变不会修改主内存
* 加上volatile 那么会强制主内存进行刷新,过期内存,线程B的内存会获取最新的主内存,进而实现了线程间变量的可见性
*
* 那他是怎么实现这个过程的,通过查看汇编指令发现多了一个lock
* lock指令相当于一个内存屏障,它保证如下三点:
*
* 将本处理器的缓存写入内存。
* 重排序时不能把后面的指令重排序到内存屏障之前的位置。
* 如果是写入动作会导致其他处理器中对应的内存无效。
*
* 加上volatile并不能保证原子性
*/
class VT implements Runnable {
private Logger logger = LoggerFactory.getLogger(Volidate.class);
//验证volatile
public volatile boolean sign = false;
public void run() {
while (true) {
if (sign) {
logger.info("你好");
} else {
logger.info("你坏");
}
}
}
}
使用场景,比如单例模式下
**
* 验证有序性
* volatile 不仅有线程间可见性的功能
* 还有防止指令重排的功能 而sync是没有这个功能的
*/
public class Singleton {
private Singleton() {
}
/**
* 防止指令重排
*/
private volatile static Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
//这个时候 指令重拍就会让这个判断出错
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
2 volatile是怎样实现的指令重排
通过查看汇编指令,发现汇编执行中有一个lock,这个lock最终操作内存屏障,达到了防止指令重排的目的
3 什么时候发生指令重排
举个栗子
instance= new Singleton(),是一个原子操作,其实际上可以抽象为下面几条JVM指令
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的 优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
说到现在,发现其实volatile是为了多线程准备的,但是他就不是为高并发准备的,因为他没有原子性,使用他的场景,一定是不依赖他当前值的场景,或者用他防止指令重排的功能,当然防止指令重排相当于也就防止了jvm的优化有些消耗性能。