互聯網校招面試必備——Java多線程 | 掘金技術徵文

本文首發於個人我的博客:尾尾部落html

本文是我刷了幾十篇一線互聯網校招java後端開發崗位的面經後總結的多線程相關題目,雖然有點小長,可是面試前看一看,相信能幫你輕鬆啃下多線程這塊大骨頭。java

什麼是進程,什麼是線程?爲何須要多線程編程?

進程是執行着的應用程序,而線程是進程內部的一個執行序列。一個進程能夠有多個線程。線程又叫作輕量級進程。 進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,是操做系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是 CPU 調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時一般擁有獨立的內存單元,而線程之間能夠共享內存。使用多線程的編程一般可以帶來更好的性能和用戶體驗,可是多線程的程序對於其餘程序是不友好的,由於它佔用了更多的 CPU 資源。面試

進程間的通訊方式

  • 管道( pipe ):管道是一種半雙工的通訊方式,數據只能單向流動,並且只能在具備親緣關係的進程間使用。進程的親緣關係一般是指父子進程關係。
  • 有名管道 (namedpipe) : 有名管道也是半雙工的通訊方式,可是它容許無親緣關係進程間的通訊。
  • 信號量(semophore ) : 信號量是一個計數器,能夠用來控制多個進程對共享資源的訪問。它常做爲一種鎖機制,防止某進程正在訪問共享資源時,其餘進程也訪問該資源。所以,主要做爲進程間以及同一進程內不一樣線程之間的同步手段。
  • 消息隊列( messagequeue ) : 消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識。消息隊列克服了信號傳遞信息少、管道只能承載無格式字節流以及緩衝區大小受限等缺點。
  • 信號 (sinal) : 信號是一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生。
  • 共享內存(shared memory ) :共享內存就是映射一段能被其餘進程所訪問的內存,這段共享內存由一個進程建立,但多個進程均可以訪問。共享內存是最快的 IPC 方式,它是針對其餘進程間通訊方式運行效率低而專門設計的。它每每與其餘通訊機制,如信號兩,配合使用,來實現進程間的同步和通訊。
  • 套接字(socket ) : 套解口也是一種進程間通訊機制,與其餘通訊機制不一樣的是,它可用於不一樣及其間的進程通訊。

線程間的通訊方式

  • 鎖機制:包括互斥鎖、條件變量、讀寫鎖
    • 互斥鎖提供了以排他方式防止數據結構被併發修改的方法。
    • 讀寫鎖容許多個線程同時讀共享數據,而對寫操做是互斥的。
    • 條件變量能夠以原子的方式阻塞進程,直到某個特定條件爲真爲止。對條件的測試是在互斥鎖的保護下進行的。條件變量始終與互斥鎖一塊兒使用。
  • 信號量機制(Semaphore):包括無名線程信號量和命名線程信號量
  • 信號機制(Signal):相似進程間的信號處理

線程間的通訊目的主要是用於線程同步,因此線程沒有像進程通訊中的用於數據交換的通訊機制。數據庫

實現多線程的三種方法

  • 繼承Thread類,重寫父類run()方法
public class thread1 extends Thread {
        public void run() {
                for (int i = 0; i < 10000; i++) {
                        System.out.println("我是線程"+this.getId());
                }
        }
        public static void main(String[] args) {
                thread1 th1 = new thread1();
                thread1 th2 = new thread1();
                th1.start();
                th2.start();
        }
}
複製代碼
  • 實現runnable接口
public class thread2 implements Runnable {
        public String ThreadName;
        public thread2(String tName){
                ThreadName = tName;
        }
        public void run() {
                for (int i = 0; i < 10000; i++) {
                        System.out.println(ThreadName);
                }
        }
        public static void main(String[] args) {
                // 建立一個Runnable接口實現類的對象
                thread2 th1 = new thread2("線程A:");
                thread2 th2 = new thread2("線程B:");
                // 將此對象做爲形參傳遞給Thread類的構造器中,建立Thread類的對象,此對象即爲一個線程
                Thread myth1 = new Thread(th1);
                Thread myth2 = new Thread(th2);
                // 調用start()方法,啓動線程並執行run()方法
                myth1.start();
                myth2.start();
        }
}
複製代碼
  • 經過Callable和Future建立線程
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class CallableThreadTest implements Callable<Integer>
{
	@Override
	public Integer call() throws Exception{
		int i = 0;
		for(;i<100;i++){
			System.out.println(Thread.currentThread().getName()+" "+i);
		}
		return i;
	}
	
	public static void main(String[] args){
		CallableThreadTest ctt = new CallableThreadTest();
		FutureTask<Integer> ft = new FutureTask<>(ctt);
		for(int i = 0;i < 100;i++){
			System.out.println(Thread.currentThread().getName()+" 的循環變量i的值"+i);
			if(i==20){
				new Thread(ft,"有返回值的線程").start();
			}
		}
		try{
			System.out.println("子線程的返回值:"+ft.get());
		} catch (InterruptedException e){
			e.printStackTrace();
		} catch (ExecutionException e){
			e.printStackTrace();
		}
	}
}
複製代碼

三種建立多線程方法的對比

一、採用實現Runnable、Callable接口的方式建立多線程時,線程類只是實現了Runnable接口或Callable接口,還能夠繼承其餘類。缺點是編程稍微複雜,若是要訪問當前線程,則必須使用Thread.currentThread()方法。 二、使用繼承Thread類的方式建立多線程時,編寫簡單,若是須要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this便可得到當前線程。缺點是線程類已經繼承了Thread類,因此不能再繼承其餘父類。 三、Runnable和Callable的區別 (1) Callable規定重寫call(),Runnable重寫run()。 (2) Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。 (3) call方法能夠拋出異常,run方法不能夠。 (4) 運行Callable任務能夠拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。經過Future對象能夠了解任務執行狀況,可取消任務的執行,還可獲取執行結果。編程

線程狀態

  • 新建狀態:新建線程對象,並無調用start()方法以前
  • 就緒狀態:調用start()方法以後線程就進入就緒狀態,可是並非說只要調用start()方法線程就立刻變爲當前線程,在變爲當前線程以前都是爲就緒狀態。值得一提的是,線程在睡眠和掛起中恢復的時候也會進入就緒狀態。
  • 運行狀態:線程被設置爲當前線程,得到CPU後,開始執行run()方法,就是線程進入運行狀態。
  • 阻塞狀態:處於運行的狀態的線程,除非執行時間很是很是很是短,不然它會由於系統對資源的調度而被中斷進入阻塞狀態。好比說調用sleep()方法後線程就進入阻塞狀態。
  • 死亡狀態:處於運行狀態的線程,當它主動或者被動結束,線程就處於死亡狀態。結束的形式,一般有如下幾種:1. 線程執行完成,線程正常結束;2. 線程執行過程當中出現異常或者錯誤,被動結束;3. 線程主動調用stop方法結束線程。

線程控制

  • join():等待。讓一個線程等待另外一個線程完成才繼續執行。如A線程線程執行體中調用B線程的join()方法,則A線程被阻塞,知道B線程執行完爲止,A才能得以繼續執行。
  • sleep():睡眠。讓當前的正在執行的線程暫停指定的時間,並進入阻塞狀態。
  • yield():線程讓步。將線程從運行狀態轉換爲就緒狀態。當某個線程調用 yiled() 方法從運行狀態轉換到就緒狀態後,CPU 會從就緒狀態線程隊列中只會選擇與該線程優先級相同或優先級更高的線程去執行。
  • setPriority():改變線程的優先級。每一個線程在執行時都具備必定的優先級,優先級高的線程具備較多的執行機會。每一個線程默認的優先級都與建立它的線程的優先級相同。main線程默認具備普通優先級。參數priorityLevel範圍在1-10之間,經常使用的有以下三個靜態常量值:MAX_PRIORITY:10;MIN_PRIORITY:1;NORM_PRIORITY:5。

PS: 具備較高線程優先級的線程對象僅表示此線程具備較多的執行機會,而非優先執行。後端

  • setDaemon(true):設置爲後臺線程。後臺線程主要是爲其餘線程(相對能夠稱之爲前臺線程)提供服務,或「守護線程」。如JVM中的垃圾回收線程。當全部的前臺線程都進入死亡狀態時,後臺線程會自動死亡。

sleep() 和 yield() 二者的區別: ① sleep()方法會給其餘線程運行的機會,不考慮其餘線程的優先級,所以會給較低優先級線程一個運行的機會。yield()方法只會給相同優先級或者更高優先級的線程一個運行的機會。 ② 當線程執行了 sleep(long millis) 方法,將轉到阻塞狀態,參數millis指定睡眠時間。當線程執行了yield()方法,將轉到就緒狀態。 ③ sleep() 方法聲明拋出InterruptedException異常,而 yield() 方法沒有聲明拋出任何異常。數組

wait、notify、notifyAll的區別

wait、notify、notifyAll是java同步機制中重要的組成部分,結合synchronized關鍵字使用,能夠創建不少優秀的同步模型。這3個方法並非Thread類或者是Runnable接口的方法,而是Object類的3個本地方法。 調用一個Object的wait與notify/notifyAll的時候,必須保證調用代碼對該Object是同步的,也就是說必須在做用等同於synchronized(obj){......}的內部纔可以去調用obj的wait與notify/notifyAll三個方法,不然就會報錯:java.lang.IllegalMonitorStateException:current thread not owner安全

先說兩個概念:鎖池和等待池 鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),因爲這些線程在進入對象的synchronized方法以前必須先得到該對象的鎖的擁有權,可是該對象的鎖目前正被線程A擁有,因此這些線程就進入了該對象的鎖池中。 等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中 @知乎--文龍bash

  • 若是線程調用了對象的 wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
  • 當有線程調用了對象的 notifyAll()方法(喚醒全部 wait 線程)或 notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify後只要一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的全部線程移動到鎖池中,等待鎖競爭
  • 優先級高的線程競爭到對象鎖的機率大,倘若某線程沒有競爭到該對象鎖,它還會留在鎖池中,惟有線程再次調用 wait()方法,它纔會從新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。

小結

  • wait:線程自動釋放其佔有的對象鎖,並等待notify
  • notify:喚醒一個正在wait當前對象鎖的線程,並讓它拿到對象鎖
  • notifyAll:喚醒全部正在wait當前對象鎖的線程 notify和notifyAll的最主要的區別是:notify只是喚醒一個正在wait當前對象鎖的線程,而notifyAll喚醒全部。值得注意的是:notify是本地方法,具體喚醒哪個線程由虛擬機控制;notifyAll後並非全部的線程都能立刻往下執行,它們只是跳出了wait狀態,接下來它們還會是競爭對象鎖。

sleep() 和 wait() 有什麼區別?

sleep()方法是線程類(Thread)的靜態方法,致使此線程暫停執行指定時間,將執行機會給其餘線程,可是監控狀態依然保持,到時後會自動恢復(線程回到就緒(ready)狀態),由於調用 sleep 不會釋放對象鎖。wait() 是 Object 類的方法,對此對象調用 wait()方法致使本線程放棄對象鎖(線程暫停執行),進入等待此對象的等待鎖定池,只有針對此對象發出 notify 方法(或 notifyAll)後本線程才進入對象鎖定池準備得到對象鎖進入就緒狀態。服務器

鎖類型

  • 可重入鎖:廣義上的可重入鎖指的是可重複可遞歸調用的鎖,在外層使用鎖以後,在內層仍然可使用,而且不發生死鎖(前提得是同一個對象或者class),這樣的鎖就叫作可重入鎖。即在執行對象中全部同步方法不用再次得到鎖。ReentrantLock和synchronized都是可重入鎖。舉個簡單的例子,當一個線程執行到某個synchronized方法時,好比說method1,而在method1中會調用另一個synchronized方法method2,此時線程沒必要從新去申請鎖,而是能夠直接執行方法method2。
  • 可中斷鎖:在等待獲取鎖過程當中可中斷。synchronized就不是可中斷鎖,而Lock是可中斷鎖。
  • 公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具備優先獲取鎖權利。非公平鎖即沒法保證鎖的獲取是按照請求鎖的順序進行的,這樣就可能致使某個或者一些線程永遠獲取不到鎖。synchronized是非公平鎖,它沒法保證等待的線程獲取鎖的順序。對於ReentrantLock和ReentrantReadWriteLock,默認狀況下是非公平鎖,可是能夠設置爲公平鎖。
  • 讀寫鎖:對資源讀取和寫入的時候拆分爲2部分處理,一個讀鎖和一個寫鎖。讀的時候能夠多線程一塊兒讀,寫的時候必須同步地寫。ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。能夠經過readLock()獲取讀鎖,經過writeLock()獲取寫鎖。

什麼是樂觀鎖和悲觀鎖

(1)樂觀鎖:很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會去判斷在此期間有沒有人去更新這個數據(可使用版本號等機制)。若是由於衝突失敗就重試。樂觀鎖適用於寫比較少的狀況下,即衝突比較少發生,這樣能夠省去了鎖的開銷,加大了系統的整個吞吐量。像數據庫提供的相似於write_condition機制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現方式CAS實現的。 (2)悲觀鎖:老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,所以每次拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖,效率比較低。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,讀鎖,寫鎖等,都是在作操做以前先上鎖。再好比Java裏面的同步原語synchronized關鍵字的實現也是悲觀鎖。

樂觀鎖的實現方式(CAS)

樂觀鎖的實現主要就兩個步驟:衝突檢測和數據更新。其實現方式有一種比較典型的就是 Compare and Swap ( CAS )。 CAS:CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。 CAS 操做中包含三個操做數 —— 須要讀寫的內存位置(V)、進行比較的預期原值(A)和擬寫入的新值(B)。若是內存位置V的值與預期原值A相匹配,那麼處理器會自動將該位置值更新爲新值B。不然處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了「 我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。 」這其實和樂觀鎖的衝突檢查+數據更新的原理是同樣的。

樂觀鎖是一種思想,CAS是這種思想的一種實現方式。

CAS的缺點

  1. ABA問題

若是內存地址V初次讀取的值是A,而且在準備賦值的時候檢查到它的值仍然爲A,那咱們就能說它的值沒有被其餘線程改變過了嗎?若是在這段期間它的值曾經被改爲了B,後來又被改回爲A,那CAS操做就會誤認爲它歷來沒有被改變過。這個漏洞稱爲CAS操做的「ABA」問題。ava併發包爲了解決這個問題,提供了一個帶有標記的原子引用類「AtomicStampedReference」,它能夠經過控制變量值的版原本保證CAS的正確性。所以,在使用CAS前要考慮清楚「ABA」問題是否會影響程序併發的正確性,若是須要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。

  1. 循環時間長開銷很大

自旋CAS(不成功,就一直循環執行,直到成功)若是長時間不成功,會給CPU帶來很是大的執行開銷。

  1. 只能保證一個共享變量的原子操做。

當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖來保證原子性。

實現一個死鎖

什麼是死鎖:兩個進程都在等待對方執行完畢才能繼續往下執行的時候就發生了死鎖。結果就是兩個進程都陷入了無限的等待中。 產生死鎖的四個必要條件: 互斥條件:一個資源每次只能被一個進程使用。 請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。 不剝奪條件:進程已得到的資源,在末使用完以前,不能強行剝奪。 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。 這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不知足,就不會發生死鎖。 考慮以下情形: (1)線程A當前持有互斥所鎖lock1,線程B當前持有互斥鎖lock2。 (2)線程A試圖獲取lock2,由於線程B正持有lock2,所以線程A會阻塞等待線程B對lock2釋放。 (3)若是此時線程B也在試圖獲取lock1,同理線程也會阻塞。 (4)二者都在等待對方所持有可是雙方都不釋放的鎖,這時便會一直阻塞造成死鎖。 死鎖的解決方法: a 撤消陷於死鎖的所有進程; b 逐個撤消陷於死鎖的進程,直到死鎖不存在; c 從陷於死鎖的進程中逐個強迫放棄所佔用的資源,直至死鎖消失。 d 從另一些進程那裏強行剝奪足夠數量的資源分配給死鎖進程,以解除死鎖狀態

如何確保 N 個線程能夠訪問 N 個資源同時又不致使死鎖?

使用多線程的時候,一種很是簡單的避免死鎖的方式就是:指定獲取鎖的順序,並強制線程按照指定的順序獲取鎖。所以,若是全部的線程都是以一樣的順序加鎖和釋放鎖,就不會出現死鎖了

volatile關鍵字

  對於過可見性、有序性及原子性問題,一般狀況下咱們能夠經過Synchronized關鍵字來解決這些個問題,不過若是對Synchronized原理有了解的話,應該知道Synchronized是一個比較重量級的操做,對系統的性能有比較大的影響,因此,若是有其餘解決方案,咱們一般都避免使用Synchronized來解決問題。而volatile關鍵字就是Java中提供的另外一種解決可見性和有序性問題的方案。對於原子性,須要強調一點,也是你們容易誤解的一點:對volatile變量的單次讀/寫操做能夠保證原子性的,如long和double類型變量,可是並不能保證i++這種操做的原子性,由於本質上i++是讀、寫兩次操做。

  • 防止重排序

問題:操做系統能夠對指令進行重排序,多線程環境下就可能將一個未初始化的對象引用暴露出來,從而致使不可預料的結果 解決原理:volatile關鍵字經過提供「內存屏障」的方式來防止指令被重排序,爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。 一、在每一個volatile寫操做前插入StoreStore屏障,在寫操做後插入StoreLoad屏障。 二、在每一個volatile讀操做前插入LoadLoad屏障,在讀操做後插入LoadStore屏障。

  • 實現可見性

問題:可見性問題主要指一個線程修改了共享變量值,而另外一個線程卻看不到 解決原理:(1)修改volatile變量時會強制將修改後的值刷新的主內存中。 (2)修改volatile變量後會致使其餘線程工做內存中對應的變量值失效。所以,再讀取該變量值的時候就須要從新從讀取主內存中的值。

  • 注:volatile並不保證變量更新的原子性

volatile使用建議

相對於synchronized塊的代碼鎖,volatile應該是提供了一個輕量級的針對共享變量的鎖,當咱們在多個線程間使用共享變量進行通訊的時候須要考慮將共享變量用volatile來修飾。 volatile是一種稍弱的同步機制,在訪問volatile變量時不會執行加鎖操做,也就不會執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。 使用建議:在兩個或者更多的線程須要訪問的成員變量上使用volatile。當要訪問的變量已在synchronized代碼塊中,或者爲常量時,不必使用volatile。 因爲使用volatile屏蔽掉了JVM中必要的代碼優化,因此在效率上比較低,所以必定在必要時才使用此關鍵字。

volatile和synchronized區別

一、volatile不會進行加鎖操做: volatile變量是一種稍弱的同步機制在訪問volatile變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。 二、volatile變量做用相似於同步變量讀寫操做: 從內存可見性的角度看,寫入volatile變量至關於退出同步代碼塊,而讀取volatile變量至關於進入同步代碼塊。 三、volatile不如synchronized安全: 在代碼中若是過分依賴volatile變量來控制狀態的可見性,一般會比使用鎖的代碼更脆弱,也更難以理解。僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它。通常來講,用同步機制會更安全些。 四、volatile沒法同時保證內存可見性和原子性: 加鎖機制(即同步機制)既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性,緣由是聲明爲volatile的簡單變量若是當前值與該變量之前的值相關,那麼volatile關鍵字不起做用,也就是說以下的表達式都不是原子操做:「count++」、「count = count+1」。

當且僅當知足如下全部條件時,才應該使用volatile變量: 一、對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。 二、該變量沒有包含在具備其餘變量的不變式中。 總結:在須要同步的時候,第一選擇應該是synchronized關鍵字,這是最安全的方式,嘗試其餘任何方式都是有風險的。尤爲在、jdK1.5以後,對synchronized同步機制作了不少優化,如:自適應的自旋鎖、鎖粗化、鎖消除、輕量級鎖等,使得它的性能明顯有了很大的提高。

synchronized

synchronized能夠保證方法或者代碼塊在運行時,同一時刻只有一個方法能夠進入到臨界區,同時它還能夠保證共享變量的內存可見性。Synchronized主要有如下三個做用:保證互斥性、保證可見性、保證順序性。

synchronized的三種應用方式

  • 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到當前實例的鎖。實現原理:指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先持有monitor(虛擬機規範中用的是管程一詞), 而後再執行方法,最後再方法完成(不管是正常完成仍是非正常完成)時釋放monitor。

    public synchronized void increase(){
        i++;
    }
    複製代碼
  • 修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到當前類對象的鎖

    public static synchronized void increase(){
        i++;
    }
    複製代碼
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。實現原理:使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置。

    static AccountingSync instance=new AccountingSync();
    synchronized(instance){
        for(int j=0;j<1000000;j++){
            i++;
        }
    }
    複製代碼

Lock

Lock是一個接口,它的的實現類提供了比synchronized更普遍意義上鎖操做,它容許用戶更靈活的代碼結構,更多的不一樣特效。Lock的實現類主要有ReentrantLock和ReentrantReadWriteLock。

Lock lock=new ReentrantLock();
lock.lock();
try{
    // do something
    // 若是有return要寫在try塊中
}finally{
    lock.unlock();
}
複製代碼

Lock接口中獲取鎖的方法

  • void lock():lock()方法是日常使用得最多的一個方法,就是用來獲取鎖。若是鎖已被其餘線程獲取,則進行等待。在發生異常時,它不會自動釋放鎖,要記得在finally塊中釋放鎖,以保證鎖必定被被釋放,防止死鎖的發生。
  • void lockInterruptibly():能夠響應中斷,當經過這個方法去獲取鎖時,若是線程 正在等待獲取鎖,則這個線程可以響應中斷,即中斷線程的等待狀態。
  • boolean tryLock():有返回值的,它表示用來嘗試獲取鎖,若是獲取成功,則返回true;若是獲取失敗(即鎖已被其餘線程獲取),則返回false。
  • boolean tryLock(long time, TimeUnit unit):和tryLock()方法是相似的,只不過區別在於這個方法在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false,同時能夠響應中斷。

Condition類

Condition是Java提供來實現等待/通知的類,Condition類還提供比wait/notify更豐富的功能,Condition對象是由lock對象所建立的。可是同一個鎖能夠建立多個Condition的對象,即建立多個對象監視器。這樣的好處就是能夠指定喚醒線程。notify喚醒的線程是隨機喚醒一個。 Condition 將 Object 監視器方法(wait、notify 和 notifyAll)分解成大相徑庭的對象,以便經過將這些對象與任意 Lock 實現組合使用,爲每一個對象提供多個等待 set (wait-set)。 其中,Lock 替代了 synchronized 方法和語句的使用,Condition 替代了 Object 監視器方法的使用。 在Condition中,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll(),傳統線程的通訊方式,Condition均可以實現,這裏注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。

Condition與Object中的wait, notify, notifyAll區別

1.Condition中的await()方法至關於Object的wait()方法,Condition中的signal()方法至關於Object的notify()方法,Condition中的signalAll()至關於Object的notifyAll()方法。 不一樣的是,Object中的這些方法是和同步鎖捆綁使用的;而Condition是須要與互斥鎖/共享鎖捆綁使用的。 2.Condition它更強大的地方在於:可以更加精細的控制多線程的休眠與喚醒。對於同一個鎖,咱們能夠建立多個Condition,在不一樣的狀況下使用不一樣的Condition。 例如,假如多線程讀/寫同一個緩衝區:當向緩衝區中寫入數據以後,喚醒"讀線程";當從緩衝區讀出數據以後,喚醒"寫線程";而且當緩衝區滿的時候,"寫線程"須要等待;當緩衝區爲空時,"讀線程"須要等待。 若是採用Object類中的wait(),notify(),notifyAll()實現該緩衝區,當向緩衝區寫入數據以後須要喚醒"讀線程"時,不可能經過notify()或notifyAll()明確的指定喚醒"讀線程",而只能經過notifyAll喚醒全部線程(可是notifyAll沒法區分喚醒的線程是讀線程,仍是寫線程)。 可是,經過Condition,就能明確的指定喚醒讀線程。

synchronized和lock的區別

synchronized Lock
存在層次 Java的關鍵字 是一個接口
鎖的釋放 一、以獲取鎖的線程執行完同步代碼,釋放鎖 二、線程執行發生異常,jvm會讓線程釋放鎖 在finally中必須釋放鎖,否則容易形成線程死鎖
鎖的獲取 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 Lock可讓等待鎖的線程響應中斷
鎖狀態 沒法判斷 能夠判斷有沒有成功獲取鎖
鎖類型 可重入 不可中斷 非公平 可重入 可中斷 公平/非公平

性能方面,JDK1.5中,synchronized是性能低效的。由於這是一個重量級操做,它對性能最大的影響是阻塞的是實現,掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性帶來了很大的壓力。相比之下使用Java提供的Lock對象,性能更高一些。多線程環境下,synchronized的吞吐量降低的很是嚴重,而ReentrankLock則能基本保持在同一個比較穩定的水平上。

到了JDK1.6,synchronize加入了不少優化措施,有自適應自旋,鎖消除,鎖粗化,輕量級鎖,偏向鎖等等。致使在JDK1.6上synchronize的性能並不比Lock差。官方也表示,他們也更支持synchronize,在將來的版本中還有優化餘地,因此仍是提倡在synchronized能實現需求的狀況下,優先考慮使用synchronized來進行同步。

鎖的狀態

Java SE1.6爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率。

偏向鎖

在沒有實際競爭的狀況下,還可以針對部分場景繼續優化。若是不只僅沒有實際競爭,自始至終,使用鎖的線程都只有一個,那麼,維護輕量級鎖都是浪費的。偏向鎖的目標是,減小無競爭且只有一個線程使用鎖的狀況下,使用輕量級鎖產生的性能消耗。輕量級鎖每次申請、釋放鎖都至少須要一次CAS,但偏向鎖只有初始化時須要一次CAS。 「偏向」的意思是,偏向鎖假定未來只有第一個申請鎖的線程會使用鎖(不會有任何線程再來申請鎖),所以,只須要在Mark Word中CAS記錄owner(本質上也是更新,但初始值爲空),若是記錄成功,則偏向鎖獲取成功,記錄鎖狀態爲偏向鎖,之後當前線程等於owner就能夠零成本的直接得到鎖;不然,說明有其餘線程競爭,膨脹爲輕量級鎖。 偏向鎖沒法使用自旋鎖優化,由於一旦有其餘線程申請鎖,就破壞了偏向鎖的假定。

輕量級鎖

輕量級鎖是由偏向所升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖。輕量級鎖是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。 使用輕量級鎖時,不須要申請互斥量,僅僅將Mark Word中的部分字節CAS更新指向線程棧中的Lock Record,若是更新成功,則輕量級鎖獲取成功,記錄鎖狀態爲輕量級鎖;不然,說明已經有線程得到了輕量級鎖,目前發生了鎖競爭(不適合繼續使用輕量級鎖),接下來膨脹爲重量級鎖。

重量級鎖

重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具有Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責作互斥,後一個用於作線程同步。

自旋鎖

自旋鎖原理很是簡單,若是持有鎖的線程能在很短期內釋放鎖資源,那麼那些等待競爭鎖的線程就不須要作內核態和用戶態之間的切換進入阻塞掛起狀態,它們只須要等一等(自旋),等持有鎖的線程釋放鎖後便可當即獲取鎖,這樣就避免用戶線程和內核的切換的消耗。 可是線程自旋是須要消耗cup的,說白了就是讓cup在作無用功,若是一直獲取不到鎖,那線程也不能一直佔用cup自旋作無用功,因此須要設定一個自旋等待的最大時間。 若是持有鎖的線程執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會致使其它爭用鎖的線程在最大等待時間內仍是獲取不到鎖,這時爭用線程會中止自旋進入阻塞狀態。

自適應自旋鎖

自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定:

  • 若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間,好比100個循環。
  • 相反的,若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能減小自旋時間甚至省略自旋過程,以免浪費處理器資源。

自適應自旋解決的是「鎖競爭時間不肯定」的問題。JVM很難感知到確切的鎖競爭時間,而交給用戶分析就違反了JVM的設計初衷。自適應自旋假定不一樣線程持有同一個鎖對象的時間基本至關,競爭程度趨於穩定,所以,能夠根據上一次自旋的時間與結果調整下一次自旋的時間。

偏向鎖、輕量級鎖、重量級鎖適用於不一樣的併發場景

偏向鎖:無實際競爭,且未來只有第一個申請鎖的線程會使用鎖。 輕量級鎖:無實際競爭,多個線程交替使用鎖;容許短期的鎖競爭。 重量級鎖:有實際競爭,且鎖競爭時間長。 另外,若是鎖競爭時間短,可使用自旋鎖進一步優化輕量級鎖、重量級鎖的性能,減小線程切換。 若是鎖競爭程度逐漸提升(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,可以提升系統的總體性能。

鎖膨脹的過程:只有一個線程進入臨界區(偏向鎖),多個線程交替進入臨界區(輕量級鎖),多線程同時進入臨界區(重量級鎖)。

AQS

AQS便是AbstractQueuedSynchronizer,一個用來構建鎖和同步工具的框架,包括經常使用的ReentrantLock、CountDownLatch、Semaphore等。 AbstractQueuedSynchronizer是一個抽象類,主要是維護了一個int類型的state屬性和一個非阻塞、先進先出的線程等待隊列;其中state是用volatile修飾的,保證線程之間的可見性,隊列的入隊和出對操做都是無鎖操做,基於自旋鎖和CAS實現;另外AQS分爲兩種模式:獨佔模式和共享模式,像ReentrantLock是基於獨佔模式模式實現的,CountDownLatch、CyclicBarrier等是基於共享模式。

線程池

若是併發的線程數量不少,而且每一個線程都是執行一個時間很短的任務就結束了,這樣頻繁建立線程就會大大下降系統的效率,由於頻繁建立線程和銷燬線程須要時間。 線程池的產生和數據庫的鏈接池相似,系統啓動一個線程的代價是比較高昂的,若是在程序啓動的時候就初始化必定數量的線程,放入線程池中,在須要是使用時從池子中去,用完再放回池子裏,這樣能大大的提升程序性能,再者,線程池的一些初始化配置,也能夠有效的控制系統併發的數量,防止由於消耗過多的內存,而把服務器累趴下。

經過Executors工具類能夠建立各類類型的線程池,以下爲常見的四種:

  • newCachedThreadPool :大小不受限,當線程釋放時,可重用該線程;
  • newFixedThreadPool :大小固定,無可用線程時,任務需等待,直到有可用線程;
  • newSingleThreadExecutor :建立一個單線程,任務會按順序依次執行;
  • newScheduledThreadPool:建立一個定長線程池,支持定時及週期性任務執行

使用線程池的好處

  • 減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
  • 運用線程池能有效的控制線程最大併發數,能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。
  • 對線程進行一些簡單的管理,好比:延時執行、定時循環執行的策略等,運用線程池都能進行很好的實現

線程池都有哪幾種工做隊列

一、ArrayBlockingQueue 是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。 二、LinkedBlockingQueue 一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列 三、SynchronousQueue 一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。 四、PriorityBlockingQueue 一個具備優先級的無限阻塞隊列。

參考

Java 多線程

Java併發:volatile內存可見性和指令重排

併發編程的鎖機制:synchronized和lock

淺談偏向鎖、輕量級鎖、重量級鎖

獲取最新資訊,請關注微信公衆號:南強說晚安

我在參加掘金技術徵文 徵文活動地址

相關文章
相關標籤/搜索