偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

讓人討厭的多線程代碼,性能怎么優(yōu)化!

開發(fā) 前端
Java中有兩種加鎖方式,一種是使用synchronized關鍵字,另外一種是concurrent包下面的Lock。

Java 中最煩人的,就是多線程,一不小心,代碼寫的比單線程還慢,這就讓人非常尷尬。

通常情況下,我們會使用 ThreadLocal 實現(xiàn)線程封閉,比如避免 SimpleDateFormat 在并發(fā)環(huán)境下所引起的一些不一致情況。其實還有一種解決方式。通過對parse方法進行加鎖,也能保證日期處理類的正確運行,代碼如圖。

圖片

1. 鎖很壞

但是,鎖這個東西,很壞。

所以,鎖對性能的影響,是非常大的。對資源加鎖以后,資源就被加鎖的線程所獨占,其他的線程就只能排隊等待這個鎖。此時,程序由并行執(zhí)行,變相的變成了順序執(zhí)行,執(zhí)行速度自然就降低了。

下面是開啟了50個線程,使用ThreadLocal和同步鎖方式性能的一個對比。

Benchmark                                 Mode  Cnt     Score      Error   Units
SynchronizedNormalBenchmark.sync thrpt 10 2554.628 ± 5098.059 ops/ms
SynchronizedNormalBenchmark.threadLocal thrpt 10 3750.902 ± 103.528 ops/ms
========去掉業(yè)務影響========
Benchmark Mode Cnt Score Error Units
SynchronizedNormalBenchmark.sync thrpt 10 26905.514 ± 1688.600 ops/ms
SynchronizedNormalBenchmark.threadLocal thrpt 10 7041876.244 ± 355598.686 ops/ms

可以看到,使用同步鎖的方式,性能是比較低的。如果去掉業(yè)務本身邏輯的影響(刪掉執(zhí)行邏輯),這個差異會更大。代碼執(zhí)行的次數(shù)越多,鎖的累加影響越大,對鎖本身的速度優(yōu)化,是非常重要的。

我們都知道,Java 中有兩種加鎖的方式,一種就是常見的synchronized 關鍵字,另外一種,就是使用 concurrent 包里面的 Lock。針對于這兩種鎖,JDK 自身做了很多的優(yōu)化,它們的實現(xiàn)方式也是不同的。

2. synchronied原理

synchronized關鍵字給代碼或者方法上鎖時,都有顯示的或者隱藏的上鎖對象。當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。

  • 給普通方法加鎖時,上鎖的對象是this
  • 給靜態(tài)方法加鎖時,鎖的是class對象。
  • 給代碼塊加鎖,可以指定一個具體的對象作為鎖

monitor,在操作系統(tǒng)里,其實就叫做管程。

那么,synchronized 在字節(jié)碼中,是怎么體現(xiàn)的呢?參照下面的代碼,在命令行執(zhí)行??javac??,然后再執(zhí)行??javap -v -p??,就可以看到它具體的字節(jié)碼。可以看到,在字節(jié)碼的體現(xiàn)上,它只給方法加了一個flag:??ACC_SYNCHRONIZED??。

synchronized void syncMethod(){
System.out.println("syncMethod");
}
======字節(jié)碼=====
synchronized void syncMethod();
descriptor: ()V
flags: ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #4
3: ldc #5
5: invokevirtual #6
8: return

我們再來看下同步代碼塊的字節(jié)碼??梢钥吹剑止?jié)碼是通過monitorenter和monitorexit兩個指令進行控制的。

void syncBlock(){
synchronized (Test.class){
}
}
======字節(jié)碼======
void syncBlock();
descriptor: ()V
flags:
Code:
stack=2, locals=3, args_size=1
0: ldc #2
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any

這兩者雖然顯示效果不同,但他們都是通過monitor來實現(xiàn)同步的。我們可以通過下面這張圖,來看一下monitor的原理。

注意了,下面是面試題目高發(fā)地。

圖片

如圖所示,我們可以把運行時的對象鎖抽象的分成三部分。其中,EntrySet 和WaitSet 是兩個隊列,中間虛線部分是當前持有鎖的線程。我們可以想象一下線程的執(zhí)行過程。

當?shù)谝粋€線程到來時,發(fā)現(xiàn)并沒有線程持有對象鎖,它會直接成為活動線程,進入 RUNNING 狀態(tài)。

接著又來了三個線程,要爭搶對象鎖。此時,這三個線程發(fā)現(xiàn)鎖已經(jīng)被占用了,就先進入 EntrySet 緩存起來,進入 BLOCKED 狀態(tài)。此時,從??jstack??命令,可以看到他們展示的信息都是??waiting for monitor entry??。

"http-nio-8084-exec-120" #143 daemon prio=5 os_prio=31 cpu=122.86ms elapsed=317.88s tid=0x00007fedd8381000 nid=0x1af03 waiting for monitor entry  [0x00007000150e1000]
java.lang.Thread.State: BLOCKED (on object monitor)
at java.io.BufferedInputStream.read(java.base@13.0.1/BufferedInputStream.java:263)
- waiting to lock <0x0000000782e1b590> (a java.io.BufferedInputStream)
at org.apache.commons.httpclient.HttpParser.readRawLine(HttpParser.java:78)
at org.apache.commons.httpclient.HttpParser.readLine(HttpParser.java:106)
at org.apache.commons.httpclient.HttpConnection.readLine(HttpConnection.java:1116)
at org.apache.commons.httpclient.HttpMethodBase.readStatusLine(HttpMethodBase.java:1973)
at org.apache.commons.httpclient.HttpMethodBase.readResponse(HttpMethodBase.java:1735)

處于活動狀態(tài)的線程,執(zhí)行完畢退出了;或者由于某種原因執(zhí)行了wait 方法,釋放了對象鎖,就會進入 WaitSet 隊列。這就是在調用??wait??之前,需要先獲得對象鎖的原因。就像下面的代碼:

synchronized (lock){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

此時,jstack顯示的線程狀態(tài)是 WAITING 狀態(tài),而原因是in Object.wait()。

"wait-demo" #12 prio=5 os_prio=31 cpu=0.14ms elapsed=12.58s tid=0x00007fb66609e000 nid=0x6103 in Object.wait()  [0x000070000f2bd000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(java.base@13.0.1/Native Method)
- waiting on <0x0000000787b48300> (a java.lang.Object)
at java.lang.Object.wait(java.base@13.0.1/Object.java:326)
at WaitDemo.lambda$main$0(WaitDemo.java:7)
- locked <0x0000000787b48300> (a java.lang.Object)
at WaitDemo$$Lambda$14/0x0000000800b44840.run(Unknown Source)
at java.lang.Thread.run(java.base@13.0.1/Thread.java:830)

發(fā)生了這兩種情況,都會造成對象鎖的釋放。進而導致 EntrySet里的線程重新爭搶對象鎖,成功搶到鎖的線程成為活動線程,這是一個循環(huán)的過程。

那 WaitSet 中的線程是如何再次被激活的呢?接下來,在某個地方,執(zhí)行了鎖的 notify 或者 notifyAll 命令,會造成WaitSet中 的線程,轉移到 EntrySet 中,重新進行鎖的爭奪。

如此周而復始,線程就可按順序排隊執(zhí)行。

3. 分級鎖

JDK1.8中,synchronized 的速度已經(jīng)有了顯著的提升。那它都做了哪些優(yōu)化呢?答案就是分級鎖。JVM會根據(jù)使用情況,對synchronized 的鎖,進行升級,它大體可以按照下面的路徑:偏向鎖->輕量級鎖->重量級鎖。

鎖只能升級,不能降級,所以一旦升級為重量級鎖,就只能依靠操作系統(tǒng)進行調度。

和鎖升級關系最大的就是對象頭里的 MarkWord,它包含Thread ID、Age、Biased、Tag四個部分。其中,Biased 有1bit大小,Tag 有2bit,鎖升級就是靠判斷Thread Id、Biased、Tag等三個變量值來進行的。

偏向鎖

在只有一個線程使用了鎖的情況下,偏向鎖能夠保證更高的效率。

具體過程是這樣的。當?shù)谝粋€線程第一次訪問同步塊時,會先檢測對象頭Mark Word中的標志位Tag是否為01,以此判斷此時對象鎖是否處于無鎖狀態(tài)或者偏向鎖狀態(tài)(匿名偏向鎖)。

01也是鎖默認的狀態(tài),線程一旦獲取了這把鎖,就會把自己的線程ID寫到MarkWord中。在其他線程來獲取這把鎖之前,鎖都處于偏向鎖狀態(tài)。

輕量級鎖

當下一個線程參與到偏向鎖競爭時,會先判斷 MarkWord 中保存的線程 ID 是否與這個線程 ID 相等,如果不相等,會立即撤銷偏向鎖,升級為輕量級鎖。

輕量級鎖的獲取是怎么進行的呢?它們使用的是自旋方式。

參與競爭的每個線程,會在自己的線程棧中生成一個 LockRecord ( LR ),然后每個線程通過 CAS (自旋)的方式,將鎖對象頭中的 MarkWord 設置為指向自己的 LR 的指針,哪個線程設置成功,就意味著哪個線程獲得鎖。

當鎖處于輕量級鎖的狀態(tài)時,就不能夠再通過簡單的對比Tag的值進行判斷,每次對鎖的獲取,都需要通過自旋。

當然,自旋也是面向不存在鎖競爭的場景,比如一個線程運行完了,另外一個線程去獲取這把鎖。但如果自旋失敗達到一定的次數(shù),鎖就會膨脹為重量級鎖。

重量級鎖

重量級鎖即為我們對synchronized的直觀認識,這種情況下,線程會掛起,進入到操作系統(tǒng)內核態(tài),等待操作系統(tǒng)的調度,然后再映射回用戶態(tài)。系統(tǒng)調用是昂貴的,重量級鎖的名稱由此而來。

如果系統(tǒng)的共享變量競爭非常激烈,鎖會迅速膨脹到重量級鎖,這些優(yōu)化就名存實亡。如果并發(fā)非常嚴重,可以通過參數(shù)??-XX:-UseBiasedLocking??禁用偏向鎖,理論上會有一些性能提升,但實際上并不確定。

4. Lock

在 concurrent 包里,我們能夠發(fā)現(xiàn)ReentrantLock和ReentrantReadWriteLock兩個類。Reentrant就是可重入的意思,它們和synchronized關鍵字一樣,都是可重入鎖。

這里有必要解釋一下可重入這個概念,因為在面試的時候經(jīng)常被問到。它的意思是,一個線程運行時,可以多次獲取同一個對象鎖。這是因為Java的鎖是基于線程的,而不是基于調用的。比如下面這段代碼,由于方法a、b、c鎖的都是當前的this,線程在調用a方法的時候,就不需要多次獲取對象鎖。

public synchronized void a(){
b();
}
public synchronized void b(){
c();
}
public synchronized void c(){
}

主要方法

LOCK是基于AQS(AbstractQueuedSynchronizer)實現(xiàn)的,而AQS 是基于 volitale 和 CAS 實現(xiàn)的。關于CAS,我們將在下一課時講解。

Lock與synchronized的使用方法不同,它需要手動加鎖,然后在finally中解鎖。Lock接口比synchronized靈活性要高,我們來看一下幾個關鍵方法。

  • lock: lock方法和synchronized沒什么區(qū)別,如果獲取不到鎖,都會被阻塞
  • tryLock: 此方法會嘗試獲取鎖,不管能不能獲取到鎖,都會立即返回,不會阻塞。它是有返回值的,獲取到鎖就會返回true
  • tryLock(long time, TimeUnit unit): 
  • lockInterruptibly: 與lock類似,但是可以鎖等待可以被中斷,中斷后返回InterruptedException

一般情況下,使用lock方法就可以。但如果業(yè)務請求要求響應及時,那使用帶超時時間的tryLock是更好的選擇:我們的業(yè)務可以直接返回失敗,而不用進行阻塞等待。tryLock這種優(yōu)化手段,采用降低請求成功率的方式,來保證服務的可用性,高并發(fā)場景下經(jīng)常被使用。

讀寫鎖

但對于有些業(yè)務來說,使用Lock這種粗粒度的鎖還是太慢了。比如,對于一個HashMap來說,某個業(yè)務是讀多寫少的場景,這個時候,如果給讀操作也加上和寫操作一樣的鎖的話,效率就會很慢。

ReentrantReadWriteLock是一種讀寫分離的鎖,它允許多個讀線程同時進行,但讀和寫、寫和寫是互斥的。使用方法如下所示,分別獲取讀寫鎖,對寫操作加寫鎖,對讀操作加讀鎖,并在finally里釋放鎖即可。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();

public void put(K k, V v){
writeLock.lock();
try {
map.put(k, v);
} finally {
writeLock.unlock();
}
}
...

那么,除了ReadWriteLock,我們能有更快的讀寫分離模式么?JDK1.8加入了哪個API?歡迎留言區(qū)評論。

公平鎖與非公平鎖

我們平常用到的鎖,都是非公平鎖??梢曰剡^頭來看一下monitor的原理。當持有鎖的線程釋放鎖的時候,EntrySet里的線程就會爭搶這把鎖。這個爭搶的過程,是隨機的,也就是說你并不知道哪個線程會獲取對象鎖,誰搶到了就算誰的。

這就有一定的概率,某個線程總是搶不到鎖,比如,線程通過setPriority 設置的比較低的優(yōu)先級。這個搶不到鎖的線程,就一直處于??饑餓??狀態(tài),這就是??線程饑餓??的概念。

公平鎖通過把隨機變成有序,可以解決這個問題。synchronized沒有這個功能,在Lock中可以通過構造參數(shù)設置成公平鎖,代碼如下。

public ReentrantReadWriteLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

由于所有的線程都需要排隊,需要在多核的場景下維護一個同步隊列,在多個線程爭搶鎖的時候,吞吐量就很低。下面是20個并發(fā)之下鎖的JMH測試結果,可以看到,非公平鎖比公平鎖性能高出兩個數(shù)量級。

Benchmark                      Mode  Cnt      Score      Error   Units
FairVSNoFairBenchmark.fair thrpt 10 186.144 ± 27.462 ops/ms
FairVSNoFairBenchmark.nofair thrpt 10 35195.649 ± 6503.375 ops/ms

5. 鎖的優(yōu)化技巧

死鎖

我們可以先看一下鎖沖突最嚴重的一種情況:死鎖。下面這段示例代碼,兩個線程分別持有了對方所需要的鎖,進入了相互等待的狀態(tài),就進入了死鎖。面試中手寫這段代碼的頻率,還是挺高的。

public class DeadLockDemo {
public static void main(String[] args){
Object object1 = new Object();
Object object2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (object1) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
}
}
}, "deadlock-demo-1");

t1.start();
Thread t2 = new Thread(() -> {
synchronized (object2) {
synchronized (object1) {
}
}
}, "deadlock-demo-2");
t2.start();
}
}

使用我們上面提到的,帶超時時間的tryLock方法,有一方讓步,可以一定程度上避免死鎖。

優(yōu)化技巧

鎖的優(yōu)化理論其實很簡單,那就是減少鎖的沖突。無論是鎖的讀寫分離,還是分段鎖,本質上都是為了避免多個線程同時獲取同一把鎖。我們可以總結一下優(yōu)化的一般思路:減少鎖的粒度、減少鎖持有的時間、鎖分級、鎖分離 、鎖消除、樂觀鎖、無鎖等。

圖片

減少鎖粒度

通過減小鎖的粒度,可以將沖突分散,減少沖突的可能,從而提高并發(fā)量。簡單來說,就是把資源進行抽象,針對每類資源使用單獨的鎖進行保護。比如下面的代碼,由于list1和list2屬于兩類資源,就沒必要使用同一個對象鎖進行處理。

public class LockLessDemo {
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
public synchronized void addList1(String v){
this.list1.add(v);
}
public synchronized void addList2(String v){
this.list2.add(v);
}
}

可以創(chuàng)建兩個不同的鎖,改善情況如下:

public class LockLessDemo {
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
final Object lock1 = new Object();
final Object lock2 = new Object();
public void addList1(String v){
synchronized (lock1) {
this.list1.add(v);
}
}
public void addList2(String v){
synchronized (lock2) {
this.list2.add(v);
}
}
}

減少鎖持有時間通過讓鎖資源盡快的釋放,減少鎖持有的時間,其他線程可更迅速的獲取鎖資源,進行其他業(yè)務的處理??紤]到下面的代碼,由于slowMethod不在鎖的范圍內,占用的時間又比較長,可以把它移動到synchronized代碼快外面,加速鎖的釋放。

public class LockTimeDemo {
List<String> list = new ArrayList<>();
final Object lock = new Object();
public void addList(String v){
synchronized (lock) {
slowMethod();
this.list.add(v);
}
}
public void slowMethod(){
}
}

?鎖分級鎖分級指的是我們文章開始講解的synchronied鎖的鎖升級,屬于JVM的內部優(yōu)化。它從偏向鎖開始,逐漸會升級為輕量級鎖、重量級鎖,這個過程是不可逆的。

鎖分離我們在上面提到的讀寫鎖,就是鎖分離技術。這是因為,讀操作一般是不會對資源產(chǎn)生影響的,可以并發(fā)執(zhí)行。寫操作和其他操作是互斥的,只能排隊執(zhí)行。所以讀寫鎖適合讀多寫少的場景。

鎖消除通過JIT編譯器,JVM可以消除某些對象的加鎖操作。舉個例子,大家都知道StringBuffer和StringBuilder都是做字符串拼接的,而且前者是線程安全的。

但其實,如果這兩個字符串拼接對象用在函數(shù)內,JVM通過逃逸分析分析這個對象的作用范圍就是在本函數(shù)中,就會把鎖的影響給消除掉。比如下面這段代碼,它和StringBuilder的效果是一樣的。

String m1(){
StringBuffer sb = new StringBuffer();
sb.append("");
return sb.toString();
}

End

Java中有兩種加鎖方式,一種是使用synchronized關鍵字,另外一種是concurrent包下面的Lock。本課時,我們詳細的了解了它們的一些特性,包括實現(xiàn)原理。下面對比如下:

類別

Synchronized

Lock

實現(xiàn)方式

monitor

AQS

底層細節(jié)

JVM優(yōu)化

Java API

分級鎖



功能特性

單一

豐富

鎖分離


讀寫鎖

鎖超時


帶超時時間的tryLock

可中斷


lockInterruptibly

Lock的功能是比synchronized多的,能夠對線程行為進行更細粒度的控制。但如果只是用最簡單的鎖互斥功能,建議直接使用synchronized。有兩個原因:

  • synchronized的編程模型更加簡單,更易于使用
  • synchronized引入了偏向鎖,輕量級鎖等功能,能夠從JVM層進行優(yōu)化,同時,JIT編譯器也會對它執(zhí)行一些鎖消除動作

多線程代碼好寫,但bug難找,希望你的代碼即干凈又強壯,兼高性能與高可靠于一身。

責任編輯:武曉燕 來源: 小姐姐味道
相關推薦

2024-01-22 09:16:47

多線程性能優(yōu)化

2022-04-11 09:58:07

數(shù)據(jù)庫SQL

2024-11-05 16:29:57

2022-07-20 07:45:15

多線程程序性能

2010-01-28 09:55:05

性能優(yōu)化

2024-09-27 09:31:25

2024-09-29 09:27:10

2018-10-25 15:55:44

Java多線程鎖優(yōu)化

2019-12-26 09:56:34

Java多線程內部鎖

2022-03-04 06:46:30

Python代碼

2015-08-25 08:55:14

優(yōu)秀代碼基因

2021-08-06 22:51:45

CPU限流容器

2025-02-25 12:00:00

Java線程開發(fā)

2009-11-27 13:24:20

PHP代碼性能優(yōu)化

2012-07-23 10:22:15

Python性能優(yōu)化優(yōu)化技巧

2019-03-06 10:25:30

Web圖片優(yōu)化命令

2012-09-04 09:55:22

代碼抓狂的代碼開發(fā)

2019-06-13 16:30:37

代碼Java編程語言

2020-06-05 09:52:43

IT部門高管首席信息官

2010-03-17 19:24:38

Java多線程循環(huán)
點贊
收藏

51CTO技術棧公眾號