volatile是JVM提供的轻量级同步机制
好,开始讲大家看不懂的东西了!
volatile有三大特性:
傻了吧,这他妈都是些什么jb东西啊?别着急,我们一个一个来。
在学习volatile之前,我们先了解一下JMM。什么又是JMM?我只知道JVM。这他妈是啥东西啊?
JMM:java内存模型。jmm是一种抽象的概念,并不真实存在,它描述的是一种规范,通过这种规范定义了程序中的各个变量的访问形式。(仔细读,还是能读懂的)
JMM关于同步的规定(仔细读):
1.线程解锁前,必须把共享变量的值刷新回主内存
2.线程加锁钱,必须读取主内存的最新值到自己的工作内存
3.加锁解锁是同一把锁
知道看不懂,开始白话文解释!
JVM我们的java虚拟机运行程序的时候,是以线程为最小刻度的。而每个线程创建的时候,jvm就会为这个线程创建一个工作内存,该工作内存是私有的,只能被当前线程所访问。
而JMM内存模型中规定:所有的变量都储存在主内存中,所有线程都能访问,但线程对变量的任何操作(读取赋值等)都必须在工作内存中进行,首先要将主内存中的变量拷贝到自己的工作内存中,然后才能对变量进行操作,操作完成后再讲变量写会主内存中。
这里我们发现了一个问题:
先试想这样一个场景:现在有一个商品只剩下最后一个,如果两个线程同时进来抢,拿到了一个变量:int a = 1;(商品的数量) 这时候这个int a = 1;会拷贝出两份,分别存在于线程1的工作内存和线程2的工作内存。 我们知道,不同线程间是无法访问对方的工作内存的。
这个时候线程1 跑得快一点抢到了最后一个商品,把int a 的值-1了,然后通知快递部门上门来取货,把这最后一个商品拿走发货,然后把最新的a的值返回给主内存。现在主内存int a 的值等于0。
但对于线程2来说,它现在只看自己的工作内存,不看主内存,对于线程2来说,int a 的值现在还是1。所以它就觉得它也抢到了商品,其实这时主内存中的int a已经是0了,已经没有商品了。这时线程2把自己工作内存的int a 的值-1,然后通知快递部门来取货,快递来了发现你他妈的商品都卖完了我来取个啥?
上面就出现了超卖的情况,其根本原因就是:多个线程之间不能知道对方的对共享变量的执行情况,大家都是盯着自己的东西在做事。就像两个施工队在山的两边一起往中间打隧道,互相不知道对方的情况,最后两个隧道在山的中间完美错过。
好!那么有没有一个办法,只要有一个线程修改了主内存的变量的值以后,其他的线程能马上知道并获取到最新的值呢?
先看看没有使用volatile关键字的情况:
1.编写一个类,模拟售卖商品的过程,商品数量我们初始化为 Int a = 1;
class Shop{ int a = 1; public void saleOne(){ this.a = a-1; } }
2.测试类
public static void main(String[] args) { Shop shop = new Shop(); new Thread(()->{ System.out.println("线程A初始化"); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } shop.saleOne(); System.out.println("线程A购买商品完成,剩余商品量:"+shop.a); },"线程A").start(); while (shop.a == 1){ } System.out.println("主线程,剩余商品量:"+shop.a); }
这里有两个线程,线程A和主线程。 程序启动的时候:
我们加上volatile关键字
class Shop{ volatile int a = 1; public void saleOne(){ this.a =a-1; } }
测试代码不变
结果:
原子性什么意思呢?
也就是完整性,比如一个线程在做一件事的时候,期间不能被加塞或分割,需要整体完整,要么同时成功要么同时失败。
大白话翻译:同一个方法,在一个线程没有执行完之前,其他线程必须给我等着。等我执行完了再放第二个线程进来。以免线程1的操作被线程2给覆盖了。比如synchronized,就保证了原子性。
给我们的Shop类创建一个增加商品库存的方法(每调一次addGoods方法,Int a就+1):
class Shop{ volatile int a = 1; public void addGoods(){ a++; } public void saleOne(){ this.a =a-1; } }
此时int a商品数量是加了volatile 修饰的,保证了不同线程之间的可见性!
测试:
public static void main(String[] args) { Shop shop = new Shop(); for(int i = 0; i < 20;i++){ new Thread(()->{ shop.addGoods(); }).start(); } //保证所有20个线程都跑完,只剩下2个线程(主线程和GC线程)的时候代码才继续往下走 //其中 Thread.yield() 方法表示主线程不执行,让给其他线程执行 while (Thread.activeCount() >2){ Thread.yield(); } System.out.println("如果保证了原子性,应该的结果是本来的1+20 = 21,但实际的值:"+shop.a); }
结果让我们大失所望,每次执行程序得到的结果都不一样
这里我们知道,volatile不能保证程序的原子性。那为什么呢?
首先明确一点 a++操作不是原子性,它有三步:
尚且a++都不是原子操作,那我们平时的业务代码是不是更长,花的时间也更多?被其他线程覆盖的机会是不是也更大?
好,现在我们来看看上面的20个线程的例子怎么来分析!
以上!就是整个代码运行流程,解释了volatile为什么不能保证原子性。我知道很多同学还是没看懂,别急,文章最后会有更直观的例子(单例模式中的线程安全问题),一看就明白了
现在我们想一想,怎么解决volatile这个缺点呢?怎么实现原子性?
我们讲第二种:
修改我们的Shop类
class Shop{ AtomicInteger atomicInteger = new AtomicInteger(1); public void addGoodsByAtomic(){ atomicInteger.getAndIncrement(); }
测试:
public static void main(String[] args) { Shop shop = new Shop(); for(int i = 0; i < 20;i++){ new Thread(()->{ shop.addGoodsByAtomic(); }).start(); } while (Thread.activeCount() >2){ Thread.yield(); } System.out.println("如果保证了原子性,应该的结果是本来的1+20 = 21,但实际的值:"+shop.atomicInteger); }
结果正确:
为什么原子类保证了原子性?这个设计到CAS锁。看我关于CAS的博客就懂了哈!
这是什么鸡巴东西?
我们写的java代码,为了提高性能,在编译器和处理器中往往会进行指令重排,例如我写的某一行代码在23行,当经过编译过后这行代码在150行。
多线程环境中,由于编译器重排的原因,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
简单来说volatile避免了指令重排,也就避免了多线程中可能产生的问题。
单例模式:
public class type3 { private static type3 type; private type3(){} private static type3 getInstance(){ if(type == null){ type = new type3(); } return type; } }
上面是一个线程不安全的单例模式,我们可以加上一个synchronized :
public class type4 { private static type4 type; private type4(){} private static synchronized type4 getInstance(){ if(type == null){ type = new type4(); } return type; } }
但synchronized把整个方法都锁了,在高并发的情况下,太重了。并发性下降了,吞吐量下降了。
所以出现了效率最高,也安全的单例模式写法:双重检查!
public class type5 { private static type5 type; private type5(){} private static type5 getInstance(){ if(type == null){ synchronized(type5.class){ if(type == null){ type = new type5(); } } } return type; } }
大家觉得上面的代码有没有什么问题?
我来梳理一下。
所以我们要给变量加上volatile关键字:
private static volatile type5 type;
大家看完了点个赞,码字不容易啊。。。