一、Java内存模型
比如有一个类:
public class HelloWorld{
private int data = 0;
public void increment(){
data++;
}
}
HelloWorld helloWorld = new HelloWorld();//对象放在堆内存中,包含实例变量
假设有2个线程,去调用同一个helloWorld对象的increment方法:
//线程1
new Thread(){
public void run(){
helloWorld.increment();
}
}.start();
//线程2
new Thread(){
public void run(){
helloWorld.increment();
}
}.start();
按理来说,两个线程执行完之后,data的值应该变成2,但是实际情况data很可能不为2,出现了并发执行的问题。
要找出这个问题的原因,要从Java内存模型的角度来分析两个线程执行时的Java内存情况:
1、假设data变量存放在主内存中;
2、每个线程在运行时都会有自己私有的线程工作内存,对应CPU级别的缓存;
3、2个线程对data去进行+1,都要先进行read,拿到data的值;
4、随后要把拿到的值load到线程的工作内存中;
5、线程在工作内存中use和assign完之后,把修改后的值store出来;
6、线程最后才把修改后的data值写回主内存中。
从以上的步骤很明显可以看出,由于两个线程在并发地执行,所以竞争cpu时间片的过程中很可能发生问题:如两个线程同时read出data的值为0。
二、内存模型中的原子性、可见性、有序性
(1)可见性
如上图中,主内存中的data值为0,线程1和线程2 read到的值都为0,当线程1先执行完increment操作,把data值改为1并写回到了主内存中,线程2能立马得到data=1的信息,则为具备可见性;
(2)原子性
如上图中,如果线程1在对data进行加1的过程中,线程2无法同时进行操作,则具备原子性;
(3)有序性
如以下一段代码:
flag = false;
//线程1
prepare();//准备资源
flag = true;
//线程2
while(!flag){
thread.sleep(1000);
}
execute();//执行操作
编译器和指令器,有时候为了提高代码执行效率,会将指令重排序,比如将flag=true排到prepare()之前去执行了。
如果具备有序性,则不会发生指令重排。
三、volatile关键字的原理
用来解决可见性和有序性,不能保证原子性;
如上图中的data,如果给data加上volatile,变成
private volatile int data = 0;
data就具备了可见性,当线程1和线程2同时把data=0的值加载到工作内存中,假设线程1先执行完了,将data=1写回主内存,此时线程2能同时得到data被改为了1的信息,失效掉之前data=0的信息,从而保证了并发操作的安全性。