1. 程式人生 > >java 多執行緒簡介

java 多執行緒簡介

程序和執行緒

程式(program)是對資料描述與操作的程式碼的集合,是應用程式執行的指令碼。

程序(process)程式的一次執行過程,是系統執行程式的基本單位。程式是靜態的,程序是動態的。系統執行一個程式即是一個程序從建立、執行到消亡的過程。可以認為每個程式就是一個程序,但有些應用程式會有多個程序,即一個應用程式至少會啟動一個程序.,多程序的實現基礎是CPU時間片輪訓或者搶佔式執行

執行緒(thread)是程序中的一個執行場景,一個程序可以啟動多個執行緒,使用多執行緒可以提高CPU的使用率,不是提高執行速度。

執行緒是程序內部的一個獨立執行單元,相當於一個子程式,一個程序中的所有執行緒都在該程序的虛擬地址空間中,

使用該程序的全域性變數和系統資源,多執行緒能實現的基礎是CPU輪訓到該程序時,該程序又分為多個時間片(對應每個執行緒),在這個程序執行的時間裡輪流執行

舉個例子,我們知道機械磁碟的IO時間大概是ms級別,CPU處理速度是ns級別,電腦要同時讀取兩個壓縮檔案並解壓,讀取每個檔案需要10秒,解壓操作需要5秒,先讀去A,再讀取B的這段時間解壓A,再解壓B,可以將讀取B檔案的空餘時間充分利用

 

多工(multi task)一個系統中可以同時執行多個程式,即有多個獨立執行的任務,每個任務對應一個程序

       程序示例:下圖顯示了多個程式同時執行(多執行緒),每個程式可能會有多個程序.

 

並行和併發

並行是指兩個或兩個以上的任務同時執行,即A任務在執行,B任務也在執行,C任務也在執行,常指程序(需要多核CPU或者多臺電腦,比如常見的Xgboost可以並行執行一個演算法)

併發是指在同一時間片段同時執行,比如兩個任務請求執行,CPU只能先執行一個,將兩個任務輪流執行,類似於序列.

      上述一個程式最少有一個程序(可以多個),一個程序最少有一個執行緒(可以多個),多個程序之間相互獨立,可以並行執行,多個執行緒只能併發執行,實際還是順序執行,單核的cpu同一個時刻只支援一個執行緒任務,多執行緒併發就是多個執行緒排隊申請呼叫cpu

 

以下重點論述多執行緒:

優缺點:

多執行緒的優點

提高CPU利用率;可以隨時停止任務;可以設定各個任務的優先順序以優化效能,提高程式的執行效率;執行緒執行完成後會自動銷燬節省記憶體。

多執行緒的缺點:

設計複雜:多執行緒共享堆記憶體和方法區,因此裡面的一些資料是可以共享的,在設計時要確保資料的準確性

資源消耗增多:多執行緒不共享棧記憶體,開啟多執行緒會增加記憶體的消耗

 

建立多執行緒的三種方式

 

1Thread類

繼承Thread可以建立執行緒,2中的單獨實現Runnable介面並不能建立執行緒,還要藉助Thread類的物件

使用Thread類建立執行緒的方式:

1自定義類繼承Thread類
2.重寫run方法
3.在run方法中編寫執行緒中執行的程式碼
4.建立上面自定義類的物件
5.呼叫start方法啟動執行緒

先看Thread的常用的構造方法:

Thread() 建立新的執行緒物件

Thread(String name) 基於指定的名字建立一個執行緒物件

Thread(Runnable target)基於Runnable介面實現類的例項(可以是匿名內部類)建立一個執行緒物件

Thread(Runnable t,String name) 根據給定的Runnable介面實現類的例項和指定的名字建立一個執行緒物件

常用方法:

void run() :包括執行緒執行時執行的程式碼,通常在子類中重寫它。

synchronized void start():啟動一個新的執行緒,然後虛擬機器呼叫新執行緒的run方法

程式碼示例

package createThreads;

public class Thread1 {
    public static void main(String[] args){
        FirstThread ft=new FirstThread();
        ft.start();
        for (int i=0;i<50;i++){
            System.out.println( Thread.currentThread().getName()+" : "+i);
        }

    }

}
class FirstThread extends Thread{

    @Override
    public void run() {
       for (int i=0;i<50;i++){
           System.out.println( Thread.currentThread().getName()+" : "+i);
       }
    }
}
out:
Thread-0 : 0
main : 0
Thread-0 : 1
main : 1
Thread-0 : 2
Thread-0 : 3
Thread-0 : 4
Thread-0 : 5
Thread-0 : 6
Thread-0 : 7
main : 2
Thread-0 : 8
main : 3
Thread-0 : 9
main : 4
main : 5
main : 6
main : 7
main : 8
main : 9

可以看到兩個執行緒的方法在同時執行,如果將執行緒啟動語句放到for迴圈後面,則不會多執行緒列印,原因是main方法的內容是順序執行的,只有start()方法在main方法體中前面部分,執行緒啟動後繼續執行後面的語句,才能實現多執行緒交替列印

練習,不考慮執行緒安全的問題,讓兩個執行緒一起列印0-100直接的數字,兩種方式,共享靜態變數,共享堆記憶體中的物件(屬性和方法)

1靜態變數共享,通過共享靜態變數的方式

public class ThreadTest01 {

    public static void main(String[] args){
        int i=0;
        MyThread m1=new MyThread("執行緒1",i);
        MyThread m2=new MyThread("執行緒2",i);
        m1.start();
        m2.start();

    }
}
class MyThread extends Thread{

    static int i;
    public MyThread(String name,int i){
        super(name);
        this.i=i;
    }

    @Override
    public void run() {
       for (;i<100;i++){
           System.out.println(getName()+" : "+i);
       }
    }
}

改進版:在上述程式碼上稍作精簡

public class ThreadTest02 {
    public static void main(String[] args){
        MyThread1 m1=new MyThread1("執行緒1");
        MyThread1 m2=new MyThread1("執行緒2");
        m1.start();
        m2.start();
    }
}
class MyThread1 extends  Thread{
    private static int i;
    public MyThread1(String name){
        super(name);
        setI(0);
    }

    public static int getI() {
        return i;
    }

    public static void setI(int i) {
        MyThread.i = i;
    }

    @Override
    public void run() {
        for (;i<100;i++){
            System.out.println( getName()+" :"+i);
        }
    }
}

物件成員變數共享,這種方法在定義類構造方法時,接收一個物件,共用物件的成員變數,這種方式稍顯麻煩

public class ThreadTest03 {
    int i=0;
    public static void main(String[] args){
        ThreadTest03 tt03=new ThreadTest03();
        MyThread2 mt1= new MyThread2("執行緒1",tt03);
        MyThread2 mt2=new MyThread2("執行緒2",tt03);
        mt1.start();
        mt2.start();


    }
}

class MyThread2 extends Thread{
    ThreadTest03 tt;
    public MyThread2(String name,ThreadTest03 tt03){
        super(name);
        tt=tt03;
    }

    @Override
    public void run() {
        for (;tt.i<100;tt.i++){
            System.out.println(getName()+" : "+tt.i);
        }
    }
}

 

2Runnable介面

Runnable介面的原始碼:

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

Runnable 介面中只有一個 run抽象 方法,實現該介面的類要重寫該方法.

使用Runnable介面建立執行緒的步驟:

.1自定義類實現Runnable介面
2.重寫run方法,run方法中是執行緒體
4.建立上述自定義類的物件
5.建立Thread物件,將上述自定義類物件作為引數傳入Thread的構造方法
6.呼叫start方法啟動執行緒

Thread類的其中一個構造方法

Thread(Runnable target)基於Runnable介面實現類的例項(可以是匿名內部類)建立一個執行緒物件

使用示例:兩個執行緒各自列印0-20,不考慮執行緒安全

public class RunnableTest01 {
    public static void main(String[] args){
    MyRunnable mr=new MyRunnable();
    Thread t1=new Thread(mr);
    Thread t2=new Thread(mr);
    t1.start();
    t2.start();

    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<20;i++){
            System.out.println( Thread.currentThread().getName()+" : "+i);
        }

    }
}
out:
Thread-1 : 0
Thread-0 : 0
Thread-1 : 1
Thread-0 : 1
Thread-1 : 2
Thread-0 : 2
Thread-1 : 3
Thread-0 : 3
Thread-1 : 4
Thread-0 : 4
Thread-1 : 5
Thread-0 : 5
Thread-1 : 6
Thread-0 : 6
Thread-1 : 7
Thread-0 : 7
Thread-1 : 8
Thread-0 : 8
Thread-1 : 9
Thread-0 : 9
Thread-1 : 10
Thread-0 : 10
Thread-1 : 11
Thread-0 : 11
Thread-1 : 12
Thread-0 : 12
Thread-1 : 13
Thread-0 : 13
Thread-1 : 14
Thread-0 : 14
Thread-0 : 15
Thread-0 : 16
Thread-1 : 15
Thread-0 : 17
Thread-1 : 16
Thread-0 : 18
Thread-1 : 17
Thread-0 : 19
Thread-1 : 18
Thread-1 : 19

接上面的練習,實現Runnable介面,兩個執行緒共同列印0-50,不考慮執行緒安全,程式碼要簡潔的多

public class RunnableTest02 {
    public static void main(String[] args){
        MyRunnable1 mr1=new MyRunnable1();
        Thread t1=new Thread(mr1,"執行緒1");
        Thread t2=new Thread(mr1,"執行緒2");
        t1.start();
        t2.start();
    }
}

class MyRunnable1 implements  Runnable{
    int i=0;
    @Override
    public void run() {
        for (;i<50;i++){
            System.out.println( Thread.currentThread().getName()+" : "+i);
        }
    }
}

Thread類和Runnable介面的區別:

  • 1 Runnable介面更適合資源的共享
  • 2 java的單繼承,多實現功能,繼承了其他類的類可以實現Runnable介面(必須重寫run方法),而Thread類的繼承類可以不重寫run方法
  • 3 Runnable介面的實現類,並不是真正的執行緒類,只是執行緒執行的目標類,若要以執行緒的方式執行run方法,需要依賴Thread類(及其子類)

 

3Callable介面

jdk1.5加入了Callable介面,實現Callable介面建立的執行緒會獲取一個返回值,並且可以宣告異常(Callable介面再java.lang包)

在敘述Cllable介面建立執行緒之前,先介紹一個概念(在這裡不深入探究,以後寫篇文章專門敘述這塊內容):

執行緒池: 

          新來一個任務,要建立一個執行緒,執行緒任務結束後,執行緒會被銷燬,資源回收。多次這樣建立執行緒,銷燬執行緒的話帶來嚴重的系統開銷,同時也不好管理工作執行緒。執行緒池解決了建立單個執行緒耗費時間和資源的問題

執行緒池是一種預建立執行緒的技術,先線上程池建立多個執行緒的集合,當執行完任務需要建立一個執行緒時,不需要再建立一個執行緒,而是直接去執行緒集合中獲取,當任務結束時,不銷燬這個執行緒,將這個執行緒放入執行緒池管理,等待執行緒池任務排程.

建立執行緒池的方式:

Executor介面,該介面只有一個接收Runnable物件的execute方法

ExecutorService是執行緒池介面,繼承自Executor介面,ExecutorService裡面有操作執行緒池的方法

  • Future<T> submit(Callable<T> task),傳入Callable物件的實現類,即提交任務,返回Future<T>型別物件
  • void shutdown(),關閉執行緒池,不會再接收新的執行緒,未執行完成的執行緒不會被關閉

Executers:Executers類提供了四種執行緒池,有許多方法,其中列舉幾個需要用到的

  • static ExecutorService newFixedThreadPool(int nThreads)返回一個建立好的固定執行緒個數的執行緒池物件,如果任務數量大於執行緒數量,則任務會進行等待。
  • public static ExecutorService newCachedThreadPool(),返回執行緒池物件,該執行緒池物件根據需要建立執行緒個數,如果執行緒池內執行緒個數小於人物個數則會建立執行緒,,最大執行緒數量是Integer.MAX_VALUE,如果執行緒的處理速度小於任務的提交速度時,會不斷建立新的執行緒來執行任務,可能會因為建立執行緒過多而耗盡系統資源(CPU和記憶體)

Future<T>介面的實現類用來接收多執行緒的執行結果

V get() throws InterruptedException, ExecutionException;get方法用來接收Callable實現類的call方法的返回值

Callable介面

public interface Callable<V> {
    V call() throws Exception;
}
Callable介面只有一個call方法,返回一個泛型型別的物件,並且可以丟擲異常,實現Callable介面,重寫call方法可以建立執行緒

使用Callable介面建立執行緒的步驟:

  • 1建立自定義類實現Callable介面,並重寫call方法(執行緒體),call方法有返回值,且要宣告或捕獲異常
  • 2建立ExecutorService執行緒池物件
  • 3.將自定義類的物件放入執行緒池裡面
  • 4.獲取執行緒的返回結果
  • 5.關閉執行緒池,不再接收新的執行緒,未執行完的執行緒不會被關閉

使用示例:

package createThreads;

import java.util.concurrent.*;

public class CallableTest01 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立執行緒池物件,使用了多型
        ExecutorService es=Executors.newFixedThreadPool(3);
        //建立Callable介面的實現類物件
        MyCallable mc1=new MyCallable(5);
        MyCallable mc2=new MyCallable(4);
        MyCallable mc3=new MyCallable(3);
        //將自定義物件方法執行緒池中
        Future <Integer>f1=es.submit(mc1);
        Future <Integer>f2=es.submit(mc2);
        Future <Integer>f3=es.submit(mc3);
        //輸出get方法返回的結果
        System.out.println(f1.get()+":"+f2.get()+":"+f3.get());
       //關閉執行緒池
        es.shutdown();



    }
}
class MyCallable implements Callable<Integer>{
    private  int count;

    public MyCallable(int count) {
        setCount(count);
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }
//重寫call方法
    @Override
    public Integer call() throws Exception {
        //計算立方
        int temp=0;
        temp=count*count*count;

        return temp;
    }
}
out:
125:64:27

三種基本的建立方式介紹完了,回過頭來看,三種方式建立執行緒的優缺點

繼承Thread

      優點:可以直接使用Thread類中的方法,重寫run方法,程式碼簡單

      缺點:繼承Thread類之後不能繼承其他類,且不方便資料共享

實現Runnable介面

    優點:在自定義類有繼承父類的情況下,還可以使用Runnable介面,重寫run方法,且方便資料共享

     缺點: 在run方法內部需要獲取到當前執行緒的Thread物件後才能使用Thread中的方法

實現Callable介面

      優點:可以獲取返回值,可以丟擲異常

      缺點:編寫程式碼比較繁瑣

 
參考:http://www.monkey1024.com/javase/655

          http://www.monkey1024.com/javase/653