java基礎(27):線程安全、線程同步、等待喚醒機制

来源:https://www.cnblogs.com/liuhui0308/archive/2019/10/11/11657262.html
-Advertisement-
Play Games

1. 多線程 如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程式每次運行結果和單線程運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是線程安全的。 我們通過一個案例,演示線程的安全問題: 電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的 ...


1. 多線程

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。程式每次運行結果和單線程運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是線程安全的。

我們通過一個案例,演示線程的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。

我們來模擬電影院的售票視窗,實現多個視窗同時賣 “功夫熊貓3”這場電影票(多個視窗一起賣這100張票)

需要視窗,採用線程對象來模擬;需要票,Runnable介面子類來模擬

測試類

public class ThreadDemo {
    public static void main(String[] args) {
        //創建票對象
        Ticket ticket = new Ticket();
        
        //創建3個視窗
        Thread t1  = new Thread(ticket, "視窗1");
        Thread t2  = new Thread(ticket, "視窗2");
        Thread t3  = new Thread(ticket, "視窗3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

模擬票

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;

    @Override
    public void run() {
        //模擬賣票
        while(true){
            if (ticket > 0) {
                //模擬選坐的操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
            }
        }
    }

運行結果發現:上面程式出現了問題

票出現了重覆的票

錯誤的票 0、-1

其實,線程安全問題都是由全局變數及靜態變數引起的。若每個線程中對全局變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全局變數是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。

1.2 線程同步(線程安全處理Synchronized)

java中提供了線程同步機制,它能夠解決上述的線程安全問題。

線程同步的方式有兩種:

  方式1:同步代碼塊

  方式2:同步方法

1.2.1 同步代碼塊

同步代碼塊: 在代碼塊聲明上 加上synchronized

synchronized (鎖對象) {
    可能會產生線程安全問題的代碼
}

同步代碼塊中的鎖對象可以是任意的對象;但多個線程時,要使用同一個鎖對象才能夠保證線程安全。

使用同步代碼塊,對電影院賣票案例中Ticket類進行如下代碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步代碼塊
            synchronized (lock){
                if (ticket > 0) {
                    //模擬電影選坐的操作
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            }
        }
    }
}

當使用了同步代碼塊後,上述的線程的安全問題,解決了。

1.2.2 同步方法

同步方法:在方法聲明上加上synchronized

public synchronized void method(){
       可能會產生線程安全問題的代碼
}

同步方法中的鎖對象是 this

使用同步方法,對電影院賣票案例中Ticket類進行如下代碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    //定義鎖對象
    Object lock = new Object();
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //同步方法
            method();
        }
    }

//同步方法,鎖對象this
    public synchronized void method(){
        if (ticket > 0) {
            //模擬選坐的操作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
        }
    }
}

靜態同步方法: 在方法聲明上加上static synchronized

public static synchronized void method(){
  可能會產生線程安全問題的代碼
}

靜態同步方法中的鎖對象是 類名.class

1.3 死鎖

同步鎖使用的弊端:當線程任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程式出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A鎖){
    synchronized(B鎖){
         
  }
}

我們進行下死鎖情況的代碼演示:

定義鎖對象類

public class MyLock {
    public static final Object lockA = new Object();
    public static final Object lockB = new Object();
}

線程任務類

public class ThreadTask implements Runnable {
    int x = new Random().nextInt(1);//0,1
    //指定線程要執行的任務代碼
    @Override
    public void run() {
        while(true){
            if (x%2 ==0) {
                //情況一
                synchronized (MyLock.lockA) {
                    System.out.println("if-LockA");
                    synchronized (MyLock.lockB) {
                        System.out.println("if-LockB");
                        System.out.println("if大口吃肉");
                    }
                }
            } else {
                //情況二
                synchronized (MyLock.lockB) {
                    System.out.println("else-LockB");
                    synchronized (MyLock.lockA) {
                        System.out.println("else-LockA");
                        System.out.println("else大口吃肉");
                    }
                }
            }
            x++;
        }
    }
}

測試類

public class ThreadDemo {
    public static void main(String[] args) {
        //創建線程任務類對象
        ThreadTask task = new ThreadTask();
        //創建兩個線程
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        //啟動線程
        t1.start();
        t2.start();
    }
}

1.4 Lock介面

查閱API,查閱Lock介面描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。

Lock介面中的常用方法

Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。

我們使用Lock介面,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下代碼修改:

public class Ticket implements Runnable {
    //共100票
    int ticket = 100;
    
    //創建Lock鎖對象
    Lock ck = new ReentrantLock();
    
    @Override
    public void run() {
        //模擬賣票
        while(true){
            //synchronized (lock){
            ck.lock();
                if (ticket > 0) {
                    //模擬選坐的操作
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);
                }
            ck.unlock();
            //}
        }
    }
}

1.5 等待喚醒機制

在開始講解等待喚醒機制之前,有必要搞清一個概念——線程之間的通信:多個線程在處理同一個資源,但是處理的動作(線程的任務)卻不相同。通過一定的手段使各個線程能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制所涉及到的方法:

wait() :等待,將正在執行的線程釋放其執行資格 和 執行權,並存儲到線程池中。

notify():喚醒,喚醒線程池中被wait()的線程,一次喚醒一個,而且是任意的。

notifyAll(): 喚醒全部:可以將線程池中的所有wait() 線程都喚醒。

其實,所謂喚醒的意思就是讓 線程池中的線程具備執行資格。必須註意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的線程。

仔細查看JavaAPI之後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable介面中,卻被定義在了Object類中,為什麼這些操作線程的方法定義在Object類中?

因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。

接下里,我們先從一個簡單的示例入手:

 

 

如上圖說示,輸入線程向Resource中輸入name ,sex , 輸出線程從資源中輸出,先要完成的任務是:

1.當input發現Resource中沒有數據時,開始輸入,輸入完成後,叫output來輸出。如果發現有數據,就wait();

2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然後,叫醒input來輸入數據。

 

下麵代碼,模擬等待喚醒機制的實現:

模擬資源類

 

public class Resource {
    private String name;
    private String sex;
    private boolean flag = false;

    public synchronized void set(String name, String sex) {
        if (flag)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // 設置成員變數
        this.name = name;
        this.sex = sex;
        // 設置之後,Resource中有值,將標記該為 true ,
        flag = true;
        // 喚醒output
        this.notify();
    }

    public synchronized void out() {
        if (!flag)
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // 輸出線程將數據輸出
        System.out.println("姓名: " + name + ",性別: " + sex);
        // 改變標記,以便輸入線程輸入數據
        flag = false;
        // 喚醒input,進行數據輸入
        this.notify();
    }
}

輸入線程任務類

public class Input implements Runnable {
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        int count = 0;
        while (true) {
            if (count == 0) {
                r.set("小明", "男生");
            } else {
                r.set("小花", "女生");
            }
            // 在兩個數據之間進行切換
            count = (count + 1) % 2;
        }
    }
}

輸出線程任務類

public class Output implements Runnable {
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.out();
        }
    }
}

測試類

public class ResourceDemo {
    public static void main(String[] args) {
        // 資源對象
        Resource r = new Resource();
        // 任務對象
        Input in = new Input(r);
        Output out = new Output(r);
        // 線程對象
        Thread t1 = new Thread(in);
        Thread t2 = new Thread(out);
        // 開啟線程
        t1.start();
        t2.start();
    }
}

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

-Advertisement-
Play Games
更多相關文章
  • 錯誤截圖 分析原因 首先,給定的異常提示信息並不精準,有多個錯誤原因都會拋出該異常。mybatis出現這個問題,通常是由Mapper interface和對應的xml文件的定義對應不上引起的,這時就需要仔細檢查對比包名、xml中的namespace、介面中的方法名稱等是否對應。我之前就因為稱忘記在x ...
  • 一、生成器簡介在python中,生成器是根據某種演算法邊迴圈邊計算的一種機制。主要就是用於操作大量數據的時候,一般我們會將操作的數據讀入記憶體中處理,可以電腦的記憶體是比較寶貴的資源,我認為的當要處理的數據超過記憶體四分之一的大小時就應該使用生成器。 二、生成器有什麼特點?1.和傳統的容器相比,生成器更節 ...
  • 在開始本篇文章之前,我想你對SpringCloud和SpringBoot的基本使用已經比較熟悉了,如果不熟悉的話可以參考我之前寫過的文章 本篇文章的源碼基於SpringBoot2.0,SpringCloud的Finchley.RELEASE 註解 我們知道,在使用Eureka作為註冊中心的時候,我們 ...
  • 位運算在redis中非常的方便使用,並且理由利用這個可以實現很多特殊的功能。這也迫使我去研究更多的redis提供的函數,只有研究的多,思路才能夠更加開放。今天我就對strings下麵的幾個函數進行了測試,也收穫頗豐。 使用setBit和bitCount可以實現用戶活躍天數的統計,大體的思路如下:我們 ...
  • #include<stdio.h>int main(){ double i; double bonus1,bonus2,bonus4,bonus6,bonus10,bonus; printf("你的利潤是:\n"); scanf("%lf",&i); bonus1=100000*0.1; bonus ...
  • Thymeleaf中有許多內置對象,可以在模板中實現各種功能。 下麵有幾個基本對象。 Web對象常用有:request、session、servletContext。 Thymeleaf提供了幾個內置變數param、session、application,分別可以訪問請求參數、session屬... ...
  • 手工操作 —— 穿孔卡片 1946年第一臺電腦誕生--20世紀50年代中期,電腦工作還在採用手工操作方式。此時還沒有操作系統的概念。 程式員將對應於程式和數據的已穿孔的紙帶(或卡片)裝入輸入機,然後啟動輸入機把程式和數據輸入電腦記憶體,接著通過控制台開關啟動程式針對數據運行;計算完畢,印表機輸出 ...
  • 一、基本介紹 logging 模塊是python自帶的一個包,因此在使用的時候,不必安裝,只需要import即可。 logging有 5 個不同層次的日誌級別,可以將給定的 logger 配置為這些級別: DEBUG:詳細信息,用於診斷問題。Value=10。 INFO:確認代碼運行正常。Value ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...