首頁 > 軟體

詳解JUC並行程式設計中的程序與執行緒學習

2022-03-24 19:00:30

程序與執行緒

程序

  • 程式由指令和資料組成,但這些指令要執行,資料要讀寫,就必須將指令載入至 CPU,資料載入至記憶體。在指令執行過程中還需要用到磁碟、網路等裝置。程序就是用來載入指令、管理記憶體、管理 IO 的
  • 當一個程式被執行,從磁碟載入這個程式的程式碼至記憶體,這時就開啟了一個程序。
  • 程序就可以視為程式的一個範例。大部分程式可以同時執行多個範例程序(例如記事本、畫圖、瀏覽器等),也有的程式只能啟動一個範例程序(例如網易雲音樂、360 安全衛士等)

執行緒

執行緒是主要負責執行指令,程序是主要管載入指令。

一個程序之內可以分為一到多個執行緒。

一個執行緒就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執行

Java 中,執行緒作為最小排程單位,程序作為資源分配的最小單位。 在 windows 中程序是不活動的,只是作為執行緒的容器

同步非同步

  • 需要等待結果返回,才能繼續執行就是同步
  • 不需要等待結果返回,就能繼續執行就是非同步

序列並行執行時間

使用多核cpu並行執行可以明顯的提高執行效率

  • 序列執行時間 = 各個執行緒時間累加和 + 彙總時間
  • 並行執行時間 = 最慢的執行緒時間 + 彙總時間

注意:單核依然是並行的思想(即:cpu輪流去執行執行緒,微觀上仍舊是序列),使用單核的多執行緒可能會比使用單核的單執行緒慢,這是因為多執行緒上下文切換反而浪費了時間。

建立和執行執行緒

1.使用 Thread

public static void main(String[] args) {
        // 建立執行緒物件
        Thread t = new Thread("執行緒1") {
            public void run() {
                // 要執行的任務
                log.debug("執行緒1被啟動了");
            }
        };
        // 啟動執行緒
        t.start();
        log.debug("測試");
    }

2.使用 Runnable 配合 Thread

public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            public void run() {
                // 要執行的任務
                log.debug("執行緒1被啟動了");
            }
        };
        // 建立執行緒物件
        Thread t = new Thread(runnable);
        t.setName("執行緒1");
        // 啟動執行緒
        t.start();
        log.debug("測試");
    }

這裡的Runnable是一個介面,介面中只有一個抽象方法,由我們來提供實現,實現中包含執行緒的程式碼就可以了。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

Thread 與 Runnable 的關係原理分析

方法1原理分析

方法2是使用runnable物件,當成引數傳給Thread構造方法,其中又呼叫了init方法,下面是Thread構造方法的原始碼

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

繼續跟蹤檢視runnable物件傳到哪裡去了,可以看到又傳給了另一個過載的init,如下

private void init(ThreadGroup g, Runnable target, String name,                      long stackSize) {        init(g, target, name, stackSize, null, true);    }private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

再次跟蹤可以看到是把runnable物件傳給了一個thread的一個成員變數

//省略部分程式碼
this.target = target;

那麼這個成員變數在哪裡在使用了呢,經過查詢可以發現是在run方法裡面,只不過Thread發現有runnable物件就會先採用runnable的run方法。

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

方法2原理分析

通過建立一個子類去重寫Thread類的run方法,這樣就不會執行父類別的run方法。

1.用 Runnable 更容易與執行緒池等高階 API 配合

2.用 Runnable 讓任務類脫離了 Thread 繼承體系,更靈活

方法3 FutureTask配合Thread建立執行緒

Future介面中含有get方法來返回結果的

//省略部分程式碼
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

而runnable本身是沒有返回結果的,runnable不能將結果傳給其他執行緒。

public interface Runnable {
    public abstract void run();
}

要注意到FutureTask也實現了Runnable介面,也可以傳給Thread的有參構造裡面。

建立執行緒的程式碼

public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 建立任務物件
        FutureTask<Integer> task3 = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.debug("執行緒1被執行了");
                return 666;
            }
        });
        // 引數1 是任務物件; 引數2 是執行緒名字,推薦
        new Thread(task3, "執行緒1").start();
        // 主執行緒阻塞,同步等待 task 執行完畢的結果
        Integer result = task3.get();
        log.debug("結果是:{}", result);
    }

檢視程序

  • 工作管理員可以檢視程序和執行緒數,也可以用來殺死程序,也可以在控制檯使用tasklist檢視程序taskkill殺死程序
  • jconsole 遠端監控設定來檢視

執行緒執行原理

JVM 中由堆、棧、方法區所組成,其中棧就是給執行緒使用的。

方法呼叫時,就會對該方法產生一個棧幀,方法的區域性變數都會在棧幀中儲存。棧是後進先出,當method2執行完就會回收,在執行完同時會記錄返回地址,然後在method1中繼續執行。

執行緒之間的棧幀是相互獨立的,之間互不干擾。

執行緒上下文切換

當上下文切換時,要儲存當前的狀態,因為可能是時間片用完了,此時執行緒還沒有結束。Java中對應的就是程式計數器

start與run方法

啟動一個執行緒必須要用start方法,如果直接呼叫類裡面的run方法實際走的是main主執行緒。

執行緒start前getState()得到的是NEW

執行緒start後getState()得到的是RUNNABLE

public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("t1被啟動");
            }
        };
        System.out.println(t1.getState());
        t1.start();
        System.out.println(t1.getState());
    }

sleep方法

在sleep期間呼叫getState()方法可以得到TIMED_WAITING

public static void main(String[] args) {
        Thread t1 = new Thread("執行緒1") {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        log.debug("執行緒1 state: {}", t1.getState());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("執行緒1 state: {}", t1.getState());
    }

sleep打斷

sleep可以使用interrupt方法打斷,打斷後會觸發InterruptedException異常

public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("進入睡眠");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("被喚醒");
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        Thread.sleep(1000);
        log.debug("打斷");
        t1.interrupt();
    }

sleep防止cpu使用100%

在沒有利用cpu來計算時,不要讓while(true)空轉浪費cpu,這時可以使用yield或 sleep 來讓出cpu的使用權給其他程式

yield方法會把cpu的使用權讓出去,然後排程執行其它執行緒。執行緒的排程最終還是依賴的作業系統的排程器。

join方法

該方法會等待執行緒的結束

static int r = 11;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("主執行緒開始");
        Thread t1 = new Thread(() -> {
            sleep(1);
            r = 888;
        },"執行緒1");
        t1.start();
//        t1.join();
        log.debug(String.valueOf(r));
        log.debug("主執行緒執行緒結束");
    }

join沒有使用時,返回的是11,若是使用join返回的是888,是主執行緒在同步等待執行緒1。

當然還有其他的方法等待

1.sleep方法等待執行緒1結束

2.利用FutureTask的get方法

join(long n)方法可以傳入引數,等待執行緒執行結束,最多等待 n 毫秒,假如執行時間大於等待的時間,就會不再等待。那麼該執行緒會直接結束嗎?答案是不會。

如下程式碼

public class Test10 {
    static int r = 11;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("主執行緒開始");
        Thread t1 = new Thread(() -> {
            sleep(2);
            r = 888;
            log.debug("執行緒1結束");
        },"執行緒1");
        t1.start();
        t1.join(1000);
        log.debug(String.valueOf(r));
        log.debug("主執行緒執行緒結束");
    }
}

輸出結果,可以看到這裡只是主執行緒不再等待。

16:28:53.360 c.Test10 [main] - 主執行緒開始
16:28:54.411 c.Test10 [main] - 11
16:28:54.411 c.Test10 [main] - 主執行緒執行緒結束
16:28:55.404 c.Test10 [執行緒1] - 執行緒1結束

interrupt 方法

interrupt可以用來打斷處於阻塞狀態的執行緒。在打斷後,會有一個打斷標記(布林值)會提示是否被打斷過,被打斷過標記為true否則為false.
但是sleep、wait和join可以來清空打斷標記。

程式碼如下

public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(4000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");
        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打斷標記:{}", t1.isInterrupted());
    }

執行緒被打斷後並不會結束執行,有人就會問了,那我們如何在打斷執行緒後關閉執行緒呢?答案就是利用打斷標記去實現。

可以線上程的死迴圈之中加入一個判斷去實現。

boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    log.debug("退出迴圈");
                    break;
                }

守護行程

Java 程序通常需要所有執行緒都執行結束,才會結束。

但是存在一種守護行程,只要其他非守護行程結束,守護行程就會結束。垃圾回收器就使用的守護行程。

執行緒的狀態

作業系統層面(早期程序的狀態)

  • 初始狀態 在語言層面建立了執行緒物件,還未與作業系統執行緒關聯
  • 可執行狀態(就緒狀態)指該執行緒已經被建立(與作業系統執行緒關聯),可以由 CPU 排程執行任務。
  • 執行狀態 獲取了 CPU 時間片執行中的狀態
  • 呼叫阻塞api使執行狀態轉為阻塞狀態
  • 終止狀態 表示執行緒已經執行完畢

Java API 層面

1、新建狀態(New)

Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
            }
        };
log.debug("t1 state {}", t1.getState());

2、就緒狀態(Runnable)與執行狀態(Running)

Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while(true) { // runnable
                }
            }
        };
t2.start();
log.debug("t2 state {}", t2.getState());

3、阻塞狀態(Blocked)

用一個執行緒拿到鎖,使得當前執行緒沒拿到鎖會出現阻塞狀態。

Thread t6 = new Thread("t6") {
            @Override
            public void run() {
                synchronized (TestState.class) { // blocked
                    try {
                        Thread.sleep(90000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
t6.start();

4、等待狀態(Waiting)

等待一個未執行完成的執行緒

t2.join(); //等待狀態

5、超時等待(Time_Waiting)

可以在指定的時間自行返回的。

6、終止狀態(TERMINATED)

執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。 終止的執行緒不可再次復生。

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!  


IT145.com E-mail:sddin#qq.com