【漫画】JAVA并发编程 怎么处理可见性和有序性问题

原创声明:本文来自大众号【胖滚猪学编程】,以漫画方式让编程so easy and interesting,转载请注明出处!

在上一篇文章并发编程三大源头中,咱们初识了并发编程的三个bug源头:可见性、原子性、有序性。了解了它们终究为什么会发作,那么今日咱们就来聊聊怎么处理这三个问题吧。

前奏

【漫画】JAVA并发编程 怎么处理可见性和有序性问题

Happens-Before是什么?

【漫画】JAVA并发编程 怎么处理可见性和有序性问题

A Happens-Before B 意味着 A 事情对 B 事情来说是可见的,不管 A 事情和 B 事情是否发作在同一个线程。例如 A 事情发作在线程 1 上,B 事情发作在线程 2 上,Happens-Before 规矩确保线程 2 上也能看到 A 事情的发作。

【漫画】JAVA并发编程 怎么处理可见性和有序性问题

Happens-Before的效果

原创声明:本文来自大众号【胖滚猪学编程】,以漫画方式让编程so easy and interesting,转载请注明出处!

happens-before准则非常重要,它是判别线程是否安全的主要依据,依托这个准则,咱们就能处理在并发环境下可见性和有序性问题。
比方某天老板问你“胖滚猪,我这段并发代码会有线程安全问题吗”,那么你能够对照着happens-before准则一个个看,要是契合其中之一并且是原子性的,你就能够大声告知老板“没得问题!”
比方这段代码:

i = 1;       //线程A履行
j = i ;      //线程B履行

j 是否等于1呢?假定线程A的操作(i = 1)happens-before线程B的操作(j = i),那么能够确认线程B履行后j = 1 必定建立,假如他们不存在happens-before准则,那么j = 1 不必定建立。
这便是happens-before准则的威力!让咱们走进它的国际吧!

Happens-Before八大准则 处理原子性和有序性问题

【漫画】JAVA并发编程 怎么处理可见性和有序性问题

规矩一:程序的次序性规矩

这条规矩是指在一个线程中,依照程序次序,前面的操作 Happens-Before 于后续的恣意操作。这规矩挺好了解的,毕竟是在一个线程中呐。
你会觉得这是个废物规矩。其实这个规矩是一个根底规矩,happens-before 是多线程的规矩,所以要和其他规矩束缚在一起才干体现出它的次序性,别着急,持续向下看。

规矩二: Volatile变量规矩

这条规矩是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。咱们在上篇文章说过,由于缓存的原因,每个线程有自己的作业内存,假如同享变量没有及时刷到主内存中,那就会导致可见性问题,线程B没有及时读到线程A的写。可是只需加上Volatile,就能够防止这个问题,相当于volatile的效果是对变量的修正会绕过高速缓存马上刷新到主存。不过要注意一下,volatile除了确保可用性,它还能够制止指定重排序哦!

public class TestVolatile1 {
private volatile static int count = 0;
public static void main(String[] args) throws Exception {
final TestVolatile1 test = new TestVolatile1();
Thread th1 = new Thread(() -> {
count = 10;
});
Thread th2 = new Thread(() -> {
//没有volatile润饰count的话极小概率会呈现等于0的状况
System.out.println("count=" + count);
});
// 发动两个线程
th1.start();
th2.start();
}
}

规矩三: 传递性规矩

这条规矩是指假如 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。这也很好了解。咱们举个比如,writer和reader是两个不同的线程,它们有如下操作:

  int x = 0;
volatile boolean v = false;
public void writer() {
x = 42; //(1)
v = true; //(2)
}
public void reader() {
if (v == true) { //(3)
// 这儿 x 会是多少呢?(4)
}
}

这个比如和上面那个Volatile的比如有个差异便是,有两个变量。那么咱们来剖析一下:
(1)和(2)在同一个线程中,依据规矩1,(1)Happens-Before于(2)
(3)和(4)在同一个线程中,同理,(3)Happens-Before于(4)
依据规矩2,由于v用了volatile润饰,那么(2)必定 Happens-Before于(3)。
那么依据传递性规矩可得:(1)Happens-Before于(4),因而x必定为42。
所以即便x没有用volatile,它也是能够确保可见性的!所以为啥刚刚说规矩1要和其他规矩联合起来看才有意思,现在你知道了吧!

规矩四: 管程中的锁规矩

指管程中的解锁必定发作在随后的加锁之前。管程是一种通用的同步原语,synchronized 是 Java 里对管程的完结。管程中的锁在 Java 里是隐式完结的,例如下面的代码,在进入同步块之前,会主动加锁,而在代码块履行完会主动开释锁,加锁以及开释锁都是编译器帮咱们完结的。

synchronized (this) { // 此处主动加锁
if (this.x < 10) {//临界区
}
} // 此处主动解锁

这个规矩比较好了解,不管是在单线程环境仍是多线程环境,一个锁处于被确定状况,那么有必要先履行unlock操作后边才干进行lock操作。
【漫画】JAVA并发编程 怎么处理可见性和有序性问题

规矩五: 线程发动规矩

主线程 A 发动子线程 B 后(线程 A 调用线程 B 的 start() 办法),子线程 B 能够看到主线程在发动子线程 B 前的操作。

private static long count = 0;
public static void main(String[] args) throws InterruptedException {
Thread B = new Thread(() -> {
// 主线程调用 B.start() 之前 一切对同享变量的修正,此处皆可见
// 因而count肯定为10
System.out.println(count);
});
// 此处对同享变量count修正
count = 10;
// 主线程发动子线程
B.start();
}

规矩六: 线程停止规矩

主线程 A 等候子线程 B 完结(主线程 A 经过调用子线程 B 的 join() 办法完结),假如在线程 A 中,调用线程 B 的 join() 并成功回来,那么主线程能够看到子线程的操作(指同享变量的操作),换句话说便是线程 B 中的恣意操作 Happens-Before 于该 join() 操作的回来。

private static long count = 0;
public static void main(String[] args) throws InterruptedException {
Thread B = new Thread(() -> {
// 主线程调用 B.start() 之前 一切对同享变量的修正,此处皆可见
// 因而count肯定为10
count = 10;
});
// 主线程发动子线程
B.start();
// 主线程等候子线程完结
B.join();
// 子线程一切对同享变量的修正 在主线程调用 B.join() 之后皆可见
System.out.println(count);//count必定为10
}

规矩七:线程中止规矩

对线程interrupt()办法的调用先行发作于被中止线程的代码检测到中止事情的发作。即线程A调用线程B的interrupt()办法,happens-before于线程A发现B被A中止(经过Thread.interrupted()办法检测到是否有中止发作)。

private static long acount = 0;
private static long bcount = 0;
public static void main(String[] args) throws InterruptedException {
Thread B = new Thread(() -> {
bcount = 7;
System.out.println("Thread A被中止前bcount="+bcount+" acount="+acount);
while (true){
if (Thread.currentThread().isInterrupted()){
bcount = 77;
System.out.println("Thread A被中止后bcount="+bcount+" acount="+acount);
return;
}
}
});
B.start();
Thread A = new Thread(() -> {
acount = 10;
System.out.println("Thread B 中止A前bcount="+bcount+" acount="+acount);
B.interrupt();
acount = 100;
System.out.println("Thread B 中止A后bcount="+bcount+" acount="+acount);
});
A.start();
}

规矩八:目标规矩

一个目标的初始化完结(结构函数履行完毕,一般都是用new初始化)happen—before它的finalize()办法的开端。finalize()是在java.lang.Object里界说的,即每一个目标都有这么个办法。这个办法在该目标被收回的时分被调用。该条准则着重的是多线程状况下目标初始化的成果有必要对发作于这以后的目标毁掉办法可见。

    public HappensBefore8(){
System.out.println("结构办法");
}
@Override
protected void finalize() throws Throwable {
System.out.println("目标毁掉");
}
public static void main(String[] args){
new HappensBefore8();
System.gc();
}

关于有序性的那些疑问

原创声明:本文来自大众号【胖滚猪学编程】,以漫画方式让编程so easy and interesting,转载请注明出处!

【漫画】JAVA并发编程 怎么处理可见性和有序性问题

扩展有序性的概念:Java内存模型中的程序天然有序性能够总结为一句话,假如在本线程内调查,一切操作都是有序的;假如在一个线程中调查另一个线程,一切操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“作业内存主主内存同步推迟”现象。 这其实还涉及到一个高频面试考点:as-if-serial语义

as-if-serial语义:不管怎么重排序,单线程程序的履行成果不能被改动。编译器、runtime和处理器都有必要恪守as-if-serial语义。所以编译器和处理器不会对存在数据依靠联系的操作做重排序,由于这种重排序会改动履行成果。可是,假如操作之间不存在数据依靠联系,这些操作就可能被编译器和处理器重排序。

划要点:单线程中确保依照次序履行。
synchronized同一时间只要一个线程在运转,也就相当于确保了有序性。至于这个两层查看事例,出问题,并不是由于synchronized没有确保有序性。而是指令重排导致了在多个线程中无序。

总结

【漫画】JAVA并发编程 怎么处理可见性和有序性问题

原创声明:本文来自大众号【胖滚猪学编程】,以漫画方式让编程so easy and interesting,转载请注明出处!