Java多線程拾遺(三)——Thread的一些API

在這裏插入圖片描述# 線程優先級和線程Id

進程有進程的優先級,線程也有優先級,但是優先級這個東西並不是越高的會優先執行,只是在CPU比較忙的時候,優先級較高的線程會有機會獲取更多CPU的執行時間,但是閒時並不會出現這種情況。因此在程序設計中,最好不要將業務與線程的優先級進行強關聯。

/**
 * autor:liman
 * createtime:2020/6/7
 * comment:線程id和線程優先級
 */
@Slf4j
public class ThreadIDAndPriority {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
           while(true){
               log.info("this is t1 thread");
           }
        });
        t1.setPriority(3);

        Thread t2 = new Thread(()->{
            while(true){
                log.info("this is t2 thread");
            }
        });
        t2.setPriority(8);
        t1.start();
        t2.start();
    }
}

上述代碼多運行幾次,發現有時候t2線程並沒有真正的佔用大部分運行時間。

翻看Thread中關於setPriority的源碼

public final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}

發現其實很簡單,新的線程的優先級不能設置小於1或者大於10,否則會拋出異常,同時線程的優先級不能高於線程組所在的優先級。線程的默認優先級與其父線程的優先級保持一致

順便說說線程id 通過線程本身的getId方法即可獲取到線程id,線程id對於我們通過jstack排查問題很有幫助。順便提一下,線程id的產生也是一個自增的id

private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

sleep和yield

yield並不常用,只是調用yield方法,會將當前線程從RUNNING狀態切換到RUNNABLE狀態,線程不會釋放鎖,yield也是一種啓發式的方法,提醒CPU本線程自願放棄CPU的執行權,如果CPU資源不緊張,則會忽略這種提醒。而sleep方法則是會讓當前線程進入阻塞狀態,但是依舊不釋放鎖

join

join某個線程A,會使得當前線程B 進入等待,直到線程A結束生命週期,或者到達指定時間,這個時候線程B是處於BLOCKED狀態的,而不是A線程。

可以參考如下示例:

/**
 * autor:liman
 * createtime:2020/6/7
 * comment:join的簡單實例
 */
@Slf4j
public class JoinDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            IntStream.rangeClosed(1,999).forEach(i->log.info("this is t1 thread,i:{}",i));
        },"t1");

        t1.start();
        t1.join();
        IntStream.rangeClosed(1,999).forEach(i->log.info("this is main thread,i:{}",i));
    }
}

上述代碼會出現主線程和t1線程交替運行的情況,如果加入了t1.join的方法,則主線程會等到t1線程運行完成之後纔開始運行

線程interrupt

線程interrupt是一個比較複雜的內容,關於線程中斷的api主要有以下三個方法

public void interrupt():
public static boolean interrupted();
public boolean isInterrupted();

在正式熟悉這些api方法之前,我們先梳理一下,目前爲止,讓一個線程進入阻塞狀態的方法有如下幾個

1、wait方法

2、sleep方法

3、join方法

同時終止一個線程,並不是直接暴力的方式終結一個線程(stop方法除外)而是將一個線程內部的中斷標誌位置爲true,讓後線程判斷該標誌位是否置爲true,如果置爲true則本身開始進行一些終止線程的操作,後面會詳細總結如何優雅的關閉一個線程的幾種方式

interrupt方法

先直接看實例

@Slf4j
public class ThreadInterruptDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true) {
                log.info("this is t1 thread");
            }
        },"t1");

        t1.start();
        Thread.sleep(10l);//主線程休眠10ms
        t1.interrupt();//調用t1線程的interrupt方法
    }
}

這個時候線程並不會終止,因爲雖然我們調用了t1.interrupt()方法,將t1線程的中斷標誌位置爲true,但是並沒有對這個中斷作出處理,因此我們線程依舊會運行,如果將上述代碼改成如下語句,則t1線程會正常退出

@Slf4j
public class ThreadInterruptDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true) {
                log.info("this is t1 thread");
                if(Thread.currentThread().isInterrupted()){//判斷當前線程是否被終止
                    log.info("interrupted");
                    break;
                }
            }
        },"t1");

        t1.start();
        Thread.sleep(10l);
        t1.interrupt();
    }
}

之前我們梳理了那些方法可以讓線程進入阻塞狀態,這些方法在《Java 高併發編程詳解》一書中被稱爲可中斷方法,如果目標線程在執行這些方法的時候,被外部中斷,則會拋出一個InterruptedException的異常,同時目標線程當前的阻塞狀態被打斷,這個時候我們可以根據拋出的異常來終止目標線程。但是需要說明的是,如果目標線程在執行可中斷方法時被中斷,則中斷標誌位被清除。

/**
 * autor:liman
 * createtime:2020/6/7
 * comment:join的簡單實例
 */
@Slf4j
public class JoinDemo {
	Thread t2 = new Thread(){
		@Override
		public void run() {
			try {
				TimeUnit.MINUTES.sleep(1);
			} catch (InterruptedException e) {
				//這一行輸出爲false,sleep方法清空了t2線程的中斷標誌位
				log.info("t2 is interrupted ? {}", this.isInterrupted());
				log.error("interrupted");
			}
		}
	};
	t2.start();
	TimeUnit.SECONDS.sleep(2);
	log.info("t1 is interrupt : {}", t2.isInterrupted());
	t2.interrupt();
	//預留3毫秒給t2調用sleep清空標誌位,不然影響判斷結果
	TimeUnit.MILLISECONDS.sleep(3);
	log.info("t1 is interrupt : {}", t2.isInterrupted());
}

最終運行結果如下,三個都爲false

在這裏插入圖片描述

如果改爲執行wait方法,則會有一樣的效果

@Slf4j
public class ThreadInterruptWaitDemo {

    private static Object MONITOR = new Object();
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{
            log.info("this is t1 thread");
            while(true){
                synchronized (MONITOR) {
                    try {
                        MONITOR.wait();
                    } catch (InterruptedException e) {
                        log.info("t1 is interrupted ? {}", Thread.currentThread().isInterrupted());
                    }
                }
            }
        });

        t1.start();
        TimeUnit.SECONDS.sleep(10);
        log.info("t1 is interrupted ? {}",t1.isInterrupted());
        t1.interrupt();
        TimeUnit.MILLISECONDS.sleep(3);
        log.info("t1 is interrupted ? {}",t1.isInterrupted());

    }
}

上述代碼運行效果一樣。

interrupted與isInterrupted

interrupted方法是一個靜態方法,isInterrupted方法是一個實例方法,兩者還是有些區別的,調用interrupted方法會直接清除掉線程的中斷標誌位。而isInterrupted則不會

如何優雅的關閉一個線程

總結完interrupt之後,我們關閉線程其實有一個相對優雅的方式了,就是通過中斷信號關閉線程

通過中斷信號關閉線程

@Slf4j
public class StopThreadGraceful {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true){
                log.info("t1 will start work");
                while(!Thread.currentThread().isInterrupted()){
                    // doing work;
                }
                log.info("t1 will exit");
                break;
            }
        });
        
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.info("main will stop t1");
        t1.interrupt();
    }
}

只是有個不好的地方,如果在上述的doing work中我們調用了可中斷方法,會導致線程的正常中斷

@Slf4j
public class StopThreadGraceful {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            while(true){
                log.info("t1 will start work");
                while(!Thread.currentThread().isInterrupted()){
                    TimeUnit.SECONDS.sleep(1);//這個會導致程序的正常退出,有可能會影響到正常業務。
                }
                log.info("t1 will exit");
            }
        });
        
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        log.info("main will stop t1");
        t1.interrupt();
    }
}

通過自己設置標誌位

由於Thread本的中斷標誌位很有可能會被擦除,因此我們通過自己設置標誌位來中斷線程

public static class MyTask extends Thread{
	//設置自己的中斷標誌位
    private volatile boolean isClosed = false;

    @Override
    public void run() {
        while(true){
            log.info("t1 will start work");
            while(!isClosed&&isInterrupted()){//兩者都判斷
                // doing work;
            }
            log.info("t1 will exit");
            break;
        }
    }
    
    //供外部調用的close方法,用於執行標記中斷。
    public void close(){
        this.isInterrupted();
        this.isClosed=true;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章