java多線程基礎

為什么使用多線程

可以最大限度地利用CPU的空閑時間來處理其它任務。異步處理不同的任務,提高任務處理效率。

線程的五種狀態

1、新建狀態(New):線程對象創建后,就進入新建狀態。
2、就緒狀態(Runnable):就緒狀態又稱可執行狀態,線程被創建后通過調用start()方法,從而啟動線程。就緒狀態的線程,隨時有可能被CPU調度運行。
3、運行狀態(Running):線程獲取CPU權限進行執行。只有就緒狀態的線程才能進入運行狀態。
4、阻塞狀態(Blocked):線程因為某種原因放棄CPU使用權,停止運行。直到線程進入就緒狀態,才可以再到運行狀態。
阻塞狀態三種情況:
(1)、等待阻塞:通過調用線程的wait()方法,讓線程等待某工作完成
(2)、同步阻塞:線程獲取同步鎖synchronized同步鎖失敗(因為鎖正在被其它線程使用),進入同步阻塞。
(3)、其它阻塞:通過調用線程的sleep()、join()或發出I/O請求,線程進入阻塞狀態。當sleep()狀態超時、join()等待終止或超時、或者I/O處理完畢時,線程重新進入就休狀態。
5、死亡狀態(Dead):線程正常直行完成或者因為異常原因退出run()方法,該線程生命周期結束。

Paste_Image.png

通過Thread和Runnable創建線程

Java的JDK開發包中,已經自帶了對多線程技術的支持,我們可以很方便的進行多線程編程。
實現多線程編程的方式主要有兩種,一種是繼承Thread類,另一種就是實現Runnable接口。而Thread和Runnable的關系就是Thread類實現了Runnable接口:

public class Thread implements Runnable {}

1、Runnable實現

java.lang.Runnable是一個接口,里面只定義了run()抽象方法。如果要實現多線程,可以實現Runnable接口,然后通過Thread thread = new Thread(new Xxx()),其中Xxx是實現Runnable接口的類。

public interface Runnable {
  public abstract void run();
}

Runnable方式實現多線程

public class RunnableTest implements Runnable{
    int num = 10;
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(this.num > 0){
                System.out.println(Thread.currentThread().getName()+" num:" +this.num-- );
            }
        }
    }
}
class Test{
    public static void main(String[] args){
        RunnableTest runnable = new RunnableTest();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        thread1.start();//只有start()后才進入就緒狀態
        thread2.start();
        thread3.start();
    }
}

運行結果

  • Thread-0 num:10
  • Thread-0 num:7
  • Thread-2 num:8
  • Thread-1 num:10
  • Thread-1 num:9
  • Thread-1 num:4
  • Thread-1 num:3
  • Thread-1 num:2
  • Thread-1 num:1
  • Thread-2 num:5
  • Thread-0 num:6

結論:三個線程共享num變量,共同對num相減十次。這種情況也是出現“非線程安全的”原因,兩個線程可能同時獲取了同一變量值,同時進行操作,比如上面的線程0和線程1都打印了10。

2、Thread實現

java.lang.Thread是一個類,實現了Runnable接口。如果要實現多線程,需要繼承Thread類,然后通過創建實現類對象來啟動線程。
Thread方式實現多線程

class ThreadTest extends Thread{
    int num = 10;
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            if(this.num > 0){
                System.out.println(this.getName() + " num:" + this.num--);
            }
        }
    }
}
class Test{
    public static void main(String[] args){
        ThreadTest threadTest1 = new ThreadTest();
        ThreadTest threadTest2 = new ThreadTest();
        ThreadTest threadTest3 = new ThreadTest();
        threadTest1.start();
        threadTest2.start();
        threadTest3.start();
    }
}

運行結果

  • Thread-0 num:10
  • Thread-0 num:9
  • Thread-0 num:8
  • Thread-0 num:7
  • Thread-0 num:6
  • Thread-2 num:10
  • Thread-2 num:9
  • Thread-1 num:10
  • Thread-1 num:9
  • Thread-0 num:1
  • ....

結論:通過繼承Thread類創建的線程,每個線程直接不會共享變量。每個線程都會各自對num進行10次相減。
實際上因為Thread實現了Runnable接口,所以我們也可以使用new Thead(theadTest)形式來啟動線程,這樣得到結果和實現Runnable接口結果時一樣的,因為所有線程都共享了同一個theadTest的num變量。

還有一個需要注意的點,就是如果多次調用start()方法,則會拋出異常:Exception in thread "main" java.lang.IllegalThreadStateException。

Thread對象交由其它線程執行

Thread的構造方法可以接收實現了Runnable接口的線程,而Thread類是Runnable接口的實現類,所以我們可以將一個Thread對象交由另一個線程執行,也就是另一個線程指向這個Thread實現類的run方法。
在說明這個問題前我們先介紹下Thread類的介個方法:

  • currentThread:返回當前代碼塊被哪個線程執行。
  • getName:返回當前線程名稱。
  • isAlive():判斷當前線程是否處于活動狀態,活動狀態是指線程已經啟動并且還沒有執行完成(就緒狀態、運行狀態和阻塞狀態)。

下面是將線程1直接交由線程2來執行:

public class ThreadDemo {
    public static void main(String[] args) {
        //創建線程1
        ThreadTest thread1 = new ThreadTest();
        thread1.setName("rename thread1");
        //創建線程2,并將線程1傳遞給線程2
        Thread thread2 = new Thread(thread1);
        thread2.setName("rename thread2");
        thread2.start();
    }
}

class ThreadTest extends Thread {

    public ThreadTest() {
        System.out.println("====1:" + Thread.currentThread().getName());
        System.out.println("====1:" + Thread.currentThread().isAlive());
        System.out.println("====2:" + this.getName());
        System.out.println("====2:" + this.isAlive());
    }

    @Override
    public void run() {
        System.out.println("====3:" + Thread.currentThread().getName());
        System.out.println("====3:" + Thread.currentThread().isAlive());
        System.out.println("====4:" + this.getName());
        System.out.println("====4:" + this.isAlive());
    }
}

上面程序執行結果:

====1:main
====1:true
====2:Thread-0
====2:false
====3:rename thread2
====3:true
====4:rename thread1
====4:false

以====1開頭的打印內容是在ThreadTest構造方法中打印的,而調用ThreadTest構造方法是在main進程中的main方法中調用的,所以Thread.currentThread()表示的是main進程(因為ThreadTest代碼塊當前被main進程執行)。那====2的打印結果又說明什么呢,我們先看一下this.getName()好Thread.currentThread().getName()的區別。

Thread.currentThread()是指執行當前代碼塊的線程,所以Thread.currentThread().getName()給出的是執行當前代碼塊的線程名稱。而this是指當前線程對象,上面的例子也就是ThreadTest的實例,所以this.getName()給出的是當前對象的線程名稱。

通過上面的說明我們就知道了====2實際打印的是ThreadTest的一個實例,Thread-0是當我們創建一個Thread實例時,Java為我們提供的默認線程名稱,并且Thread-0此時并沒有運行(當前是main進程在執行)。
====3和=====4是在ThreadTest的run方法定義的,當啟動thread2線程時候會執行該run方法。====3同樣指的是運行當前代碼塊的線程,也就是"rename thread2"(因為我們在啟動thread2時為其重新定義名稱),并且thread2處于運行狀態(在執行當前代碼塊)。而====4的this此時還是值得ThreadTest的實例,也就是thread1線程,thread1也進行了重命名"rename thread1"(調用構造方法時還沒有進行重命名,所以當時的名稱是默認名稱Thread-0),并且當前thread1線程并沒有執行,thread1中的run方法是交由thread2線程執行的。

注意當前執行線程(Thread.currentThread())和當前線程對象(this)的區別。但是要清楚this.currentThread()和Thread.currentThread()是一樣。

上面的實例就說明:線程被創建后可以不執行,而是交由其它線程執行。其它線程啟動后,就會調用這個被創建的線程的run方法。

3、Runnable和Thread區別

  • 通過實現Runnable接口能夠解決單繼承問題,如果實現Thread類則不能繼承其它類了。
  • 通過Runnable接口方式實現多線程,能夠起到資源共享的目的,因為多個線程共用一個Runnable實現類,而Thread是繼承Runnable接口,每個Thread都會實現各自的Runnable接口。這種說法也不絕對,因為我們也可以將同一個Thread的實現類交由new Thread(Runnable target)來達到共享變量的目的。

4、Thread中start()和run()方法

start()和run()都是Thread中的方法,start()會啟動一個新線程,新線程會去執行相應的run()方法,start()不能重復調用。run()和普通成員方法一樣,單獨調用run()方法,不會啟動新線程,而是使用當前的線程執行run()方法(哪個線程調用的run方法,就由哪個線程來執行),這個run()和普通方法一樣,可以被重復調用。
run()源代碼:

@Override
public void run() {
    if (target != null) {
        //交由指定的線程來執行定義的run方法
        target.run();
    }
}

target是一個Runnable對象,run()方法直接調用Thread線程的Runnable的run()方法,并不會新建一個線程。

start()源代碼:

public synchronized void start() {
    
    if (threadStatus != 0)//如果線程不是處于就緒狀態,拋出異常
        throw new IllegalThreadStateException();
    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 */
        }
    }
}

private native void start0(); //啟動線程的本地方法

start()通過調用本地start0()方法,啟動一個線程,新線程會調用run()方法。

線程常用方法

sleep()

Thread.sleep()的作用是“當前正在執行的線程”休眠指定時間,這個當前正在執行的線程指的是Thread.currentThread()。

getId()

this.getId()用于獲取線程的唯一標識。

yield()方法

yield()方法的作用是讓當前線程放棄CPU的使用權,讓其它任務執行。但是放棄的時間不固定,有可能馬上又獲取到CPU時間片。
測試下面代碼,查看注釋Thread.yield()前后的執行耗時。

class YieldThread extends Thread {
    @Override
    public void run() {
        int count = 0;
        long beginTime = System.currentTimeMillis();
        for(int i= 0; i < 1000000; i++){
            //Thread.yield();
            count = count + i + 1;
        }
        long endTime = System.currentTimeMillis();
        System.out.println("耗時" + (endTime - beginTime) + "毫秒!");
    }
}
//注釋Thread.yield()打印結果:
耗時6毫秒!
//打開注釋
耗時820毫秒!

停止線程

Java中有以下三種方式來停止正在運行的線程。

  1. 使用退出標志,讓線程正常退出,也就是執行完了run方法后自動停止。
  2. 使用Thread類提供的stop()、suspend()或resume(),但是這三種方法是不建議使用的,因為它們可能產生不可預期的結果(可能一些清理工作得不到完成;對一些鎖定對象進行了“解鎖”,導致得不到同步處理,從而出現數據不一致的問題),并且這三個方法已經被標記為廢棄了。
  3. 使用interrupt方法來中斷線程,進而手動實現停止當前線程。

對于第一點沒什么說的,通過判斷退出標志位來退出run方法。第二點中Thread中自帶的stop()和suspend()由于不安全(從JDK2,開始不建議使用),所以已經過時不在建議使用了。線程中斷是目前停止線程的通用方式,也是我們這里要說的方式。

線程中斷方法

使用interrupt()方法并不會像使用for-break那樣立即退出循環,調用interrupt()方法只是將當前線程打了一個標記,也就是中斷標記。我們還需要加入一個判斷邏輯,來手動停止當線程。
Thread類提供了兩個方法來判斷當前線程中斷標記:

  • Thread.interrupted():判斷當前線程是否已經中斷,當前線程指的是當前正在運行的線程(如果使用threadInstance.intrrupted()方式則指的是當前線程對象,有可能不是正在運行的線程)。Thread.interrupted()除了具有判斷線程是否中斷的功能,它還會清除線程中斷標記。也就是說如果當前線程中斷狀態為true,那么調用過interrupted后線程中斷狀態重新恢復到false。
  • isInterrupted():判斷線程對象(有可能不是正在運行的線程)是否已經中斷,該方法不是靜態方法,所以不能通過Thread.currentThread調用。

注意isInterrupted和interrupted的區別:所以只能被線程對象調用,這時候線程對象有可能并沒有運行;另一個重要的區別在于isInterrupted方法不會清楚中斷狀態。

停止運行狀態的線程

停止運行中的線程,我們可以通過:判斷線程中斷標記+手動拋出異常的方式來停止線程。

class ThreadDemo extends Thread {
    @Override
    public void run()  {
        try {
            while (true){
                if(this.isInterrupted())
                    throw new InterruptedException();
                
                //執行業務邏輯
            }
        }catch (InterruptedException e) {
            System.out.println("進入Catch代碼塊,run方法執行完成,退出異常");
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        ThreadDemo  threadDemo = new ThreadDemo();
        threadDemo.start();
        //中斷線程標記
        threadDemo.interrupt();
    }
}

注意try-catch需要在while循環外部,否則不會退出run方法。

停止阻塞狀態的線程

停止阻塞狀態的線程比較容易,因為它不需要我們判斷中斷標記。當對阻塞狀態中的線程調用interrupt()方法時,會自動拋出InterrupterdException異常,并且會清除中斷標記狀態值,也就是變為false。

public class ThreadTest {
    public static void main(String[] args) {
        ThreadDemo  threadDemo = new ThreadDemo();
        threadDemo.start();
        threadDemo.interrupt();
    }
}

class ThreadDemo extends Thread {
    @Override
    public void run()  {
        try {
            while (true){
                //業務執行邏輯
                
                Thread.sleep(10000);
            }
        }catch (InterruptedException e) {
            System.out.println("進入Catch代碼塊,run方法執行完成,退出異常");
        }
    }
}

需要注意try-catch在代碼中的維值,如果將異常處理放在while()中,這樣while(true)不會被停止。

暫停線程

暫停線程是指此線程能夠暫時停止,之后還能恢復運行。我們可以使用Thread類中的suspend()方法來暫停線程,使用resume()方法來恢復線程執行。

需要注意:suspend()、resume()方法和 stop()方法都已經廢棄了,因為它們有可能產生不可預知的后果

suspend()和resume()使用實例:

class MyThread extends Thread {
    private long i = 0;

    public long getI() {
        return i;
    }
    @Override
    public void run() {
        while (true)
            i++;
    }
}
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();
        myThread.start();
        Thread.sleep(3000);
        myThread.suspend();
        System.out.println("i:" + myThread.getI());
        Thread.sleep(3000);
        System.out.println("i:" + myThread.getI());
        myThread.resume();
        Thread.sleep(3000);
        myThread.suspend();
        System.out.println("i:" + myThread.getI());
        Thread.sleep(3000);
        System.out.println("i:" + myThread.getI());
    }
}
打印結果:
i:1629776063
i:1629776063
i:3296163993
i:3296163993

從上面的打印結果可以看到,線程確實暫停了,之后又重新恢復了。之所以將它們廢棄,主要是使用suspend()和resume()極易造成公共同步對象獨占,導致其它線程無法訪問公共對象。

synchronized void printString(String str) {
  if(Thread.currentThread().getName().equals("TheadA")) {
    System.out.println("被線程A獨占");
    Thread.currentThread().suspend();
  }
}

除了上面問題,suspend()和resume()方法如果使用不當,也會造成數據不同步,所以我們應該避免使用這兩個方法。

synchronized同步鎖

原理:每個對象都有且只有一個同步鎖,同步鎖依賴于對象存在。當調用某個對象的synchronized方法時,就獲取該對象的同步鎖。例如synchronized(obj)就是獲取了obj的同步鎖,不同線程對同步鎖訪問是互斥的。就是說一個時間點,對象的同步鎖只能被一個線程調用,其它線程如果要使用,需要等待正在使用同步鎖的線程釋放掉后才能使用。

synchronized規則:

  • 當一個線程訪問某對象的synchronized方法或synchronized代碼塊時,其它線程對該對象的該synchronized方法或者synchronized代碼塊訪問將被受阻;
  • 當一個線程訪問某個對象的synchronized方法或synchronized代碼塊時,其它線程可以訪問該對象的非synchronized方法或synchronized代碼塊;
  • 當一個線程訪問某個對象的synchronized方法或synchronized代碼塊時,當其它對象訪問該對象的synchronized方法或synchronized代碼塊時,其它對象的線程將被受阻。
public class RunnableTest implements Runnable{
    @Override
    public void run() {
        synchronized(this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName()+" num:" + i );
            }
        }
    }
}

class Test{
    public static void main(String[] args){
        RunnableTest runnable = new RunnableTest();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        Thread thread3 = new Thread(runnable);
        thread1.start();//只有start()后才進入就緒狀態
        thread2.start();
        thread3.start();
    }
}

運行結果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-0 num:2
  • Thread-2 num:0
  • Thread-2 num:1
  • Thread-2 num:2
  • Thread-1 num:0
  • Thread-1 num:1
  • Thread-1 num:2

結論:
當一個線程訪問對象的synchronized方法或者代碼塊,其它線程將會被阻塞。Thread0、Thread1、Thread2共用RunnableTest實現Runnable接口的同步鎖,當一個線程運行synchronized()代碼塊時候,其它線程需要等待正在運行的線程釋放同步鎖后才能運行。

再來看下使用Thread方式實現多線程的獲取同步鎖的執行流程

class ThreadTest extends Thread{
    int num = 10;
    @Override
    public void run() {
        synchronized(this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName()+" num:" + i );
            }
        }

    }
}

class Test{
    public static void main(String[] args){
        ThreadTest threadTest1 = new ThreadTest();
        ThreadTest threadTest2 = new ThreadTest();
        ThreadTest threadTest3 = new ThreadTest();
        threadTest1.start();
        threadTest2.start();
        threadTest3.start();
    }
}

運行結果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-0 num:2
  • Thread-2 num:0
  • Thread-1 num:0
  • Thread-2 num:1
  • Thread-1 num:1
  • Thread-2 num:2
  • Thread-1 num:2

結論:
發現并沒有我們之前說的Thread0、Thread1、Thread2阻塞順序執行,這個主要是和Thread形式創建多線程有關,trhreadTest1、trhreadTest2、trhreadTest3是三個不同的對象,它們是通過new ThreadTest()創建的三個對象,這里synchronized(this)是指的ThreadTest對象,所以threadTest1、threadTest2、threadTest3是獲取的三個不同的同步鎖。而上面使用RunnableTest方式實現的多線程,this是指的RunnableTest,這樣三個線程使用的是同一個對象的同步鎖。

當一個進程訪問對象的同步鎖時,其它線程可以訪問這個對象的非synchronize代碼塊

class ThreadTest2{
    public void synMethod(){
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num: " + i);
            }
        }
    }

    public void nonSynMethod(){
        for(int i=0;i<3;i++){
            System.out.println(Thread.currentThread().getName() + " num:" + i);
        }
    }
}
class Test{
    public static void main(String[] args){
        final ThreadTest2 threadTest = new ThreadTest2();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.synMethod();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.nonSynMethod();
            }
        });
        thread1.start();
        thread2.start();
    }
}

返回結果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-1 num:0
  • Thread-0 num:2
  • Thread-1 num:1
  • Thread-1 num:2

結論:
thread1訪問對象的synchronize代碼塊,thread2訪問非synchronized代碼塊。thread2并沒有因為thread1受阻。

當一個線程訪問一個對象的synchronized方法或代碼塊,其它線程訪問這個對象的其它synchronized也是受阻的。

class ThreadTest2{
    public void synMethod1(){
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num: " + i);
            }
        }
    }

    public void synMethod2(){
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num:" + i);
            }
        }
    }
}
class Test{
    public static void main(String[] args){
        final ThreadTest2 threadTest = new ThreadTest2();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.synMethod1();
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadTest.synMethod2();
            }
        });
        thread1.start();
        thread2.start();
    }
}

返回結果

  • Thread-0 num:0
  • Thread-0 num:1
  • Thread-0 num:2
  • Thread-1 num:0
  • Thread-1 num:1
  • Thread-1 num:2

結論:thread1、thread2都會調用ThreadTest2的synchronized(this)代碼塊,而這個this都是ThreadTest2,所以線程2需要等到線程1執行完synchronized才能執行。

synchronized方法和synchronized代碼塊

synchronized方法是用synchronized修飾類方法,synchronized代碼塊是用synchronized修飾代碼塊的。synchronized代碼塊可以更精準的控制限制區域,有時效率也是比synchronized方法高的。

class ThreadTest2{
    public synchronized void synMethod1(){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num: " + i);
            }
    }

    public void synMethod2(){
        //this獲取當前對象的同步鎖,如果修改成xxx,則獲取xxx的同步鎖
        synchronized (this){
            for(int i=0;i<3;i++){
                System.out.println(Thread.currentThread().getName() + " num:" + i);
            }
        }
    }
}

實例鎖和全局鎖

  • 實例鎖:如果鎖在某一個實例上面,那么該鎖就是實例鎖。如果這個類是單例,那么這個鎖也具有全局鎖的概念。實例鎖使用synchronized關鍵字。
  • 全局鎖:如果鎖針對的是一個類上面,無論多少個實例共享這個鎖。全局鎖使用static synchronized,或者鎖在該類的class或classloader上面。
class SomeLock{
    public synchronized void intanceLockA(){}
    public synchronized void instanceLockB(){};
    public static synchronized void globalLockA(){};
    public static synchronized void globalLockB(){};
}
  1. x.instaceLockA()和x.instanceLockB(),二者不能同時被訪問,因為二者都是訪問的都是x的實例鎖。
  2. x.instaceLockA()和y.instaceLockA(),二者可以同時被訪問,因為二者訪問的不是同一個對象的鎖。
  3. x.globalLocckA()和y.globalLockB(),二者不能同時訪問,因為y.globalLockB()相當于SomeLock.globalLockB(),x.globalLockA()相當于SomeLock.globalLockA(),二者使用的是同一個同步鎖,所以不能同時被訪問。
  4. x.instaceLocakA()和b.globalLockA(),二者可以同時被訪問,因為一個是示例的鎖,一個是類的鎖。

線程的等待與喚醒

線程的等待與喚醒使用了Object類中的wait()、wait(long timeout)、wait(long timeout,int nanos)、notify()、notifyAll()

  • wait():使線程進入等待狀態(等待阻塞),直到其它線程調用該對象的 notify()或notifyAll(),當前線程會被喚醒(進入就緒狀態)。
  • wait(long timeout):使線程進入等待狀態(等待阻塞),直到其它線程調用該對象的notify()或notifyAll()或超過了指定時間,當前線程會被喚醒(進入就緒狀態)。
  • wait(long timeout,int nanos):使線程進入等待狀態(等待阻塞),直到其它線程調用該對象的notify()或notifyAll()或超過了指定時間或被其它線程中斷,當前線程會被喚醒(進入就緒狀態)。
  • notify():喚醒在此對象監視器(同步鎖的實現原理)上等待的單個線程。
  • notifyAll():喚醒在此對象監視器上等待的多個線程。

注意:
wait()的作用是讓當前線程等待,當前線程指的是正在cpu運行的線程,而不是調用wait()方法的線程。wait()、notify()、notifyAll()都是屬于Object類下邊的方法,之所以在Object下面而沒有在Thread類下面,主要原因就是同步鎖。

wait()和notify()都是對對象的同步鎖進行操作,同步鎖是對象持有的,并且每個對象有且僅有一個。

線程讓步yield()

yield()的作用是讓步。讓當前線程由運行狀態進入就緒狀態,從而讓其他具有高優先級的線程獲取cpu執行。但是并不會保證當前線程調用yield()后,其它同等級線程一定獲取到cpu執行權。也有可能當前線程又進入到運行狀態。

yield()與wait()的區別

  • wait()會由運行狀態進入等待狀態(阻塞狀態),而yield()會從運行狀態進入就緒狀態。
  • wait()會釋放對象的同步鎖,而yield()是不會釋放對象的同步鎖的。

線程休眠sleep()

sleep()在Thread.class類中定義,讓當前線程由運行狀態進入休眠狀態(阻塞狀態)。sleep()需要指定休眠時間,線程休眠時間會大于等于該休眠時間;線程被重新喚醒時會由阻塞狀體進入就緒狀態。

sleep()與wait()區別

sleep()和wait()都會讓線程由運行狀態進入阻塞狀態,但是wait()會釋放對象同步鎖,而sleep()不會釋放同步鎖。

線程join()

join()在Thread.class類中定義,讓主線程等待子線程結束后才能繼續運行。

// 主線程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子線程
public class Son extends Thread {
    public void run() {
        ...
    }
}

Son線程是在Father線程中創建的,并且調用了s.join(),這樣Father線程要等到Son線程執行完成后,才會執行。可以查看下join()的源代碼:

public final void join() throws InterruptedException {
    join(0);
}
public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

join()中通過wait()進行等待,所以即使是子線程調用的join(),而真實等待的是正在執行的父進程。

線程優先級

在操作系統中,線程可以劃分優先級,優先級較高的線程被CPU優先執行。設置線程優先級就是幫助“線程規劃器”確定下次選哪一個線程來優先執行。
java中線程優先級從1~10,默認是5。Thead類提供了3個常量預定義優先級的值:

    public final static int MIN_PRIORITY = 1;
    public final static int NORM_PRIORITY = 5;
    public final static int MAX_PRIORITY = 10;

線程優先級具有繼承性,這里的繼承性是指比如在A線程中啟動B線程,那么B線程就有與A線程同樣的優先級。
需要注意的是,高優先級的線程總是大部分都會先執行完成,但是并不代表高優先級的線程執行完成后,再去執行低優先級的線程。高優先級,只說明CPU盡量將執行資源給優先級比較高的線程。

守護線程

在Java線程有兩種線程,非守護線程(又稱為用戶線程)和守護線程(Daemon)。
守護線程是一種特殊的線程,它的特性就是陪伴,當進程中不存在非守護線程了,則守護線程自動銷毀。比如GC線程就是典型的守護線程,當進程中沒有非守護線程了,則垃圾回收線程自動銷毀。
比如下面測試用例:

class DaemonThread extends Thread {
    @Override
    public void run() {
        int i =0;
        try {
            while (true) {
                System.out.println(i++);
                Thread.sleep(1000);
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true);
        daemonThread.start();
        Thread.sleep(5000);
        System.out.println("main線程退出,daemonThread也不會執行了");
    }
}
打印結果:
0
1
2
3
4
main線程退出,daemonThread也不會執行了

因為DaemonThread是守護線程,而main線程為非守護線程,當main線程退出后,daemonThread也會退出。

關注我

歡迎關注我的公眾號,會定期推送優質技術文章,讓我們一起進步、一起成長!
公眾號搜索:data_tc
或直接掃碼:??


歡迎關注我
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,214評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,315評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,699評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,882評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,441評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,189評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,388評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,613評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,112評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,334評論 2 377

推薦閱讀更多精彩內容

  • 寫在前面的話: 這篇博客是我從這里“轉載”的,為什么轉載兩個字加“”呢?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,772評論 12 45
  • 前言 多線程并發編程是Java編程中重要的一塊內容,也是面試重點覆蓋區域,所以學好多線程并發編程對我們來說極其重要...
    嘟爺MD閱讀 7,332評論 21 272
  • 進程:正在執行中的程序,其實是應用程序在內存中運行的那片空間。 線程:進程中的一個執行單元,負責進程中程序的執行。...
    七弦桐語閱讀 472評論 2 7
  • 簡介 本次主要介紹java多線程中的同步,也就是如何在java語言中寫出線程安全的程序。如何在java語言中解決非...
    小人物灌籃閱讀 491評論 0 1
  • 吃完pizza之后,發過朋友圈之后,一陣陣的大笑之后,又剩下了什么呢,六個可愛的室友,在一起笑,在一起嗨,哈哈大笑...
    Janice1閱讀 257評論 0 0