天天看點

JavaSE學習筆記(十四)多線程多線程

點選這裡檢視更多JavaSE的筆記

如果對多線程很感興趣,可以提前看下進階知識 --> JavaSE學習筆記(十八)JUC并發程式設計

章節目錄

  • 多線程
      • 1、Process(程序)與Thread(線程)
      • 2、線程、程序、多線程核心概念
      • 3、建立線程
          • 3.1、第一種方式:繼承Thread類(重點)
          • 3.2、第二種方式:實作Runnable接口(重點)
          • 3.3、小結
          • 3.4、示例:多個線程同時操作同一個對象
          • 3.5、示例:龜兔賽跑
          • 3.6、第三種方式:實作Callable接口(了解即可)
      • 4、靜态代理
      • 5、Lamda表達式
      • 6、線程停止
      • 7、線程休眠 sleep
      • 8、線程禮讓 yield
      • 9、線程強制執行 join
      • 10、線程狀态 Thead.State
      • 11、線程優先級 Priority
      • 12、守護線程 daemon
      • 13、線程同步(多個線程操作同一個資源)
      • 14、三大不安全案例
      • 15、同步方法及同步塊
      • 16、測試JUC安全類型的集合(JUC裡有一個關于list安全的集合:CopyOnWriteArrayList)
      • 17、死鎖
      • 18、Lock(鎖)
      • 19、生産者消費者問題
          • 19.1、解決方式1
          • 19.2、解決方式2
      • 20、線程池
    • 總結

多線程

JavaSE學習筆記(十四)多線程多線程

1、Process(程序)與Thread(線程)

  • 說起程序,就不得不說下程式。程式是指令和資料的有序集合,其本身沒有任何運作的含義,是一個靜态的概念
  • 而程序則是執行程式的一次執行過程,它是一個動态的概念。是系統資源配置設定的機關
  • 通常在一個程序中可以包含若幹個線程,當然一個程序中至少有一個線程,不然沒有存在的意義。線程是CPU排程和執行的機關
  • 注意:
    • 很多多線程是模拟出來的,真正的多線程是指有多個CPU,即多核,如伺服器。如果是模拟出來的多線程,即在一個CPU的情況下,在同一個時間點,CPU隻能執行一個代碼,因為切換的很快,是以就有同時執行的錯覺。

2、線程、程序、多線程核心概念

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

3、建立線程

3.1、第一種方式:繼承Thread類(重點)
JavaSE學習筆記(十四)多線程多線程
package com.object.thread;

//建立線程方式一:繼承Thread類,重寫run()方法,調用start開啟線程
//總結:注意,線程開啟不一定立即執行,由CPU排程執行
public class ThreadDemo01 extends Thread{
    @Override
    public void run() {
        //run方法線程體
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代碼--" + i);
        }
    }

    public static void main(String[] args) {
        //main線程,主線程
        //建立一個線程對象
        ThreadDemo01 td = new ThreadDemo01();
        //td.run(); 正常調用run方法的話是按代碼順序執行
        //調用start()方法開啟線程
        td.start(); //使用start開啟線程将會多個線程交替執行

        for (int i = 0; i < 1000; i++) {
            System.out.println("我在寫代碼---" + i);
        }
    }
}
           

示例:網圖下載下傳(這裡用到了一個java工具類庫commons-io)

package com.object.thread;

import org.apache.commons.io.FileUtils;

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

//練習Thread,實作多線程同步下載下傳圖檔
public class ThreadDemo02 extends Thread{

    private String url;
    private String name;

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

    //下載下傳圖檔線程執行體
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("成功下載下傳檔案:" + name);
    }

    public static void main(String[] args) {
        ThreadDemo02 t1 = new ThreadDemo02("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案1.jpg");
        ThreadDemo02 t2 = new ThreadDemo02("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案2.jpg");
        ThreadDemo02 t3 = new ThreadDemo02("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案3.jpg");

        t1.start();
        t2.start();
        t3.start();
        //這三個線程看似是t1\t2\t3的順序運作,實際上并不是按順序的,随機運作的
    }
}

//下載下傳器
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方法出現問題");
        }
    }
}
           
3.2、第二種方式:實作Runnable接口(重點)
JavaSE學習筆記(十四)多線程多線程
package com.object.runnable;

//建立線程方式2:實作runnable接口,重寫run方法,執行線程需要丢入runnable接口實作類,調用start方法

public class RunnableDemo01 implements Runnable{
    @Override
    public void run() {
        //run方法線程體
        for (int i = 0; i < 20; i++) {
            System.out.println("我在看代碼--" + i);
        }
    }

    public static void main(String[] args) {
        //建立Runnable接口的實作類對象
        RunnableDemo01 runnableDemo01 = new RunnableDemo01();

        //建立線程對象,通過線程對象來開啟我們的線程、代理
        //Thread thread = new Thread(runnableDemo01);
        //thread.start();
        new Thread(runnableDemo01).start();


        for (int i = 0; i < 1000; i++) {
            System.out.println("我在寫代碼---" + i);
        }
    }
}
           

示例:網圖下載下傳(這裡用到了一個java工具類庫commons-io)

package com.object.runnable;

import org.apache.commons.io.FileUtils;

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

public class RunnableDemo02 implements Runnable{

    private String url;
    private String name;

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

    //下載下傳圖檔線程執行體
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("成功下載下傳檔案:" + name);
    }

    public static void main(String[] args) {


        RunnableDemo02 t1 = new RunnableDemo02("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案1.jpg");
        RunnableDemo02 t2 = new RunnableDemo02("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案2.jpg");
        RunnableDemo02 t3 = new RunnableDemo02("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案3.jpg");

        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
        //這三個線程看似是t1\t2\t3的順序運作,實際上并不是按順序的,随機運作的
    }
}

//下載下傳器
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方法出現問題");
        }
    }
}
           
3.3、小結
  • 繼承Thread類
    • 子類繼承Thread類具備多線程能力
    • 啟動線程:子類對象.start()
    • 不建議使用:存在OOP單繼承局限性
  • 實作Runnable接口
    • 實作接口Runnable具有多線程能力
    • 啟動線程:傳入目标對象+Thread對象.start()
    • 推薦使用:可以避免單繼承局限性,靈活友善,友善同一個對象被多個線程使用
//一份資源
StartThread station = new StartThread4();

//多個代理
new Thread(station, "小明").start();
new Thread(station, "小紅").start();
new Thread(station, "小吳").start();
           
3.4、示例:多個線程同時操作同一個對象
package com.object.runnable;

//多個線程同時操作同一個對象
//買火車票的例子

//發現問題:多個線程操作同一個資源的情況下,線程不安全,資料紊亂。這裡就涉及到線程并發的問題
public class RunnableDemo03 implements Runnable{
    //票數
    private int ticketNums = 10;

    @Override
    public void run() {
        while (true) {
            if (ticketNums <= 0) {
                break;  //沒有票時結束
            }
            try {
                Thread.sleep(200);  //通過線程睡眠的方式确實能減少搶到同張票的次數,但還是沒能解決
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "--> 拿到了第" + ticketNums-- + "張票");
            //Thread裡的currentTread方法能擷取這個線程的資訊,比如姓名
        }
    }

    public static void main(String[] args) {
        RunnableDemo03 ticket = new RunnableDemo03();
        new Thread(ticket, "小吳").start();   //這裡給線程傳了一個名字
        new Thread(ticket, "老師").start();
        new Thread(ticket, "黃牛黨").start();
    }
}
           
3.5、示例:龜兔賽跑
JavaSE學習筆記(十四)多線程多線程
package com.object.runnable;

//模拟龜兔賽跑
public class Race implements Runnable{

    //勝利者
    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            //模拟兔子休息
            if (Thread.currentThread().getName().equals("兔子") && i%10==0) {
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //擷取比賽是否結束情況
            boolean flag = gameOver(i);
            //如果比賽結束,就停止程式
            if (flag) {
                break;
            }

            System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步");
        }
    }

    //判斷是否完成比賽
    private boolean gameOver(int steps){
        //判斷是否存在勝利者
        if (winner != null) {   //已存在勝利者
            return true;
        } else{
            if (steps >= 100) {
                winner = Thread.currentThread().getName();
                System.out.println("winner is" + winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Race race = new Race();
        new Thread(race, "兔子").start();
        new Thread(race, "烏龜").start();
    }
}
           
3.6、第三種方式:實作Callable接口(了解即可)

1、實作Callable接口,需要傳回值類型

2、重寫call方法,需要抛出異常

3、建立目标對象

4、建立執行服務:ExecutorService ser = Executors.newFixedThreadPool(1);

5、送出執行:Futureresult1 = ser.submit(t1);

6、擷取結果:boolean r1 = result1.get();

7、關閉服務:ser.shutdownNow();

示例:網圖下載下傳(這裡用到了一個java工具類庫commons-io)

package com.object.callable;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

//線程建立方式三:實作Callable接口
/*
    Callable的好處:
        1、可以定義傳回值
        2、可以抛出異常
 */
public class CallableDemo01 implements Callable<Boolean> {
    private String url;
    private String name;

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

    //下載下傳圖檔線程執行體
    @Override
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("成功下載下傳檔案:" + name);
        return true;
    }

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


        CallableDemo01 c1 = new CallableDemo01("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案1.jpg");
        CallableDemo01 c2 = new CallableDemo01("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案2.jpg");
        CallableDemo01 c3 = new CallableDemo01("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20201130170324506.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDE3NjM5Mw%3D%3D%2Csize_16%2Ccolor_FFFFFF%2Ct_70&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1659801398&t=295178de5fdf10b068707c41fa1a99c2", "檔案3.jpg");

        //建立執行服務
        ExecutorService ser = Executors.newFixedThreadPool(3);  //建立一個池子,裝線程,這裡需要輸入一下線程數

        //送出執行
        Future<Boolean> r1 = ser.submit(c1);
        Future<Boolean> r2 = ser.submit(c2);
        Future<Boolean> r3 = ser.submit(c3);

        //擷取結果
        System.out.println(r1.get());
        System.out.println(r2.get());
        System.out.println(r3.get());

        //關閉服務
        ser.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方法出現問題");
        }
    }
}
           

4、靜态代理

JavaSE學習筆記(十四)多線程多線程
package com.object.thread;

/*
    靜态代理模式總結:
        真實對象和代理對象都要實作同一個接口
        代理對象要代理真實角色
        好處:
            代理對象可以做很多真實對象做不了的事情
            真實對象專注做自己的事情
 */
public class ThreadDemo03 {
    public static void main(String[] args) {
        new WeddingCompany(new You()).HappyMarry();
    }
}

interface Marry{
    //人間四大喜事
        //久旱逢甘霖
        //他鄉遇故知
        //洞房花燭夜
        //金榜題名時
    void HappyMarry();
}

//真實角色,你去結婚
class You implements Marry{
    @Override
    public void HappyMarry() {
        System.out.println("我要結婚啦");
    }
}

//代理角色,幫助你結婚
class WeddingCompany implements Marry{
    //代理誰?-->真實目标角色
    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        before();
        this.target.HappyMarry();//這就是真是對象
        after();
    }

    private void after() {
        System.out.println("結婚之前,布置場地");
    }

    private void before() {
        System.out.println("結婚之後,收尾款");
    }
}
           

5、Lamda表達式

JavaSE學習筆記(十四)多線程多線程
JavaSE學習筆記(十四)多線程多線程
JavaSE學習筆記(十四)多線程多線程

示例1:

package com.lambda;

public class LambdaDemo01 {

    //3、靜态内部類
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("I like lambda2");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        like = new Like2();
        like.lambda();

        //4、局部内部類
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("I like lambda3");
            }
        }
        like = new Like3();
        like.lambda();

        //5、匿名内部類,沒有類的名稱,必須借助接口或者父類
        like = new ILike(){
            @Override
            public void lambda() {
                System.out.println("I lile lambda4");
            }
        };
        like.lambda();

        //6、使用lambda表達式簡化
        like = ()->{
            System.out.println("I like lambda5");
        };
        like.lambda();
    }
}

//1、定義一個函數式接口
interface ILike{
    void lambda();
}

//2、實作類
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("I like lambda!");
    }
}
           

示例2:

package com.lambda;

public class LambdaDemo02 {


    public static void main(String[] args) {

        //1、lambda表達式簡化
//        Ilove love = (int a, int b)->{
//            System.out.println("I love you" + a);
//        };
        //簡化1:去掉參數類型
//        love = (a, b) ->{
//            System.out.println("I love you" + a);
//        };
        //簡化2:去掉括号(隻有一個參數的時候才能去掉小括号)
//        love = a -> {
//            System.out.println("I love you" + a);
//        };

        //簡化3:去掉花括号
        Ilove love = (a, b) -> System.out.println("I love you" + a + b);

        //總結:
            //lambda表達式隻能在隻有一行代碼的情況下才能簡化成為一行,如果有多行,那麼就用代碼塊包裹
            //前提是接口為函數式接口
        //多個參數也可以去掉參數類型,要去掉就都去掉

        love.love(521, 50);
    }
}

interface Ilove{
    void love(int a, int b);
//    void love2(int b);    接口内隻能有一個方法,不然lambda表達式會儲存
}
           

6、線程停止

JavaSE學習筆記(十四)多線程多線程
JavaSE學習筆記(十四)多線程多線程
package com.thread;

/*
    測試stop
    1、建議線程正常停止--->利用次數,不建議死循環
    2、建議使用标志位--->設定一個标志位
    3、不要使用stop或者destroy等過時或者JDK不建議使用的方法
 */
public class StopDemo01 implements Runnable{

    //1、線程中定義線程體使用的辨別
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;

        //2、線程體使用該辨別
        while (flag) {
            System.out.println("run ... Thread" + i++);
        }
    }

    //3、設定一個公開的方法停止線程,轉換标志位
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        StopDemo01 stopDemo01 = new StopDemo01();
        new Thread(stopDemo01).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main" + i);
            if (i == 900) {
                //4、調用stop方法切換标志位,讓線程停止
                stopDemo01.stop();
                System.out.println("線程在此停止");
            }
        }
    }
}
           
JavaSE學習筆記(十四)多線程多線程

7、線程休眠 sleep

  • sleep(時間)指定目前線程阻塞的毫秒數
  • sleep存在異常InterruptedException
  • sleep時間達到後線程進入就緒狀态
  • sleep可以模拟網絡延時,倒計時等
  • 每一個對象都有一個鎖,sleep不會釋放鎖

示例:搶票休眠事件,放大問題

package com.thread;

//模拟網絡延時:放大問題的發生性(出現對個人同時搶到一張票的情況)
public class SleepDemo01 implements Runnable{
    //票數
    private int ticketNums = 10;

    @Override
    public void run() {
        while (true) {
            if (ticketNums <= 0) {
                break;  //沒有票時結束
            }
            try {
                Thread.sleep(200);  //通過線程睡眠的方式确實能減少搶到同張票的次數,但還是沒能解決
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "--> 拿到了第" + ticketNums-- + "張票");
            //Thread裡的currentTread方法能擷取這個線程的資訊,比如姓名
        }
    }

    public static void main(String[] args) {
        SleepDemo01 ticket = new SleepDemo01();
        new Thread(ticket, "小吳").start();   //這裡給線程傳了一個名字
        new Thread(ticket, "老師").start();
        new Thread(ticket, "黃牛黨").start();
    }
}
           

示例:使用sleep實作倒計時

package com.thread;

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

public class SleepDemo02 {
    public static void main(String[] args) throws InterruptedException {
        //tenDown();    十秒倒計時

        //列印倒計時
        Date date = new Date(System.currentTimeMillis());//擷取系統目前時間
        while (true) {
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("HH:mm:ss").format(date));//轉換時間格式
            date = new Date(System.currentTimeMillis());//更新目前時間
        }
    }

    //模拟倒計時
    public static void tenDown() throws InterruptedException {
        int num = 10;
        while (true) {
            Thread.sleep(1000);
            System.out.println(num--);
            if (num == 0) {
                break;
            }
        }
    }
}
           

8、線程禮讓 yield

  • 禮讓線程,讓目前正在執行的線程暫停,但不阻塞
  • 将線程從運作狀态轉為就緒狀态
  • 讓cpu重新排程,禮讓不一定成功!看CPU心情
package com.thread;

//測試禮讓線程
//禮讓不一定成功,看CPU心情
public class YieldDemo01 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) {
        YieldDemo01 yield = new YieldDemo01();

        new Thread(yield, "a").start();
        new Thread(yield, "b").start();
    }
}
           

9、線程強制執行 join

  • jion合并線程,待此線程執行完畢後,再執行其他線程,其他線程阻塞
  • 可以想象成插隊
package com.thread;

//測試join方法
//可以了解為插隊
public class JoinDemo01 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 500; i++) {
            System.out.println("vip程序來了" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //啟動我們的線程
        JoinDemo01 joinDemo01 = new JoinDemo01();
        Thread thread = new Thread(joinDemo01);
        thread.start();

        //主線程
        for (int i = 0; i < 200; i++) {
            System.out.println("main" + i);
            if (i == 100) {
                thread.join();//main線程阻塞
            }
        }
    }
}
           

10、線程狀态 Thead.State

  • NEW 尚未啟動的線程處于此狀态
  • RUNNABLE 在java虛拟機中執行的線程處于此狀态
  • BLOCKED 被阻塞等待螢幕鎖定的線程處于此狀态
  • WAITING 正在等待另一個線程執行特定動作的線程處于此狀态
  • TIMED_WAITING 正在等待另一個線程執行動作達到指定等待時間的線程處于此狀态
  • TERMINATED 已退出的線程處于此狀态

一個線程可以在給定時間點處于一個狀态。這些狀态是不反映任何作業系統線程狀态的虛拟機狀态

package com.thread;

public class StateDemo01 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread( ()->{
            for (int i = 0; i < 10; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("/");
        });

        //觀察狀态
        Thread.State state = thread.getState();
        System.out.println(state);  //NEW

        //觀察啟動後
        thread.start(); //啟動線程
        state = thread.getState();
        System.out.println(state);  //RUNNABLE

        while (state != Thread.State.TERMINATED) {  //隻要線程不終止,就一直輸出狀态
            Thread.sleep(100);  //增加延遲不讓他輸出那麼快
            state = thread.getState();  //更新線程狀态
            System.out.println(state);  //輸出狀态
        }

        //thread.start(); 死亡之後的線程無法再啟動
    }
}
           

11、線程優先級 Priority

  • java提供一個線程排程器來監控程式中啟動後進入就緒狀态的所有線程,線程排程器按照優先級決定應該排程哪個線程來執行
  • 線程的優先級用數字表示,範圍從1~10
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10
    • Thread.NORM_PRIORITY = 5
  • 使用以下方式改變或擷取優先級
    • getPriority() setPriority(int xxx)
  • 優先級的設定建議在start()排程前
  • 優先級低隻是意味着獲得排程的機率低,并不是優先級低就不會被調用了,這都是看CPU的排程
package com.thread;

public class PriorityDemo01 {
    public static void main(String[] args) {
        //主線程預設優先級
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());

        MyPriority myPriority = new MyPriority();

        Thread t1 = new Thread(myPriority);
        Thread t2 = new Thread(myPriority);
        Thread t3 = new Thread(myPriority);
        Thread t4 = new Thread(myPriority);
        Thread t5 = new Thread(myPriority);
        Thread t6 = new Thread(myPriority);
        Thread t7 = new Thread(myPriority);

        //先設定優先級,再啟動
        t1.start();

        t2.setPriority(1);
        t2.start();

        t3.setPriority(5);
        t3.start();

        //t4.setPriority(-1);   //給的參數必須在1~10,超出這個範圍就會報錯IllegalArgumentException

        t4.setPriority(Thread.MAX_PRIORITY);    //Thread.MAX_PRIORITY是10
        t4.start();

        t5.setPriority(Thread.MIN_PRIORITY);    //Thread.MIN_PRIORITY是1
        t5.start();

        t6.setPriority(Thread.NORM_PRIORITY);   //Thread.NORM_PRIORITY是5
        t6.start();

        t7.setPriority(3);
        t7.start();
    }

    static class MyPriority implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
        }
    }
}
           

12、守護線程 daemon

  • 線程分為使用者線程和守護線程
  • 虛拟機必須確定使用者線程執行完畢;使用者線程比如:main()
  • 虛拟機不用等待守護線程執行完畢;守護線程比如:gc()
  • 如:背景記錄記錄檔、監控記憶體、垃圾回收等
package com.thread;

//測試守護程序
//上帝守護你
public class DaemonDemo01 {
    public static void main(String[] args) {
        God god = new God();

        Thread threadGod = new Thread(god);
        threadGod.setDaemon(true);  //預設是false表示使用者線程,正常的線程都是使用者線程...修改為true表示更改為守護線程
        threadGod.start();  //上帝線程啟動    守護線程啟動...
        //JVM無需等待守護線程執行完成就可以關閉

        new Thread(new You()).start();  //你線程啟動 使用者線程啟動...
    }

}

//你
class You implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 35; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("我還活着呀" + i);
        }
        System.out.println("======Goodbye World!========");
    }
}

//上帝
class God implements Runnable{
    @Override
    public void run() {
        System.out.println("上帝守護着你");
    }
}
           

13、線程同步(多個線程操作同一個資源)

  • 并發:同一個對象被多個線程同時操作
  • 處理多線程問題時,多個線程通路同一個對象,并且某些線程還想修改這個對象。這時候我們就需要線程同步,線程同步其實就是一個等待機制,多個需要同時通路此對象的線程進入這個對象的等待池形成隊列,等待前面線程使用完畢,下一個線程再使用。
  • 線程同步形成條件:隊列 + 鎖
  • 由于同一程序的多個線程共享同一塊存儲空間,在帶來友善的同僚,也帶來了通路沖突的問題,為了保障資料在方法中被通路時的正确性,在通路中加入鎖機制synchronized,當一個線程獲得對象的排它鎖,獨占資源,其他線程必須等待,使用後釋放鎖即可,存在以下問題:
    • 一個線程持有鎖會導緻其他所需要此鎖的線程挂起
    • 在多線程競争下,加鎖,釋放鎖會導緻比較多的上下文切換 和 排程延時,引起性能問題
    • 如果一個優先級高的線程等待一個優先級低的線程釋放鎖 會導緻優先級倒置,引起性能問題

14、三大不安全案例

  • 案例一:搶票
package com.unsafe;

//不安全的買票
//線程不安全,存在同時搶到一張票和負票的問題
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();

        new Thread(buyTicket, "小吳").start();
        new Thread(buyTicket, "老師").start();
        new Thread(buyTicket, "黃牛黨").start();
    }
}

class BuyTicket implements Runnable{
    private int ticket = 10;    //票數
    boolean flag = true;    //外部停止

    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //搶票
    private void buy() throws InterruptedException {
        //判斷是否有票
        if (ticket <= 0) {
            flag = false;
            return;
        }

        //模拟搶票延時,放大問題發生可能性
        Thread.sleep(3000);

        //搶票
        System.out.println(Thread.currentThread().getName() + "搶到第" + ticket-- + "張票");
    }
}
           
  • 案例二:取錢
package com.unsafe;

//模拟銀行取錢
//存在餘額負數情況,沒有餘額了還能取錢
public class UnsafeBank {
    public static void main(String[] args) {
        Account getmarriedFund = new Account(1000000, "結婚基金");

        new Bank(getmarriedFund, 500000, "你").start();
        new Bank(getmarriedFund, 1000000, "girlFriend").start();
    }
}

//賬戶
class Account{
    private int money;  //賬戶金額
    private String name;    //賬戶名稱
    
    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

//模拟銀行取款
class Bank extends Thread {
    private Account account; //賬戶
    private int nowMoney;   //手上有多少錢
    private int drawingMoney;   //取多少錢

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }

    public int getNowMoney() {
        return nowMoney;
    }

    public void setNowMoney(int nowMoney) {
        this.nowMoney = nowMoney;
    }

    public int getDrawingMoney() {
        return drawingMoney;
    }

    public void setDrawingMoney(int drawingMoney) {
        this.drawingMoney = drawingMoney;
    }

    public Bank(Account account, int drawingMoney, String name) {
        this.account = account;
        this.drawingMoney = drawingMoney;
        super.setName(name);
    }

    //取錢
    @Override
    public void run() {
        try {
            DrawMoney();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //取錢方法
    private void DrawMoney() throws InterruptedException {
        if ( (account.getMoney() - getDrawingMoney()) < 0) {
            System.out.println(Thread.currentThread().getName() + "取錢失敗" + account.getName() + "賬戶餘額不足");
            return;
        }

        //模拟延遲:放大問題發生性
        Thread.sleep(3000);

        //餘額變換 = 賬戶餘額-要取的錢
        account.setMoney(account.getMoney() - getDrawingMoney());

        //手上有多少錢
        setNowMoney(getNowMoney() + getDrawingMoney());

        System.out.println(Thread.currentThread().getName() + "取錢成功,目前" + account.getName() + "剩餘餘額:" + account.getMoney());
        System.out.println(Thread.currentThread().getName() + "手上拿着" + getNowMoney() + "元錢!");
        System.out.println(account.getName() + "目前賬戶餘額" + account.getMoney());
    }
}
           
  • 案例三:線程不安全的集合
package com.unsafe;

import java.util.ArrayList;

//線程不安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            new Thread( ()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}
           

15、同步方法及同步塊

  • 由于我們可以通過private關鍵字來保證資料對象隻能被方法通路,是以我們隻需要針對方法提出一套機制,這套機制就是synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊
  • 同步方法:
  • synchronized方法控制對“對象”的通路,每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則線程會阻塞,方法一旦執行,就會獨占該鎖,直到該方法傳回才釋放鎖,後面被阻塞的線程才能獲得這個鎖,繼續執行
  • 缺陷:若将一個大的方法申明為synchronized将會影響效率
    JavaSE學習筆記(十四)多線程多線程
  • 同步塊:synchronized(Obj){}
  • Obj稱之為同步螢幕
    • Obj可以是任何對象,但是推薦使用共享資源作為同步螢幕
    • 同步方法中無需指定同步螢幕,因為同步方法的同步螢幕就是this,就是這個對象本身,或者是class【反射中講解】
  • 同步螢幕的執行過程
    • 1、第一個線程通路,鎖定同步螢幕,執行其中的代碼
    • 2、第二個線程通路,發現同步螢幕被鎖定,無法通路
    • 3、第一個線程通路完畢,解鎖同步螢幕
    • 4、第二個線程通路,發現同步螢幕沒有鎖,然後鎖定并通路
  • 案例一:搶票(增加同步鎖)
package com.unsafe;

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

        new Thread(buyTicket, "小吳").start();
        new Thread(buyTicket, "老師").start();
        new Thread(buyTicket, "黃牛黨").start();
    }
}

class BuyTicket implements Runnable{
    private int ticket = 10;    //票數
    boolean flag = true;    //外部停止

    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //搶票
    //synchronized同步方法,鎖的是this
    private synchronized void buy() throws InterruptedException {
        //判斷是否有票
        if (ticket <= 0) {
            flag = false;
            return;
        }

        //模拟搶票延時,放大問題發生可能性
        Thread.sleep(3000);

        //搶票
        System.out.println(Thread.currentThread().getName() + "搶到第" + ticket-- + "張票");
    }
}
           
  • 案例二:取錢(增加同步鎖)
package com.unsafe;

//模拟銀行取錢
public class UnsafeBank {
    public static void main(String[] args) {
        Account getmarriedFund = new Account(10000000, "結婚基金");

        new Bank(getmarriedFund, 500000, "你").start();
        new Bank(getmarriedFund, 1000000, "girlFriend").start();
    }
}

//賬戶
class Account{
    private int money;  //賬戶金額
    private String name;    //賬戶名稱

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

//模拟銀行取款
class Bank extends Thread {
    private Account account; //賬戶
    private int nowMoney;   //手上有多少錢
    private int drawingMoney;   //取多少錢

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }

    public int getNowMoney() {
        return nowMoney;
    }

    public void setNowMoney(int nowMoney) {
        this.nowMoney = nowMoney;
    }

    public int getDrawingMoney() {
        return drawingMoney;
    }

    public void setDrawingMoney(int drawingMoney) {
        this.drawingMoney = drawingMoney;
    }

    public Bank(Account account, int drawingMoney, String name) {
        this.account = account;
        this.drawingMoney = drawingMoney;
        super.setName(name);
    }

    //取錢
    @Override
    public void run() {
        /*
            使用synchronized塊來鎖
            鎖的對象是變化的量,需要增删改的對象
            預設在方法前面添加synchronized鎖的對象是this,顯然在現在這種情況鎖this并不能保障資料安全,隻能使用synchronized塊來自定義鎖account
         */
        synchronized (account) {
            try {
                DrawMoney();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //取錢方法
    private void DrawMoney() throws InterruptedException {
        if ( (account.getMoney() - getDrawingMoney()) < 0) {
            System.out.println(Thread.currentThread().getName() + "取錢失敗" + account.getName() + "賬戶餘額不足");
            return;
        }

        //模拟延遲:放大問題發生性
        Thread.sleep(3000);

        //餘額變換 = 賬戶餘額-要取的錢
        account.setMoney(account.getMoney() - getDrawingMoney());

        //手上有多少錢
        setNowMoney(getNowMoney() + getDrawingMoney());

        System.out.println(Thread.currentThread().getName() + "取錢成功,目前" + account.getName() + "剩餘餘額:" + account.getMoney());
        System.out.println(Thread.currentThread().getName() + "手上拿着" + getNowMoney() + "元錢!");
        System.out.println(account.getName() + "目前賬戶餘額" + account.getMoney());
    }
}
           
  • 案例三:增加同步鎖實作線程安全的集合
package com.unsafe;

import java.util.ArrayList;

//增加同步鎖實作線程安全的集合
public class UnsafeList {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            new Thread( ()->{
                //在這裡把變化的量,list對象加一個同步鎖
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}
           

16、測試JUC安全類型的集合(JUC裡有一個關于list安全的集合:CopyOnWriteArrayList)

package com.thread;

import java.util.concurrent.CopyOnWriteArrayList;

//測試JUC安全類型的集合
public class JUCArrayList {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread().start();
            list.add(Thread.currentThread().getName());
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(list.size());
    }
}
           

17、死鎖

  • 多個線程各自占有一些共享資源,并且互相等待其他線程占有的資源才能運作,而導緻兩個或多個線程都在等待對方釋放資源,都停止執行的情形,某一個同步塊同時擁有“兩個以上對象的鎖”時,就可能會發生“死鎖”的問題。
package com.thread;

//死鎖:多個線程互相抱着對方需要的資源,然後形成僵持
public class LockDemo01 {
    public static void main(String[] args) {
        new Thread(new Makeup(0, "灰姑娘")).start();
        new Thread(new Makeup(1, "白雪公主")).start();
    }
}

//口紅
class Lipstick{
}

//鏡子
class Mirror{
}

class Makeup extends Thread{
    //需要的資源隻有一份,用static來保證隻有一份
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    //選擇
    private int choice;
    //使用的人
    private String girlName;

    public Makeup(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        try {
            makeup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void makeup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(this.girlName + "搶到了口紅");
                Thread.sleep(1000);
                //此部位鎖内又拿鎖,會與下面互強鎖形成僵持,程式卡死
//                synchronized (mirror) {
//                    System.out.println(this.girlName + "搶到了鏡子");
//                }
            }
            synchronized (mirror) {
                System.out.println(this.girlName + "搶到了鏡子");
            }
        } else {
            synchronized (mirror) {
                System.out.println(this.girlName + "搶到了鏡子");
                Thread.sleep(2000);
//                synchronized (lipstick) {
//                    System.out.println(this.girlName + "搶到了口紅");
//                }
            }
            synchronized (lipstick) {
                System.out.println(this.girlName + "搶到了口紅");
            }
        }
    }
}
           
  • 産生死鎖的四個必要條件:
    • 互斥條件:一個資源每次隻能被一個程序使用
    • 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放
    • 不剝奪條件:程序已獲得的資源,在未使用完之前,不能強行剝奪
    • 循環等待條件:若幹程序之間形成一種頭尾相接的循環等待資源關系
  • 上面列出了死鎖的四個必要條件,我們隻要想辦法破其中的任意一個或多個條件就可以避免死鎖發生

18、Lock(鎖)

  • 從JDK5.0開始,Java提供了更強大的線程同步機制,通過顯式定義同步鎖對象來實作同步。同步鎖使用Lock對象充當。
  • java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對Lock對象加鎖,線程開始通路共享資源之前應先獲得Lock對象
  • ReentrantLock類實作了Lock,它擁有與synchronized相同的并發性和記憶體語義,在實作線程安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。

示例:搶票

package com.thread;

import java.util.concurrent.locks.ReentrantLock;

//測試Lock鎖
//買票例子
public class LockDemo02 {
    public static void main(String[] args) {
        ticket ticket = new ticket();
        new Thread(ticket).start();
        new Thread(ticket).start();
        new Thread(ticket).start();
    }
}

class ticket implements Runnable{
    int ticketNums = 10;

    //線程不安全
//    @Override
//    public void run() {
//        while (ticketNums > 0) {
//            System.out.println(ticketNums--);
//            try {
//                Thread.sleep(1000);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//    }

    //定義Lock鎖
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (ticketNums > 0){
            try{
                lock.lock();    //加鎖
                System.out.println(ticketNums--);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();  //解鎖
            }
        }
    }
}
           

總結

class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock()
        try{
            //需要保證線程安全的代碼
        } finally{
            lock.unlock();
            //如果同步代碼有異常,要将unlock()寫入finally語句塊
        }
    }
}
           
  • synchronized與Lock的對比
    • Lock是顯式鎖(手動開啟或關閉鎖,别忘記關閉鎖)synchronized是隐式鎖,出了作用域自動釋放
    • Lock隻有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
    • 使用Lock鎖,JVM将花費較少的時間來排程線程,性能更好。并且具有更好的擴充性(提供更多的子類)
    • 優先使用順序:
      • Lock > 同步代碼塊(已經進入了方法體,配置設定了相應資源)> 同步方法(在方法體之外)

19、生産者消費者問題

  • 應用場景:生産者和消費者問題
    • 假設倉庫中隻能存放一件産品,生産者将生産出來的産品放入倉庫,消費者将倉庫中産品取走消費
    • 如果倉庫中沒有産品,則生産者将産品放入倉庫,否則停止生産并等待,直到倉庫中的産品被消費者取走為止
    • 如果倉庫中放有産品,則消費者可以将産品取走消費,否則停止消費并等待,直到倉庫中再次放入産品為止
      JavaSE學習筆記(十四)多線程多線程
  • 這是一個線程同步問題,生産者和消費者共享同一個資源,并且生産者和消費者之間互相依賴,互為條件:
    • 對于生産者,沒有生産産品之前,要通知消費者等待。而生産了産品之後,又需要馬上通知消費者消費
    • 對于消費者,在消費之後,要通知生産者已經結束消費,需要生産新的産品以供消費
    • 在生産者消費者問題中,僅有synchronized是不夠的
      • synchronized可阻止并發更新同一個共享資源,實作了同步
      • synchronized不能用來實作不同線程之間的消息傳遞(通信)
  • Java提供了幾個方法解決線程之間的通信問題
方法名 作用
wait() 表示線程一直等待,直到其他線程通知,與sleep不同,會釋放鎖
wait(long timeout) 指定等待的毫秒數
notify() 喚醒一個處于等待狀态的線程
notifyAll() 喚醒同一個對象上所有調用wait()方法的線程,優先級别高的線程優先排程
  • 注意:均是Object類的方法,都隻能在同步方法或同步代碼塊中使用,否則會抛出LIIegalMonitorStateException
19.1、解決方式1
  • 并發協作模型“生産者/消費者模式" --> 管程法
    • 生産者:負責生産資料的子產品(可能是方法、對象、線程、程序)
    • 消費者:負責處理資料的子產品(可能是方法、對象、線程、程序)
    • 緩沖區:消費者不能直接使用生産者的資料,他們之間有一個“緩沖區”
  • 生産者将生産好的資料放入緩沖區,消費者從緩沖區拿出資料
    JavaSE學習筆記(十四)多線程多線程

示例:買賣雞肉

package com.thread;

//測試:生産者消費者模型 --> 利用緩沖區解決:管程法

//生産者,消費者,産品,緩沖區
public class PCDemo01 {
    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 = 1; i < 100; i++) {
            container.push(new Chicken(i));
            System.out.println("生産了" + i + "隻雞");
        }
    }
}

//消費者
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;
    }
}

//緩沖區
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;
    }
}
           
19.2、解決方式2
  • 并發協作模型“生産者/消費者模式“ -->信号燈法
    • 通過定義一個标志位true、false來判斷代碼需要執行

示例:

package com.thread;

//測試生産者消費者問題2:信号燈法,标志位解決
public class PCDemo02 {
    public static void main(String[] args) {
        TV tv = new TV();
        new Player(tv).start();
        new Watcher(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 Watcher extends Thread{
    TV tv;
    public Watcher(TV tv){
        this.tv = tv;
    }

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

//産品 --> 節目
class TV{
    String voice;   //通知喚醒
    //演員表演,觀衆等待 T
    //觀衆觀看,演員等待 F
    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 = !this.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;
    }
}
           

20、線程池

  • 背景:經常建立和銷毀、使用量特别大的資源,比如并發情況下的線程,對性能影響很大。
  • 思路:提前建立好多個線程,放入線程池中,使用時直接擷取,使用完放回池中。可以避免頻繁建立銷毀、實作重複利用。類似生活中的公共交通工具。
  • 好處:
    • 提高響應速度(減少了建立新線程的時間)
    • 降低資源消耗(重複利用線程池中線程,不需要每次都建立)
    • 便于線程管理(…)
      • corePoolSize:核心池的大小
      • maximumPoolSize:最大線程數
      • keepAliveTime:線程沒有任務時最多保持多長時間後會終止
  • JDK5.0起提供了線程池相關API:ExecutorService和Executors
  • ExecutorService:真正的線程池接口。常見子類ThreadPoolExecutor
    • void execute(Runnable command):執行任務/指令,沒有傳回值,一般用來執行Runndable
    • Futuresubmit(Callable task):執行任務,有傳回值,一般用來執行Callable
    • void shutdown():關閉連接配接池
  • Executors:工具類、線程池的工廠類,用于建立并傳回不同類型的線程池
package com.thread;

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

//測試線程池
public class PoolDemo01 {
    public static void main(String[] args) {
        //1、建立服務,建立線程池
        //newFixedThreadPool 參數為:線程池大小
        ExecutorService service = Executors.newFixedThreadPool(10);

        //執行
        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.thread;

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

public class ThreadSumUp {
    public static void main(String[] args) {
        new MyThread1().start();

        new Thread(new MyThread2()).start();

        FutureTask<Integer> integerFutureTask = new FutureTask<>(new MyThread3());
        new Thread(integerFutureTask).start();
        try {
            Integer integer = integerFutureTask.get();
            System.out.println(integer);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

//1、繼承Thread類
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("This is MyThread1");
    }
}

//2、實作Runnable接口
class MyThread2 implements Runnable{
    @Override
    public void run() {
        System.out.println("This is MyThread2");
    }
}

//3、實作Callable接口
class MyThread3 implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("This is MyThread3");
        return 100;
    }
}
           
如果有同學發現筆記存在問題,麻煩私信找我,我馬上來更正,謝謝!