volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
好,开始讲大家看不懂的东西了!
volatile有三大特性:
傻了吧,这都是些什么东西啊?别着急,我们一个一个来。
在学习volatile之前,我们先了解一下JMM。什么又是JMM?我只知道JVM。这他妈是啥东西啊?
JMM:java内存模型。jmm是一种抽象的概念,并不真实存在,它描述的是一种规范,通过这种规范定义了程序中的各个变量的访问形式。(仔细读,还是能读懂的)
JMM关于同步的规定(仔细读):
知道看不懂,开始白话文解释!
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锁。
我们写的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;
}
}
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;
查发现type确实为null,好,放行
3. A线程new了一个实例出来,这是把这个最新的实例返回给主内存,主内存的对象变量从Null变为有值
4. A线程完成,B线程被放synchronized开始进行B线程的第二次检查
5. 但由于type5 变量没有volatile修饰,所以线程B不能马上获取到最新的值,它不知道现在对象已经被new出来了,在线程B自己的工作内存了对象依然为null。
6. B线程通过第二次检查,又new了一个对象出来。单例的目标没有达成,上面的代码失败。
所以我们要给变量加上volatile关键字:
private static volatile type5 type;