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
  • 使用原因: 在我們服務端調用第三方介面時,如:支付寶,微信支付,我們服務端需要模擬http請求並加上一些自己的邏輯響應給前端最終達到我們想要的效果 1.使用WebClient 引用命名空間 using System.Net; using System.Collections.Specialized; ...
  • WPF 實現帶蒙版的 MessageBox 消息提示框 WPF 實現帶蒙版的 MessageBox 消息提示框 作者:WPFDevelopersOrg 原文鏈接: https://github.com/WPFDevelopersOrg/WPFDevelopers.Minimal 框架使用大於等於.N ...
  • 一、JSON(JavaScript Object Notation)的簡介: ① JSON和XML類似,主要用於存儲和傳輸文本信息,但是和XML相比,JSON更小、更快、更易解析、更易編寫與閱讀。 ② C、Python、C++、Java、PHP、Go等編程語言都支持JSON。 二、JSON語法規則: ...
  • 1.避免Scoped模式註冊的服務變成Singleton模式 當提供一個生命周期模式為Singleton的服務實例時,如果發現該服務中還依賴生命周期模式為Scoped的服務實例(Scoped服務實例將被一個Singleton服務實例所引用),那麼這個被依賴的Scoped服務實例最終會成為一個Sing ...
  • 索引時資料庫提高數據查詢處理性能的一個非常關鍵的技術,索引的使用可以對性能產生上百倍甚至上千倍的影響。接下來,會介紹索引的基本原理、概念,並深入學習資料庫中所使用的索引結構和存儲方式,以及如何管理、維護索引等。 1.索引的基本概念 索引時用來快速查詢表記錄的一種存儲結構,一般使用索引有一下兩個方面: ...
  • django2 路由控制器 Route路由,是一種映射關係。路由是把客戶端請求的url路徑和用戶請求的應用程式,這裡意指django裡面的視圖進行綁定映射的一種關係。 請求路徑和視圖函數不是一一對應的關係 在django中所有的路由最終都被保存到一個叫urlpatterns的文件里,並且該文件必須在 ...
  • 1、我們的目標是獲取微博某博主的全部圖片、視頻 2、拿到網址後 我們先觀察 打開F12 隨著下滑我們發現載入出來了一個叫mymblog的東西,展開響應發現需要的東西就在裡面 3、重點來了!!! 通過觀察發現第二頁比第一頁多了參數since_id 而第二頁的since_id參數剛好在上一頁中能獲取到, ...
  • 一、實現原理 在Servlet3協議規範中,包含在JAR文件/META-INFO/resources/路徑下的資源可以直接訪問。 二、舉例說明 如下圖所示,是我新建的一個Spring Boot Starter項目:zimug-minitor-threadpool,用於實現可配置、可觀測的線程池。其中 ...
  • 精華筆記: static final常量:應用率高 必須聲明同時初始化 由類名打點來訪問,不能被改變 建議:常量所有字母都大寫,多個單詞用_分隔 編譯器在編譯時會將常量直接替換為具體的數,效率高 何時用:數據永遠不變,並且經常使用 抽象方法: 由abstract修飾 只有方法的定義,沒有具體的實現( ...
  • Python有一個for...else語法,它的寫法如下 for i in range(0,100): if i == 3: break else: print("Not found") 該語句表示:若for迴圈遍歷完畢,則執行else部分的語句。也就是說上述代碼不會有任何輸出,而下述代碼會輸出“N ...