Java 多執行緒學習筆記

SJMP1573發表於2020-11-18

學習視訊參考連結:https://www.bilibili.com/video/BV1V4411p7EF?p=27

  • 執行緒簡介
  • 執行緒的實現(重點)
  • 執行緒狀態
  • 執行緒同步(重點)
  • 執行緒通訊問題
  • 高階主題(重點)

1、執行緒簡介

棧空間操作起來最快但是棧很小,通常大量的物件都是放在堆空間,棧和堆的大小都可以通過 JVM 的啟動引數來進行調整,棧空間用光了會引發 StackOverflowError,而堆和常量池空間不足則會引發 OutOfMemoryError。

String str = new String("hello"); 

上面的語句中變數 str 放在棧上,用 new 建立出來的字串物件放在堆上,而 “hello” 這個字面量是放在方法區的。

在這裡插入圖片描述
例子:

開車 + 打電話

吃飯 + 玩手機

這些動作都可以抽象為任務,雖然看起來一心二用,但人只有一個大腦,在一個時間片刻只能處理一個任務。

CPU 也是一樣,面對多個任務,只能在一個時間片刻處理一個任務。

主執行緒呼叫 run 方法和呼叫 start 方法開啟子執行緒的區別如下圖所示。
在這裡插入圖片描述

  • 執行緒就是獨立的執行路徑;
  • 在程式執行時,即使沒有自己建立執行緒,後臺也會有多個執行緒,如主執行緒,GC 執行緒;
  • main()稱之為主執行緒,為系統的入口,用於執行整個程式;
  • 在一個程式中,如果開闢了多個執行緒,執行緒的執行由排程器安排排程,排程器是與作業系統緊密相關的,先後順序是不能人為干預。
  • 對同一份資源操作時,會存在資源搶奪的問題,需要加入併發控制;
  • 執行緒會帶來額外的開銷,如 CPU 排程時間,併發控制開銷。
  • 每個執行緒在自己的工作記憶體互動,記憶體控制不當會造成資料不一致

2、執行緒實現

執行緒的三種實現方式:

在這裡插入圖片描述

2.1 繼承 Thread 類,重寫 run 方法

繼承 Thread 類,重寫 run 方法。建立這個類的物件,再呼叫 start() 即可

package com.sjmp.Thread01;

/**
 * @ClassName ThreadTest
 * @Description TODO
 * @Author sjmp1573
 * @Date DATE{TIME}
 */
public  class ThreadTest {

    // 繼承 Thread 類並重寫 run 方法
    public static class MyThread extends Thread{
        @Override
        public void run() {
            System.out.println("I am a child thread");
        }
    }

    public static void main(String[] args) {
        //建立一個執行緒
        MyThread thread = new MyThread();
        //啟動執行緒
        thread.start();
    }
}

下載檔案需要在 pom.xml 中 commons io 包。

使用該方法下載網路圖片。

package com.sjmp.demo01;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

/**
 * @author: sjmp1573
 * @date: 2020/11/15 20:58
 * @description:
 */

public class TestThread2 extends Thread{

//    網路圖片地址
    private String url;
//    儲存的檔名
    private String name;

    public TestThread2(String url,String name){
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
//        進入執行緒後,會建立一個下載器,下載器通過 downloader 方法,傳入 url 和 name 下載相應的資源
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下載了檔名為:"+ name);
    }

    public static void main(String[] args) {
//        這是 TestThread2 類的主方法
//        建立三個繼承 Thread 的子類
        TestThread2 test01 = new TestThread2("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");
        TestThread2 test02 = new TestThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");
        TestThread2 test03 = new TestThread2("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2044644877,1766802492&fm=15&gp=0.jpg", "test03");

//        並開啟執行緒
        test01.start();
        test02.start();
        test03.start();

    }

}

//下載器,這是一個類
class WebDownloader{
//    下載方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO異常,downloader 方法出現問題");
        }
    }
}

        <!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.8.0</version>
        </dependency>

在這裡插入圖片描述

2.2 繼承 Runnable 介面,建立 Tread 物件

繼承 Runnable 介面,建立 Tread 物件,傳入實現類,開啟 start 方法.

package com.sjmp.Thread01;

/**
 * @ClassName ThreadRunnableTest
 * @Description TODO
 * @Author sjmp1573
 * @Date DATE{TIME}
 */
public class ThreadRunnableTest {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("I am a child thread --Runnable");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        new Thread(thread).start();
        new Thread(thread).start();
    }
}

以上兩種方式的比較:

繼承 Thread 類

  • 子類繼承 Thread 類具備多執行緒能力
  • 啟動執行緒:子類物件 .start()
  • 不建議使用:避免 OOP 單繼承侷限性

實現 Runnable 介面

  • 實現介面 Runnable 具有多執行緒能力
  • 啟動執行緒:傳入目標物件+Thread物件.start()
  • 推薦使用:避免單繼承侷限性,方便同一個物件被多個執行緒使用。

火車搶票例項:

Runnable 實現多執行緒,創造一個實列 ticketRunnable ,可共享給多個執行緒。

package com.sjmp.demo01;

/**
 * @author: sjmp1573
 * @date: 2020/11/15 21:45
 * @description:
 */

// 多個執行緒同時操作同一個物件
//    買火車票的例子
//    發現問題:多個執行緒操作同一個資源,執行緒不安全,資料紊亂!

public class TicketRunnable implements Runnable{

    private int ticketNums = 10;

    @Override
    public void run() {
        while (true){
            if (ticketNums<=0){
                break;
            }
//            模擬延時
            /*
                IllegalArgumentException
                if the value of {@code millis} is negative, or the value of
                {@code nanos} is not in the range {@code 0-999999}
                InterruptedException
                if any thread has interrupted the current thread. The
                <i>interrupted status</i> of the current thread is
                cleared when this exception is thrown.
            */

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ticketNums--;
            System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketNums+"票");

        }
    }

    public static void main(String[] args) {
//        實現了 Runnable 介面的類,建立其例項
        TicketRunnable ticketRunnable = new TicketRunnable();
//        ticketRunnable 例項可用於多個執行緒,其中的資源被共享。
        new Thread(ticketRunnable,"01小明+++++").start();
        new Thread(ticketRunnable,"02老師-----").start();
        new Thread(ticketRunnable,"03黃牛=====").start();

    }
}

2.3 實現 Callable 介面(瞭解)

  1. 實現 Callable 介面,需要返回值型別
  2. 重寫 call 方法,需要丟擲異常
  3. 建立目標物件
  4. 建立執行服務:ExecutorService = Executor.newFixedThreadPool(1);
  5. 提交執行:Future result1 = ser.submit(1);
  6. 獲取結果:boolean r1 = result.get()
  7. 關閉服務:ser.shutdownNow():
package com.sjmp.demo01;

import java.util.concurrent.*;

/**
 * @author: sjmp1573
 * @date: 2020/11/15 22:16
 * @description:
 */

public class ThreadByCallable implements Callable<Boolean> {

    //    網路圖片地址
    private String url;
    //    儲存的檔名
    private String name;

    public ThreadByCallable(String url,String name){
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
        //        進入執行緒後,會建立一個下載器,下載器通過 downloader 方法,傳入 url 和 name 下載相應的資源
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url,name);
        System.out.println("下載了檔名為:"+ name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {

//        這是 TestThread2 類的主方法
//        建立三個繼承 Thread 的子類
        ThreadByCallable test01 = new ThreadByCallable("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=3796445054,4193265240&fm=26&gp=0.jpg", "test01");
        ThreadByCallable test02 = new ThreadByCallable("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1605454961548&di=c3b49cc5869f058a6cded1434ea56f85&imgtype=0&src=http%3A%2F%2Fpic1.win4000.com%2Fwallpaper%2F5%2F538ec3134b63b.jpg", "test02");
        ThreadByCallable test03 = new ThreadByCallable("https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2044644877,1766802492&fm=15&gp=0.jpg", "test03");


//        建立執行服務:
        ExecutorService service = Executors.newFixedThreadPool(3);

//       提交執行
        Future<Boolean> submit01 = (Future<Boolean>) service.submit(test01);
        Future<Boolean> submit02 = (Future<Boolean>) service.submit(test02);
        Future<Boolean> submit03 = (Future<Boolean>) service.submit(test03);

        boolean rs1 = submit01.get();
        boolean rs2 = submit02.get();
        boolean rs3 = submit03.get();

//        關閉服務
        service.shutdownNow();
    }
}


//下載器,這是一個類
class WebDownloader{
//    下載方法
    public void downloader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO異常,downloader 方法出現問題");
        }
    }
}

2.4 Lamda 表示式

Lamda 表示式屬於函數語言程式設計的概念

(paraems) -> expressionp[表示式]
(params) -> statement[語句]
(params) -> {statements}
a->System.out.println("i like lamda-->"+a);
new Thread(()->System.out.println("多執行緒學習...")).start();
  • 理解 Functional Interface(函式式介面)是學習 Java8 Lambda 表示式的關鍵所在。

  • 函式式介面的定義:

    • 任何介面,如果只包含唯一一個抽象方法,那麼它就是函式式介面。
    • 對於函式式介面,可以通過 Lamda 表示式來建立該介面的物件。

Lamda 表示式的演進:

package com.sjmp.demo02;

/**
 * @author: sjmp1573
 * @date: 2020/11/16 20:11
 * @description:
 */

public class LamdaExpression {
//    3.2 實現函式式介面的第二種方法,靜態內部類
    static class Like2 implements ILike{
    @Override
    public void lamda() {
        System.out.println("------3.2 靜態內部類實現函式式介面-----");
    }
}


    public static void main(String[] args) {

//    3.1 實現函式式介面的第一種方法
        ILike like1 = new Like1();
        like1.lamda();
        System.out.println("--3.1 普通方法實現函式式介面--");

//    3.2 實現函式式介面的第二種方法,靜態內部類
        new Like2().lamda();


//    3.3 區域性內部類實現函式式介面
        class Like3 implements ILike{
            @Override
            public void lamda() {
                System.out.println("------3.3 區域性內部類實現函式式介面--------");
            }
        }

        new Like3().lamda();

//    3.4 匿名內部類實現函式式介面

        new ILike() {
            @Override
            public void lamda() {
                System.out.println("------3.4 匿名內部類實現函式式介面----------");
            }
        }.lamda();



//      3.5 lamda 表示式實現函式式介面

        ILike like5 = ()->{
            System.out.println("--3.5 lamda 表示式實現函式式介面--");
        };
        like5.lamda();
    }

}

// 1. 定義一個函式式介面
interface ILike{
    void lamda();
}

// 2. 實現類
class Like1 implements ILike{
    @Override
    public void lamda() {

    }
}

2.5 靜態代理模式

在這裡插入圖片描述多執行緒 Thread 為代理,Runnable 為被代理物件:

package com.sjmp.demo02;

/**
 * @author: sjmp1573
 * @date: 2020/11/16 19:32
 * @description:
 */

//這是代理
public class StaticProxy implements Marry{

    private Marry you;

    public StaticProxy(You you){
        this.you = you;
    }

    public static void main(String[] args) {

//        Runnable 是被代理的物件,Thread 是代理

        /*
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("----結婚----");
                }
            }).start();
        */

//        使用 lamda 表示式
        new Thread(()->{
            System.out.println("----結婚----");
        }).start();
        

        new StaticProxy(new You()).HappyMarry();

    }

    @Override
    public void HappyMarry() {
        doBefore();
        you.HappyMarry();
        doAfter();
    }
    public static void doBefore(){
        System.out.println("-----婚前佈置------");
    }
    public static void doAfter(){
        System.out.println("-------婚後收錢-----");
    }

}

interface Marry{
    void HappyMarry();
}
//真實角色,Marry

class You implements Marry{
    @Override
    public void HappyMarry() {
        System.out.println("----- 被代理人 開始結婚------");
    }
}

3、執行緒的 5 種狀態

  1. 建立
  2. 就緒
  3. 阻塞
  4. 執行
  5. 死亡
    在這裡插入圖片描述
    在這裡插入圖片描述

3.1 執行緒的一些常用方法

執行緒的一些方法如下圖所示:

在這裡插入圖片描述

3.1.1 執行緒休眠——sleep()

  • sleep(時間)指定當前執行緒阻塞的毫秒數;
  • sleep 存在異常 InterruptedException;
  • sleep 時間達到後執行緒進入就緒狀態;
  • sleep 可以模擬網路延時,倒數計時等;
  • sleep 每一個物件都有一個鎖,sleep 不會釋放鎖;

sleep() 方法的用處

package com.sjmp.method;

import java.awt.*;
import java.text.SimpleDateFormat;
import java.util.Date;

import static java.lang.Thread.*;

/**
 * @author: sjmp1573
 * @date: 2020/11/16 21:50
 * @description:
 */

public class TestSleep {

    public static void main(String[] args) throws InterruptedException {
//        Thread.sleep()  用於倒數計時

//        tenStop();


//        列印當前系統時間
        Date date = new Date(System.currentTimeMillis());

        boolean flag = true;
        int i = 5;
        while(flag){
            if(--i<=0){
                flag = false;
            }
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date));
            date = new Date(System.currentTimeMillis());//更新時間

        }
    }

//    寫一個倒數計時的方式
    public static void tenStop() throws InterruptedException {
        int num = 10;
        while(true){
            try{
                sleep(1000);
            }catch ( InterruptedException e){
                e.printStackTrace();
            }
            if (num<=0){
                break;
            }
            System.out.println(num--);
        }
    }
}

3.1.2 執行緒禮讓——yield()

  • 禮讓執行緒,讓當前正在執行的執行緒暫停,但不阻塞;
  • 將執行緒從執行狀態轉為就緒狀態;
  • 讓 CPU 從新排程,有可能還是排程該禮讓執行緒。
package com.sjmp.method;

/**
 * @author: sjmp1573
 * @date: 2020/11/16 22:22
 * @description:
 */

public class TestYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"開啟了執行緒");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"結束了執行緒");
    }

    public static void main(String[] args) {

        TestYield testYield = new TestYield();
        Thread threadA = new Thread(testYield,"threadA");
        Thread threadB = new Thread(testYield,"threadB");

        threadA.start();
        threadB.start();
    }

}

3.1.2 合併執行緒——Join()

Join 合併執行緒,待此執行緒執行完成後,再執行其他執行緒,其他執行緒阻塞。
可以想象成插隊。

package com.sjmp.method;

/**
 * @author: sjmp1573
 * @date: 2020/11/17 20:46
 * @description:
 */

public class TestJoin implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("Thread : "+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestJoin join = new TestJoin();
        Thread thread = new Thread(join);
        thread.start();

        for (int i = 0; i < 200; i++) {
            if (i==100){
                thread.join();
            }
            System.out.println("main :"+i);
        }
    }
}

3.2 停止執行緒的方式

  • 不推薦使用 JDK 提供的 stop ()、destroy()方法。【已棄用】
  • 推薦執行緒自己停止下來
  • 建議使用一個標誌位進行終止變數 , 當 flag == false,則終止執行緒執行。
package com.sjmp.demo02;

/**
 * @author: sjmp1573
 * @date: 2020/11/16 21:33
 * @description:
 */

public class ThreadStop implements Runnable{

    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("--- ThreadStop  ---"+i);
            i++;
        }
    }
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {

        ThreadStop threadStop = new ThreadStop();
        Thread thread = new Thread(threadStop);
        thread.start();


        for (int i = 0; i < 1000; i++) {
            if (i==900){
                threadStop.stop();
            }
            System.out.println("--- main ---"+i);
        }
    }

}

3.3 執行緒狀態觀測

Thread.State
執行緒狀態。執行緒可以處於以下狀態之一:

  • NEW
    尚未啟動的執行緒處於此狀態。
  • RUNNABLE
    在 Java 虛擬機器中執行的執行緒處於此狀態。
  • BLOCKED
    被阻塞等待監視器鎖定的執行緒處於此狀態。
  • WAITING
    正在等待另一個執行緒執行特定動作的執行緒處於此狀態。
  • TERMINATED
    已退出的執行緒處於此狀態。
    一個執行緒可以在給定時間點處於一個狀態。
package com.sjmp.method;

/**
 * @author: sjmp1573
 * @date: 2020/11/17 21:10
 * @description:
 */

public class TestState{
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(()->{
            for (int i = 0; i < 20; i++) {
                try {
                    System.out.println(i);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread 執行結束了!");
        });

        Thread.State state = thread.getState();
        System.out.println(state);

        thread.start();
        System.out.println(thread.getState());

        System.out.println(" 我開始迴圈了 ");

        while(state != Thread.State.TIMED_WAITING){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            state = thread.getState();
            System.out.println(state);
        }
    }
}

3.4 執行緒優先順序

  • Java 提供一個執行緒排程器來監控程式中啟動後進入就緒狀態的所有執行緒,執行緒排程器按照優先順序決定應該排程哪個執行緒來執行。
  • 執行緒的優先順序用數字表示,範圍從1~10.
    Thread.MIN_PRIORITY=1;
    Thread.MAX_PRIORITY=10;
    Thread.NORM_PRIORITY=5;
  • 使用以下方式改變或獲取優先順序
    getPriority().setPriority(int xxx)

優先順序的設定建議在 start() 排程前

package com.sjmp.method;

/**
 * @author: sjmp1573
 * @date: 2020/11/17 21:32
 * @description:
 */

public class TestPriority implements Runnable {
    @Override
    public void run() {
        System.out.println("當前執行緒:"+Thread.currentThread().getName());

    }

    public static void main(String[] args) {

        TestPriority runnable = new TestPriority();

        Thread thread01 = new Thread(runnable,"01");
        Thread thread02 = new Thread(runnable,"02");
        Thread thread03 = new Thread(runnable,"03");
        Thread thread04 = new Thread(runnable,"04");
        Thread thread05 = new Thread(runnable,"05");

        thread01.setPriority(Thread.MAX_PRIORITY);
        thread02.setPriority(7);
        thread03.setPriority(6);
        thread04.setPriority(5);
        thread05.setPriority(4);

        thread01.start();
        thread02.start();
        thread03.start();
        thread04.start();
        thread05.start();
    }
}

3.5 守護(daemon)執行緒

  • 執行緒分為使用者執行緒守護執行緒
  • 虛擬機器必須確保使用者執行緒執行完畢
  • 虛擬機器不用等待守護執行緒執行完畢
  • 如,後臺記錄操作日誌,監控記憶體垃圾回收等待…
    在這裡插入圖片描述
package com.sjmp.method;

/**
 * @author: sjmp1573
 * @date: 2020/11/17 21:43
 * @description:
 */

public class TestsetDaemon {
    public static void main(String[] args) {
        God god = new God();
        You you = new You();

        Thread thread = new Thread(god);
        thread.setDaemon(true);

        thread.start();
        new Thread(you).start();
    }
}
class God implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("Daemoning...");
        }
    }
}

class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 365; i++) {
            System.out.println("living...");
        }
        System.out.println("game over---------------------");
    }
}

3.6 併發

同一個物件多個執行緒同時操作

在這裡插入圖片描述現實生活中,我們會遇到”同一個資源,多個人都想使用”的問題,比如,食堂排隊打飯,每個人都想吃飯,最天然的解決辦法就是,排隊一個個來。

處理多執行緒問題時,多個執行緒訪問同一個物件,並且某些執行緒還想修改這個物件.這時候我們就需要執行緒同步.執行緒同步其實就是一種等待機制,多個需要同時訪問!此物件的執行緒進入這個物件的等待池形成佇列,等待前面執行緒使用完畢,下一個執行緒再使用。
在這裡插入圖片描述
形成執行緒安全的條件:

佇列和鎖

4、執行緒同步

由於同一程式的多個執行緒共享同一塊儲存空間,在帶來方便的同時,也帶來了訪問衝突問題,為了保證資料在方法中被訪問時的正確性,在訪問時加入鎖機制synchronized,當一個執行緒獲得物件的排它鎖,獨佔資源,其他執行緒必須等待,使用後釋放鎖即可.存在以下問題:

  • 一個執行緒持有鎖會導致其他所有需要此鎖的執行緒掛起;
  • 在多執行緒競爭下,加鎖,釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題;
  • 如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能問題.

4.1 執行緒不安全舉例

舉例:不安全的售票

package com.sjmp.Concurrent;

/**
 * @author: sjmp1573
 * @date: 2020/11/17 22:03
 * @description:
 */

public class UnsafeBuyTicket {

}

class BuyTicket implements Runnable{
    private int ticketNum = 10;
    boolean  flag = true;
    @Override
    public void run() {
        while (flag){
            buy();
        }
        System.out.println("售罄");
    }
    //加關鍵字 synchronized 就可以變成執行緒安全的
    public void buy(){
        if (ticketNum<=0){
            flag = false;
            return;
        }
        try {
        //            模擬買票延時
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" get ticket:"+ticketNum);
        ticketNum--;
    }

    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        Thread thread01 = new Thread(buyTicket,"01");
        Thread thread02 = new Thread(buyTicket,"02");
        Thread thread03 = new Thread(buyTicket,"03");

        thread01.start();
        thread02.start();
        thread03.start();
    }
}

舉例:銀行取錢
程式碼省略

舉例:執行緒不安全的集合
可參考:ArrayList為什麼是執行緒不安全的:https://blog.csdn.net/qq_42183409/article/details/100586255

package com.sjmp.Concurrent;

import java.util.ArrayList;

/**
 * @author: sjmp1573
 * @date: 2020/11/17 22:22
 * @description:
 */

public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

舉例:執行緒安全的集合:CopyOnWriteArrayList

package com.sjmp.Concurrent;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @author: sjmp1573
 * @date: 2020/11/18 9:48
 * @description:
 */

public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

4.2 同步方法

  • 由於我們可以通過關鍵字 private 關鍵字來保證資料物件只能被方法訪問,所以我們只要針對方法提出一套機制,這套機制就是 synchronized 關鍵字,它包括兩種方法:
    synchronized 方法和 synchronized 塊.

同步方法:

public synchronized void method(int args){}
  • synchronized 方法控制 “物件” 的訪問,每個物件對應一把鎖,每個 synchronized 方法都必須獲得呼叫該方法的物件的鎖才能執行,否則執行緒會阻塞,方法一旦執行,就獨佔該鎖,直到該方法返回才釋放,後面被阻塞的執行緒才能獲得這個鎖,繼續執行。
    缺陷:若將一個大的方法申明為 synchronized 將會影響效率。

在這裡插入圖片描述

4.3 同步塊

同步塊:synchronized(Obj){}
Obj 稱之為同步監視器

  • Obj 可以是任何物件,但是推薦使用共享資源作為同步監視器
  • 同步方法中無需指定同步監視器,因為同步方法的同步監視器就是 this ,就是這個物件本身,或者是 class
  • 同步監視器的執行過程
  1. 第一個執行緒訪問,鎖定同步監視器,執行其中的程式碼
  2. 第二個執行緒訪問,發現同步監視器被鎖定,無法訪問
  3. 第一個執行緒訪問完畢,解鎖同步監視器
  4. 第二個執行緒訪問,發現同步監視器沒有鎖,然後鎖定並訪問

4.3 死鎖

多個執行緒各自佔有一些共享資源,並且互相等待其他執行緒佔有的資源才能執行,而導致兩個或者多個執行緒都在等待對方釋放資源,都停止執行的情形。某一個同步塊同時擁“兩個以上物件的鎖”時,就可能會發生“死鎖”的問題。

產生死鎖的四個必要條件:

  1. 互斥條件:一個資源每次只能被一個程式使用。
  2. 請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:程式已獲得的資源,在未使用完之前,不能強行剝奪。
  4. 迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係。

上述四個條件,只要破壞其任意一個就可避免死鎖的發生。

4.4 Lock(鎖)

  • 從 JDK 5.0 開始,Java 提供了更強大的執行緒同步機制——通過顯示定義同步鎖物件來實現同步。同步鎖使用 Lock物件充當
  • java.util.concurrent.locks.Lock 介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對 Lock 物件加鎖,執行緒開始訪問共享資源之前應先獲得 Lock 物件
  • ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是 ReentrantLock ,可以顯示加鎖、釋放鎖。

synchronized 與 Lock 的對比

  • Lock 是顯示鎖(手動開啟和關閉),synchronized 是隱式鎖,出了作用域自動釋放
  • Lock 只有程式碼加鎖,synchronized 有程式碼塊鎖和方法鎖
  • 使用 Lock 鎖,JVM 將花費較少的時間來排程執行緒,效能更好。並具有更好的擴充套件性(提供更多的子類)
  • Lock > 同步程式碼塊(已經進入了方法體,分配了相應資源)>同步方法(在方法體之外)
package com.sjmp.Concurrent;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: sjmp1573
 * @date: 2020/11/18 16:52
 * @description:
 */

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

        Ticket ticket = new Ticket();

        new Thread(ticket).start();
        new Thread(ticket).start();
        new Thread(ticket).start();

    }

}


class Ticket extends Thread{
    private int ticketNums = 10;
//    定義 lock 鎖
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while(true){
            try{
                lock.lock();
                if (ticketNums>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticketNums--);

                }else{
                    break;
                }
            }finally {
                lock.unlock();
            }
        }
    }
}

5.執行緒通訊

應用場景:生產者和消費者問題

  • 假設倉庫中只能存放一件產品,生產者將生產出來的產品放入倉庫,消費者將倉庫中產品取走消費。

  • 如果倉庫中沒有產品,則將生產者將產品放入倉庫,否則停止生產並等待,直到倉庫中的產品被消費者取走為止。

  • 如果倉庫中放有產品,則消費者可以將產品取走消費,否則停止消費,直到倉庫中再次放入產品為止。

在這裡插入圖片描述這是一個執行緒同步問題,生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互為條件。

  • 對於生產者,沒有生產產品之前,要通知消費者等待.而生產了產品之後,又需要馬上通知消費者消費
  • 對於消費者,在消費之後,要通知生產者已經結束消費,需要生產新的產品以供消費.
  • 在生產者消費者問題中,僅有synchronized是不夠的
    synchronized 可阻止併發更新同一個共享資源,實現了同步
    synchronized 不能用來實現不同執行緒之間的訊息傳遞(通訊)

5.1 解決執行緒之間通訊問題的幾個方法

注意:均是 Object 類的方法,都只能在同步方法或者同步程式碼塊中使用,否則會丟擲異常 llegalMonitorStateException

在這裡插入圖片描述sleep 與 wait 的區別可參考連結:https://www.xuexila.com/baikezhishi/537124.html

5.2 解決執行緒之間通訊的方式1:管程法

併發寫作模型“生產者/消費者模式”–>管程法

  • 生產者:負責生產資料的模組(可能是方法,物件,執行緒,程式);
  • 消費者:負責處理資料的模組(可能是方法,物件,執行緒,程式)
  • 緩衝區:消費者不能直接使用生產者的資料,他們之間有個緩衝區

生產者將生產好的資料放入緩衝區,消費者從緩衝區拿出資料

在這裡插入圖片描述

package com.sjmp.advanced;

/**
 * @author: sjmp1573
 * @date: 2020/11/18 20:52
 * @description:
 */

// 生產者,消費者,產品,緩衝區
public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();

        new Productor(container).start();
        new Consumer(container).start();
    }

}

// 生產者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生產了"+i+"只雞");
            container.push(new Chicken(i));
        }
    }
}


class SynContainer{
//    需要一個容器的大小
    Chicken[] chickens = new Chicken[10];
//    容器計數器
    int count = 0;


//    生產者放入產品
    public synchronized void push(Chicken chicken){
//        如果容器滿了,就需要等待消費者消費
        if (count == chickens.length){
//            通知消費者消費,生產等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        chickens[count] = chicken;
        count++;
        this.notifyAll();
    }


//    消費者消費產品
    public synchronized Chicken pop(){
//        判斷能否消費
        if(count==0){
//            等待生產者生產,消費者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
//        如果可以消費
        count--;
        Chicken chicken = chickens[count];
//        可以通知消費了
        this.notifyAll();

        return chicken;
    }
}



class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消費了-->"+container.pop().id+"只雞");
        }
    }
}



// 產品
class Chicken{
    int id;  //產品編號
    public Chicken(int id){
        this.id = id;
    }
}

5.3 解決執行緒之間通訊的方式1:訊號燈法

package com.sjmp.advanced;

/**
 * @author: sjmp1573
 * @date: 2020/11/18 21:34
 * @description:
 */

public class TestPC2 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Wathcher(tv).start();
    }
}

//生產者--演員
class Player extends Thread{
    TV tv;
    public Player(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2==0){
                this.tv.play("快樂大本營");
            }else{
                this.tv.play("天天向上");
            }
        }
    }
}

//觀眾
class Wathcher extends Thread{
    TV tv;
    public Wathcher(TV tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//產品--節目
class TV{
//    演員表演,觀眾等待  T
//    觀眾觀看,演員等待  F
    String voice;  // 表演節目
    boolean flag = true;

//    表演
    public synchronized void play(String voice){
        if(!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演員表演了: "+voice);
//        通知觀眾觀看
        this.notifyAll();// 通知喚醒
        this.voice = voice;
        this.flag = !flag;
    }

//    觀看
    public synchronized void watch(){
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("觀看了: "+voice);
//        通知演員表演
        this.notifyAll();
        this.flag = !this.flag;
    }
}

5.4 使用執行緒池

背景:經常建立和銷燬、使用量特別大的資源,比如併發情況下的執行緒,對效能影響很大。
思路:提前建立好多個執行緒,放入執行緒池中,使用時直接獲取,使用完放回池中。
可以避免頻繁建立銷燬、實現重複利用。類似生活中的公共交通工具。

優點:
提高響應速度(減少了建立新執行緒的時間)
降低資源消耗(重複利用執行緒池中執行緒,不需要每次都建立)
便於執行緒管理…

  • corePoolSize:核心池的大小
  • maximumPoolSize:最大執行緒數
  • keepAliveTime:執行緒沒有任務時最多保持多長時間會終止
package com.sjmp.advanced;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: sjmp1573
 * @date: 2020/11/18 21:53
 * @description:
 */

public class TestPool {
    public static void main(String[] args) {
//        1.建立服務,建立執行緒池
        ExecutorService service = Executors.newFixedThreadPool(10);
//        newFixedThreadPool 引數為執行緒池大小
//        執行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

//        2.關閉連線
        service.shutdown();

    }
}
class MyThread implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

補充內容:

package com.sjmp.Thread01;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @ClassName ThreadFutureTest
 * @Description TODO
 * @Author sjmp1573
 * @Date DATE{TIME}
 */
public class ThreadFutureTest {
    public static class CallerTask implements Callable<String>{
        @Override
        public String call() throws Exception {
            return "Thread-Callable- hello";
        }
    }

    public static void main(String[] args) {
        // 建立非同步任務
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
        new Thread(futureTask).start();
        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如上程式碼中的 CallerTask 類實現了 Callable 介面的 call() 方法。在 main 函式內首先建立了一個 FutrueTask 物件(建構函式為 CallerTask 的例項),然後使用建立的 FutrueTask 物件作為任務建立了一個執行緒並且啟動它,最後通過futureTask.get() 等待任務執行完畢並返回結果。

小結:使用繼承方式的好處是方便傳參,你可以在子類裡面新增成員變數,通過 set 方法設定引數或者通過建構函式進行傳遞,而如果使用 Runnable 方式,則只能使用主執行緒裡面被宣告為 final 的變數。不好的地方是 Java 不支援多繼承,如果繼承了 Thread 類,那麼子類不能再繼承其他類,而 Runable 則沒有這個限制。前兩種方式都沒辦法拿到任務的返回結果,但是 Futuretask 方式可以。

相關文章