1.1 介紹下 synchronizedjava
synchronized關鍵字解決的是多個線程之間訪問資源的同步性,synchronized關鍵字能夠保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。面試
另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的 Mutex Lock 來實現的,Java 的線程是映射到操做系統的原生線程之上的。sql
若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的 synchronized 效率低的緣由。編程
慶幸的是在 Java 6 以後 Java 官方對從 JVM 層面對synchronized 較大優化,引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷,因此如今的 synchronized 鎖效率也優化得很不錯了。數組
1.2 實際怎麼使用 synchronized ,在項目中用到了嗎安全
synchronized關鍵字最主要的三種使用方式:多線程
面試中面試官常常會說:「單例模式瞭解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現單利模式的原理唄!」架構
雙重校驗鎖實現對象單例(線程安全)併發
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判斷對象是否已經實例過,沒有實例化過才進入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外,須要注意 uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要。框架
uniqueInstance 採用 volatile 關鍵字修飾也是頗有必要的, uniqueInstance = new Singleton(); 這段代碼實際上是分爲三步執行:
可是因爲 JVM 具備指令重排的特性,執行順序有可能變成 1->3->2。指令重排在單線程環境下不會出先問題,可是在多線程環境下會致使一個線程得到尚未初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用 getUniqueInstance() 後發現 uniqueInstance 不爲空,所以返回 uniqueInstance,但此時 uniqueInstance 還未被初始化。
使用 volatile 能夠禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。
1.3 講一下 synchronized 關鍵字的底層原理
synchronized 關鍵字底層原理屬於 JVM 層面。
① synchronized 同步語句塊的狀況
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代碼塊");
}
}
}
經過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息
從上面咱們能夠看出:
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。 當執行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在於每一個Java對象的對象頭中,synchronized 鎖即是經過這種方式獲取鎖的,也是爲何Java中任意對象能夠做爲鎖的緣由) 的持有權.當計數器爲0則能夠成功獲取,獲取後將鎖計數器設爲1也就是加1。相應的在執行 monitorexit 指令後,將鎖計數器設爲0,代表鎖被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另一個線程釋放爲止。
② synchronized 修飾方法的的狀況
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修飾的方法並無 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法,JVM 經過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否聲明爲同步方法,從而執行相應的同步調用。
1.4 說說 JDK1.6 以後的synchronized 關鍵字底層作了哪些優化,能夠詳細介紹一下這些優化嗎
JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減小鎖操做的開銷。
鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖能夠升級不可降級,這種策略是爲了提升得到鎖和釋放鎖的效率。
1.5 談談 synchronized和ReenTrantLock 的區別
① 二者都是可重入鎖
二者都是可重入鎖。「可重入鎖」概念是:本身能夠再次獲取本身的內部鎖。好比一個線程得到了某個對象的鎖,此時這個對象鎖尚未釋放,當其再次想要獲取這個對象的鎖的時候仍是能夠獲取的,若是不可鎖重入的話,就會形成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,因此要等到鎖的計數器降低爲0時才能釋放鎖。
② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API
synchronized 是依賴於 JVM 實現的,前面咱們也講到了 虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了不少優化,可是這些優化都是在虛擬機層面實現的,並無直接暴露給咱們。ReenTrantLock 是 JDK 層面實現的(也就是 API 層面,須要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),因此咱們能夠經過查看它的源代碼,來看它是如何實現的。
③ ReenTrantLock 比 synchronized 增長了一些高級功能
相比synchronized,ReenTrantLock增長了一些高級功能。主要來講主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖能夠綁定多個條件)
若是你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。
④ 性能已不是選擇標準
2.1 講一下Java內存模型
在 JDK1.2 以前,Java的內存模型實現老是從主存(即共享內存)讀取變量,是不須要進行特別的注意的。而在當前的 Java 內存模型下,線程能夠把變量保存本地內存(好比機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能形成一個線程在主存中修改了一個變量的值,而另一個線程還繼續使用它在寄存器中的變量值的拷貝,形成數據的不一致。
要解決這個問題,就須要把變量聲明爲volatile,這就指示 JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。
說白了,volatile關鍵字的主要做用就是保證變量的可見性而後還有一個做用是防止指令重排序。
2.2 說說 synchronized 關鍵字和 volatile 關鍵字的區別
synchronized關鍵字和volatile關鍵字比較
3.1 爲何要用線程池?
線程池提供了一種限制和管理資源(包括執行一個任務)。 每一個線程池還維護一些基本統計信息,例如已完成任務的數量。
這裏借用《Java併發編程的藝術》提到的來講一下使用線程池的好處:
3.2 實現Runnable接口和Callable接口的區別
若是想讓線程池執行任務的話須要實現的Runnable接口或Callable接口。 Runnable接口或Callable接口實現類均可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執行。二者的區別在於 Runnable 接口不會返回結果可是 Callable 接口能夠返回結果。
備註: 工具類Executors能夠實現Runnable對象和Callable對象之間的相互轉換。(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
3.3 執行execute()方法和submit()方法的區別是什麼呢?
1)execute()方法用於提交不須要返回值的任務,因此沒法判斷任務是否被線程池執行成功與否;
2)submit()方法用於提交須要返回值的任務。線程池會返回一個future類型的對象,經過這個future對象能夠判斷任務是否執行成功,而且能夠經過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前線程一段時間後當即返回,這時候有可能任務沒有執行完。
3.4 如何建立線程池
《阿里巴巴Java開發手冊》中強制線程池不容許使用 Executors 去建立,而是經過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險**
Executors 返回線程池對象的弊端以下:
方式一:經過構造方法實現
方式二:經過Executor 框架的工具類Executors來實現
咱們能夠建立三種類型的ThreadPoolExecutor:
對應Executors工具類中的方法如圖所示:
4.1 介紹一下Atomic 原子類
Atomic 翻譯成中文是原子的意思。在化學上,咱們知道原子是構成通常物質的最小單位,在化學反應中是不可分割的。在咱們這裏 Atomic 是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程干擾。
因此,所謂原子類說簡單點就是具備原子/原子操做特徵的類。
併發包java.util.concurrent的原子類都存放在java.util.concurrent.atomic下,以下圖所示。
4.2 JUC 包中的原子類是哪4類?
基本類型
使用原子的方式更新基本類型
數組類型
使用原子的方式更新數組裏的某個元素
引用類型
對象的屬性修改類型
4.3 講講 AtomicInteger 的使用
AtomicInteger 類經常使用方法
public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //若是輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。
AtomicInteger 類的使用示例
使用 AtomicInteger 以後,不用對 increment() 方法加鎖也能夠保證線程安全。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger以後,不須要對該方法加鎖,也能夠實現線程安全。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
4.4 能不能給我簡單介紹一下 AtomicInteger 類的原理
AtomicInteger 線程安全原理簡單分析
AtomicInteger 類的部分源碼:
// setup to use Unsafe.compareAndSwapInt for updates(更新操做時提供「比較並替換」的做用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操做,從而避免 synchronized 的高開銷,執行效率大爲提高。
CAS的原理是拿指望的值和本來的一個值做比較,若是相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到「原來的值」的內存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內存中可見,所以 JVM 能夠保證任什麼時候刻任何線程總能拿到該變量的最新值。
5.1 AQS 介紹
AQS的全稱爲(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面。
AQS是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用普遍的大量的同步器,好比咱們提到的ReentrantLock,Semaphore,其餘的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基於AQS的。固然,咱們本身也能利用AQS很是輕鬆容易地構造出符合咱們本身需求的同步器。
5.2 AQS 原理分析
AQS 原理這部分參考了部分博客,在5.2節末尾放了連接。
在面試中被問到併發知識的時候,大多都會被問到「請你說一下本身對於AQS原理的理解」。下面給你們一個示例供你們參加,面試不是背題,你們必定要假如本身的思想,即便加入不了本身的思想也要保證本身可以通俗的講出來而不是背出來。
下面大部份內容其實在AQS類註釋上已經給出了,不過是英語看着比較吃力一點,感興趣的話能夠看看源碼。
5.2.1 AQS 原理概覽
AQS核心思想是,若是被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工做線程,而且將共享資源設置爲鎖定狀態。若是被請求的共享資源被佔用,那麼就須要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。
CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node)來實現鎖的分配。
看個AQS(AbstractQueuedSynchronizer)原理圖:
AQS使用一個int成員變量來表示同步狀態,經過內置的FIFO隊列來完成獲取資源線程的排隊工做。AQS使用CAS對該同步狀態進行原子操做實現對其值的修改。
private volatile int state;//共享變量,使用volatile修飾保證線程可見性
狀態信息經過procted類型的getState,setState,compareAndSetState進行操做
//返回同步狀態的當前值
protected final int getState() {
return state;
}
// 設置同步狀態的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操做)將同步狀態值設置爲給定值update若是當前同步狀態的值等於expect(指望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
5.2.2 AQS 對資源的共享方式
AQS定義兩種資源共享方式
ReentrantReadWriteLock 能夠當作是組合式,由於ReentrantReadWriteLock也就是讀寫鎖容許多個線程同時對某一資源進行讀。
不一樣的自定義同步器爭用共享資源的方式也不一樣。自定義同步器在實現時只須要實現共享資源 state 的獲取與釋放方式便可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。
5.2.3 AQS底層使用了模板方法模式
同步器的設計是基於模板方法模式的,若是須要自定義同步器通常的方式是這樣(模板方法模式很經典的一個應用):
這和咱們以往經過實現接口的方式有很大區別,這是模板方法模式很經典的一個運用。
AQS使用了模板方法模式,自定義同步器時須要重寫下面幾個AQS提供的模板方法:
isHeldExclusively()//該線程是否正在獨佔資源。只有用到condition才須要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。
默認狀況下,每一個方法都拋出UnsupportedOperationException。 這些方法的實現必須是內部線程安全的,而且一般應該簡短而不是阻塞。AQS類中的其餘方法都是final ,因此沒法被其餘類使用,只有這幾個方法能夠被其餘類使用。
以ReentrantLock爲例,state初始化爲0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨佔該鎖並將state+1。此後,其餘線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。固然,釋放鎖以前,A線程本身是能夠重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。
再以CountDownLatch以例,任務分爲N個子線程去執行,state也初始化爲N(注意N要與線程個數一致)。這N個子線程是並行執行的,每一個子線程執行完後countDown()一次,state會CAS(Compare and Swap)減1。等到全部子線程都執行完後(即state=0),會unpark()主調用線程,而後主調用線程就會從await()函數返回,繼續後餘動做。
通常來講,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種便可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock。
5.3 AQS 組件總結
歡迎工做一到五年的Java工程師朋友們加入Java高級架構:706315665
羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,
MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)
合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!