Java中線程安全和線程不安全解析和示例

簡介

本文作爲多線程編程的第一篇文章,將從一個簡單的例子開始,帶你真正從代碼層次理解什麼是線程不安全,以及爲什麼會出現線程不安全的情況。文章中將提供一個完整的線程不安全示例,希望你可以跟隨文章,自己真正動手運行一下此程序,體會一下多線程編程中必須要考慮的線程安全問題。

一.從經典的線程不安全的示例開始

經典案例: 兩個線程,共同讀寫一個全局變量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++這個操作時,JVM將會分爲三個步驟完成(非原子性):

  1. 某線程從內存中讀取count
  2. 某線程修改count值。
  3. 某線程將count重新寫入內存。

這個操作過程應該很好理解,你可簡單的類比爲把大象裝進冰箱裏的三個步驟。在單線程中執行上述代碼是不會出現小於2萬的這種情況,爲什麼多線程就發生了跟預期不一致的情況呢?爲了徹底弄清楚這個問題,你需要先理解什麼是線程?

線程像病毒一樣,不能夠獨立的存活於世間,需要寄生在宿主細胞中。線程也是不能夠獨立的生存在系統中,線程需要依附於進程存在。什麼是進程?

進程是代碼在數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位

線程是進程的一個執行路徑,一個進程至少有一個線程,多個線程則會共享進程中的資源。

2.理解問題的根源:

有了對線程的認識後,我們再去思考,count++的3個步驟,由於線程會共享進程中的資源,所以在這三步中,任何一步都可能被其他線程打斷,導致count值還沒來得及寫入,就被其他線程讀取或寫入。

3.腦補還原出錯的流程:

  • 假如count值爲1,線程1讀取到count值後,將count修改爲2,此時還沒來得及將結果寫入內存,內存中的count值還是1。
  • 另一個線程2,讀取到count值爲1後,也將其修改爲2,併成功寫入內存中,此時內存中的count值變爲了2。
  • 隨後線程1也將count的結果2寫入到內存中,count在內存中的結果依然是2(理應爲3)。

上述場景中,兩個線程各自執行了一次count++,但count值卻只增加了1,這就是問題所在。

總結

多線程可以並行執行一些任務,提高處理效率,但也同時帶來了新的問題,也就是線程安全問題,多個線程之間,操作同一資源時,也出現了讓人意向不到的的情況,其原因是這些操作可能不是原子性操作,簡單的說,我們肉眼看起來程序執行了一步操作,但在JVM中可能需要分多個步驟執行,多個線程可能會打亂了JVM的執行順序,隨後也就發生了不可預知的問題。

那麼在Java中,怎麼應對這種問題呢?Java隨着版本的升級,提供了很多解決方案,比如:Concurrent包中的類。但我們下一篇文章,將講解一種最簡單、最方便的一種解決方案,上述案例代碼僅僅通過增加一個單詞,就可以輕鬆避免線程安全的問題,它就是synchronized關鍵字。

喜歡本文,請收藏和點贊,也請繼續閱讀本專欄的其他文章,本專欄將結合各種場景代碼,徹底講透徹java中的併發問題和synchronized各種使用場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章