java如何判断原子性,Java-可见性、原子性、有序性

关键字: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变量的操作都发生工作内存中,操作完在某个时刻会将结果刷新回主存;

07b35103822f

未命名文件 (21).png

3.可见性

可见性,线程A对共享变量count的修改,在其他线程(线程B、线程C)立即可见,这就是可见性。

以下代码对共享变量count的修改,线程间不具有可见性。

private int count;//共享变量

public void setCount(int count) {//线程A执行setCount方法

this.count = count;

}

public int getCount() {//线程B执行getCount方法

return this.count;

}

创建线程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方法,理论最后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。

07b35103822f

未命名文件 (22).png

private volatile int count;//共享变量

public void setCount() {

this.count++;

}

public void test() {

for (int i = 0; i < 1000; i++) {

//创建线程执行setCount操作

}

}

如何解决?

使用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 ;

以上分析有不对的地方,请指出,互相学习,谢谢哦!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值