天天看點

Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

我的部落格中有更多後端開發面試題,點我檢視!

Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

并發大全

2萬字參透并發程式設計

  • jvm

    中線程分為哪些狀态
  • 在執行

    Thread.start()

    方法後,線程是不是馬上運作。

多線程導論

  • 原子性是指在一個操作中就是

    cpu

    不可以在中途暫停然後再排程,既不被中斷操作,要不執行完成,要不就不執行。
  • 可見性是指當多個線程通路同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
  • 有序性即程式執行的順序按照代碼的先後順序執行。

多線程設計模式解讀—Producer-Consumer模式

Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

請簡述一下線程的sleep()方法和yield()方法有什麼差別?

  • sleep()

    方法給其他線程運作機會時不考慮線程的優先級,是以會給低優先級的線程以運作的機會;yield()方法隻會給相同優先級或更高優先級的線程以運作的機會
  • 線程執行

    sleep()

    方法後轉入阻塞(blocked)狀态,而執行

    yield()

    方法後轉入就緒(ready)狀态
  • sleep()

    方法聲明抛出InterruptedException,而

    yield()

    方法沒有聲明任何異常

在Java中wait和seelp方法的不同

  • wait

    作用于對象,

    sleep

    作用于 Thread
  • wait

    釋放對象鎖,

    sleep

    不釋放對象鎖
  • wait

    運作于同步方法或同步代碼塊,

    sleep

    可運作于任何地方
  • wait

    不需要捕獲異常,

    sleep

    需要捕獲異常

Thread類中的yield方法有什麼作用?

暫停目前線程,将目前線程從

Running

狀态轉到

Ready

狀态,讓出控制權給其他線程

談談 wait / notify 關鍵字的了解

  • wait / notify 存在于所有對象;

    使用時需要

    synchronized

    ,否則

    IllegalMonitorStateException

  • 調用

    wait

    方法會讓目前線程阻塞,讓出對象鎖;若

    wait

    有設定時間參數,到時間後自動喚醒;
  • notify

    一次喚醒一個等待的線程;

    notifyAll

    一次性喚醒所有該對象上的等待線程。

為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?

wait,notify,notifyAll,sleep這些方法都跟線程的狀态變化有關,為什麼jdk把前三個方法放在Object類裡面,而把sleep放在Thread類裡面?
  • Java 内置鎖機制中,鎖是對象級而不是線程級,任何對象都能建立鎖;
  • 一個線程可以有多個鎖,若跟線程綁定可能會不夠用。

程序和線程之間的通信方式

  • 程序:無名管道、有名管道、信号、共享記憶體、消息隊列、信号量
  • 線程:互斥量、讀寫鎖、自旋鎖、線程信号、條件變量

建立線程的方法

  • 繼承

    Thread

    類建立線程
  • 實作

    Runnable

    接口建立線程
  • 使用

    Callable

    Future

    建立線程
  • 使用線程池例如用

    Executor

    架構

如何停止一個線程

  • 使用退出标志,使線程正常退出,也就是當

    run

    方法完成後線程終止。
  • 使用

    interrupt

    方法中斷線程。
  • 不推薦使用

    stop、suspend及resume

    方法。相當于電腦斷電關機一樣,是不安全的方法。

線程同步的方法。

同步的實作方面有兩種,分别是

synchronized,wait

notify

  • wait()

    :使一個線程處于等待狀态,并且釋放所持有的對象的lock。

    sleep()

    :使一個正在運作的線程處于睡眠狀态,是一個靜态方法,調用此方法要捕捉InterruptedException異常。
  • notify()

    :喚醒一個處于等待狀态的線程,注意的是在調用此方法的時候,并不能确切的喚醒某一個等待狀态的線程,而是由JVM确定喚醒哪個線程,而且不是按優先級。
  • notifyAll()

    :喚醒所有處入等待狀态的線程,注意并不是給所有喚醒線程一個對象的鎖,而是讓它們競争。

原子操作的實作原理是通過CAS實作的。

一般問CAS底層原理,我遇到這種問題,直接說比較并更新的過程,底層實作調用了unsafe類,unsafe類是用C++實作的,沒研究過,代碼。糊弄過去的

countDownLatch

countDownLatch

是在java1.5被引入,跟它一起被引入的工具類還有

CyclicBarrier

Semaphore

concurrentHashMap

BlockingQueue

  • countDownLatch

    這個類使一個線程等待其他線程各自執行完畢後再執行。
  • 是通過一個計數器來實作的,計數器的初始值是線程的數量。每當一個線程執行完畢後,計數器的值就-1,當計數器的值為0時,表示所有線程都執行完畢,然後在閉鎖上等待的線程就可以恢複工作了。

CountDownLatch和CyclicBarrier差別:

  • countDownLatch

    是一個計數器,線程完成一個記錄一個,計數器遞減,隻能隻用一次
  • CyclicBarrier

    的計數器更像一個閥門,需要所有線程都到達,然後繼續執行,計數器遞增,提供reset功能,可以多次使用

Runnable和Thread差別

1.

Runnable

Thread

相比優點有:

  • 由于Java不允許多繼承,是以實作了

    Runnable

    接口可以再繼承其他類,但是

    Thread

    明顯不可以
  • Runnable

    可以實作多個相同的程式代碼的線程去共享同一個資源,而

    Thread

    并不是不可以,而是相比于

    Runnable

    來說,不太适合,具體原因文章中有。
當以

Thread

方式去實作資源共享時,實際上源碼内部是将

Thread

向下轉型為了

Runnable

,實際上内部依然是以

Runnable

形式去實作的資源共享

2.

Runnable

為什麼不可以直接run

闡述文章中已有,

Runnable

其實相對于一個Task,并不具有線程的概念,如果你直接去調用

Runnable

run

,其實就是相當于直接在主線程中執行了一個函數而已,并未開啟線程去執行,帶來的“災害”文章中有。

Java線程池中submit() 和 execute()方法有什麼差別?

submit()

可以擷取傳回值,

execute()

不行。

RUN()

Start()

方法差別是,

Start()

會重新建立一個線程去運作,而

RUN()

是隻是去單獨的調用。

可以用RUN()完成線程的同步

CompletableFuture,這個是JDK1.8裡的新特性,通過它怎麼實作多線程并發控制?

多線程實操

面試題,子線程10次子線程2執行20次與主線程100次來回循環執行50次

如何讓多個線程交替執行?

一種是基于

synchronized

wait/notify

,另外一種是基于

Lock

Condition(ReentrantLock)

.

  • 循環内加鎖
  • 判斷共享狀态變量,如果狀态值表示還沒輪到目前線程執行,則調用調用鎖對象的

    wait

    方法
  • 等待狀态變化的線程被喚醒,執行任務,然後改變狀态,然後調用鎖對象的

    notify

    方法

使用者自己定義一個類,用這個類繼承

thread

類,實作

thread

run

方法,然後再使用使用者自己定義這個類建立一個對象,最後調用

thread

類的

start

方法激活線程。

VOLATILE

可見性,是指線程之間的可見性,一個線程修改的狀态對另一個線程是可見的。
  • 記憶體屏障
  • 記憶體可見

1.當寫一個

volatile

變量時,JMM會把該線程對應的本地記憶體中的變量強制重新整理到主記憶體中去;

2.這個寫操作會導緻其他線程中的緩存無效。

  • 禁止指令重排序優化。
  • 不能保證原子性
volatile對于單個的共享變量的讀/寫具有原子性,但是像num++這種複合操作,volatile無法保證其原子性,當然文中也提出了解決方案,就是使用并發包中的原子操作類,通過循環CAS地方式來保證num++操作的原子性

volatile

隻保證可見性 有序性,不保證原子性的原因,在于寄存器無法被

volatile

重新整理

  • 你說的本地緩存,指的是線程工作記憶體,不是cpu的寄存器
  • 線程工作記憶體,确實被設定為無效,下次直接從記憶體裡面取,但是寄存器在之前已經進去了,這是導緻

    volatile

    不能保證原子性的原因。如果

    volatile

    也能把寄存器的值給設定為無效,那麼原子性就保證了

LOCK字首指令篇(一):Java 之深入淺出解讀 volatile

原創|《菜鳥讀并發》java記憶體模型之volatile深入解讀

一文帶你了解Java中Lock的實作原理

Synchronized

volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻隻允許一條線程操作。

  • 既可以修飾方法也可以修飾代碼塊

對于同步方法,JVM采用

ACC_SYNCHRONIZED

标記符來實作同步。 對于同步代碼塊。JVM采用

monitorenter

monitorexit

兩個指令來實作同步。

  • 同步代碼塊使用

    monitorenter

    monitorexit

    兩個指令實作。可以把執行

    monitorenter

    指令了解為加鎖,執行

    monitorexit

    了解為釋放鎖。 每個對象維護着一個記錄着被鎖次數的計數器。未被鎖定的對象的該計數器為0,當一個線程獲得鎖(執行

    monitorenter

    )後,該計數器自增變為 1 ,當同一個線程再次獲得該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行

    monitorexit

    指令)的時候,計數器再自減。當計數器為0的時候。鎖将被釋放,其他線程便可以獲得鎖。
Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal
Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

sychronized

是java中最基本同步互斥的手段,可以修飾代碼塊,方法,類.

在修飾代碼塊的時候需要一個

reference

對象作為鎖的對象.

在修飾方法的時候預設是目前對象作為鎖的對象.

在修飾類時候預設是目前類的

Class

對象作為鎖的對象.

synchronized

會在進入同步塊的前後分别形成

monitorenter

monitorexit

位元組碼指令.在執行

monitorenter

指令時會嘗試擷取對象的鎖,如果此沒對象沒有被鎖,或者此對象已經被目前線程鎖住,那麼鎖的計數器加一,每當

monitorexit

被鎖的對象的計數器減一.直到為0就釋放該對象的鎖.由此

synchronized

是可重入的,不會出現自己把自己鎖死.

sychronized

是java中最基本同步互斥的手段,可以修飾代碼塊,方法,類.

在修飾代碼塊的時候需要一個

reference

對象作為鎖的對象.

在修飾方法的時候預設是目前對象作為鎖的對象.

在修飾類時候預設是目前類的Class對象作為鎖的對象.

synchronized

會在進入同步塊的前後分别形成

monitorenter

monitorexit

位元組碼指令.在執行

monitorenter

指令時會嘗試擷取對象的鎖,如果此沒對象沒有被鎖,或者此對象已經被目前線程鎖住,那麼鎖的計數器加一,每當

monitorexit

被鎖的對象的計數器減一.直到為0就釋放該對象的鎖.由此

synchronized

是可重入的,不會出現自己把自己鎖死.

synchronized 和 ReentrantLock有什麼差別

除了

synchronized

的功能,多了三個進階功能.

等待可中斷,公平鎖,綁定多個

Condition

.

  • 等待可中斷:在持有鎖的線程長時間不釋放鎖的時候,等待的線程可以選擇放棄等待. tryLock(long timeout, TimeUnit unit)
  • 公平鎖:按照申請鎖的順序來一次獲得鎖稱為公平鎖.

    synchronized

    的是非公平鎖,

    ReentrantLock

    可以通過構造函數實作公平鎖. new RenentrantLock(boolean fair)
  • 綁定多個

    Condition

    通過多次

    newCondition

    可以獲得多個

    Condition

    對象,可以簡單的實作比較複雜的線程同步的功能.通過

    await(),signal()

    ;

JAVA兩個字元串如何比較大小

compareTo() 的傳回值是int, 它是先比較對應字元的大小(ASCII碼順序)

Lock與synchronized有以下差別:

  • synchronized

    會自動釋放鎖,而

    Lock

    必須手動釋放鎖。
  • Lock

    可以讓等待鎖的線程響應中斷,而

    synchronized

    不會,線程會一直等待下去。
  • 通過

    Lock

    可以知道線程有沒有拿到鎖,而

    synchronized

    不能。
  • Lock

    能提高多個線程讀操作的效率。
  • synchronized

    能鎖住類、方法和代碼塊,而

    Lock

    是塊範圍内的。

volatile和synchronized的差別

  • volatile

    synchronized

    執行成本更低,因為它不會引起線程上下文的切換和排程
  • volatile

    本質是在告訴

    jvm

    目前變量在寄存器(工作記憶體)中的值是不确定的,需要從主存中讀取;

    synchronized

    則是鎖定目前變量,隻有目前線程可以通路該變量,其他線程被阻塞住。
  • volatile

    隻能用來修飾變量,而

    synchronized

    可以用來修飾變量、方法、和類。
  • volatile

    可以實作變量的可見性,禁止重排序和單次讀/寫的原子性;而

    synchronized

    則可以變量的可見性,禁止重排序和原子性。
  • volatile

    不會造成線程的阻塞;

    synchronized

    可能會造成線程的阻塞。
  • volatile

    标記的變量不會被編譯器優化;

    synchronized

    标記的變量可以被編譯器優化。

深入解讀synchronized和ReentrantLock

  • synchronized

    阻塞的線程狀态為

    BLOCKED

  • ReentrantLock

    阻塞的線程狀态為

    BLOCKED

    或者

    WAITTING

深入解讀synchronized和ReentrantLock

synchronized 和 ReentrantLock有什麼差別

除了

synchronized

的功能,多了三個進階功能.

等待可中斷,公平鎖,綁定多個

Condition

.

  • 等待可中斷:在持有鎖的線程長時間不釋放鎖的時候,等待的線程可以選擇放棄等待.

    tryLock(long timeout, TimeUnit unit)

  • 公平鎖:按照申請鎖的順序來一次獲得鎖稱為公平鎖.

    synchronized

    的是非公平鎖,

    ReentrantLock

    可以通過構造函數實作公平鎖.

    new RenentrantLock(boolean fair)

  • 綁定多個

    Condition

    通過多次

    newCondition

    可以獲得多個

    Condition

    對象,可以簡單的實作比較複雜的線程同步的功能.通過

    await(),signal();

鎖更新

synchronized

鎖更新原理:在鎖對象的對象頭裡面有一個

threadid

字段,在第一次通路的時候

threadid

為空,

jvm

讓其持有偏向鎖,并将

threadid

設定為其線程

id

,再次進入的時候會先判斷

threadid

是否與其線程

id

一緻,如果一緻則可以直接使用此對象,如果不一緻,則更新偏向鎖為輕量級鎖,通過自旋循環一定次數來擷取鎖,執行一定次數之後,如果還沒有正常擷取到要使用的對象,此時就會把鎖從輕量級更新為重量級鎖,此過程就構成了

synchronized

鎖的更新。

鎖的更新的目的:鎖更新是為了減低了鎖帶來的性能消耗。在

Java 6

之後優化

synchronized

的實作方式,使用了偏向鎖更新為輕量級鎖再更新到重量級鎖的方式,進而減低了鎖帶來的性能消耗。

偏向鎖

把MARKWORD的threadID改成自己的threadID

在鎖競争不強烈的情況下,通常一個線程會多次擷取同一個鎖,為了減少擷取鎖的代價 引入了偏向鎖,會在

Java

對象頭中記錄擷取鎖的線程的

threadID

  • 當線程發現對象頭的

    threadID

    存在時。判斷與目前線程是否是同一線程。
  • 如果是則不需要再次加、解鎖。
  • 如果不是,則判斷

    threadID

    是否存活。不存活:設定為無鎖狀态,其他線程競争設定偏向鎖。存活:查找

    threadID

    堆棧資訊判斷是否需要繼續持有鎖。需要持有則更新

    threadID

    線程的鎖為輕量級鎖。不需要持有則撤銷鎖,設定為無鎖狀态等待其它線程競争。

不是同一個對象就更新為輕量級鎖

因為偏向鎖的撤銷操作還是比較重的,導緻進入安全點,是以在競争比較激烈時,會影響性能,可以使用

-XX:-UseBiasedLocking=false

禁用偏向鎖。

輕量級鎖

當偏向鎖更新為輕量級鎖時,其它線程嘗試通過CAS方式設定對象頭來擷取鎖。

給MARKWORD一個指針,指向自己的lock record

  • 會先在目前線程的棧幀中設定

    Lock Record

    ,用于存儲目前對象頭中的

    mark word

    的拷貝。
  • 複制

    mark word

    的内容到

    lock record

    ,并嘗試使用

    cas

    mark word

    的指針指向

    lock record

  • 如果替換成功,則擷取偏向鎖
  • 替換不成功,則會自旋重試一定次數。
  • 自旋一定次數或有新的線程來競争鎖時,輕量級鎖膨脹為重量級鎖。
重量級鎖

自旋是消耗CPU的,是以在自旋一段時間,或者一個線程在自旋時,又有新的線程來競争鎖,則輕量級鎖會膨脹為重量級鎖。重量級鎖,通過

monitor

實作,

monitor

底層實際是依賴作業系統的

mutex lock

(互斥鎖)實作。需要從使用者态,切換為核心态,成本比較高

CAS

CAS

可能遇到

ABA

問題,即記憶體中的值為

A

,變為

B

後,又變為了

A

,此時

A

為新值,不應該替換。可以采取:

A-1

B-2

A-3

的方式來避免這個問題

ABA問題是一種異常現象:如果在算法中的節點可以被循環使用,那麼在使用“比較并交換”指令時就可能出現這個問題(如果在沒有垃圾回收機制的環境 中)。在CAS操作中将判斷“V的值是否仍然為A?”,并且如果是的話就繼續執行更新操作。在大多數情況下,這種判斷是足夠的。然而,有時候還需要知道 “自從上次看到V的值為A以來,這個值是否發生了變化?”在某些算法中,如果V值首先由A程式設計B,在由B程式設計A,那麼仍然被認為發生了變化,并需要重新執 行算法中的某些步驟。

如果在算法中采用自己的方式來管理節點對象的記憶體,那麼可能出現

ABA

問題。在這種情況下,即使連結清單的頭結點仍然隻想之前觀察到的節點,那麼也不足 以說明連結清單的内容沒有發生變化。

如果通過垃圾回收器來管理連結清單節點仍然無法避免

ABA

問題,那麼還有一個相對簡單的解決方法:不是隻是更新某個引用的值, 而是更新兩個值,包含一個引用和一個版本号。即使這個值由

A

變成

B

,然後又變為

A

,版本号也将是不同的。

AtomicStampedReference

以 及

AtomicMarkableReference

支援在兩個變量上執行原子的條件更新。

AtomicStampedReference

将更新一個“對象 —-引用”二進制組,通過在引用上加上“版本号”,進而避免

ABA

問題。類似地,

AtomicMarkableReference

将更新一個“對象引用—- 布爾值”二進制組,在某些算法中将通過這種二進制組使節點儲存在連結清單中同時又将其标記為“已删除節點”。

參考

  • Java synchronized 原理從開始到放棄
  • 再有人問你synchronized是什麼,就把這篇文章發給他。

ReentrantLock

以對象的方式來操作對象鎖.相對于

sychronized

需要在

finally

中去釋放鎖,需要手動釋放。

ReentrantLock

裡面的

true

false

就是 公平鎖和非公平鎖的實作

ReentrantLock

重入鎖,是實作

Lock

接口的一個類,也是在實際程式設計中使用頻率很高的一個鎖,支援重入性,表示能夠對共享資源能夠重複加鎖,即目前線程擷取該鎖再次擷取不會被阻塞。在

java

關鍵字

synchronized

隐式支援重入性,

synchronized

通過擷取自增,釋放自減的方式實作重入。與此同時,

ReentrantLock

還支援公平鎖和非公平鎖兩種方式。

ReentrantLock

state

字段表示目前線程重入鎖的次數,當state為0時候,表示鎖是空閑的。

compareAndSetState(0, 1)

表示當

state

,直接更新為

1

,即空閑時候,直接

CAS

上鎖;線程如果更新成功了,

setExclusiveOwnerThread()

設定目前線程為鎖的占有

可重入鎖原理

重入鎖實作可重入性原理或機制是:每一個鎖關聯一個線程持有者和計數器,當計數器為 0 時表示該鎖沒有被任何線程持有,那麼任何線程都可能獲得該鎖而調用相應的方法;

當某一線程請求成功後,JVM會記下鎖的持有線程,并且将計數器置為 1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,如果計數器為 0,則釋放該鎖

ReentrantLock

還是使用上面的例子,

ReentrantLock

的使用很簡單,使用

new ReentrantLock(boolean isFair)

來建立一個公平或者非公平鎖,使用.lock()方法加鎖。使用.unlock()方法,是以我們就從這三個方法入手,來簡單的看一下它是如何實作鎖的。

ReentrantLock

主要是用了一個巧妙的資料結構(帶頭尾指針的雙連結清單)和

CAS

加自旋以及使用

LockSupport

park

,

unpark

(類似

wait

,

notify

)來實作加鎖和解鎖。

CAS原理

AQS (AbstractQueuedSynchronizer)

由一個state和cas 阻塞隊列組成

什麼是AQS

AQS(AbstractQueuedSynchronizer)

AQS

是JDK下提供的一套用于實作基于FIFO等待隊列的阻塞鎖和相關的同步器的一個同步架構。這個抽象類被設計為作為一些可用原子

int

值來表示狀态的同步器的基類。如果你有看過類似

CountDownLatch

類的源碼實作,會發現其内部有一個繼承了

AbstractQueuedSynchronizer

的内部類

Sync

。可見

CountDownLatch

是基于

AQS

架構來實作的一個同步器.類似的同步器在

JUC

下還有不少。(eg.

Semaphore

)

AQS用法

如上所述,

AQS

管理一個關于狀态資訊的單一整數,該整數可以表現任何狀态。比如,

Semaphore

用它來表現剩餘的許可數,

ReentrantLock

用它來表現擁有它的線程已經請求了多少次鎖;

FutureTask

用它來表現任務的狀态(尚未開始、運作、完成和取消)

AQS AbstractQueuedSynchronizer

J.U.C

是基于

AQS

實作的,

AQS

是一個同步器,設計模式是模闆模式。

  • 核心資料結構:雙向連結清單 +

    state

    (鎖狀态)
  • 底層操作:

    CAS

    Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
           

AQS

中的

int

類型的

state

值,這裡就是通過

CAS

(樂觀鎖)去修改

state

的值。

lock

的基本操作還是通過樂觀鎖來實作的。

擷取鎖通過

CAS

,那麼沒有擷取到鎖,等待擷取鎖是如何實作的?我們可以看一下

else

分支的邏輯,

acquire

方法:

  • tryAcquire

    :會嘗試再次通過

    CAS

    擷取一次鎖。
  • addWaiter

    :通過自旋

    CAS

    ,将目前線程加入上面鎖的雙向連結清單(等待隊列)中。
  • acquireQueued

    :通過自旋,判斷目前隊列節點是否可以擷取鎖。

可以看到,當目前線程到頭部的時候,嘗試

CAS

更新鎖狀态,如果更新成功表示該等待線程擷取成功。從頭部移除。基本可以确認,釋放鎖就是對

AQS

中的狀态值

State

進行修改。同時更新下一個連結清單中的線程等待節點。

Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

可以看到在整個實作過程中,

lock

大量使用

CAS

+自旋。是以根據

CAS

特性,

lock

建議使用在低鎖沖突的情況下。目前java1.6以後,官方對

synchronized

做了大量的鎖優化(偏向鎖、自旋、輕量級鎖)。是以在非必要的情況下,建議使用

synchronized

做同步操作。

AQS

定義兩種資源共享方式:

Exclusive(

獨占,隻有一個線程能執行,如

ReentrantLock

Share

(共享,多個線程可同時執行,如

Semaphore

/

CountDownLatch

)。

先大體說一下同步等待隊列的特點:先進先出。擷取鎖失敗的線程構造節點加入隊列尾部,阻塞自己,等待喚醒。執行完成的線程從頭部移出隊列,并喚醒後續節點的線程。頭結點是目前擷取到鎖的線程

公平鎖:

Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

非公平鎖:

Java多線程導論及延申我的部落格中有更多後端開發面試題,點我檢視!多線程導論VOLATILESynchronizedReentrantLockThreadLocal

偏向鎖

鎖不存在多線程競争,并且應由一個線程多次獲得鎖

當線程通路同步塊時,會使用 CAS 将線程 ID 更新到鎖對象的 MarkWord 中,如果更新成功則獲得偏向鎖,并且之後每次進入這個對象鎖相關的同步塊時都不需要再次擷取鎖了。(檢查thread id)

輕量級鎖

當代碼進入同步塊時,如果同步對象為無鎖狀态時,目前線程會在棧幀中建立一個鎖記錄( LockRecord)區域,同時将鎖對象的對象頭中 MarkWord 拷貝到鎖記錄中,再嘗試使用 CAS 将 MarkWord 更新為指向鎖記錄的指針。

如果更新成功,目前線程就獲得了鎖。

如果更新失敗 JVM 會先檢查鎖對象的

MarkWord

是否指向目前線程的鎖記錄。

如果是則說明目前線程擁有鎖對象的鎖,可以直接進入同步塊。

不是則說明有其他線程搶占了鎖,如果存在多個線程同時競争一把鎖,輕量鎖就會膨脹為重量鎖。

輕量鎖能提升性能的原因是:認為大多數鎖在整個同步周期都不存在競争,是以使用 CAS 比使用互斥開銷更少。但如果鎖競争激烈,輕量鎖就不但有互斥的開銷,還有 CAS 的開銷,甚至比重量鎖更慢。

會自旋,否則進行鎖膨脹

重量級鎖

适應性自旋

在使用

CAS

時,如果操作失敗,

CAS

會自旋再次嘗試。由于自旋是需要消耗

CPU

資源的,是以如果長期自旋就白白浪費了

CPU

。 JDK1.6加入了适應性自旋:

如果某個鎖自旋很少成功獲得,那麼下一次就會減少自旋。

ABA問題(CAS的應用:AtomicStampedReference)

  • 一個變量

    int state = 0

  • 線程

    A

    讀取到

    state = 0

  • 線程

    B

    修改

    state = 1

    再改回

    state = 0

    此時線程 A 再讀取

    state

    ,還是 ,但實際上已經

    state

    已經被修改過。

對于對改動敏感的變量操作,可以使用

AtomicStampedReference

,它會同時儲存時間戳以确認是否發生改動。

參考

  • ReentrantLock原理從開始到放棄

ThreadLocal

Map的Key是弱引用類型(WeakReference),而Value是強引用類型,如果Key被回收,Value卻不會被回收。

ThreadLocal

Synchronized

都是為了解決多線程中相同變量的通路沖突問題,不同的點是

  • Synchronized

    是通過線程等待,犧牲時間來解決通路沖突
  • ThreadLocal

    是通過每個線程單獨一份存儲空間,犧牲空間來解決沖突,并且相比于

    Synchronized

    ThreadLocal

    具有線程隔離的效果,隻有線上程内才能擷取到對應的值,線程外則不能通路到想要的值。

由于

Thread

包含變量

ThreadLocalMap

,是以

ThreadLocalMap

Thread

的生命周期是一樣長,如果都沒有手動删除對應key,都會導緻記憶體洩漏。

但是使用弱引用可以多一層保障:弱引用

ThreadLocal

不會記憶體洩漏,對應的

value

在下一次

ThreadLocalMap

調用

set()

,

get()

,

remove()

的時候會被清除。

是以,

ThreadLocal

記憶體洩漏的根源是:由于

ThreadLocalMap

的生命周期跟

Thread

一樣長,如果沒有手動删除對應

key

就會導緻記憶體洩漏,而不是因為弱引用。

參考

  • ThreadLocal記憶體洩露的原因

實作線程安全的方式

  • 加鎖 利用Synchronized或者ReenTrantLock來對不安全對象進行加鎖,來實作線程執行的串行化,進而保證多線程同時操作對象的安全性,一個是文法層面的互斥鎖,一個是API層面的互斥鎖.
  • 非阻塞同步來實作線程安全。原理就是:通俗點講,就是先進性操作,如果沒有其他線程争用共享資料,那操作就成功了;如果共享資料有争用,産生沖突,那就再采取其他措施(最常見的措施就是不斷地重試,直到成功為止)。這種方法需要硬體的支援,因為我們需要操作和沖突檢測這兩個步驟具備原子性。通常這種指令包括CAS SC,FAI TAS等
  • 線程本地化,一種無同步的方案,就是利用Threadlocal來為每一個線程創造一個共享變量的副本來(副本之間是無關的)避免幾個線程同時操作一個對象時發生線程安全問題。

阻塞隊列

  • ArrayBlockingQueue

    :基于數組實作的一個阻塞隊列,在建立

    ArrayBlockingQueue

    對象時必須制定容量大小。并且可以指定公平性與非公平性,預設情況下為非公平的,即不保證等待時間最長的隊列最優先能夠通路隊列。
  • LinkedBlockingQueue

    :基于連結清單實作的一個阻塞隊列,在建立

    LinkedBlockingQueue

    對象時如果不指定容量大小,則預設大小為

    Integer.MAX_VALUE

  • PriorityBlockingQueue

    :以上2種隊列都是先進先出隊列,而PriorityBlockingQueue卻不是,它會按照元素的優先級對元素進行排序,按照優先級順序出隊,每次出隊的元素都是優先級最高的元素。注意,此阻塞隊列為無界阻塞隊列,即容量沒有上限(通過源碼就可以知道,它沒有容器滿的信号标志),前面2種都是有界隊列。
  • DelayQueue

    :基于

    PriorityQueue

    ,一種延時阻塞隊列,DelayQueue中的元素隻有當其指定的延遲時間到了,才能夠從隊列中擷取到該元素。

    DelayQueue

    也是一個無界隊列,是以往隊列中插入資料的操作(生産者)永遠不會被阻塞,而隻有擷取資料的操作(消費者)才會被阻塞。