線程相關的幾個問題
來自小馬哥公開課
創建線程
如何創建?
歸根到底new Thread()
並調用start()
是java創建並運行線程的唯一方式. Runnable
,Callable
以及繼承 Thread
, 還有lamda表達式等類似代碼其實都是線程的使用方式.
相比較而言, 創建進程的Java方式是使用Runtime#exec(String command)
方法實現.
如何銷毀?
Java正在執行的線程只有在執行完畢, 或者拋出異常後才能停止. 也就是基本可以認為: 線程一旦開始執行就無法從外部控制其銷毀.
Thread#stop()
方法用來停止線程, 但此方法過於暴力, 可能會導致資源不會正確釋放, 會導致死鎖, 此方法已經標記為過期.
while(true)
循環類線程可以使用interrupt()
方法通過拋出InterruptedException
來結束線程, 但要註意只有線程阻塞情況下才會拋出此異常, 因此, 對於非阻塞狀態的線程, 是使用isInterrupted()
方法來判斷中斷標誌來結束控制循環.
while(!isInterrupted()){
try{
Thread.sleep(5000);
}catch(InterruptedException e){
e.pringStackTrace();
break;
}
}
如何停止?
使用狀態標誌變量停止方式如下:
class TaskThread implements Runnable{
private volatile boolean stoped = fasle; // 標識字段, 註意線程安全問題, 使用volatile保證可見性
@Override
public void run(){
if(!stopped){
// todo
}
}
public void setStoped(boolean stop){
stoped=stop;
}
}
控制線程的執行順序
使用join()
join()
方法會阻塞調用它的線程. 方法的關鍵代碼如下:
public final synchronized void join(long millis){
while (isAlive()) {
wait(millis);
}
}
可見其最終是使用Object#wait(long millis)
來實現, 也就是說, 我們自己也可以使用Object#wait()
來控制線程的執行順序.
另外的方法
- 使用單線程池的方式
ExecutorService singlePool = Executors.newSingleThreadExecutor()
singlePool.submit(t1);
singlePool.submit(t2);
singlePool.shutdown();
- 使用
CountdownLatch
- 使用循環檢測線程
t1.start()
while(t1.isAlive()){
//Thread.sleep(5);
}
t2.start();
- 使用
Thread.sleep(long millis)
線程異常
Java只能捕獲unchecked異常. 線程異常時線程即終止.
如何捕獲?
可以使用下面方式捕獲線程拋出的異常, 這樣做的話線程的異常堆棧將不會輸出到System.error標準輸出流.
對於堆棧過多的場景來自定義錯誤handler處理, 可以防止錯誤堆棧導致內存耗盡. Spring boot 中的SpringApplication有相關的接口.
Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh);
線程池中的異常處理
除了上面的方法, 還可以使用如下方法捕獲異常.
ThreadPoolExecutor#afterExecute(Runnable r, Throwable t)
線程狀態
NEW 新建線程 new
RUNABLE 可運行, start()
BLOCKED 阻塞, 獲取鎖
WAITING 等待
TIME_WAITING 超時等待
TERMINATED 終結
如何獲取 JVM 中所有線程狀態
- 使用外部工具:
jstack pid
可以顯示出所有線程. - 代碼使用JMX:
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] ThreadIds = threadMXBean.getAllThreadIds();
for(long threadId : ThreadIds){
ThreadInfo threadInfo =ThreadMXBean.getThreadInfo(threadId);
}
線程同步
synchronized 在方法和代碼塊上的區別,可以查看字節碼文件. 關註monitorenter和monitorexit指令, 方法的話關註其flag的標誌位.
synchronized 和 ReentrantLock 的區別
二者均可重入. ReentrantLock 多了三個功能, 可中斷, 公平鎖, 綁定多個Condition. ReentrantLock 性能可能更高一點, 也有更多的控制, 比如嘗試獲得鎖, 釋放等, 同時也可以綁定多個條件.
偏向鎖在 ReentrantLock 中的體現
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 當前的state
if (c == 0) { //首次獲取到鎖
if (compareAndSetState(0, acquires)) { //CAS並設置狀態
setExclusiveOwnerThread(current); //線程暫存
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //判定是否存儲當前線程, 是的話就是重入
int nextc = c + acquires; //狀態新增
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); //狀態回寫
return true;
}
return false;
}
線程通訊
wait()
和 notity()
notifyAll()
在 Object 中定義的原因?
一個很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。鎖是線程爭用的對象, 與線程不應該過度連接. Object上也有一些頭信息, 為鎖的管理提供方便.
wait()
和 notity()
為什麽必須在 synchronized 中執行?
很簡單, 多線程環境下的鎖才有意義, synchronized 表明有資源爭用, 即鎖的爭奪, 這是鎖存在的前提.
線程退出
主線程退出時deamon子線程何時退出
關於這個問題, 實際上是一個depands-on的答案, 有時候子線程會繼續執行一段時間, 有時候則在主線程退出後立即退出, 一般認為主線程退出後臺線程也會退出, 但並不能認為子線程一定不會執行.
Shutdown Hook 方法的使用
方法簽名: Runtime#addShutdownHook(Thread thread)
這個鉤子方法可以在線程退出時回調完成一些工作, 比如資源回收,還有一些對象的銷毀工作.
如何確保主線程退出前所有線程執行完畢
ThreadGroup
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
int count = threadGroup.activeCount();
Thread[] threads = new Thread[count];
threadGroup.enumerate(threads,true); //復制到這個數組
for (Thread t : threads) {
System.out.println(t.getState());
}
線程相關的幾個問題