Java多執行緒就是這麼簡單

魏寶航發表於2020-12-14

多執行緒

關於多執行緒有關的概念:

  • 程式:程式指正在執行的程式,並且具有一定獨立功能
  • 執行緒:執行緒是程式中的一個執行單位,負責當前程式程式的執行,一個程式中至少會有一個執行緒,如果一個程式中包含多個執行緒,那麼可稱為多執行緒程式
  • 單執行緒:當要執行多個任務時,cpu只會依次執行,當一個任務執行完後,再去執行另外一個任務
  • 多執行緒:多個任務可以同時進行

在Java中,不同執行緒會有不同的優先順序搶佔cpu,如果執行緒優先順序相同,就會隨機先去一個執行緒去執行

Java程式執行時會預設執行3個程式:

  • main主執行緒
  • gc垃圾回收機制
  • 異常處理機制

我們如何能夠判斷程式是否是多執行緒?

如果我們能夠將程式的執行用一條直線畫出來,就說明是單執行緒

關於執行緒的常用API方法

在這裡插入圖片描述

  1. run():該方法需要被重寫,重寫的內容就是需要執行的操作

  2. start():呼叫該方法就會啟動相應的執行緒,並呼叫當前執行緒的run方法

  3. sleep(long millitime):將當前執行緒進入阻塞狀態(不會釋放鎖,即同步監視器)

  4. join():當a執行緒呼叫b執行緒的join方法時,a執行緒會進入阻塞狀態,直到b執行緒的任務執行完畢

  5. isAlive():判斷當前執行緒是否存活

  6. yield():呼叫該方法後回釋放當前執行緒的cpu執行權,當時並不代表不會再次執行,有可能釋放後,又是該執行緒搶佔到了cpu的執行權

  7. currentThread():Thread類中的靜態方法,會返回執行當前程式的執行緒

  8. getName():返回當前執行緒的名字

  9. setName():設定執行緒的名字

  10. getPriority():設定執行緒的優先順序(

MAX_PRIORITY=10
MIN_PRIORITY=1
NORM_PRIORITY=5 預設優先順序

  1. wait():將執行緒進入阻塞狀態(會釋放掉鎖),只能在同步程式碼塊或同步方法中使用

  2. notify():將另外一個執行緒喚醒

  3. notifyAll():喚醒所有被阻塞的執行緒

執行緒建立的4種方式

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-feelaAyr-1607955739349)(C:\Users\華為\AppData\Roaming\Typora\typora-user-images\image-20201214222120401.png)]

一:繼承Thread類

  1. 首先建立一個類去繼承Thread
  2. 重寫Thread中的run方法
  3. main()中建立該物件
  4. 呼叫該物件的start方法,啟動執行緒
public class test {
    public static void main(String[] args) {
        MyThread1 myThread1=new MyThread1();
        myThread1.start();
    }
}
class MyThread1 extends Thread{
    @Override
    public void run() {
        System.out.println("繼承了Thread");
    }
}

二:實現Runnable介面

  1. 建立一個類實現Runnable介面
  2. 實現介面的run方法
  3. 在main()中建立實現Runnable的物件
  4. 建立Thread物件,並把剛建立好的類傳參
  5. 呼叫Thread物件的start方法,啟動執行緒
public class test {
    public static void main(String[] args) {
        MyThread1 myThread1=new MyThread1();
        Thread t=new Thread(myThread1);
        t.start();
    }
}
class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("實現了Runnable");
    }
}

三:實現Callable介面

  1. 建立一個類實現Callable介面
  2. 實現介面的call()方法
  3. 在main中建立實現Callable的物件
  4. 建立FutureTask物件並把上面建立的物件傳參
  5. 建立Thread物件,傳入FutureTask物件
  6. 呼叫Thread的start方法,啟動執行緒
public class test {
    public static void main(String[] args) {
        MyThread2 myThread2=new MyThread2();
        FutureTask futureTask=new FutureTask(myThread2);
        Thread t=new Thread(futureTask);
        t.start();
    }
}
class MyThread2 implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("實現了Callable");
        return null;
    }
}

此方式如果需要得到返回值需要呼叫futureTask.get();

但是會拋異常,用try,catch方法捕捉一下就好了

四:執行緒池

  1. 建立ExecutorService物件
  2. 傳入相應的執行緒物件
  3. 結束執行緒池
public class test {
    public static void main(String[] args) {
        //建立執行緒池,設定執行緒池執行緒的數量為10
        ExecutorService service = Executors.newFixedThreadPool(10);
        //execute適用於實現了Runnable的物件
        service.execute(new MyThread1());
        //submit適用於實現了Callable的物件
        service.submit(new MyThread2());
        //結束執行緒池
        service.shutdown();
    }
}
class MyThread1 implements Runnable{
    @Override
    public void run() {
        System.out.println("實現了Runnable");
    }
}
class MyThread2 implements Callable{
    @Override
    public Object call() throws Exception {
        System.out.println("實現了Callable");
        return null;
    }
}

Runnable和Callable的對比:

  • Callable可以有返回值,執行完相應操作後可以返回需要的結果
  • 可以拋異常,可以將call方法中的異常丟擲
  • 支援泛型,可以指定返回值型別

執行緒安全

什麼是執行緒安全問題呢?

當多個執行緒對同一個共享資料操作時,執行緒執行還沒來得及更新處理共享的資料,從而使得其他操作的執行緒並未得到最新的資料,從而產生問題

舉個例子:

  1. 當甲乙兩人向同一賬戶存錢,讓甲乙兩個執行緒同時存錢,如果甲向賬戶存了1000元,並列印此時餘額,應為1000元,但是如果此時乙也存了1000元,就會導致,顯示餘額為2000元,並不是甲當時的餘額
  2. 還有就是火車售票問題,如果多個視窗同時售票,如果1號視窗正在賣001號票時,此時還未處理完成,這是2號視窗也賣了001號票,這就導致產生了兩個001號票

那麼如何解決呢?

有三種方式:

方法一:同步程式碼塊

synchronized(Object obj)
{
	//操作內容                
}
  • synchronized():傳入的可以是任意類的物件,但必須是多個執行緒共用的,一般可以利用this,即當前物件(Runnable),Thread不太行,因為繼承多個Thread類會導致this物件不一致
  • 被包住的程式碼執行為單執行緒,當一個執行緒執行完後,另外一個執行緒才有可能會分配到執行權去執行
  • 多個執行緒必須共用同一把鎖,這樣才能夠判斷一個執行緒是夠執行
  • Runnable一般很實用,因為多個執行緒都呼叫同一個類的方法,但是Thread就需要自己定義靜態變數或者當前的唯一類即(windows.class)
public class test {
    public static void main(String[] args) {
        Window t1 = new Window("視窗1");
        Window t2 = new Window("視窗2");
        Window t3 = new Window("視窗3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window extends Thread{
    private static int ticket=100;
    //繼承方式,要用靜態物件,因為繼承實現多執行緒有多個物件,不是共用一個物件
    private static Object obj=new Object();
    public Window(String name){
        super(name);
    }
    @Override
    public void run() {
        while(true){
            //不可以用this,同理,因為有很多物件,不唯一
            synchronized(obj){
                if(ticket>0){
                    System.out.println(getName()+":賣票,票號為:"+ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

方法二:同步方法

和同步程式碼塊類似

就是將共享資料的操作封裝成方法

將這個方法用鎖鎖住

public class test {
    public static void main(String[] args) {
        Window t1 = new Window("視窗1");
        Window t2 = new Window("視窗2");
        Window t3 = new Window("視窗3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window extends Thread{
    private static int ticket=100;
    public Window(String name){
        super(name);
    }
    @Override
    public void run() {
        while(true){
            show();
        }
    }
    public static synchronized void show(){
        if(ticket>0){
            System.out.println(Thread.currentThread().getName()+":賣票,票號為:"+ticket);
            ticket--;
        }
    }
}

注意:

  • show方法要定義成靜態,因為如果是非靜態,它的鎖預設是當前物件,繼承方式就會有多個鎖,如果是Runnable就可以,所以需要程式設計靜態,這樣預設鎖就是當前類物件

方法三:lock鎖

  • 首先建立一個ReentrantLock物件
  • 在執行共享資料之前將鎖開啟,呼叫lock方法
  • 在結束時將鎖解開,呼叫unlock方法
public class test {
    public static void main(String[] args) {
        Windows w = new Windows();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("視窗1");
        t2.setName("視窗2");
        t3.setName("視窗3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Windows implements Runnable{
    private int ticket=100;
    private ReentrantLock lock=new ReentrantLock(true);
    @Override
    public void run() {
        while(true){
            try {
                lock.lock();
                if(ticket>0){
                    System.out.println(Thread.currentThread().getName()+":賣票,票號為:"+ticket);
                    ticket--;
                }
                else{
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

問題:該方式和synchrnized有什麼不同呢?

synchronized在執行完相應程式碼後會自動上鎖解鎖,而lock需要手動上鎖和解鎖,較為靈活

執行緒的死鎖

描述:死鎖是指兩個或兩個以上的程式在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程式稱為死鎖程式。

舉個例子:兩個人迎面相遇,甲希望乙會給他讓路,而乙希望甲給他讓他讓路,就這樣兩個人僵持在這裡,最終誰也不給誰讓路,導致死鎖問題

public class test {
    public static void main(String[] args) {
        StringBuffer s1=new StringBuffer();
        StringBuffer s2=new StringBuffer();
        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }

            }
        }.start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    s1.append("c");
                    s2.append("3");
                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

本例中,執行緒1首先拿到了s1鎖,然後阻塞了一段時間,這段時間執行緒2拿到了s2鎖,這是執行緒1就緒需要s2鎖,而執行緒2需要s1鎖,兩者誰都拿不到,就會僵持住

解決死鎖的相應辦法:

  • 減少共享變數的使用
  • 設計相應的演算法去規避死鎖問題
  • 儘量減少鎖的巢狀使用

執行緒的通訊

  • wait():將執行緒進入阻塞狀態(會釋放掉鎖),只能在同步程式碼塊或同步方法中使用

  • notify():將另外一個優先順序高的執行緒喚醒

  • notifyAll():喚醒所有被阻塞的執行緒

注意:這三個方法只能夠在同步程式碼塊或者同步方法使用,都定義在了Object類中

例題要求:

讓兩個執行緒交替列印1-100之間的數字

public class 執行緒通訊 {
    public static void main(String[] args) {
        Number number=new Number();
        Thread t1=new Thread(number);
        Thread t2=new Thread(number);
        t1.setName("執行緒一");
        t2.setName("執行緒二");
        t1.start();
        t2.start();
    }
}
class Number implements Runnable{
    private int number=1;

    @Override
    public void run() {
        while(true){
            synchronized (this) {

                notify();
                //喚醒全部
                // notifyAll();
                if(number<=100){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;

                    try {
                        //使得呼叫如下方法程式阻塞,執行wait後,鎖就被釋放
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

  • 當執行緒1首次進入,會列印出1,然後呼叫了wait方法 ,進入阻塞狀態
  • 此時執行緒2進入,首先會喚醒執行緒1,然後列印2,然後自己進入阻塞狀態
  • 兩者交替阻塞喚醒,直到列印完為止

那麼問題是sleep和wait方法有什麼異同?

兩個方法宣告的位置不同,sleep是Thread中宣告的,而wait是Object中宣告的

sleep可以在任何情景呼叫,而wait只能夠在同步程式碼塊或同步方法中使用

sleep執行後不會釋放當前的鎖,而wait會釋放掉當前的鎖

生產者和消費者問題:

生產者(Priductor)將產品交給店員(Clerk),而消費者(Customer)從店員處取走產品,店員一次只能持有固定數量的產品(比如20個),如果生產者檢視生產更多的產品,店員會叫生產者停一下,如果店中有空位放產品了再通知生產者繼續生產:如果店中沒有產品了,店員會告訴消費者等一下,如果店中有產品了再通知消費者來取走產品。

package 生產者與消費者問題;

public class 生產者 {
    public static void main(String[] args) {
        Clerk clerk=new Clerk();
        Producer p1 = new Producer(clerk);
        p1.setName("生產者");
        Consumer c1 = new Consumer(clerk);
        c1.setName("消費者");
        p1.start();
        c1.start();
    }
}
class Clerk{

    private int productCount=0;

    public synchronized void consumeProduct() {
        if(productCount>0){
            System.out.println(Thread.currentThread().getName()+":開始消費第"+productCount+"個產品");
            productCount--;
            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void produceProduct() {
        if(productCount<20){
            productCount++;
            System.out.println(Thread.currentThread().getName()+":開始生產第"+productCount+"個產品");
            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
class Producer extends Thread{
    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+":開始生產產品......");
        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            clerk.produceProduct();
        }
    }
}
class Consumer extends Thread{
    private Clerk clerk;

    public Consumer(Clerk clerk){
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+":開始消費產品......");
        while(true){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consumeProduct();
        }
    }
}

  • 生產者和消費者會共享產品數量
  • 我們可以在售貨員類中定義方法,當此時的生產數量未達到標準時,就會進行生產,然後會喚醒消費的程式,否則就會進入阻塞狀態
  • 而當產品數量不足時,消費者就會喚醒生產程式,此時,自己進入阻塞狀態

相關文章