violate与线程安全
1 violate
线程可见性:
可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
为什么会出现这种问题呢?
我们知道,java线程通信是通过共享内存的方式进行通信的,而我们又知道,为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。
java线程内存模型:
实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。
对于jvm来说,主内存是所有线程共享的java堆,而工作内存中的共享变量的副本是从主内存拷贝过去的,是线程私有的局部变量,位于java栈中。
那么我们怎么知道什么时候工作内存的变量会刷写到主内存当中呢?
这就涉及到java的happens-before关系了。
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。
简单来说,只要满足了happens-before关系,那么他们就是可见的。
例如:
线程A中执行i=1,线程B中执行j=i。如果线程A的操作和线程B的操作满足happens-before关系,那么j就一定等于1,否则j的值就是不确定的。
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()方法的开始;
从上面的happens-before规则,显然,一般只需要使用volatile关键字,或者使用锁的机制,就能实现内存的可见性了。
代码如下:
在把running的violate的去掉的时候,发现running不可见,结果如下:
使用violate之后的结果如下:
2DCL(Double Check Lock)
使用violate的原因:防止synchronized指令重排序,详见https://blog.csdn.net/zhouzhou_98/article/details/106259552
violate具有防止指令重排,但不具备原子性
2 CAS
AtomicXXX的原理是基于CAS,CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术。CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。(也称作乐观锁)
CAS最大的问题在于ABA问题:
ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。
关于ABA问题我想了一个例子:在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。解决这个问题的方案的一个策略是每一次倒水假设有一个自动记录仪记录下,这样主人回来就可以分辨在她离开后是否发生过重新倒满的情况。这也是解决ABA问题目前采用的策略。
|