volatile是什么?
volatile是JVM提供的轻量级的同步机制。
- 保证可见性;
- 不保证原子性;
- 禁止指令重排;
JMM
(Java内存模型)
要求是:原子性,有序性,可见性。
本身是不存在的,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
可见性
JMM关于同步的规定:
- 线程解锁前,必须将共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后,再将变量写回主内存。不能直接操作主内存中的变量。各个线程中的工作内存中存储着主内存中的变量副本拷贝。因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。其简要的过程如下图:
如下示例:说明线程的可见性
在公共区放置对象。他的属性age=25。所有线程先拿到25,然后在自己的线程工作空间中对数据进行操作,在返回给主内存。
主物理内存发生修改,则迅速通知其他线程,这就叫做可见性。(主内存就是插在电脑上的内存)
代码展示
验证volatile的可见性
class MyData{
int number = 0;
public void add{
this.number+=60;
}
public static void main (String[] args){
public void seeOkVolatile(){
MyData mydata = new MyData();
new Thread(()->{
system.out.println(Thread.currentThread().getName()+"come in");
try{
TimeUntil.SECOND.sleep(3);
}catch(Exception e){
e.printStackTrace();
}
mydata.add();
system.out.println(Thread.currentThread().getName()+"come out");
},
"AAA").start();
while(mydata.number==0){
system.out.println("main 阻塞");
}
}
}
改变之后(给变量添了volatile关键字)
class MyData{
volatile int number = 0;
public void add{
this.number+=60;
}
public static void main (String[] args){
public void seeOkVolatile(){
MyData mydata = new MyData();
new Thread(()->{
system.out.println(Thread.currentThread().getName()+"come in");
try{
TimeUntil.SECOND.sleep(3);
}catch(Exception e){
e.printStackTrace();
}
mydata.add();
system.out.println(Thread.currentThread().getName()+"come out");
},"AAA").start();
while(mydata.number==0){
system.out.println("main 阻塞");
}
}
}
在加了volatile关键字之后,对于每一个线程对共享变量的修改都会通知主内存的, 然后,这样就体现了可见性。
volatile不保证原子性
原子性是什么?
不可分割,中间不能被加塞或者分割。要么同时成功,要么同时失败。
如下表示有20个线程,
for(int i = 0;i<20;i++){
new Thread(()->{
},String.valueOf(i)+"name").start();
本应输出number是20000,但是每次都不会达到这个值,所以不保证原子性。
这是加了Synchronize来解决++的多线程不安全问题。
理论上是可以用synchronize的额,但是不值得。
不保证原子性的理论知识
线程调度太快了,后面的额线程会把前面的线程的写回给主内存的值覆盖掉。所以不保证原子性。
解决不保证原子性
- 加synchronize,
- 使用JUC下的Atomic类
运行结果对比:
有序性
但是在volatile关键字修饰下是:禁止指令重排序
案例
在执行的时候通过指令重排,flag的执行在a前面。执行完后,未来的及执行a,直接走method02,a的值是从0加5的,而不是从1开始加5的。导致了不确定的结果,这是我们不期望的。
禁止指令重排小结
禁止指令重排,从而避免了多线程环境下程序出现乱序执行现象。
你在哪些地方用到过volatile?(经典面试)
单例模式中
在没加volatile,synchronized之前的单例模式:
public class SingleDemo{
private static SingleDemo instance=null;
private void SingleDemo(){
system.out.println("构造器");
}
public static SingleDemo getInstance(){
if(instance==null){
instance = new SingleDemo();
}
return instance;
}
public static void main(String[] args){
for(int i = 0;i<10;i++){
new Thread(()->{
SingleDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
运行如下:
这种问题可以通过在getInstance函数前面加synchronized 解决,但是非常不推荐。
利用双端检锁解决:
在进去之前判断一次,进去后上锁,出来时再判断一次。但是,仍然是不安全的。因为底层有指令重排。
public class SingleDemo{
private static SingleDemo instance=null;
private void SingleDemo(){
system.out.println("构造器");
}
//双端检锁机制
public static SingleDemo getInstance(){
if(instance==null){
synchronized(SingleDemo.class){
instance = new SingleDemo();
}
}
return instance;
}
public static void main(String[] args){
for(int i = 0;i<10;i++){
new Thread(()->{
SingleDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
所以当instance不为null时,由于instance为初始化完成,这样也会造成多线程安全问题。
我们在申明的属性前加上volatile关键字,就可以解决。