1. 程式人生 > >第100次提醒:++ 不是執行緒安全的

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