深入了解Java中Synchronized的各種使用方法
在Java當(dāng)中synchronized通常是用來(lái)標(biāo)記一個(gè)方法或者代碼塊。在Java當(dāng)中被synchronized標(biāo)記的代碼或者方法在同一個(gè)時(shí)刻只能夠有一個(gè)線程執(zhí)行被synchronized修飾的方法或者代碼塊。因此被synchronized修飾的方法或者代碼塊不會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)的情況,也就是說(shuō)被synchronized修飾的代碼塊是并發(fā)安全的。
Synchronized關(guān)鍵字
synchronized關(guān)鍵字通常使用在下面四個(gè)地方:
- synchronized修飾實(shí)例方法。
- synchronized修飾靜態(tài)方法。
- synchronized修飾實(shí)例方法的代碼塊。
- synchronized修飾靜態(tài)方法的代碼塊。
在實(shí)際情況當(dāng)中我們需要仔細(xì)分析我們的需求選擇合適的使用synchronized方法,在保證程序正確的情況下提升程序執(zhí)行的效率。
Synchronized修飾實(shí)例方法
下面是一個(gè)用Synchronized修飾實(shí)例方法的代碼示例:
public class SyncDemo { private int count; public synchronized void add() { count++; } public static void main(String[] args) throws InterruptedException { SyncDemo syncDemo = new SyncDemo(); Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { syncDemo.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { syncDemo.add(); } }); t1.start(); t2.start(); t1.join(); // 阻塞住線程等待線程 t1 執(zhí)行完成 t2.join(); // 阻塞住線程等待線程 t2 執(zhí)行完成 System.out.println(syncDemo.count);// 輸出結(jié)果為 20000 } }
在上面的代碼當(dāng)中的add方法只有一個(gè)簡(jiǎn)單的count++操作,因?yàn)檫@個(gè)方法是使用synchronized修飾的因此每一個(gè)時(shí)刻只能有一個(gè)線程執(zhí)行add方法,因此上面打印的結(jié)果是20000。如果add方法沒(méi)有使用synchronized修飾的話,那么線程t1和線程t2就可以同時(shí)執(zhí)行add方法,這可能會(huì)導(dǎo)致最終count的結(jié)果小于20000,因?yàn)閏ount++操作不具備原子性。
上面的分析還是比較明確的,但是我們還需要知道的是synchronized修飾的add方法一個(gè)時(shí)刻只能有一個(gè)線程執(zhí)行的意思是對(duì)于一個(gè)SyncDemo類的對(duì)象來(lái)說(shuō)一個(gè)時(shí)刻只能有一個(gè)線程進(jìn)入。比如現(xiàn)在有兩個(gè)SyncDemo的對(duì)象s1和s2,一個(gè)時(shí)刻只能有一個(gè)線程進(jìn)行s1的add方法,一個(gè)時(shí)刻只能有一個(gè)線程進(jìn)入s2的add方法,但是同一個(gè)時(shí)刻可以有兩個(gè)不同的線程執(zhí)行s1和s2的add方法,也就說(shuō)s1的add方法和s2的add是沒(méi)有關(guān)系的,一個(gè)線程進(jìn)入s1的add方法并不會(huì)阻止另外的線程進(jìn)入s2的add方法,也就是說(shuō)synchronized在修飾一個(gè)非靜態(tài)方法的時(shí)候“鎖”住的只是一個(gè)實(shí)例對(duì)象,并不會(huì)“鎖”住其它的對(duì)象。其實(shí)這也很容易理解,一個(gè)實(shí)例對(duì)象是一個(gè)獨(dú)立的個(gè)體別的對(duì)象不會(huì)影響他,他也不會(huì)影響別的對(duì)象。
Synchronized修飾靜態(tài)方法
Synchronized修飾靜態(tài)方法:
public class SyncDemo { private static int count; public static synchronized void add() { count++; // 注意 count 也要用 static 修飾 否則編譯通過(guò)不了 } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { SyncDemo.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { SyncDemo.add(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(SyncDemo.count); // 輸出結(jié)果為 20000 } }
上面的代碼最終輸出的結(jié)果也是20000,但是與前一個(gè)程序不同的是。這里的add方法用static修飾的,在這種情況下真正的只能有一個(gè)線程進(jìn)入到add代碼塊,因?yàn)橛胹tatic修飾的話是所有對(duì)象公共的,因此和前面的那種情況不同,不存在兩個(gè)不同的線程同一時(shí)刻執(zhí)行add方法。
你仔細(xì)想想如果能夠讓兩個(gè)不同的線程執(zhí)行add代碼塊,那么count++的執(zhí)行就不是原子的了。那為什么沒(méi)有用static修飾的代碼為什么可以呢?因?yàn)楫?dāng)沒(méi)有用static修飾時(shí),每一個(gè)對(duì)象的count都是不同的,內(nèi)存地址不一樣,因此在這種情況下count++這個(gè)操作仍然是原子的!
Sychronized修飾多個(gè)方法
synchronized修飾多個(gè)方法示例:
public class AddMinus { public static int ans; public static synchronized void add() { ans++; } public static synchronized void minus() { ans--; } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { AddMinus.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { AddMinus.minus(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(AddMinus.ans); // 輸出結(jié)果為 0 } }
在上面的代碼當(dāng)中我們用synchronized修飾了兩個(gè)方法,add和minus。這意味著在同一個(gè)時(shí)刻這兩個(gè)函數(shù)只能夠有一個(gè)被一個(gè)線程執(zhí)行,也正是因?yàn)閍dd和minus函數(shù)在同一個(gè)時(shí)刻只能有一個(gè)函數(shù)被一個(gè)線程執(zhí)行,這才會(huì)導(dǎo)致ans最終輸出的結(jié)果等于0。
對(duì)于一個(gè)實(shí)例對(duì)象來(lái)說(shuō):
public class AddMinus { public int ans; public synchronized void add() { ans++; } public synchronized void minus() { ans--; } public static void main(String[] args) throws InterruptedException { AddMinus addMinus = new AddMinus(); Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { addMinus.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { addMinus.minus(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(addMinus.ans); } }
上面的代碼沒(méi)有使用static關(guān)鍵字,因此我們需要new出一個(gè)實(shí)例對(duì)象才能夠調(diào)用add和minus方法,但是同樣對(duì)于AddMinus的實(shí)例對(duì)象來(lái)說(shuō)同一個(gè)時(shí)刻只能有一個(gè)線程在執(zhí)行add或者minus方法,因此上面代碼的輸出同樣是0。
Synchronized修飾實(shí)例方法代碼塊
Synchronized修飾實(shí)例方法代碼塊
public class CodeBlock { private int count; public void add() { System.out.println("進(jìn)入了 add 方法"); synchronized (this) { count++; } } public void minus() { System.out.println("進(jìn)入了 minus 方法"); synchronized (this) { count--; } } public static void main(String[] args) throws InterruptedException { CodeBlock codeBlock = new CodeBlock(); Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { codeBlock.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { codeBlock.minus(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(codeBlock.count); // 輸出結(jié)果為 0 } }
有時(shí)候我們并不需要用synchronized去修飾代碼塊,因?yàn)檫@樣并發(fā)度就比較低了,一個(gè)方法一個(gè)時(shí)刻只能有一個(gè)線程在執(zhí)行。因此我們可以選擇用synchronized去修飾代碼塊,只讓某個(gè)代碼塊一個(gè)時(shí)刻只能有一個(gè)線程執(zhí)行,除了這個(gè)代碼塊之外的代碼還是可以并行的。
比如上面的代碼當(dāng)中add和minus方法沒(méi)有使用synchronized進(jìn)行修飾,因此一個(gè)時(shí)刻可以有多個(gè)線程執(zhí)行這個(gè)兩個(gè)方法。在上面的synchronized代碼塊當(dāng)中我們使用了this對(duì)象作為鎖對(duì)象,只有拿到這個(gè)鎖對(duì)象的線程才能夠進(jìn)入代碼塊執(zhí)行,而在同一個(gè)時(shí)刻只能有一個(gè)線程能夠獲得鎖對(duì)象。也就是說(shuō)add函數(shù)和minus函數(shù)用synchronized修飾的兩個(gè)代碼塊同一個(gè)時(shí)刻只能有一個(gè)代碼塊的代碼能夠被一個(gè)線程執(zhí)行,因此上面的結(jié)果同樣是0。
這里說(shuō)的鎖對(duì)象是this也就CodeBlock類的一個(gè)實(shí)例對(duì)象,因?yàn)樗i住的是一個(gè)實(shí)例對(duì)象,因此當(dāng)實(shí)例對(duì)象不一樣的時(shí)候他們之間是沒(méi)有關(guān)系的,也就是說(shuō)不同實(shí)例用synchronized修飾的代碼塊是沒(méi)有關(guān)系的,他們之間是可以并發(fā)的。
Synchronized修飾靜態(tài)代碼塊
public class CodeBlock { private static int count; public static void add() { System.out.println("進(jìn)入了 add 方法"); synchronized (CodeBlock.class) { count++; } } public static void minus() { System.out.println("進(jìn)入了 minus 方法"); synchronized (CodeBlock.class) { count--; } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { CodeBlock.add(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { CodeBlock.minus(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(CodeBlock.count); } }
上面的代碼是使用synchronized修飾靜態(tài)代碼塊,上面代碼的鎖對(duì)象是CodeBlock.class,這個(gè)時(shí)候他不再是鎖住一個(gè)對(duì)象了,而是一個(gè)類了,這個(gè)時(shí)候的并發(fā)度就變小了,上一份代碼當(dāng)鎖對(duì)象是CodeBlock的實(shí)例對(duì)象時(shí)并發(fā)度更大一些,因?yàn)楫?dāng)鎖對(duì)象是實(shí)例對(duì)象的時(shí)候,只有實(shí)例對(duì)象內(nèi)部是不能夠并發(fā)的,實(shí)例之間是可以并發(fā)的。但是當(dāng)鎖對(duì)象是CodeBlock.class的時(shí)候,實(shí)例對(duì)象之間時(shí)不能夠并發(fā)的,因?yàn)檫@個(gè)時(shí)候的鎖對(duì)象是一個(gè)類。
應(yīng)該用什么對(duì)象作為鎖對(duì)象
在前面的代碼當(dāng)中我們分別使用了實(shí)例對(duì)象和類的class對(duì)象作為鎖對(duì)象,事實(shí)上你可以使用任何對(duì)象作為鎖對(duì)象,但是不推薦使用字符串和基本類型的包裝類作為鎖對(duì)象,這是因?yàn)樽址畬?duì)象和基本類型的包裝對(duì)象會(huì)有緩存的問(wèn)題。字符串有字符串常量池,整數(shù)有小整數(shù)池。因此在使用這些對(duì)象的時(shí)候他們可能最終都指向同一個(gè)對(duì)象,因?yàn)橹赶虻亩际峭粋€(gè)對(duì)象,線程獲得鎖對(duì)象的難度就會(huì)增加,程序的并發(fā)度就會(huì)降低。
比如在下面的示例代碼當(dāng)中就是由于鎖對(duì)象是同一個(gè)對(duì)象而導(dǎo)致并發(fā)度下降:
import java.util.concurrent.TimeUnit; public class Test { public void testFunction() throws InterruptedException { synchronized ("HELLO WORLD") { System.out.println(Thread.currentThread().getName() + "\tI am in synchronized code block"); TimeUnit.SECONDS.sleep(5); } } public static void main(String[] args) { Test t1 = new Test(); Test t2 = new Test(); Thread thread1 = new Thread(() -> { try { t1.testFunction(); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread thread2 = new Thread(() -> { try { t2.testFunction(); } catch (InterruptedException e) { e.printStackTrace(); } }); thread1.start(); thread2.start(); } }
在上面的代碼當(dāng)中我們使用兩個(gè)不同的線程執(zhí)行兩個(gè)不同的對(duì)象內(nèi)部的testFunction函數(shù),按道理來(lái)說(shuō)這兩個(gè)線程是可以同時(shí)執(zhí)行的,因?yàn)閳?zhí)行的是兩個(gè)不同的實(shí)例對(duì)象的同步代碼塊。但是上面代碼的執(zhí)行首先一個(gè)線程會(huì)進(jìn)入同步代碼塊然后打印輸出,等待5秒之后,這個(gè)線程退出同步代碼塊另外一個(gè)線程才會(huì)再進(jìn)入同步代碼塊,這就說(shuō)明了兩個(gè)線程不是同時(shí)執(zhí)行的,其中一個(gè)線程需要等待另外一個(gè)線程執(zhí)行完成才執(zhí)行。這正是因?yàn)閮蓚€(gè)Test對(duì)象當(dāng)中使用的"HELLO WORLD"字符串在內(nèi)存當(dāng)中是同一個(gè)對(duì)象,是存儲(chǔ)在字符串常量池中的對(duì)象,這才導(dǎo)致了鎖對(duì)象的競(jìng)爭(zhēng)。
下面的代碼執(zhí)行的結(jié)果也是一樣的,一個(gè)線程需要等待另外一個(gè)線程執(zhí)行完成才能夠繼續(xù)執(zhí)行,這是因?yàn)樵贘ava當(dāng)中如果整數(shù)數(shù)據(jù)在[-128, 127]之間的話使用的是小整數(shù)池當(dāng)中的對(duì)象,使用的也是同一個(gè)對(duì)象,這樣可以減少頻繁的內(nèi)存申請(qǐng)和回收,對(duì)內(nèi)存更加友好。
import java.util.concurrent.TimeUnit; public class Test { public void testFunction() throws InterruptedException { synchronized (Integer.valueOf(1)) { System.out.println(Thread.currentThread().getName() + "\tI am in synchronized code block"); TimeUnit.SECONDS.sleep(5); } } public static void main(String[] args) { Test t1 = new Test(); Test t2 = new Test(); Thread thread1 = new Thread(() -> { try { t1.testFunction(); } catch (InterruptedException e) { e.printStackTrace(); } }); Thread thread2 = new Thread(() -> { try { t2.testFunction(); } catch (InterruptedException e) { e.printStackTrace(); } }); thread1.start(); thread2.start(); } }
Synchronized與可見性和重排序
可見性
當(dāng)一個(gè)線程進(jìn)入到synchronized同步代碼塊的時(shí)候,將會(huì)刷新所有對(duì)該線程的可見的變量,也就是說(shuō)如果其他線程修改了某個(gè)變量,而且線程需要在Synchronized代碼塊當(dāng)中使用,那就會(huì)重新刷新這個(gè)變量到內(nèi)存當(dāng)中,保證這個(gè)變量對(duì)于執(zhí)行同步代碼塊的線程是可見的。
當(dāng)一個(gè)線程從同步代碼塊退出的時(shí)候,也會(huì)將線程的工作內(nèi)存同步到內(nèi)存當(dāng)中,保證在同步代碼塊當(dāng)中修改的變量對(duì)其他線程可見。
重排序
Java編譯器和JVM當(dāng)發(fā)現(xiàn)能夠讓程序執(zhí)行的更快的時(shí)候是可能對(duì)程序的指令進(jìn)行重排序處理的,也就是通過(guò)調(diào)換程序指令執(zhí)行的順序讓程序執(zhí)行的更快。
但是重排序很可能讓并發(fā)程序產(chǎn)生問(wèn)題,比如說(shuō)當(dāng)一個(gè)在synchronized代碼塊當(dāng)中的寫操作被重排序到synchronized同步代碼塊外部了這顯然是有問(wèn)題的。
在JVM的實(shí)現(xiàn)當(dāng)中是不允許synchronized代碼塊內(nèi)部的指令和他前面和后面的指令進(jìn)行重排序的,但是在synchronized內(nèi)部的指令是可能與synchronized內(nèi)部的指令進(jìn)行重排序的,比較著名的就是DCL單例模式,他就是在synchronized代碼塊當(dāng)中存在重排序的,如果你對(duì)DCL單例模式還不是很熟悉,你可以閱讀這篇文章的DCL單例模式部分。
總結(jié)
在本篇文章當(dāng)中主要介紹了各種synchronized的使用方法,總結(jié)如下:
Synchronized修飾實(shí)例方法,這種情況不同的對(duì)象之間是可以并發(fā)的。
Synchronized修飾實(shí)例方法,這種情況下不同的對(duì)象是不能并發(fā)的,但是不同的類之間可以進(jìn)行并發(fā)。
Sychronized修飾多個(gè)方法,這多個(gè)方法在統(tǒng)一時(shí)刻只能有一個(gè)方法被執(zhí)行,而且只能有一個(gè)線程能夠執(zhí)行。
Synchronized修飾實(shí)例方法代碼塊,同一個(gè)時(shí)刻只能有一個(gè)線程執(zhí)行代碼塊。
Synchronized修飾靜態(tài)代碼塊,同一個(gè)時(shí)刻只能有一個(gè)線程執(zhí)行這個(gè)代碼塊,而且不同的對(duì)象之間不能夠進(jìn)行并發(fā)。
應(yīng)該用什么對(duì)象作為鎖對(duì)象,建議不要使用字符串和基本類型的包裝類作為鎖對(duì)象,因?yàn)镴ava對(duì)這些進(jìn)行優(yōu)化,很可能多個(gè)對(duì)象使用的是同一個(gè)鎖對(duì)象,這會(huì)大大降低程序的并發(fā)度。
程序在進(jìn)入和離開Synchronized代碼塊的時(shí)候都會(huì)將線程的工作內(nèi)存刷新到內(nèi)存當(dāng)中,以保證數(shù)據(jù)的可見性,這一點(diǎn)和volatile關(guān)鍵字很像,同時(shí)Synchronized代碼塊中的指令不會(huì)和Synchronized代碼塊之間和之后的指令進(jìn)行重排序,但是Synchronized代碼塊內(nèi)部可能進(jìn)行重排序。
以上就是深入了解Java中Synchronized的各種使用方法的詳細(xì)內(nèi)容,更多關(guān)于Java Synchronized用法的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
啟動(dòng)Tomcat報(bào)錯(cuò)Unsupported major.minor version xxx的解決方法
這篇文章主要為大家詳細(xì)介紹了啟動(dòng)Tomcat報(bào)錯(cuò)Unsupported major.minor version xxx的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-11-11springboot集成ftp實(shí)現(xiàn)文件上傳
這篇文章主要為大家詳細(xì)介紹了springboot集成ftp實(shí)現(xiàn)文件上傳,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05spring事務(wù)Propagation及其實(shí)現(xiàn)原理詳解
這篇文章主要介紹了spring事務(wù)Propagation及其實(shí)現(xiàn)原理詳解,分享了相關(guān)代碼示例,小編覺得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02Java中PriorityQueue實(shí)現(xiàn)最小堆和最大堆的用法
很多時(shí)候都會(huì)遇到PriorityQueue,本文主要介紹了Java中PriorityQueue實(shí)現(xiàn)最小堆和最大堆的用法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06Spring Cloud中Eureka開啟密碼認(rèn)證的實(shí)例
這篇文章主要介紹了Spring Cloud中Eureka開啟密碼認(rèn)證的實(shí)例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05