java的并发采用的是共享内存模型,java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
java线程之间的通信由java内存模型(简称JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
JMM决定了一个线程对共享变量的写入何时对另一个线程可见。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile
。
需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
JMM中无法保证:每个操作必须立即对任意线程可见(这是顺序所保证的)
==>JMM中,只有当前线程把本地内存中写过的数据刷新到主内存后,这个写操作才能对其他线程可见
。
(1)原子性:
定义:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作
。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
Java中的原子性操作包括:
(2)可见性:
定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
可见性是指一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
为什么会出现这种问题呢?
我们知道,java线程通信是通过共享内存的方式进行通信的,而我们又知道,为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存
。
java线程内存模型:
实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。
对于jvm来说,主内存是所有线程共享的java堆,而工作内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。
那么我们怎么知道什么时候工作内存的变量会刷写到主内存当中呢?
这就涉及到java的happens-before
关系了。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
简单来说,只要满足了happens-before关系,那么他们就是可见的。
例如:
线程A中执行i=1,线程B中执行j=i。如果线程A的操作和线程B的操作满足happens-before关系,那么j就一定等于1,否则j的值就是不确定的。
happens-before关系如下:
从上面的happens-before规则,显然,一般只需要使用volatile关键字,或者使用锁的机制,就能实现内存的可见性了。
(3)有序性:
定义:即程序执行的顺序按照代码的先后顺序执行。
Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。
为什么会出现不一致的情况呢?---->这是由于重排序的缘故。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
举个例子:
线程A:
context = loadContext();
inited = true;
线程B:
while(!inited ){
sleep
}
doSomethingwithconfig(context);
如果线程A发生了重排序:
inited = true;
context = loadContext();
那么线程B就会拿到一个未初始化的content去配置,从而引起错误。
因为这个重排序对于线程A来说是不会影响线程A的正确性的,而如果loadContext()方法被阻塞了,为了增加Cpu的利用率,这个重排序是可能的。
如果要防止重排序,需要使用volatile关键字,volatile关键字可以保证变量的操作是不会被重排序的。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。
另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
附上相关的链接:
https://mp.weixin.qq.com/s/snxOvuED1KvrOVexdsutSg
感谢并参考:
https://mp.weixin.qq.com/s/7NyuuUpGRtmUjQsWe2AhpA