Java線程同步及實(shí)現(xiàn)方法詳解
1. 什么是線程同步?
首先,引用一個(gè)非常經(jīng)典的例子來(lái)說(shuō)明為什么要進(jìn)行線程同步
當(dāng)我們有多個(gè)線程要同時(shí)訪問(wèn)一個(gè)變量或?qū)ο髸r(shí),如果這些線程中既有讀又有寫(xiě)操作時(shí),就會(huì)導(dǎo)致變量值或?qū)ο蟮臓顟B(tài)出現(xiàn)混亂,從而導(dǎo)致程序異常。 舉個(gè)例子,動(dòng)物園有三個(gè)窗口同時(shí)在售賣(mài)門(mén)票,假設(shè)還剩最后一張門(mén)票時(shí),有兩個(gè)窗口同時(shí)有人在買(mǎi)門(mén)票,此時(shí)兩個(gè)窗口都觀察到還有一張門(mén)票,于是兩個(gè)窗口都選擇了賣(mài)出,此時(shí)門(mén)票數(shù)變成了-1,出現(xiàn)錯(cuò)誤。
還可能會(huì)出現(xiàn)其他情況的錯(cuò)誤,比如剩余10張票時(shí),兩個(gè)窗口同時(shí)售賣(mài)出一張票后修改票數(shù)為9。
package test; import java.io.*; public class TicketThreadTest { public static void main(String[] args) throws IOException, InterruptedException { TicketThread ticket1 = new TicketThread(); Thread thread1 = new Thread(ticket1, "窗口1"); Thread thread2 = new Thread(ticket1, "窗口2"); Thread thread3 = new Thread(ticket1, "窗口3"); thread1.start(); thread2.start(); thread3.start(); thread1.join(); // 等待三個(gè)線程結(jié)束后打印賣(mài)出總數(shù) thread2.join(); thread3.join(); System.out.println("sellNum: " + TicketThread.sellNum); } } class TicketThread implements Runnable { private static int ticketNum = 20; // 總票數(shù) public static int sellNum = 0; // 統(tǒng)計(jì)賣(mài)出總票數(shù) @Override public void run() { while (true) { if (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "賣(mài)出了一張票,剩余:" + --ticketNum); // 賣(mài)出一張票 sellNum++; // 賣(mài)出總票數(shù)加1 } else { break; } try { Thread.sleep(10); // 每次sleep 10ms,提高出錯(cuò)可能 } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
這是某次程序運(yùn)行結(jié)果,很顯然賣(mài)出29張票,發(fā)生了錯(cuò)誤
窗口3賣(mài)出了一張票,剩余:19
窗口1賣(mài)出了一張票,剩余:18
窗口2賣(mài)出了一張票,剩余:17
窗口3賣(mài)出了一張票,剩余:16
窗口1賣(mài)出了一張票,剩余:16
窗口2賣(mài)出了一張票,剩余:16
窗口1賣(mài)出了一張票,剩余:15
窗口3賣(mài)出了一張票,剩余:14
窗口2賣(mài)出了一張票,剩余:15
窗口2賣(mài)出了一張票,剩余:13
窗口3賣(mài)出了一張票,剩余:12
窗口1賣(mài)出了一張票,剩余:13
窗口3賣(mài)出了一張票,剩余:9
窗口1賣(mài)出了一張票,剩余:11
窗口2賣(mài)出了一張票,剩余:10
窗口1賣(mài)出了一張票,剩余:7
窗口3賣(mài)出了一張票,剩余:8
窗口2賣(mài)出了一張票,剩余:8
窗口1賣(mài)出了一張票,剩余:5
窗口3賣(mài)出了一張票,剩余:6
窗口2賣(mài)出了一張票,剩余:6
窗口2賣(mài)出了一張票,剩余:4
窗口3賣(mài)出了一張票,剩余:3
窗口1賣(mài)出了一張票,剩余:4
窗口1賣(mài)出了一張票,剩余:2
窗口3賣(mài)出了一張票,剩余:2
窗口2賣(mài)出了一張票,剩余:1
窗口2賣(mài)出了一張票,剩余:-1
窗口1賣(mài)出了一張票,剩余:0
sellNum: 29
2. Java線程同步方法
Java線程同步有7種方法
- 使用 synchronized關(guān)鍵字實(shí)現(xiàn)線程同步
- 使用wait和notify實(shí)現(xiàn)線程同步
- 使用特殊域變量(volatile)實(shí)現(xiàn)線程同步
- 使用重入鎖實(shí)現(xiàn)線程同步,在JavaSE5.0中新增了一個(gè)java.util.concurrent包來(lái)支持同步
- 使用局部變量實(shí)現(xiàn)線程同步,如果使用ThreadLocal管理變量,則每一個(gè)使用該變量的線程都獲得該變量的副本,副本之間相互獨(dú)立,這樣每一個(gè)線程都可以隨意修改自己的變量副本,而不會(huì)對(duì)其他線程產(chǎn)生影響。
- 使用阻塞隊(duì)列實(shí)現(xiàn)線程同步
- 使用原子變量實(shí)現(xiàn)線程同步
3 使用synchronized實(shí)現(xiàn)線程同步
synchronized的作用主要有三個(gè):
- 原子性:確保線程互斥地訪問(wèn)同步代碼
- 可見(jiàn)性:保證共享變量的修改能夠及時(shí)可見(jiàn)
可見(jiàn)性是通過(guò)Java內(nèi)存模型中的“對(duì)一個(gè)變量unlock操作之前,必須要同步到主內(nèi)存中;如果對(duì)一個(gè)變量進(jìn)行l(wèi)ock操作,則將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用此變量前,需要重新從主內(nèi)存中l(wèi)oad操作或assign操作初始化變量值”來(lái)保證的
- 有序性:有效解決重排序問(wèn)題,即 “一個(gè)unlock操作先行發(fā)生(happen-before)于后面對(duì)同一個(gè)鎖的lock操作”
happen-before:If one action _happens-before _another, then the first is visible to and ordered before the second. 如果指令甲happens-before指令乙,那么指令甲必須排序在指令乙之前,并且指令甲的執(zhí)行結(jié)果對(duì)指令乙可見(jiàn)。
3.1 同步代碼塊
同步代碼塊是通過(guò)鎖定一個(gè)指定的對(duì)象,來(lái)對(duì)同步代碼塊中的代碼進(jìn)行同步。 一個(gè)線程訪問(wèn)一個(gè)對(duì)象中的synchronized(this)同步代碼塊時(shí),其他試圖訪問(wèn)該代碼塊的線程將被阻塞。 注意synchronized必須鎖住的是指定的對(duì)象,不同對(duì)象間不會(huì)阻塞,如果需要鎖住類(lèi)對(duì)象,只需要使用synchronized(Class clazz)鎖住類(lèi)即可。
我們使用同步代碼塊來(lái)解決售票問(wèn)題
package test; import java.io.*; public class TicketThreadTest { public static void main(String[] args) throws IOException, InterruptedException { TicketThread ticket1 = new TicketThread(); Thread thread1 = new Thread(ticket1, "窗口1"); Thread thread2 = new Thread(ticket1, "窗口2"); Thread thread3 = new Thread(ticket1, "窗口3"); thread1.start(); thread2.start(); thread3.start(); thread1.join(); // 等待三個(gè)線程結(jié)束后打印賣(mài)出總數(shù) thread2.join(); thread3.join(); System.out.println("sellNum: " + TicketThread.sellNum); } } class TicketThread implements Runnable { private static int ticketNum = 20; // 總票數(shù) public static int sellNum = 0; // 統(tǒng)計(jì)賣(mài)出總票數(shù) @Override public void run() { while (true) { synchronized (this) { // 鎖住對(duì)共享變量的訪問(wèn) if (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "賣(mài)出了一張票,剩余:" + --ticketNum); // 賣(mài)出一張票 sellNum++; // 賣(mài)出總票數(shù)加1 } else { break; } } try { Thread.sleep(10); // 每次sleep 10ms,提高出錯(cuò)可能 } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
從運(yùn)行結(jié)果可以看到synchronized修飾的代碼塊同一時(shí)間只能有一個(gè)線程訪問(wèn)
窗口1賣(mài)出了一張票,剩余:19
窗口2賣(mài)出了一張票,剩余:18
窗口3賣(mài)出了一張票,剩余:17
窗口3賣(mài)出了一張票,剩余:16
窗口2賣(mài)出了一張票,剩余:15
窗口1賣(mài)出了一張票,剩余:14
窗口1賣(mài)出了一張票,剩余:13
窗口2賣(mài)出了一張票,剩余:12
窗口3賣(mài)出了一張票,剩余:11
窗口2賣(mài)出了一張票,剩余:10
窗口3賣(mài)出了一張票,剩余:9
窗口1賣(mài)出了一張票,剩余:8
窗口1賣(mài)出了一張票,剩余:7
窗口2賣(mài)出了一張票,剩余:6
窗口3賣(mài)出了一張票,剩余:5
窗口2賣(mài)出了一張票,剩余:4
窗口3賣(mài)出了一張票,剩余:3
窗口1賣(mài)出了一張票,剩余:2
窗口3賣(mài)出了一張票,剩余:1
窗口1賣(mài)出了一張票,剩余:0
sellNum: 20
注意上述類(lèi)中的ticketNum和sellNum都屬于類(lèi)對(duì)象,如果我們使用不同的實(shí)例對(duì)象,使用synchronized(this)鎖住的不是同一個(gè)對(duì)象,會(huì)發(fā)現(xiàn)并沒(méi)有實(shí)現(xiàn)線程同步,此時(shí)就需要鎖住synchronized(this.getClass())。
使用不同實(shí)例對(duì)象,main方法中修改如下:
//使用不同實(shí)例對(duì)象,main方法中修改如下: TicketThread ticket1 = new TicketThread(); TicketThread ticket2 = new TicketThread(); TicketThread ticket3 = new TicketThread(); Thread thread1 = new Thread(ticket1, “窗口1”); Thread thread2 = new Thread(ticket2, “窗口2”); Thread thread3 = new Thread(ticket3, “窗口3”); //TicketThread類(lèi)中修改為 synchronized(this.getClass())或synchronized(TicketThread.class)
3.2 同步方法
同步方法是對(duì)這個(gè)方法塊里的代碼進(jìn)行同步,而這種情況下鎖定的對(duì)象就是方法所屬的對(duì)象自身。
相當(dāng)于使用synchronized(this)鎖住方法中的代碼
如果這個(gè)方法是靜態(tài)同步方法呢?那么線程鎖定的就不是這個(gè)類(lèi)的對(duì)象了,而是這個(gè)類(lèi)對(duì)應(yīng)的java.lang.Class類(lèi)型的對(duì)象。
相當(dāng)于使用synchronized(this.getClass())鎖住方法中的代碼
**注意:**當(dāng)一個(gè)同步方法或者同步塊被某個(gè)線程執(zhí)行時(shí),這個(gè)對(duì)象就被鎖定了,其他線程無(wú)法在此時(shí)訪問(wèn)這個(gè)對(duì)象的同步方法,也不能執(zhí)行同步塊,但可以訪問(wèn)非同步方法中的非同步代碼塊。
上述售票問(wèn)題使用同步方法實(shí)現(xiàn)線程同步
package test; import java.io.*; public class TicketThreadTest { public static void main(String[] args) throws IOException, InterruptedException { TicketThread ticket1 = new TicketThread(); Thread thread1 = new Thread(ticket1, "窗口1"); Thread thread2 = new Thread(ticket1, "窗口2"); Thread thread3 = new Thread(ticket1, "窗口3"); thread1.start(); thread2.start(); thread3.start(); thread1.join(); // 等待三個(gè)線程結(jié)束后打印賣(mài)出總數(shù) thread2.join(); thread3.join(); System.out.println("sellNum: " + TicketThread.sellNum); } } class TicketThread implements Runnable { private static int ticketNum = 20; // 總票數(shù) public static int sellNum = 0; // 統(tǒng)計(jì)賣(mài)出總票數(shù) @Override public void run() { while (true) { if(!sellOneTicket()){ break; } try { Thread.sleep(10); // 每次sleep 10ms,提高出錯(cuò)可能 } catch (InterruptedException e) { throw new RuntimeException(e); } } } private synchronized boolean sellOneTicket(){ if (ticketNum > 0) { System.out.println(Thread.currentThread().getName() + "賣(mài)出了一張票,剩余:" + --ticketNum); // 賣(mài)出一張票 sellNum++; // 賣(mài)出總票數(shù)加1 return true; } else { return false; } } }
到此這篇關(guān)于Java線程同步及實(shí)現(xiàn)方法詳解的文章就介紹到這了,更多相關(guān)Java線程同步內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot應(yīng)用部署到外置Tomcat的實(shí)現(xiàn)
SpringBoot內(nèi)置tomcat使用很方便,本文主要介紹了SpringBoot應(yīng)用部署到外置Tomcat的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07SpringBoot+隨機(jī)鹽值+雙重MD5實(shí)現(xiàn)加密登錄
數(shù)據(jù)加密在很多項(xiàng)目上都可以用到,大部分都會(huì)采用MD5進(jìn)行加密,本文主要介紹了SpringBoot+隨機(jī)鹽值+雙重MD5實(shí)現(xiàn)加密登錄,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02Java分頁(yè)查詢(xún)--分頁(yè)顯示(實(shí)例講解)
下面小編就為大家?guī)?lái)一篇Java分頁(yè)查詢(xún)--分頁(yè)顯示(實(shí)例講解)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08Java中JVM的雙親委派、內(nèi)存溢出、垃圾回收和調(diào)優(yōu)詳解
這篇文章主要介紹了Java中JVM的雙親委派、內(nèi)存溢出、垃圾回收和調(diào)優(yōu)詳解,類(lèi)加載器是Java虛擬機(jī)(JVM)的一個(gè)重要組成部分,它的主要作用是將類(lèi)的字節(jié)碼加載到內(nèi)存中,并生成對(duì)應(yīng)的Class對(duì)象,需要的朋友可以參考下2023-07-07SpringBoot集成thymeleaf瀏覽器404的解決方案
前后端不分離的古早 SpringMVC 項(xiàng)目通常會(huì)使用 thymeleaf 模板引擎來(lái)完成 html 頁(yè)面與后端接口之間的交互,如果要將項(xiàng)目架構(gòu)升級(jí)成 SpringBoot , thymeleaf 也可以照常集成,但有時(shí)候會(huì)踩到一些坑,所以本文給大家介紹了SpringBoot集成thymeleaf瀏覽器404的解決方案2024-12-12spring-boot-thin-launcher插件分離jar包的依賴(lài)和配置方式
這篇文章主要介紹了spring-boot-thin-launcher插件分離jar包的依賴(lài)和配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09客戶(hù)端Socket與服務(wù)端ServerSocket串聯(lián)實(shí)現(xiàn)網(wǎng)絡(luò)通信
這篇文章主要為大家介紹了客戶(hù)端Socket與服務(wù)端ServerSocket串聯(lián)實(shí)現(xiàn)網(wǎng)絡(luò)通信的內(nèi)容詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-03-03springboot+element-ui實(shí)現(xiàn)多文件一次上傳功能
這篇文章主要介紹了springboot+element-ui多文件一次上傳功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-06-06