Volatile
Volatile 是Java虚拟机提供的轻量级的同步机制
1、保证可见性
public class JMMDemo {// 在num前添加关键字volatile,保证num在所有线程可见,即修改就被通知private volatile static int num = 0;public static void main(String[] args) throws InterruptedException {// 主线程new Thread(()->{// 线程1while(num == 0){}}).start();TimeUnit.SECONDS.sleep(1);num = 1;System.out.println(num);}
}
2、不保证原子性
原子性:不可分割的操作
线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败
package Volatile;// 验证 volatile不保证原子性
public class vDemo02 {private volatile static int num = 0;public static void add(){num++;}public static void main(String[] args) {for (int i = 1; i <= 20; i++) {new Thread(()->{for (int j = 0; j < 1000; j++) {add();}}).start();}while(Thread.activeCount()>2){Thread.yield();}System.out.println(Thread.currentThread().getName()+" "+num);}
}
以上示例可以说明volatile不保证原子性
我们已经将num声明为volatile,运行结果之后可以发现,num依然小于20000
或许会有笔者忘记了为什么num会小于20000,又为什么小于20000会表明程序没有原子性,我们再来回顾一下,没有忘记的读者可以跳过该部分。
首先,读者需要知道的是,在底层,num++其实并不是一条指令,而是三条指令(感兴趣的小伙伴看图解操作系统),第一步,从内存中读取num的值放到寄存器中,第二步,对寄存器内的值进行+1运算,第三步,将寄存器的值放回到num的地址中。
该实例开启了20条线程。20条线程都在执行同样的操作,即调用add方法1000次,总过加起来有20个1000。而因为线程之间会相互抢占CPU资源,在没有锁(synchronized、lock等)的情况下,会出现以下情况:
此时num=50,线程A进入add方法,将num++;即num=51
但线程A还未写入内存时,便被线程B抢了CPU资源
因为A没有将51写入内存,B看到的num依旧是50,此时他对num++;然后写入内存
然后A又抢到CPU资源,因为A之前的操作停留在num++上,也就是说,它的下一步是将num写入内存(机器很傻,它不会察觉自己手上的num已经发生改变,而是遵循着代码的顺序,执行下一条指令,而num++后的下一条指令为“把num写入内存”)它将自己手上的num,也就是51,又写进内存
因此,A线程与B线程进行了两次num++操作,理应num应该从50变到52,结果只是从50变成51,而在程序中,这种情况可能出现,也可能不会出现,可能在任何时候出现,因此无法预测,每一次运行都是一个全新的结果。
现在理解为何示例不保证原子性了吧,因为它并没有遵循“线程在执行任务的时候不可被打扰,其操作要么全部成功,要么全部失败”,很显然,A线程在执行任务的时候被中断了,num++成功,但写入内存失败。
如何保证线程具有原子性呢?
很简单,使用lock或synchronized锁就好了
但如果不见lock或synchronized锁,怎样保证原子性呢?
使用Atomic**
如:num为integer类型,则使用AtomicInteger
// 验证 volatile不保证原子性
public class vDemo02 {// 不保证原子性//private volatile static int num = 0;// 原子性操作private static AtomicInteger num = new AtomicInteger();public static void add(){// num++;num.getAndIncrement(); // CAS计算机底层并发原理,效率极高!!}public static void main(String[] args) {for (int i = 1; i <= 20; i++) {new Thread(()->{for (int j = 0; j < 1000; j++) {add();}}).start();}while(Thread.activeCount()>2){Thread.yield();}System.out.println(Thread.currentThread().getName()+" "+num);}
}
这些类的底层都是直接与操作系统挂钩,在内存中修改值
3、禁止指令重排
什么是指令重排
你写的程序,计算机并不是按照你写的那样去执行的
源代码=>编译器优化的重排=>指令并行也可能会重排=>内存系统也会重排=>执行
处理器在执行指令重排的时候,会考虑数据之间的依赖性,意图保证程序不出错
但在并发情况下,这种重排可能会产生错误
例如:
a,b,c,d四个值默认都是0
以下是我们开了两个线程,代码顺序为自上往下
线程A | 线程B |
---|---|
x=a | y=b |
b=1 | a=2 |
正常结果:x=0 y=0
但实际上指令重排后可能会产生如下顺序:
线程A | 线程B |
---|---|
b=1 | a=2 |
x=a | y=b |
指令重排导致的结果为:x=2 y=1
volatile可以避免指令重排
volatile可以生成内存屏障,内存屏障为CPU指令,作用:
1、保证特定的操作的执行顺序
2、保证某些变量的内存可见性