第101次提醒:++ 不是線程安全的


瘋狂創客圈 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) 聊天程序【 億級流量】實戰 開源項目實戰



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM