瘋狂創客圈 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 面試題 一網打盡
- 瘋狂創客圈 【 博客園 總入口 】