多執行緒編程1-定義理解與三種實現方式

多執行緒編程

進程與執行緒的理解:

進程: 是程式的一次動態執行過程,它經歷了從程式碼載入、執行到執行完畢的一個完整過程,這個過程就是進程產生、發展到最終消亡的過程;

多進程: 作業系統能同時運行多個進程(程式),由於CPU具備分時機制,在每個進程都能循環獲得自己的CPU時間片;由於CPU執行的速度非常快,使得所有的程式好像是在同時運行一樣。

image-20210402215645884

進程與執行緒的區別與聯繫:

  1. 進程是資源調度的基本單位,運行一個可執行程式會創建一個或多個執行緒,進程就是運行起來的可執行程式;
  2. 執行緒是程式執行的基本單位,是輕量級的進程,每個進程中都有唯一的主執行緒,且只能有一個,主執行緒和進程是相互依存的關係,主執行緒結束,進程也會結束。

image-20210402220317818

具體實例(word):

每次啟動Word對於作業系統而言就相當於啟動了一個系統的進程,而在這個進程之上又有許多其他程式在運行(拼寫檢查等),那麼對於這些程式就是一個個多執行緒。如果Word關閉了,則這些拼寫檢查的執行緒也肯定會消失,但是如果拼寫檢查的執行緒消失了,並不一定會讓Word的進程消失;

多插一句:如果打開兩個word文檔,則表示當前作業系統創建了兩個進程。

多執行緒實現:

實現多執行緒需要一個執行緒的主體類,這個類可以繼承Thread、實現Runnable以及Callable介面完成定義;

Thread實現多執行緒:

繼承結構如下:

public class Thread extends Object implements Runnable

實現介面Runnable,所以必須實現介面中的抽象方法:

Modifier and Type Method Description
void run() 當一個實現介面Runnable的對象被用來創建執行緒時,啟動執行緒會導致對象的run方法在單獨執行的執行緒中被調用。
void start() 使執行緒開始執行;Java虛擬機調用這個執行緒的run方法。

當產生多個對象時,這些對象就會並發的執行run()方法中的程式碼;

雖然多執行緒的執行方法都在run()方法中定義,但是在實際進行多執行緒啟動時並不能直接調用此方法,由於多執行緒時需要並發執行的,所以需要通過作業系統的資源調度才能執行,這樣多執行緒的啟動就必須利用Thread類中的start()方法完成,調用此方法會間接的調用run()方法。

實例:

package Java從入門到項目實戰.多執行緒編程.Java多執行緒實現;
class MyThread extends Thread{  //單繼承
    private String title;
    public MyThread(String title){
        this.title = title;
    }
    //覆寫執行緒的run方法
    @Override
    public void run() {
        for (int i = 0 ; i < 10; i++){
            System.out.println(this.title+"運行,i =" +i);
        }
    }
}
public class Main{
    public static void main(String[] args){
        new MyThread("執行緒A").start();   //實例化執行緒對象並啟動
		new MyThread("執行緒B").start();
        new MyThread("執行緒C").start();
        
        //對照
        /*沒有開啟多執行緒*/
        new MyThread("執行緒A").run();
        new MyThread("執行緒B").run();
        new MyThread("執行緒C").run();
    }
}

由效果圖可以看出,三個執行緒在交替執行:

image-20210403225445774

假如面試題:

為什麼執行緒啟動的時候必須調用start()方法而不是直接調用run()方法?

在本程式中,程式調用了Thread類繼承而來的start()方法後,實際上他執行的還是覆寫後的run()方法,那為什麼不直接調用run()?

簡單的說下:是因為多執行緒需要調用作業系統的資源,在start()下有一個關鍵的部分start0()方法,並且在start0()方法上使用了navite關鍵字定義;

public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
private native void start0(); //navite

什麼是navite?

navite是指:Java本機介面(Java Native Interface)簡稱:JNI;使用Java調用本機作業系統的函數功能完成一些特殊操作;

在Java中將start0()方法體交給JVM進行實現,所以這樣就會出現在windows或者在Linux中實現的start0()的是不同,不關係過程,只關心結果(是否調用了本機的作業系統的函數);

start0()作用:交由JVM進行匹配不同的作業系統,實現start0()方法體,功能:實現本機函數的調用;

具體百度、Google吧。

Runnable介面實現多執行緒:

出現的原因:為了解決Thread實現多執行緒出現的單繼承問題;並且增加了函數式介面;

Modifier and Type Method Description
void run() 當一個實現介面Runnable的對象被用來創建執行緒時,啟動執行緒會導致對象的run方法在單獨執行的執行緒中被調用。

實現程式碼:

class MyThread implements Runnable{
    private String title;
    public MyThread(String title){
        this.title = title;
    }

    @Override
    public void run() {  //執行緒方法覆寫
        for (int i = 0; i< 10;i++){
            System.out.println(this.title+"運行,i"+i);
        }
    }
}

啟動方式一:

Thread threadA = new Thread(new MyThread("執行緒A"));
Thread threadB = new Thread(new MyThread("執行緒B"));
Thread threadC = new Thread(new MyThread("執行緒C"));
Thread threadD = new Thread(new MyThread("執行緒D"));
Thread threadE = new Thread(new MyThread("執行緒E"));

threadA.start();
threadB.start();
threadC.start();
threadD.start();
threadE.start();

啟動方式二:

//通過Lambal表達式定義執行緒主體
for(int x = 0;  x < 3;x++){
    String title = "執行緒對象-"+x;
    //實際上Thread傳入的類型是Runnable
    new Thread(()->{  //Lambda實現執行緒主體
        for(int y = 0; y < 20; y++){
            System.out.println(title+"運行,y"+y);
        }
    }).start();
}

Thread與Runnable的聯繫:

繼承結構:

public class Thread extends Object implements Runnable

在之前繼承Thread類的時候實際上覆寫的還是Runnable介面的run()方法。

實現並發訪問資源:

package Java從入門到項目實戰.多執行緒編程.Java多執行緒實現;
class MyThreadConcurrent implements Runnable {
    private int ticket = 5;

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //同步操作--》從5-1票數
            /*synchronized(this){
                if(this.ticket > 0){
                    System.out.println("賣票,ticket = "+this.ticket--);
                }
            }*/
            //票數亂數
            if(this.ticket > 0){
                System.out.println("賣票,ticket = "+this.ticket--);
            }

        }
    }
}
public class 並發資源訪問 {
    public static void main(String[] args) {
        MyThreadConcurrent thread = new MyThreadConcurrent();
        new Thread(thread).start();  //第一個執行緒
        new Thread(thread).start();  //第二個執行緒
        new Thread(thread).start();  //第三個執行緒
    }
}

總結一句話:Thread有單繼承的局限性以及在有些情況下結構的不合理性;所以後面多執行緒的實現使用的都是Runnable介面。

Callable介面實現多執行緒:

為什麼要使用Callable介面來實現多執行緒?

因為使用Callable介面實現彌補了Runnable實現多執行緒沒有返回值的問題。

繼承結構如下:

@FunctionalInterface
public interface Callable<V>{
	public V call() throws Exception{
        
    }
}

定義的時候可以設置一個泛型,此泛型的類型就是call()方法的返回的數據類型,好處:可以避免向下轉型的安全隱患。

執行緒類主體完成後,需要啟動多執行緒的話還是需要通過Thread類實現的,又因為我們的Callable介面與Thread沒有聯繫,所以我們需要FutureTask類實現兩者之間的聯繫;如圖所示:

image-20210404234544335

通過FutureTask類繼承結構可以發現它是Runnable介面的子類;

程式碼實現如下:

package Java從入門到項目實戰.多執行緒編程.Java多執行緒實現;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class CallableThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println("執行緒執行 x = "+i);
        }
        return "xbhog";
    }
}

public class Callable介面實現多執行緒 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //將Callable實例化包裝在FutureTask類中,這樣就可以與Runnable介面關聯
        FutureTask<String> task = new FutureTask<String>(new CallableThread());
        //執行緒啟動
        new Thread(task).start();
        //獲取call()的返回值
        System.out.println("【執行緒返回數據】:"+task.get());
    }
}