1. 程式人生 > >多執行緒概述及執行緒的建立和啟動

多執行緒概述及執行緒的建立和啟動

多執行緒概述及執行緒的建立和啟動

概述

我們之前寫的程式都只是在做單執行緒的程式設計,所有的程式只有一條順序執行流,程式從main方法開始執行,依次向下執行每行程式碼,如果程式執行過程中某行程式碼遇到了阻塞,則程式將會停滯在該處。

單個執行緒往往功能非常有限,所以我們引入了多執行緒來進行功能上的優化。多執行緒的概念聽起來會讓很多初學者或者說是不瞭解作業系統的同學感到特別的難。

舉個簡單的例子以前的單執行緒的程式就相當於我們開了一個餐廳,但是隻僱傭了一個服務員,每次只能服務一個顧客,這樣的效率就會很低,多執行緒就相當於是我們僱傭了多個服務員,他們可以同時服務多個顧客。這就是所謂的多執行緒。

Java提供了一套完整的多執行緒支援,包括執行緒的建立,執行緒的控制,執行緒同步,等等

定義

幾乎所有的作業系統都會支援同時執行多個任務,一個任務通常就是一個程式,每個執行中的程式就是一個程序。當一個程式執行時,內部可能包含了多個順序執行流,每一個順序執行流就是一個執行緒。首先我們要區分以下幾個概念。

程式
說起程序,就不得不說下程式。先看定義:程式是指令和資料的有序集合,其本身沒有任何執行的含義,是一個靜態的概念。而程序則是在處理機上的一次執行過程,它是一個動態的概念。這個不難理解,其實程序是包含程式的,程序的執行離不開程式,程序中的文字區域就是程式碼區,也就是程式。的集合

程序
狹義定義:程序就是一段程式的執行過程。

廣義定義:程序是一個具有一定獨立功能的程式關於某個資料集合的一次執行活動。它是作業系統動態執行的基本單元,在傳統的作業系統中,程序既是基本的分配單元,也是基本的執行單元。

簡單的來講程序的概念主要有兩點:
第一,程序是一個實體。每一個程序都有它自己的地址空間,一般情況下,包括文字區域(text region)、資料區域(data region)和堆疊(stack region)。文字區域儲存處理器執行的程式碼;資料區域儲存變數和程序執行期間使用的動態分配的記憶體;堆疊區域儲存著活動過程呼叫的指令和本地變數。
第二,程序是一個“執行中的程式”。程式是一個沒有生命的實體,只有處理器賦予程式生命時,它才能成為一個活動的實體,我們稱其為程序。

執行緒
通常在一個程序中可以包含若干個執行緒,當然一個程序中至少有一個執行緒,不然沒有存在的意義。執行緒可以利用程序所擁有的資源,在引入執行緒的作業系統中,通常都是把程序作為分配資源的基本單位,而把執行緒作為獨立執行和獨立排程的基本單位,由於執行緒比程序更小,基本上不擁有系統資源,故對它的排程所付出的開銷就會小得多,能更高效的提高系統多個程式間併發執行的程度。

多執行緒

講到多那就不得不談一談兩個概念,一個是併發,一個是並行。
併發是指多個同一時刻,只能有一條指令被執行,但是多個指令被快速的輪換執行。

並行是指在同一時刻,多條指令在多個處理器上同時被執行。

為什麼我們只談了多執行緒而不談多程序呢,因為相比於程序來講,執行緒具有更高的效能,因為多執行緒的資料,記憶體等都是共享的,而程序,所有的都是獨立的,因此多執行緒的 併發效能要比多程序高的多。

執行緒建立的三種方式

那麼在Java中我們如何建立一個執行緒呢?Java提供了三種方式

繼承Thread類

1.定義Thread類的子類,並重寫run()方法。run()方法中的內容代表所要執行的任務。
2.建立子類例項。
3.呼叫start()方法啟動執行緒。

package org.xupt.thread;

public class ThreadExtend extends Thread{
    private int i;
    //重寫run()方法
    @Override
    public void run() {
        for (; i < 100; i++) {
            System.out.println(getName() + " " + i);
        }
    }
}

package org.xupt.thread;

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if(i == 10){
                new ThreadExtend().start();
                new ThreadExtend().start();
            }
        }
    }
}

執行上面的程式之後我們會看到雖然我們只建立了兩個執行緒,但是實際的程式裡有三個執行緒。這是因為我們的main方法的方法體本身本身也是一個執行緒。
上面的程式我們還用到了兩個方法。
Thread.currentThread():返回正在執行的執行緒物件

getName():該方法返回執行緒的名字。預設為(Thread-0、Thread-1、…)
當然如果如覺得名字不夠具體生動,我們可以用setName()方法設定你喜歡的名字。
**但是你有沒有發現什麼問題?**我們說程序中的資料是共享的但是兩個程序輸出時i都是從0開始的!!!這是為什麼?

你以為我要說什麼牛逼的答案,其實,就是因為你new了兩次物件,每次都建立了一個新的例項,他們兩個當然不會共享了,因為自己都有。所以這種方法建立的執行緒不能共享類的例項變數。

實現Runable介面

1.定義Runable的實現類,並重寫run()方法。

2.建立Runable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread才是真正的執行緒物件。

3.呼叫start()方法啟動執行緒。

package org.xupt.threadrunable;

public class ThreadRunable implements Runnable{
    public int i;
    @Override
    public void run() {
        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

package org.xupt.threadrunable;

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 10){
                ThreadRunable threadRunable = new ThreadRunable();
                new Thread(threadRunable,"Thread-1").start();
                new Thread(threadRunable,"Thread-2").start();

            }
        }
    }
}

執行後我們可以發現兩個子執行緒的i是共享的。

Runable介面中只含有一個run()抽象方法。介面使用了@FunctionalInterface修飾。所以我們可以通過Lambda表示式建立Runable物件。

package org.xupt.threadrunable;

public class RunableLambda {
    public static void main(String[] args) {
        Runnable r = () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
        };
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 10) {
                new Thread(r, "Thread-1").start();
                new Thread(r, "Thread-2").start();
            }

        }
    }
}

需要注意的是這裡的i不是共享的!!!

使用Callable和Future建立執行緒

我們不難看出上面的方法中Thread類的作用就是把run()方法包裝成執行緒執行體。那麼是不是可以把任意方法(包含返回值)都直接包裝成執行緒執行體呢?
從Java5開始,Java提供了一Callable介面(類似於Runable介面的增強版),提供了一個call()方法可以作為執行體,但是call()方法比run()方法的功能更加強大。

1.可以有返回值

2.可以丟擲異常

Java5提供了一個Future介面來代表Callable接口裡call()方法的返回值,併為Future介面提供了一個FutureTask實現類,改實現類實現了Future介面,並且實現了Runable介面可以作為Thread的target。
介面主要方法:
1.boolean cancel(boolean mayInterfaceIfRunning):試圖取消該Future裡的關聯的Callable任務

2.V get():返回Callable任務裡call()方法的返回值。呼叫該方法將導致執行緒阻塞,必須等到子執行緒結束後才會有返回值。

3.V get(long timeout,TimeUnit unit):返回call()方法的返回值。指定時間內無返回,將丟擲TimeoutException異常

4.boolean isCancelled():如果Callable任務正常完成前被取消返回True

5.boolean isDone():如果Callable任務正常完成,返回True

建立執行緒步驟如下:
1.建立Callable介面的實現類,重寫call()方法(可以有返回值),然後建立Callable的實現類的例項物件(使用Lamabd表示式)。
2.試用FutureTask類來包裝Callable物件,該FutureTask物件封裝了Callable物件的call()方法的返回值。
3.使用FutureTask物件作為Thread的target建立並啟動執行緒
4.呼叫FutureTask的get()方法來獲得返回值。

package org.xupt.threadcallable;

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

public class ThreadCallable {
    public static void main(String[] args) {
        FutureTask<Integer> integerFutureTask = new FutureTask<>((Callable<Integer>) () -> {
            int i = 0;
            for (; i < 100; i++) {
                System.out.println(Thread.currentThread().getName() + " " + i);
            }
            return i;
        });

        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 10) {
                new Thread(integerFutureTask, "Thread-1").start();
            }
        }
        //接收返回值
        try {
            System.out.println("Thread-1的返回值: " + integerFutureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

三種建立方式的比較

採用實現介面的方法建立執行緒的優缺點
1.只是實現了介面,還可以繼承其他類

2.多個執行緒可以共享一個target物件,所以適合多個執行緒來處理同一個資源,可以很好的將CPU、程式碼和資料分開,較好的體現面向物件的思想。

3.缺點就是程式設計比較複雜,如果需要訪問執行緒就需要使用Thread.currentThread().getName()方法。
採用繼承Thread類的方法建立執行緒的優缺點
1.程式設計簡單,訪問執行緒可以直接用this

2.缺點就是不可以再繼承其他類