java 内存模型
JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory), 线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行, 而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。 不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
可见性 原子性 有序性
线程安全取决于是否有多个线程访问(说句废话),避免线程安全问题主要从原子性,可见性,有序性着手
1.原子性 同生共死 -- 要么全部执行要不全部不执行不可中断!
举例:
x = 10 原子性操作 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x 非原子操作 实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,
虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++ 非原子操作 x++、x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1 非原子操作
注意: 在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性,这就导致了long、double类型的变量在32位虚拟机中是非原子操作
2.可见性
可见性就是对一个变量进行修改,其他的线程能够立即看到修改的值,JMM利用主内存。工作内存修改后刷新到主内存,别的线程调用时会把主内存的最新得值刷新到工作内存 这种方式是依赖主内存来实现可见性
举例 :
int i = 1;//主内存
//线程1执行的代码
i = 10;
//线程2执行的代码
j = i;
主内存变量A原始值为1,线程1从主内存取出变量A,修改A的值为2,在线程1未将变量A写回主内存的时候,线程2拿到变量A的值仍然为1 造成可见性问题
Java提供了volatile关键字来保证可见性。当对非volatile变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中,非volatile变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。而声明变量是volatile的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时, 它会跳过CPU cache这一步去内存中读取新值。volatile只确保了可见性,并不能确保原子性
3.有序性
指令重排序:
即程序执行的顺序按照代码的先后顺序执行。为了提高性能,编译器和处理器常常会对指令做重排序。CPU虽然并不保证完全按照代码顺序执行, 但它会保证程序最终的执行结果和代码顺序执行时的结果一致。 重排序过程不会影响到单线程程序的执行,但会影响到多线程并发执行的正确性。
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
代码中,由于语句1和语句2没有数据依赖性,可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,
那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。
可以通过volatile关键字来保证一定的“有序性”,volatile关键字本身就包含了禁止指令重排序的语义,volatile前的代码还会在voaltile前,
其后的代码还会在其后。另外可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,
相当于是让线程顺序执行同步代码,自然就保证了有序性,synchronized标记的变量可以被编译器优化。
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
volatile 变量:轻量级多线程同步机制,不会引起上下文切换和线程调度。仅提供内存可见性保证,不提供原子性。
CAS 原子指令:轻量级多线程同步机制,不会引起上下文切换和线程调度。它同时提供内存可见性和原子化更新保证。
内部锁和显式锁:重量级多线程同步机制,可能会引起上下文切换和线程调度,它同时提供内存可见性和原子性。
https://juejin.im/entry/59ba2a22f265da065d2b5789 引用此文 -- 更加精细