詳解Java并發(fā)編程之內(nèi)置鎖(synchronized)
簡介
synchronized在JDK5.0的早期版本中是重量級鎖,效率很低,但從JDK6.0開始,JDK在關(guān)鍵字synchronized上做了大量的優(yōu)化,如偏向鎖、輕量級鎖等,使它的效率有了很大的提升。
synchronized的作用是實現(xiàn)線程間的同步,當(dāng)多個線程都需要訪問共享代碼區(qū)域時,對共享代碼區(qū)域進行加鎖,使得每一次只能有一個線程訪問共享代碼區(qū)域,從而保證線程間的安全性。
因為沒有顯式的加鎖和解鎖過程,所以稱之為隱式鎖,也叫作內(nèi)置鎖、監(jiān)視器鎖。
如下實例,在沒有使用synchronized的情況下,多個線程訪問共享代碼區(qū)域時,可能會出現(xiàn)與預(yù)想中不同的結(jié)果。
public class Apple implements Runnable { private int appleCount = 5; @Override public void run() { eatApple(); } public void eatApple(){ appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } public static void main(String[] args) { Apple apple = new Apple(); Thread t1 = new Thread(apple, "小強"); Thread t2 = new Thread(apple, "小明"); Thread t3 = new Thread(apple, "小花"); Thread t4 = new Thread(apple, "小紅"); Thread t5 = new Thread(apple, "小黑"); t1.start(); t2.start(); t3.start(); t4.start(); t5.start(); } }
可能會輸出如下結(jié)果:
小強吃了一個蘋果,還剩3個蘋果
小黑吃了一個蘋果,還剩3個蘋果
小明吃了一個蘋果,還剩2個蘋果
小花吃了一個蘋果,還剩1個蘋果
小紅吃了一個蘋果,還剩0個蘋果
輸出結(jié)果異常的原因是eatApple方法里操作不是原子的,如當(dāng)A線程完成appleCount的賦值,還沒有輸出,B線程獲取到appleCount的最新值,并完成賦值操作,然后A和B同時輸出。(A,B線程分別對應(yīng)小黑、小強)
如果改下eatApple方法如下,還會不會有線程安全問題呢?
public void eatApple(){ System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + --appleCount + "個蘋果"); }
還是會有的,因為--appleCount不是原子操作,--appleCount可以用另外一種寫法表示:appleCount = appleCount - 1,還是有可能會出現(xiàn)以上的異常輸出結(jié)果。
synchronized的使用
synchronized分為同步方法和同步代碼塊兩種用法,當(dāng)每個線程訪問同步方法或同步代碼塊區(qū)域時,首先需要獲得對象的鎖,搶到鎖的線程可以繼續(xù)執(zhí)行,搶不到鎖的線程則阻塞,等待搶到鎖的線程執(zhí)行完成后釋放鎖。
1.同步代碼塊
鎖的對象是object:
public class Apple implements Runnable { private int appleCount = 5; private Object object = new Object(); @Override public void run() { eatApple(); } public void eatApple(){ //同步代碼塊,此時鎖的對象是object synchronized (object) { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } } //...省略main方法 }
2.同步方法,修飾普通方法
鎖的對象是當(dāng)前類的實例對象:
public class Apple implements Runnable { private int appleCount = 5; @Override public void run() { eatApple(); } public synchronized void eatApple() { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } //...省略main方法 }
等價于以下同步代碼塊的寫法:
public void eatApple() { synchronized (this) { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } }
3.同步方法,修飾靜態(tài)方法
鎖的對象是當(dāng)前類的class對象:
public class Apple implements Runnable { private static int appleCount = 5; @Override public void run() { eatApple(); } public synchronized static void eatApple() { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } //...省略main方法 }
等價于以下同步代碼塊的寫法:
public static void eatApple() { synchronized (Apple.class) { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } }
4.同步方法和同步代碼塊的區(qū)別
a.同步方法鎖的對象是當(dāng)前類的實例對象或者當(dāng)前類的class對象,而同步代碼塊鎖的對象可以是任意對象。
b.同步方法是使用synchronized修飾方法,而同步代碼塊是使用synchronized修飾共享代碼區(qū)域。同步代碼塊相對于同步方法來說粒度更細(xì),鎖的區(qū)域更小,一般鎖范圍越小效率就越高。如下情況顯然同步代碼塊更適用:
public static void eatApple() { //不需要同步的耗時操作1 //... synchronized (Apple.class) { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } //不需要同步的耗時操作2 //... }
內(nèi)置鎖的可重入性
內(nèi)置鎖的可重入性是指當(dāng)某個線程試圖獲取一個它已經(jīng)持有的鎖時,它總是可以獲取成功。如下:
public static void eatApple() { synchronized (Apple.class) { synchronized (Apple.class) { synchronized (Apple.class) { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } } } }
如果鎖不是可重入的,那么假如某線程持有了該鎖,然后又需要等待持有該鎖的線程釋放鎖,這不就造成死鎖了嗎?
synchronized可以被繼承嗎?
synchronized不可以被繼承,如果子類中重寫后的方法需要實現(xiàn)同步,則需要手動添加synchronized關(guān)鍵字。
public class AppleParent { public synchronized void eatApple(){ } } public class Apple extends AppleParent implements Runnable { private int appleCount = 5; @Override public void run() { eatApple(); } @Override public void eatApple() { appleCount--; System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果"); } //...省略main方法 }
基于內(nèi)置鎖的等待和喚醒
基于內(nèi)置鎖的等待和喚醒是使用Object類中的wait()和notify()或notifyAll()來實現(xiàn)的。這些方法的調(diào)用前提是已經(jīng)持有對應(yīng)的鎖,所以只能在同步方法或者同步代碼塊里調(diào)用。如果在沒有獲取到對應(yīng)鎖的情況下調(diào)用則會拋出IllegalMonitorStateException異常。下面介紹下相關(guān)的幾個方法:
wait():使當(dāng)前線程無限期地等待,直到另一個線程調(diào)用notify()或notifyAll()。
wait(long timeout):指定一個超時時間,超時時間過后線程將會被自動喚醒。線程也可以在超時時間之前被notify()或notifyAll()喚醒。注意,wait(0)等同于調(diào)用wait()。
wait(long timeout, int nanos):類似于wait(long timeout),主要區(qū)別是wait(long timeout, int nanos)提供了更高的精度。
notify():隨機喚醒一個在相同鎖對象上等待的線程。
notifyAll():喚醒所有在相同鎖對象上等待的線程。
一個簡單的等待喚醒實例:
public class Apple { //蘋果數(shù)量 private int appleCount = 0; /** * 買蘋果 */ public synchronized void getApple() { try { while (appleCount != 0) { wait(); } } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "買了5個蘋果"); appleCount = 5; notify(); } /** * 吃蘋果 */ public synchronized void eatApple() { try { while (appleCount == 0) { wait(); } } catch (InterruptedException ex) { ex.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "吃了1個蘋果"); appleCount--; notify(); } }
/** * 生產(chǎn)者,買蘋果 */ public class Producer extends Thread{ private Apple apple; public Producer(Apple apple, String name){ super(name); this.apple = apple; } @Override public void run(){ while (true) apple.getApple(); } } /** * 消費者,吃蘋果 */ public class Consumer extends Thread{ private Apple apple; public Consumer(Apple apple, String name){ super(name); this.apple = apple; } @Override public void run(){ while (true) apple.eatApple(); } }
public class Demo { public static void main(String[] args) { Apple apple = new Apple(); Producer producer = new Producer(apple,"小明"); Consumer consumer = new Consumer(apple, "小紅"); producer.start(); consumer.start(); } }
輸出結(jié)果:
小明買了5個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小明買了5個蘋果
小紅吃了1個蘋果
......
到此這篇關(guān)于Java并發(fā)編程之內(nèi)置鎖(synchronized)的文章就介紹到這了,更多相關(guān)Java內(nèi)置鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何解決Could not transfer artifact org.spri
在Maven更新過程中遇到“Could not transfer artifact org.springframework.boot”錯誤通常是由于網(wǎng)絡(luò)問題,解決方法是在Maven的設(shè)置中忽略HTTPS,添加特定語句后,可以正常下載依賴,但下載速度可能較慢,這是一種常見的解決方案,希望對遇到相同問題的人有所幫助2024-09-09Java利用redis zset實現(xiàn)延時任務(wù)詳解
zset作為redis的有序集合數(shù)據(jù)結(jié)構(gòu)存在,排序的依據(jù)就是score。本文就將利用zset score這個排序的這個特性,來實現(xiàn)延時任務(wù),感興趣的可以了解一下2022-08-08SpringBoot @PropertySource與@ImportResource有什么區(qū)別
這篇文章主要介紹了SpringBoot @PropertySource與@ImportResource有什么區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01Java?Chassis3應(yīng)用視角的配置管理技術(shù)解密
這篇文章主要為大家介紹了Java?Chassis3應(yīng)用視角的配置管理相關(guān)的機制和背后故事,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2024-01-01SpringBoot利用切面注解及反射實現(xiàn)事件監(jiān)聽功能
這篇文章主要介紹了springboot事件監(jiān)聽,通過利用切面、注解、反射實現(xiàn),接下來將對這幾種方式逐一說明,具有很好的參考價值,希望對大家有所幫助2022-07-07詳解@ConditionalOnMissingBean注解的作用
這篇文章主要介紹了詳解@ConditionalOnMissingBean注解的作用,@ConditionalOnMissingBean,它是修飾bean的一個注解,主要實現(xiàn)的是,當(dāng)你的bean被注冊之后,如果而注冊相同類型的bean,就不會成功,它會保證你的bean只有一個,需要的朋友可以參考下2023-10-10