- GETSTATIC 將靜態變量 val壓入棧中;
- ICONST_1 將常量1壓入棧中;
- IADD 執行加(+)運算操作;
- PUTSTATIC 將結果放回 val變量 。

文章插圖
可以看到執行 +1 這個操作其實是在獨立棧內進行,不同線程其實有不同的操作棧 。
當同樣執行到 PUTSTATIC 時,也不會考慮線程(1)情況 直接把自己運算結果寫進 val 。這樣也就出現了并發問題,并非我們想象的多線程執行都能改變val的值 。

文章插圖
怎么解決這種并發問題?設計初衷上說val+1操作的邏輯時希望在讀取val值上進行+1的操作,而非在+1過程中初始val值由于其他線程操作而改變 。因此在計算機指令上就給到了一個指令 cmpxchg,在將棧里面值交換到堆里面val時,比較val初始值么沒有變化執行成,否則執行失敗 。如果指令執行失敗了,我們再重新進行新val值的計算直到完成一次成功操作 。這也就是 解決Java并發一個基本算法 CAS(Compare-and-Swap) 。
CAS算法有三個操作數,通過內存中的值(V)、預期原始值(A)、修改后的新值 。
如果內存中的值和預期原始值相等,就將修改后的新值保存到內存中 。如果內存中的值和預期原始值不相等,說明共享數據已經被修改,放棄已經所做的操作,然后重新執行剛才的操作,直到重試成功 。Java中Unsafe 中的getAndAddInt就是使用的這個算法,不妨詳細解讀下其代碼 。

文章插圖
到這里還涉及到一個線程變量修改同步問題,由于計算機結構復雜性,CPU、Mem等各級緩存特性、不同操作系統、不同廠商硬件等等,其中有著很多緩存/同步設計;為了屏蔽這些復雜性,java提供了volatile 關鍵字來進行保證 。截取一段The Java Language Specification (Java SE 10 Edition)原文:

文章插圖
抓重點的理解:字段被聲明為volatile,在這種情況下,Java內存模型確保所有線程都看到變量的一致值 。
試一試,多線程性能更好?按照前面解決的思路,修改下之前的代碼進行測試下 。另外將耗時也記錄一下:

文章插圖
是不是發現,val 的數值已經和單線程的一致了都是 1000,沒有并發問題了 。性能上從這個例子可以看到,單線程耗時6ms,多線程耗時29ms 。不用質疑結果是沒錯的,明顯多線程耗時更高 。
可以看出多線程運行簡單程序并不一定能夠提升性能,因為其開啟線程有相關的開銷;同時看到其 復雜性高、維護成本高、可讀性降低 等缺陷 。對于簡單業務邏輯場景,不建議用多線程 。
在此基礎上,加上模擬下相關業務邏輯,模擬邏輯執行doSomeThings(),模擬實現邏輯就是線程休眠 1ms 。相關代碼,耗時記錄如下:

文章插圖
這個例子里面 多線程性能優勢,與單線程的1914ms 相比多線程只需要 262ms 。當然具體提升的數值和運行的機器、CPU等等有關系,筆者電腦是 4核8線程的情況 。
本篇總結下,介紹了進程、線程以及相關發展史;展示了一個具體的并發問題;詳細分析了并發問題的發生原因以及解決辦法 。最后對多線程并發程序進行了驗證,以及相關性能上的探究 。
經驗總結擴展閱讀
- 從緩存入門到并發編程三要素詳解 Java中 volatile 、final 等關鍵字解析案例
- 學習ASP.NET Core Blazor編程系列五——列表頁面
- centos7中配置java + mysql +jdk+使用jar部署項目
- Java實現6種常見排序
- 數據結構與算法【Java】08---樹結構的實際應用
- 學習ASP.NET Core Blazor編程系列四——遷移
- 二 Java之POI導出Excel:多個sheet
- 15 JavaObject類
- 【設計模式】Java設計模式 - 命令模式
- Redis高并發分布式鎖詳解
