-
原子性:一个操作或者多个操作,要么都被执行且不能被中断打断,要么都不执行;
Java的原子性操作有以下三种:
a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
b.所有引用reference的赋值操作
c.java.concurrent.Atomic.* 包中所有类的一切操作
-
可见性:当多线程访问同一个变量时,一个线程改变该变量,其他的线程也能立即看到修改的值;
-
重排序:为了提高指令的运行性能,在编译时或者运行时对指令的执行顺序进行了调整,重排序又分为编译时重排序和运行时重排序。编译时重排序是指编译源代码的时候就对代码执行顺序进行分析,在遵循as-if-serial的原则前提下对源码的执行顺序进行调整,as-if-serial原则是指在单线程环境下,无论怎么重排序,代码的执行结果都是确定的。运行时重排序是指为了提高运行的速度,系统对执行的顺序进行调整。
一个线程修改共享变量的之后,另外一个线程真的能够马上拿到变量的最新值吗?我们来做一个测试:
public class VolatileTest_1 {
private static boolean flag=false;
public static void main(String[] args) throws InterruptedException {
Thread thread_1=new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName()+":"+flag);
}
}
}
});
Thread thread_2=new Thread(new Runnable() {
@Override
public void run() {
flag=true;
System.out.println(Thread.currentThread().getName()+":"+flag);
}
});
thread_1.start();
Thread.sleep(1000);
thread_2.start();
}
}
运行结果:
可以看到线程0一直在循环,那么线程0并没有到线程1更新的flag值。
那么是什么原因导致了内存不可见了呢?
- cache缓存:因为cpu的速度远远高于主内存的速度,为减少资源的浪费,在cup上又设置了多级的cache,而线程运行的时候会先将数据拷贝到线程内部的cache里面,也就是工作内存(working memery),那么多个线程访问同一个变量是就变成每个线程访问自己的cache,变相导致了内存的不可见。
2.重排序:除了cache的原因就是,执行顺序的重排序,导致线程A线程读取某个变量的时候,线程B还没将该变量的最新值写会主内存。
解决缓存一致性的操作有两种
- 总线加锁:是一种独占式的方式,只能当前的cpu使用,其他的cpu进入阻塞的状态,效率低下;
- 缓存一致性协议(MESI):确保缓存中使用的共享变量都是一致的
volatile的原理:有volatile修饰的共享变量进行写操作的候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。
- 将当前处理器缓存行数据刷写到系统主内存。
- 这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。
Volatile的使用场景:状态标记、double check
状态标记的应用场景:比如上面举的例子,用volatile修饰flag变量便能解决主存不可见的问题
Double checked:双重检查的单例模式
public class SingleTon_01 {
private static volatile SingleTon_01 singleTon_01;
private SingleTon_01(){}
public static SingleTon_01 getSingleTon_01() {
if (singleTon_01==null) {
synchronized (SingleTon_01.class) {
if (singleTon_01==null) {//避免其他的线程已对singleTon_01进行了赋值
singleTon_01=new SingleTon_01();
}
}
}
return singleTon_01;
}
}