91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java多線程之線程安全問題怎么解決

發布時間:2022-04-22 10:15:32 來源:億速云 閱讀:182 作者:iii 欄目:編程語言

本篇內容主要講解“Java多線程之線程安全問題怎么解決”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Java多線程之線程安全問題怎么解決”吧!

Java多線程之線程安全問題怎么解決


1.線程安全概述

1.1什么是線程安全問題

首先我們需要明白操作系統中線程的調度是搶占式執行的,或者說是隨機的,這就造成線程調度執行時線程的執行順序是不確定的,有一些代碼執行順序不同不影響程序運行的結果,但也有一些代碼執行順序發生改變了重寫的運行結果會受影響,這就造成程序會出現bug,對于多線程并發時會使程序出現bug的代碼稱作線程不安全的代碼,這就是線程安全問題。

下面,將介紹一種典型的線程安全問題實例,整數自增問題。

1.2一個存在線程安全問題的程序

有一天,老師布置了這樣一個問題:使用兩個線程將變量count自增10萬次,每個線程承擔5萬次的自增任務,變量count的初始值為0
這個問題很簡單,最終的結果我們也能夠口算出來,答案就是10萬。
小明同學做事非常迅速,很快就寫出了下面的一段代碼:

class Counter {
    private int count;
    public void increase() {
        ++this.count;
    }
    public int getCount() {
        return this.count;
    }}public class Main11 {
    private static final int CNT = 50000;
    private static final Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < CNT; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int j = 0; j < CNT; j++) {
                counter.increase();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());
    }}

按理來說,結果應該是10萬,我們來看看運行結果:
Java多線程之線程安全問題怎么解決
運行的結果比10萬要小,你可以試著運行該程序你會發現每次運行的結果都不一樣,但絕大部分情況,結果都會比預期的值要小,下面我們就來分析分析為什么會這樣。

2.線程加鎖與線程不安全的原因

2.1案例分析

上面我們使用多線程運行了一個程序,將一個變量值為0的變量自增10萬次,但是最終實際結果比我們預期結果要小,原因就是線程調度的順序是隨機的,造成線程間自增的指令集交叉,導致運行時出現兩次自增但值只自增一次的情況,所以得到的結果會偏小。

我們知道一次自增操作可以包含以下幾條指令:

  1. 將內存中變量的值加載到寄存器,不妨將該操作記為load

  2. 在寄存器中執行自增操作,不妨將該操作記為add

  3. 將寄存器的值保存至內存中,不妨將該操作記為save

我們來畫一條時間軸,來總結一下常見的幾種情況:

情況1: 線程間指令集,無交叉,運行結果與預期相同,圖中寄存器A表示線程1所用的寄存器,寄存器B表示線程2所用的寄存器,后續情況同理。
Java多線程之線程安全問題怎么解決
情況2: 線程間指令集存在交叉,運行結果低于預期結果。
Java多線程之線程安全問題怎么解決
情況3: 線程間指令集完全交叉,實際結果低于預期。
Java多線程之線程安全問題怎么解決
根據上面我們所列舉的情況,發現線程運行時沒有交叉指令的時候運行結果是正常的,但是一旦有了交叉會導致自增操作的結果會少1,綜上可以得到一個結論,那就是由于自增操作不是原子性的,多個線程并發執行時很可能會導致執行的指令交叉,導致線程安全問題。

那如何解決上述線程不安全的問題呢?當然有,那就是對對象加鎖。

2.2線程加鎖

2.2.1什么是加鎖

為了解決由于“搶占式執行”所導致的線程安全問題,我們可以對操作的對象進行加鎖,當一個線程拿到該對象的鎖后,會將該對象鎖起來,其他線程如果需要執行該對象的任務時,需要等待該線程運行完該對象的任務后才能執行。

舉個例子,假設要你去銀行的ATM機存錢或者取款,每臺ATM機一般都在一間單獨的小房子里面,這個小房子有一扇門一把鎖,你進去使用ATM機時,門會自動的鎖上,這個時候如果有人要來取款,那它得等你使用完并出來它才能進去使用ATM,那么這里的“你”相當于線程,ATM相當于一個對象,小房子相當于一把鎖,其他的人相當于其他的線程。
Java多線程之線程安全問題怎么解決
Java多線程之線程安全問題怎么解決
在java中最常用的加鎖操作就是使用synchronized關鍵字進行加鎖。

2.2.2如何加鎖

synchronized 會起到互斥效果, 某個線程執行到某個對象的 synchronized 中時, 其他線程如果也執行到同一個對象 synchronized 就會阻塞等待。
線程進入 synchronized 修飾的代碼塊, 相當于加鎖,退出 synchronized 修飾的代碼塊, 相當于 解鎖

java中的加鎖操作可以使用synchronized關鍵字來實現,它的常見使用方式如下:

方式1: 使用synchronized關鍵字修飾普通方法,這樣會使方法所在的對象加上一把鎖。
例如,就以上面自增的程序為例,嘗試使用synchronized關鍵字進行加鎖,如下我對increase方法進行了加鎖,實際上是對某個對象加鎖,此鎖的對象就是this,本質上加鎖操作就是修改this對象頭的標記位。

class Counter {
    private int count;
    synchronized public void increase() {
        ++this.count;
    }
    public int getCount() {
        return this.count;
    }}

多線程自增的main方法如下,后面會以相同的栗子介紹synchronized的其他用法,后面就不在列出這段代碼了。

public class Main11 {
    private static final int CNT = 50000;
    private static final Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < CNT; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int j = 0; j < CNT; j++) {
                counter.increase();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());
    }}

看看運行結果:
Java多線程之線程安全問題怎么解決
方式2: 使用synchronized關鍵字對代碼段進行加鎖,但是需要顯式指定加鎖的對象。
例如:

class Counter {
    private int count;
    public void increase() {
        synchronized (this){
            ++this.count;
        }
    }
    public int getCount() {
        return this.count;
    }}

運行結果:
Java多線程之線程安全問題怎么解決
方式3: 使用synchronized關鍵字修飾靜態方法,相當于對當前類的類對象進行加鎖。

class Counter {
    private static int count;
    synchronized public static void increase() {
        ++count;
    }
    public int getCount() {
        return this.count;
    }}

運行結果:
Java多線程之線程安全問題怎么解決
常見的用法差不多就是這些,對于線程加鎖(線程拿鎖),如果兩個線程同時拿一個對象的鎖,就會產生鎖競爭,兩個線程同時拿兩個不同對象的鎖不會產生鎖競爭。
對于synchronized這個關鍵字,它的英文意思是同步,但是同步在計算機中是存在多種意思的,比如在多線程中,這里同步的意思是“互斥”;而在IO或網絡編程中同步指的是“異步”,與多線程沒有半點的關系。

synchronized 的工作過程:

  1. 獲得互斥鎖lock

  2. 從主內存拷貝變量的最新副本到工作的內存

  3. 執行代碼

  4. 將更改后的共享變量的值刷新到主內存

  5. 釋放互斥鎖unlock

synchronized 同步塊對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題,即死鎖問題,關于死鎖后續文章再做介紹。

綜上,synchronized關鍵字加鎖有如下性質:互斥性,刷新內存性,可重入性。

synchronized關鍵字也相當于一把監視器鎖monitor lock,如果不加鎖,直接使用wait方法(一種線程等待的方法,后面細說),會拋出非法監視器異常,引發這個異常的原因就是沒有加鎖。

2.2.3再析案例

對自增那個代碼上鎖后,我們再來分析一下為什么加上了所就線程安全了,先列代碼:

class Counter {
    private int count;
    synchronized public void increase() {
        ++this.count;
    }
    public int getCount() {
        return this.count;
    }}public class Main11 {
    private static final int CNT = 50000;
    private static final Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < CNT; i++) {
                counter.increase();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int j = 0; j < CNT; j++) {
                counter.increase();
            }
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.getCount());
    }}

多線程并發執行時,上一次就分析過沒有指令集交叉就不會出現問題,因此這里我們只討論指令交叉后,加鎖操作是如何保證線程安全的,不妨記加鎖為lock,解鎖為unlock,兩個線程運行過程如下:
線程1首先拿到目標對象的鎖,對對象進行加鎖,處于lock狀態,當線程2來執行自增操作時會發生阻塞,直到線程1的自增操作完畢,處于unlock狀態,線程2才會就緒取執行線程2的自增操作。
Java多線程之線程安全問題怎么解決
加鎖后線程就是串行執行,與單線程其實沒有很大的區別,那多線程是不是沒有用了呢?但是對方法加鎖后,線程運行該方法才會加鎖,運行完該方法就會自動解鎖,況且大部分操作并發執行是不會造成線程安全的,只有少部分的修改操作才會有可能導致線程安全問題,因此整體上多線程運行效率還是比單線程高得多。

2.3線程不安全的原因

首先,線程不安全根源是線程間的調度充滿隨機性,導致原有的邏輯被改變,造成線程不安全,這個問題無法解決,無可奈何。

多個線程針對同一資源進行寫(修改)操作,并且針對資源的修改操作不是原子性的,可能會導致線程不安全問題,類似于數據庫的事務。

由于編譯器的優化,內存可見性無法保證,就是當線程頻繁地對同一個變量進行讀操作時,會直接從寄存器上讀值,不會從內存上讀值,這樣內存的值修改時,線程就感知不到該變量已經修改,會導致線程安全問題(這是編譯器優化的結果,現代的編譯器都有類似的優化不止于Java),因為相比于寄存器,從內容中讀取數據的效率要小的多,所以編譯器會盡可能地在邏輯不變的情況下對代碼進行優化,單線程情況下是不會翻車的,但是多線程就不一定了,比如下面一段代碼:

import java.util.Scanner;public class Main12 {
    private static int isQuit;
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (isQuit == 0) {

            }
            System.out.println("線程thread執行完畢!");
        });
        thread.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("請輸入isQuit的值,不為0線程thread停止執行!");
        isQuit = sc.nextInt();
        System.out.println("main線程執行完畢!");
    }}

運行結果:
Java多線程之線程安全問題怎么解決
我們從運行結果可以知道,輸入isQuit后,線程thread沒有停止,這就是編譯器優化導致線程感知不到內存可見性,從而導致線程不安全。
我們可以使用volatile關鍵字保證內存可見性。
我們可以使用volatile關鍵字修飾isQuit來保證內存可見性。

import java.util.Scanner;public class Main12 {
    volatile private static int isQuit;
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (isQuit == 0) {

            }
            System.out.println("線程thread執行完畢!");
        });
        thread.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("請輸入isQuit的值,不為0線程thread停止執行!");
        isQuit = sc.nextInt();
        System.out.println("main線程執行完畢!");
    }}

運行結果:
Java多線程之線程安全問題怎么解決

synchronized與volatile關鍵字的區別:
synchronized關鍵字能保證原子性,但是是否能夠保證內存可見性要看情況(上面這個栗子是不行的),而volatile關鍵字只能保證內存可見性不能保證原子性。
保證內存可見性就是禁止編譯器做出如上的優化而已。

import java.util.Scanner;public class Main12 {
    private static int isQuit;
    //鎖對象
    private static final Object lock = new Object();
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
                synchronized (lock) {
                    while (isQuit == 0) {

                    }
                    System.out.println("線程thread執行完畢!");
                }
        });
        thread.start();

        Scanner sc = new Scanner(System.in);
        System.out.println("請輸入isQuit的值,不為0線程thread停止執行!");
        isQuit = sc.nextInt();
        System.out.println("main線程執行完畢!");
    }}

運行結果:
Java多線程之線程安全問題怎么解決

編譯器優化除了導致內存可見性感知不到的問題,還有指令重排序也會導致線程安全問題,指令重排序也是編譯器優化之一,就是編譯器會智能地(保證原有邏輯不變的情況下)調整代碼執行順序,從而提高程序運行的效率,單線程沒問題,但是多線程可能會翻車,這個原因了解即可。

3.線程安全的標準類

Java 標準庫中很多都是線程不安全的。這些類可能會涉及到多線程修改共享數據, 又沒有任何加鎖措施。例如,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder。
但是還有一些是線程安全的,使用了一些鎖機制來控制,例如,Vector (不推薦使用),HashTable (不推薦使用),ConcurrentHashMap (推薦),StringBuffer。
還有的雖然沒有加鎖, 但是不涉及 “修改”, 仍然是線程安全的,例如String。

在線程安全問題中可能你還會遇到JMM模型,在這里補充一下,JMM其實就是把操作系統中的寄存器,緩存和內存重新封裝了一下,其中在JMM中寄存器和緩存稱為工作內存,內存稱為主內存。
其中緩存分為一級緩存L1,二級緩存L2和三級緩存L3,從L1到L3空間越來越大,最大也比內存空間小,最小也比寄存器空間大,訪問速度越來越慢,最慢也比內存的訪問速度快,最快也沒有寄存器訪問快。

4.Object類提供的線程等待方法

除了Thread類中的能夠實現線程等待的方法,如join,sleep,在Object類中也提供了相關線程等待的方法。

序號方法說明
1public final void wait() throws InterruptedException釋放鎖并使線程進入WAITING狀態
2public final native void wait(long timeout) throws InterruptedException;相比于方法1,多了一個最長等待時間
3public final void wait(long timeout, int nanos) throws InterruptedException相比于方法2,等待的最長時間精度更大
4public final native void notify();喚醒一個WAITING狀態的線程,并加鎖,搭配wait方法使用
5public final native void notifyAll();喚醒所有處于WAITING狀態的線程,并加鎖(很可能產生鎖競爭),搭配wait方法使用

上面介紹synchronized關鍵字的時候,如果不對線程加鎖會產生非法監視異常,我們來驗證一下:

public class TestDemo12 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("執行完畢!");
        });

        thread.start();
        System.out.println("wait前");
        thread.wait();
        System.out.println("wait后");
    }}

看看運行結果:
Java多線程之線程安全問題怎么解決
果然拋出了一個IllegalMonitorStateException,因為wait方法的執行步驟為:先釋放鎖,再使線程等待,你現在都沒有加鎖,那如何釋放鎖呢?所以會拋出這個異常,但是執行notify是無害的。

wait方法常常搭配notify方法搭配一起使用,前者能夠釋放鎖,使線程等待,后者能獲取鎖,使線程繼續執行,這套組合拳的流程圖如下:
Java多線程之線程安全問題怎么解決

現在有兩個任務由兩個線程執行,假設線程2比線程1先執行,請寫出一個多線程程序使任務1在任務2前面完成,其中線程1執行任務1,線程2執行任務2。
這個需求可以使用wait/notify來實現。

class Task{
    public void task(int i) {
        System.out.println("任務" + i + "完成!");
    }}public class WiteNotify {
    //鎖對象
    private static final Object lock = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            synchronized (lock) {
                Task task1 = new Task();
                task1.task(1);
                //通知線程2線程1的任務完成
                System.out.println("notify前");
                lock.notify();
                System.out.println("notify后");
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (lock) {
                Task task2 = new Task();
                //等待線程1的任務1執行完畢
                System.out.println("wait前");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                task2.task(2);
                System.out.println("wait后");
            }
        });
        thread2.start();
        Thread.sleep(10);
        thread1.start();
    }}

運行結果:
Java多線程之線程安全問題怎么解決

到此,相信大家對“Java多線程之線程安全問題怎么解決”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

遂昌县| 安泽县| 屏南县| 泗水县| 长丰县| 临安市| 尼木县| 平原县| 察隅县| 南平市| 兰州市| 贵南县| 长子县| 日照市| 潮安县| 永城市| 额敏县| 河曲县| 平顺县| 耿马| 白沙| 康乐县| 冕宁县| 盐亭县| 息烽县| 濮阳市| 罗源县| 环江| 淄博市| 靖江市| 阿图什市| 清远市| 仪征市| 麟游县| 丽水市| 西吉县| 芜湖县| 江安县| 碌曲县| 六盘水市| 利辛县|