一、内存模型的相关概念
计算机在执行程序时,指令的执行都是在CPU中。在执行指令的过程中,会将临时变量存储在主存(物理内存)中。 CPU执行指令很快,而从内存中读取和写入数据却相对来说很慢,所以就有了高速缓存。 在程序运行时,先将数据从主存复制一份到高速缓存中,然后CPU执行指令时,就可以直接从高速缓存中读取和写入数据,然后刷新到内存中。
这里就产生了一个问题。 比如i是一个共享变量,初始值为0
i = i+1;
在单线程中,CPU执行指令时,先从高速缓存中取出数据,然后进行加1操作,再写入高速缓存中,然后刷新到主存中。 但是多线程中,每一个线程都有自己的工作缓存,线程A,B从高速缓存中读取到数据i的值都为0,在进行加1操作后,写入自己的工作缓存中,再刷新到内存中。 进行两次加法操作,值却为1。
这就是缓存不一致问题。
解决缓存不一致有两个解决方法。
1.在总线加入LOCK#锁
2.使用缓存一致性协议
总线加入锁来阻塞其他线程访问时,会导致线程阻塞,使效率低下。
缓存一致性协议的核心思想是:当线程对一个共享变量进行写操作时,会发出通知,让其他的线程该变量的缓存行置为无效,其他线程操作该变量时,会重新从内存中读取数据。
二、JAVA内存模型
Java内存模型规定所有的变量都存在主存中,每个线程都有自己的工作内存(高速缓存),线程的所有操作都只能操作工作内存,不能操作主存,也不能操作其他线程的工作内存。
三、并发编程中的三个概念
1.原子性
一个操作或多个操作,全部执行不会被其他操作打断。 原子性只针对读取和写入指令。 比如
i = i + 1;
这个代码是没有原子性的,先从高速缓存中取出i的数据,然后进行加1操作,最后写入高速缓存,再刷新到内存中。
如果要保证这个操作的原子性,就可以通过synchronized和lock来实现。
2.可见性
多个线程访问同一个变量时,一个线程修改了变量的值,其他线程能立即看到修改的值。
Java提供了volatile来保证有效性。
当一个共享变量被volatile修饰时,它保证当线程修改它的值时立即被更新到主存中,其他线程读取时,从主存中读取值。
3.有序性
程序执行顺序按照代码的先后顺序执行。
处理器为了提高程序运行效率,可能会对代码执行顺序进行优化。这里可能会发生指令重排序。
比如两个赋值语句
context = init()
boolean flag = true;
处理器在执行时,可能先执行flag = true,再执行初始化。
多线程执行时,如果后面有根据flag为真,对初始化内容的操作。线程1先执行flag=true,还未初始化内容,线程2根据flag为真执行了对内容的操作,就会导致异常。
while(!flag){
sleep()
}
dosomething(context);
可以用synchronized和look保证每个时刻只有一个线程访问代码块,相当于顺序执行代码。
四、深入剖析volatile关键字
- 保证不同线程对这个变量具有可见性,即一个线程修改了某个变量的值,这个新值对其他线程是立即可见的。
- 被volatile修饰的变量禁止指令重排序
多个线程对一个volatile修饰的值进行操作时,可能会导致原子性不一致的问题。 因为volatile只保证了可见性和有序性。
对于自增代码
i++;
由于该操作不是原子性,所以可能存在一个线程从内存中取出值,之后另一个线程对该数据取值操作写入内存。因为可见性是指读操作,之前线程的值已经取出来了,不会变,之后操作得到的值就是1,而不是2。
解决方法是该自增代码加Lock或synchronized
转载于:https://my.oschina.net/u/3371784/blog/2934209
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/101300.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...