簡介
本文作為多線程程式設計的第一篇文章,将從一個簡單的例子開始,帶你真正從代碼層次了解什麼是線程不安全,以及為什麼會出現線程不安全的情況。文章中将提供一個完整的線程不安全示例,希望你可以跟随文章,自己真正動手運作一下此程式,體會一下多線程程式設計中必須要考慮的線程安全問題。
一.什麼是線程安全
《Java Concurrency In Practice》作者Brian Goetz的定義:“當多個線程通路一個對象時,如果不用考慮這些線程在運作時環境下的排程和交替執行,也不需要進行額外的同步或者在調用方進行任何其他的協調操作,調用這個對象的行為都可以獲得正确的結果,那這個對象是線程安全的。
為什麼不把所有操作都做成線程安全的?
實作線程安全是有成本的,比如線程安全的程式運作速度會相對較慢、開發的複雜度也提高了,提高了人力成本。
二.從經典的線程不安全的示例開始
經典案例: 兩個線程,共同讀寫一個全局變量
count
,每個線程執行10000次
count++
,
count
的最終結果會是20000嗎,在心中猜測一下運作結果?
經典案例的代碼實作:
package com.study.synchronize.object;
/**
* 線程不安全案例:兩個線程同時累加同一個變量,結果值會小于實際值
*/
public class ConcurrentProblem implements Runnable {
private static ConcurrentProblem concurrentProblem = new ConcurrentProblem();
private static int count;
public static void main(String[] args) {
Thread thread1 = new Thread(concurrentProblem);
Thread thread2 = new Thread(concurrentProblem);
thread1.start();
thread2.start();
try {
// 等待兩個線程都運作結束後,再列印結果
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//期待結果是20000,但是結果會小于這個值
System.out.println(count);
}
/**
* 多線程問題原因:count++這行代碼要分三步執行;1:讀取;2:修改;3:寫入。
* 在這三步中,任何一步都可能被其他線程打斷,導緻值還沒來得及寫入,就被其他線程讀取或寫入,這就是多線程并行操作統一變量導緻的問題。
*/
@Override
public void run() {
for (int k = 0; k < 10000; k++) {
count++;
}
}
}
多次運作結果:
count
最終值會小于等于20000。
三.剖析問題:多線程累加為什麼會有小于預期值這種情況呢
1.了解JVM如何執行 count++
count++
程式執行
count++
這個操作時,JVM将會分為三個步驟完成(非原子性):
- 某線程從記憶體中讀取
。count
- 某線程修改
值。count
- 某線程将
重新寫入記憶體。count
這個操作過程應該很好了解,你可簡單的類比為
把大象裝進冰箱裡的三個步驟
。在單線程中執行上述代碼是不會出現
小于2萬
的這種情況,為什麼多線程就發生了跟預期不一緻的情況呢?為了徹底弄清楚這個問題,你需要先了解什麼是線程?
線程像
病毒
一樣,不能夠獨立的存活于世間,需要寄生在
宿主細胞
中。線程也是不能夠獨立的生存在系統中,
線程
需要依附于
程序
存在。什麼是程序?
程序是代碼在資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關
線程是程序的一個執行路徑,一個程序至少有一個線程,多個線程則會共享程序中的資源。
2.了解問題的根源:
有了對線程的認識後,我們再去思考,
count++
的3個步驟,由于線程會共享程序中的資源,是以在這三步中,任何一步都可能被其他線程打斷,導緻
count
值還沒來得及寫入,就被其他線程讀取或寫入。
3.腦補還原出錯的流程:
- 假如
值為1,線程1讀取到count
值後,将count
修改為2,此時還沒來得及将結果寫入記憶體,記憶體中的count值還是1。count
- 另一個線程2,讀取到
值為1後,也将其修改為2,并成功寫入記憶體中,此時記憶體中的count
值變為了2。count
- 随後線程1也将
的結果2寫入到記憶體中,count
在記憶體中的結果依然是2(理應為3)。count
上述場景中,兩個線程各自執行了一次
count++
,但count值卻隻增加了1,這就是問題所在。
總結
多線程可以并行執行一些任務,提高處理效率,但也同時帶來了新的問題,也就是線程安全問題,多個線程之間,操作同一資源時,也出現了讓人意向不到的的情況,其原因是這些操作可能不是原子性操作,簡單的說,我們肉眼看起來程式執行了一步操作,但在JVM中可能需要分多個步驟執行,多個線程可能會打亂了JVM的執行順序,随後也就發生了不可預知的問題。
那麼在Java中,怎麼應對這種問題呢?Java随着版本的更新,提供了很多解決方案,比如:Concurrent包中的類。但我們下一篇文章,将講解一種最簡單、最友善的一種解決方案,上述案例代碼僅僅通過增加一個單詞,就可以輕松避免線程安全的問題,它就是
synchronized
關鍵字。
喜歡本文,請收藏和點贊,也請繼續閱讀本專欄的其他文章,本專欄将結合各種場景代碼,徹底講透徹java中的并發問題和
synchronized
各種使用場景。