Java多線程干貨系列—(一)Java多線程基礎(chǔ)

前言

多線程并發(fā)編程是Java編程中重要的一塊內(nèi)容,也是面試重點覆蓋區(qū)域,所以學(xué)好多線程并發(fā)編程對我們來說極其重要,下面跟我一起開啟本次的學(xué)習(xí)之旅吧。

正文

線程與進程

1 線程:進程中負責(zé)程序執(zhí)行的執(zhí)行單元
線程本身依靠程序進行運行
線程是程序中的順序控制流,只能使用分配給程序的資源和環(huán)境

2 進程:執(zhí)行中的程序
一個進程至少包含一個線程

3 單線程:程序中只存在一個線程,實際上主方法就是一個主線程

4 多線程:在一個程序中運行多個任務(wù)
目的是更好地使用CPU資源

線程的實現(xiàn)

繼承Thread類

java.lang包中定義, 繼承Thread類必須重寫run()方法

class MyThread extends Thread{
    private static int num = 0;
     
    public MyThread(){
        num++;
    }
     
    @Override
    public void run() {
        System.out.println("主動創(chuàng)建的第"+num+"個線程");
    }
}

創(chuàng)建好了自己的線程類之后,就可以創(chuàng)建線程對象了,然后通過start()方法去啟動線程。注意,不是調(diào)用run()方法啟動線程,run方法中只是定義需要執(zhí)行的任務(wù),如果調(diào)用run方法,即相當(dāng)于在主線程中執(zhí)行run方法,跟普通的方法調(diào)用沒有任何區(qū)別,此時并不會創(chuàng)建一個新的線程來執(zhí)行定義的任務(wù)。

public class Test {
    public static void main(String[] args)  {
        MyThread thread = new MyThread();
        thread.start();
    }
}
class MyThread extends Thread{
    private static int num = 0;
    public MyThread(){
        num++;
    }
    @Override
    public void run() {
        System.out.println("主動創(chuàng)建的第"+num+"個線程");
    }
}

在上面代碼中,通過調(diào)用start()方法,就會創(chuàng)建一個新的線程了。為了分清start()方法調(diào)用和run()方法調(diào)用的區(qū)別,請看下面一個例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyThread thread1 = new MyThread("thread1");
        thread1.start();
        MyThread thread2 = new MyThread("thread2");
        thread2.run();
    }
}
 
 
class MyThread extends Thread{
    private String name;
     
    public MyThread(String name){
        this.name = name;
    }
     
    @Override
    public void run() {
        System.out.println("name:"+name+" 子線程ID:"+Thread.currentThread().getId());
    }
}

運行結(jié)果:


從輸出結(jié)果可以得出以下結(jié)論:

1)thread1和thread2的線程ID不同,thread2和主線程ID相同,說明通過run方法調(diào)用并不會創(chuàng)建新的線程,而是在主線程中直接運行run方法,跟普通的方法調(diào)用沒有任何區(qū)別;

2)雖然thread1的start方法調(diào)用在thread2的run方法前面調(diào)用,但是先輸出的是thread2的run方法調(diào)用的相關(guān)信息,說明新線程創(chuàng)建的過程不會阻塞主線程的后續(xù)執(zhí)行。

實現(xiàn)Runnable接口

在Java中創(chuàng)建線程除了繼承Thread類之外,還可以通過實現(xiàn)Runnable接口來實現(xiàn)類似的功能。實現(xiàn)Runnable接口必須重寫其run方法。
下面是一個例子:

public class Test {
    public static void main(String[] args)  {
        System.out.println("主線程ID:"+Thread.currentThread().getId());
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
} 
class MyRunnable implements Runnable{
    public MyRunnable() {
    }

    @Override
    public void run() {
        System.out.println("子線程ID:"+Thread.currentThread().getId());
    }
}

Runnable的中文意思是“任務(wù)”,顧名思義,通過實現(xiàn)Runnable接口,我們定義了一個子任務(wù),然后將子任務(wù)交由Thread去執(zhí)行。注意,這種方式必須將Runnable作為Thread類的參數(shù),然后通過Thread的start方法來創(chuàng)建一個新線程來執(zhí)行該子任務(wù)。如果調(diào)用Runnable的run方法的話,是不會創(chuàng)建新線程的,這根普通的方法調(diào)用沒有任何區(qū)別。

事實上,查看Thread類的實現(xiàn)源代碼會發(fā)現(xiàn)Thread類是實現(xiàn)了Runnable接口的。

在Java中,這2種方式都可以用來創(chuàng)建線程去執(zhí)行子任務(wù),具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現(xiàn)Runnable接口看起來更加簡潔,但是由于Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現(xiàn)Runnable接口。

使用ExecutorService、Callable、Future實現(xiàn)有返回結(jié)果的多線程

多線程后續(xù)會學(xué)到,這里暫時先知道一下有這種方法即可。

ExecutorService、Callable、Future這個對象實際上都是屬于Executor框架中的功能類。想要詳細了解Executor框架的可以訪問http://www.javaeye.com/topic/366591 ,這里面對該框架做了很詳細的解釋。返回結(jié)果的線程是在JDK1.5中引入的新特征,確實很實用,有了這種特征我就不需要再為了得到返回值而大費周折了,而且即便實現(xiàn)了也可能漏洞百出。

可返回值的任務(wù)必須實現(xiàn)Callable接口,類似的,無返回值的任務(wù)必須Runnable接口。執(zhí)行Callable任務(wù)后,可以獲取一個Future的對象,在該對象上調(diào)用get就可以獲取到Callable任務(wù)返回的Object了,再結(jié)合線程池接口ExecutorService就可以實現(xiàn)傳說中有返回結(jié)果的多線程了。下面提供了一個完整的有返回結(jié)果的多線程測試例子,在JDK1.5下驗證過沒問題可以直接使用。代碼如下:

/**
* 有返回值的線程 
*/  
@SuppressWarnings("unchecked")  
public class Test {  
public static void main(String[] args) throws ExecutionException,  
    InterruptedException {  
   System.out.println("----程序開始運行----");  
   Date date1 = new Date();  
  
   int taskSize = 5;  
   // 創(chuàng)建一個線程池  
   ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
   // 創(chuàng)建多個有返回值的任務(wù)  
   List<Future> list = new ArrayList<Future>();  
   for (int i = 0; i < taskSize; i++) {  
    Callable c = new MyCallable(i + " ");  
    // 執(zhí)行任務(wù)并獲取Future對象  
    Future f = pool.submit(c);  
    // System.out.println(">>>" + f.get().toString());  
    list.add(f);  
   }  
   // 關(guān)閉線程池  
   pool.shutdown();  
  
   // 獲取所有并發(fā)任務(wù)的運行結(jié)果  
   for (Future f : list) {  
    // 從Future對象上獲取任務(wù)的返回值,并輸出到控制臺  
    System.out.println(">>>" + f.get().toString());  
   }  
  
   Date date2 = new Date();  
   System.out.println("----程序結(jié)束運行----,程序運行時間【"  
     + (date2.getTime() - date1.getTime()) + "毫秒】");  
}  
}  
  
class MyCallable implements Callable<Object> {  
private String taskNum;  
  
MyCallable(String taskNum) {  
   this.taskNum = taskNum;  
}  
  
public Object call() throws Exception {  
   System.out.println(">>>" + taskNum + "任務(wù)啟動");  
   Date dateTmp1 = new Date();  
   Thread.sleep(1000);  
   Date dateTmp2 = new Date();  
   long time = dateTmp2.getTime() - dateTmp1.getTime();  
   System.out.println(">>>" + taskNum + "任務(wù)終止");  
   return taskNum + "任務(wù)返回運行結(jié)果,當(dāng)前任務(wù)時間【" + time + "毫秒】";  
}
}

代碼說明:
上述代碼中Executors類,提供了一系列工廠方法用于創(chuàng)先線程池,返回的線程池都實現(xiàn)了ExecutorService接口。
public static ExecutorService newFixedThreadPool(int nThreads)
創(chuàng)建固定數(shù)目線程的線程池。

public static ExecutorService newCachedThreadPool()
創(chuàng)建一個可緩存的線程池,調(diào)用execute 將重用以前構(gòu)造的線程(如果線程可用)。如果現(xiàn)有線程沒有可用的,則創(chuàng)建一個新線程并添加到池中。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。

public static ExecutorService newSingleThreadExecutor()
創(chuàng)建一個單線程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
創(chuàng)建一個支持定時及周期性的任務(wù)執(zhí)行的線程池,多數(shù)情況下可用來替代Timer類。

ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,返回Future。如果Executor后臺線程池還沒有完成Callable的計算,這調(diào)用返回Future對象的get()方法,會阻塞直到計算完成。

線程的狀態(tài)

在正式學(xué)習(xí)Thread類中的具體方法之前,我們先來了解一下線程有哪些狀態(tài),這個將會有助于后面對Thread類中的方法的理解。

  • 創(chuàng)建(new)狀態(tài): 準(zhǔn)備好了一個多線程的對象
  • 就緒(runnable)狀態(tài): 調(diào)用了start()方法, 等待CPU進行調(diào)度
  • 運行(running)狀態(tài): 執(zhí)行run()方法
  • 阻塞(blocked)狀態(tài): 暫時停止執(zhí)行, 可能將資源交給其它線程使用
  • 終止(dead)狀態(tài): 線程銷毀

當(dāng)需要新起一個線程來執(zhí)行某個子任務(wù)時,就創(chuàng)建了一個線程。但是線程創(chuàng)建之后,不會立即進入就緒狀態(tài),因為線程的運行需要一些條件(比如內(nèi)存資源,在前面的JVM內(nèi)存區(qū)域劃分一篇博文中知道程序計數(shù)器、Java棧、本地方法棧都是線程私有的,所以需要為線程分配一定的內(nèi)存空間),只有線程運行需要的所有條件滿足了,才進入就緒狀態(tài)。

當(dāng)線程進入就緒狀態(tài)后,不代表立刻就能獲取CPU執(zhí)行時間,也許此時CPU正在執(zhí)行其他的事情,因此它要等待。當(dāng)?shù)玫紺PU執(zhí)行時間之后,線程便真正進入運行狀態(tài)。

線程在運行狀態(tài)過程中,可能有多個原因?qū)е庐?dāng)前線程不繼續(xù)運行下去,比如用戶主動讓線程睡眠(睡眠一定的時間之后再重新執(zhí)行)、用戶主動讓線程等待,或者被同步塊給阻塞,此時就對應(yīng)著多個狀態(tài):time waiting(睡眠或等待一定的事件)、waiting(等待被喚醒)、blocked(阻塞)。

當(dāng)由于突然中斷或者子任務(wù)執(zhí)行完畢,線程就會被消亡。

下面這副圖描述了線程從創(chuàng)建到消亡之間的狀態(tài):


在有些教程上將blocked、waiting、time waiting統(tǒng)稱為阻塞狀態(tài),這個也是可以的,只不過這里我想將線程的狀態(tài)和Java中的方法調(diào)用聯(lián)系起來,所以將waiting和time waiting兩個狀態(tài)分離出來。

注:sleep和wait的區(qū)別:

  • sleepThread類的方法,waitObject類中定義的方法.
  • Thread.sleep不會導(dǎo)致鎖行為的改變, 如果當(dāng)前線程是擁有鎖的, 那么Thread.sleep不會讓線程釋放鎖.
  • Thread.sleepObject.wait都會暫停當(dāng)前的線程. OS會將執(zhí)行時間分配給其它線程. 區(qū)別是, 調(diào)用wait后, 需要別的線程執(zhí)行notify/notifyAll才能夠重新獲得CPU執(zhí)行時間.

上下文切換

對于單核CPU來說(對于多核CPU,此處就理解為一個核),CPU在一個時刻只能運行一個線程,當(dāng)在運行一個線程的過程中轉(zhuǎn)去運行另外一個線程,這個叫做線程上下文切換(對于進程也是類似)。

由于可能當(dāng)前線程的任務(wù)并沒有執(zhí)行完畢,所以在切換時需要保存線程的運行狀態(tài),以便下次重新切換回來時能夠繼續(xù)切換之前的狀態(tài)運行。舉個簡單的例子:比如一個線程A正在讀取一個文件的內(nèi)容,正讀到文件的一半,此時需要暫停線程A,轉(zhuǎn)去執(zhí)行線程B,當(dāng)再次切換回來執(zhí)行線程A的時候,我們不希望線程A又從文件的開頭來讀取。

因此需要記錄線程A的運行狀態(tài),那么會記錄哪些數(shù)據(jù)呢?因為下次恢復(fù)時需要知道在這之前當(dāng)前線程已經(jīng)執(zhí)行到哪條指令了,所以需要記錄程序計數(shù)器的值,另外比如說線程正在進行某個計算的時候被掛起了,那么下次繼續(xù)執(zhí)行的時候需要知道之前掛起時變量的值時多少,因此需要記錄CPU寄存器的狀態(tài)。所以一般來說,線程上下文切換過程中會記錄程序計數(shù)器、CPU寄存器狀態(tài)等數(shù)據(jù)。

說簡單點的:對于線程的上下文切換實際上就是 存儲和恢復(fù)CPU狀態(tài)的過程,它使得線程執(zhí)行能夠從中斷點恢復(fù)執(zhí)行。

雖然多線程可以使得任務(wù)執(zhí)行的效率得到提升,但是由于在線程切換時同樣會帶來一定的開銷代價,并且多個線程會導(dǎo)致系統(tǒng)資源占用的增加,所以在進行多線程編程時要注意這些因素。

線程的常用方法

編號 方法 說明
1 public void start() 使該線程開始執(zhí)行;Java 虛擬機調(diào)用該線程的 run 方法。
2 public void run() 如果該線程是使用獨立的 Runnable 運行對象構(gòu)造的,則調(diào)用該 Runnable 對象的 run 方法;否則,該方法不執(zhí)行任何操作并返回。
3 public final void setName(String name) 改變線程名稱,使之與參數(shù) name 相同。
4 public final void setPriority(int priority) 更改線程的優(yōu)先級。
5 public final void setDaemon(boolean on) 將該線程標(biāo)記為守護線程或用戶線程。
6 public final void join(long millisec) 等待該線程終止的時間最長為 millis 毫秒。
7 public void interrupt() 中斷線程。
8 public final boolean isAlive() 測試線程是否處于活動狀態(tài)。
9 public static void yield() 暫停當(dāng)前正在執(zhí)行的線程對象,并執(zhí)行其他線程。
10 public static void sleep(long millisec) 在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠(暫停執(zhí)行),此操作受到系統(tǒng)計時器和調(diào)度程序精度和準(zhǔn)確性的影響。
11 public static Thread currentThread() 返回對當(dāng)前正在執(zhí)行的線程對象的引用。

靜態(tài)方法

currentThread()方法

currentThread()方法可以返回代碼段正在被哪個線程調(diào)用的信息。

public class Run1{
    public static void main(String[] args){                 
    System.out.println(Thread.currentThread().getName());
    }
}

sleep()方法

方法sleep()的作用是在指定的毫秒數(shù)內(nèi)讓當(dāng)前“正在執(zhí)行的線程”休眠(暫停執(zhí)行)。這個“正在執(zhí)行的線程”是指this.currentThread()返回的線程。

sleep方法有兩個重載版本:

sleep(long millis)     //參數(shù)為毫秒
sleep(long millis,int nanoseconds)    //第一參數(shù)為毫秒,第二個參數(shù)為納秒

sleep相當(dāng)于讓線程睡眠,交出CPU,讓CPU去執(zhí)行其他的任務(wù)。
但是有一點要非常注意,sleep方法不會釋放鎖,也就是說如果當(dāng)前線程持有對某個對象的鎖,則即使調(diào)用sleep方法,其他線程也無法訪問這個對象??聪旅孢@個例子就清楚了:

public class Test {
     
    private int i = 10;
    private Object object = new Object();
     
    public static void main(String[] args) throws IOException  {
        Test test = new Test();
        MyThread thread1 = test.new MyThread();
        MyThread thread2 = test.new MyThread();
        thread1.start();
        thread2.start();
    } 
     
     
    class MyThread extends Thread{
        @Override
        public void run() {
            synchronized (object) {
                i++;
                System.out.println("i:"+i);
                try {
                    System.out.println("線程"+Thread.currentThread().getName()+"進入睡眠狀態(tài)");
                    Thread.currentThread().sleep(10000);
                } catch (InterruptedException e) {
                    // TODO: handle exception
                }
                System.out.println("線程"+Thread.currentThread().getName()+"睡眠結(jié)束");
                i++;
                System.out.println("i:"+i);
            }
        }
    }
}

輸出結(jié)果:


從上面輸出結(jié)果可以看出,當(dāng)Thread-0進入睡眠狀態(tài)之后,Thread-1并沒有去執(zhí)行具體的任務(wù)。只有當(dāng)Thread-0執(zhí)行完之后,此時Thread-0釋放了對象鎖,Thread-1才開始執(zhí)行。

注意,如果調(diào)用了sleep方法,必須捕獲InterruptedException異?;蛘邔⒃摦惓O蛏蠈訏伋觥.?dāng)線程睡眠時間滿后,不一定會立即得到執(zhí)行,因為此時可能CPU正在執(zhí)行其他的任務(wù)。所以說調(diào)用sleep方法相當(dāng)于讓線程進入阻塞狀態(tài)。

yield()方法

調(diào)用yield方法會讓當(dāng)前線程交出CPU權(quán)限,讓CPU去執(zhí)行其他的線程。它跟sleep方法類似,同樣不會釋放鎖。但是yield不能控制具體的交出CPU的時間,另外,yield方法只能讓擁有相同優(yōu)先級的線程有獲取CPU執(zhí)行時間的機會。

注意,調(diào)用yield方法并不會讓線程進入阻塞狀態(tài),而是讓線程重回就緒狀態(tài),它只需要等待重新獲取CPU執(zhí)行時間,這一點是和sleep方法不一樣的。
代碼:

public class MyThread  extends Thread{
    @Override
    public void run() {
        long beginTime=System.currentTimeMillis();
        int count=0;
        for (int i=0;i<50000000;i++){
            count=count+(i+1);
            //Thread.yield();
        }
        long endTime=System.currentTimeMillis();
        System.out.println("用時:"+(endTime-beginTime)+" 毫秒!");
    }
}

public class Run {
    public static void main(String[] args) {
        MyThread t= new MyThread();
        t.start();
    }
}

執(zhí)行結(jié)果:

用時:3 毫秒!

如果將 //Thread.yield();的注釋去掉,執(zhí)行結(jié)果如下:

用時:16080 毫秒!

對象方法

start()方法

start()用來啟動一個線程,當(dāng)調(diào)用start方法后,系統(tǒng)才會開啟一個新的線程來執(zhí)行用戶定義的子任務(wù),在這個過程中,會為相應(yīng)的線程分配需要的資源。

run()方法

run()方法是不需要用戶來調(diào)用的,當(dāng)通過start方法啟動一個線程之后,當(dāng)線程獲得了CPU執(zhí)行時間,便進入run方法體去執(zhí)行具體的任務(wù)。注意,繼承Thread類必須重寫run方法,在run方法中定義具體要執(zhí)行的任務(wù)。

getId()

getId()的作用是取得線程的唯一標(biāo)識
代碼:

public class Test {
    public static void main(String[] args) {
        Thread t= Thread.currentThread();
        System.out.println(t.getName()+" "+t.getId());
    }
}

輸出:

main 1

isAlive()方法

方法isAlive()的功能是判斷當(dāng)前線程是否處于活動狀態(tài)
代碼:

public class MyThread  extends Thread{
    @Override
    public void run() {
        System.out.println("run="+this.isAlive());
    }
}
public class RunTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        System.out.println("begin =="+myThread.isAlive());
        myThread.start();
        System.out.println("end =="+myThread.isAlive());
    }
}

程序運行結(jié)果:

begin ==false
run=true
end ==false

方法isAlive()的作用是測試線程是否偶處于活動狀態(tài)。什么是活動狀態(tài)呢?活動狀態(tài)就是線程已經(jīng)啟動且尚未終止。線程處于正在運行或準(zhǔn)備開始運行的狀態(tài),就認為線程是“存活”的。
有個需要注意的地方

  System.out.println("end =="+myThread.isAlive());

雖然上面的實例中打印的值是true,但此值是不確定的。打印true值是因為myThread線程還未執(zhí)行完畢,所以輸出true。如果代碼改成下面這樣,加了個sleep休眠:

public static void main(String[] args) throws InterruptedException {
        MyThread myThread=new MyThread();
        System.out.println("begin =="+myThread.isAlive());
        myThread.start();
        Thread.sleep(1000);
        System.out.println("end =="+myThread.isAlive());
    }

則上述代碼運行的結(jié)果輸出為false,因為mythread對象已經(jīng)在1秒之內(nèi)執(zhí)行完畢。

join()方法

在很多情況下,主線程創(chuàng)建并啟動了線程,如果子線程中藥進行大量耗時運算,主線程往往將早于子線程結(jié)束之前結(jié)束。這時,如果主線程想等待子線程執(zhí)行完成之后再結(jié)束,比如子線程處理一個數(shù)據(jù),主線程要取得這個數(shù)據(jù)中的值,就要用到j(luò)oin()方法了。方法join()的作用是等待線程對象銷毀。

public class Thread4 extends Thread{
    public Thread4(String name) {
        super(name);
    }
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + "  " + i);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 啟動子進程
        new Thread4("new thread").start();
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                Thread4 th = new Thread4("joined thread");
                th.start();
                th.join();
            }
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}

執(zhí)行結(jié)果:

main  0
main  1
main  2
main  3
main  4
new thread  0
new thread  1
new thread  2
new thread  3
new thread  4
joined thread  0
joined thread  1
joined thread  2
joined thread  3
joined thread  4
main  5
main  6
main  7
main  8
main  9

由上可以看出main主線程等待joined thread線程先執(zhí)行完了才結(jié)束的。如果把th.join()這行注釋掉,運行結(jié)果如下:

main  0
main  1
main  2
main  3
main  4
main  5
main  6
main  7
main  8
main  9
new thread  0
new thread  1
new thread  2
new thread  3
new thread  4
joined thread  0
joined thread  1
joined thread  2
joined thread  3
joined thread  4

getName和setName

用來得到或者設(shè)置線程名稱。

getPriority和setPriority

用來獲取和設(shè)置線程優(yōu)先級。

setDaemon和isDaemon

用來設(shè)置線程是否成為守護線程和判斷線程是否是守護線程。

守護線程和用戶線程的區(qū)別在于:守護線程依賴于創(chuàng)建它的線程,而用戶線程則不依賴。舉個簡單的例子:如果在main線程中創(chuàng)建了一個守護線程,當(dāng)main方法運行完畢之后,守護線程也會隨著消亡。而用戶線程則不會,用戶線程會一直運行直到其運行完畢。在JVM中,像垃圾收集器線程就是守護線程。

在上面已經(jīng)說到了Thread類中的大部分方法,那么Thread類中的方法調(diào)用到底會引起線程狀態(tài)發(fā)生怎樣的變化呢?下面一幅圖就是在上面的圖上進行改進而來的:

停止線程

停止線程是在多線程開發(fā)時很重要的技術(shù)點,掌握此技術(shù)可以對線程的停止進行有效的處理。
停止一個線程可以使用Thread.stop()方法,但最好不用它。該方法是不安全的,已被棄用。
在Java中有以下3種方法可以終止正在運行的線程:

  • 使用退出標(biāo)志,使線程正常退出,也就是當(dāng)run方法完成后線程終止
  • 使用stop方法強行終止線程,但是不推薦使用這個方法,因為stop和suspend及resume一樣,都是作廢過期的方法,使用他們可能產(chǎn)生不可預(yù)料的結(jié)果。
  • 使用interrupt方法中斷線程,但這個不會終止一個正在運行的線程,還需要加入一個判斷才可以完成線程的停止。

暫停線程

interrupt()方法

線程的優(yōu)先級

在操作系統(tǒng)中,線程可以劃分優(yōu)先級,優(yōu)先級較高的線程得到的CPU資源較多,也就是CPU優(yōu)先執(zhí)行優(yōu)先級較高的線程對象中的任務(wù)。
設(shè)置線程優(yōu)先級有助于幫“線程規(guī)劃器”確定在下一次選擇哪一個線程來優(yōu)先執(zhí)行。
設(shè)置線程的優(yōu)先級使用setPriority()方法,此方法在JDK的源碼如下:

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);
        }
    }

在Java中,線程的優(yōu)先級分為1~10這10個等級,如果小于1或大于10,則JDK拋出異常throw new IllegalArgumentException()。
JDK中使用3個常量來預(yù)置定義優(yōu)先級的值,代碼如下:
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

線程優(yōu)先級特性:

  • 繼承性
    比如A線程啟動B線程,則B線程的優(yōu)先級與A是一樣的。
  • 規(guī)則性
    高優(yōu)先級的線程總是大部分先執(zhí)行完,但不代表高優(yōu)先級線程全部先執(zhí)行完。
  • 隨機性
    優(yōu)先級較高的線程不一定每一次都先執(zhí)行完。

守護線程

在Java線程中有兩種線程,一種是User Thread(用戶線程),另一種是Daemon Thread(守護線程)。
Daemon的作用是為其他線程的運行提供服務(wù),比如說GC線程。其實User Thread線程和Daemon Thread守護線程本質(zhì)上來說去沒啥區(qū)別的,唯一的區(qū)別之處就在虛擬機的離開:如果User Thread全部撤離,那么Daemon Thread也就沒啥線程好服務(wù)的了,所以虛擬機也就退出了。

守護線程并非虛擬機內(nèi)部可以提供,用戶也可以自行的設(shè)定守護線程,方法:public final void setDaemon(boolean on) ;但是有幾點需要注意:

  • thread.setDaemon(true)必須在thread.start()之前設(shè)置,否則會跑出一個IllegalThreadStateException異常。你不能把正在運行的常規(guī)線程設(shè)置為守護線程。 (備注:這點與守護進程有著明顯的區(qū)別,守護進程是創(chuàng)建后,讓進程擺脫原會話的控制+讓進程擺脫原進程組的控制+讓進程擺脫原控制終端的控制;所以說寄托于虛擬機的語言機制跟系統(tǒng)級語言有著本質(zhì)上面的區(qū)別)

  • 在Daemon線程中產(chǎn)生的新線程也是Daemon的。 (這一點又是有著本質(zhì)的區(qū)別了:守護進程fork()出來的子進程不再是守護進程,盡管它把父進程的進程相關(guān)信息復(fù)制過去了,但是子進程的進程的父進程不是init進程,所謂的守護進程本質(zhì)上說就是“父進程掛掉,init收養(yǎng),然后文件0,1,2都是/dev/null,當(dāng)前目錄到/”)

  • 不是所有的應(yīng)用都可以分配給Daemon線程來進行服務(wù),比如讀寫操作或者計算邏輯。因為在Daemon Thread還沒來的及進行操作時,虛擬機可能已經(jīng)退出了。

同步與死鎖

  1. 同步代碼塊
    在代碼塊上加上"synchronized"關(guān)鍵字,則此代碼塊就稱為同步代碼塊

  2. 同步代碼塊格式

synchronized(同步對象){
    需要同步的代碼塊;
}
  1. 同步方法
    除了代碼塊可以同步,方法也是可以同步的
  2. 方法同步格式
synchronized void 方法名稱(){}

synchronized后續(xù)會單獨來學(xué)習(xí)。(●'?'●)

面試題

線程和進程有什么區(qū)別?
答:一個進程是一個獨立(self contained)的運行環(huán)境,它可以被看作一個程序或者一個應(yīng)用。而線程是在進程中執(zhí)行的一個任務(wù)。線程是進程的子集,一個進程可以有很多線程,每條線程并行執(zhí)行不同的任務(wù)。不同的進程使用不同的內(nèi)存空間,而所有的線程共享一片相同的內(nèi)存空間。別把它和棧內(nèi)存搞混,每個線程都擁有單獨的棧內(nèi)存用來存儲本地數(shù)據(jù)。

如何在Java中實現(xiàn)線程?
答:
創(chuàng)建線程有兩種方式:
一、繼承 Thread 類,擴展線程。
二、實現(xiàn) Runnable 接口。

啟動一個線程是調(diào)用run()還是start()方法?
答:啟動一個線程是調(diào)用start()方法,使線程所代表的虛擬處理機處于可運行狀態(tài),這意味著它可以由JVM 調(diào)度并執(zhí)行,這并不意味著線程就會立即運行。run()方法是線程啟動后要進行回調(diào)(callback)的方法。

Thread類的sleep()方法和對象的wait()方法都可以讓線程暫停執(zhí)行,它們有什么區(qū)別?
答:sleep()方法(休眠)是線程類(Thread)的靜態(tài)方法,調(diào)用此方法會讓當(dāng)前線程暫停執(zhí)行指定的時間,將執(zhí)行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結(jié)束后會自動恢復(fù)(線程回到就緒狀態(tài),請參考第66題中的線程狀態(tài)轉(zhuǎn)換圖)。wait()是Object類的方法,調(diào)用對象的wait()方法導(dǎo)致當(dāng)前線程放棄對象的鎖(線程暫停執(zhí)行),進入對象的等待池(wait pool),只有調(diào)用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態(tài)。

線程的sleep()方法和yield()方法有什么區(qū)別?
答:
① sleep()方法給其他線程運行機會時不考慮線程的優(yōu)先級,因此會給低優(yōu)先級的線程以運行的機會;yield()方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運行的機會;
② 線程執(zhí)行sleep()方法后轉(zhuǎn)入阻塞(blocked)狀態(tài),而執(zhí)行yield()方法后轉(zhuǎn)入就緒(ready)狀態(tài);
③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
④ sleep()方法比yield()方法(跟操作系統(tǒng)CPU調(diào)度相關(guān))具有更好的可移植性。

請說出與線程同步以及線程調(diào)度相關(guān)的方法。
答:

  • wait():使一個線程處于等待(阻塞)狀態(tài),并且釋放所持有的對象的鎖;
  • sleep():使一個正在運行的線程處于睡眠狀態(tài),是一個靜態(tài)方法,調(diào)用此方法要處理InterruptedException異常;
  • notify():喚醒一個處于等待狀態(tài)的線程,當(dāng)然在調(diào)用此方法的時候,并不能確切的喚醒某一個等待狀態(tài)的線程,而是由JVM確定喚醒哪個線程,而且與優(yōu)先級無關(guān);
  • notityAll():喚醒所有處于等待狀態(tài)的線程,該方法并不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態(tài);

總結(jié)

以上就是多線程的一些基礎(chǔ)概念,可能總結(jié)的不夠仔細,多多包涵。后續(xù)會針對一些比較重要的知識點單獨列出來總結(jié)。學(xué)好多線程是拿高薪的基礎(chǔ),小伙伴一起加油吧!

參考

該文為本人學(xué)習(xí)的筆記,方便以后自己跳槽前復(fù)習(xí)。參考網(wǎng)上各大帖子,取其精華整合自己的理解而成。還有,關(guān)注我個人主頁的公眾號,里面電子書資源有《Java多線程編程核心技術(shù)》以及《JAVA并發(fā)編程實踐》高清版,需要的小伙伴自己取。

《Java多線程編程核心技術(shù)》
《JAVA并發(fā)編程實踐》
Java并發(fā)編程:Thread類的使用
關(guān)于Java并發(fā)編程的總結(jié)和思考
JAVA多線程實現(xiàn)的三種方式

整理的思維導(dǎo)圖

個人整理的多線程基礎(chǔ)的思維導(dǎo)圖,導(dǎo)出的圖片無法查看備注的一些信息,所以需要源文件的童鞋可以關(guān)注我個人主頁上的公眾號,回復(fù)多線程基礎(chǔ)即可獲取源文件。


一直覺得自己寫的不是技術(shù),而是情懷,一篇篇文章是自己這一路走來的痕跡??繉I(yè)技能的成功是最具可復(fù)制性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡(luò),希望未來技術(shù)之巔上有你也有我。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容