文章目录
- 原子性
- 代码验证
- 解决方法
- 可见性
- 代码验证
- 导致原因
- 解决方法
- 有序性
- 概念
- 代码验证
原子性
原子性的概念是 当一个线程访问某个共享的变量时,对其他线程来看,该操作要么执行完毕要么没有发生,不会看到中间值。所以原子性只存在于多线程共享成员变量中,单线程或者多线程个对局部变量的操作都可以理解为是原子性的。
java中八大基本类型中long、double类型修饰的变量是非原子性,除此之外,剩下的六个都是原子性的,下面写一个demo来进行验证。
代码验证
因为 double 和long修饰的变量占64位,所以,32位系统对long类型变量寻址最少2次,而64位系统只需要执行1次,因此它们的非原子性只有在jdk32位下才能进行验证。
1、综上所述,要想验证的话,首先下载jdk32位版本,下载地址:jdk32位下载 下载之后配置全局变量;配置之后进入命令提示窗口查看java版本,如下显示为32位且为client端(没有数字提示即为32位;server端默认为64位,无法切换到32位,所以要下载32位jdk)
(server端的demo如下:)
2、打开idea,配置idea的启动jdk,为下载32位jdk的路径
3、最后代码来验证long类型的非原子性
这里启动两个线程,分别对共享变量赋予0和-1,然后第三个线程main线程来进行查看
public class AtomicTest implements Runnable {
static long value = 0;
private final long valueToSet;
public AtomicTest(long valueToSet) {
this.valueToSet = valueToSet;
}
public static void main(String[] args) {
Thread thread1 = new Thread(new AtomicTest(0L));
Thread thread2 = new Thread(new AtomicTest(-1L));
thread1.start();
thread2.start();
long snapShort;
//java模式为client模式,不会进行循环优化,snapShort=value不会循环外提
while (0 == (snapShort = value) || -1 == snapShort) {
}
//不等于0和1的时候打印出来
System.out.printf("Unexpected data: %d(0x%016x)", snapShort, snapShort);
//退出程序,否则子线程无限循环,程序永远不会终止
System.exit(0);
}
@Override
public void run() {
//两个线程不断的给共享变量value进行赋值,如果
for (; ; ) {
value = valueToSet;
}
}
}
如下为打印的结果:
我们可以看到,这里产生了一个中间值,非0(0x0000000000000000)也非-1(0xffffffffffffffff)所以,可以证明,long类型修饰的变量在32位系统下是不会保证原子性的。
解决方法
解决方法就是对共享变量value加上volatile关键字了,这里有人会问,volatile不是不能保证原子性吗?
volatile是可以保证写操作的原子性的,因为大部分例子都是拿i++进行举例,i++是分了三步(read-modify-write),包括读、写、更改,所以voliate肯定不能保证i++的原子性,但是本例子只有一个写操作,故可以保证原子性,所以面试的时候不要再说volatile不能保证原子性啦。
可见性
可见性的概念是 多线程环境下,一个线程更改了共享变量的值,其他线程可以立刻读取到更新的结果,这样其他线程不会读取到旧的数据,保证程序的正常运行。
代码验证
现在写一个demo:主线程先休眠1s(java语言会规定父线程在启动子线程之前,对变量的更改对于子线程来说是可见的,所以父线程休眠1s,要先启动子线程),子线程读取共享变量,然后主线程再修改共享变量,最后发现,子线程一直在循环,也就是一直读取的都是之前的变量。
public class VisibilityDemo {
private static boolean flag = true;
public static void main(String[] args) {
new Thread(()->{
while (flag){
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}
}
结果是子线程一直无法读取到主线程更新共享变量后的值,这就是没有保证线程之间的可见性
导致原因
在说解决方法之前,先来分析一下为什么没有保证可见性。
因为主线程休眠的时间内,只有一个线程在使用共享变量,这就导致JIT编译器认为真的只有一个线程对其访问,从而导致JIT为了避免重复读取主存中的变量,提高运行的效率,就把flag变量一直定义为true。
另一方面,可见性和计算机的存储系统有很大的关系:
1、程序中的变量可能会被分配到处理器中的寄存器中(Register)而不是主内存中进行存储,每一个线程如果运行在不同的处理器上,那他们无法读取对方处理器内寄存器中的值,所以就会导致变量不可见的现象。
2、另外,处理器对主存的访问并不是直接的,是通过高速缓存(Cache)进行读取的,而在高速缓存和处理器之间还有一个缓冲区叫写缓冲器(Store Buffer),所以该线程对共享变量的更改可能只写到了写缓冲器中,并没有到主存内,而每个处理器的写缓冲器又是隔离的,所以也无法看到共享变量的更新。(没有写到自己的高速缓存中)
3、即便该处理器的线程将变量写到高速缓存时,该处理器通知其他处理器的时候,其他处理器可能仅仅将该变量同步到自己的无效化队列(Invalidate Queue)中,没有更新到自己的高速缓存中。(没有写到对方的高速缓存中)
解决方法
虽然一个处理器无法读取另外一个处理器中的变量,但是处理器之间可以遵循缓存一致性协议(Cache Coherence Protocol)来解决该问题:该处理器可以读取其他部件(主内存、其他处理器的高速缓存)到自身处理器中高速缓存的过程叫做缓存同步。
所以,从写入的角度来看:当前处理器一定要把变量更改后的值更新到自己的高速缓存或者主存中,这个过程叫做冲刷处理器缓存;从读取的角度来看:如果其他处理器更新了共享变量,当前处理器一定要从主存或其他处理器的高速缓存中拉取变量到自己的高速缓存中,这个过程叫做刷新处理器缓存。
解决方法就是对该变量加上volatile关键字,它的作用就是高速JIT编译器,该变量会被多个线程访问,不需要进行优化,并且会使cpu执行冲刷处理器缓存和刷新处理缓存的过程。
下面是自己总结的一张图可以作为参考:
注意:可见性问题是多线程情况下产生的,和运行在几个处理器上是没有关系的,即使多个线程运行在同一个处理器上时,因为有时间片的分配以及上下文切换(一个线程对变量的修改会被该线程的上下文保存起来,导致其他线程无法查看),还是无法保证可见性。
有序性
概念
有序性是在一个处理器上运行的线程对共享变量的操作在其他处理器的线程来看是乱序的。乱序指的是对内存访问操作的顺序并不是从上到下的。
代码验证
这里直接拿单例模式的doubleCheck进行讲解(跑了好多线程,没有复现不符合可见性的情况,没有截图,所以就用这个例子替代啦)
public class SingleThreadDemo {
private SingleThreadDemo(){
}
private volatile static SingleThreadDemo INSTANCE;
public static SingleThreadDemo getInstance(){
//操作1
if (INSTANCE==null){
synchronized (SingleThreadDemo.class){
if (INSTANCE==null){
//操作2
INSTANCE = new SingleThreadDemo();
}
}
}
//操作3
return INSTANCE;
}
public static void main(String[] args) {
getInstance();
}
}
如上,之所以要对变量加上volatile,是因为当一个线程A在操作2的时候,另一个线程B在操作1,这时候线程A正在初始化,而线程B发现实例不为null,就会直接走到操作3,进行返回,这个过程就有可能出现问题。原因如下:
创建一个对象的过程要分为三部分:
INSTANCE = new SingleThreadDemo();
1、分配对象的存储空间,并标识该存储空间的引用
objRef = allocate(SingleThreadDemo.class)
2、初始化引用的对象objRef
invokeConstructor(objRef)
3、将对象给变量
INSTANCE = objRef
而JIT编译器操作的顺序可能并不是123,而是132,因为处理器为了提高指令的执行效率,会动态调整指令的顺序,这就有可能线程A在执行操作2的代码时,先执行了上述第13步,还未执行到第2步,而线程B虽然判断对象不为null,但是由于对象并未初始化,直接执行操作3就会出现问题。