volatile
volatile 是java 虚拟机提供的最轻量级的同步机制, java 的内存模型为volatile 专门定义了特殊的访问规则,当一个变量被定义成volatile 类型的之后, 他就会具备两种特性
- 保证变量对所有的线程可见
- 能够防止指令重排
volatile 保证可见性
普通变量线程不可见
普通变量在线程之间传递是依靠主内存来实现的。举例:线程 A 修改了一个普通变量的值, 并且往主内存里面回写, 另外一条线程 B 在线程A回写之后在对主内存的值进行读取, 新变量值次才会对线程B可见!
volatile 变量线程可见性
所谓对所有线程可见的意思是, 当一个线程对此变量做出修改之后, 其他的线程能够立马知道这个变量的最新值。
注意: volatile变量保证在线程之间是可见的 并不能得出 volatile变量 在并发下是线程安全的结论!原因是 java 里面的运算操作符并不是原子的!这导致volatile 变量 在并发下一样是不安全的
测试
可见 在主线程中修改了 flag = 1 然后在其他的线程中可以得到这个最新的变量值!
public class demo01 {
// 测试可见性
// 定义一个变量值, 不使用 volatile 修饰该变量的话, 程序会进入四循环, 当前线程无法得到 变量 flag 被修改之后的值
// volatile 能够保证可见性
/**
*
*/
static volatile Integer flag = 0;
public static void main(String[] args) {
new Thread(() -> {
while (flag == 0) {
}
}, "A").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在主线程中修改它, 判断在其他线程能否检测到它的变化。
flag = 1;
System.out.println("flag: " + flag);
}
}
volatile 不保证原子性
下面的测试, 每次的结果都是不一样的,其原因就是 **num++;**本身不是一个原子的操作, java 在进行这个操作的时候并不是一个原子性的, 可以通过 javap -c demo.class来查看字节码文件,我们的自增操作被转化为了多条字节码, 也就意味着简单的自增交由 java 执行的时候将会是一个多步的操作!
自增不安全
简单分析这个字节码文件, 当线程执行 getstatic 指令把当前的num值取到操作栈顶的时候, volatile 保证此时的num值是正确的, 但是在执行inconst_l iadd 这些指令的时候 也许其他线程已经对 num的做出了改变!那么操作栈顶的值就是一个过期的数值, puystatic 就会把一个较小的num同步到主内存中
public class demo {
private volatile static int num = 10;
public static void increase() {
num++;
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increase();
}
}, String.valueOf(i)).start();
}
// 最后线程的存活数量大于 2 的话就证明 上面的 20 条线程还没执行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + num);
}
}
解决!
针对原子性的问题, 我们可以使用锁或者具有原子操作的原子类来保障!
private volatile static AtomicInteger num = new AtomicInteger();
// cas 的原子性操作
public static void increase() {
num.incrementAndGet();
}
public synchronized static void increase() {
num++;
}
volatile 防止指令重排
什么是指令重排
普通类型的变量在方法中执行的时候, 只能保证在执行过程中所有依赖赋值结果的地方都能获得到正确的结果! 而不能保证变量的赋值操作顺序与程序代码中的执行顺序是一致的!
简单点就是在一个方法中 的赋值操作 a = 1; 对于普通变量来说, 这行代码执行的顺序和源代码中的顺序可能是不一样的
测试
下面的代码正确的执行顺序是
- 初始化
- 初始化之后
但是由于可能会发生指令重排! a = 1; 这个操作可能会被提前执行! 就会出现下面的错乱现象! 在被 volatile 修饰之后便不会发生!
初始化
初始化之后
1
初始化
初始化
初始化之后
1
初始化之后
public class demo4 {
Integer a = 0;
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
System.out.println(new demo4().test());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, i + "").start();
}
}
public int test() throws InterruptedException {
System.out.println("初始化");
a = 1;
while (a == 0) {
TimeUnit.SECONDS.sleep(10);
}
System.out.println("初始化之后");
return a;
}
}
内存屏障
在汇编中我们可以看到volatile 类型的变量在赋值操作之后会多进行一个空操作, 也就是所谓的内存屏障! 来防止指令重排的发生!