Java多線程(1)-- 基本概念

一、使用線程

有三種使用線程的方法:

* 實現 Runnable 接口;

* 實現 Callable 接口;

* 繼承 Thread 類。

實現 Runnable 和Callable 接口的類只能當做一個可以在線程中運行的任務,不是真正意義上的線程,因此最后還需要通過 Thread來調用??梢哉f任務是通過線程驅動從而執行的。


1、實現 Runnable 接口

需要實現 run() 方法。

通過 Thread 調用start() 方法來啟動線程。

public class MyRunnable implements Runnable {

??? public void run() {

??????? // ...

??? }

}

public static void main(String[] args) {

??? MyRunnable instance = new MyRunnable();

??? Thread thread = new Thread(instance);

??? thread.start();

}


2、實現 Callable 接口

與 Runnable 相比,Callable可以有返回值,返回值通過FutureTask 進行封裝。

public class MyCallable implements Callable {

??? public Integer call() {

??????? return 123;

??? }

}

public static void main(String[] args) throws ExecutionException, InterruptedException {

??? MyCallable mc = new MyCallable();

??? FutureTask ft = newFutureTask<>(mc);

??? Thread thread = new Thread(ft);

??? thread.start();

??? System.out.println(ft.get());

}


Callable與Runnable

???? 先說一下java.lang.Runnable吧,它是一個接口,在它里面只聲明了一個run()方法:

public interface Runnable {

????? public abstract void run();

}

由于run()方法返回值為void類型,所以在執行完任務之后無法返回任何結果。


Callable位于java.util.concurrent包下,它也是一個接口,在它里面也只聲明了一個方法,只不過這個方法叫做call():

public interface Callable {

??? /**

???? * Computes a result, orthrows an exception if unable to do so.

???? *

???? * @return computedresult

???? * @throws Exception ifunable to compute a result

???? */

??? V call() throwsException;

}

可以看到,這是一個泛型接口,call()函數返回的類型就是傳遞進來的V類型。

那么怎么使用Callable呢?一般情況下是配合ExecutorService來使用的,在ExecutorService接口中聲明了若干個submit方法的重載版本:

Future submit(Callable task);

Future submit(Runnable task, T result);

Future submit(Runnable task);


Future

Future就是對于具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成、獲取結果。必要時可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。

Future類位于java.util.concurrent包下,它是一個接口,在Future接口中聲明了5個方法,下面依次解釋每個方法的作用:

cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。

isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。

isDone方法表示任務是否已經完成,若任務完成,則返回true;

get()方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。

也就是說Future提供了三種功能:

  1)判斷任務是否完成;

  2)能夠中斷任務;

  3)能夠獲取任務執行結果。

因為Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了下面的FutureTask。


FutureTask

先來看一下FutureTask的實現:

public class FutureTask implementsRunnableFuture

FutureTask類實現了RunnableFuture接口,我們看一下RunnableFuture接口的實現:

public interface RunnableFuture extends Runnable,Future {

??? void run();

}

可以看出RunnableFuture繼承了Runnable接口和Future接口,而FutureTask實現了RunnableFuture接口。所以它既可以作為Runnable被線程執行,又可以作為Future得到Callable的返回值。FutureTask 可用于異步獲取執行結果或取消執行任務的場景。當一個計算任務需要執行很長時間,那么就可以用 FutureTask 來封裝這個任務,主線程在完成自己的任務之后再去獲取結果。

事實上,FutureTask是Future接口的一個唯一實現類。


使用示例:

1.使用Callable+Future獲取執行結果

public class Test {

??? public static voidmain(String[] args) {

??????? ExecutorServiceexecutor = Executors.newCachedThreadPool();

??????? Task task = newTask();

???????Future result = executor.submit(task);

??????? executor.shutdown();

??? }


??? try {

???????System.out.println("task運行結果"+result.get());

??? } catch(InterruptedException e) {

??????? e.printStackTrace();

??? } catch(ExecutionException e) {

??????? e.printStackTrace();

??? }

}


class Task implements Callable{

??? @Override

??? public Integer call()throws Exception {

???????System.out.println("子線程在進行計算");

??????? Thread.sleep(3000);

??????? int sum = 0;

??????? for(inti=0;i<100;i++)

??????????? sum += i;

??????? return sum;

??? }

}???


2.使用Callable+FutureTask獲取執行結果

public class Test {

??? public static voidmain(String[] args) {

??????? //第一種方式

??????? ExecutorServiceexecutor = Executors.newCachedThreadPool();

??????? Task task = newTask();

???????FutureTask futureTask = newFutureTask(task);

???????executor.submit(futureTask);

??????? executor.shutdown();

??? }

? ? ?try {

??????System.out.println("task運行結果"+futureTask.get());

???? } catch(InterruptedException e) {

?????? e.printStackTrace();

???? } catch(ExecutionException e) {

?????? e.printStackTrace();

???? }

}

class Task implements Callable{

??? @Override

??? public Integer call()throws Exception {

???????System.out.println("子線程在進行計算");

??????? Thread.sleep(3000);

??????? int sum = 0;

??????? for(inti=0;i<100;i++)

??????????? sum += i;

??????? return sum;

??? }

}

3、繼承 Thread

同樣也是需要實現 run() 方法,因為 Thread類也實現了Runable 接口。

當調用 start() 方法啟動一個線程時,虛擬機會將該線程放入就緒隊列中等待被調度,當一個線程被調度時會執行該線程的 run() 方法。

public class MyThread extends Thread {

??? public void run() {

??????? // ...

??? }

}

public static void main(String[] args) {

??? MyThread mt = new MyThread();

??? mt.start();

}


實現接口 VS 繼承Thread

實現接口會更好一些,因為:

Java 不支持多重繼承,因此繼承了 Thread類就無法繼承其它類,但是可以實現多個接口;

類可能只要求可執行就行,繼承整個 Thread 類開銷過大。


Executor

Executor 管理多個異步任務的執行,而無需程序員顯式地管理線程的生命周期。這里的異步是指多個任務的執行互不干擾,不需要進行同步操作。

主要有三種 Executor:

CachedThreadPool:一個任務創建一個線程;

FixedThreadPool:所有任務只能使用固定大小的線程;

SingleThreadExecutor:相當于大小為 1 的FixedThreadPool。


Executors.newCachedThreadPool();??????? //創建一個線程池,容量大小為????????

????????????????????????????????????????????????????????????????????Integer.MAX_VALUE

Executors.newFixedThreadPool(int);??? //創建固定容量大小的線程池

Executors.newSingleThreadExecutor();?? //創建容量為1的線程池


public static void main(String[] args) {

??? ExecutorService executorService =Executors.newCachedThreadPool();

??? for (int i = 0; i < 5; i++) {

??????? executorService.execute(newMyRunnable());

??? }

??? executorService.shutdown();

}


守護線程(Daemon):

所謂守護線程是指在程序運行的時候在后臺提供一種通用服務的線程,比如垃圾回收線程就是一個很稱職的守護者,并且這種線程并不屬于程序中不可或缺的部分。

在Java中有兩類線程:User Thread(用戶線程)、Daemon

Thread(守護線程)

用個比較通俗的比如,任何一個守護線程都是整個JVM中所有非守護線程的保姆;

只要當前JVM實例中尚存在任何一個非守護線程沒有結束,守護線程就全部工作;只有當最后一個非守護線程結束時,守護線程隨著JVM一同結束工作。

守護線程與普通線程的唯一區別是:當JVM中所有的線程都是守護線程的時候,JVM就可以退出了;如果還有一個或以上的非守護線程則不會退出。(以上是針對正常退出,調用System.exit則必定會退出)

守護線程并非只有虛擬機內部提供,用戶在編寫程序時也可以自己設置守護線程。用戶可以用Thread的setDaemon(true)方法設置當前線程為守護線程。setDeamon(true)的唯一意義就是告訴JVM不需要等待它退出,讓JVM喜歡什么退出就退出吧,不用管它。

Thread daemonTread= new Thread();?

? //設定 daemonThread 為 守護線程,default false(非守護線程)?

?daemonThread.setDaemon(true);?

?//驗證當前線程是否為守護線程,返回 true 則為守護線程?

?daemonThread.isDaemon();?


這里有幾點需要注意:

(1) thread.setDaemon(true)必須在thread.start()之前設置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置為守護線程。

(2) 在Daemon線程中產生的新線程也是Daemon的。

(3) 不要認為所有的應用都可以分配給Daemon來進行服務,比如讀寫操作或者計算邏輯。

因為你不可能知道在所有的User完成之前,Daemon是否已經完成了預期的服務任務。一旦User退出了,可能大量數據還沒有來得及讀入或寫出,計算任務也可能多次運行結果不一樣。這對程序是毀滅性的。造成這個結果理由已經說過了:一旦所有User Thread離開了,虛擬機也就退出運行了。


二、線程狀態

????? Java中線程中狀態可分為五種:New(新建狀態),Runnable(就緒狀態),Running(運行狀態),Blocked(阻塞狀態),Dead(死亡狀態)。

  New:新建狀態,當線程創建完成時為新建狀態,即new Thread(...),還沒有調用start方法時,線程處于新建狀態。

  Runnable:就緒狀態,當調用線程的的start方法后,線程進入就緒狀態,等待CPU資源。處于就緒狀態的線程由Java運行時系統的線程調度程序(thread scheduler)來調度。

  Running:運行狀態,就緒狀態的線程獲取到CPU執行權以后進入運行狀態,開始執行run方法。

? ? ? ?Blocked:阻塞表示線程在等待Monitor lock。比如,線程試圖通過synchronized去獲取某個鎖,但是其他線程已經獨占了,那么當前線程就會處于阻塞狀態。

? ? ? ?WAITING(等待):表示正在等待其他線程采取某些操作。一個常見的場景是類似生產者消費者模式,發現任務條件尚未滿足,就讓當前消費者線程等待(wait),另外的生產者線程去準備任務數據,然后通過類似notify等動作,通知消費線程可以繼續工作了。Thread.join()也會令線程進入等待狀態。

? ? ? ?TIMED_WAIT(計時等待):其進入條件和等待狀態類似,但是調用的是存在超時條件的方法,比如wait或join等方法的指定超時版本。

? ? ? ?TERMINATED(終止):不管是意外退出還是正常執行結束,線程已經完成使命,終止運行,也有人把這個狀態叫作死亡。

以下是關系到線程運行狀態的幾個方法:

  1)start方法

start()用來啟動一個線程,當調用start方法后,系統才會開啟一個新的線程來執行用戶定義的子任務,在這個過程中,會為相應的線程分配需要的資源。

一個線程兩次調用start()方法會出現什么情況?

Java的線程是不允許啟動兩次的,第二次調用必然會拋出IllegalThreadStateException,這是一種運行時異常,多次調用start被認為是編程錯誤。

  2)run方法

run()方法是不需要用戶來調用的,當通過start方法啟動一個線程之后,當線程獲得了CPU執行時間,便進入run方法體去執行具體的任務。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執行的任務。

? ? ? ? 3)wait/notify/notifyAll方法的使用  

??? ????wait()使當前線程阻塞,前提是必須先獲得鎖,一般配合synchronized關鍵字使用,即一般在synchronized同步代碼塊里使用wait()、notify/notifyAll()方法。

當線程執行wait()方法時候,會釋放當前的鎖,然后讓出CPU,進入等待狀態。

???? 既然wait方式是通過對象的monitor對象來實現的,所以只要在同一對象上去調用

???? notify/notifyAll方法,就可以喚醒對應對象monitor上等待的線程了。

? ? 只有當notify/notifyAll()被執行時候,才會喚醒一個或多個正處于等待狀態的線程,然后繼續往下執行,直到執行完synchronized代碼塊的代碼或是中途遇到wait(),再次釋放鎖。

也就是說,notify/notifyAll()的執行只是喚醒沉睡的線程,而不會立即釋放鎖,鎖的釋放要看代碼塊的具體執行情況。所以在編程中,盡量在使用了notify/notifyAll()后立即退出臨界區,以喚醒其他線程。

? ? notify和wait的順序不能錯,如果A線程先執行notify方法,B線程再執行wait方法,那么B線程是無法被喚醒的。

? ? ?notify和notifyAll的區別

??? notify方法只喚醒一個等待(對象的)線程并使該線程開始執行。所以如果有多個線程等待一個對象,這個方法只會喚醒其中一個線程,選擇哪個線程取決于操作系統對多線程管理的實現。

? ? notifyAll會喚醒所有等待(對象的)線程,盡管哪一個線程將會第一個處理取決于操作系統的實現。如果當前情況下有多個線程需要被喚醒,推薦使用notifyAll方法。

? ? 最后,有兩點點需要注意:

??? (1)調用wait方法后,線程是會釋放對monitor對象的所有權的。

??? (2)一個通過wait方法阻塞的線程,必須同時滿足以下兩個條件才能被真正執行:

? ? ? ? ? ? ? ? 線程需要被喚醒(超時喚醒或調用notify/notifyll)。

? ? ? ? ? ? ? ? 線程喚醒后需要競爭到鎖(monitor)。


? ? 4)sleep/yield/join方法解析

??? ??????這組方法跟上面方法的最明顯區別是:這幾個方法都位于Thread類中,而上面三個方法都位于Object類中。

??? (1)sleep方法

???????? sleep方法的作用是讓當前線程暫停指定的時間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區別。

???????? 最簡單的區別是,wait方法依賴于同步,而sleep方法可以直接調用。而更深層次的區別在于sleep方法只是暫時讓出CPU的執行權,并不釋放鎖。而wait方法則需要釋放鎖。

??? (2)yield方法

???????? 調用yield方法會讓當前線程交出CPU權限,讓CPU去執行其他的線程。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,

???????? yield方法只能讓擁有相同優先級的線程有獲取CPU執行時間的機會。yield方法只是將Running狀態轉變為Runnable狀態。

????  注意,調用yield方法并不會讓線程進入阻塞狀態,而是讓線程重回就緒狀態,它只需要等待重新獲取CPU執行時間,這一點是和sleep方法不一樣的。

??? (3)join方法

???????? join方法有三個重載版本:

???????? join()

???????? join(long millis)???? //參數為毫秒

???????? join(long millis,int nanoseconds)??? //第一參數為毫秒,第二個參數為納秒????????

???????? join方法的作用是父線程等待子線程執行完成后再執行,換句話說就是將異步執行的線程合并為同步的線程。如果調用的是無參join方法,則等待thread執行完畢,如果調用的是指定了時間參數的join方法,則等待一定的時間??梢钥闯鰆oin方法就是通過wait方法來將線程的阻塞,如果join的線程還在執行,則將當前線程阻塞起來,直到join的線程執行完成,當前線程才能執行。由于wait方法會讓線程釋放對象鎖,所以join方法同樣會讓線程釋放對一個對象持有的鎖。

???????? 不過有一點需要注意,這里的join只調用了wait方法,卻沒有對應的notify方法,原因是Thread的start方法中做了相應的處理,所以當join的線程執行完成以后,會自動喚醒主線程繼續往下執行。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Java多線程學習 [-] 一擴展javalangThread類 二實現javalangRunnable接口 三T...
    影馳閱讀 2,986評論 1 18
  • 本文主要講了java中多線程的使用方法、線程同步、線程數據傳遞、線程狀態及相應的一些線程函數用法、概述等。 首先講...
    李欣陽閱讀 2,492評論 1 15
  • 林炳文Evankaka原創作品。轉載自http://blog.csdn.net/evankaka 本文主要講了ja...
    ccq_inori閱讀 668評論 0 4
  • 文章來源:http://www.54tianzhisheng.cn/2017/06/04/Java-Thread/...
    beneke閱讀 1,522評論 0 1
  • 從中可以看出,中國對于這次工業革命是怎樣的一個態度呢?歷史上的技術浪潮有哪些呢?兩次工業革命對中國的影響是怎樣的呢...
    透透媽閱讀 170評論 0 1