violate与线程安全

 

1  violate
violate与线程安全 
线程可见性:
可见性是值一个线程对共享变量的修改,对于另一个线程来说是否是可以看到的。
 
为什么会出现这种问题呢?
 
我们知道,java线程通信是通过共享内存的方式进行通信的,而我们又知道,为了加快执行的速度,线程一般是不会直接操作内存的,而是操作缓存。
 
java线程内存模型:
 
violate与线程安全 
 
实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的。而如果另一个变量没有读取主内存中的新的值,而是使用旧的值的话,同样的也可以列为不可见。
 
对于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关键字,或者使用锁的机制,就能实现内存的可见性了。
 
 
 
 
 
 
代码如下:
 
/**
 * volatile 关键字,使一个变量在多个线程间可见
 * A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
 * 使用volatile关键字,会让所有线程都会读到变量的修改值
 *
 * 在下面的代码中,running是存在于堆内存的t对象中
 * 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
 * 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
 *
 * 使用volatile,将会强制所有线程都去堆内存中读取running的值
 *
 * 可以阅读这篇文章进行更深入的理解
 *
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;
 
import java.util.concurrent.TimeUnit;
 
public class T01_HelloVolatile {
/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
T01_HelloVolatile t = new T01_HelloVolatile();
 
new Thread(t::m, "t1").start();
 
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
 
t.running = false;
}
}
 
 
 
在把running的violate的去掉的时候,发现running不可见,结果如下:
violate与线程安全 
使用violate之后的结果如下:
violate与线程安全 
 
 
2DCL(Double Check Lock)
public class Singleton {
    Private  violate static Singleton instance = null;
    public  static Singleton getInstance() {
        if(null == instance) {    // 线程二检测到instance不为空
            synchronized (Singleton.class) {
                if(null == instance) {
                    instance = new Singleton();    // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
                }
            }
        }
 
        return instance;    // 后面线程二执行时将引发:对象尚未初始化错误
 
    }
}
 violate与线程安全
使用violate的原因:防止synchronized指令重排序,详见https://blog.csdn.net/zhouzhou_98/article/details/106259552
 
 
violate具有防止指令重排,但不具备原子性
/**
 * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
 * 运行下面的程序,并分析结果
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;
 
import java.util.ArrayList;
import java.util.List;
 
public class T04_VolatileNotSync {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) count++;
}
public static void main(String[] args) {
T04_VolatileNotSync t = new T04_VolatileNotSync();
 
List<Thread> threads = new ArrayList<Thread>();
 
for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}
 
threads.forEach((o)->o.start());
 
threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
 
System.out.println(t.count);
 
 
}
}
 
 
结果并没有等于10万
violate与线程安全 
原因violate不具备原子性,存在多个线程使用一样的值进行++操作,解决办法使用synchronized
 
 
重新修改的代码:
/**
 * 对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性
 * @author mashibing
 */
package com.mashibing.juc.c_012_Volatile;
 
import java.util.ArrayList;
import java.util.List;
 
 
public class T05_VolatileVsSync {
/*volatile*/ int count = 0;
 
synchronized void m() {
for (int i = 0; i < 10000; i++)
count++;
}
 
public static void main(String[] args) {
T05_VolatileVsSync t = new T05_VolatileVsSync();
 
List<Thread> threads = new ArrayList<Thread>();
 
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
 
threads.forEach((o) -> o.start());
 
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
 
System.out.println(t.count);
 
}
 
}
 
 
 
2 CAS
violate与线程安全 
/**
 * 解决同样的问题的更高效的方法,使用AtomXXX类
 * AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
 * @author mashibing
 */
package com.mashibing.juc.c_018_00_AtomicXXX;
 
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
 
 
public class T01_AtomicInteger {
/*volatile*/ //int count1 = 0;
AtomicInteger count = new AtomicInteger(0);
 
/*synchronized*/ void m() {
for (int i = 0; i < 10000; i++)
//if count1.get() < 1000
count.incrementAndGet(); //count1++
}
 
public static void main(String[] args) {
T01_AtomicInteger t = new T01_AtomicInteger();
 
List<Thread> threads = new ArrayList<Thread>();
 
for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
 
threads.forEach((o) -> o.start());
 
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
 
System.out.println(t.count);
 
}
 
}
 
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问题目前采用的策略。
 
经典的CAS问题:
小明在提款机,提取了50元,因为提款机问题,
有两个线程,同时把余额从100变为50
线程1(提款机):获取当前值100,期望更新为50,
线程2(提款机):获取当前值100,期望更新为50,
线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50
 
线程3(默认):获取当前值50,期望更新为100,
 
这时候线程3成功执行,余额变为100,
 
线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!
 
此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)
这就是ABA问题带来的成功提交。
 
 
violate与线程安全