Java多線程(二)

来源:https://www.cnblogs.com/xiaozhao01/archive/2022/08/05/16555150.html
-Advertisement-
Play Games

Java多線程(二) 四、線程的同步 4.1 線程同步的引入: 多線程出現了安全問題。 問題的原因: 當多條語句在操作同一個線程共用數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共用數據的錯誤。例如:買票問題、銀行卡消費問題等等。 解決辦法: 對多條操作共用數據 ...


Java多線程(二)

目錄

四、線程的同步

4.1 線程同步的引入:

  • 多線程出現了安全問題
  • 問題的原因: 當多條語句在操作同一個線程共用數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共用數據的錯誤。例如:買票問題、銀行卡消費問題等等。
  • 解決辦法: 對多條操作共用數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行

​ 所以,Java 對於多線程的安全問題提供了專業的解決方式:同步機制

4.2 線程同步的方式之一:同步代碼塊

  1. 語法格式:
synchronized (對象/同步監視器){	//得到對象的鎖,才能操作同步代碼
	// 需要被同步的代碼
}

說明:

​ (1)操作共用數據的代碼,即為需要被同步的代碼。 --> 不能包含代碼多了,也不能包含代碼少了。

​ (2)共用數據:多個線程共同操作的變數。

​ (3)同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖

​ (4)要求:多個線程必須要共用同一把鎖

  1. 使用同步代碼塊解決在實現 Runnable 介面的方式創建多線程的線程安全問題
//	例子:創建三個視窗賣票,總票數為100張.使用實現Runnable介面的方式
class Window1 implements Runnable{

    private int ticket = 100;
//    Object obj = new Object();
    @Override
    public void run() {
//      Object obj = new Object();
        while(true){
            // 同步代碼塊---begin
            synchronized (this){	// 此時的this:唯一的 Window1 的對象w   //方式二:synchronized (object) {

                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
            // 同步代碼塊---end
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  1. 使用同步代碼塊解決繼承 Thread 類的方式創建多線程的線程安全問題
class Window2 extends Thread{

    private static int ticket = 100;
//    private static Object obj = new Object();

    @Override
    public void run() {

        while(true){

            synchronized (Window2.class){//	Window2.class表示window2這一個類,只會載入一次
//       方式二:synchronized (obj){	               
//              synchronized (this){	錯誤的方式:this分別代表著t1,t2,t3三個對象

                if(ticket > 0){

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(getName() + ":賣票,票號為:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

public class WindowTest2 {
    public static void main(String[] args) {
        Window2 t1 = new Window2();
        Window2 t2 = new Window2();
        Window2 t3 = new Window2();

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}

4.3 線程同步的方式之二:同步方法

  1. 同步方法:即將操作共用數據的代碼完整的聲明在一個方法中,將該方法聲明為同步方法。

  2. 語法格式:

//	將synchronized放在方法聲明中,一般放在許可權符和返回類型之間,表示整個方法為同步方法
public synchronized void 方法名 (String name){ 
	// 需要被同步的代碼
}

說明:

(1)同步方法仍然涉及到同步監視器,只是不需要我們顯式的聲明。

(2)非靜態的同步方法,同步監視器是:this。

(3)靜態的同步方法,同步監視器是:當前類本身。

  1. 使用同步方法解決在實現 Runnable 介面的方式創建多線程的線程安全問題
class Window3 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show(){	//同步監視器:this

        if (ticket > 0) {
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
            ticket--;
            }
    }
}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();

        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();
    }
}
  1. 使用同步方法解決繼承 Thread 類的方式創建多線程的線程安全問題
class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {

        while (true) {
            show();
        }
    }
    
    private static synchronized void show(){	//同步監視器:Window4.class
        //private synchronized void show(){ //同步監視器:t1,t2,t3。此種解決方式是錯誤的
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
            ticket--;
        }
    }
}


public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");

        t1.start();
        t2.start();
        t3.start();

    }
}

總結:

通過上面四個售票例子可以看出:

在實現 Runnable 介面創建多線程的方式中,我們可以考慮使用 this 充當同步監視器。

而在繼承Thread類創建多線程的方式一般不使用 this 充當同步監視器,因為每個線程的 this 為該線程的實例對象,不滿足多個線程共用一把鎖,所以一般考慮用當前類本身充當。

4.4 同步的優勢與局限:

  • 優勢:解決了線程的安全問題。

  • 局限: 操作同步代碼時,只能有一個線程參與,其他線程等待。相當於是一個單線程的過程,效率低。也可能會造成死鎖問題。

4.5 線程安全的單例模式之懶漢式

public class BankTest {

}

class Bank{

    private Bank(){}

    private static Bank instance = null;

    public static Bank getInstance(){
        //方式一:效率稍差
//        synchronized (Bank.class) {
//            if(instance == null){
//                instance = new Bank();
//            }
//            return instance;
//        }
        //方式二:效率更高
        if(instance == null){
            synchronized (Bank.class) {
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

/*
	方式二效率更高的原因:
	不需要每個線程都要進去同步方法裡面去,可能只有最前面幾個進程進入同步方法。
	而方式一是所有線程都要進入同步方法導致效率較低。
*/

4.6 同步鎖機制:

  • 同步機制中的鎖在《Thinking in Java》中,是這麼說的:對於併發工作,你需要某種方式來防止兩個任務訪問相同的資源(其實就是共用資源競爭)。防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定並使用它了。

  • synchronized的鎖是什麼?

    1. 任意對象都可以作為同步鎖。所有對象都自動含有單一的鎖(監視器)。

    2. 同步方法的鎖:靜態方法(類名.class)、非靜態方法(this)。

    3. 同步代碼塊:可以自己指定,很多時候也是指定為this或類名.class。

  • 註意:

    1. 必須確保使用同一個資源的多個線程共用一把鎖,否則就無法保證共用資源的安全 。
    2. 一個線程類中的所有靜態方法共用同一把鎖(類名.class),所有非靜態方法共用同一把鎖(this),同步代碼塊(指定需謹慎)。

4.7 釋放鎖的操作:

  1. 當前線程的同步方法、同步代碼塊執行結束。
  2. 當前線程在同步代碼塊、同步方法中遇到 break、return 終止了該代碼塊、該方法的繼續執行。
  3. 當前線程在同步代碼塊、同步方法中出現了未處理的 Error 或 Exception,導致異常結束。
  4. 當前線程在同步代碼塊、同步方法中執行了線程對象的 wait() 方法,當前線程暫停,並釋放鎖。

4.8 不會釋放鎖的操作:

  1. 線程執行同步代碼塊或同步方法時,程式調用 Thread.sleep()、 Thread.yield() 方法暫停當前線程的執行。

  2. 線程執行同步代碼塊時,其他線程調用了該線程的 suspend() 方法將該線程掛起,該線程不會釋放鎖(同步監視器)。

    應儘量避免使用suspend()和resume()來控制線程

4.9 線程的死鎖問題

  1. 死鎖:

    • 不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖。
    • 出現死鎖後,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續。
  2. 解決方法:

    • 專門的演算法、原則。
    • 儘量減少同步資源的定義。
    • 儘量避免嵌套同步。
  3. 例子:

//死鎖的演示
class A {
	public synchronized void foo(B b) { //同步監視器:A類的對象:a
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 進入了A實例的foo方法"); // ①
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 企圖調用B實例的last方法"); // ③
		b.last();
	}

	public synchronized void last() {//同步監視器:A類的對象:a
		System.out.println("進入了A類的last方法內部");
	}
}

class B {
	public synchronized void bar(A a) {//同步監視器:b
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 進入了B實例的bar方法"); // ②
//		try {
//			Thread.sleep(200);
//		} catch (InterruptedException ex) {
//			ex.printStackTrace();
//		}
		System.out.println("當前線程名: " + Thread.currentThread().getName()
				+ " 企圖調用A實例的last方法"); // ④
		a.last();
	}

	public synchronized void last() {//同步監視器:b
		System.out.println("進入了B類的last方法內部");
	}
}

public class DeadLock implements Runnable {
	A a = new A();
	B b = new B();

	public void init() {
		Thread.currentThread().setName("主線程");
		// 調用a對象的foo方法
		a.foo(b);
		System.out.println("進入了主線程之後");
	}

	public void run() {
		Thread.currentThread().setName("副線程");
		// 調用b對象的bar方法
		b.bar(a);
		System.out.println("進入了副線程之後");
	}

	public static void main(String[] args) {
		DeadLock dl = new DeadLock();
		new Thread(dl).start();

		dl.init();
	}
}
  1. 註意:

    不是程式運行成功就說明程式裡面沒有死鎖問題,很多時候出現死鎖是一個概率問題。在開發中應儘量避免死鎖情況。

4.10 線程同步的方式之三:Lock鎖

  1. JDK 5.0 開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象來實現同步。同步鎖使用 Lock 對象充當。

  2. java.util.concurrent.locks.Lock 介面是控制多個線程對共用資源進行訪問的工具。鎖提供了對共用資源的獨占訪問,每次只能有一個線程對 Lock 對象加鎖,線程開始訪問共用資源之前應先獲得 Lock 對象。

  3. ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和 記憶體語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以 顯式加鎖、釋放鎖。

  4. 語法:

class A{
    //1.實例化ReentrantLock
	private final ReentrantLock lock = new ReenTrantLock();
    
	public void m(){
        //2.調用鎖定方法lock()
		lock.lock();
		try{
			//保證線程安全的代碼;
		}
		finally{
            //3.調用解鎖方法:unlock()
			lock.unlock(); 
		}
	}
}

  1. (面試題)synchronized 與 Lock 的對比

    • 相同:二者都可以解決線程安全問題。
    • 不同:
      1. Lock 是顯式鎖(手動開啟和關閉鎖),synchronized 是隱式鎖,出了作用域自動釋放。
      2. Lock 只有代碼塊鎖,synchronized 有代碼塊鎖和方法鎖。
      3. 使用 Lock 鎖,JVM 將花費較少的時間來調度線程,性能更好。並且具有更好的擴展性(提供更多的子類)。
  2. 優先使用順序:

Lock —— 同步代碼塊(已經進入了方法體,分配了相應資源) —— 同步方法(在方法體之外)

4.11 (簡單介紹)公平鎖和非公平鎖

  1. 公平鎖(Fair):加鎖前檢查是否有排隊等待的線程,優先排隊等待的線程,先來先得。

    非公平鎖(Nonfair):加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待。

  2. Java中的 ReentrantLock 預設的 lock() 方法採用的是非公平鎖。

//	源碼:
//	ReentrantLock當中的lock()方法,是通過static內部類sync來進行鎖操作
public void lock() 
{
     sync.lock();
}
-------------------------------------------------------------------
//定義成final型的成員變數,在構造方法中進行初始化 
private final Sync sync;
//無參數預設非公平鎖
public ReentrantLock() 
{
    sync = new NonfairSync();
}
//根據參數初始化為公平鎖或者非公平鎖 
public ReentrantLock(boolean fair) 
{
    sync = fair ? new FairSync() : new NonfairSync();
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 事件起因 之前做一個駕照考題的項目,有一個這樣的問題,每當我選好了科目和駕照類型後(如圖1),點擊開始考試就會跳到考試頁面(Test.tsx),並且在Test組件中對我架設的中間層發起請求獲取數據(如圖2)。 如果用戶手滑的話不小心點到了左上角的返回,或者狠狠地把屏幕往右滑動一下的話,都會返回 ...
  • 1. 事件起因 事情是這樣的,我之前在做一個仿網易雲的項目,想實現一個功能,就是點擊歌曲列表組件(MusicList)中每一個item時,底部播放欄(FooterMusic)就能夠獲取我所點擊的這首歌曲的所有信息(如圖1到圖2),但是底部播放欄是直接放在外殼組件App.vue中固定定位的,歌曲列表組 ...
  • 引言 當今社會對軟體需求在相當長的時間里將保持旺盛,而軟體開發周期長、個性化難、順應需求變更不變,如何可以才能將軟體開發定製變得簡單方便快捷呢? 1. 軟體開發設計現狀 目前軟體的開發設計都是定向開發,即根據項目需求將相關的數據關係、業務邏輯、功能模塊及介面插件等揉合在一起並與人機交互整體開發(若涉 ...
  • 單元測試JUnit 單元測試的目的是針對方法進行測試, **JUnit的兩個要點:**①必須是公開的,無參數,無返回值的方法 ②測試方法必須使用@Test註解標記 public class JUnitTest { @Test public void Testusername() { way way ...
  • 目錄 一.簡介 二.效果演示 三.源碼下載 四.猜你喜歡 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL (ES) 學習路線推薦 : OpenGL (ES) 學習目錄 >> OpenGL ES 轉場 零基礎 O ...
  • 多線程併發 在多核CPU中,利用多線程併發編程,可以更加充分地利用每個核的資源 在Java中,一個應用程式對應著一個JVM實例(也有地方稱為JVM進程),如果程式沒有主動創建線程,則只會創建一個主線程。但這不代表JVM中只有一個線程,JVM實例在創建的時候,同時會創建很多其他的線程(比如垃圾收集器線 ...
  • Java多線程(三) 五、線程的通信 5.1 wait() 與 notify() 和 notifyAll() 介紹: wait():令當前線程掛起並放棄CPU、同步資源並等待,使別的線程可訪問並修改共用資源,而當前線程排隊等候其他線程調用notify() 或 notifyAll() 方法喚醒,喚醒後 ...
  • (目錄) turtle庫的介紹 turtle庫也叫海龜庫,1969年誕生,是turtle繪圖體系的Python實現。turtle庫是Python語言的標準庫之一,是入門級的圖形繪製函數庫。 turtle庫的使用 方法一:import turtle 這種方法在之後每次引用turtle庫裡面的函數都需要 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...