第8章 Java多線程與併發

在這裏插入圖片描述進程是操作系統層面的任務併發,線程是更細粒度的任務控制,是進程中的子任務的併發。

在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述
前2個幾乎沒什麼用,所以主要寫第三個的兩種方式

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        String value = "test";
        System.out.println("go");
        Thread.currentThread().sleep(5000);
        System.out.println(" over ");
        return value;
    }

}


public class FutureTaskDemo {

    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask<String>(new MyCallable());
        new Thread(futureTask).start();
        if (!futureTask.isDone()){
            System.out.println("還未完成");
        }
        try {
            System.out.println("futureTask.get() = " + futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

在這裏插入圖片描述
第二種

public class ThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();

        Future<String> future = executorService.submit(new MyCallable());
        if (!future.isDone()){
            System.out.println("還未完成");
        }
        try {
            System.out.println("futureTask.get() = " + future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            // 一定要關閉
            executorService.shutdown();
        }
    }
}

在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述
下邊是我自己的整理


線程得5種狀態

新生狀態,就緒狀態,運行狀態,阻塞狀態,死亡狀態。
球員身份,賽場就緒,搶到足球,被拌倒了(需要重新就緒),罰下場

新生狀態—new的時候
就有了自己的工作空間,跟主存空間進行交互。
調用start進入就緒狀態。

線程進入就緒狀態得四種情況
1:start
2:解除阻塞
3:yield 高風亮節,調度到他又讓出。完事直接進入就緒狀態,不進入阻塞狀態。不釋放鎖。
4:CPU得線程切換。

線程進行阻塞狀態的四種原因 13稱爲等待,24稱爲阻塞,
1:sleep,抱着資源睡覺
2:wait,遇到紅燈,站到一邊,不佔用資源
3:join,加入,插隊等候
4:read,write,IO等阻塞。

死亡狀態
1:正常執行完畢
2:強行終止

線程停止
在這裏插入圖片描述

/**
 * @author jx
 * @create 2018-11-09-19:15
 * 中止線程得兩種方式
 * 1:線程正常執行完畢
 * 2:Stop/destory   不要使用
 * 3:加入標識
 */
public class StopThread implements Runnable {

    //加入標識 標記線程是否可以正常運行
    // //volatile可以保證是從內存中獲取,而不是在寄存器中,這樣多線程的時候,有變化可以第一時間知道
    private volatile boolean flag = true;
    private String name;

    public StopThread(String name) {
        this.name = name;
    }

    //對外提供改變flag的方法
    public void change() {
        this.flag = false;
    }


    @Override
    public void run() {
        //關聯標識true  正常執行  false線程停止
        int i = 0;
        while (flag) {
            System.out.println(name + "-->" + i);
        }
    }


    public static void main(String[] args) {

        StopThread st = new StopThread("LALAAL");
        new Thread(st).start();

        for (int i = 0; i < 99; i++) {
            if (i == 88) {
                st.change();
                System.out.println("線程中止了!!!!!!!!!!!!");
            }
            System.out.println("main--->" + i);
        }
    }
}

線程休眠
在這裏插入圖片描述

/**
 * @author jx
 * @create 2018-10-21-23:44
 * 模擬龜兔賽跑
 */
public class Racer implements Runnable {

    private static String winner;//勝利者

    @Override
    public void run() {
        for (int steps = 1; steps <= 100; steps++) {

            if (Thread.currentThread().getName().equals("小兔子") && steps % 10 == 0) {
                try {
                // 線程休眠
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "---" + steps);

            //比賽是否結束
            boolean flag = gameOver(steps);
            if (flag) {
                break;
            }
        }


    }

    private boolean gameOver(int steps) {

        if (winner != null) {//存在勝利者
            return true;
        } else {
            if (steps == 100) {
                winner = Thread.currentThread().getName();
                System.out.println("winner = " + winner);
                return true;
            }
        }
        return false;
    }


    public static void main(String[] args) {
        Racer racer = new Racer();
        new Thread(racer, "小烏龜").start();
        new Thread(racer, "小兔子").start();
    }
}

禮讓線程

在這裏插入圖片描述
禮讓成功
在這裏插入圖片描述
寫在哪個線程體中,哪個線程就會進行禮讓。
在這裏插入圖片描述Join插隊線程

在這裏插入圖片描述成功插隊之後,必須插隊的線程執行完,才能繼續執行其他的線程

在這裏插入圖片描述線程的狀態
在這裏插入圖片描述線程中的其他方法

在這裏插入圖片描述


線程安全

難點:保證安全 還要保證性能。要鎖的準。

併發:同一資源 多個線程 同時操作。
在改的情況下,要考慮線程安全,光讀沒有關係。

解決:一是隊列,只能由一個線程進行操作,那怎麼才能知道這個線程有沒有操作完呢?
加一個排他鎖,比如比如sleep就有排他性。

兩個條件,一個隊列 一個鎖,我們稱之爲線程同步

多個線程訪問同一資源,並且某些線程還想修改這個對象,這個時候我們就要線程同步了,
其實就是一種等待機制,多個線程進入這個對象的等待池中形成隊列,等待前邊一個線程使用完畢後,下一個線程再進行使用。

在這裏插入圖片描述
Java中的鎖synchronized關鍵字 ,排他鎖。會引起性能的問題,(優化之後影響不大)
原因:
1:其他線程只能等待
2:加速釋放鎖,上下文切換(在操作系統中,CPU切換到另一個進程需要保存當前進程的狀態並恢復另一個進程的狀態:當前運行任務轉爲就緒(或者掛起、刪除)狀態,另一個被選定的就緒任務成爲當前任務。上下文切換包括保存當前任務的運行環境,恢復將要運行任務的運行環境。)和調度延時。
3:如果一個優先級高的線程在等待優先級低的線程,會導致優先級倒置,引起性能問題。

Synchronized 鎖的是對象,資源,
同步方法,同步塊。

Synchronized 鎖的是對象,資源,
同步方法,同步塊。
Synchronized 鎖的是對象,資源,
同步方法,同步塊。

線程同步

由於我們可以通過private關鍵字來保證數據對象只能被方法訪問,所以我們只需要針對方法提供一套機制就能保證線程安全,這套機制就是synchronized關鍵字,它包括兩種用法,synchronized方法和synchronized塊。

在這裏插入圖片描述synchronized方法 控制對“成員變量”和“類變量”的訪問,每個對象都有對應的一把鎖,每個synchronized方法都必須獲得 調用該方法的對象 的 鎖 才能執行,否則線程阻塞,方法一旦執行就獨佔該鎖,直到方法結束返回時纔將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。

若是將一個方法整個聲明爲synchronized,將大打印效果效率。


臨界值

數據出現負數
在這裏插入圖片描述
B在這裏操作最後一份資源,其他線程進來了一看,還有一份,先在這裏等,等B真的操作完了,並且把資源數量改了,其他線程只能去修改爲0的資源的數量,造成負數。

出現相同值
在這裏插入圖片描述線程把資源副本拷貝到自己的工作空間,正在操作或者返回的路上,其他線程來把資源拷貝到他自己的工作空間了,這時再修改,就會出現相同值得情況。


Synchronized鎖成員方法,鎖的只是this,是這個對象,方法裏必須都是本對象的資源,我的鎖才能鎖的住。
靜態方法:類.class
同步塊:鎖要操作的對象

注意:要鎖不變的對象,比如你住房間,你鎖房門,怎麼改變裏邊東西都沒事,但是你鎖了傢俱,導致別人還是能到你房間裏來,看見這個東西還是會出現線程安全問題。


同步塊的效率優化:雙重檢測,1資源是否存在,2臨界資源。

在這裏插入圖片描述同步塊:synchronized(obj){}
1 obj稱爲同步監視器
2 obj可以是任何對象,但是推薦使用共享資源作爲同步監視器(分佈式要考慮分佈式鎖)
3 同步方法中無須指定同步監視器,因爲同步方法的同步監視器是this該對象本身,或者類的模子。

同步監視器的執行過程
1:第一個線程訪問,鎖定同步監視器執行代碼。
2:第二個線程訪問,發現同步監視器被鎖定,無法訪問。
3:第一個線程訪問完畢,解鎖同步監視器。
4:第二個線程訪問,發現同步監視器未鎖,鎖定並訪問


Jdk1.7和 1.8得區別

線程安全操作併發容器
concurrent 併發包,由synchronized換成reentrantLock
CopyOnWriteArrayList 在寫的基礎上進行拷貝,保證寫的時候是一個正確的拷貝
1.7
在這裏插入圖片描述1.8
在這裏插入圖片描述

生產者服務者

生產者服務者的模式就是實現應用層和服務層的解耦。不用一直盯着消費者,你有調用我幫你完成,你沒有調用我就歇着。大部分是使用消息隊列來實現的。(也有阻塞的,沒有好與不好)

應用層–
服務層–
數據層–

線程通信
在這裏插入圖片描述還需要通信。
在這裏插入圖片描述
兩種實現:
1:隊列,也叫緩衝區。管程法,利用管道和容器。
在這裏插入圖片描述2:信號燈法,標識法。
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述


Happen before 指令重排

在這裏插入圖片描述計算機得運算過程
在這裏插入圖片描述1:從內存中獲得要執行得下一條指令
2:對指令進行解碼翻譯。從寄存器中取值
3:拷貝到主存中,進行一系列得運算操作
4:將結果寫回到寄存器中。
在這裏插入圖片描述注意,計算機爲了效率進行指令重排,但是在多線程中,重排後得結果可能不正確,造成線程不安全。單線程肯定是等結果,所以沒有影響。

執行代碼的順序可能與編寫代碼的順序不一致,既虛擬機優化代碼順序則爲指令重排
Happen before編譯器或者運行時環境爲了優化程序性能而採取的對指令進行重排的一種手段。
在虛擬機層面,爲了儘可能地減少內存操作速度遠慢於CPU運行速度所帶來的CPU空置影響,虛擬機將會按照自己的規則編寫的程序規則打亂,寫在後邊的代碼有可能先執行,寫在前邊的後執行,以儘可能充分的利用CPU。
比如有兩個操作,第一個運行很慢,那就很有可能會會先執行第二個操作,提前使用CPU來加快整體效率。不管誰先開始,總之後面的代碼在一些情況下存在先結束的可能。
在硬件層面上,CPU會將接收到一批指令按照其規則進行重排序,同樣是基於CPU速度比緩存快的原因,和上一點目的類似,只是 硬件處理的話每次只能在接收到的有限的指令範圍內進行重排序,而虛擬機可以在更大的層面,更多的指令範圍內進行重排序。

數據依賴

在這裏插入圖片描述指令重排代碼
在這裏插入圖片描述甚至
在這裏插入圖片描述

按照代碼預期
System.out.println(“HappenBefore.main -->” + a );
不可能執行,但是存在指令重排。不光執行了,而且結果還爲1.
因爲指令重排得時候a的值還沒回來。


Volatile

Volatile —輕量級的synchronized
Synchronized:保證併發和同步
Volatile只保證了同步的數據的可見,實現了部分功能

保證線程之間的變量的可見性,當線程a對變量x進行修改之後,在線程a後邊執行的其他線程都能看到變量x的變化。就是說要符合以下兩個規則
1:線程對變量進行修改之後,要立刻寫回到主內存中。
2:線程對變量讀取的時候,要從主內存中讀,而不是緩存中。

主內存訪問過高肯定影響效率,所以工作內存有必要存在。

在這裏插入圖片描述
各線程的工作內存間彼此獨立,互不可見,在線程啓動的時候,虛擬機爲每一塊內存分配一塊內存空間,不僅包含了內部定義的局部變量,也包含了線程需要的共享變量(非線程內構造對象)的副本,即爲了提高執行效率。

Volatile能保持變量的可見性,但是不能保持原子性。(要麼都執行,要麼都不執行)
比如上一個例子中
在這裏插入圖片描述這四步就是要保證原子性操作。但是volatile不能保證。
不過可以使用CAS來控制。

不要將volatile用在getAndOperate場合(這種場合不原子,需要再加鎖),僅僅set或者get的場景是適合volatile的


內存屏障

內存屏障(memory barrier)是一個CPU指令。

基本上,它是這樣一條指令:
a) 確保一些特定操作執行的順序;
b) 影響一些數據的可見性(可能是某些指令執行後的結果)。

編譯器和CPU可以在保證輸出結果一樣的情況下對指令重排序,使性能得到優化。
插入一個內存屏障,相當於告訴CPU和編譯器先於這個命令的必須先執行,後於這個命令的必須後執行。

內存屏障另一個作用是強制更新一次不同CPU的緩存。

例如,一個寫屏障會把這個屏障前寫入的數據刷新到緩存,這樣任何試圖讀取該數據的線程將得到最新值,而不用考慮到底是被哪個cpu核心或者哪顆CPU執行的。

內存屏障(memory barrier)和volatile什麼關係?

上面的虛擬機指令裏面有提到,如果你的字段是volatile,Java內存模型將在寫操作後插入一個寫屏障指令,在讀操作前插入一個讀屏障指令。這意味着如果你對一個volatile字段進行寫操作,

你必須知道:
1、一旦你完成寫入,任何訪問這個字段的線程將會得到最新的值。
2、在你寫入前,會保證所有之前發生的事已經發生,並且任何更新過的數據值也是可見的,因爲內存屏障會把之前的寫入值都刷新到緩存。

在這裏插入圖片描述
回到前面的JVM指令:從Load到store到內存屏障,一共4步,其中最後一步jvm讓這個最新的變量的值在所有線程可見,也就是最後一步讓所有的CPU內核都獲得了最新的值,但中間的幾步(從Load到Store)是不安全的,中間如果其他的CPU修改了值將會丟失。

CAS是基於樂觀鎖的,也就是說當寫入的時候,如果寄存器舊值已經不等於現值,說明有其他CPU在修改,那就繼續嘗試。所以這就保證了操作的原子性。(舊值,新值,預期值)

在這裏插入圖片描述並不能保證num=1是原子操作,但是可以保禁止指令重排


DCL單例模式

/**
 * @author jx
 * @create 2018-11-12-16:21
 * <p>
 * 單例模式 多線程情況下,對外存在一個對象  懶漢式基礎上加入併發控制
 * 1:構造器私有化-->避免外部new構造器
 * 2:內部提供一個私有的靜態屬性-->爲了讓外部可以new對象,存儲對象的地址
 * 3:提供共有的靜態方法-->獲取屬性
 */
public class DoubleCheckedLocking {

    //這裏如果一上來就new一個,就是餓漢式,如果沒有new就是懶漢式
    private static volatile DoubleCheckedLocking instance;

    private DoubleCheckedLocking() {
    }

    public static DoubleCheckedLocking getInstance(long time) {
        //dcl權限控制  雙重檢測
        if (null != instance) {
            return instance;
        }
        synchronized (DoubleCheckedLocking.class) {
            if (null == instance) {
                try {
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //指令重排有可能發生在此處
                instance = new DoubleCheckedLocking();
                //new一個對象的時候,可能發生3件事情
                // 1:開闢空間
                // 2:初始化對象信息
                // 3:返回對象的地址給引用
                //在2.3之間如果發生指令重排
                //沒有volatile 其他線程可能會訪問一個沒有初始化的對象

            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println(DoubleCheckedLocking.getInstance(500));
        });
        t.start();
        System.out.println(DoubleCheckedLocking.getInstance(1000));
    }
}



Thread Local

表示的是每個線程自身的存儲區域,也就是每個線程自己的一畝三分地。舉個例子,一個銀行,每個用戶是一條線程,每個線程都有自己的Thread Local,每個用戶都有自己的保險櫃一樣,類似於map的K-V,K是線程的信息,V就是對應的存儲內容。好處就是每個線程相互獨立,又可以共享這個內存區域。也可以保證在多線程的情況下,每個線程存儲的數據的安全。
每個數據只有自己能看到,別的線程是看不到的。
這個大的Thread Local 官方建議用private static 來修飾。來實現線程安全,get/set/initialValue三個方法。。
一般用在跟自己線程相關的東西,比如每個線程有自己數據庫的鏈接,操作自己的,大家不影響。或者每一個線程有自己的用戶登錄信息。
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述


可重入鎖

作爲併發共享數據保證一致性的工具。大多數內置鎖都是可重入的,也就是說,如果一個線程試圖獲取一個已經由它自己持有的鎖時,那麼這個請求會立刻成功,並且會將這個鎖的計數值+1,當線程退出同步代碼塊的時候,計數器將會遞減,當計數器的值等於0的時候,鎖釋放。在沒有可重入鎖的支持,第二次企圖獲得鎖時會進入死鎖狀態。
就比如你已經有了進入房子的鑰匙,那你就可以進入衛生間,進入廚房。

在這裏插入圖片描述

核心原理
改成可重入鎖,鎖可以延續使用
實現機制,就是看看這個進來的線程是不是鎖定的線程,如果是,就不用等
直接用 每個鎖都有一個計數器
在這裏插入圖片描述


CAS

鎖分爲兩類,悲觀鎖和樂觀鎖

悲觀鎖:synchronized是獨佔鎖,悲觀鎖,會導致其他需要鎖的線程被掛起,等待持有鎖的線程釋放鎖。
樂觀鎖:每次不加鎖,而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。

樂觀鎖實現:
比較並交換

有三個值,當前內存中的值V,舊的預期值A,將要更新的值B,先獲得內存中當前的內存值V,將V與A進行比較,要是相等就改爲B,並返回true,否則什麼都不做,返回false。
CAS是一組原子操作,不會被外部打斷。
屬於硬件級別的操作(利用CPU的CAS指令,同時藉助JNI來完成非阻塞算法)比加鎖效率高。

ABA問題:如果變量V初次讀取是A,並且在準備賦值的時候還是A,那就說明他一定沒被其他線程修改過嗎?如果這期間他被改爲其他值,之後又被修改爲A,那CAS就會誤認爲他沒有被修改過。

在這裏插入圖片描述
解決辦法

看版本號是否相同,如果相同,纔會更新難到數據庫,解決ABA問題。
CAS的自旋。循環體中做了三件事
1:獲得當前值
2:當前值+1,計算目標值。
3:進行CAS操作,成功則跳出循環,失敗重複上述步驟。

如何保證獲得的當前值是內存中的最新值呢?很簡單,用volatile關鍵字來保證。

下文例子借鑑小灰中的例子 致敬劉欣大佬

假設有一個遵循CAS原理的提款機,小灰有100元存款,要用這個提款機來提款50元。

由於提款機硬件出了點小問題,小灰的提款操作被同時提交兩次,開啓了兩個線程,兩個線程都是獲取當前值100元,要更新成50元。

理想情況下,應該一個線程更新成功,另一個線程更新失敗,小灰的存款只被扣一次。

線程1首先執行成功,把餘額從100改成50。線程2因爲某種原因阻塞了。這時候,小灰的媽媽剛好給小灰匯款50元。

線程2仍然是阻塞狀態,線程3執行成功,把餘額從50改成100。

線程2恢復運行,由於阻塞之前已經獲得了“當前值”100,並且經過compare檢測,此時存款實際值也是100,所以成功把變量值100更新成了50。

我錢沒了50!!!!!!!!!

在這裏插入圖片描述
真正要做到嚴謹的CAS機制,我們在比較階段不僅要比較期望值A和地址V中的實際值,還要比較變量的版本號是否一致。

在這裏插入圖片描述Native JUC技術 在這裏插入圖片描述是硬件技術 效率很高。

在這裏插入圖片描述


多線程總結

其實就是多條路,有了多線程,程序和程序之間,代碼和代碼之間,就可以不用等待了,同時進行了。

多線程實現的方式

1:實現runnable 重寫run方法
必須借用靜態代理,來一個thread對象,放一個真實對象,放一個線程對象。
2:繼承thread類 重寫run方,調用start方法。
子類對象,調用start方法
3:實現callable接口重寫call方法

線程同步:即當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作, 其他線程才能對該內存地址進行操作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章