前言 在 "上一篇" 文章中,回顧了Java的集合。而在本篇文章中主要介紹 多線程 的相關知識。主要介紹的知識點為線程的介紹、多線程的使用、以及在多線程中使用的一些方法。 線程和進程 線程 表示進程中負責程式執行的執行單元,依靠程式進行運行。線程是程式中的順序控制流,只能使用分配給程式的資源和環境。 ...
前言
在上一篇文章中,回顧了Java的集合。而在本篇文章中主要介紹多線程的相關知識。主要介紹的知識點為線程的介紹、多線程的使用、以及在多線程中使用的一些方法。
線程和進程
線程
表示進程中負責程式執行的執行單元,依靠程式進行運行。線程是程式中的順序控制流,只能使用分配給程式的資源和環境。
進程
表示資源的分配和調度的一個獨立單元,通常表示為執行中的程式。一個進程至少包含一個線程。
進程和線程的區別
- 進程至少有一個線程;它們共用進程的地址空間;而進程有自己獨立的地址空間;
- 進程是資源分配和擁有的單位,而同一個進程內的線程共用進程的資源;
- 線程是處理器調度的基本單位,但進程不是;
生命周期
線程和進程一樣分為五個階段:創建、就緒、運行、阻塞和終止。
- 新建狀態:使用 new 關鍵字和 Thread 類或其子類建立一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程式start() 這個線程。
- 就緒狀態:當線程對象調用了start()方法之後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM里線程調度器的調度。
- 運行狀態:如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
- 阻塞狀態:如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源後可以重新進入就緒狀態。可以分為三種:
- 等待阻塞:運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。
- 同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。
- 其他阻塞:通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
- 死亡狀態:一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。
可以用下述圖來進行理解線程的生命周期:
註:上述圖來自http://www.runoob.com/wp-content/uploads/2014/01/java-thread.jpg。
在瞭解了線程和進程之後,我們再來簡單的瞭解下單線程和多線程。
單線程
程式中只存在一個線程,實際上主方法就是一個主線程。
多線程
多線程是指在同一程式中有多個順序流在執行。 簡單的說就是在一個程式中有多個任務運行。
那麼在什麼情況下用多線程呢?
一般來說,程式中有兩個以上的子系統需要併發執行的,這時候就需要利用多線程編程。通過對多線程的使用,可以編寫出高效的程式。
那麼是不是使用很多線程就能提高效率呢?
不一定的。因為程式中上下文的切換開銷也很重要,如果創建了太多的線程,CPU
花費在上下文的切換的時間將多於執行程式的時間!這時是會降低程式執行效率的。
所以有效利用多線程的關鍵是理解程式是併發執行而不是串列執行的。
線程的創建
一般來說,我們在對線程進行創建的時候,一般是繼承Thread 類或實現Runnable 介面。其實還有一種方式是實現 Callable介面,然後與Future 或線程池結合使用, 類似於Runnable介面,但是就功能上來說更為強大一些,也就是被執行之後,可以拿到返回值。
這裡我們分別一個例子使用繼承Thread 類、實現Runnable 介面和實現Callable介面與Future結合來進行創建線程。
代碼示例:
註:線程啟動的方法是start而不是run。因為使用start方法整個線程處於就緒狀態,等待虛擬機來進行調度。而使用run,也就是當作了一個普通的方法進行啟動,這樣虛擬機不會進行線程調度,虛擬機會執行這個方法直到結束後自動退出。
代碼示例:
public class Test {
public static void main(String[] args) {
ThreadTest threadTest=new ThreadTest();
threadTest.start();
RunalbeTest runalbeTest=new RunalbeTest();
Thread thread=new Thread(runalbeTest);
thread.start();
CallableTest callableTest=new CallableTest();
FutureTask<Integer> ft = new FutureTask<Integer>(callableTest);
Thread thread2=new Thread(ft);
thread2.start();
try {
System.out.println("返回值:"+ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class ThreadTest extends Thread{
@Override
public void run() {
System.out.println("這是一個Thread的線程!");
}
}
class RunalbeTest implements Runnable{
@Override
public void run() {
System.out.println("這是一個Runnable的線程!");
}
}
class CallableTest implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("這是一個Callable的線程!");
return 2;
}
}
運行結果:
這是一個Thread的線程!
這是一個Runnable的線程!
這是一個Callable的線程!
返回值:2
通過上述示例代碼中,我們發現使用繼承 Thread 類的方式創建線程時,編寫最為簡單。而使用Runnable、Callable 介面的方式創建線程的時候,需要通過Thread類的構造方法Thread(Runnable target) 構造出對象,然後調用start方法來運行線程代碼。順便說下,其實Thread類實際上也是實現了Runnable介面的一個類。
但是在這裡,我推薦大家創建單線程的時候使用繼承 Thread 類方式創建,多線線程的時候使用Runnable、Callable 介面的方式來創建創建線程。
至於為什麼呢?在下麵中的描述已給出理由。
- 繼承 Thread 類創建的線程,可以直接使用Thread類中的方法,比如休眠直接就可以使用sleep方法,而不必在前面加個Thread;獲取當前線程Id,只需調用getId就行,而不必使用Thread.currentThread().getId() 這麼一長串的代碼。但是使用Thread 類創建的線程,也有其局限性。比如資源不能共用,無法放入線程池中等等。
- 使用Runnable、Callable 介面的方式創建的線程,可以實現資源共用,增強代碼的復用性,並且可以避免單繼承的局限性,可以和線程池完美結合。但是也有不好的,就是寫起來不太方便,使用其中的方法不夠簡介。
總的來說就是,單線程建議用繼承 Thread 類創建,多線程建議- 使用Runnable、Callable 介面的方式創建。
線程的一些常用方法
yield
使用yield方法表示暫停當前正在執行的線程對象,並執行其他線程。
代碼示例:
public class YieldTest {
public static void main(String[] args) {
Test1 t1 = new Test1("張三");
Test1 t2 = new Test1("李四");
new Thread(t1).start();
new Thread(t2).start();
}
}
class Test1 implements Runnable {
private String name;
public Test1(String name) {
this.name=name;
}
@Override
public void run() {
System.out.println(this.name + " 線程運行開始!");
for (int i = 1; i <= 5; i++) {
System.out.println(""+this.name + "-----" + i);
// 當為3的時候,讓出資源
if (i == 3) {
Thread.yield();
}
}
System.out.println(this.name + " 線程運行結束!");
}
}
執行結果一:
張三 線程運行開始!
張三-----1
張三-----2
張三-----3
李四 線程運行開始!
李四-----1
李四-----2
李四-----3
張三-----4
張三-----5
張三 線程運行結束!
李四-----4
李四-----5
李四 線程運行結束!
執行結果二:
張三 線程運行開始!
李四 線程運行開始!
李四-----1
李四-----2
李四-----3
張三-----1
張三-----2
張三-----3
李四-----4
李四-----5
李四 線程運行結束!
張三-----4
張三-----5
張三 線程運行結束!
上述中的例子我們可以看到,啟動兩個線程之後,哪個線程先執行到3,就會讓出資源,讓另一個線程執行。
在這裡順便說下,yield和sleep的區別。
- yield: yield只是使當前線程重新回到可執行狀態,所以執行yield()的線程有可能在進入到可執行狀態後馬上又被執行。
- sleep:sleep使當前線程進入停滯狀態,所以執行sleep()的線程在指定的時間內肯定不會被執行;
join
使用join方法指等待某個線程終止。也就是說當子線程調用了join方法之後,後面的代碼只有等待該線程執行完畢之後才會執行。
如果不好理解,這裡依舊使用一段代碼來進行說明。
這裡我們創建兩個線程,並使用main方法執行。順便提一下,其實main方法也是個線程。如果直接執行的話,可能main方法執行完畢了,子線程還沒執行完畢,這裡我們就讓子線程使用join方法使main方法最後執行。
代碼示例:
public class JoinTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+ "主線程開始運行!");
Test2 t1=new Test2("A");
Test2 t2=new Test2("B");
t1.start();
t2.start();
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");
}
}
class Test2 extends Thread{
public Test2(String name) {
super(name);
}
public void run() {
System.out.println(this.getName() + " 線程運行開始!");
for (int i = 0; i < 5; i++) {
System.out.println("子線程"+this.getName() + "運行 : " + i);
try {
sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 線程運行結束!");
}
}
執行結果:
main主線程開始運行!
B 線程運行開始!
子線程B運行 : 0
A 線程運行開始!
子線程A運行 : 0
子線程A運行 : 1
子線程B運行 : 1
子線程B運行 : 2
子線程B運行 : 3
子線程B運行 : 4
B 線程運行結束!
子線程A運行 : 2
子線程A運行 : 3
子線程A運行 : 4
A 線程運行結束!
main主線程運行結束!
上述示例中的結果顯然符合我們的預期。
priority
使用setPriority表示設置線程的優先順序。
每個線程都有預設的優先順序。主線程的預設優先順序為Thread.NORM_PRIORITY。
線程的優先順序有繼承關係,比如A線程中創建了B線程,那麼B將和A具有相同的優先順序。
JVM提供了10個線程優先順序,但與常見的操作系統都不能很好的映射。如果希望程式能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的調度方式
- static int MAX_PRIORITY 線程可以具有的最高優先順序,取值為10。
- static int MIN_PRIORITY 線程可以具有的最低優先順序,取值為1。
- static int NORM_PRIORITY 分配給線程的預設優先順序,取值為5。
但是設置優先順序並不能保證線程一定先執行。我們可以通過一下代碼來驗證。
代碼示例:
public class PriorityTest {
public static void main(String[] args) {
Test3 t1 = new Test3("張三");
Test3 t2 = new Test3("李四");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
class Test3 extends Thread {
public Test3(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName() + " 線程運行開始!");
for (int i = 1; i <= 5; i++) {
System.out.println("子線程"+this.getName() + "運行 : " + i);
try {
sleep(new Random().nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(this.getName() + " 線程運行結束!");
}
}
執行結果一:
李四 線程運行開始!
子線程李四運行 : 1
張三 線程運行開始!
子線程張三運行 : 1
子線程張三運行 : 2
子線程李四運行 : 2
子線程李四運行 : 3
子線程李四運行 : 4
子線程張三運行 : 3
子線程李四運行 : 5
李四 線程運行結束!
子線程張三運行 : 4
子線程張三運行 : 5
張三 線程運行結束!
執行結果二:
張三 線程運行開始!
子線程張三運行 : 1
李四 線程運行開始!
子線程李四運行 : 1
子線程張三運行 : 2
子線程張三運行 : 3
子線程李四運行 : 2
子線程張三運行 : 4
子線程李四運行 : 3
子線程張三運行 : 5
子線程李四運行 : 4
張三 線程運行結束!
子線程李四運行 : 5
李四 線程運行結束!
執行結果三:
李四 線程運行開始!
子線程李四運行 : 1
張三 線程運行開始!
子線程張三運行 : 1
子線程李四運行 : 2
子線程李四運行 : 3
子線程李四運行 : 4
子線程張三運行 : 2
子線程張三運行 : 3
子線程張三運行 : 4
子線程李四運行 : 5
子線程張三運行 : 5
李四 線程運行結束!
張三 線程運行結束!
線程中一些常用的方法
線程中還有許多方法,但是這裡並不會全部細說。只簡單的列舉了幾個方法使用。更多的方法使用可以查看相關的API文檔。這裡我也順便總結了一些關於這些方法的描述。
- sleep:在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行);不會釋放對象鎖。
- join:指等待t線程終止。
- yield:暫停當前正在執行的線程對象,並執行其他線程。
- setPriority:設置一個線程的優先順序。
- interrupt:一個線程是否為守護線程。
- wait:強迫一個線程等待。它是Object的方法,也常常和sleep作為比較。需要註意的是wait會釋放對象鎖,讓其它的線程可以訪問;使用wait必須要進行異常捕獲,並且要對當前所調用,即必須採用synchronized中的對象。
- isAlive: 判斷一個線程是否存活。
- activeCount: 程式中活躍的線程數。
- enumerate: 枚舉程式中的線程。
- currentThread: 得到當前線程。
- setDaemon: 設置一個線程為守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)。
- setName: 為線程設置一個名稱。
- notify(): 通知一個線程繼續運行。它也是Object的一個方法,經常和wait方法一起使用。
結語
其實這篇文章很久之前都已經打好草稿了,但是由於各種原因,只到今天才寫完。雖然也只是簡單的介紹了一下多線程的相關知識,也只能算個入門級的教程吧。不過寫完之後,感覺自己又重新複習了一遍多線程,對多線程的理解又加深了一些。
話已盡此,不在多說。
原創不易,如果感覺不錯,希望給個推薦!您的支持是我寫作的最大動力!
參考:https://blog.csdn.net/evankaka/article/details/44153709#t1
版權聲明:
作者:虛無境
博客園出處:http://www.cnblogs.com/xuwujing
CSDN出處:http://blog.csdn.net/qazwsxpcm
個人博客出處:http://www.panchengming.com