synchronized與Lock的區別與使用詳解
引言
昨天在學習別人分享的面試經驗時,看到Lock的使用。想起自己在上次面試也遇到了synchronized與Lock的區別與使用。於是,我整理了兩者的區別和使用情況,同時,對synchronized的使用過程一些常見問題的總結,最後是參照原始碼和說明文件,對Lock的使用寫了幾個簡單的Demo。
技術點:
1、執行緒與程式:
在開始之前先把程式與執行緒進行區分一下,一個程式最少需要一個程式,而一個程式最少需要一個執行緒。關係是執行緒–>程式–>程式的大致組成結構。所以執行緒是程式執行流的最小單位,而程式是系統進行資源分配和排程的一個獨立單位。以下我們所有討論的都是建立線上程基礎之上。
2、Thread的幾個重要方法:
我們先了解一下Thread的幾個重要方法。a、start()方法,呼叫該方法開始執行該執行緒;b、stop()方法,呼叫該方法強制結束該執行緒執行;c、join方法,呼叫該方法等待該執行緒結束。d、sleep()方法,呼叫該方法該執行緒進入等待。e、run()方法,呼叫該方法直接執行執行緒的run()方法,但是執行緒呼叫start()方法時也會執行run()方法,區別就是一個是由執行緒排程執行run()方法,一個是直接呼叫了執行緒中的run()方法!!
看到這裡,可能有些人就會問啦,那wait()和notify()呢?要注意,其實wait()與notify()方法是Object的方法,不是Thread的方法!!同時,wait()與notify()會配合使用,分別表示執行緒掛起和執行緒恢復。
這裡還有一個很常見的問題,順帶提一下:wait()與sleep()的區別,簡單來說wait()會釋放物件鎖而sleep()不會釋放物件鎖。這些問題有很多的資料,不再贅述。
3、執行緒狀態:
執行緒總共有5大狀態,通過上面第二個知識點的介紹,理解起來就簡單了。
新建狀態:新建執行緒物件,並沒有呼叫start()方法之前
就緒狀態:呼叫start()方法之後執行緒就進入就緒狀態,但是並不是說只要呼叫start()方法執行緒就馬上變為當前執行緒,在變為當前執行緒之前都是為就緒狀態。值得一提的是,執行緒在睡眠和掛起中恢復的時候也會進入就緒狀態哦。
執行狀態:執行緒被設定為當前執行緒,開始執行run()方法。就是執行緒進入執行狀態
阻塞狀態:執行緒被暫停,比如說呼叫sleep()方法後執行緒就進入阻塞狀態
死亡狀態:執行緒執行結束
4、鎖型別
可重入鎖:在執行物件中所有同步方法不用再次獲得鎖
可中斷鎖:在等待獲取鎖過程中可中斷
公平鎖: 按等待獲取鎖的執行緒的等待時間進行獲取,等待時間長的具有優先獲取鎖權利
讀寫鎖:對資源讀取和寫入的時候拆分為2部分處理,讀的時候可以多執行緒一起讀,寫的時候必須同步地寫
synchronized與Lock的區別
1、我把兩者的區別分類到了一個表中,方便大家對比:
類別 | synchronized | Lock |
---|---|---|
存在層次 | Java的關鍵字,在jvm層面上 | 是一個類 |
鎖的釋放 | 1、以獲取鎖的執行緒執行完同步程式碼,釋放鎖 2、執行緒執行發生異常,jvm會讓執行緒釋放鎖 | 在finally中必須釋放鎖,不然容易造成執行緒死鎖 |
鎖的獲取 | 假設A執行緒獲得鎖,B執行緒等待。如果A執行緒阻塞,B執行緒會一直等待 | 分情況而定,Lock有多個鎖獲取的方式,具體下面會說道,大致就是可以嘗試獲得鎖,執行緒可以不用一直等待 |
鎖狀態 | 無法判斷 | 可以判斷 |
鎖型別 | 可重入 不可中斷 非公平 | 可重入 可判斷 可公平(兩者皆可) |
效能 | 少量同步 | 大量同步 |
或許,看到這裡還對LOCK所知甚少,那麼接下來,我們進入LOCK的深入學習。
Lock詳細介紹與Demo
以下是Lock介面的原始碼,筆者修剪之後的結果:
public interface Lock {
/**
* Acquires the lock.
*/
void lock();
/**
* Acquires the lock unless the current thread is
* {@linkplain Thread#interrupt interrupted}.
*/
void lockInterruptibly() throws InterruptedException;
/**
* Acquires the lock only if it is free at the time of invocation.
*/
boolean tryLock();
/**
* Acquires the lock if it is free within the given waiting time and the
* current thread has not been {@linkplain Thread#interrupt interrupted}.
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* Releases the lock.
*/
void unlock();
}
從Lock介面中我們可以看到主要有個方法,這些方法的功能從註釋中可以看出:
lock():獲取鎖,如果鎖被暫用則一直等待
unlock():釋放鎖
tryLock(): 注意返回型別是boolean,如果獲取鎖的時候鎖被佔用就返回false,否則返回true
tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待引數時間
lockInterruptibly():用該鎖的獲得方式,如果執行緒在獲取鎖的階段進入了等待,那麼可以中斷此執行緒,先去做別的事
通過 以上的解釋,大致可以解釋在上個部分中“鎖型別(lockInterruptibly())”,“鎖狀態(tryLock())”等問題,還有就是前面子所獲取的過程我所寫的“大致就是可以嘗試獲得鎖,執行緒可以不會一直等待”用了“可以”的原因。
下面是Lock一般使用的例子,注意ReentrantLock是Lock介面的實現。
lock():
package com.brickworkers;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
private Lock lock = new ReentrantLock();
//需要參與同步的方法
private void method(Thread thread){
lock.lock();
try {
System.out.println("執行緒名"+thread.getName() + "獲得了鎖");
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("執行緒名"+thread.getName() + "釋放了鎖");
lock.unlock();
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
//執行緒1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t2");
t1.start();
t2.start();
}
}
//執行情況:執行緒名t1獲得了鎖
// 執行緒名t1釋放了鎖
// 執行緒名t2獲得了鎖
// 執行緒名t2釋放了鎖
tryLock():
package com.brickworkers;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
private Lock lock = new ReentrantLock();
//需要參與同步的方法
private void method(Thread thread){
/* lock.lock();
try {
System.out.println("執行緒名"+thread.getName() + "獲得了鎖");
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("執行緒名"+thread.getName() + "釋放了鎖");
lock.unlock();
}*/
if(lock.tryLock()){
try {
System.out.println("執行緒名"+thread.getName() + "獲得了鎖");
}catch(Exception e){
e.printStackTrace();
} finally {
System.out.println("執行緒名"+thread.getName() + "釋放了鎖");
lock.unlock();
}
}else{
System.out.println("我是"+Thread.currentThread().getName()+"有人佔著鎖,我就不要啦");
}
}
public static void main(String[] args) {
LockTest lockTest = new LockTest();
//執行緒1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockTest.method(Thread.currentThread());
}
}, "t2");
t1.start();
t2.start();
}
}
//執行結果: 執行緒名t2獲得了鎖
// 我是t1有人佔著鎖,我就不要啦
// 執行緒名t2釋放了鎖
看到這裡相信大家也都會使用如何使用Lock了吧,關於tryLock(long time, TimeUnit unit)和lockInterruptibly()不再贅述。前者主要存在一個等待時間,在測試程式碼中寫入一個等待時間,後者主要是等待中斷,會丟擲一箇中斷異常,常用度不高,喜歡探究可以自己深入研究。
前面比較重提到“公平鎖”,在這裡可以提一下ReentrantLock對於平衡鎖的定義,在原始碼中有這麼兩段:
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
從以上原始碼可以看出在Lock中可以自己控制鎖是否公平,而且,預設的是非公平鎖,以下是ReentrantLock的建構函式:
public ReentrantLock() {
sync = new NonfairSync();//預設非公平鎖
}
補充:
回來再看自己的部落格,發現東西闡述的不夠完整。這裡再做一下補充,為了不誤導大家,儘量介紹給大家正確的表述:
1、兩種鎖的底層實現方式:
synchronized:我們知道java是用位元組碼指令來控制程式(這裡不包括熱點程式碼編譯成機器碼)。在位元組指令中,存在有synchronized所包含的程式碼塊,那麼會形成2段流程的執行。
我們點選檢視SyncDemo.java的原始碼SyncDemo.class,可以看到如下:
如上就是這段程式碼段位元組碼指令,沒你想的那麼難吧。言歸正傳,我們可以清晰段看到,其實synchronized對映成位元組碼指令就是增加來兩個指令:monitorenter和monitorexit。當一條執行緒進行執行的遇到monitorenter指令的時候,它會去嘗試獲得鎖,如果獲得鎖那麼鎖計數+1(為什麼會加一呢,因為它是一個可重入鎖,所以需要用這個鎖計數判斷鎖的情況),如果沒有獲得鎖,那麼阻塞。當它遇到monitorexit的時候,鎖計數器-1,當計數器為0,那麼就釋放鎖。
那麼有的朋友看到這裡就疑惑了,那圖上有2個monitorexit呀?馬上回答這個問題:上面我以前寫的文章也有表述過,synchronized鎖釋放有兩種機制,一種就是執行完釋放;另外一種就是傳送異常,虛擬機器釋放。圖中第二個monitorexit就是發生異常時執行的流程,這就是我開頭說的“會有2個流程存在“。而且,從圖中我們也可以看到在第13行,有一個goto指令,也就是說如果正常執行結束會跳轉到19行執行。
這下,你對synchronized是不是瞭解的很清晰了呢。接下來我們再聊一聊Lock。
Lock:Lock實現和synchronized不一樣,後者是一種悲觀鎖,它膽子很小,它很怕有人和它搶吃的,所以它每次吃東西前都把自己關起來。而Lock呢底層其實是CAS樂觀鎖的體現,它無所謂,別人搶了它吃的,它重新去拿吃的就好啦,所以它很樂觀。具體底層怎麼實現,博主不在細述,有機會的話,我會對concurrent包下面的機制好好和大家說說,如果面試問起,你就說底層主要靠volatile和CAS操作實現的。
現在,我要說的是:儘可能去使用synchronized而不要去使用LOCK
什麼概念呢?我和大家打個比方:你叫jdk,你生了一個孩子叫synchronized,後來呢,你領養了一個孩子叫LOCK。起初,LOCK剛來到新家的時候,它很乖,很懂事,各個方面都表現的比synchronized好。你很開心,但是你內心深處又有一點淡淡的憂傷,你不希望你自己親生的孩子竟然還不如一個領養的孩子乖巧。這個時候,你對親生的孩子教育更加深刻了,你想證明,你的親生孩子synchronized並不會比領養的孩子LOCK差。(只是打個比方)
那如何教育呢?
在jdk1.6~jdk1.7的時候,也就是synchronized16、7歲的時候,你作為爸爸,你給他優化了,具體優化在哪裡呢:
1、執行緒自旋和適應性自旋
我們知道,java’執行緒其實是對映在核心之上的,執行緒的掛起和恢復會極大的影響開銷。並且jdk官方人員發現,很多執行緒在等待鎖的時候,在很短的一段時間就獲得了鎖,所以它們線上程等待的時候,並不需要把執行緒掛起,而是讓他無目的的迴圈,一般設定10次。這樣就避免了執行緒切換的開銷,極大的提升了效能。
而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。他可以根據它前面執行緒的自旋情況,從而調整它的自旋,甚至是不經過自旋而直接掛起。
2、鎖消除
什麼叫鎖消除呢?就是把不必要的同步在編譯階段進行移除。
那麼有的小夥伴又迷糊了,我自己寫的程式碼我會不知道這裡要不要加鎖?我加了鎖就是表示這邊會有同步呀?
並不是這樣,這裡所說的鎖消除並不一定指代是你寫的程式碼的鎖消除,我打一個比方:
在jdk1.5以前,我們的String字串拼接操作其實底層是StringBuffer來實現的(這個大家可以用我前面介紹的方法,寫一個簡單的demo,然後檢視class檔案中的位元組碼指令就清楚了),而在jdk1.5之後,那麼是用StringBuilder來拼接的。我們考慮前面的情況,比如如下程式碼:
String str1="qwe";
String str2="asd";
String str3=str1+str2;
底層實現會變成這樣:
StringBuffer sb = new StringBuffer();
sb.append("qwe");
sb.append("asd");
我們知道,StringBuffer是一個執行緒安全的類,也就是說兩個append方法都會同步,通過指標逃逸分析(就是變數不會外洩),我們發現在這段程式碼並不存線上程安全問題,這個時候就會把這個同步鎖消除。
3、鎖粗化
在用synchronized的時候,我們都講究為了避免大開銷,儘量同步程式碼塊要小。那麼為什麼還要加粗呢?
我們繼續以上面的字串拼接為例,我們知道在這一段程式碼中,每一個append都需要同步一次,那麼我可以把鎖粗化到第一個append和最後一個append(這裡不要去糾結前面的鎖消除,我只是打個比方)
4、輕量級鎖
5、偏向鎖
關於最後這兩種,我希望留給有緣的讀者自己去查詢,我不希望我把一件事情描述的那麼詳細,自己動手得到才是你自己的,最後兩種並不難。
最後
筆者水平一般,不過此部落格在引言中的目的已全部達到。這只是筆者在學習過程中的總結與概括,如存在不正確的,歡迎大家批評指出。
PS:如果覺得我的分享不錯,歡迎大家隨手點贊、轉發。
(完)
Java團長
專注於Java乾貨分享
掃描上方二維碼獲取更多Java乾貨
相關文章
- synchronized與Lock的區別synchronized
- Lock 和 synchronized的區別synchronized
- Lock、Synchronized鎖區別解析synchronized
- Latch 與 Lock 的區別
- synchronized 與 Lock 的對比synchronized
- synchronized與ReentrantLock的區別synchronizedReentrantLock
- Synchronized 與 ReentrantLock 的區別synchronizedReentrantLock
- volatile與synchronized的區別synchronized
- 四、Synchronized與Lock原理synchronized
- rem與em的使用和區別詳解REM
- PHP isset()與empty()的使用區別詳解PHP
- 詳解not in與not exists的區別與用法
- synchronized 與 Lock 的那點事synchronized
- equals與==的區別(詳解)
- Java synchronized與ReentrantLock的區別JavasynchronizedReentrantLock
- PHP中isset()與empty()的使用區別詳解PHP
- 詳解iframe與frame的區別
- Kafka與ActiveMQ的區別與聯絡詳解KafkaMQ
- 詳解Kafka與ActiveMQ的區別與聯絡!KafkaMQ
- TCP與UDP區別詳解TCPUDP
- cookie和session的詳解與區別CookieSession
- Java運算子>>與>>>區別詳解Java
- volatile和synchronized的區別與聯絡[轉]synchronized
- xargs 命令詳解,xargs 與管道的區別
- cookie與session的區別(圖文詳解)CookieSession
- xargs命令詳解,xargs與管道的區別
- Java中Vector與ArrayList的區別詳解Java
- Truncate table 詳解及與delete,drop 的區別delete
- Truncate table詳解及與delete,drop的區別delete
- PHPCookie與Session的使用與區別PHPCookieSession
- 【面試普通人VS高手系列】lock和synchronized區別面試synchronized
- Python之列表與元組的區別詳解Python
- 裝飾器模式詳解(UML、原理、使用、與代理模式的區別等)模式
- systemctl的操作詳解總結及其與service的區別
- table中cesllspacing與cellpadding的區別詳解padding
- 主流RPC框架詳解,以及與SOA、REST的區別RPC框架REST
- Android中Handler Runnable與Thread的區別詳解Androidthread
- mysql與redis的區別與使用場景MySqlRedis