volatile是jvm提供的轻量级的同步机制
-
保证可见性(一个线程的修改对其它线程是可见的)
-
不保证原子性
-
禁止指令重排序
什么是指令重排?
计算机在执行程序时,为了提高性能,编译器和处理器会对指令做重排,过程如下
源代码--》编译器优化的重排--》指令并行的重排--》内存系统的重排--》最终执行的指令
处理器在进行重排时必须考虑数据之间的依赖性
单线程环境下重拍后执行的结果与代码顺序执行的结果一致
多线程环境下线程交替进行,由于编译器优化重排的存在,两个线程使用的变量能否保证一致性无法确定/** * 如果有两个线程 * 一个执行fun1,一个执行fun2 * 1. 当fun1中语句一和语句二执行时发生指令重排,语句二在语句一之前执行 * 那么语句二执行完后,没等语句一执行,fun2中执行,a=5 * 2. 语句一先执行时,最后a=6 * 所以多线程环境下,由于编译器的优化重排,结果不确定 * <p> * volatile会禁止指令重排,让执行顺序按照代码编写的顺序执行 */ class ResortedDemo { int a; boolean flag; public void fun1() { a = 1;//语句一 flag = true;//语句二 } public void fun2() { if (flag) { a = a + 5; System.out.println(a); } } }
JMM(java memory model)java内存模型
jmm本身是一种抽象的概念,并不真实存在,它描述的是一组规范,通过这组字段定义了程序中各个变量(包括程序字段,静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 线程解锁前,必须把共享变量的值刷新回主内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程, 每个线程创建时JVM都会为其分配一个工作内存
线程对变量的操作必须在工作内存进行,首先将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,然后再将变量写回到主内存 ,
不能直接操作主内存的变量,线程之间不能访问其它线程的工作内存,因此线程间的通信(传值)必须通过主内存完成
volatile代码实例
/**
* # 验证volatile的可见性(每次读取都直接从主内存读取)
* 假设number=0;
* 1. number前面没有volatile,没有可见性
* 就算第一个线程更改了number的值,并写回了主内存,但由于没有可见性
* main线程并不知道已经修改,所以main线程中number一直等于0,一直在while循环中
* 2. number前面加上volatile,保证可见性
* 第一个线程更改完值并写回主内存后,由于number的可见性,
* main线程中立刻就能读取到主内存中number的修改,跳出while循环
* # 验证volatile不保证原子性
* 1. 原子性是什么意思
* 不可分割,完整性,也就是某个线程正在做某个具体业务时中间不可以被加塞或者被分割,
* 其它线程不能对该线程获取的资源进行任何操作(包括读取)
* 要么同时成功,要么同时失败
* 2.如何验证:
* 创建20个线程,每个线程对number做1000次加一操作,若最后number=20*1000,则有原子性,反之则没有
* <p>
* 经运行程序,number<20000,所以volatile不保证原子性
* <p>
* 3. 为什么不保证原子性
* number++:分为三步骤
* 1. 从主内存取值
* 2. 对值操作加1
* 3. 写回主内存
* <p>
* 多个线程同时读取到之内存中number的值,并操作加1,
* 最后把值写回主内存过程中,可能会出现数据丢失写值的效果
* 4. 如何解决原子性
* 1. 加sync
* 2. 使用juc包下的AtomicInteger
* 使用juc包下的AtomicInteger中CAS通过在将值写入主内存时循环比较的方式保证原子性
*/
public class VolatileDemo {
public static void main(String[] args) {
A a = new A();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
//测试volatile修饰的变量,不保证原子性
a.number++;
//测试AtomicInteger解决原子性
a.atomicInteger.getAndIncrement();
}
}, "线程" + i).start();
}
//需要等待上面20个线程运行结束,才能运行主线程
//后台有两个默认线程,所以大于2
while (Thread.activeCount() > 2) {
//让当前线程获得可运行状态
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "number=" + a.number);
System.out.println(a.atomicInteger.get());
}
public static void verifyVisible() {
A a = new A();
//第一个线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "start");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
a.change();
System.out.println(Thread.currentThread().getName() + "end:" + a.number);
}).start();
//第二个线程
while (a.number == 0) {
//main线程一直在这里循环,如果a.number一直等于0,也就是没有可见性
//如果跳出了循环,就代表可见性触发了
}
System.out.println(Thread.currentThread().getName() + "end:" + a.number);
}
}
class A {
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger(0);
public void change() {
number = 26;
}
}