本文將會回答這幾個問題:java
顯然,線程安全的問題只會出如今多線程環境中,那麼爲何會有多線程呢?
最先期的計算機十分原始,尚未操做系統。想要使用計算機時,人們先把計算機能夠執行的指令刻在紙帶上,而後讓計算機從紙帶上讀取每一條指令,依次執行。這時候的計算機每次只能執行一個任務,是地地道道的單線程。
這種狀況下就產生了三個問題:
1. 計算資源的嚴重浪費
計算機在執行任務時,總少不了一些輸入輸出操做,好比計算結果的打印等。這時候CPU只能等待輸入輸出的完成。因此每每一個任務執行下來,可能CPU大部分人時間都是空閒的。而在當時CPU但是一種很是昂貴的資源,因而人們就想怎麼可以提升CPU的利用率呢?
2. 任務分配的不公平
如今假如咱們有十個任務須要執行,這但是很常見的。而計算機每次只能執行一個任務,直到執行結束,中間不能中斷。那麼問題來了,是先執行張三給的任務呢?仍是先幹李四的活呢?張三和李四可能擁有一樣的優先級,所以不管怎麼分配任務總會有人不滿意,以爲不公平。
3. 程序編寫十分困難
計算機一次只能執行一個任務,因此編寫程序的時候每每要把不少工做集成到一個程序中,這給程序的編寫人員帶來了極大的挑戰。能不能把程序分模塊編寫,而後讓模塊之間只進行必要的通訊呢?
爲了解決這些問題,計算機操做系統應運而生。操做系統就是管理計算機硬件與軟件資源的計算機程序。那麼操做系統如何同時執行多個任務呢?操做系統給每一個任務分配一個進程,而後給進程分配相應的計算資源、IO資源等,這樣進程就能執行起來了。操做系統會控制多個進程之間的切換,給每一個進程分配必定的執行時間,而後再切換另外一個進程,這樣多個進程即可以輪流着交替執行。由於輪流的時間很短,用戶會以爲彷彿在獨佔計算機資源來執行本身的任務。
進程雖然必定程度上緩解了咱們提到的那三個問題,可是仍是會存在問題。給你們舉兩個例子。一個例子是進程只能幹一件事,或者說進程中的代碼是串行執行的。這有什麼問題嗎?固然有。好比咱們用軟件安裝包安裝一個程序,安裝過程當中忽然不想安裝了,而後點擊了取消按鈕,結果你發現程序並無取消安裝。爲何呢?由於進程正在執行安裝程序的代碼,用戶的輸入只有等待安裝程序的代碼完成以後才能執行。因此你發現等進程響應了你取消安裝的輸入時,其實安裝程序早已執行完成。用專業術語來講,就是用戶接口的響應性太差了,用戶的輸入不能第一時間響應,甚至出現界面假死現象。另外一個例子是如今大部分的處理器是多處理器,好比如今有一個雙處理器,而只有一個任務。那麼這個任務只能由一個進程來執行,而一個進程只能由一個處理器來執行,那麼就有50%的計算資源被浪費了。
這時候,就要說到線程了。線程是進程中實施調度和分派的基本單位。一個進程能夠有多個線程,但至少有一個線程;而一個線程只能在一個進程的地址空間內活動。內存資源分配給進程,同一個進程的全部線程共享該進程全部資源。而CPU分配給線程,即真正在處理器運行的是線程。多線程的出現便解決了咱們以前提到的三個問題,可是多線程每每會帶來許多意想不到的問題,這就是接下來咱們要說的線程安全了。算法
在談什麼是線程安全的問題以前,先給你們舉一個線程不安全的例子,直接上代碼編程
public class Test { private static int count; private static class Thread1 extends Thread { public void run() { for (int i = 0; i < 1000; i++) { count ++; try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread1 t1 = new Thread1(); Thread1 t2 = new Thread1(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
這段代碼實現的邏輯很簡單,首先定義了一個int型的count變量,而後開啓了兩個線程,每一個線程執行1000次循環,循環中對count進行加1操做。等待兩個線程都執行完成後,打印count的值。那麼這段代碼的輸出結果是多少呢?可能不少人會說是2000。可是程序運行後卻發現結果大機率不是2000,而是一個比2000略小的數,好比1998這樣,並且每次運行的結果可能都不相同。
那麼這是爲何呢?這就是線程不安全。線程安全是指在多線程環境下,程序能夠始終執行正確的行爲,符合預期的邏輯。好比咱們剛剛的程序,共兩個線程,每一個線程對count變量累加1000次,預期的邏輯是count被累加了2000次,而代碼執行的結果卻不是2000,因此它是線程不安全的。
爲何是不安全的呢?由於count++的指令在實際執行的過程當中不是原子性的,而是要分爲讀、改、寫三步來進行;即先從內存中讀出count的值,而後執行+1操做,再將結果寫回內存中,以下圖所示。
這就是線程在計算機中真實的執行過程,看起來好像沒問題啊,別急,再看一張圖
看出來問題了麼?上圖中線程1執行了兩次自加操做,而線程2執行了一次自加操做,可是count卻從6變成了8,只加了2.咱們看一下爲何會出現這種狀況。當線程1讀取count的值爲6完成後,此時切換到了線程2執行,線程2一樣讀取到了count的值爲6,然後進行改和寫操做,count的值變爲了7;此時線程又切回了線程1,可是線程1中count的值依然是線程2修改前的6,這就是問題所在!!!即線程2修改了count的值,可是這種修改對線程1不可見,致使了程序出現了線程不安全的問題,沒有符合咱們預期的邏輯。
相信你們如今已經對線程不安全已經有了必定的認識了。如今咱們總結一下致使線程不安全的緣由,主要有三點:安全
前兩點前面已經舉例了,如今在解釋一下第三點。爲何程序執行的順序會和代碼的執行順序不一致呢?java平臺包括兩種編譯器:靜態編譯器(javac)和動態編譯器(jit:just in time)。靜態編譯器是將.java文件編譯成.class文件(二進制文件),以後即可以解釋執行。動態編譯器是將.class文件編譯成機器碼,以後再由jvm運行。問題通常會出如今動態編譯器上,由於動態編譯器爲了程序的總體性能會對指令進行重排序,雖然重排序能夠提高程序的性能,可是重排序以後會致使源代碼中指定的內存訪問順序與實際的執行順序不同,就會出現線程不安全的問題。數據結構
下面簡單談談針對以上的三個問題,java程序如何保證線程安全呢?
針對問題1:JDK裏面提供了不少atomic類,好比AtomicInteger, AtomicLong, AtomicBoolean等等,這些類自己能夠經過CAS來保證操做的原子性;另外Java也提供了各類鎖機制,來保證鎖內的代碼塊在同一時刻只能有一個線程執行,好比剛剛的例子咱們就能夠加鎖,以下:多線程
synchronized (Test.class){ count ++; }
這樣,就可以保證一個線程在多count值進行讀、改、寫操做時,其餘線程不可對count進行操做,從而保證了線程的安全性。
針對問題2:一樣能夠經過synchronized關鍵字加鎖來解決。與此同時,java還提供了一種輕量級的鎖,即volatile關鍵字,要優於synchronized的性能,一樣能夠保證修改對其餘線程的可見性。volatile通常用於對變量的寫操做不依賴於當前值的場景中,好比狀態標記量等。
針對問題3:能夠經過synchronized關鍵字定義同步代碼塊或者同步方法保障有序性,另外也能夠經過Lock接口保障有序性。
怎麼樣?如今是否是對線程安全有了更加深刻的理解了呢?
推薦閱讀
爲何有紅黑樹?什麼是紅黑樹?看完這篇你就明白了
《深刻淺出話數據結構》系列之什麼是B樹、B+樹?爲何二叉查找樹不行?
都2020年了,據說你還不會歸併排序?手把手教你手寫歸併排序算法jvm
以爲文章有用的話, 點贊+ 關注唄,好讓更多的人看到這篇文章,也激勵博主寫出更多的好文章。
更多關於 算法、數據結構和計算機基礎知識的內容,歡迎掃碼關注個人原創公衆號「 超悅編程」。