1. 程式人生 > 實用技巧 >Java進階--多執行緒

Java進階--多執行緒

執行緒簡介

任務

程序
程序(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎,每個應用程式就是一個程序。

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

多執行緒
多執行緒(multithreading)**是指從軟體或者硬體上實現多個執行緒併發執行的技術。
    

執行緒實現


Thread類建立執行緒

建立執行緒方式一

  1. 繼承Thread類
    2. 重寫run方法
    3. 呼叫start開啟執行緒
package Thread.demo01建立執行緒;
/*
建立執行緒方式一:
    1. 繼承Thread類
    2. 重寫run方法
    3. 呼叫start開啟執行緒
 */

public class TestThread1 extends Thread{
    @Override
    public void run() {
        // run方法執行緒體
        for (int i = 0; i <20 ; i++) {
            System.out.println("我在看程式碼------"+i);
        }
    }

    public static void main(String[] args) {       // 主執行緒
        //建立一個執行緒物件
        TestThread1 tt = new TestThread1();
        // 呼叫start()方法
        tt.start();

        for (int i = 0; i < 200; i++) {
            System.out.println("我在學習多執行緒-------"+i);
        }
        
    }
}

案例:實現多執行緒下載圖片

需要用到第三方工具包commons.io-2.6.jar,下載地址:
連結: https://pan.baidu.com/s/1hwOa9DiY4bjTZ2AV1n6JpA 提取碼: s1ag
下載好以後將其新增到lib資料夾下,記得Add to Build path

package Thread.demo01建立執行緒;

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;

public class PictureDownloadThread extends Thread{
    String url;
    String name;

    public PictureDownloadThread(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) {
        PictureDownloadThread pt1 = new PictureDownloadThread("https://pic.cnblogs.com/avatar/1418974/20181015113902.png","Kuang頭.png");
        PictureDownloadThread pt2= new PictureDownloadThread("https://t7.baidu.com/it/u=3616242789,1098670747&fm=79&app=86&size=h300&n=0&g=4n&f=jpeg?sec=1596611654&t=6e68bfae4f52559d8856b922d491307a","girl.png");
        PictureDownloadThread pt3 = new PictureDownloadThread("https://t7.baidu.com/it/u=3204887199,3790688592&fm=79&app=86&size=h300&n=0&g=4n&f=jpeg?sec=1596611654&t=58b381c7ecb89fc6eb26d71cdde4e72a","flower.png");

        pt1.start();
        pt2.start();
        pt3.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方法出現問題");
        }
    }
}

練習結果:
    
    

實現Runable介面(推薦)

建立執行緒方式二

  1. 實現runable介面
  2. 重寫run方法
  3. 執行執行緒需要丟入runable介面的實現類,呼叫start()
package Thread.demo01建立執行緒;
/**
 * 建立執行緒方式二:
 *  1.實現runable介面
 *  2.重寫run方法
 *  3.執行執行緒需要丟入runable介面的實現類,呼叫start()
 */
public class CreateThread2 implements Runnable{
    @Override
    public void run() {
        // run方法執行緒體
        for (int i = 0; i <20 ; i++) {
            System.out.println("我在看程式碼------"+i);
        }
    }

    public static void main(String[] args) {    // 主執行緒  
        //建立Runable介面的實現類
        CreateThread2 thread2 = new CreateThread2();
        Thread thread = new Thread(thread2);
        // 呼叫start()方法
        thread.start();
        
        for (int i = 0; i < 200; i++) {
            System.out.println("我在學習多執行緒-------"+i);
        }
    }
}


案例:實現多個執行緒購買火車票

package Thread.demo01建立執行緒;
/**
 * 多個執行緒同時操作同一個物件
 * 買火車票
 */
public class TestThread implements Runnable {
    private int ticktnumbers = 10;
    @Override
    public void run() {
        while (true){
            if(ticktnumbers <=0){
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "拿到了" + ticktnumbers-- + "票");
        }
    }
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        new Thread(testThread,"小明").start();
        new Thread(testThread,"老師").start();
        new Thread(testThread,"黃牛").start();
    }
}


發現問題:多個執行緒操作了同一個資源的情況下,執行緒不安全了,資料紊亂,執行緒併發問題,需要實現執行緒同步。

案例:龜兔賽跑

package Thread.demo01建立執行緒;
import javax.swing.*;

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(2);
                } 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();

    }

}


實現Callable介面

建立執行緒方式三

  • 實現Callable介面 ,需要返回值型別
  • 重寫call方法
  • 建立目標物件  CreateThreadCallable ct1 = new CreateThreadCallable();
  • 建立執行服務: ExecutorService ser = Executors.newFixedThreadPool(3);// 3表示執行緒數`
  • 提交執行:      Future<Boolean> result1 = ser.submit(ct1);
  • 獲取結果:   boolean rs1 = ``result1 ``.get();
  • 關閉服務:       ser.shutdown();
package Thread.demo01建立執行緒;
import org.apache.commons.io.FileUtils;

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

/**
 * 執行緒建立方式三:實現Callable介面
 *
 */
public class CreateThreadCallable implements Callable<Boolean> {
    String url;
    String name;

    public CreateThreadCallable(String url,String name){
        this.url = url;
        this.name = name;
    }
    @Override
    public Boolean call() throws Exception {
        WebDownloader2 webDownloader = new WebDownloader2();
        webDownloader.downloader(url,name);
        System.out.println("下載了檔名為:"+name);
        return true;
    }

    public static void main(String[] args) {
        CreateThreadCallable ct1 = new CreateThreadCallable("https://pic.cnblogs.com/avatar/1418974/20181015113902.png","Kuang.png");
        CreateThreadCallable ct2= new CreateThreadCallable("https://t7.baidu.com/it/u=3616242789,1098670747&fm=79&app=86&size=h300&n=0&g=4n&f=jpeg?sec=1596611654&t=6e68bfae4f52559d8856b922d491307a","girl.png");
        CreateThreadCallable ct3 = new CreateThreadCallable("https://t7.baidu.com/it/u=3204887199,3790688592&fm=79&app=86&size=h300&n=0&g=4n&f=jpeg?sec=1596611654&t=58b381c7ecb89fc6eb26d71cdde4e72a","flower.png");

        //建立執行服務
        ExecutorService ser = Executors.newFixedThreadPool(3);
        // 提交執行
        Future<Boolean> r1 = ser.submit(ct1);
        Future<Boolean> r2 = ser.submit(ct2);
        Future<Boolean> r3 = ser.submit(ct3);
        //獲取結果
        try {
            boolean rs1 = r1.get();
            boolean rs2 = r2.get();
            boolean rs3 = r3.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //關閉服務
        ser.shutdown();
    }
}

class WebDownloader2{
    //下載方法
    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方法出現問題");
        }
    }
}

靜態代理

靜態代理舉例

靜態代理模式:
真實物件和代理物件都實現同一個介面
代理物件要代理真實角色
靜態代理的好處:
1.  代理物件可以做很多真實物件做不了的事情
2. 真實物件可專注做自己的事情

package Thread.demo02靜態代理;
// 人間四大喜事: 久旱逢甘霖,他鄉遇故知,洞房花燭夜,金榜題名時
interface Marry{
    void happyMarry();
}

// 真實角色
class You implements Marry{
    @Override
    public void happyMarry() {
        System.out.println("Nick要結婚了,超開心...");
    }
}

// 代理角色
class WeddingCompany implements Marry{
    private Marry target;

    public WeddingCompany(Marry target) {
        this.target = target;       //真實目標角色
    }

    @Override
    public void happyMarry() {
        System.out.println("結婚之前,佈置現場");
        this.target.happyMarry();   //真實物件
        System.out.println("結婚之後,收尾款");
    }
}
public class StaticProxy{
    public static void main(String[] args) {
        // 真實物件掉方法
        You you = new You();
        you.happyMarry();
        
        //通過代理幫你調方法
        WeddingCompany weddingCompany = new WeddingCompany(new You());
        weddingCompany.happyMarry();

        // lambda表示式
        /*
        new Thread(()-> System.out.println("我愛你")).start();
        new Thread( new Runnable() {
            @Override
            public void run() {
                System.out.println("我愛你");
            }
        }).start();
        // Thread就是靜態代理     Runnable是真實的物件
        new WeddingCompany(new You()).happyMarry();*/
    }
}

執行緒中的靜態代理分析

1.Thread類實現了Runable介面,****即Thread類相當於上文中的"婚慶公司"

 
** 2.我們寫的類也是實現了Runnable介面,即我們寫的類相當於上文中的"結婚人You"**

 
 3.在實現了Runnable介面後通過代理類Thread物件完成執行緒的啟動
A.在代理類Thread物件的建立中,聲明瞭我們所寫的實際物件,eg:"myRunnable"。
B.然後由Thread類協助我們完成這一系列的操作。
C.看似簡單的start()背後,代理類Thread還幫助我們做了很多事。

Lambda表示式


Lambda 表示式,也可稱為閉包。Lambda 允許把函式作為一個方法的引數(函式作為引數傳遞進方法中)。使用 Lambda 表示式可以使程式碼變的更加簡潔緊湊。Lambda表示式可以替代以前廣泛使用的內部匿名類,各種回撥,比如事件響應器、傳入Thread類的Runnable等。

lambda 表示式的語法格式如下:**

(parameters) -> expression
或
(parameters) ->{ statements; }

Lambda表示式的特徵:

  • 型別宣告(可選):可以不需要宣告引數型別,編譯器會識別引數值。
  • 引數圓括號(可選):在單個引數時可以不使用括號,多個引數時必須使用。
  • 大括號和return關鍵字(可選):如果只有一個表示式,則可以省略大括號和return關鍵字,編譯器會自動的返回值;相對的,在使用大括號的情況下,則必須指明返回值。
package Thread.demo03Lambda表示式;

interface ILove{
    void love(int a);   // 介面只有一個方法,這個介面叫做函式式介面
}
class Love implements ILove{
    @Override
    public void love(int a) {
        System.out.println("I Love you: " + a);
    }
}

public class TestLambda2 {

    static  class Love2 implements ILove{
        @Override
        public void love(int a) {
            System.out.println("I Love you: " + a);
        }
    }

    public static void main(String[] args) {
        
        //區域性內部類
        class Love3 implements ILove{
            @Override
            public void love(int a) {
                System.out.println("I Love you: " + a);
            }
        }
        //外部類實現介面
        ILove love = new Love();
        love.love(1);
        
        //靜態內部類
        ILove love2 = new Love2();
        love.love(2);
        
        //區域性內部類
        ILove love3 = new Love3();
        love3.love(3);

        // 匿名內部類
        ILove love4 = new ILove() {
            @Override
            public void love(int a) {
                System.out.println("I Love you: " + a);
            }
        };
        love4.love(4);
        
        //Lambda表示式
        ILove love5 = (int a)->{
            System.out.println("I Love you: " + a);
        };
        love5.love(5);

        //去掉引數型別
        ILove love6 = (a)->{
            System.out.println("I Love you: " + a);
        };
        love6.love(6);

        ILove love7 = a->{
            System.out.println("I Love you: " + a);
        };
        love7.love(7);

        //  lambda表達只有一行程式碼,還可繼續簡化成下面的情況
        ILove love8 = a->System.out.println("I Love you: " + a);
        love8.love(7);
    }
}

執行緒狀態

執行緒的生命週期

一個執行緒的生命週期,執行緒是一個動態執行的過程,它也有一個從產生到死亡的過程。下圖顯示了一個執行緒完整的生命週期。
                

描述執行緒狀態的方法

下表列出了Thread類的一些重要方法:

序號 方法描述
1 public final void setPriority(int priority) 更改執行緒的優先順序。
2 public static void sleep(long millisec)
在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和排程程式精度和準確性的影響。
3 public final void join(long millisec)
等待該執行緒終止的時間最長為 millis 毫秒。
4 public static void yield()
暫停當前正在執行的執行緒物件,並執行其他執行緒。
5 public final void setDaemon(boolean on)
將該執行緒標記為守護執行緒或使用者執行緒。
6 public final void join(long millisec)
等待該執行緒終止的時間最長為 millis 毫秒。
7 public void interrupt()
中斷執行緒。
8 public final boolean isAlive()
測試執行緒是否處於活動狀態。

執行緒停止

package Thread.demo04執行緒狀態;
/* 測試停止執行緒
    1. 建議執行緒正常停止-->利用次數,不建議死迴圈
    2. 建議使用標誌位--->設定一個標誌位
    3. 不要使用stop或者destroy等過時或者JDK不建議使用的方法
 */
public class StopTest implements Runnable{
    // 1. 設定標誌位
    private boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        while(flag){
            System.out.println("run.........Thread"+ i++);
        }
    }
    // 2.設定一個公開的方法停止執行緒,轉換標誌位
    public void stop(){
        this.flag = false;
    }
    public static void main(String[] args) {
        StopTest stopTest = new StopTest();
        new Thread(stopTest).start();

        for (int i = 0; i <1000; i++) {
            System.out.println("main"+i);
            if(i==900){
                stopTest.stop();
                System.out.println("執行緒該停止了");
            }
        }
    }
}

執行緒休眠

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

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

public class TestSleep2 {
    public static  void tenDown(){
        int num = 10;
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(num--);
            if(num<0){
                break;
            }
        }
    }
    
    public static void main(String[] args) {
        // TestSleep2.tenDown();
        // 獲取系統當前時間
        Date startTime = new Date(System.currentTimeMillis());
        while(true){
            try {
                Thread.sleep(1000);
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime = new Date(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 輸出結果
21:36:41
21:36:42
21:36:43
21:36:44
21:36:45
21:36:46
21:36:47
21:36:48
21:36:49
21:36:50
21:36:51

執行緒禮讓(yield)

package Thread.demo04執行緒狀態;
/*
    禮讓不一定成功,看CPU心情
 */

class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+ "執行緒開始執行。");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"執行緒停止執行。");
    }
}
public class TestYield {
    public static void main(String[] args) {
        MyYield myYield = new MyYield();
        new Thread(myYield,"A").start();
        new Thread(myYield,"B").start();
    }

}

// 禮讓成功
A執行緒開始執行。
B執行緒開始執行。
B執行緒停止執行。
A執行緒停止執行。

//禮讓不一定成
B執行緒開始執行。
A執行緒開始執行。
B執行緒停止執行。
A執行緒停止執行。

Join合併執行緒

package Thread.demo04執行緒狀態;

// 執行緒插隊
public class TestJoin implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10 ; i++) {
            System.out.println("VIP來了....." + i);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TestJoin testJoin = new TestJoin();
        Thread thread = new Thread(testJoin);
        thread.start();

        for (int i = 0; i < 10 ; i++) {
            if(i==2){
                thread.join();	//開始插隊
            }
            System.out.println("main"  + i );
        }
    }
}

/* 執行結果
	要等到插隊執行緒執行完了之後,才執行主執行緒。
VIP 來了.....0
main0
VIP 來了.....1
main1
VIP 來了.....2
VIP 來了.....3
VIP 來了.....4
VIP 來了.....5
VIP 來了.....6
VIP 來了.....7
VIP 來了.....8
VIP 來了.....9
main2
main3
main4
main5
main6
main7
main8
main9

*/

執行緒觀測

package Thread.demo04執行緒狀態;
/* 觀察,測試執行緒的狀態
	Thread.State
 */
public class TestState {
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(()->{
            for (int i = 0; i < 5; 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);  //Run

        // 只要執行緒不終止,就一直輸出狀態
        while(state != Thread.State.TERMINATED){
            Thread.sleep(100);
            state = thread.getState();      // 更新執行緒狀態
            System.out.println(state);
        }
        // thread.start(); 執行緒只能啟動一次,一旦中斷就不能在啟動了

    }
}

執行緒優先順序

在作業系統中,執行緒可以劃分優先順序,Cpu優先執行優先順序高的執行緒的任務。在java中執行緒優先順序分為1~10,如果小於1或者大於10,則jdk報illegalArgumentException()異常。設定執行緒優先順序使用setPriority()方法。
1、執行緒優先順序具有繼承性。a執行緒啟動b執行緒,b執行緒的優先順序和a執行緒的優先順序是一樣的。
2、執行緒具有規則性。高優先順序的執行緒總是大部分先執行完,並不是高優先順序的完全先執行完。執行緒的優先順序和執行順序無關。執行緒的優先順序具有一定的規則性,cpu儘量將執行資源讓給優先順序比較高的執行緒。

package Thread.demo04執行緒狀態;
class MyPriorxity implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
    }
}
public class TestPriority {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
        MyPriorxity myPriorxity = new MyPriorxity();
        Thread t1 = new Thread(myPriorxity);
        Thread t2 = new Thread(myPriorxity);
        Thread t3 = new Thread(myPriorxity);
        Thread t4 = new Thread(myPriorxity);
        Thread t5 = new Thread(myPriorxity);
        Thread t6 = new Thread(myPriorxity);

        t1.setPriority(6);	//先設定優先順序,後啟動
        t1.start();
        t2.setPriority(10);
        t2.start();
        t3.setPriority(4);
        t3.start();
        t4.setPriority(5);
        t4.start();
        t5.setPriority(2);
        t5.start();
        t6.setPriority(3);
        t6.start();
    }
}

// 輸出結果
main-->5
Thread-1-->10
Thread-2-->4
Thread-0-->6
Thread-3-->5
Thread-5-->3
Thread-4-->2

守護執行緒

執行緒分為使用者執行緒守護執行緒
虛擬機器必須確保使用者執行緒執行完畢
虛擬機器不必等待守護執行緒執行完畢
如:後臺記錄操作日誌,監控記憶體,垃圾回收等待。

package Thread.demo04執行緒狀態;
// 測試守護執行緒


class You implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <36500 ; i++) {
            System.out.println("你一生都開心的活著");
        }
        System.out.println("----good bye----");
    }
}

class God implements Runnable{

    @Override
    public void run() {
        while(true){
            System.out.println("上帝保佑著你");
        }
    }
}
public class TestDaemon {

    public static void main(String[] args) {
        You you = new You();
        God god = new God();
        Thread godthread = new Thread(god,"God");
        godthread.setDaemon(true);		//將上帝執行緒設定為守護執行緒
        godthread.start();
        Thread youthread = new Thread(you,"You");
        youthread.start();

    }
}

執行緒同步

佇列和鎖

理解:****多個執行緒操作同一個資源,才會存線上程同步,也即是:併發。
例項:> 上萬人同時搶票、銀行取錢> 解決:> 排隊、物件等待池、 > 佇列(FIFO)+鎖、synchronized> 加鎖帶來的問題:> ** 一個執行緒持有鎖,會導致其它需要該鎖的執行緒掛起> ** 線上程競爭下,加鎖會導致較多的上下文比較和排程延時,引起效能問題
** 優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖,會導致效能倒置**>
**

執行緒存在的三種不安全

不安全的買票

package Thread.demo05執行緒同步;

class BuyTicket implements Runnable {
    private int ticktnumbers = 10;
    boolean flag = true; // 外部停止方式
    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void buy() throws InterruptedException {
        if (ticktnumbers <= 0) {
            flag = false;
        }
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "買了第" + ticktnumbers-- + "張票。");
    }
}

public class UnSafeBuyTicket{
    public static void main(String[] args) {
        BuyTicket ticket = new BuyTicket();
        new Thread(ticket,"小明").start();
        new Thread(ticket,"老師").start();
        new Thread(ticket,"黃牛").start();
    }
}

//結果
小明買了第8張票。
老師買了第10張票。
黃牛買了第9張票。
小明買了第5張票。
黃牛買了第7張票。
老師買了第6張票。
老師買了第3張票。
黃牛買了第4張票。
小明買了第2張票。
老師買了第1張票。
黃牛買了第0張票。
小明買了第-1張票。
老師買了第-2張票。
Process finished with exit code 0

不安全的取錢

package Thread.demo05執行緒同步;

import oop.demo05.A;

class Account{
    int money;  //餘額
    String name;    //卡名

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

// 銀行:模擬取款
class Drawer extends  Thread{
    Account account;
    int drawingMoney;
    int nowMoney;

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

    @Override
    public void run() {
        if(account.money - drawingMoney < 0){
            System.out.println(Thread.currentThread().getName()+"正在取錢不夠了,取不了");
        }
        try {
            Thread.sleep(1000);     // 放大問題的發生性
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 卡內餘額 = 卡里的錢 - 取出的錢
        account.money = account.money - drawingMoney;
        // 手裡的錢 =  手裡的錢 + 取出的錢
        nowMoney = nowMoney + drawingMoney;

        System.out.println(account.name + "餘額為:" +account.money);
        System.out.println(this.getName()+"手裡的錢:"+nowMoney);
    }
}

public class UnSafeBank {
    public static void main(String[] args) {
        Account account = new Account(100000,"結婚基金");
        Drawer you = new Drawer(account,50000,"你");
        Drawer wife = new Drawer(account,100000,"妻子");
        you.start();
        wife.start();

    }
}
// 結果
妻子正在取錢不夠了,取不了
結婚基金餘額為:50000
結婚基金餘額為:-50000
你手裡的錢:50000
妻子手裡的錢:100000

不安全的集合

package Thread.demo05執行緒同步;

import java.util.ArrayList;
import java.util.List;

// 集合執行緒不安全
public class UnSafeList {
    public static void main(String[] args) {
        List<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);
        System.out.println(list.size());
    }
}
// 輸出結果不滿10000
9992

同步(synchronized)

同步方法

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

同步塊

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

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

安全買票

package Thread.demo05執行緒同步;

class BuyTicket implements Runnable {
    private int ticktnumbers = 10;
    boolean flag = true; // 外部停止方式
    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // synchronized同步方法: 鎖的是this
    public synchronized void buy() throws InterruptedException {
        if (ticktnumbers <= 0) {
            flag = false;
            return;
        }
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "買了第" + ticktnumbers-- + "張票。");
    }
}

public class UnSafeBuyTicket{
    public static void main(String[] args) {
        BuyTicket ticket = new BuyTicket();
        new Thread(ticket,"小明").start();
        new Thread(ticket,"老師").start();
        new Thread(ticket,"黃牛").start();
    }
}
//結果
小明買了第10張票。
黃牛買了第9張票。
老師買了第8張票。
小明買了第7張票。
黃牛買了第6張票。
老師買了第5張票。
小明買了第4張票。
黃牛買了第3張票。
老師買了第2張票。
老師買了第1張票。

Process finished with exit code 0


安全的銀行

package Thread.demo05執行緒同步;

import oop.demo05.A;

class Account{
    int money;  //餘額
    String name;    //卡名

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

// 銀行:模擬取款
class Drawer extends  Thread{
    Account account;
    int drawingMoney;
    int nowMoney;

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

    @Override
    public  void run() {

        // 取錢的方法run加synchronized鎖的是this,此處即為Drawer的例項物件
        //所以不能在run上加synchronized,而應該給同步資源account加鎖,因為增刪改的物件是account
        synchronized (account){
            if(account.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName()+"正在取錢不夠了,取不了");
                return ;
            }
            try {
                Thread.sleep(1000);     // 放大問題的發生性
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 卡內餘額 = 卡里的錢 - 取出的錢
            account.money = account.money - drawingMoney;
            // 手裡的錢 =  手裡的錢 + 取出的錢
            nowMoney = nowMoney + drawingMoney;

            System.out.println(account.name + "餘額為:" +account.money);
            System.out.println(this.getName()+"手裡的錢:"+nowMoney);
        }
    }
}

public class UnSafeBank {
    public static void main(String[] args) {
        Account account = new Account(200000,"結婚基金");
        Drawer you = new Drawer(account,50000,"你");
        Drawer wife = new Drawer(account,100000,"妻子");
        you.start();
        wife.start();

    }
}

//
結婚基金餘額為:150000
你手裡的錢:50000
結婚基金餘額為:50000
妻子手裡的錢:100000

安全的列表

package Thread.demo05執行緒同步;

import java.util.ArrayList;
import java.util.List;

// 集合執行緒不安全
public class UnSafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                    //同一時間存線上程往列表同一位置新增資料,被覆蓋掉一些
                }
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //System.out.println(list);
        System.out.println(list.size());
    }
}
//輸出結果
10000

死鎖


死鎖:多個執行緒互相抱著對方需要的資源,然後形成僵持。

產生死鎖的原因

(1) 因為系統資源不足。
(2) 程序執行推進的順序不合適。
(3) 資源分配不當等。
如果系統資源充足,程序的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。其次,程序執行推進順序與速度不同,也可能產生死鎖。

產生死鎖的四個必要條件

(1) 互斥條件:一個資源每次只能被一個程序使用。
(2) 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
(3) 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
(4) 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。

死鎖的解除與預防

理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。所以,在系統設計、程序排程等方面注意如何不讓這四個必要條件成立,如何確定資源的合理分配演算法,避免程序永久佔據系統資源。此外,也要防止程序在處於等待狀態的情況下佔用資源。因此,對資源的分配要給予合理的規劃。

死鎖例項

package Thread.demo06死鎖;

import com.sun.jdi.event.ThreadStartEvent;

//口紅
class Lipstick{

}

//鏡子
class  Mirror{

}

//化妝
class Makeup extends Thread{
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
    int choice;
    String girlName;

    public Makeup(int choice,String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }
    //化妝: 互相持有對方的鎖
    private void makeup() throws InterruptedException {
       if(choice==1){
           synchronized (lipstick){
               System.out.println(this.girlName+"獲得口紅的鎖。");
               Thread.sleep(1000);
               synchronized (mirror){
                   System.out.println(this.girlName+"1s鍾後獲得鏡子的鎖。");
               }
           }
       }else{
           synchronized (mirror){
               System.out.println(this.girlName+"獲得鏡子的鎖。");
               Thread.sleep(2000);
               synchronized (lipstick){
                   System.out.println(this.girlName+"1s鍾後獲得口紅的鎖。");
               }
           }
       }
    }

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

public class DeadLock {
    public static void main(String[] args) {
        Makeup g1 = new Makeup(1,"灰姑涼");
        Makeup g2= new Makeup(2,"白雪公主");
        g1.start();
        g2.start();
    }
}

出現死鎖:

解決:不讓鎖中抱對方的鎖


    private void makeup() throws InterruptedException {
       if(choice==1){
           synchronized (lipstick){
               System.out.println(this.girlName+"獲得口紅的鎖。");
               Thread.sleep(1000);
           }
           synchronized (mirror){		//將對方的鎖拿出來
                   System.out.println(this.girlName+"1s鍾後獲得鏡子的鎖。");
           }
       }else{
           synchronized (mirror){
               System.out.println(this.girlName+"獲得鏡子的鎖。");
               Thread.sleep(2000); 
           }
           synchronized (lipstick){	 //將對方的鎖拿出來
                   System.out.println(this.girlName+"1s鍾後獲得口紅的鎖。");
           }
       }
    }

解除死鎖:
**            **

Lock鎖

package Thread.demo07Lock鎖;

import java.util.concurrent.locks.ReentrantLock;

//測試Lock鎖
class TestLooK2 implements Runnable{
    private int ticktnumbers = 10;
    boolean flag = true; // 外部停止方式

    // 定義lock鎖:顯示的鎖
    // ReentrantLock 可重入鎖
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (flag) {
            try {
                buy();
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public  void buy(){
        try{
            lock.lock();    //加鎖
            if (ticktnumbers <= 0) {
                flag = false;
                return;
            }
            System.out.println(Thread.currentThread().getName() + "買了第" + ticktnumbers-- + "張票。");
        }finally {
            lock.unlock();  //解鎖
        }
    }
}
public class TestLock {
    public static void main(String[] args) {
        TestLooK2 testLooK2 = new TestLooK2();
        new Thread(testLooK2).start();
        new Thread(testLooK2).start();
        new Thread(testLooK2).start();
    }
}


執行緒通訊

執行緒通訊的方法

  • wait() 表示執行緒一直等待,直到其他執行緒通知,與sleep不同,會釋放鎖
  • wait(long timeout) 指定等待的毫秒數
  • notify() 喚醒一個處於等待狀態的執行緒
  • notifyAll() 喚醒同一個物件上所有呼叫wait()方法的執行緒,優先級別高的執行緒先排程

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

生產者消費者問題

生產者消費者問題(英語:Producer-consumer problem),是一個多執行緒同步問題的經典案例。該問題描述了兩個共享固定大小緩衝區的執行緒——即所謂的“生產者”和“消費者”,在實際執行時會發生的問題。生產者的主要作用是生成一定量的資料放到緩衝區中,然後重複此過程。與此同時,消費者也在緩衝區消耗這些資料。該問題的關鍵就是要保證生產者不會在緩衝區滿時加入資料,消費者也不會在緩衝區中空時消耗資料。

解決方式1-管程法

生產者消費者模型是一個併發協作的模型:
生產者:負責生產資料的模組(可能是方法、物件、執行緒、程序)
消費者:負責處理資料的模組(可能是方法、物件、執行緒、程序)
緩衝區(倉庫):消費者和生產者之間通訊的橋樑,生產者將生產好的產品放入緩衝區,消費者從緩衝區中取出產品。
        

package Thread.demo08執行緒通訊;
//測試:生產者消費者模型-->管程法
//生產者、消費者、緩衝區、產品
class Producer extends Thread{
    SynContainer container;

    public Producer(SynContainer container){
        this.container = container;
    }

    //生產
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Product(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 Product{
    int id;
    public Product(int id) {
        this.id = id;
    }
}
// 緩衝區
class SynContainer{

    Product[] products = new Product[10];//需要容器大小
    int count = 0;    //容器計數器

    public synchronized void push(Product product){//生產者放入產品
        if(count==products.length){ //如果緩衝區滿了,就需要等待消費者消費
            try {
                this.wait();//生產者等待下一次生產
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果沒有滿,就繼續生產產品
        products[count] = product;
        count++;
        this.notifyAll();// 可以通知消費者消費了
    }

    //消費者消費產品
    public synchronized Product pop(){
        if(count==0){
            //等待生產者生產,消費者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Product product = products[count];
        //消費了之後,就可以通知生產者繼續生產
        this.notifyAll();
        return product;
    }
}

public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Producer(container).start();
        new Consumer(container).start();
    }
}

解決方式2-訊號燈法

package Thread.demo08執行緒通訊;

//測試:生產者消費者模型-->標誌法

public class TestPC2 {
    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 < 10; 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 < 10; i++) {
            this.tv.watch();
        }
    }
}
//產品--節目
class TV{
    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 = !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;
    }

}
//
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:抖音紀錄片
觀眾觀看了:抖音紀錄片
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:抖音紀錄片
觀眾觀看了:抖音紀錄片
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:抖音紀錄片
觀眾觀看了:抖音紀錄片
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:抖音紀錄片
觀眾觀看了:抖音紀錄片
演員表演了:快樂大本營
觀眾觀看了:快樂大本營
演員表演了:抖音紀錄片
觀眾觀看了:抖音紀錄片
Process finished with exit code 0

執行緒池

package Thread.demo09執行緒池;


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

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

        // 2. 執行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //3. 關閉連線
        service.shutdown();
    }
}

class MyThread implements Runnable{

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName());
    }
}
// 輸出
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-5
pool-1-thread-4

總結

參考內容:
狂神說Java(多執行緒學習) https://www.bilibili.com/video/BV1V4411p7EF?p=2