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
Java基礎知識回顧之五 ----- 多線程