第100次提醒:++ 不是執行緒安全的
目錄
瘋狂創客圈 Java 分散式聊天室【 億級流量】實戰系列之 -17【 部落格園 總入口 】
原始碼IDEA工程獲取連結: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 {
public int 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 面試題 一網打盡
瘋狂創客圈 【 部落格園 總入口 】