本次內容主要講認識Java中的多線程、線程的啟動與中止、yield()和join、線程優先級和守護線程。
1、Java程序天生就是多線程的
一個Java程序從main()方法開始執行,然后按照既定的代碼邏輯執行,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序,因為執行main()方法的是一個名稱為main的線程。
import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; public class OnlyMain { public static void main(String[] args) { //Java 虛擬機線程系統的管理接口
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不需要獲取同步的monitor和synchronizer信息,僅僅獲取線程和線程堆棧信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍歷線程信息,僅打印線程ID和線程名稱信息
for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } }
執行main方法,可以看到輸出了6個線程信息,如下圖所示:
main線程:用戶程序入口。
Reference Handler:清除Reference的線程。
Finalizer:調用對象finalize方法的線程。
Signal Dispatcher:分發處理發送給JVM信號的線程。
Attach Listener:內存dump,線程dump,類信息統計,獲取系統屬性等。
Monitor Ctrl-Break:監控Ctrl-Break中斷信號的線程。
2、線程的啟動與中止
2.1 啟動線程
啟動一個新線程有2種方式,這樣說的原因是參考Thread類的說明:There are two ways to create a new thread of execution.
先看第一種:One is to declare a class to be a subclass of <code>Thread</code>。意思就是繼承自Thread類,重寫run()方法,然后new出一個對象,調用start()方法。
class PrimeThread extends Thread { long minPrime; PrimeThread(long minPrime) { this.minPrime = minPrime; } public void run() { } public static void main(String[] args) { PrimeThread p = new PrimeThread(143); p.start(); } }
再看第二種:The other way to create a thread is to declare a class that implements the <code>Runnable</code> interface。也就是說實現Runnable接口,然后實現run()方法,再把此類的實例作為一個參數初始化一個Thread實例,執行start()方法。
class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { } public static void main(String[] args) { PrimeRun p = new PrimeRun(143); new Thread(p).start(); } }
2.2 Thread和Runnable的區別
Thread才是Java里對線程的唯一抽象,Runnable只是對任務(業務邏輯)的抽象。Thread可以接受任意一個Runnable的實例並執行。
2.3 線程的終止
(1)線程自然終止
要么是run執行完成了,要么是拋出了一個未處理的異常導致線程提前結束。
(2)stop
暫停、恢復和停止操作對應在線程Thread的API就是suspend()、resume()和stop()。但是這些API是過期的,也就是不建議使用的。不建議使用的原因主要有:以suspend()方法為例,在調用后,線程不會釋放已經占有的資源(比如鎖),而是占有着資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop()方法在終結一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態下。正因為suspend()、resume()和stop()方法帶來的副作用,這些方法才被標注為不建議使用的過期方法。
(3)中斷
安全的終止則是其他線程通過調用某個線程A的interrupt()方法對其進行中斷操作,中斷好比其他線程對該線程打了個招呼,“A,你要中斷了”,不代表線程A會立即停止自己的工作,同樣的A線程完全可以不理會這種中斷請求。因為java里的線程是協作式的,不是搶占式的。線程通過檢查自身的中斷標志位是否被置為true來進行響應。線程通過方法isInterrupted()來進行判斷是否被中斷,也可以調用靜態方法Thread.interrupted()來進行判斷當前線程是否被中斷,不過Thread.interrupted()會同時將中斷標識位改寫為false。
如果一個線程處於了阻塞狀態(如線程調用了sleep、join、wait等),則在線程在檢查中斷標志時如果發現中斷標志為true,則會在這些阻塞方法調用處拋出InterruptedException異常,並且在拋出異常后會立即將線程的中斷標示位清除,即重新設置為false。
不建議自定義一個取消標志位來中止線程的運行。因為run方法里有阻塞調用時會無法很快檢測到取消標志,線程必須從阻塞調用返回后,才會檢查這個取消標志。這種情況下,使用中斷會更好,主要有以下2個原因:
① 一般的阻塞方法,如sleep等本身就支持中斷的檢查。
② 檢查中斷位的狀態和檢查取消標志位沒什么區別,用中斷位的狀態還可以避免聲明取消標志位,減少資源的消耗。
注意:處於死鎖狀態的線程無法被中斷。
2.4 run()和start()
Thread類是Java里對線程概念的抽象,可以這樣理解:我們通過new Thread()其實只是new出一個Thread的實例,還沒有和操作系統中真正的線程掛起鈎來。只有執行了start()方法后,才實現了真正意義上的啟動線程。start()方法讓一個線程進入就緒隊列等待分配cpu,分到cpu后才調用實現的run()方法,start()方法不能重復調用,如果重復調用會拋出異常,通過源碼可以發現,start()方法先判斷當前線程的狀態,如果線程狀態不為0,就會拋出異常。而run方法是業務邏輯實現的地方,本質上和任意一個類的任意一個成員方法並沒有任何區別,可以重復執行,也可以被單獨調用。
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(); 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 */ } } }
3、其他基礎API
3.1 yield
yield()方法:使當前線程讓出CPU占有權,但讓出的時間是不可設定的。也不會釋放鎖資源。注意:因為又不是每個線程都需要這個鎖的,而且執行yield( )的線程不一定就會持有鎖。
所有執行yield()的線程有可能在進入到就緒狀態后會被操作系統再次選中馬上又被執行。
yield()方法在ConcurrentHashMap的initTable()方法中有用到,源碼如下圖所示:
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
sizeCtl小於0表示有其他線程在進行table的初始化,對於table的初始化工作,同一時間只能有一個線程執行,所以其他線程把CPU執行權讓出,,提高CPU利用率。
3.2 join
把指定的線程加入到當前線程,可以將兩個交替執行的線程合並為順序執行的線程。比如在線程B中調用了線程A的join()方法,直到線程A執行完畢后,才會繼續執行線程B。
下面用一個隔壁老王舔到最后一無所有的故事來說明join的使用方法。有一天隔壁老王在食堂排隊打飯,看到女神也來打飯,於是叫女神排在自己的前面,但是他萬萬沒有想到的是,女神讓她的男朋友排在女神前面,隔壁老王傻眼了。
public class JoinDemo { //隔壁老王的女神 static class Goddess implements Runnable { //女神的男朋友 private Thread gf; public Goddess(Thread gf) { this.gf = gf; } public void run() { System.out.println("女神開始排隊打飯....."); try { System.out.println("女神讓男朋友插隊....."); gf.join(); } catch (InterruptedException e) { e.printStackTrace(); } try { Thread.sleep(2000);//休眠2秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 女神打飯完成."); } } //隔壁老王的女神的男朋友 static class GoddessBoyfriend implements Runnable { public void run() { System.out.println("女神的男朋友開始排隊打飯....."); try { Thread.sleep(2000);//休眠2秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 女神的男朋友打飯完成."); } } public static void main(String[] args) throws Exception { //女神的男朋友 GoddessBoyfriend goddessBoyfriend = new GoddessBoyfriend(); Thread gbf = new Thread(goddessBoyfriend); //女神 Thread goddess = new Thread(new Goddess(gbf)); goddess.start(); gbf.start(); System.out.println("隔壁老王開始排隊打飯....."); System.out.println("隔壁老王請女神插隊....."); goddess.join(); try { Thread.sleep(2000);//主線程休眠2秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 隔壁老王打飯完成."); } }
4、線程優先級
在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的范圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。
設置線程優先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨占。在不同的JVM以及操作系統上,線程規划會存在差異,有些操作系統甚至會忽略對線程優先級的設定。所以線程的優先級在Java里一般不會特別設置,特別是不能把程序的正確運行寄托在線程的優先級上。
5、守護線程
Daemon(守護)線程是一種支持型線程,因為它主要被用作程序中后台調度以及支持性工作。這意味着,當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置為Daemon線程。我們一般用不上,比如垃圾回收線程就是Daemon線程。Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊並不一定會執行。在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。
下面是一個守護線程的例子:
public class DaemonThread { private static class UseThread extends Thread { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + " I am extends Thread."); } } } public static void main(String[] args) throws InterruptedException { UseThread useThread = new UseThread(); useThread.setDaemon(true); useThread.start(); Thread.sleep(500); } }
在主線程運行500毫秒后,主線程自然終止,因為useThread是守護線程,所以Java虛擬機退出。
后面會繼續介紹Java並發編程有關知識,如有不足之處請指出,筆者及時修正,謝謝。