天天看點

Java技術棧——Java多線程詳述一.多線程二、線程池

Java技術棧——Java多線程詳述

  • 一.多線程
    • 1.1多線程概述
    • 1.2 程式運作原理
      • 1.2.1 搶占式排程詳解
    • 1.3 主線程
    • 1.4 Thread類
    • 1.5 建立線程
      • 1.5.1 run()與start()
      • 1.5.2 繼承Thread類原理
      • 1.5.3 多線程的記憶體圖解
      • 1.5.4 擷取線程名稱
    • 1.6 建立線程方式—實作Runnable接口
      • 1.6.1 Runnable的優點
    • 1.7 線程的匿名内部類使用
  • 二、線程池
    • 2.1 線程池概念
    • 2.2 使用線程池方式--Runnable接口
    • 2.3 使用線程池方式—Callable接口

一.多線程

1.1多線程概述

    在了解學習多線程之前,我們先要熟悉了解幾個關于多線程有關的概念。

程序:程序指正在運作的程式。确切的來說,當一個程式進入記憶體運作,即變成一個程序,程序是處于運作過程中的程式,并且具有一定獨立功能。

Java技術棧——Java多線程詳述一.多線程二、線程池

線程:線程是程序中的一個執行單元,負責目前程序中程式的執行,一個程序中至少有一個線程。一個程序中是可以有多個線程的,這個應用程式也可以稱之為多線程程式。

Java技術棧——Java多線程詳述一.多線程二、線程池

    簡而言之:一個程式運作後至少有一個程序,一個程序中可以包含多個線程。什麼是多線程呢? 多線程定義:在一個程式中,這些獨立運作的程式片段叫作“線程”。即就是一個程式中有多個線程在同時執行。

    我們可以通過程式執行流程,來區分單線程程式與多線程程式的不同:

單線程程式:即,若有多個任務隻能依次執行。當上一個任務執行結束後,下一個任務開始執行。如接水,有一個水龍頭,一個人接完,下一個人才能開始接水。

多線程程式:即,若有多個任務可以同時執行。如在飲水機處接水,溫水處與熱水處可以同時放水。

1.2 程式運作原理

分時排程:所有線程輪流使用 CPU 的使用權,平均配置設定每個線程占用 CPU 的時間。分時排程可以在分時排程類中公平地分布處理資源。核心的其他部分可以在短時間内獨占處理器,而不會縮短使用者察覺的響應時間。在Java中可以設定一個或多個程序的優先級級别,優先級的級别範圍通常為 0 到 +10(不指定的話,java預設建立為5),值越低,優先級越高。

搶占式排程:這裡有必要說明一下,搶占式排程是實時排程的一種。優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會随機選擇一個(線程随機性),Java使用的為搶占式排程。

1.2.1 搶占式排程詳解

    現在大部分電腦作業系統都支援多程序并發運作,即支援多個軟體同時運作,比如打開了微信,并且同時聽着某易音樂,然後在CSDN上編寫分享部落格,“感覺上,這些運用在同時進行。”實際上,CPU(中央處理器)使用搶占式排程模式在多個線程間進行着高速的切換(針對于某一個核來說),我們根本就沒有察覺到,對于CPU的一個核而言,某個時刻,隻能執行一個線程,而 CPU的在多個線程間切換速度相對我們的感覺要快,看上去就是在同一時刻運作。是以,仔細想想就會發現,多線程程式并不能提高程式的運作速度,但能夠提高程式運作效率,讓CPU的使用率更高。

1.3 主線程

    再來看看我們最開始之前,學習常用的場景,當我們在dos指令行中輸入java空格類名回車後,啟動JVM,并且加載對應的class檔案。虛拟機并會從main方法開始執行我們的程式代碼,一直把main方法的代碼執行完成。如果在執行過程遇到循環時間比較長的代碼,那麼在循環之後的其他代碼是不會被馬上執行的。如下代碼示範:

class Demo{
	String name;
	Demo(String name){	
		this.name = name;
	}

	void show() {
		for (int i=1;i<=10000 ;i++ ) {
			System.out.println("name="+name+",i="+i);
		}
	}
}
class ThreadDemo {
	public static void main(String[] args) {
    	Demo demo = new Demo("CSDN");
		Demo demo2 = new Demo("NDSC");
		demo.show();
		demo2.show();
	}
}
           

    若在上述代碼中show方法中的循環執行次數很多,這時在demo.show();下面的代碼是不會馬上執行的,并且在dos視窗會看到不停的輸出name=CSDN,i=++,這樣的語句。是因為:jvm啟動後,必然有一個執行路徑(線程)從main方法開始的,一直執行到main方法結束,這個線程在java中稱之為主線程。當程式的主線程執行時,如果遇到了循環而導緻程式在指定位置停留時間過長,則無法馬上執行下面的程式,需要等待循環結束後能夠執行。

    那麼,能否實作一個主線程負責執行其中一個循環,再由另一個線程負責其他代碼的執行,最終實作多部分代碼同時執行的效果?當然可以,Java中的多線程技術能夠實作同時執行。

1.4 Thread類

    該如何建立線程呢?通過API中搜尋,查到Thread類。通過閱讀Thread類中的描述,我們能夠了解到Thread是程式中的執行線程,并且Java 虛拟機是支援并允許應用程式并發地運作多個執行線程。

構造方法

Thread()構造方法摘要 描述
Thread() 配置設定線程對象
Thread(String name) 配置設定新的線程對象,将指定的name作為其線程名稱

常用方法

類型 名稱 描述
void start() 使該線程開始執行;Java虛拟機實際上調用的是該線程的run()方法
void run() 該線程具體要執行的操作
static void sleep(long m) 讓目前正在執行的線程休眠毫秒,(暫停執行)

1.5 建立線程

    建立新執行線程有兩種方法。

繼承Thread:将類聲明為 Thread 的子類。該子類應重寫 Thread 類的 run 方法。建立對象,開啟線程。run方法相當于其他線程的main方法。

繼承Thread建立線程的步驟:

1 定義一個類繼承Thread。

2 重寫run方法。

3 建立子類對象,就是建立線程對象。

4 調用start方法,開啟線程并讓線程執行,此時jvm會去調用run方法。

實作Runnable:聲明一個實作 Runnable 接口的類,該類會實作 run 方法,然後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啟線程。

main

//測試類
public class Demo {
	public static void main(String[] args) {
		ThreadDemo td = new ThreadDemo("線程Demo!");
		//開啟新線程
		td.start();
		//在主方法中執行for循環
		for (int i = 0; i < 10; i++) {
			System.out.println("main線程!"+i);
		}
	}
}
           

建立線程類

//建立線程類
public class ThreadDemo extends ThreadDemo {
	public ThreadDemo (String name) {
	//調用父類的String參數的構造方法,指定線程的名稱
		super(name);
	}
	//重寫run方法
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在執行!"+i);
		}
	}
}
           

1.5.1 run()與start()

注意:線程對象調用 run方法和調用start方法差別?

    在了解這個問題之前,我們應該清楚什麼是run()方法,什麼是start()方法;

run():就是繼承Thread類,或者實作runnable要實作的方法,本質上是一個成員函數,但并不是多線程的方式,就是一個普通的方法。我們從源碼就能看出就是簡單的普通方法的調用。

run()源碼

@Override
    public void run() {
     // 簡單的運作,不會新起線程,target 是 Runnable
        if (target != null) {
            target.run();
        }
    }
           

start():要了解start方法,我們最好從源碼入手,start 方法的源碼也沒幾行代碼,注釋也比較詳細,最主要的是 start0() 方法。

start()源碼

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
     // 沒有初始化,抛出異常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);
	// 是否啟動的辨別符
    boolean started = false;
    try {
     // start0() 是啟動多線程的關鍵
     // 這裡會建立一個新的線程,是一個 native 方法
     // 執行完成之後,新的線程已經在運作了
        start0();
        // 主線程執行
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
           

start0()源碼

    start0()的源碼隻有一行,為了友善了解start執行過程以及日後的學習,總結了start0的每一步調用執行過程。

  • Step1::start0方法實際上調用的是jvm.cpp檔案的JVM_StartThread方法,(是否建立線程,以及能否建立線程,比如記憶體已滿,無法繼續建立新的線程,都是在這一步進行判斷的);
  • Step2:如果能建立,則調用JavaThread方法建立線程(包括初始化相關變量),就在建立線程的時候,傳入了java_start,做為線程運作函數的初始位址;
  • Step3:當子線程完成初始化之後,父線程會執行Thread::start方法,設定線程狀态為RUNNABLE;
  • Step4:這時候子線程就可以開始執行thread->run()方法了。
    Java技術棧——Java多線程詳述一.多線程二、線程池

    補充:start0為什麼會被标記成native本地方法。衆所周知,Java其最大的優點之一就是跨平台性,start() 方法調用 start0() 方法後,該線程并不一定會立馬執行,隻是将線程變成了可運作狀态(NEW —> RUNNABLE)。具體什麼時候執行,取決于 CPU ,由 CPU 統一排程。可以在不同系統上運作,每個系統的 CPU 排程算法不一樣,是以就需要做不同的處理,這件事情就隻能交給 JVM 來實作了,start0() 方法自然就表标記成了 native。

    是以綜上,線程對象調用run方法不開啟線程,僅是對象調用方法。線程對象調用start開啟線程,并讓jvm調用run方法在開啟的線程中執行。

1.5.2 繼承Thread類原理

    我們為什麼要繼承Thread類,并調用其的start方法才能開啟線程呢?繼承Thread類:因為Thread類用來描述線程,具備線程應該有的功能。那為什麼不直接建立Thread類的對象呢?如下代碼:

Thread t1 = new Thread();
//這樣做沒有錯,但是該start調用的是Thread類中的run方法
//這個run方法沒有做什麼事情,更重要的是這個run方法中并沒有定義我們需要讓線程執行的代碼。
t1.start();
           

    建立線程是為了建立程式單獨的執行路徑,讓多部分代碼實作同時執行。也就是說線程建立并執行需要給定線程要執行的任務。對于之前所講的主線程,它的任務定義在main函數中。自定義線程需要執行的任務都定義在run方法中。Thread類run方法中的任務并不是我們所需要的,隻有重寫這個run方法。既然Thread類已經定義了線程任務的編寫位置(run方法),那麼隻要在編寫位置(run方法)中定義任務代碼即可。是以進行了重寫run方法動作。

1.5.3 多線程的記憶體圖解

    多線程執行時,在記憶體中的運作方式其實很簡單:多線程執行時,在棧記憶體中,其實每一個執行線程都有一片自己所屬的棧記憶體空間。進行方法的壓棧和彈棧。當執行線程的任務結束了,線程自動在棧記憶體中釋放了。但是當所有的執行線程都結束了,那麼程序就結束了。

Java技術棧——Java多線程詳述一.多線程二、線程池

1.5.4 擷取線程名稱

    開啟的線程都會有自己的獨立運作棧記憶體,而這些線程都是有其預設的名字的,當然也可以自定義線程名。根據Thread類的API文檔整理如下。

函數名 功能
Thread.currentThread() 擷取目前線程對象
Thread.currentThread().getName() 擷取目前線程對象的名稱
class MyThread extends Thread { 
   MyThread(String name){
   	super(name);
   }
   //複寫其中的run方法
   public void run(){
   	for (int i=1;i<=100 ;i++ ){
   		System.out.println(Thread.currentThread().getName()+",i="+i);
   	}
   }
}

class ThreadDemo {
   public static void main(String[] args) {
   	//建立兩個線程任務
   	MyThread d = new MyThread();
   	MyThread d2 = new MyThread();
   	//沒有開啟新線程, 在主線程調用run方法
   	d.run();
   	//開啟一個新線程,新線程調用run方法
   	d2.start();
   }
}
           

    通過結果觀察,原來主線程的名稱:main;自定義的線程:Thread-0,線程多個時,數字順延。如Thread-1…注意:進行多線程程式設計時,不要忘記了Java程式運作是從主線程開始,main方法就是主線程的線程執行内容。

1.6 建立線程方式—實作Runnable接口

    建立線程的另一種方法是聲明實作 Runnable 接口的類。該類然後實作 run 方法。然後建立Runnable的子類對象,傳入到某個線程的構造方法中,開啟線程。檢視Runnable接口說明文檔:Runnable接口用來指定每個線程要執行的任務。包含了一個 run 的無參數抽象方法,需要由接口實作類重寫該方法。

實作Runnable接口,建立線程的步驟:

  • 1、定義類實作Runnable接口。
  • 2、覆寫接口中的run方法。。
  • 3、建立Thread類的對象
  • 4、将Runnable接口的子類對象作為參數傳遞給Thread類的構造函數。
  • 5、調用Thread類的start方法開啟線程。

示例

public class RunnableDemo {
	public static void main(String[] args) {
		//建立線程執行目标類對象
		Runnable runnable = new MyRunnable();
		//将Runnable接口的子類對象作為參數傳遞給Thread類的構造函數
		Thread thread = new Thread(runn);
		Thread thread2 = new Thread(runn);
		//開啟線程
		thread.start();
		thread2.start();
		for (int i = 0; i < 10; i++) {
			System.out.println("main線程:正在執行!"+i);
		}
	}
}
           

線程執行類示例

public class MyRunnable implements Runnable{
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println("線程!"+i);
		}
	}
}
           

1.6.1 Runnable的優點

    第二種方式實作Runnable接口避免了單繼承的局限性,是以較為常用。實作Runnable接口的方式,更加的符合面向對象,線程分為兩部分,一部分線程對象,一部分線程任務。繼承Thread類,線程對象和線程任務耦合在一起。一旦建立Thread類的子類對象,既是線程對象,有又有線程任務。實作runnable接口,将線程任務單獨分離出來封裝成對象,類型就是Runnable接口類型。Runnable接口對線程對象和線程任務進行解耦。

1.7 線程的匿名内部類使用

    使用線程的内匿名内部類方式,可以友善的實作每個線程執行不同的線程任務操作。

  • 方式1:建立線程對象時,直接重寫Thread類中的run方法
new Thread() {
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(Thread.currentThread().getName()+ ">>>>" + i);
		}
	}
}.start();
           
  • 方式2:使用匿名内部類的方式實作Runnable接口,重新Runnable接口中的run方法
Runnable runnable = new Runnable() {
	public void run() {
		for (int i = 0; i < 100; 1x++) {
			System.out.println(Thread.currentThread().getName()+ ">>>>" + i);
		}
	}
};
new Thread(runnable ).start();
           

二、線程池

2.1 線程池概念

    線程池,其實就是一個容納多個線程的容器,其中的線程可以反複使用,省去了頻繁建立線程對象的操作,無需反複建立線程而消耗過多資源。

    在java中,如果每個請求到達就建立一個新線程,開銷是相當大的。在實際使用中,建立和銷毀線程花費的時間和消耗的系統資源都相當大,甚至可能要比在處理實際的使用者請求的時間和資源要多的多。除了建立和銷毀線程的開銷之外,活動的線程也需要消耗系統資源。如果在一個jvm裡建立太多的線程,可能會使系統由于過度消耗記憶體或“切換過度”而導緻系統資源不足。為了防止資源不足,需要采取一些辦法來限制任何給定時刻處理的請求數目,盡可能減少建立和銷毀線程的次數,特别是一些資源耗費比較大的線程的建立和銷毀,盡量利用已有對象來進行服務。

    線程池主要用來解決線程生命周期開銷問題和資源不足問題。通過對多個任務重複使用線程,線程建立的開銷就被分攤到了多個任務上了,而且由于在請求到達時線程已經存在,是以消除了線程建立所帶來的延遲。這樣,就可以立即為請求服務,使用應用程式響應更快。另外,通過适當的調整線程中的線程數目可以防止出現資源不足的情況。

2.2 使用線程池方式–Runnable接口

    通常,線程池都是通過線程池工廠建立,再調用線程池中的方法擷取線程,再通過線程去執行任務方法。

Executors:線程池建立工廠類

  • public static ExecutorService newFixedThreadPool(int nThreads):傳回線程池對象
  • ExecutorService:線程池類
  • Future<?> submit(Runnable task):擷取線程池中的某一個線程對象,并執行
  • Future接口:用來記錄線程任務執行完畢後産生的結果。線程池建立與使用

使用線程池中線程對象的步驟,代碼示例:

public class ThreadPoolDemo {
	public static void main(String[] args) {
	//建立線程池,包含10個線程
	ExecutorService service = Executors.newFixedThreadPool(10);
	RunnableDemo rd = new RunnableDemo ();
	//從線程池中擷取線程對象,然後調用RunnableDemo 中的run()
	service.submit(rd);
	//注意:submit方法調用結束後,程式并不終止,是因為線程池控制了線程的關閉。将使用完的線程又歸還到了線程池中
	//關閉線程池
	//service.shutdown();
	}
}
           

Runnable接口實作類

public class RunnableDemo implements Runnable {
	@Override
	public void run() {
		System.out.println("線程示例啟動");
		System.out.println("線程: " +Thread.currentThread().getName());
		System.out.println("線程關閉"+Thread.currentThread().getName());
	}
}
           

2.3 使用線程池方式—Callable接口

lCallable接口:與Runnable接口功能相似,用來指定線程的任務。其中的call()方法,用來傳回線程任務執行完畢後的結果,call方法可抛出異常。

  • Future submit(Callable task):擷取線程池中的某一個線程對象,并執行線程中的call()方法
  • Future接口:用來記錄線程任務執行完畢後産生的結果。

    代碼示例:

public class ThreadPoolDemo {
	public static void main(String[] args) {
		//建立線程池,包含10個線程
		ExecutorService service = Executors.newFixedThreadPool(2);//包含2個線程對象
		//建立Callable對象
		CallableDemo cd = new CallableDemo ();
		service.submit(c);
		service.submit(c);		
	}
}
           

Callable接口實作類,call方法可抛出異常、傳回線程任務執行完畢後的結果

public class CallableDemo implements Callable {
@Override
public Object call() throws Exception {
	System.out.println("線程示例:call");
	System.out.println("線程: " +Thread.currentThread().getName());
	System.out.println("線程結束:"+Thread.currentThread().getName());
	return null;
	}
}