volitale关键字
简介:
volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。
修饰的变量只能做赋值操作,不能做自增操作
1. 并发编程的3个基本概念
(1) 原子性
定义:即一个操作或者多个操作,要么全部执行并且执行过程不会被任何因素打断,要么就都不执行。
(2) 可见性
定义: 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
(3) 有序性
定义: 即线程执行的顺序按照代码的先后顺序执行
2. volatile关键字作用
- 保证内存可见性
private static volatile boolean flag = true;
public static main(String[] args){
new Thread(()->{
try{
Thread.sleep(5000);
} catch (InterruptedException){
e.printStackTrace();
}
flag = false;
}).start();
while(flag){
}
}
注意:
在上述代码中,如果不加关键字volatile,while循环将一直执行无法跳出循环。
因为Java的内存机制,每一个内存都有自己的一个独有的工作内存,
如果工作内存的数据没有发生改变,此线程就不会再去读取主内存
或者将修改过后的值放回到主内存中。所以就算其他线程修改了flag的值,
while也不会去读取主内存中修改过的flag值,就会一直使用
工作内存的flag值,所以会一直循环下去。
- 禁止指令重排
指令的执行顺序并不一定会像我们编写的顺序那样执行,为了保证执行上的效率,JVM可能会对指令进行重排序。最金典的例子就是多线程场景下的双重检查加锁的单例实现:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() { //1
if (instance == null) { //2
synchronized (Singleton.class) { //3
if (instance == null) { //4
instance = new Singleton(); //5
}
}
}
return instance;
}
}
注意:
需要加volatile关键字的原因是,在并发情况下,如果没有这个关键字,
在第5行会出现问题。因为第五行代码“instance = new Singleton()”
并不是原子性操作,在JVM中被分为如下三个阶段执行:
1. 为instance分配内存
2. 初始化instance
3. 将instance变量指向分配的内存空间
由于JVM可能存在重排序,可能会执行第3步然后再执行第2步。也就是说可能
会出现instance变量还没有初始化完成,其他线程就已经判断了该变量不为null,
结果返回了 一个没有初始化的半成品。而加上volatile关键字,可以保证instance
变量的操作不会被JVM重排。
3. 不保证原子性
虽然volatile关键字可以保证内存可见性和有序性,但是不能保证原子性。也就是说对volatile修饰的变量进行的操作,不保证多线程安全。如下实例:
private static CountDownLatch countDownLatch = new CountDownLatch(1000);
private volatile static int num = 0;
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
try {
num++;
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
System.out.println(num);
}
静态变量num被volatile所修饰,并且同时开启1000个线程对其进行累加的操作,按道理来说,其结果应该为1000,但实际的情况是,每次运行结果都是一个小于1000的数字,并且不固定。那么这是为什么呢?原因是因为“num++;”这行代码并不是原子操作,尽管它被volatile所修饰了也依然如此。++操作的执行过程如下面所示:
- 首先获取变量i的值
- 将该变量的值+1
- 将该变量的值写回到对应的主内存中
虽然每次获取num值的时候,也就是执行上述第一步的时候,都拿到的是主内存的最新变量值,但是在进行第二步num+1的时候,可能其他线程在此期间已经对num做了多次修改,这时再进行第二三步操作之后就会覆盖了一个旧值,发生了错误。比如说:线程A在执行第一步的时候读取到此时num的值为3,然后在执行第二步之前,其他多个线程已经对该值进行了多次修改,使得num值变为了10。而线程A此时执行第二步,将原先的num值为3的结果+1变为了4,最后再将4写回到主内存中(实际此时num应该为11)。所以这也就是最后的执行结果为什么都会是一个小于1000的值的原因,内存可见性只能保证在第一步操作上的内存可见性而已。
所以如果要解决上面代码的多线程安全问题,可以采取加锁synchronized的方式,也可以使用concurrent包下的原子类AtomicInteger,以下的代码演示了使用AtomicInteger来包装num变量的方式:
private static CountDownLatch countDownLatch = new CountDownLatch(1000);
private static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executor.execute(() -> {
try {
num.getAndIncrement();
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
System.out.println(num);
}