原子性 可见性 有序性

简介

并发安全的三个要素, 原子性/可见性/有序性。

原子性

即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。

现实问题

银行转账:A给B转1000元, 那么存在两个操作:从A账户减去1000元,在B账户加上1000元。如果第一个操作完成之后终止了, 那么就会导致B账户金额没有增加。 所以要保证两个操作要么都执行, 要么都不执行。

程序问题

假设赋值过程不具备原子性,变量赋值包括两个过程:为低16位赋值,为高16位赋值

i = 9;

如果低16位赋完值,突然被中断,此时恰好有其它线程去读i值,将会读取错误数据。

Java中的原子性
j基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
看下面一个例子i:
哪些操作是原子性操作:

x = 10;        //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4

1是原子性的, 执行这个语句的会直接将数值10写入到工作内存中。
2不是原子性的,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存,这2个操作都是原子性操作,但是合起来就不是原子性操作了。同理可推3 、4。

可见性
一个线程修改了变量的值, 其它的线程能够立即看到新值。

代码示例

//线程A执行的代码
int i = 0;
i = 10;

//线程B执行的代码
j = i;

AB两个线程执行上面的代码, 就有可能A线程执行 i = 10的时候,只在自己的工作内存中更新了, 没有更新到主内寸, 此时切换B线程执行看到的i还是0。

Java中的可见性
java中的一个关键子volatile用来保证可见性。volatile修饰的变量, 线程更新之后会直接刷新的主内存。

synchronized和Lock也就有可见性的性质,synchronized和Lock能保证了同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

有序性

即程序执行的顺序按照代码的先后顺序执行。

实例
举个简单的例子,看下面这段代码:

int i = 0;            //1
boolean b = false;    //2
i = 1;                //3
b = true;             //4

上面代码的顺序是1234, 但是jvm执行的顺序一定是1234吗? 不一定, 可能会发生指令重排(Instruction Reorder)。
1 2 两条语句谁先谁后并不形象执行结果, 那么就有可能处理器为了提高程序运行效率,可能会对输入代码进行优化,让2语句优先于1执行。
指令重排需要先决条件,就是保证程序最终执行结果和代码顺序执行的结果是一致的
那么如何能保证指令重排不影像执行结果呢?

int a = 10;    //1
int b = 2;    //2
a = a + 3;    //3
b = a * a;     //4

这段代码可能的执行顺序 2 1 3 4
那么有没有可能执行顺序 2 1 4 3

不会,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,4 依赖 3的执行结果,那么处理器会保证 3 会在 4 之前执行。

虽然重排序不会影响单个线程内程序执行的结果,如果多线程呢?

//线程A:
context = loadContext();   //1
inited = true;             //2

 //线程B:
while(!inited ){
   sleep()
}
doSomethingwithconfig(context);

1 2 可能会被重排序。假如发生了重排序,执行顺序为 2 1, 线程B会以为初始化工作已经完成,跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。可以看出指令重排可能会对多线程协作产生影像。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

Java中的有序性
Java中也存在指令重排, 但是不会影响单个线程的执行结果, 却会影响到多线程并发执行的正确性。

java中的关键字volatile关键字会限制指令重排,当然可以做到有序
synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

java天生就有一些有序性, 不通过处理就能保证有序性, 我们称之为happens-before 原则,如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作

  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作

  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C

  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生

  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行

  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

第一条:就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,但是虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

第二条:就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

第三条:如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

第四条:实际上就是体现happens-before原则具备传递性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值