volatile关键字的作用主要有以下两个作用
*内存可见性:当一个线程修改了一个共享变量时,另外一个线程能读到这个被修改的变量值。
*有序性:禁止指令重排序。
1、内存可见性
1.1、可见性问题
代码示例:
import java.util.Scanner;
/**
* 观察内存可见性
*/
public class Exe_01 {
public static class Counter{
public static int count=0;
}
public static void main(String[] args) {
Thread t1=new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"线程启动");
while(Counter.count==0){
//一直循环
}
System.out.println(Thread.currentThread().getName()+"线程退出");
},"t1");
//启动线程
t1.start();
//确保t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2=new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"线程启动");
//获取用户输入
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个不为0的数字:");
Counter.count=sc.nextInt();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程退出");
},"t2");
//启动线程
t2.start();
}
}
1.2、volatile解决内存可见性问题
添加volatile关键字修改代码
import java.util.Scanner;
/**
* 观察内存可见性
*/
public class Exe_01 {
public static class Counter{
public static volatile int count=0;
}
public static void main(String[] args) {
Thread t1=new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"线程启动");
while(Counter.count==0){
//一直循环
}
System.out.println(Thread.currentThread().getName()+"线程退出");
},"t1");
//启动线程
t1.start();
//确保t1先启动
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2=new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"线程启动");
//获取用户输入
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个不为0的数字:");
Counter.count=sc.nextInt();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程退出");
},"t2");
//启动线程
t2.start();
}
}
运行结果:
2、有序性(指令执行的有序)
2.1、内存屏障
作用就是保证指令的执行先后顺序
屏障类型 | 指令示例 | 说明 |
LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读操作先于laod2执行 |
StoreStore | Store1;StoreStore;Store2 | 保证Store1的写操作先于Store2执行,并刷新到主内存 |
LoadStore | Load1;LoadStore;Store2 | 保证load1的读操作结束先于Store2的写操作执行 |
StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的操作才能执行 |
1、在每个volatile写操作之前插入StoreStore屏障,这样就能让其他线程修改变量A之后,让修改的新变量对当前线程可见。
2、在写操作之后插入StoreLoad屏障,这样就能让其他线程获取变量A的时候,能够获取到已经被当前线程修改的值
3、在每个volatile读操作之前插入LoadLoad屏障,这样就能让当前线程获取变量A的时候,保证其它线程也能获取相同的值,这样线程读取到的数据就一样了。
4、在读操作之后插入LoadStore屏障,这样就能让当前线程在其它线程修改变量A的值之前,获取到主内存里面变量A的值。
注:
volatile可以保证内存可见性,是因为在编译的过程中,在volatile修饰的变量前后都加入了相关的内存屏障
volatile可以保证有序性。
3、原子性
代码示例
public class Demo_402 {
private static volatile int num=50000;
public static void main(String[] args) throws InterruptedException {
Counter04 counter=new Counter04();
//创建两个线程
Thread thread1=new Thread(()->{
for (int i = 0; i < num; i++) {
//自增操作
counter.increase();
}
});
Thread thread2=new Thread(()->{
for (int i = 0; i < num; i++) {
//自增操作
counter.increase1();
}
});
//启动线程
thread1.start();
thread2.start();
//等待线程结束
thread1.join();
thread2.join();
//获取自增后的count值
System.out.println("count结果="+counter.count);
}
}
class Counter04{
public int count=0;
//执行自增操作
public void increase(){
//在方法之前加上synchronized,修饰来说明这是一个需要加锁的方法
count++;
}
//方法不加锁
public void increase1(){
count++;
}
}
运行结果:
volatile关键字不保证原子性,原子性是由synchronized保证的,在多线程环境中一般两个关键字搭配使用。
4、总结
1、volatile通过内存屏障,保证内存可见性。
2、volatile通过内存屏障,禁止指令重排序,从而保证有序性。
3、volatile不保证原子性。
*通过内存屏障,当一个线程修改了一个共享变量的时候,后面的线程就必须从主内存中重新读取最新的值
*通过内存屏障,让相邻的两条指令串行执行,从而保证内存可见性。
注意:volatile没有别的用法,就只是修饰变量