線程安全性是咱們在進行 Java 併發編程的時候必需要先考慮清楚的一個問題。這個類在單線程環境下是沒有問題的,那麼咱們就能確保它在多線程併發的狀況下表現出正確的行爲嗎?html
我這我的,在沒有副業以前,一心撲在工做上面,因此處理的蠻駕輕就熟,心態也一直保持的不錯;但有了副業以後,心態就變得像坐過山車同樣。副業收入超過主業的時候,人特別亢奮,像打了雞血同樣;副業遲遲打不開局面的時候,人就變得惶惶不可終日。java
彷彿我就只能是個單線程,副業和主業並行開啓多線程模式的時候,我就變得特別沒有安全感,儘管總體的收入比沒有副業以前有了很大的改善。編程
怎麼讓我本身變得有安全感,我還沒想清楚(你要是有好的方法,請必定要告訴我)。但怎麼讓一個類在多線程的環境下是安全的,有 3 條法則,讓我來告訴你:安全
一、不在線程之間共享狀態變量。
二、將狀態變量改成不可變。
三、訪問狀態變量時使用同步。微信
那你可能要問,狀態變量是什麼?多線程
咱們先來看一個沒有狀態變量的類吧,代碼示例以下。併發
class Chenmo { public void write() { System.out.println("我尋了半生的春天,你一笑即是了。"); } }
Chenmo 這個類就是無狀態變量的,它只有一個方法,既沒有成員變量,也沒有類變量。任何訪問它的線程都不會影響另一個線程的結果,由於兩個線程之間沒有共享任何的狀態變量。因此能夠下這樣一個結論:無狀態變量的類必定是線程安全的。性能
而後咱們再來看一個有狀態變量的類。假設沉默(Chenmo 類)每寫一行字(write()
方法),就要作一次統計,這樣好找出版社索要稿費。咱們爲 Chenmo 類增長一個統計的字段,代碼示例以下。this
class Chenmo { private long count = 0; public void write() { System.out.println("我尋了半生的春天,你一笑即是了。"); count++; } }
Chenmo 類在單線程環境下是能夠準確統計出行數的,但多線程的環境下就不行了。由於遞增運算 count++
能夠拆分爲三個操做:讀取 count,將 count 加 1,將計算結果賦值給 count。多線程的時候,這三個操做發生的時序多是混亂的,最終統計出來的 count 值就會比預期的值小。atom
PS:具體的緣由能夠回顧上一節《Java 併發編程(一):摩拳擦掌》。
寫做不易,咱不能虧待了沉默,對不對?那就想點辦法吧。
假定線程 A 正在修改 count 變量,這時候就要防止線程 B 或者線程 C 使用這個變量,從而保證線程 B 或者線程 C 在使用 count 的時候是線程 A 修改事後的狀態。
怎麼防止呢?能夠在 write()
方法上加一個 synchronized
關鍵字。代碼示例以下。
class Chenmo { private long count = 0; public synchronized void write() { System.out.println("我尋了半生的春天,你一笑即是了。"); count++; } }
關鍵字 synchronized
是一種最簡單的同步機制,能夠確保同一時刻只有一個線程能夠執行 write()
,也就保證了 count++
在多線程環境下是安全的。
在編寫併發應用程序時,咱們必需要保持一種正確的觀念,那就是——首先要確保代碼可以正確運行,而後再是如何提升代碼的性能。
但衆所周知,synchronized
的代價是昂貴的,多個線程之間訪問 write()
方法是互斥的,線程 B 訪問的時候必需要等待線程 A 訪問結束,這沒法體現出多線程的核心價值。
java.util.concurrent.atomic.AtomicInteger
是一個提供原子操做的 Integer 類,它提供的加減操做是線程安全的。因而咱們能夠這樣修改 Chenmo 類,代碼示例以下。
class Chenmo { private AtomicInteger count = new AtomicInteger(0); public void write() { System.out.println("我尋了半生的春天,你一笑即是了。"); count.incrementAndGet(); } }
write()
方法再也不須要 synchronized
關鍵字保持同步,因而多線程之間就再也不須要以互斥的方式來調用該方法,能夠在必定程度上提高統計的效率。
某一天,出版社統計稿費的形式變了,不只要統計行數,還要統計字數,因而 Chenmo 類就須要再增長一個成員變量了。代碼示例以下。
class Chenmo { private AtomicInteger lineCount = new AtomicInteger(0); private AtomicInteger wordCount = new AtomicInteger(0); public void write() { String words = "我這一生,走過許多地方的路,行過許多地方的橋,看過許屢次的雲,喝過許多種類的酒,卻只愛過一個正當年齡的人。"; System.out.println(words); lineCount.incrementAndGet(); wordCount.addAndGet(words.length()); } }
你以爲這段代碼是線程安全的嗎?
結果顯而易見,這段代碼不是線程安全的。由於 lineCount 和 wordCount 是兩個變量,儘管它們各自是線程安全的,但線程 A 進行 lineCount 加 1 的時候,並不可以保證線程 B 是在線程 A 執行完 wordCount 統計後開始 lineCount 加 1 的。
該怎麼辦呢?方法也很簡單,代碼示例以下。
class Chenmo { private int lineCount = 0; private int wordCount = 0; public void write() { String words = "我這一生,走過許多地方的路,行過許多地方的橋,看過許屢次的雲,喝過許多種類的酒,卻只愛過一個正當年齡的人。"; System.out.println(words); synchronized (this) { lineCount++; wordCount++; } } }
對行數統計(lineCount++)和字數統計(wordCount++)的代碼進行加鎖,保證這兩行代碼是原子性的。也就是說,線程 B 在進行統計的時候,必需要等待線程 A 統計完以後再開始。
synchronized (lock) {...}
是 Java 提供的一種簡單的內置鎖機制,用於保證代碼塊的原子性。線程在進入加鎖的代碼塊以前自動獲取鎖,而且退出代碼塊的時候釋放鎖,能夠保證一組語句做爲一個不可分割的單元被執行。
上一篇:Java 併發編程(一):簡介
下一篇:如何保證共享變量的原子性?
微信搜索「沉默王二」公衆號,關注後回覆「免費視頻」獲取 500G 高質量教學視頻(已分門別類)。