java synchronized加鎖和釋放流程詳解
為什么需要加鎖
在多線程環(huán)境中,多個線程同時運行同一個方法時,如果其中有對某一個資源就行修改處理時,可能會存在先后操作的問題,使得邏輯不一致,程序運行的結果不時我們想要的。
線程如何加鎖
這里只講synchronized進行加鎖,并且只進行使用原理的闡述,其他加鎖方式使用另外的篇幅。
加鎖是為了避免多個線程同時進行邏輯處理時,可能會有數(shù)據(jù)不一致等情況從而影響程序的邏輯的準確性。 所以我們可以使用一個對象,給該對象設置一個鎖狀態(tài)標記,其他線程要進行邏輯處理時需要把該狀態(tài)設置成功才能正常進行,不然就阻塞掛起。 這里問題來了,如果是我們直接在代碼中添加一個狀態(tài)標志,那么多線程的情況下設置這個狀態(tài)下可能還是會有同時處理的情況。
這里我們可以依賴java提供的synchronized關鍵字。
java內存布局和監(jiān)視器鎖
剛剛我們提到,可以給對象設置一個鎖狀態(tài)標記,其實vjm已經(jīng)幫我們實現(xiàn)了,我們平常寫的java對象經(jīng)過編譯字節(jié)碼后,是會在內存中添加一個額外的信息的,這里就涉及到另一個概念,java對象的內存布局或者說java對象的數(shù)據(jù)結構。
當我們通過new關鍵字來新建一個對象時,jvm會在堆內存中開辟一塊內存存儲該對象實例,對象實例除了擁有我們自己定義的一些屬性方法等,還會擁有額外的其他的信息。
分為三塊:
- 對象頭
對象頭中會存儲有hashcode,GC信息,鎖標記等等。
- 實例數(shù)據(jù)
實例數(shù)據(jù)就是我們自定義的各個字段和方法信息。
- 填充對齊
簡單理解為虛擬機中存儲一個對象約定對象大小為8字節(jié)的整數(shù)倍,所以如果不夠的話會額外占用一點空間湊數(shù)。
好了,簡單說到這里就ok了,這里可以看到對象在實際運行過程中擁有鎖標記的,這里稱為監(jiān)視器鎖,實際上對象頭的鎖信息會更多,這里只是簡單概括一下。在程序中通過synchronize關鍵字進行加鎖的話,jvm會幫助我們標記該對象是由那個線程占有了,并且保證其他線程不會再擁有,只有當線程釋放了改對象的鎖后才可以重新進行鎖競爭。
同時synchorize關鍵詞能保證操作的對象是直接從內存中獲取的(內存可見性)。
使用方式如下:
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ī)則是定義在代碼塊中或者在定義方法時。
剛剛我們提到,java對象頭中有鎖標記,所以下面的邏輯就是對lock這個對象進行鎖競爭
while (++total <= 10000) { synchronized (lock) { count++; } }
而如果我們synchronized是在方法中定義的話,則是對當前類的實例進行鎖競爭,這里就是C1的實例對象,也即是C1 c1 = new C1()中的c1;而如果程序中還有C1 c11 = new C1()的定義,那么是分開競爭的。也即是同一個對象才進行鎖競爭。
class C1{ private int count; public synchronized void run() { int total = 0; while (++total <= 10000) { count++; } } }
如果對象的方法是static的,那么進行鎖競爭的是類對象,這個是jvm進行class字節(jié)碼加載時生成的。
class C1{ private int count; public static synchronized void run() { int total = 0; while (++total <= 10000) { count++; } } }
至此,我們可以把監(jiān)視器鎖和synchronized關鍵字梳理了一遍。以上的重點信息是:java對象內存布局和監(jiān)視器鎖以及synchronized關鍵字的處理邏輯。如果需要深入可以對各個點進行往下研究。
線程的等待和喚醒
wait()方法
- 首先我們需要了解wait()方法的繼承體系,他是在Object對象的基類方法,也就是說所有的對象都擁有wait()方法。一個線程調用了java的wait()方法后,當前線程會被阻塞掛起,這里的調用指的是線程里面調用了加鎖對象的wait()方法。
- 線程被阻塞掛起后是需要喚醒的,下面會講到喚醒方法,但是也可以調用重載方法wait(long timeout),讓線程被阻塞后超過一定時間還沒被喚醒而自動喚醒。
notify()方法
- notify()方法也是繼承于Object對象。當某個線程調用了加鎖對象的notify方法后,會喚醒之前在該對象進行獲取監(jiān)視器鎖時失敗而被阻塞的線程,如果有多個線程同時被阻塞,notify()方法只會有一個線程被喚醒,如果需要喚醒全部,則可以調用notifyAll()方法。
所以面試中會被問到wait和notify的作用,可以側重的知識點是:
- 1.調用wait之前一定是獲取到鎖的,所以要保證在synchronized塊中。
- 2.調用wait后會釋放該對象的鎖。
- 3.調用notify()方法也要是獲取鎖, 也要保證在synchronized塊中。
- 4.調用notify()方法喚醒一個線程,調用notifyAll()方法喚醒全部被阻塞線程。
- 5.調用notify()或者notifyAll()方法只是喚醒了其他被阻塞的線程,他們有了重新競爭鎖的條件,但是當前線程還沒有釋放鎖的,只有調用了wait()方法才會釋放鎖。
用一個生產者消費者模型來看看wait和notify的用法。生產者消費者模型可以簡單理解為有一個容器,當里面沒有數(shù)據(jù)時生產者會往里面添加數(shù)據(jù),滿了則暫停當前的工作等待消費者消費數(shù)據(jù)后通知他繼續(xù)添加。
消費者會往里面拿數(shù)據(jù),沒有了數(shù)據(jù)則暫停工作等待生產者生產了數(shù)據(jù)并通知他繼續(xù)消費。
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) { //如果隊列沒有數(shù)據(jù),調用wait()方法,阻塞自己 if (queue.isEmpty()) { try { System.out.println("消費者線程阻塞"); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列不為空,消費數(shù)據(jù);如果線程被生產者通過notifyAll()方法喚醒后,線程重新獲取到鎖時是從這里執(zhí)行的 System.out.println("消費者線程消費數(shù)據(jù): " + queue.poll()); //消費者消費后,喚醒可能由于之前隊列滿了而主動阻塞自己的生產者 lock.notifyAll(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { synchronized (lock) { while (true) { //如果隊列數(shù)據(jù)滿了,調用wait()方法,阻塞自己 if (queue.size() > 10) { System.out.println("生產者線程阻塞"); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果隊列沒有滿,生產數(shù)據(jù); 如果被其他線程喚醒,在下次獲取到鎖的時候生產數(shù)據(jù) System.out.println("生產者線程生產數(shù)據(jù)"); queue.add(counter.incrementAndGet()); //隊列有數(shù)據(jù)了,喚醒之前可能沒有數(shù)據(jù)而主動祖寺啊自己的消費者 lock.notifyAll(); } } } }).start(); }
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
解決java使用file.createNewFile()創(chuàng)建文件時報錯目錄不存在的問題
這篇文章主要介紹了解決java使用file.createNewFile()創(chuàng)建文件時報錯目錄不存在的問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-06-06Mybatis實戰(zhàn)教程之入門到精通(經(jīng)典)
MyBatis是支持普通SQL查詢,存儲過程和高級映射的優(yōu)秀持久層框架,通過本文給大家介紹Mybatis實戰(zhàn)教程之入門到精通,對mybatis實戰(zhàn)教程相關知識感興趣的朋友一起學習吧2016-01-01