相关概念
- 原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
- 可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
- 有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的(但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致)
可见性问题
- 示例demo
public class Template {
private boolean generalFlag = false;
private volatile boolean volatileFlag = false;
public void generalRefresh(){
this.generalFlag = true; //普通写操作
System.out.println("线程:"+ Thread.currentThread().getName() +":修改共享变量generalFlag");
}
public void generalLoad(){
while (!generalFlag){}
System.out.println("线程:"+Thread.currentThread().getName()+":当前线程嗅探到generalFlag的状态的改变");
}
public void volatileRefresh(){
this.volatileFlag = true; //volatile写操作
System.out.println("线程:"+ Thread.currentThread().getName() +":修改共享变量volatileFlag");
}
public void volatileLoad(){
while (!volatileFlag){}
System.out.println("线程:"+Thread.currentThread().getName()+":当前线程嗅探到volatileFlag的状态的改变" );
}
public static void testGeneralOperation() throws InterruptedException {
Template template = new Template();
Thread threadA = new Thread(()->{template.generalRefresh();},"threadA");
Thread threadB = new Thread(()->{template.generalLoad();},"threadB");
threadB.start();
Thread.sleep(2000);
threadA.start();
}
public static void testVolatileOperation() throws InterruptedException {
Template template = new Template();
Thread threadC = new Thread(()->{template.volatileRefresh();},"threadC");
Thread threadD = new Thread(()->{template.volatileLoad();},"threadD");
threadC.start();
Thread.sleep(2000);
threadD.start();
}
public static void main(String[] args) throws InterruptedException {
testGeneralOperation(); // 测试普通变量在线程间的通信
// testVolatileOperation() // 测试volidate变量在线程间的通信
}
}
- testGeneralOperation();执行结果
线程A修改了变量后线程B并没有获取到最新的值,这时的状态是线程B一直在自旋。
- testVolatileOperation();执行结果
线程C修改volatileFlag 后线程D立马感知到,退出循环。
volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。
原子性问题
- 实例demo
public class AtomicSample {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 10; i++ ){
Thread thread = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++;
}
});
thread.start();
}
Thread.sleep(1000);
System.out.println(count);
}
}
- 执行结果
可以看出在 count++ 的过程中,出现了主存和线程的本地缓存变量不一致的问题,这是由于多个线程同时计算同一个数,当其中一个线程计算完成刷回主存,则其他线程便会会写失败,这时循环失效。所以 volatile 修饰的变量在多线程环境下不能保证原子性。这里对非原子操作 count++ 加锁可避免该问题。
有序性问题
- 示例demo
public class ReOrderSample {
private static int x = 0, y = 0;
private static int a = 0, b =0;
static Object object = new Object();
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (;;){
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread t1 = new Thread(() -> {
//synchronized (object){
a = 1;
x = b;
//}
});
Thread t2 = new Thread(() -> {
//synchronized (object){
b = 1;
y = a;
//}
});
t1.start();t2.start();
t1.join();t2.join();
/**
* cpu 或者 jit 对代码进行了指令重排
* 1,1 || 0,1 || 1,0 || 0,0
*/
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}
}
}
- 执行结果
出现(0,0)的这种现象有两种原因:
① 处理器在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待,处理器这种乱序执行的技术,可以大大提高执行效率。
② 处理器大多会利用缓存(cache)以提高性能,尽可能地避免其访问主内存的时间开销,这种情况下会存在一个现象,即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。这时,各CPU所看到同一内存地址的数据的值可能不一致。从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能不一致。这种内存可见性问题造成的结果就好像是内存访问指令发生了重排序一样。
PS:将两个线程中的 synchronized 代码块打开之后,便不会出现指令重排的现象;将变量a,b声明volatile之后,不会发生指令重排;使用Unsafe类手动添加内存屏障之后,不会发生指令重排。(Unsafe类的操作可参考美团团队技术博客(https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html))。
volatile内存语义
- 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 禁止指令重排序优化。
图中 NO 代表的是当前两种操作在这个情况下,操作一和操作二是否允许重排序。
- 基于保守策略的JMM内存屏障插入策略
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
VarHandle变量句柄
- 概念
定义一个用来操作对象的字段、数组元素的跟 java.util.concurrent.atomic 和 sun.misc.Unsafe 等价的标准工具,它提供了一个标准的栅栏操作(fence operation)工具集用于精细地控制内存排序和一个标准的可达性栅栏操作(reachability-fence operation)来保证一个被引用的对象是强可达的(strongly reachable)JDK9引入,主要体现在VarHandle的访问模式。主要使用在JUC包中,代替Unsafe类操作
- VarHandle的访问模式
- 读模式,例如用带 volatile 内存排序效果(volatile memory ordering effects)的语义去读一个变量;
- 写模式,例如使用 release 内存操作效果(release memory ordering effects)去更新变量;
- 原子更新模式,例如使用带 volatile 内存排序效果的去CAS更新变量;
- 数值原子更新模式,例如使用带 plain memory order effects 的写和用于读的 acquire memory order effects 来执行 get-and-add;
- 按位原子更新模式,例如使用 release memory order effects 的写和 plain memory order effects 的读来执行 get-and-bitwise-and。
- VarHandle操作示例Demo
public class VarHandleSample {
static class Student{
public int age = 20;
protected long score = 500;
private String name = "anthony";
public String[] course = new String[]{"Chinese","English","History"};
@Override
public String toString() {
return "Student{" +
"age=" + age +
", score=" + score +
", name='" + name + '\'' +
", course=" + Arrays.toString(course) +
'}';
}
}
// 用于成员变量和静态变量的 VarHandle 使用 java.lang.invoke.MethodHandles.Lookup 下的方法生成。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
accessPublicField();
// accessProtectedField();
// accessPrivateField();
// accessArrayField();
}
public static void accessPublicField() throws NoSuchFieldException, IllegalAccessException {
Student studentOne = new Student();
VarHandle ageHandle = MethodHandles.lookup()
.in(Student.class)
.findVarHandle(Student.class,"age",int.class);
ageHandle.set(studentOne,40);
System.out.println(studentOne.age);
}
public static void accessProtectedField() throws NoSuchFieldException, IllegalAccessException {
Student studentOne = new Student();
VarHandle scoreHandle = MethodHandles.lookup()
.in(Student.class)
.findVarHandle(Student.class,"score",long.class);
scoreHandle.set(studentOne,1000);
System.out.println(studentOne.score);
}
public static void accessPrivateField() throws NoSuchFieldException, IllegalAccessException {
Student studentOne = new Student();
VarHandle nameHandle = MethodHandles.lookup()
.in(Student.class)
.findVarHandle(Student.class,"name",String.class);
nameHandle.set(studentOne,"anthonyPrivate");
System.out.println(studentOne.name);
}
public static void accessArrayField() throws NoSuchFieldException, IllegalAccessException {
Student studentOne = new Student();
VarHandle courseHandle = MethodHandles.arrayElementVarHandle(String[].class);
courseHandle.compareAndSet(studentOne.course,0,"Chinese","chinese");
courseHandle.compareAndSet(studentOne.course,1,"English","english");
System.out.println(studentOne.course[0]);
System.out.println(studentOne.course[1]);
}
}
VarHandle参考:https://jekton.github.io/2018/07/22/java-translation-jep-193-Variable-Handles/