狂神說_JUC並發編程_1


0.學習方法
源碼+官方文檔:

其實官方文檔就是源碼編譯出來的,其本質還是看源碼,不過文檔會比較方便學習

  • 只有多看源碼,多研究文檔才會進步
  • Java英文文檔可以通過點擊查看源碼獲取
  • Java1.8中文文檔(中文 – 谷歌版)
    •       在線版: https://blog.fondme.cn/apidoc/jdk-1.8-google/
    •       下載鏈接: https://pan.baidu.com/s/10wTC1F_4EUPsHzrn-_sPTw 密碼:k7rm

1.什么是JUC
JUC其實就是Java.Util.concurrent包的縮寫

java.util.concurrent
java.util.concurrent.atomi
java.util.concurrentlocks


是 java.util 工具包、包、分類

  • 回顧開啟線程的三種方式:

Thread

Runnable

Callable

 

 

 

 

2.線程與進程
線程、進程,如果不能使用一句話說出來的技術,不扎實!

打開(Win10)任務管理器可以清楚看到執行的線程與進程:

 

 

 


參考博客:什么是線程?什么是進程

進程:

  • 官方定義:

  進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配的基本單位,是操作系統結構的基礎

  • 簡單理解:

  進行(運行)中的程序,如打開任務管理器后中各種.exe程序

線程:

  • 官方定義:

  線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以並發多個線程,每條線程並行執行不同的任務。

  • 簡單理解:

  線程是真正執行資源調度(使程序跑起來)的主體,一個進程往往可以包含多個線程,但至少包含一個線程。

  如:開一個idea進程,其中至少有—> 線程1:輸入代碼,線程2:自動保存

😶老是強調多線程,那么 Java真的可以開啟線程嗎?

答案是 : 不能。查看Thread.start()源碼可得知:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}
// 本地方法,底層的C++ ,Java 無法直接操作硬件
private native void start0();

並發編程

並發編程:並發、並行

  • 並發(多線程操作同一個資源)
    • CPU 一核 ,模擬出來多條線程,天下武功,唯快不破,快速交替
  • 並行(多個人一起行走)
    • CPU 多核 ,多個線程可以同時執行; 線程池
public class Test1 {
    public static void main(String[] args) {
        // 獲取cpu的核數
        // CPU 密集型,IO密集型
        System.out.println(Runtime.getRuntime().availableProcessors());
       //輸出為8
        //說明筆者電腦為八核處理器
    }
}

線程的幾個狀態:

從源碼回答,有理有據

public enum State {
    // 新生
    NEW,
    // 運行
    RUNNABLE,
    // 阻塞
    BLOCKED,
    // 等待,死死地等
    WAITING,
    // 超時等待
    TIMED_WAITING,
    // 終止
    TERMINATED;
}

wait與sleep的區別

看源碼說話嗷🎈

//Object.wait()
public final void wait() throws InterruptedException {
    wait(0);
}

//Thread.sleep()
public static native void sleep(long millis) throws InterruptedException;

來自不同的類

  • wait() 來自 Object類
  • sleep() 來自 Thread類

關於鎖的釋放

  • wait() 會釋放鎖:wait是進入線程等待池等待,出讓系統資源,其他線程可以占用CPU。
  • sleep() 不出讓系統資源;(簡單來說,就是抱着🔒睡覺)

是否需要捕獲異常?

  需要。源碼都在上面寫的死死的,throws InterruptedException 😓都不知網上隨手一搜的博客說wait() 不用捕獲異常怎么搞得。

使用范圍:

  wait() 需要在同步代碼塊中使用

// wait、notify/notifyAll必須在同步控制塊、同步方法里面使用。而sleep的使用在任意地方。
synchronized(x){
    x.notify()
   //或者wait()
}
    1. sleep()可以在任何地方睡

  1. 作用對象:

    1. wait()定義在Object類中,作用於對象本身
    2. sleep()定義在Thread 類中,作用當前線程。
  2. 方法屬性:

    1. wait()是實例方法
    2. sleep()是靜態方法 有static

 

 

 並發類中進程睡眠使用的函數TimeUnit.Days.sleep(1)

同時只要線程都有中斷異常,所以wait也有中斷異常。

 

 

 

3.Lock鎖(重點)

  • 回顧用傳統的 synchronized 實現 線程安全的賣票例子

真正的多線程開發,公司中的開發,降低耦合性 線程就是一個單獨的資源類,沒有任何附屬的操作! 對於資源類只有: 屬性、方法

開啟線程錯誤方式:

class Ticket implements Runnable{}

耦合度高,違背了oop(面向對象)思想

public class SaleTicket_WithSynchronized {
    public static void main(String[] args) {
        // 並發:多線程操作同一個資源類, 把資源類丟入線程
        Ticket ticket = new Ticket();
        // @FunctionalInterface 函數式接口,jdk1.8 lambda表達式 (參數)->{ 代碼 }
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "C").start();

    }
}

//資源類 OOP:
class Ticket {
    //屬性、方法
    private int number = 30;
    //賣票方法
    //用synchronized 上鎖
    public synchronized void saleTicket() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "賣出了第" + (number--) + "張票,剩余" + number + "張");
        }
    }
}

lambda表達式簡化了runnable函數式接口的使用,使得程序更加簡潔,保證了資源類的獨立性和程序的解耦合

 

package com.bupt;

public class SaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(()->{
            for (int i = 0; i < 4; i++) {
                ticket.saleTicket();
            }
        },"A").start();
        //上下兩個線程寫法是等價的,Runable匿名內部類使用lambda表達式進行了簡化

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    ticket.saleTicket();
                }
            }
        }, "B").start();
    }
}

class Ticket{   //只是一個資源類,保證只遵循oop原則
    private int number = 30;

    public synchronized void saleTicket(){
        if(number > 0){
            System.out.println(Thread.currentThread().getName() + "賣出了第" + number-- + "張票,剩余"+ number);
        }
    }
}

 

用Lock接口

Lock源碼定義:
在這里插入圖片描述

常用上鎖語句:

Lock l = ...;
l.lock();   //加鎖
try { // access the resource protected by this lock
} 
finally 
{ l.unlock(); //解鎖} 

ReentrantLock 可重入鎖

 

 

 

  • 公平鎖與非公平鎖(簡單認識,后面詳解)

    • 公平鎖: 非常公平, 不能夠插隊,必須先來后到!
    • 非公平鎖:非常不公平,可以插隊 (默認都是非公平)
  • 上案例

 

// Lock 三部曲
// 1、 new ReentrantLock();
// 2、 lock.lock(); // 加鎖
// 3、 finally=> lock.unlock(); // 解鎖
public class SaleTicket_WithLock {
    public static void main(String[] args) {
        // 並發:多線程操作同一個資源類, 把資源類丟入線程
        Ticket ticket = new Ticket();
        // @FunctionalInterface 函數式接口,jdk1.8 lambda表達式 (參數)->{ 代碼 }
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 1; i < 40; i++) {
                ticket.saleTicket();
            }
        }, "C").start();

    }

}

class Ticket2 {
    // 屬性、方法
    private int number = 30;
    Lock lock = new ReentrantLock();

    public void sale() {
        lock.lock(); // 加鎖
        try {
            // 業務代碼
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "賣出了第" +
                        (number--) + "張票,剩余:" + number + "張");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 解鎖
        }
    }
}

Synchronized 和 Lock 區別

Lock為什么比synchronized 能更好的實現同步訪問

 

 

 

 

 

 


Java中的鎖——Lock和synchronized

4、相比於synchronized,Lock接口所具備的其他特性

①嘗試非阻塞的獲取鎖tryLock():當前線程嘗試獲取鎖,如果該時刻鎖沒有被其他線程獲取到,就能成功獲取並持有鎖

②能被中斷的獲取鎖lockInterruptibly():獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷的時候,會拋出中斷異常同時釋放持有的鎖

③超時的獲取鎖tryLock(long time, TimeUnit unit):在指定的截止時間獲取鎖,如果沒有獲取到鎖返回false

 

 

 

 

 ReentrantLock中lock(),tryLock(),lockInterruptibly()的區別

 

Java線程的6種狀態及切換(透徹講解)

 

 join方法:原來主線程和子線程是並行的關系,但是一旦使用了join()方法,就會變成串行的關系;當主線程調用子線程的join()方法時,意味着必須等子線程執行完畢之后,主線程才會開始執行。

 

 

 

 yield是線程失去CPU資源,從運行狀態變成就緒狀態

wait是釋放當前鎖,讓其他線程獲取鎖,當被喚醒后就參與獲取鎖的競爭

非公平鎖按照一定的策略比如耗時等選擇線程執行,但是公平鎖是按照先來后到,一般選擇非公平鎖

4.生產者消費者問題

從pc(producer<–>consumer)問題看鎖的本質

1.synchronized版本

  • 生產者消費者synchronized版本

案例:對一個數字不停進行 +1 -1操作,加完了減,減完了加

 

public class PC_WithSynchronized {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MINUS").start();
    }
}
//判斷等待-->業務代碼-->通知
//數字:資源類
class Data{
    //屬性
    private int number = 0;
    //+1方法
    public synchronized  void increment() throws InterruptedException {
        if (number != 0){
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        //加完了通知其他線程
        this.notifyAll();
    }

    //-1
    public synchronized void decrement() throws InterruptedException {
        if (number == 0){
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"-->"+number);
        //減完了通知其他線程
        this.notifyAll();
    }
}

此時輸出:

加減交替執行,一片祥和~

 

..
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
ADD-->1
MINUS-->0
...

問題來了,現在只有"ADD"和"MINUS"兩個線程執行操作,如果增加多兩個線程會怎樣呢?

於是在main方法中增加了"ADD2" 和 "MINUS2"兩條線程:

new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        try {
            data.increment();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"ADD2").start();
new Thread(()->{
    for (int i = 0; i < 10; i++) {
        try {
            data.decrement();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
},"MINUS2").start();

出大問題了:出現了數據錯誤甚至死鎖問題

 

 

 

原因如下:

  • 用if判斷只執行了一次判斷,而wait()方法會導致🔒的釋放
  • 具體說明:以兩個加法線程ADD、ADD2舉例:
    •   比如ADD先執行,執行時調用了wait方法,那它會等待,此時會釋放鎖。
    •   那么線程ADD2 獲得鎖並且也會執行wait()方法,且釋放鎖,同時兩個加線程一起進入等待狀態,等待被喚醒。
    •   此時減線程中的某一個線程執行完畢並且喚醒了這倆加線程(notifyAll),那么這倆加線程不會一起執行,其中ADD獲取了鎖並且加1,執行完畢之后ADD2再執行。
    •   如果是if的話,那么ADD修改完num后,ADD2不會再去判斷num的值,直接會給num+1,如果是while的話,ADD執行完之后,ADD2還會去判斷num的值,因此就不會執行。
  • 上述情況稱為:虛假喚醒

 

 

 

 

 

 

  • 此時解決方法:將 if 改為 while

package com.kuangstudy.JUC;

/**
 * @BelongsProject: itstack-demo-design-1-00
 * @BelongsPackage: com.kuangstudy.JUC
 * @Author: lgwayne
 * @CreateTime: 2021-02-20 17:32
 * @Description:
 */
public class PC_WithSynchronized {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MINUS").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"ADD2").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"MINUS2").start();
    }
}
//判斷等待-->業務代碼-->通知
//數字:資源類
class Data{
  //data類相同,不再贅述
}

 虛假喚醒的一個形象的例子

當一個條件滿足時,很多線程都被喚醒了,但是只有其中部分是有用的喚醒,其它的喚醒都是無用功
1.比如說買貨,如果商品本來沒有貨物,突然進了一件商品,這是所有的線程都被喚醒了,但是只能一個人買,所以其他人都是假喚醒,獲取不到對象的鎖

相關博文:https://blog.csdn.net/qq_39455116/article/details/87101633

結果一片祥和~

...
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
ADD2-->1
MINUS2-->0
...    

 

 

 

通過Lock找到condition來配合控制對線程的喚醒

 

 

public class PC_WithLock {
    public static void main(String[] args) {
        //主線程測試方法與上述一致,不再贅述
    }
}

//判斷等待-->業務代碼-->通知
//數字:資源類
class Data_withLock {
    //屬性
    private int number = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    //+1方法
    public void increment() throws InterruptedException {
        lock.lock();
        try {
            while (number != 0) {
                //等待
                condition.await();
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            //加完了通知其他線程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    //-1
    public void decrement() throws InterruptedException {
        lock.lock();
        try {
            while (number == 0) {
                //等待
                condition.await();
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "-->" + number);
            //減完了通知其他線程
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

 

 

4.使用condition實現精准喚醒線程

用condition搭配狀態位置試試

public class PC_WithAwakeByCondition {
    public static void main(String[] args) {
        Data_AwakeInOrder data = new Data_AwakeInOrder();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printA();
            }
        },"線程A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printB();
            }
        },"線程B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.printC();
            }
        },"線程C").start();
    }
}

//資源類
class Data_AwakeInOrder{
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1;

    public void printA(){
        lock.lock();
        try {
        // 業務,判斷-> 執行-> 通知
            while (number != 1) {
            // 等待
                condition1.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>AAAAAA"+"-----number為->"+number);
            // 喚醒,喚醒指定的人,B
            number = 2;
            condition2.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        try {
            // 業務,判斷-> 執行-> 通知
            while (number != 2) {
                // 等待
                condition2.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>BBBBBB"+"-----number為->"+number);
            // 喚醒,喚醒指定的人C
            number = 3;
            condition3.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printC(){
        lock.lock();
        try {
            // 業務,判斷-> 執行-> 通知
            while (number != 3) {
                // 等待
                condition3.await();
            }
            System.out.println(Thread.currentThread().getName() + "=>CCCCCC"+"-----number為->"+number);
            // 喚醒,喚醒指定的人A
            number = 1;
            condition1.signal();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

運行結果:😁一片祥和~依序執行

...
線程A=>AAAAAA-----number為->1
線程B=>BBBBBB-----number為->2
線程C=>CCCCCC-----number為->3
線程A=>AAAAAA-----number為->1
線程B=>BBBBBB-----number為->2
線程C=>CCCCCC-----number為->3
...

看視頻的時候看到有人用wait和notify 也實現了精准喚醒,接下來稍作嘗試

5.使用wait()與notify()實現精准喚醒

public class PC_AwakeWithWait {
    public static void main(String[] args) {
        Data_AwakeInOrderByWait data = new Data_AwakeInOrderByWait();

        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printA();
            }
        },"T_A").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printB();
            }
        },"T_B").start();
        new Thread(()->{
            for (int i = 0; i < 100; i++) {
                data.printC();
            }
        },"T_C").start();
    }
}

//資源類
class Data_AwakeInOrderByWait{

    private int number = 1;

    public synchronized void printA() {
        // 業務,判斷-> 執行-> 通知
        while (number != 1) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>AAAAAA->"+number);
        // 喚醒,喚醒指定的人,B
        number = 2;
        this.notifyAll();
    }

    public synchronized void printB(){
        // 業務,判斷-> 執行-> 通知
        while (number != 2) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>BBBBBB->"+number);
        // 喚醒,喚醒指定的人,B
        number = 3;
        this.notifyAll();

    }
    public synchronized void printC(){
        // 業務,判斷-> 執行-> 通知
        while (number != 3) {
            // 等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "=>CCCCCC->"+number);
        // 喚醒,喚醒指定的人,B
        number = 1;
        this.notifyAll();
    }
}

結果也確實可以實現精准喚醒:

...
T_A=>AAAAAA->1
T_B=>BBBBBB->2
T_C=>CCCCCC->3
T_A=>AAAAAA->1
T_B=>BBBBBB->2
T_C=>CCCCCC->3
...

后來的技術是迭代出的更優秀的版本。等學成歸來對比下這兩種喚醒的效率

 

 

5.8個代碼加鎖的例子

用4套八種加鎖的例子對比理解鎖的本質

  • 情況一:實例化一個對象

    連續十次執行線程,A/B兩條線程交替執行   一般前一個線程獲取資源的概率更大

public class Test1 {
    public static void main(String[] args) {

        Phone phone = new Phone();
        for (int i = 0; i < 10; i++) {
            //鎖的存在
            new Thread(() -> {
                phone.sendSms();
            }, "A").start();

            new Thread(() -> {
                phone.call();
            }, "B").start();
        }
    }
}

class Phone {
    public synchronized void sendSms() {
        System.out.println("發短信###");
    }
    public synchronized void call() {
        System.out.println("打電話******");
    }
}
...
發短信###
打電話******
打電話******
打電話******
發短信###
...

在線程執行的過程中增加sleep()休眠(Phone類不變)

public static void main(String[] args) {
        Phone phone = new Phone();
        for (int i = 0; i < 10; i++) {
            //鎖的存在
            new Thread(() -> {
                phone.sendSms();
            }, "A").start();

            //增加0.5s的休眠時間
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            new Thread(() -> {
                phone.call();
            }, "B").start();
        }
    }
發短信###
打電話******
發短信###
打電話******
  • 結果:

    發短信—>打電話

    依次執行

在Phone的 發短信的方法中增加2s的休眠(main方法不變)

class Phone {
    public synchronized void sendSms() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信###");
    }

    public synchronized void call() {
        System.out.println("打電話******");
    }
}

結果:

隨機執行

總結

synchronized 鎖的對象是方法的調用者,也就是實例化后的對象。
兩個方法用的是同一個鎖,誰先拿到誰執行!
在例子中,線程的調度是隨機的,誰先拿到誰先執行

情況二:實例化一個對象

  • 兩個對象,兩個調用者,兩把🔒

  • 兩個問題:

3. 增加了一個普通方法后!先執行發短信還是Hello? 普通方法
4. 兩個對象,兩個同步方法, 發短信還是 打電話? // 打電話
public class Test2  {
    public static void main(String[] args) {
        // 兩個對象,兩個調用者,兩把鎖!
        Phone2 phone1 = new Phone2();
        Phone2 phone2 = new Phone2();

        //鎖的存在
        new Thread(phone1::sendSms,"A").start();

        new Thread(phone1::hello,"C1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone2::call,"B").start();

        new Thread(phone2::hello,"C2").start();
    }
}

class Phone2{
    public synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信sssss <--"+Thread.currentThread().getName());
    }

    public synchronized void call(){
        System.out.println("打電話cccc <--"+Thread.currentThread().getName());
    }

    public void hello(){
        System.out.println("hello <--"+Thread.currentThread().getName());
    }
}

運行結果(多次)

hello <--C1
打電話cccc <--B
hello <--C2
發短信sssss <--A
    • 總結:

      • synchronized 鎖的對象是方法的調用者,也就是實例化后的對象。
      • 普通方法不會上🔒,因此會首先打印hello,速度快
      • A 線程調用 sendSms 時延時了 2s,因此會先調用B線程的發短信方法
  • 情況三:靜態方法

    • 問題:

5.增加兩個靜態的同步方法,只有一個對象,先打印 發短信?打電話?
6.兩個對象!增加兩個靜態的同步方法, 先打印 發短信?打電話?

例子1(實例化一個對象)

public class Test3  {
    public static void main(String[] args) {
        Phone3 phone1 = new Phone3();
        new Thread(()->{
            phone1.sendSms();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone1.call();
        },"B").start();
    }
}
class Phone3{
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    public static synchronized void call(){
        System.out.println("打電話");
    }
}
    • 運行結果:

      發短信 -->打電話

  • 例子2:(實例化兩個對象)

Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{
    phone1.sendSms();
},"A").start();
try {
    TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}
//另一個實例化對象
new Thread(()->{
    phone2.call();
},"B").start();
    • 運行結果:

      發短信 -->打電話

  • 總結:

    • 兩個對象的Class類模板只有一個,static方法時在類加載的時候初始化,因此鎖的是Class對象
    • 在休眠后(極大可能)調用發短信對象,同時對模板上🔒,因此依次打印

情況四:靜態與普通方法

問題:

7.1個靜態的同步方法,1個普通的同步方法 ,一個對象,先打印 發短信?打電話?
8.1個靜態的同步方法,1個普通的同步方法 ,兩個對象,先打印 發短信?打電話?

例子7:

public class Test4  {
    public static void main(String[] args) {
        Phone4 phone1 = new Phone4();
        new Thread(()->{
            phone1.sendSms();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone1.call();
        },"B").start();
    }
}
class Phone4{
    public static synchronized void sendSms(){
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("發短信");
    }
    public synchronized void call(){
        System.out.println("打電話");
    }
}
    • 運行結果:
      • 打電話 --> 發短信
  • 例子8:

        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        new Thread(()->{
            phone1.sendSms();
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            phone2.call();
        },"B").start();
  • 總結:

    • 普通方法 🔒的是對象
    • static 方法 🔒的是模板

雙冒號表達式:

插播一個知識點,雙冒號(::)表達式

詳見:JAVA 8 ‘::’ 關鍵字,帶你深入了解它!

 

 

 

 

6.集合不安全類

  • List不安全

  • 狂神多線程 的 三個線程不安全的案例中有提及,今天在並發角度測試下

 

public class TestList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
             list.add(UUID.randomUUID().toString().substring(0,5));
             System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

執行程序后會拋出:

java.util.ConcurrentModificationException異常(后續拋出同樣異常不做細說)

在這里插入圖片描述

也就是不同線程同時操作了同一list索引元素拋出的異常。

解決方法:

public static void main(String[] args) {
    //        List<String> list = new ArrayList<>();
    //1.集合自帶的線程安全的list
    //        List<String> list = new Vector<>();

    //2.Collections工具類強行上鎖
    //        List<String> list =Collections.synchronizedList(new ArrayList<>());

    //3.用JUC包下的讀寫數組CopyOnWriteArrayList,讀寫分離
    List<String> list = new CopyOnWriteArrayList<>();
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString().substring(0, 5));
            System.out.println(list);
        }, String.valueOf(i)).start();
    }
}

一、CopyOnWriteArrayList介紹
①、CopyOnWriteArrayList,寫數組的拷貝,支持高效率並發且是線程安全的,讀操作無鎖的ArrayList。所有可變操作都是通過對底層數組進行一次新的復制來實現。
②、CopyOnWriteArrayList適合使用在讀操作遠遠大於寫操作的場景里,比如緩存。它不存在擴容的概念,每次寫操作都要復制一個副本,在副本的基礎上修改后改變Array引用。CopyOnWriteArrayList中寫操作需要大面積復制數組,所以性能肯定很差。
③、CopyOnWriteArrayList 合適讀多寫少的場景,不過這類慎用 ,因為誰也沒法保證CopyOnWriteArrayList 到底要放置多少數據,萬一數據稍微有點多,每次add/set都要重新復制數組,這個代價實在太高昂了。在高性能的互聯網應用中,這種操作分分鍾引起故障。

二、CopyOnWriteArrayList 有幾個缺點:
1、由於寫操作的時候,需要拷貝數組,會消耗內存,如果原數組的內容比較多的情況下,可能導致young gc或者full gc。
(1、young gc :年輕代(Young Generation):對象被創建時,內存的分配首先發生在年輕代(大對象可以直接被創建在年老代),大部分的對象在創建后很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱為Minor GC或叫Young GC。
2、年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC后存活了下來),則會被復制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時,將執行Major GC,也叫 Full GC

2、不能用於實時讀的場景,像拷貝數組、新增元素都需要時間,所以調用一個set操作后,讀取到數據可能還是舊的,雖然CopyOnWriteArrayList 能做到最終一致性,但是還是沒法滿足實時性要求;

三、CopyOnWriteArrayList的一些方法
1、add(E e) :將指定元素添加到此列表的尾部,返回值為boolean。
2、add(int index, E element) : 在此列表的指定位置上插入指定元素。
3、clear():從此列表移除所有元素。
4、contains(Object o) :如果此列表包含指定的元素,則返回 true。
5、equals(Object o) :比較指定對象與此列表的相等性。
6、get(int index) : 返回列表中指定位置的元素。
7、hashCode() : 返回此列表的哈希碼值。
8、indexOf(E e, int index) : 返回第一次出現的指定元素在此列表中的索引,從 index 開始向前搜索,如果沒有找到該元素,則返回 -1。
9、indexOf(Object o) :返回此列表中第一次出現的指定元素的索引;如果此列表不包含該元素,則返回 -1。
10、isEmpty() :如果此列表不包含任何元素,則返回 true。
11、iterator() :返回以恰當順序在此列表元素上進行迭代的迭代器,返回值為 Iterator。
12、lastIndexOf(E e, int index) :返回最后一次出現的指定元素在此列表中的索引,從 index 開始向后搜索,如果沒有找到該元素,則返回 -1。
13、lastIndexOf(Object o) : 返回此列表中最后出現的指定元素的索引;如果列表不包含此元素,則返回 -1。
14、remove(int index) :移除此列表指定位置上的元素。
15、remove(Object o) :從此列表移除第一次出現的指定元素(如果存在),返回值為 boolean。
16、set(int index, E element) :用指定的元素替代此列表指定位置上的元素。
17、size() :返回此列表中的元素數。
18、subList(int fromIndex, int toIndex) :返回此列表中 fromIndex(包括)和 toIndex(不包括)之間部分的視圖,返回值為 List 。

五、總結
CopyOnWriteArrayList這是一個ArrayList的線程安全的變體,其原理大概可以通俗的理解為:初始化的時候只有一個容器,很長一段時間,這個容器數據、數量等沒有發生變化的時候,大家(多個線程),都是讀取(假設這段時間里只發生讀取的操作)同一個容器中的數據,所以這樣大家讀到的數據都是唯一、一致、安全的,但是后來有人往里面增加了一個數據,這個時候CopyOnWriteArrayList 底層實現添加的原理是先copy出一個容器(可以簡稱副本),再往新的容器里添加這個新的數據,最后把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取數據,仍然是讀取到舊的容器里的數據。

四、Java代碼示例

package chapter3.copyonwrite;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author czd
 */
public class CopyOnWriteArrayListTest {
    public static void main(String[] args) {
        /**
         * 構造方法摘要
         * CopyOnWriteArrayList():
         *           創建一個空列表。
         * CopyOnWriteArrayList(Collection<? extends E> c):
         *           創建一個按 collection 的迭代器返回元素的順序包含指定 collection 元素的列表。
         * CopyOnWriteArrayList(E[] toCopyIn):
         *           創建一個保存給定數組的副本的列表。
         */

        /**
         * 1、add(E e) :將指定元素添加到此列表的尾部,返回值為boolean。
         *
         * 2、iterator() :返回以恰當順序在此列表元素上進行迭代的迭代器,返回值為Iterator<E>。
         */
        CopyOnWriteArrayList<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
        Boolean addBoolean = copyOnWriteArrayList.add(1);
        System.out.println("是否添加到此列表的尾部?" + addBoolean);
        Iterator<Integer> iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()){
            System.out.println("iterator的結果: " + iterator.next());
        }

        /**
         * 3、add(int index, E element) :在此列表的指定位置上插入指定元素。
         */
        CopyOnWriteArrayList<Integer> copyOnWriteArrayList1 = new CopyOnWriteArrayList<>();
        copyOnWriteArrayList1.add(1);
        copyOnWriteArrayList1.add(2);
        copyOnWriteArrayList1.add(2,5);
//        copyOnWriteArrayList1.add(4,6);
        System.out.println("index = 0 的值為:" + copyOnWriteArrayList1.get(0));
        System.out.println("index = 1 的值為:" + copyOnWriteArrayList1.get(1));
        System.out.println("index = 2 的值為:" + copyOnWriteArrayList1.get(2));
//        System.out.println("index = 4 的值為:" + copyOnWriteArrayList1.get(4));
        /**
         * 注意:如果使用add(int index, E element),必須保證index的前面的index存在值,不然會報錯。
         *      (可以把上兩行注釋去掉,運行試試結果)
         */

        /**
         * 4.1、indexOf(E e, int index)
         *           返回第一次出現的指定元素在此列表中的索引,從 index 開始向前搜索,如果沒有找到該元素,則返回 -1。
         * 4.2、indexOf(Object o)
         *           返回此列表中第一次出現的指定元素的索引;如果此列表不包含該元素,則返回 -1。
         */
        CopyOnWriteArrayList<Integer> copyOnWriteArrayList2 = new CopyOnWriteArrayList<>();
        copyOnWriteArrayList2.add(1);
        copyOnWriteArrayList2.add(2);
        copyOnWriteArrayList2.add(3);
        Integer indexFirst = copyOnWriteArrayList2.indexOf(2);
        System.out.println("2的index為:" + indexFirst);

        /**
         * 5.1、lastIndexOf(E e, int index)
         *           返回最后一次出現的指定元素在此列表中的索引,從 index 開始向后搜索,如果沒有找到該元素,則返回 -1。
         * 5.2、lastIndexOf(Object o)
         *           返回此列表中最后出現的指定元素的索引;如果列表不包含此元素,則返回 -1。
         */
        CopyOnWriteArrayList<Integer> copyOnWriteArrayList3 = new CopyOnWriteArrayList<>();
        copyOnWriteArrayList3.add(1);
        copyOnWriteArrayList3.add(2);
        copyOnWriteArrayList3.add(3);
        copyOnWriteArrayList3.add(3);
        copyOnWriteArrayList3.add(4);
        Integer lastIndexOf = copyOnWriteArrayList3.lastIndexOf(3);
        System.out.println("列表中最后出現的指定元素的索引: " + lastIndexOf);

        /**
         * 6、remove(int index) :移除此列表指定位置上的元素,並且此下標后面的值會進一位,
         *                      即index為1的值被remove掉,index為2的值就變為index為1的值。
         */
        CopyOnWriteArrayList<Integer> copyOnWriteArrayList4 = new CopyOnWriteArrayList<>();
        copyOnWriteArrayList4.add(5);
        copyOnWriteArrayList4.add(6);
        copyOnWriteArrayList4.add(7);
        copyOnWriteArrayList4.add(8);
        Integer removeResult = copyOnWriteArrayList4.remove(1);
        System.out.println("copyOnWriteArrayList4索引為1的值:" + removeResult);
        System.out.println("copyOnWriteArrayList4中是否還存在索引為1的值:" + copyOnWriteArrayList4.get(1));


        /**
         * 7、remove(Object o) :從此列表移除第一次出現的指定元素(如果存在)。
         */
        CopyOnWriteArrayList<String> copyOnWriteArrayList5 = new CopyOnWriteArrayList<>();
        copyOnWriteArrayList5.add("5");
        copyOnWriteArrayList5.add("6");
        copyOnWriteArrayList5.add("6");
        copyOnWriteArrayList5.add("7");
        copyOnWriteArrayList5.add("7");
        System.out.println("沒被remove前的size: " + copyOnWriteArrayList5.size());
        Boolean removeBoolean = copyOnWriteArrayList5.remove("7");
        System.out.println("copyOnWriteArrayList5是否移除7成功?" + removeBoolean);
        System.out.println("index為3的值為:" + copyOnWriteArrayList5.get(3));
        System.out.println("被remove后的size: " + copyOnWriteArrayList5.size());

        /**
         * 8、set(int index, E element) :用指定的元素替代此列表指定位置上的元素。
         */
        CopyOnWriteArrayList<String> copyOnWriteArrayList6 = new CopyOnWriteArrayList<>();
        copyOnWriteArrayList6.add("1");
        copyOnWriteArrayList6.add("2");
        copyOnWriteArrayList6.add("3");
        copyOnWriteArrayList6.add("4");
        copyOnWriteArrayList6.add("5");
        System.out.println("沒使用set()方法前,index = 1 的值為:" + copyOnWriteArrayList6.get(1));
        copyOnWriteArrayList6.set(1,"czd");
        System.out.println("使用set()方法后,index = 1 的值為:" + copyOnWriteArrayList6.get(1));

        

    }
}

 

Set不安全

例子

public class TestSet {
    public static void main(String[] args) {
        Set<Object> set = new HashSet<>();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                set.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(set);
            }, String.valueOf(i)).start();
        }
    }
}

解決方法:

public static void main(String[] args) {
    //        Set<Object> set = new HashSet<>();
    //1.用collections工具類強行上鎖:
    //        Set<Object> set = Collections.synchronizedSet(new HashSet<>());

    //2.用CopyOnWriteArrayList 實現的CopyOnWriteArraySet
    Set<String> set  = new CopyOnWriteArraySet<>();

    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            set.add(UUID.randomUUID().toString().substring(0, 5));
            System.out.println(set);
        }, String.valueOf(i)).start();
    }
}

 

 

 hashSet本質

public HashSet() {
map = new HashMap<>();
}

// add set 本質就是 map key是無法重復的!
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

private static final Object PRESENT = new Object(); // 不變的值,做value

Map不安全

例子:

public class TestMap {
    public static void main(String[] args) {
        // 默認等價於什么? new HashMap<>(16,0.75);
        // Map<String, Object> map = new HashMap<>();
        //1.用juc下線程安全的map
        Map<String, Object> map = new ConcurrentHashMap<>();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0, 5));
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}

 

 

 

7.Callable使用
下列博客對callable有詳細的介紹,值得一看✌

Callable接口及Futrue接口詳解

目錄

Callable接口
Futrue接口
1.使用Callable和Future的完整示例
2.使用Callable和FutureTask的完整示例
3.使用Runnable來獲取返回結果的實現

有兩種創建線程的方法-一種是通過創建Thread類,另一種是通過使用Runnable創建線程。但是,Runnable缺少的一項功能是,當線程終止時(即run()完成時),我們無法使線程返回結果。為了支持此功能,Java中提供了Callable接口。

  • 為了實現Runnable,需要實現不返回任何內容的run()方法,
  • 對於Callable,需要實現在完成時返回結果的call()方法。
  • 請注意,不能使用Callable創建線程,只能使用Runnable創建線程。
  • 另一個區別是call()方法可以引發異常,而run()則不能。
  • 為實現Callable而必須重寫call()方法。

為把Callable對象扔給Thread需要借助Runnable的實現類futureTask作為中間人

 

 

 

 

 

 

8. JUC常用輔助類

8.1 CountDownLatch

顧名思義:倒計時鎖存器

不管你線程中間執行的情況,結果若是線程執行完了,那就再執行最后語句,如果沒達到條件就一直等

(領導:不看過程只看結果)

 

 

 

 

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 總數是6,必須要執行任務的時候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" Go out");
                countDownLatch.countDown(); // 數量-1
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); // 等待計數器歸零,然后再向下執行
        System.out.println("Close Door");
    }
}
1 Go out
5 Go out
4 Go out
3 Go out
2 Go out
6 Go out
Close Door

如果創建了7條任務線程,但只countDown了6次,那么將會一直阻塞線程

總結:

CountDownLatch countDownLatch = new CountDownLatch(6); 創建線程總數

countDownLatch.countDown(); 實行完線程數-1

countDownLatch.await(); 等待計數器歸零,然后再向下執行
每次有線程調用 countDown() 數量-1,假設計數器變為0,countDownLatch.await() 就會被喚醒,繼續執行

8.2 CyclicBarrier

見名之意:循環障礙,與8.1 方法相反的,加法計時器

public class CyclicBarrierDemo {
    public static void main(String[] args) {

        /**
         * 集齊7顆龍珠召喚神龍
         */
        // 召喚龍珠的線程:7條線程
        // 創建成功后執行runnable接口打印“召喚七顆龍珠成功”
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召喚七顆龍珠成功");
        });
        for (int i = 1; i <= 7; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "收集到第" + temp + "個龍珠");

                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

創建線程Barrier后開啟線程執行:

CyclicBarrier(int parties, Runnable barrierAction) 
創建一個新的 CyclicBarrier ,當給定數量的線程(線程)等待時,它將跳閘,當屏障跳閘時執行給定的屏障動作,由最后一個進入屏障的線程執行。

並在調用await()方法時自動+1:

如果執行的線程數到達設定值,則會執行 創建時設定的屏障動作,
如果無法到達則線程會處在阻塞狀態
8.2.1 lambda為什么要用final?
原創來源: lambda里面賦值局部變量必須是final原因

簡單復習下lambda函數:匿名實現函數式接口的方法

Runnable r= new Runnable() {
    @Override
    public void run() {
        System.out.println("這是常用實例化函數式接口");
    }
};

Runnable rl = ()->System.out.println("這是lambda實例化接口");

 

 

 

8.3 Semaphore

見名之意:信號量,可近似看作資源池。

舉例子(搶車位):

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 線程數量:停車位! 限流!
        //創建只有3個停車位的停車場
        Semaphore semaphore = new Semaphore(3);

        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                // acquire() 得到
                try {
                    //搶車位。。。
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"搶到車位👍");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"離開車位😀");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {、
                    //停車結束,離開車位
                    semaphore.release(); // release() 釋放
                }
            },String.valueOf(i)+"號車-->").start();
        }

    }
}
1號車-->搶到車位👍
3號車-->搶到車位👍
2號車-->搶到車位👍
2號車-->離開車位😀
1號車-->離開車位😀
4號車-->搶到車位👍
3號車-->離開車位😀
6號車-->搶到車位👍
5號車-->搶到車位👍
5號車-->離開車位😀
6號車-->離開車位😀
4號車-->離開車位😀

每個人都能停到車🚘

9.讀寫鎖

ReadWriteLock 接口

所有已知實現類: ReentrantReadWriteLock

  • 讀:可多條線程同時獲取數據
  • 寫:只能單條線程寫入

獨占鎖/排它鎖/寫鎖   共享鎖/讀鎖

public class TestReadWriteLock {
    public static void main(String[] args) {
        //未上鎖:
        //        MyCache myCache = new MyCache();
        //上了讀寫鎖:
        MyCacheWithLock myCache = new MyCacheWithLock();
        //寫入:
        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.write(temp + "", temp + "");
            }, String.valueOf(i)).start();
        }

        for (int i = 1; i <= 5; i++) {
            final int temp = i;
            new Thread(() -> {
                myCache.read(temp + "");
            }, String.valueOf(i)).start();
        }
    }
}

class MyCacheWithLock {
    private volatile Map<String, Object> map = new HashMap<>();

    //讀寫鎖:對數據更精准控制
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock lock = new ReentrantLock();

    //寫數據:只希望有一個線程在執行
    public void write(String key, Object value) {
        readWriteLock.writeLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + "寫入" + key);
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "寫入完成!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //讀數據:可一條或者多條同時執行
    public void read(String key) {
        readWriteLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "讀取數據:" + key);
            Object o = map.get(key);
            System.out.println(Thread.currentThread().getName() + "讀取數據完成-->" + o);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
    /**
     * 存入數據過程上鎖,安全
     */
}


/**
 * 未上鎖:
 */
class MyCache {
    private volatile Map<String, Object> map = new HashMap<>();

    //寫數據:
    public void write(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "寫入" + key);
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "寫入完成!");
    }

    //讀數據:
    public void read(String key) {
        System.out.println(Thread.currentThread().getName() + "讀取數據:" + key);
        Object o = map.get(key);
        System.out.println(Thread.currentThread().getName() + "讀取數據完成-->" + o);
    }
    /**
 * 運行結果:
 * 寫入線程會被 讀取線程中斷,造成臟讀,對數據不安全
 */
}

10.阻塞隊列

Interface BlockingQueue

是數據結構中隊列Queue的延展使用。

 

 

 

 

 

 

 

 

 

什么情況下我們會使用阻塞隊列?

對線程並發處理,線程池!

學會使用隊列的操作: 添加 和 移除

 

第一組:

@Test
public void test1(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.add("a")); //true
    System.out.println(blockingQueue.add("b")); // true
    System.out.println(blockingQueue.add("c"));  //true
    //        System.out.println(blockingQueue.add("d"));
    //      會拋異常:java.lang.IllegalStateException: Queue full

    System.out.println("=============================");
    System.out.println(blockingQueue.remove()); //a
    System.out.println(blockingQueue.remove()); //b
    System.out.println(blockingQueue.remove()); //c
    //        System.out.println(blockingQueue.remove());           
    //會拋出 NoSuchElementException異常
    System.out.println(blockingQueue.element()); //如無元素則會報NoSuchElementException異常
}

第二組:

@Test
public void test2(){
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a"));//true
    System.out.println(blockingQueue.offer("b"));//true
    System.out.println(blockingQueue.offer("c"));//true
    System.out.println(blockingQueue.offer("d"));//false

    System.out.println("=============================");
    System.out.println(blockingQueue.poll()); //a
    System.out.println(blockingQueue.poll()); //b
    System.out.println(blockingQueue.poll()); //c
    System.out.println(blockingQueue.poll()); //null

    System.out.println("=============================");
    System.out.println(blockingQueue.offer("e"));//true
    System.out.println(blockingQueue.offer("f"));//true
    System.out.println(blockingQueue.peek());       //e  : 隊首元素

}

第三組:

@Test
public void test3() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    blockingQueue.put("a");
    blockingQueue.put("b");
    blockingQueue.put("c");
    //        blockingQueue.put("d"); // 隊列沒有位置了,一直阻塞

    System.out.println("=============================");
    System.out.println(blockingQueue.take()); //a
    System.out.println(blockingQueue.take()); //b
    System.out.println(blockingQueue.take()); //c
    //        System.out.println(blockingQueue.take()); // 隊列沒有值可以取,一直阻塞

}

第四組:

@Test
public void test4() throws InterruptedException {
    ArrayBlockingQueue<Object> blockingQueue = new ArrayBlockingQueue<>(3);
    System.out.println(blockingQueue.offer("a"));//true
    System.out.println(blockingQueue.offer("b"));//true
    System.out.println(blockingQueue.offer("c"));//true
    System.out.println(blockingQueue.offer("d", 2, TimeUnit.SECONDS));//false

    System.out.println("=============================");
    System.out.println(blockingQueue.poll()); //a
    System.out.println(blockingQueue.poll()); //b
    System.out.println(blockingQueue.poll()); //c
    System.out.println(blockingQueue.poll(2,TimeUnit.SECONDS)); //null

}

SynchronousQueue 同步隊列
沒有容量==> 進去一個元素,必須等待取出來之后,才能再往里面放一個元素!

和其他的BlockingQueue 不一樣, SynchronousQueue 不存儲元素,put了一個元素,必須從里面先take取出來,否則不能再put進去值!

public class TestSynchronousQueue {
    public static void main(String[] args) {
        SynchronousQueue<String> bq = new SynchronousQueue<>();
        new Thread(() -> {
            try {
                System.out.println(Thread.currentThread().getName() + " put 1");
                bq.put("1");
                System.out.println(Thread.currentThread().getName() + " put 2");
                bq.put("2");
                System.out.println(Thread.currentThread().getName() + " put 3");
                bq.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T1").start();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get =>" + bq.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get =>" + bq.take());
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + " get =>" + bq.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "T2").start();
    }
}
/**
T1 put 1
T2 get =>1
T1 put 2
T2 get =>2
T1 put 3
T2 get =>3
*/

 

11.線程池(※)
學習目標:

三大方法
七大參數
四種拒絕策略
11.0 池化技術
在系統開發過程中,我們經常會用到池化技術來減少系統消耗,提升系統性能。

說人數: 簡單點來說,就是提前保存大量的資源,以備不時之需,池化技術就是通過復用來提升性能。

常見池:

對象池
通過復用對象來減少創建對象、垃圾回收的開銷;
連接池
(數據庫連接池、Redis連接池和HTTP連接池等)通過復用TCP連接來減少創建和釋放連接的時間
線程池通過復用線程提升性能
使用內存池的優點

  • 降低資源消耗。這個優點可以從創建內存池的過程中看出,當我們在創建內存池的時候,分配的都是一塊塊比較規整的內存塊,減少內存碎片的產生。
  • 提高相應速度。這個可以從分配內存和釋放內存的過程中看出。每次的分配和釋放並不是去調用系統提供的函數或操作符去操作實際的內存,而是在復用內存池中的內存。
  • 方便管理。

使用內存池的缺點

  • 缺點就是很可能會造成內存的浪費,因為要使用內存池需要在一開始分配一大塊閑置的內存,而這些內存不一定全部被用到。

11.1 三大方法
阿里巴巴 Java 開發手冊中 對線程池的規范:

 

 

Executors 工具類中3大方法(詳見API)

public static ExecutorService newSingleThreadExecutor()
    //創建一個線程池,根據需要創建新的線程,但在可用時將重用先前構建的線程。
public static ExecutorService newFixedThreadPool(int nThreads)
//創建一個線程池,使用固定數量的線程操作了共享無界隊列
public static ExecutorService newCachedThreadPool()
    //創建一個線程池,根據需要創建新的線程,但在可用時將重用先前構建的線程。
定時線程池(newScheduledThreadPool)

 

public class TestMethods {
    public static void main(String[] args) {
        //        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        // 單個線程:此時只有pool-1-thread-1
        //         ExecutorService threadPool = Executors.newFixedThreadPool(5);
        // 創建一個固定的線程池的大小: 此時最多有pool-1-thread-5 ok
        ExecutorService threadPool = Executors.newCachedThreadPool(); 
        // 可伸縮的,遇強則強,遇弱則弱 : 此時最多開啟到pool-1-thread-31 ok 去執行

        try {
            for (int i = 0; i < 100; i++) {
                // 使用了線程池之后,使用線程池來創建線程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 線程池用完,程序結束,關閉線程池
            threadPool.shutdown();
        }
    }
}
package com.zhw.learning.thread;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author zhw
 * 創建一個可定期或者延時執行任務的定長線程池,支持定時及周期性任務執行
 *
 * Executors.newScheduledThreadPool(3):
 * public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
 *         return new ScheduledThreadPoolExecutor(corePoolSize);
 *     }
 * public ScheduledThreadPoolExecutor(int corePoolSize) {
 *         super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
 *               new DelayedWorkQueue());
 *     }
 */
public class ScheduledThreadPoolTest {

    public static void main(String[] args) throws InterruptedException {
        final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);

        System.out.println("提交時間: " + sdf.format(new Date()));

        //延遲3秒鍾后執行任務
//        scheduledThreadPool.schedule(new Runnable() {
//            @Override
//            public void run() {
//                System.out.println("運行時間: " + sdf.format(new Date()));
//            }
//        }, 3, TimeUnit.SECONDS);

        //延遲1秒鍾后每隔3秒執行一次任務
        scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("運行時間: " + sdf.format(new Date()));
            }
        }, 1, 3, TimeUnit.SECONDS);
        Thread.sleep(10000);

        scheduledThreadPool.shutdown();
    }

}

11.2 七大參數

源碼分析:

//創建單一線程的線程池
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
//創建固定線程數的線程池
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
//創建代緩存的線程池:
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

其本質都是調用本質ThreadPoolExecutor創建線程池,也是 阿里巴巴規范中提及的方法:

public ThreadPoolExecutor(int corePoolSize,  //核心線程池大小
                          int maximumPoolSize, //最大核心線程池大小
                          long keepAliveTime, //超時了沒有人調用就會釋放
                          TimeUnit unit,  //超時單位
                          BlockingQueue<Runnable> workQueue,//阻塞隊列
                          ThreadFactory threadFactory,// 線程工廠:創建線程的,一般不用動
                          RejectedExecutionHandler handler/*拒絕策略*/) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

11. 2.1 手動創建線程池:

 

 

 

 

銀行辦理業務舉例:

  • 有5個櫃台,每次辦理一個人。
  • 當天值班櫃員只有2人。
  • 剩余3個臨時工等待超過一定時間會離開櫃台。
  • 候客區有3個座位,可容納3人等待。
public class TestDefPool {
    public static void main(String[] args) {
        // 自定義線程池!工作 ThreadPoolExecutor
        ExecutorService threadPool = new ThreadPoolExecutor(
            2,  //當天值班員工(核心線程池大小)
            5, //櫃台總數:最大核心線程池大小
            3, //臨時工的超時等待:超時了沒有人調用就會釋放
            TimeUnit.SECONDS, //超時等待單位
            new LinkedBlockingDeque<>(3),//候客區:阻塞隊列
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.DiscardOldestPolicy()); //滿了后告訴客人辦不了業務了:拋異常RejectedExecutionException
        try {
            // 最大承載:Deque + max 此處為:5+3=8
            // 超過 RejectedExecutionException
            for (int i = 1; i <= 9; i++) {
                // 使用了線程池之后,使用線程池來創建線程
                threadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " ok");
                });
            }
        } catch (
            Exception e)

        {
            e.printStackTrace();
        } finally

        {
            // 線程池用完,程序結束,關閉線程池
            threadPool.shutdown();
        }
    }
}

11.4 四種拒絕策略

 

 

 

/**
1. new ThreadPoolExecutor.AbortPolicy() 
// 銀行滿了,還有人進來,不處理這個人的,拋出異常:RejectedExecutionException
 
2.new ThreadPoolExecutor.CallerRunsPolicy() // 哪來的去哪里!
//公司叫你來銀行辦業務,銀行滿人辦不了,回公司找人辦
//新開辟的線程搞不定調用主線程

3.new ThreadPoolExecutor.DiscardPolicy() 
//銀行辦不了了,不辦你業務
//隊列滿了,丟掉任務,不會拋出異常!

4.new ThreadPoolExecutor.DiscardOldestPolicy() 
//排隊人滿了,你看看最早開始的客戶搞定沒,沒搞定就被拒絕了。
//隊列滿了,嘗試去和最早的競爭,也不會拋出異常!
*/

 

 

11.5小結:
根據上述參數對線程池調優:主要針對線程池大小調優

IO密集型

io密集型則是根據當前應用的數量來設置最大線程數,一般是應用數*2

一般來說:文件讀寫、DB讀寫、網絡請求等。

這類任務的特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成(因為IO的速度遠遠低於CPU和內存的速度)。對於IO密集型任務,任務越多,CPU效率越高,但也有一個限度。常見的大部分任務都是IO密集型任務,比如Web應用。

CPU密集型

一般來說:計算型代碼、Bitmap轉換、Gson轉換等。

這種計算密集型任務雖然也可以用多任務完成,但是任務越多,花在任務切換的時間就越多,CPU執行任務的效率就越低,所以,要最高效地利用CPU,計算密集型任務同時進行的數量應當等於CPU的核心數 

// 獲取CPU的核數
System.out.println(Runtime.getRuntime().availableProcessors());

java中的方法引用

Lock為什么比synchronized 能更好的實現同步訪問 

 

BlockingQueue(阻塞隊列)詳解

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM