天天看點

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

簡介

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

一.什麼是線程安全

《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++

這個操作時,JVM将會分為三個步驟完成(非原子性):

  1. 某線程從記憶體中讀取

    count

  2. 某線程修改

    count

    值。
  3. 某線程将

    count

    重新寫入記憶體。

這個操作過程應該很好了解,你可簡單的類比為

把大象裝進冰箱裡的三個步驟

。在單線程中執行上述代碼是不會出現

小于2萬

的這種情況,為什麼多線程就發生了跟預期不一緻的情況呢?為了徹底弄清楚這個問題,你需要先了解什麼是線程?

線程像

病毒

一樣,不能夠獨立的存活于世間,需要寄生在

宿主細胞

中。線程也是不能夠獨立的生存在系統中,

線程

需要依附于

程序

存在。什麼是程序?

程序是代碼在資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關

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

2.了解問題的根源:

有了對線程的認識後,我們再去思考,

count++

的3個步驟,由于線程會共享程序中的資源,是以在這三步中,任何一步都可能被其他線程打斷,導緻

count

值還沒來得及寫入,就被其他線程讀取或寫入。

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

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

    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

各種使用場景。

繼續閱讀