java synchronized加鎖和釋放流程詳解
為什么需要加鎖
在多線程環(huán)境中,多個(gè)線程同時(shí)運(yùn)行同一個(gè)方法時(shí),如果其中有對(duì)某一個(gè)資源就行修改處理時(shí),可能會(huì)存在先后操作的問題,使得邏輯不一致,程序運(yùn)行的結(jié)果不時(shí)我們想要的。
線程如何加鎖
這里只講synchronized進(jìn)行加鎖,并且只進(jìn)行使用原理的闡述,其他加鎖方式使用另外的篇幅。
加鎖是為了避免多個(gè)線程同時(shí)進(jìn)行邏輯處理時(shí),可能會(huì)有數(shù)據(jù)不一致等情況從而影響程序的邏輯的準(zhǔn)確性。 所以我們可以使用一個(gè)對(duì)象,給該對(duì)象設(shè)置一個(gè)鎖狀態(tài)標(biāo)記,其他線程要進(jìn)行邏輯處理時(shí)需要把該狀態(tài)設(shè)置成功才能正常進(jìn)行,不然就阻塞掛起。 這里問題來了,如果是我們直接在代碼中添加一個(gè)狀態(tài)標(biāo)志,那么多線程的情況下設(shè)置這個(gè)狀態(tài)下可能還是會(huì)有同時(shí)處理的情況。
這里我們可以依賴java提供的synchronized關(guān)鍵字。
java內(nèi)存布局和監(jiān)視器鎖
剛剛我們提到,可以給對(duì)象設(shè)置一個(gè)鎖狀態(tài)標(biāo)記,其實(shí)vjm已經(jīng)幫我們實(shí)現(xiàn)了,我們平常寫的java對(duì)象經(jīng)過編譯字節(jié)碼后,是會(huì)在內(nèi)存中添加一個(gè)額外的信息的,這里就涉及到另一個(gè)概念,java對(duì)象的內(nèi)存布局或者說java對(duì)象的數(shù)據(jù)結(jié)構(gòu)。
當(dāng)我們通過new關(guān)鍵字來新建一個(gè)對(duì)象時(shí),jvm會(huì)在堆內(nèi)存中開辟一塊內(nèi)存存儲(chǔ)該對(duì)象實(shí)例,對(duì)象實(shí)例除了擁有我們自己定義的一些屬性方法等,還會(huì)擁有額外的其他的信息。
分為三塊:
- 對(duì)象頭
對(duì)象頭中會(huì)存儲(chǔ)有hashcode,GC信息,鎖標(biāo)記等等。
- 實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)就是我們自定義的各個(gè)字段和方法信息。
- 填充對(duì)齊
簡(jiǎn)單理解為虛擬機(jī)中存儲(chǔ)一個(gè)對(duì)象約定對(duì)象大小為8字節(jié)的整數(shù)倍,所以如果不夠的話會(huì)額外占用一點(diǎn)空間湊數(shù)。
好了,簡(jiǎn)單說到這里就ok了,這里可以看到對(duì)象在實(shí)際運(yùn)行過程中擁有鎖標(biāo)記的,這里稱為監(jiān)視器鎖,實(shí)際上對(duì)象頭的鎖信息會(huì)更多,這里只是簡(jiǎn)單概括一下。在程序中通過synchronize關(guān)鍵字進(jìn)行加鎖的話,jvm會(huì)幫助我們標(biāo)記該對(duì)象是由那個(gè)線程占有了,并且保證其他線程不會(huì)再擁有,只有當(dāng)線程釋放了改對(duì)象的鎖后才可以重新進(jìn)行鎖競(jìng)爭(zhēng)。
同時(shí)synchorize關(guān)鍵詞能保證操作的對(duì)象是直接從內(nèi)存中獲取的(內(nèi)存可見性)。
使用方式如下:
public class ThreadTest { public static void main(String[] args) { Task task = new Task(); for (int i = 0; i< 50; i++) { new Thread(task).run(); } System.out.println(task.getCount()); } } class Task implements Runnable{ private int count; private Object lock = new Object(); public int getCount() { return count; } public void setCount(int count) { this.count = count; } @Override public void run() { int total = 0; while (++total <= 10000) { synchronized (lock) { count++; } } } }
synchronized究竟鎖了誰
synchronized關(guān)鍵字的語法規(guī)則是定義在代碼塊中或者在定義方法時(shí)。
剛剛我們提到,java對(duì)象頭中有鎖標(biāo)記,所以下面的邏輯就是對(duì)lock這個(gè)對(duì)象進(jìn)行鎖競(jìng)爭(zhēng)
while (++total <= 10000) { synchronized (lock) { count++; } }
而如果我們synchronized是在方法中定義的話,則是對(duì)當(dāng)前類的實(shí)例進(jìn)行鎖競(jìng)爭(zhēng),這里就是C1的實(shí)例對(duì)象,也即是C1 c1 = new C1()中的c1;而如果程序中還有C1 c11 = new C1()的定義,那么是分開競(jìng)爭(zhēng)的。也即是同一個(gè)對(duì)象才進(jìn)行鎖競(jìng)爭(zhēng)。
class C1{ private int count; public synchronized void run() { int total = 0; while (++total <= 10000) { count++; } } }
如果對(duì)象的方法是static的,那么進(jìn)行鎖競(jìng)爭(zhēng)的是類對(duì)象,這個(gè)是jvm進(jìn)行class字節(jié)碼加載時(shí)生成的。
class C1{ private int count; public static synchronized void run() { int total = 0; while (++total <= 10000) { count++; } } }
至此,我們可以把監(jiān)視器鎖和synchronized關(guān)鍵字梳理了一遍。以上的重點(diǎn)信息是:java對(duì)象內(nèi)存布局和監(jiān)視器鎖以及synchronized關(guān)鍵字的處理邏輯。如果需要深入可以對(duì)各個(gè)點(diǎn)進(jìn)行往下研究。
線程的等待和喚醒
wait()方法
- 首先我們需要了解wait()方法的繼承體系,他是在Object對(duì)象的基類方法,也就是說所有的對(duì)象都擁有wait()方法。一個(gè)線程調(diào)用了java的wait()方法后,當(dāng)前線程會(huì)被阻塞掛起,這里的調(diào)用指的是線程里面調(diào)用了加鎖對(duì)象的wait()方法。
- 線程被阻塞掛起后是需要喚醒的,下面會(huì)講到喚醒方法,但是也可以調(diào)用重載方法wait(long timeout),讓線程被阻塞后超過一定時(shí)間還沒被喚醒而自動(dòng)喚醒。
notify()方法
- notify()方法也是繼承于Object對(duì)象。當(dāng)某個(gè)線程調(diào)用了加鎖對(duì)象的notify方法后,會(huì)喚醒之前在該對(duì)象進(jìn)行獲取監(jiān)視器鎖時(shí)失敗而被阻塞的線程,如果有多個(gè)線程同時(shí)被阻塞,notify()方法只會(huì)有一個(gè)線程被喚醒,如果需要喚醒全部,則可以調(diào)用notifyAll()方法。
所以面試中會(huì)被問到wait和notify的作用,可以側(cè)重的知識(shí)點(diǎn)是:
- 1.調(diào)用wait之前一定是獲取到鎖的,所以要保證在synchronized塊中。
- 2.調(diào)用wait后會(huì)釋放該對(duì)象的鎖。
- 3.調(diào)用notify()方法也要是獲取鎖, 也要保證在synchronized塊中。
- 4.調(diào)用notify()方法喚醒一個(gè)線程,調(diào)用notifyAll()方法喚醒全部被阻塞線程。
- 5.調(diào)用notify()或者notifyAll()方法只是喚醒了其他被阻塞的線程,他們有了重新競(jìng)爭(zhēng)鎖的條件,但是當(dāng)前線程還沒有釋放鎖的,只有調(diào)用了wait()方法才會(huì)釋放鎖。
用一個(gè)生產(chǎn)者消費(fèi)者模型來看看wait和notify的用法。生產(chǎn)者消費(fèi)者模型可以簡(jiǎn)單理解為有一個(gè)容器,當(dāng)里面沒有數(shù)據(jù)時(shí)生產(chǎn)者會(huì)往里面添加數(shù)據(jù),滿了則暫停當(dāng)前的工作等待消費(fèi)者消費(fèi)數(shù)據(jù)后通知他繼續(xù)添加。
消費(fèi)者會(huì)往里面拿數(shù)據(jù),沒有了數(shù)據(jù)則暫停工作等待生產(chǎn)者生產(chǎn)了數(shù)據(jù)并通知他繼續(xù)消費(fèi)。
public static void main(String[] args) { Object lock = new Object(); AtomicInteger counter = new AtomicInteger(0); Queue<Integer> queue = new LinkedList<>(); new Thread(new Runnable() { @Override public void run() { synchronized (lock) { while (true) { //如果隊(duì)列沒有數(shù)據(jù),調(diào)用wait()方法,阻塞自己 if (queue.isEmpty()) { try { System.out.println("消費(fèi)者線程阻塞"); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊(duì)列不為空,消費(fèi)數(shù)據(jù);如果線程被生產(chǎn)者通過notifyAll()方法喚醒后,線程重新獲取到鎖時(shí)是從這里執(zhí)行的 System.out.println("消費(fèi)者線程消費(fèi)數(shù)據(jù): " + queue.poll()); //消費(fèi)者消費(fèi)后,喚醒可能由于之前隊(duì)列滿了而主動(dòng)阻塞自己的生產(chǎn)者 lock.notifyAll(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { synchronized (lock) { while (true) { //如果隊(duì)列數(shù)據(jù)滿了,調(diào)用wait()方法,阻塞自己 if (queue.size() > 10) { System.out.println("生產(chǎn)者線程阻塞"); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊(duì)列沒有滿,生產(chǎn)數(shù)據(jù); 如果被其他線程喚醒,在下次獲取到鎖的時(shí)候生產(chǎn)數(shù)據(jù) System.out.println("生產(chǎn)者線程生產(chǎn)數(shù)據(jù)"); queue.add(counter.incrementAndGet()); //隊(duì)列有數(shù)據(jù)了,喚醒之前可能沒有數(shù)據(jù)而主動(dòng)祖寺啊自己的消費(fèi)者 lock.notifyAll(); } } } }).start(); }
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot+vue?若依項(xiàng)目在windows2008R2企業(yè)版部署流程分析
這篇文章主要介紹了springboot+vue?若依項(xiàng)目在windows2008R2企業(yè)版部署流程,本次使用jar包啟動(dòng)后端,故而準(zhǔn)備打包后的jar文件,需要的朋友可以參考下2022-12-12java對(duì)象中什么時(shí)候適合用static修飾符踩坑解決記錄
這篇文章主要為大家介紹了java對(duì)象中什么時(shí)候適合用static修飾符踩坑解決記錄,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09Spring框架實(shí)現(xiàn)AOP添加日志記錄功能過程詳解
這篇文章主要介紹了Spring框架實(shí)現(xiàn)AOP添加日志記錄功能過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12MyBatisPlus的IService接口實(shí)現(xiàn)
MyBatisPlus是一個(gè)為MyBatis提供增強(qiáng)的工具,它通過IService接口簡(jiǎn)化了數(shù)據(jù)庫的CRUD操作,IService接口封裝了一系列常用的數(shù)據(jù)操作方法,本文就來介紹一下,感興趣的可以了解一下2024-10-10Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(11)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07SpringBoot中的@RestControllerAdvice注解詳解
這篇文章主要介紹了SpringBoot中的@RestControllerAdvice注解詳解,RestControllerAdvice注解用于創(chuàng)建全局異常處理類,用于捕獲和處理整個(gè)應(yīng)用程序中的異常,需要的朋友可以參考下2024-01-01