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