1、计算机中线程不安全问题产生缘由
计算机在执行程序时,每条指令都是在CPU中执行的,执行的过程会涉及到读取和写入。程序运行过程当中的临时数据是存放在主存(物理内存)中的,这就会产生一个问题,因为CPU的执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU的执行速度相比就慢不少,所以若是任什么时候候对数据的操做都要经过和内存的交互来进行,就会大大下降指令的执行速度。
为了解决这个问题,计算机中就有了CPU缓存的概念。在程序的运行过程当中,操做的数据会从内存中复制一份到CPU缓存中,当CPU进行计算的时候就能够直接从它的缓存中读取数据和写入数据,当运算结束以后,再将缓存中的数据刷新到主存中。举个例子:
CPU在执行这段代码时,会先从高速缓存中查看是否有t的值,若是有,则直接拿来使用,若是没有,则会从主存中读取,读取以后会复制一份存放在高速缓存中方便下次使用。以后cup进行对t加1操做,而后把数据写入高速缓存,最后会把高速缓存中的数据刷新到主存中。
这一过程在单线程运行下时没问题的,单当在多线程的状况下就会有问题了。在多核CPU中,每条线程可能会运行在不一样的CPU中,所以每一个线程运行时有本身的CPU缓存。这时就会出现同一个变量在两个CPU缓存中值不一致的状况。
例如,两个线程分别读取了t的值,假设此时t的值为0,而且把t的值存到了各自的CPU缓存中,而后线程1对t进行了加1操做,此时t的值为1,并把t的值写回到了主存中。但线程2中的CPU缓存仍是0,进行加1操做后,t的值为1,再把t写回主存,此时就出现了线程不安全的问题。
对非普通变量进行读写的时候,每一个线程先从内存拷贝变量到CPU缓存中。若是计算机有多个CPU,每一个线程可能在不一样的CPU上被处理,这意味着每一个线程能够拷贝到不一样的 CPU cache 中。
2、如何确保线程安全
Java中有几种机制来确保线程安全,例如Volatile、Synchronized关键字,这些机制适用于各类平台。一般,要保证线程安全,就是指要保证如下三个方面特性的完整和正常:
1.原子性---在对数据进行操做时这个操做时不可分割的,好比a=0这个操做就是个原子操做,a++(其实是a=a+1)这个操做是可分隔的,它就不是一个原子操做。java中使用synchronized、lock、unlock来保证原子性。
2.可见性---一个线程对主内存的修改能及时被其余线程看到,也就是说,在多线程环境下,某个共享变量若是被其中一个线程给修改了,其余线程可以当即知道这个共享变量已经被修改了,当其余线程要读取这个变量的时候,最终会去内存中读取,而不是从本身的工做空间中读取。
好比:使用volatile修饰的变量就具备可见性,volatile修饰的变量不容许内部缓存和指令重排序,即它的操做不通过CPU缓存,而直接修改内存。volatile 保证了新值能当即同步到主内存,以及每次使用前当即从主内存刷新。但普通变量作不到这点,普通变量的值在线程间传递均须要经过线程--->工做空间--->主内存这样的顺序来完成。
3.有序性---java使用volatile和synchronized来保证线程之间的有序性,volatile是由于自己包含“禁止执行重排序”的语义,synchronized是由“一个变量在同一时刻只容许一个线程对其进行lock操做”得到的。产生这个问题缘由是虚拟机在执行代码时,并不会按照咱们事先编写好的代码顺序来执行,好比下面两行代码:
对于这两行代码,不管是先执行a=1仍是b=2,都不会对a,b最终的值有任何影响,因此虚拟机在编译的时候,是有可能对它们重排序的。CPU采用了容许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的机制,假如执行int a = 1须要100ms的时间,而执行int b = 2 须要1ms的时间,而且执行哪句代码都不会对a,b的最终值产生影响,那固然是先执行b=2这句代码了。因此,虚拟机在进行代码编译优化的时候,对于那些改变顺序以后不会对最终变量的值形成影响的代码,是有可能将他们进行重排序的。
那么这个时候指令的重排序虽然对值没有什么影响,但可能会出现线程安全的问题。
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while(!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
这段代码最终打印的必定是42吗?若是没有重排序的话,打印的确实会是42,但若是number = 42和ready = true被进行了重排序,颠倒了顺序,那么就有可能打印出0了,而不是42。(由于number的初始值会是0)。在没有同步的状况下,编译器、处理器在运行时均可能对操做的执行顺序进行一些意向不到的调整,咱们也没法肯定代码的实际执行顺序。
若是一个变量被声明成volatile的话,那么这个变量不会被重排序,也就是说,虚拟机会保证这个变量以前的代码必定比它先执行,而以后的代码必定比它后执行。例如把上面中的number声明为volatile,那么number = 42必定会比ready = true先执行。不过须要注意的是,虚拟机只是保证了这个变量的执行顺序,而它以前或以后的代码执行顺序仍是有可能会进行重排序的。
3、volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操做通知到其余线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,所以不会将该变量上的操做与其余内存操做一块儿重排序。volatile变量不会被缓存在寄存器或者对其余处理器不可见的地方,所以在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操做,所以也就不会使执行线程阻塞,所以volatile变量是一种比sychronized关键字更轻量级的同步机制。
当一个变量定义为 volatile 以后,将具有两种特性:
1.保证此变量对全部的线程的可见性,这里的“可见性”,如以前所述,当一个线程修改了这个变量的值,volatile 保证了新值能当即同步到主内存,以及每次使用前当即从主内存刷新。但普通变量作不到这点,普通变量的值在线程间传递均须要经过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操做,这个操做至关于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障以前的位置),只有一个CPU访问内存时,并不须要内存屏障;(什么是指令重排序:是指CPU采用了容许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
4、volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,可是写操做稍慢,由于它须要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
5、volatile并不能代替synchronized
前面提到,volatile和synchronized均可以用来保证线程安全,可是volatile能够替代synchronized吗?答案是不行的,volatile只能保证变量的可见性,并不能保证原子性,所以在高并发的状况对元素进行操做可能会形成混乱。下面来看两个例子:
/**
* @author 一池春水倾半城
* @date 2019/9/24
*/
public class Volatile {
volatile boolean flag;
void check() {
if (flag == !flag) {
System.out.println("WTF?");
}
}
void swap() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = !flag;
}
public static void main(String[] args) {
Volatile thread = new Volatile();
Thread t2 = new Thread() {
@Override
public void run() {
while (true) {
thread.check();
}
}
};
t2.start();
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
thread.swap();
}
}
};
t1.start();
}
}
打印结果:WTF?
WTF?
WTF?
WTF?
...
...
上面的代码启动了两个线程,一个用来改变flag的值,另外一个用来判断flag == !flag,按照常理来讲flag是不会等于!flag的,可是根据上面打印的结果来看却出现了这种状况,这是为何呢?
这是因为volatile的机制形成的,
volatile只保证了可见性而没有保证原子性,也就是说,共享数据会因高并发被同一个数据覆盖。通俗点讲,
多个线程同时改变主内存中的某个值的时候,一个线程改变了这个值,并通知给其余线程及时更新本身线程内缓冲区的副本,可是因为线程改变volatile修饰的变量后须要写入到公共内存中+其余线程再读取,这个过程必然会慢于其余线程写出的速度,致使其余线程还没来得及更新本身副本变量就执行了写出,致使主内存中的数据被覆盖。所以在高并发的状况下不对某个数据的写入加锁,即使设置了volatile可见性,依然会出现问题。
而当删除了flag上的volatile修饰以后,程序则不会输出任何信息,这个时候是保证了原子性吗?并无。
t1线程和t2线程在开始运行时都从主存中读取了flag到各自的工做区中(Cpu缓存),运行过程当中使用的是各自工做区中的flag,不会再去从主存中读取。所以,t1线程修改的flag只在t1线程的工做空间中生效,而t2并不会读取到flag的改变,因此就不会出现上面加上volatile修饰的状况。