詭異并發(fā)三大惡人之有序性
上一節(jié)阿粉我和大家一起打到了并發(fā)中的惡人可見性和原子性,這一節(jié)我們繼續(xù)討伐三惡之一的有序性。
序、有序性的闡述
有序性為什么要探討?因為 Java 是面向對象編程的,關注的只是最終結果,很少去研究其具體執(zhí)行過程?正如上一篇文章在介紹可見性時描述的一樣,操作系統(tǒng)為了提升性能,將 Java 語言轉換成機器語言的時候,吩咐編譯器對語句的執(zhí)行順序進行了一定的修改,以促使系統(tǒng)性能達到最優(yōu)。所以在很多情況下,訪問一個程序變量(對象實例字段,類靜態(tài)字段和數(shù)組元素)可能會使用不同的順序執(zhí)行,而不是程序語義所指定的順序執(zhí)行。
正如大家所熟知那樣,Java語言是運行在 Java 自帶的 JVM(Java Virtual Machine) 環(huán)境中,在JVM環(huán)境中源代碼(.class)的執(zhí)行順序與程序的執(zhí)行順序(runtime)不一致,或者程序執(zhí)行順序與編譯器執(zhí)行順序不一致的情況下,我們就稱程序執(zhí)行過程中發(fā)生了重排序。
而編譯器的這種修改是自以為能保證最終運行結果!因為在單核時代完全沒問題;但是隨著多核時代的到來,多線程的環(huán)境下,這種優(yōu)化碰上線程切換就大大的增加了事故的出現(xiàn)幾率!
好心辦了壞事!
也就是說,有序性 指的是在代碼順序結構中,我們可以直觀的指定代碼的執(zhí)行順序, 即從上到下按序執(zhí)行。但編譯器和CPU處理器會根據(jù)自己的決策,對代碼的執(zhí)行順序進行重新排序。優(yōu)化指令的執(zhí)行順序,提升程序的性能和執(zhí)行速度,使語句執(zhí)行順序發(fā)生改變,出現(xiàn)重排序,但最終結果看起來沒什么變化(單核)。
有序性問題 指的是在多線程環(huán)境下(多核),由于執(zhí)行語句重排序后,重排序的這一部分沒有一起執(zhí)行完,就切換到了其它線程,導致的結果與預期不符的問題。這就是編譯器的編譯優(yōu)化給并發(fā)編程帶來的程序有序性問題。
用圖示就是:
阿粉小結:編譯優(yōu)化最終導致了有序性問題。
一、導致有序性的原因:
如果一個線程寫入值到字段 a,然后寫入值到字段 b ,而且b的值不依賴于 a 的值,那么,處理器就能夠自由的調(diào)整它們的執(zhí)行順序,而且緩沖區(qū)能夠在 a 之前刷新b的值到主內(nèi)存。此時就可能會出現(xiàn)有序性問題。
例子:
- 1import java.time.LocalDateTime;
- 2
- 3/**
- 4 * @author :mmzsblog
- 5 * @description:并發(fā)中的有序性問題
- 6 * @date :2020年2月26日 15:22:05
- 7 */
- 8public class OrderlyDemo {
- 9
- 10 static int value = 1;
- 11 private static boolean flag = false;
- 12
- 13 public static void main(String[] args) throws InterruptedException {
- 14 for (int i = 0; i < 199; i++) {
- 15 value = 1;
- 16 flag = false;
- 17 Thread thread1 = new DisplayThread();
- 18 Thread thread2 = new CountThread();
- 19 thread1.start();
- 20 thread2.start();
- 21 System.out.println("=========================================================");
- 22 Thread.sleep(6000);
- 23 }
- 24 }
- 25
- 26 static class DisplayThread extends Thread {
- 27 @Override
- 28 public void run() {
- 29 System.out.println(Thread.currentThread().getName() + " DisplayThread begin, time:" + LocalDateTime.now());
- 30 value = 1024;
- 31 System.out.println(Thread.currentThread().getName() + " change flag, time:" + LocalDateTime.now());
- 32 flag = true;
- 33 System.out.println(Thread.currentThread().getName() + " DisplayThread end, time:" + LocalDateTime.now());
- 34 }
- 35 }
- 36
- 37 static class CountThread extends Thread {
- 38 @Override
- 39 public void run() {
- 40 if (flag) {
- 41 System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now());
- 42 System.out.println(Thread.currentThread().getName() + " CountThread flag is true, time:" + LocalDateTime.now());
- 43 } else {
- 44 System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now());
- 45 System.out.println(Thread.currentThread().getName() + " CountThread flag is false, time:" + LocalDateTime.now());
- 46 }
- 47 }
- 48 }
- 49}
運行結果:
從打印的可以看出:在 DisplayThread 線程執(zhí)行的時候肯定是發(fā)生了重排序,導致先為 flag 賦值,然后切換到 CountThread 線程,這才出現(xiàn)了打印的 value 值是1,falg 值是 true 的情況,再為 value 賦值;不過出現(xiàn)這種情況的原因就是這兩個賦值語句之間沒有聯(lián)系,所以編譯器在進行代碼編譯的時候就可能進行指令重排序。
用圖示,則為:
二、如何解決有序性
2.1、volatile
volatile 的底層是使用內(nèi)存屏障來保證有序性的(讓一個 CPU 緩存中的狀態(tài)(變量)對其他 CPU 緩存可見的一種技術)。
volatile 變量有條規(guī)則是指對一個 volatile 變量的寫操作, Happens-Before于后續(xù)對這個 volatile 變量的讀操作。并且這個規(guī)則具有傳遞性,也就是說:
此時,我們定義變量 flag 時使用 volatile 關鍵字修飾,如:
- 1 private static volatile boolean flag = false;
此時,變量的含義是這樣子的:
也就是說,只要讀取到 flag=true; 就能讀取到 value=1024;否則就是讀取到flag=false; 和 value=1 的還沒被修改過的初始狀態(tài);
但也有可能會出現(xiàn)線程切換帶來的原子性問題,就是讀取到 flag=false; 而value=1024 的情況;看過上一篇講述[原子性]()的文章的小伙伴,可能就立馬明白了,這是線程切換導致的。
2.2、加鎖
此處我們直接采用Java語言內(nèi)置的關鍵字 synchronized,為可能會重排序的部分加鎖,讓其在宏觀上或者說執(zhí)行結果上看起來沒有發(fā)生重排序。
代碼修改也很簡單,只需用 synchronized 關鍵字修飾 run 方法即可,代碼如下:
- 1 public synchronized void run() {
- 2 value = 1024;
- 3 flag = true;
- 4 }
同理,既然是加鎖,當然也可以使用 Lock 加鎖,但 Lock 必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖現(xiàn)象。這點在使用的時候一定要注意!
使用該種方式加鎖也很簡單,代碼如下:
- 1 readWriteLock.writeLock().lock();
- 2 try {
- 3 value = 1024;
- 4 flag = true;
- 5 } finally {
- 6 readWriteLock.writeLock().unlock();
- 7 }
好了,以上內(nèi)容就是我對并發(fā)中的有序性的一點理解與總結了,通過這三篇文章我們也就大致掌握了并發(fā)中常見的可見性、有序性、原子性問題以及它們常見的解決方案。
最后
阿粉簡單總結下三篇文章文章中使用的解決方案之間的區(qū)別:
References
[1]: https://juejin.im/post/5d52abd1e51d4561e6237124
[2]: https://juejin.im/post/5d89fd1bf265da03e71b3605
[3]: https://www.cnblogs.com/54chensongxia/p/12120117.html
[4]: http://ifeve.com/jmm-faq-reordering/