关键字:Java内存模型(JMM)、线程安全、可见性、原子性、有序性

1.线程安全(JMM)

多线程执行某个操作的结果跟期望的一致,那么这个操作就是线程安全。

2.Java内存模型(JMM)

(1)每条执行都是在CPU上执行,而数据保存在主存中,CPU执行速度比主存快,如果每次都从主存读写数据,这样会降低CPU执行效率,为解决这个问题,提出了高速缓存,CPU在执行指令时,将数据拷贝到高速缓存,读写都在缓存上,执行完将结果刷新给内存; (2)内存模型是共享内存系统对多线程读写操作行为的规范;规范中提供的volatile、synchronized等可以解决CPU多级缓存、CPU指令重排等导致的内存问题; 如图所示 (1)线程A、线程B、线程C分别有自己的工作内存(工作内存是对高速缓存的抽象),工作内存都拥有count变量的副本,而count变量是存在于主存当中; (2)每个线程对count变量的操作都发生工作内存中,操作完在某个时刻会将结果刷新回主存;
可见性、原子性、有序性 - 图1
未命名文件 (21).png

3.可见性

可见性,线程A对共享变量count的修改,在其他线程(线程B、线程C)立即可见,这就是可见性。 以下代码对共享变量count的修改,线程间不具有可见性。

  1. private int count;//共享变量
  2. public void setCount(int count) {//线程A执行setCount方法
  3. this.count = count;
  4. }
  5. public int getCount() {//线程B执行getCount方法
  6. return this.count;
  7. }

创建线程A对共享变量count进行赋值操作,创建线程B对共享变量count进行取值操作,理论上赋值操作执行完以后进行取值,获取到的值应该是最新,但结果并不是一定的,偶然发现count值为0。

为什么会出现这样的情况?

这是线程A的赋值操作对于线程B来不可见导致的。线程A对count进行赋值,例如赋值为10,结果存在线程A的工作内存中,没有立即更新到主内存,当线程B进行取值,主存的值没有更新还是0,所以取值为0。当然,这种情况不是必现的,但也留下不安全的因素。

如何解决?

(1)使用volatile修饰共享变量count 其他代码不变,仅给共享变量count增加volatile修饰。 volatile作用,强制将修改后的值更新到主存,如果其他线程对该值有缓存,则会失效,那么就会从主存重新获取值; 修改代码如下:
private volatile int count;//共享变量
……
(2)使用synchronized修饰setCount、getCount方法 那么setCount、getCount就变成同步方法,只有获取了锁才能进入,而且setCount方法释放锁以后,count值会立马同步在主存。
public synchronized void setCount(int count) {//线程A执行setCount方法
this.count = count;
}
public synchronized int getCount() {//线程B执行getCount方法
return this.count;
}
(3)volatile和synchronized比较 共同点: volatile和synchronized都可以保证可见性; 异同点: (1)针对以上的赋值操作,volatile比较synchronized更轻量级,毕竟synchronized会阻塞其他线程; (2)volatile修饰变量,使其内容具有可见性,不能使变量的所有操作具有可见性,所以对于i++,运算本身就不是原子性,涉及个三个操作,所以仅靠volatile是无法保证线程安全的,用synchronized显示比较合适一些。

4.原子性

原子性,即一个操作或多个操作的执行,不会被其他因素打扰,要么全部执行,要么都不执行。 注意:java对基本类型变量,简单的读写和赋值操作是原子性。
int a=0;//具有原子性
int b=a;//不具有原子性,分两步,第一步获取a的值,然后将值写入工作内存给b赋值。
以下代码具有原子性,赋值操作要么执行,要么不执行;
public void setCount(int count) {//对共享变量的赋值操作
this.count = count;
}
以下操作不具有原子性,创建1000个线程执行setCount方法, setCoun()t{thi.s.count++ }
理论最后count的值应该是1000,但是实际结果不确定。因为 thi.s.count++不是原子性操作,分三个步骤,获取this.count的值、值加一、值赋值给this.count。 可能有人注意到,volatile保证count值可见性,每个线程进行++操作,其他线程应该能看到才对。前面分析可见性,对比volatile和synchronized时有提到,volatile保证了变量内容的可见性,但不能保证操作变量的可见性。 例如,当count值为10,线程A执行setCount,获取this.count的值放入操作数栈,这时线程B抢占CPU时间片同样执行setCount方法,主存count的值还是10(因为线程A只是获取this.count的值放入操作数栈),获取this.count的值、值+1、值赋值给this.count,然后刷新回主存,线程A也知道了count的值修改成11,但是操作数栈中还是10,这时候线程A抢占CPU时间片继续执行,进行+1操作赋值给this.count,然后刷新回主存,这时候主存count的值变成了11。
可见性、原子性、有序性 - 图2
未命名文件 (22).png
private volatile int count;//共享变量
public void setCount() {
this.count++;
}

  1. public void test() {<br /> for (int i = 0; i < 1000; i++) {<br /> //创建线程执行setCount操作<br /> }<br /> }

如何解决?

使用synchronized修饰setCount方法 其他代码不变,用synchronized修改setCount方法,保证setCount方法是原子性,因为synchronized可以实现大范围操作的原子性;
……
public synchronized void setCount() {
this.count++;
}
……

5.有序性

有序性,程序按照代码编写的先后顺序执行; 以下代码,如果b赋值先于a赋值,那么代码就没有按照编写的先后顺序执行,代码被重排了。
public void set() {
int a=1;
int b=2;
}

什么是指令重排?

CPU为了提供程序执行效率,会对指令执行顺序进行重排,以上代码,b赋值可能先于a赋值执行。 但以下代码不会涉及指令重排,因为b赋值依赖于a。
public void set() { int a=1; int b=a; }

指令重排带来的问题?

单线程来说,指令重排会提高CPU执行效率,基本上没有任何问题,但对于多线程,重排会导致指令执行的不确定性。 以下代码相信大家都很熟悉,HttpManager是一个单例,提供getInstance方法获取单例,在单线程情况下,getInstance方法没有任何问题,但如果是多线程,情况就不一样了。 主要问题是 INSTANCE = new HttpManager()这行代码; INSTANCE = new HttpManager()主要分三个步骤, (1)开辟内存空间; (2)初始化对象变量; (3)INSTANCE 指向内存空间。 指令重排,导致(3)先于(2),那么这么会有什么问题呢? 线程A执行getInstance方法,判断INSTANCE 为空,获取了锁进入synchronized 代码块,执行HttpManager的实例化代码,先执行上述步骤(1),接着执行步骤(3),接着线程B执行getInstance方法,判断INSTANCE不为空(因为线程A已经实例化了INSTANCE )立马返回,在调用对象方法时发现mContext为空(因为没有执行步骤(2)),导致方法无法执行下去。
public class HttpManager { private static HttpManager INSTANCE; private Context mContext; private HttpManager(Context context) { mContext = context; } public static HttpManager getInstance(Context context) { if (INSTANCE == null) { synchronized (HttpManager.class) { if (INSTANCE == null) { INSTANCE = new HttpManager(context); } } } return INSTANCE; } }
以下代码也会因为重排序,出现问题; 线程A进入a方法判断while循环条件成立,循环执行doSomething(),线程B进入b方法,由于指令重排,语句(2)可能先于(1)执行,那么线程A判断循环条件不成立跳出循环进而执行doSomething1方法,但由于语句(1)没有执行导致context为空,那么线程A执行doSomething1方法就有可能出现异常;
private Context context; private boolean isStop; public void a() { while (!isStop) { doSomething(); } doSomething1(context); } public void b() { context = loadContext();//(1) isStop = true;//(2) }

如何解决重排序问题?

(1)用volatile 修饰变量; 例子1,使用volatile 修饰INSTANCE,这样可以防止指令重排; 例子2,使用volatile 修饰isStop,volatile机制保证语句1一定在语句2之前执行完,并对后续语句可见。 (2)用synchronized 修饰方法; 例子2,用synchronized修饰方法a、b,保证同一时刻只有一个线程获取锁进入a或b方法;
public synchronized void a() { while (!isStop) { doSomething(); } doSomething1(context); } public synchronized void b() { context = loadContext();//(1) isStop = true;//(2) }

6.volatile机制和原理

加入volatile 关键字的指令,相当于多了一个lock前缀指令,可以理解为内存屏障: (1)保证volatile指令后面的指令不会排在volatile指令的前面,前面的指令不会排到volatile指令的后面,而且保证前面指令的执行对后面指令是可见的; 以下代码,用volatile 修饰isStop ,可以保证(1)、(2)在执行(4)、(5)的时候已经执行了,结果对(4)、(5)可见; 注意,但是不能保证(1)在(2)之前执行,(4)在(5)之前执行,因为他们没有依赖关系。
private volatile boolean isStop ; public void a() { Person p=new Person();//(1) Context context = loadContext();//(2) isStop = true;//(3) p.setName(“name”);//(4) Resources resources = context.getResources();//(5) while (!isStop) { doSomething(); } doSomething1(context); }
(2)它会强制对缓存的修改刷新到主内存; (3)如果是写操作,会导致其他线程的缓存无效;

7.volatile和synchronized比较

synchronized 保证有序性、可见性、原子性,比较重量级; volatile禁止重排序、可见性,不过范围比synchronized 小,比较轻量级
总结: 解决线程安全问题,就是解决多线程情况下可见性、原子性、有序性问题。 保证可见性,使用volatile或者synchronized ; 保证原子性,使用synchronized; 保证有序性,使用volatile或者synchronized ;