原子性
原子性的概念是 当一个线程访问某个共享的变量时,对其他线程来看,该操作要么执行完毕要么没有发生,不会看到中间值。所以原子性只存在于多线程共享成员变量中,单线程或者多线程个对局部变量的操作都可以理解为是原子性的。
java中八大基本类型中long、double类型修饰的变量是非原子性,除此之外,剩下的六个都是原子性的,下面写一个demo来进行验证。
代码验证
因为 double 和long修饰的变量分别占32和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;
while (0 == (snapShort = value) || -1 == snapShort) {
}
System.out.printf("Unexpected data: %d(0x%016x)", snapShort, snapShort);
System.exit(0);
}
@Override
public void run() {
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执行冲刷处理器缓存和刷新处理缓存的过程。 下面是自己总结的一张图可以作为参考:
注意:可见性问题是多线程情况下产生的,和运行在几个处理器上是没有关系的,即使多个线程运行在同一个处理器上时,因为有时间片的分配以及上下文切换(一个线程对变量的修改会被该线程的上下文保存起来,导致其他线程无法查看),还是无法保证可见性。
|