說到多線程帶來的風險,首先要了解一個概念-臨界區。安全
什麼是臨界區?多線程
臨界區是用來表示一種公共的資源(共享數據),它能夠被多個線程使用,可是在每次只能有一個線程可以使用它,當臨界區資源正在被一個線程使用時,其餘的線程就只能等待當前線程執行完以後才能使用該臨界區資源。ide
好比一臺飲水機,好比辦公室辦公室裏有一支筆,它一次只能被一我的使用,假如它正在被甲使用時,其餘想要使用這支筆的人只能等甲使用完這支筆以後才能容許另外一我的去使用。這就是臨界區的概念。函數
使用多線程主要會帶來如下幾個問題:性能
(一)線程安全問題優化
線程安全問題指的是在某一線程從開始訪問到結束訪問某一數據期間,該數據被其餘的線程所修改,那麼對於當前線程而言,該線程就發生了線程安全問題,表現形式爲數據的缺失,數據不一致等。spa
線程安全問題發生的條件:線程
1)多線程環境下,即存在包括本身在內存在有多個線程。code
2)多線程環境下存在共享資源,且多線程操做該共享資源。blog
3)多個線程必須對該共享資源有非原子性操做。
示例代碼:
package com.wangx.thread.t3; public class Sequence { private int value; public int getNext() { return value++; } public static void main(String[] args) { Sequence sequence = new Sequence(); // while (true) { // System.out.println(sequence.getNext()); // } new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + " " + sequence.getNext()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + " " + sequence.getNext()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
上述代碼中多個線程同時調用getNext()方法,該方法對value進行自增的操做,正常的狀況下不會出現重複數字或缺失某個數字,可是運行的結果爲:
Thread-0 28 Thread-0 30 Thread-1 30 Thread-1 31 Thread-0 31 Thread-1 32 Thread-0 32 Thread-0 33 Thread-1 33
能夠看到打印出來的數據有重複數據,這就是產生了線程安全問題,線程安全問題是多線程中很嚴重的問題,由於任何一個程序,咱們都是但願可以獲得正確的結果的,因此若是結果不正確,那麼程序將變得沒有意義,因此咱們應該避免線程安全問題的產生。
線程安全問題的解決思路:
1)儘可能不使用共享變量,將沒必要要的共享變量變成局部變量來使用。
2)使用synchronized關鍵字同步代碼塊,或者使用jdk包中提供的Lock爲操做進行加鎖。
3)使用ThreadLocal爲每個線程創建一個變量的副本,各個線程間獨立操做,互不影響。
(二)性能問題
線程的生命週期開銷是很是大的,一個線程的建立到銷燬都會佔用大量的內存。同時若是不合理的建立了多個線程,cup的處理器數量小於了線程數量,那麼將會有不少的線程被閒置,閒置的線程將會佔用大量的內存,爲垃圾回收帶來很大壓力,同時cup在分配線程時還會消耗其性能。
因此在jdk中提出了線程池的概念,模擬一個池,預先建立有限合理個數的線程放入池中,當須要執行任務時從池中取出空閒的先去執行任務,執行完成後將線程歸還到池中,這樣就減小了線程的頻繁建立和銷燬,節省內存開銷和減少了垃圾回收的壓力。同時由於任務到來時自己線程已經存在,減小了建立線程時間,提升了執行效率,並且合理的建立線程池數量還會使各個線程都處於忙碌狀態,提升任務執行效率,線程池還提供了拒絕策略,當任務數量到達某一臨界區時,線程池將拒絕任務的進入,保持現有任務的順利執行,減小池的壓力。
(三)活躍性問題
1)死鎖,死鎖指的是當某一個線程正A在佔用臨界區資源的使用權,而其必需要另一臨界區資源才能執行完成,可是存在另外一個臨界區資源正被線程B所佔有,且線程B須要線程A持有的資源才能只能完成,這個就會致使兩個線程都在等待着對方,從而產生死鎖。多個線程環形佔用資源也是同樣的會產生死鎖問題。
死鎖是一個很是嚴重的問題,它會致使進程再也不工做,是應該避免和時時當心的問題。想要避免死鎖,可使用無鎖函數(cas)或者使用重入鎖,經過重入鎖使線程中斷或限時等待能夠有效的規避死鎖問題。
2)飢餓,飢餓指的是某一線程或多個線程由於某些緣由一直獲取不到資源,致使程序一直沒法執行。如某一線程優先級過低致使一直分配不到資源,或者是某一線程一直佔着某種資源不妨,致使該線程沒法執行等。與死鎖相比,飢餓現象仍是有可能在一段時間以後恢復執行的。能夠設置合適的線程優先級來儘可能避免飢餓的產生。
3)活鎖,活鎖體現了一種謙讓的美德,每一個線程都想把資源讓給對方,可是因爲機器「智商」不夠,可能會產生一直將資源讓來讓去,致使資源在兩個線程間跳動而沒法使某一線程真正的到資源並執行,這就是活鎖的問題。
(四)阻塞
阻塞是用來形容多線程的問題,幾個線程之間共享臨界區資源,那麼當一個線程佔用了臨界區資源後,全部須要使用該資源的線程都須要進入該臨界區等待,等待會致使線程掛起,一直不能工做,這種狀況就是阻塞,若是某一線程一直都不釋放資源,將會致使其餘全部等待在這個臨界區的線程都不能工做。當咱們使用synchronized或重入鎖時,咱們獲得的就是阻塞線程,如論是synchronized或者重入鎖,都會在試圖執行代碼前,獲得臨界區的鎖,若是得不到鎖,線程將會被掛起等待,知道其餘線程執行完成並釋放鎖且拿到鎖爲止。能夠經過減小鎖持有時間,讀寫鎖分離,減少鎖的粒度,鎖分離,鎖粗化等方式來優化鎖的性能。