第100次提醒:++ 不是執行緒安全的
目錄
瘋狂創客圈 Java 分散式聊天室【 億級流量】實戰系列之 -17【部落格園 總入口 】
原始碼IDEA工程獲取連結:ofollow,noindex" target="_blank">Java 聊天室 實戰 原始碼
寫在前面
大家好,我是作者尼恩。
目前正在組織 瘋狂創客圈的幾個兄弟,從0開始進行高併發的100級流量(不是使用者)聊天器的實戰。
在設計客戶端之前,發現一個非常重要的基礎知識點,沒有講到。這個知識點就是Java併發包。
由於Java併發包將被頻繁使用到,所以不得不停下來,先介紹一下。
一道簡單執行緒安全題,不知道有多少人答不上來
某次面試,候選人是從重慶一所211大學畢業了一年的初級Java工程師,暫且簡稱Y君。
在尼恩面試前,Y君已經過了第一關,通過了PM同事的技術面試,PM同事甚至還反饋說Y君的繼承不錯。理論上,Y君的offer已經沒有什麼懸念了。
於是,尼恩想前面無數次面試一樣,首先開始了多執行緒方面的問題。
先上來就是砸出一個古老的面試問題:
程式為什麼要用多執行緒,單執行緒不是很好嗎?
多執行緒有什麼意義?
多執行緒會帶來哪些問題,如何解決?
++操作是執行緒安全的嗎?
乖乖,Y君的答案,令人出人意料。
答曰:“我從來沒有用過多線,不是太清楚多執行緒的意義,也不清楚多執行緒能帶來哪些問題”。
乖乖,看一看Y君的簡歷,這個又是一個埋頭幹活,被增刪改查坑害了的小兄弟!
這已經不是第一個了,我已經記不清楚,有多少面試的兄弟,搞不清楚一這些非常基礎的併發程式設計的知識。
單體WEB應用的時代,已經離我們遠去了。 微服務、非同步架構的分散式應用時代,已經全面開啟。
對於那些面試失敗的兄弟,為了提升他們的水平,尼恩都會給他提一個善意的建議。讓他們去做一個簡單的併發自增運算的實驗,看看自增運算是否執行緒安全的。
實驗:併發的自增運算
使用10條執行緒,對一個共享的變數,每條執行緒自增100萬次。看看最終的結果,是不是1000萬?
完成這個小實驗,就知道++運算是否是執行緒安全的了。
實驗程式碼如下:
/** * Created by 尼恩 at 瘋狂創客圈 */ package com.crazymakercircle.operator; import com.crazymakercircle.util.Print; /** * 不安全的自增 運算 */ public class NotSafePlus { public static final int MAX_TURN = 1000000; static class NotSafeCounter implements Runnable { publicint amount = 0; public void increase() { amount++; } @Override public void run() { int turn = 0; while (turn < MAX_TURN) { ++turn; increase(); } } } public static void main(String[] args) throws InterruptedException { NotSafeCounter counter=new NotSafeCounter(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(counter); thread.start(); } Thread.sleep(2000); Print.tcfo("理論結果:" + MAX_TURN * 10); Print.tcfo("實際結果:" + counter.amount); Print.tcfo("差距是:" + (MAX_TURN * 10 - counter.amount)); } }
執行程式,輸出的結果是:
[main|NotSafePlus:main]:理論結果:10000000 [main|NotSafePlus:main]:實際結果:9264046 [main|NotSafePlus:main]:差距是:735954
也就是說,併發執行後,總計自增1000萬次,結果少了70多萬次,差距是巨大的,在10%左右。
當然,這只是一次結果,每一次執行,差距都是不同的。大家可以動手執行體驗一下。
從結果可以看出,自增運算子不是執行緒安全的。
++ 運算的原理
自增運算子,至少包括三個JVM指令
-
從記憶體取值
-
暫存器增加1
-
存值到記憶體
這三個指令,在JVM內部,是獨立進行的,中間完全可能會出現多個執行緒併發進行。
比如:當amount=100是,有三個執行緒讀同一時間取值,讀到的都是100,增加1後結果為101,三個執行緒都存值到amount的記憶體,amount的結果是101,而不是103。
JVM內部,從記憶體取值,暫存器增加1,存值到記憶體,這三個操作自身是不可以再分的,這三個操作具備原子性,是執行緒安全的,也叫原子操作。兩個、或者兩個以上的原子操作合在一起進行,就不在具備原子性。比如先讀後寫,那麼就有可能在讀之後,這個變數被修改過,寫入後就出現了資料不一致的情況。
Java 的原子操作類
對於每一種基本型別,在java 的併發包中,提供了一組執行緒安全的原子操作類。
對於Integer型別 ,對應的原子操作類是AtomicInteger 類。
java.util.concurrent.atomic.AtomicInteger
使用 AtomicInteger類,實現上面的實驗,程式碼如下:
import java.util.concurrent.atomic.AtomicInteger; /** * 安全的 ++ 運算 */ public class SafePlus { public static final int MAX_TURN = 1000000; static class NotSafeCounter implements Runnable { public AtomicInteger amount = new AtomicInteger(0); public void increase() { amount.incrementAndGet(); } @Override public void run() { int turn = 0; while (turn < MAX_TURN) { ++turn; increase(); } } } public static void main(String[] args) throws InterruptedException { NotSafeCounter counter=new NotSafeCounter(); for (int i = 0; i < 10; i++) { Thread thread = new Thread(counter); thread.start(); } Thread.sleep(2000); Print.tcfo("理論結果:" + MAX_TURN * 10); Print.tcfo("實際結果:" + counter.amount); Print.tcfo("差距是:" + (MAX_TURN * 10 - counter.amount.get())); } }
執行程式碼,結果如下;
[main|NotSafePlus:main]:理論結果:10000000 [main|NotSafePlus:main]:實際結果:10000000 [main|NotSafePlus:main]:差距是:0
這一次,10條執行緒,累加1000w次,結果是1000w。
看起來,如果需要執行緒安全,需要使用Java併發包中的原子類。
寫在最後
下一篇:Netty 中的Future 回撥實現與執行緒池詳解。這個也是一個非常重要的基礎篇。
瘋狂創客圈 Java 死磕系列
-
Java (Netty) 聊天程式【 億級流量】實戰 開源專案實戰
- Netty 原始碼、原理、JAVA NIO 原理
- Java 面試題 一網打盡
-
瘋狂創客圈 【 部落格園 總入口 】