java多線程基礎

為什么使用多線程

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

線程的五種狀態(tài)

1栽烂、新建狀態(tài)(New):線程對象創(chuàng)建后,就進入新建狀態(tài)恋脚。
2腺办、就緒狀態(tài)(Runnable):就緒狀態(tài)又稱可執(zhí)行狀態(tài),線程被創(chuàng)建后通過調(diào)用start()方法糟描,從而啟動線程怀喉。就緒狀態(tài)的線程,隨時有可能被CPU調(diào)度運行蚓挤。
3磺送、運行狀態(tài)(Running):線程獲取CPU權(quán)限進行執(zhí)行。只有就緒狀態(tài)的線程才能進入運行狀態(tài)灿意。
4、阻塞狀態(tài)(Blocked):線程因為某種原因放棄CPU使用權(quán)崇呵,停止運行缤剧。直到線程進入就緒狀態(tài),才可以再到運行狀態(tài)域慷。
阻塞狀態(tài)三種情況:
(1)荒辕、等待阻塞:通過調(diào)用線程的wait()方法,讓線程等待某工作完成
(2)犹褒、同步阻塞:線程獲取同步鎖synchronized同步鎖失敗(因為鎖正在被其它線程使用)抵窒,進入同步阻塞。
(3)叠骑、其它阻塞:通過調(diào)用線程的sleep()李皇、join()或發(fā)出I/O請求,線程進入阻塞狀態(tài)宙枷。當sleep()狀態(tài)超時掉房、join()等待終止或超時茧跋、或者I/O處理完畢時,線程重新進入就休狀態(tài)卓囚。
5瘾杭、死亡狀態(tài)(Dead):線程正常直行完成或者因為異常原因退出run()方法,該線程生命周期結(jié)束哪亿。

Paste_Image.png

通過Thread和Runnable創(chuàng)建線程

Java的JDK開發(fā)包中粥烁,已經(jīng)自帶了對多線程技術(shù)的支持,我們可以很方便的進行多線程編程蝇棉。
實現(xiàn)多線程編程的方式主要有兩種页徐,一種是繼承Thread類,另一種就是實現(xiàn)Runnable接口银萍。而Thread和Runnable的關(guān)系就是Thread類實現(xiàn)了Runnable接口:

public class Thread implements Runnable {}

1变勇、Runnable實現(xiàn)

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

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

Runnable方式實現(xiàn)多線程

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()后才進入就緒狀態(tài)
        thread2.start();
        thread3.start();
    }
}

運行結(jié)果

  • 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

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

2、Thread實現(xiàn)

java.lang.Thread是一個類夜赵,實現(xiàn)了Runnable接口明棍。如果要實現(xiàn)多線程,需要繼承Thread類寇僧,然后通過創(chuàng)建實現(xiàn)類對象來啟動線程摊腋。
Thread方式實現(xiàn)多線程

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

運行結(jié)果

  • 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
  • ....

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

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

Thread對象交由其它線程執(zhí)行

Thread的構(gòu)造方法可以接收實現(xiàn)了Runnable接口的線程,而Thread類是Runnable接口的實現(xiàn)類值戳,所以我們可以將一個Thread對象交由另一個線程執(zhí)行议谷,也就是另一個線程指向這個Thread實現(xiàn)類的run方法。
在說明這個問題前我們先介紹下Thread類的介個方法:

  • currentThread:返回當前代碼塊被哪個線程執(zhí)行堕虹。
  • getName:返回當前線程名稱卧晓。
  • isAlive():判斷當前線程是否處于活動狀態(tài),活動狀態(tài)是指線程已經(jīng)啟動并且還沒有執(zhí)行完成(就緒狀態(tài)赴捞、運行狀態(tài)和阻塞狀態(tài))逼裆。

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

public class ThreadDemo {
    public static void main(String[] args) {
        //創(chuàng)建線程1
        ThreadTest thread1 = new ThreadTest();
        thread1.setName("rename thread1");
        //創(chuàng)建線程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());
    }
}

上面程序執(zhí)行結(jié)果:

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

以====1開頭的打印內(nèi)容是在ThreadTest構(gòu)造方法中打印的赦政,而調(diào)用ThreadTest構(gòu)造方法是在main進程中的main方法中調(diào)用的胜宇,所以Thread.currentThread()表示的是main進程(因為ThreadTest代碼塊當前被main進程執(zhí)行)。那====2的打印結(jié)果又說明什么呢恢着,我們先看一下this.getName()好Thread.currentThread().getName()的區(qū)別桐愉。

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

通過上面的說明我們就知道了====2實際打印的是ThreadTest的一個實例系洛,Thread-0是當我們創(chuàng)建一個Thread實例時,Java為我們提供的默認線程名稱略步,并且Thread-0此時并沒有運行(當前是main進程在執(zhí)行)描扯。
====3和=====4是在ThreadTest的run方法定義的,當啟動thread2線程時候會執(zhí)行該run方法纳像。====3同樣指的是運行當前代碼塊的線程荆烈,也就是"rename thread2"(因為我們在啟動thread2時為其重新定義名稱),并且thread2處于運行狀態(tài)(在執(zhí)行當前代碼塊)竟趾。而====4的this此時還是值得ThreadTest的實例,也就是thread1線程宫峦,thread1也進行了重命名"rename thread1"(調(diào)用構(gòu)造方法時還沒有進行重命名岔帽,所以當時的名稱是默認名稱Thread-0),并且當前thread1線程并沒有執(zhí)行导绷,thread1中的run方法是交由thread2線程執(zhí)行的犀勒。

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

上面的實例就說明:線程被創(chuàng)建后可以不執(zhí)行贾费,而是交由其它線程執(zhí)行钦购。其它線程啟動后,就會調(diào)用這個被創(chuàng)建的線程的run方法褂萧。

3押桃、Runnable和Thread區(qū)別

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

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

start()和run()都是Thread中的方法滨嘱,start()會啟動一個新線程峰鄙,新線程會去執(zhí)行相應的run()方法,start()不能重復調(diào)用九孩。run()和普通成員方法一樣先馆,單獨調(diào)用run()方法,不會啟動新線程躺彬,而是使用當前的線程執(zhí)行run()方法(哪個線程調(diào)用的run方法煤墙,就由哪個線程來執(zhí)行),這個run()和普通方法一樣宪拥,可以被重復調(diào)用仿野。
run()源代碼:

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

target是一個Runnable對象,run()方法直接調(diào)用Thread線程的Runnable的run()方法她君,并不會新建一個線程脚作。

start()源代碼:

public synchronized void start() {
    
    if (threadStatus != 0)//如果線程不是處于就緒狀態(tài),拋出異常
        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()通過調(diào)用本地start0()方法球涛,啟動一個線程,新線程會調(diào)用run()方法校镐。

線程常用方法

sleep()

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

getId()

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

yield()方法

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

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()打印結(jié)果:
耗時6毫秒!
//打開注釋
耗時820毫秒!

停止線程

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

  1. 使用退出標志贮预,讓線程正常退出,也就是執(zhí)行完了run方法后自動停止链嘀。
  2. 使用Thread類提供的stop()萌狂、suspend()或resume(),但是這三種方法是不建議使用的怀泊,因為它們可能產(chǎn)生不可預期的結(jié)果(可能一些清理工作得不到完成茫藏;對一些鎖定對象進行了“解鎖”,導致得不到同步處理霹琼,從而出現(xiàn)數(shù)據(jù)不一致的問題)务傲,并且這三個方法已經(jīng)被標記為廢棄了。
  3. 使用interrupt方法來中斷線程枣申,進而手動實現(xiàn)停止當前線程售葡。

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

線程中斷方法

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

  • Thread.interrupted():判斷當前線程是否已經(jīng)中斷,當前線程指的是當前正在運行的線程(如果使用threadInstance.intrrupted()方式則指的是當前線程對象数焊,有可能不是正在運行的線程)永淌。Thread.interrupted()除了具有判斷線程是否中斷的功能,它還會清除線程中斷標記佩耳。也就是說如果當前線程中斷狀態(tài)為true仰禀,那么調(diào)用過interrupted后線程中斷狀態(tài)重新恢復到false。
  • isInterrupted():判斷線程對象(有可能不是正在運行的線程)是否已經(jīng)中斷蚕愤,該方法不是靜態(tài)方法答恶,所以不能通過Thread.currentThread調(diào)用。

注意isInterrupted和interrupted的區(qū)別:所以只能被線程對象調(diào)用萍诱,這時候線程對象有可能并沒有運行悬嗓;另一個重要的區(qū)別在于isInterrupted方法不會清楚中斷狀態(tài)。

停止運行狀態(tài)的線程

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

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

注意try-catch需要在while循環(huán)外部周瞎,否則不會退出run方法。

停止阻塞狀態(tài)的線程

停止阻塞狀態(tài)的線程比較容易饵蒂,因為它不需要我們判斷中斷標記声诸。當對阻塞狀態(tài)中的線程調(diào)用interrupt()方法時,會自動拋出InterrupterdException異常退盯,并且會清除中斷標記狀態(tài)值彼乌,也就是變?yōu)閒alse。

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){
                //業(yè)務執(zhí)行邏輯
                
                Thread.sleep(10000);
            }
        }catch (InterruptedException e) {
            System.out.println("進入Catch代碼塊渊迁,run方法執(zhí)行完成慰照,退出異常");
        }
    }
}

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

暫停線程

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

需要注意:suspend()蝌蹂、resume()方法和 stop()方法都已經(jīng)廢棄了患整,因為它們有可能產(chǎn)生不可預知的后果

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());
    }
}
打印結(jié)果:
i:1629776063
i:1629776063
i:3296163993
i:3296163993

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

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

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

synchronized同步鎖

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

synchronized規(guī)則:

  • 當一個線程訪問某對象的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()后才進入就緒狀態(tài)
        thread2.start();
        thread3.start();
    }
}

運行結(jié)果

  • 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

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

再來看下使用Thread方式實現(xiàn)多線程的獲取同步鎖的執(zhí)行流程

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

運行結(jié)果

  • 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

結(jié)論:
發(fā)現(xiàn)并沒有我們之前說的Thread0、Thread1帖池、Thread2阻塞順序執(zhí)行奈惑,這個主要是和Thread形式創(chuàng)建多線程有關(guān),trhreadTest1睡汹、trhreadTest2肴甸、trhreadTest3是三個不同的對象,它們是通過new ThreadTest()創(chuàng)建的三個對象囚巴,這里synchronized(this)是指的ThreadTest對象原在,所以threadTest1、threadTest2彤叉、threadTest3是獲取的三個不同的同步鎖庶柿。而上面使用RunnableTest方式實現(xiàn)的多線程,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();
    }
}

返回結(jié)果

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

結(jié)論:
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();
    }
}

返回結(jié)果

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

結(jié)論:thread1碰辅、thread2都會調(diào)用ThreadTest2的synchronized(this)代碼塊,而這個this都是ThreadTest2介时,所以線程2需要等到線程1執(zhí)行完synchronized才能執(zhí)行。

synchronized方法和synchronized代碼塊

synchronized方法是用synchronized修飾類方法凌彬,synchronized代碼塊是用synchronized修飾代碼塊的沸柔。synchronized代碼塊可以更精準的控制限制區(qū)域,有時效率也是比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關(guān)鍵字俭正。
  • 全局鎖:如果鎖針對的是一個類上面,無論多少個實例共享這個鎖焙畔。全局鎖使用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():使線程進入等待狀態(tài)(等待阻塞)生年,直到其它線程調(diào)用該對象的 notify()或notifyAll(),當前線程會被喚醒(進入就緒狀態(tài))廓奕。
  • wait(long timeout):使線程進入等待狀態(tài)(等待阻塞)抱婉,直到其它線程調(diào)用該對象的notify()或notifyAll()或超過了指定時間,當前線程會被喚醒(進入就緒狀態(tài))桌粉。
  • wait(long timeout,int nanos):使線程進入等待狀態(tài)(等待阻塞)蒸绩,直到其它線程調(diào)用該對象的notify()或notifyAll()或超過了指定時間或被其它線程中斷,當前線程會被喚醒(進入就緒狀態(tài))铃肯。
  • notify():喚醒在此對象監(jiān)視器(同步鎖的實現(xiàn)原理)上等待的單個線程患亿。
  • notifyAll():喚醒在此對象監(jiān)視器上等待的多個線程。

注意:
wait()的作用是讓當前線程等待押逼,當前線程指的是正在cpu運行的線程步藕,而不是調(diào)用wait()方法的線程。wait()挑格、notify()咙冗、notifyAll()都是屬于Object類下邊的方法,之所以在Object下面而沒有在Thread類下面恕齐,主要原因就是同步鎖乞娄。

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

線程讓步y(tǒng)ield()

yield()的作用是讓步。讓當前線程由運行狀態(tài)進入就緒狀態(tài)士骤,從而讓其他具有高優(yōu)先級的線程獲取cpu執(zhí)行范删。但是并不會保證當前線程調(diào)用yield()后,其它同等級線程一定獲取到cpu執(zhí)行權(quán)拷肌。也有可能當前線程又進入到運行狀態(tài)到旦。

yield()與wait()的區(qū)別

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

線程休眠sleep()

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

sleep()與wait()區(qū)別

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

線程join()

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

// 主線程
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線程中創(chuàng)建的辆沦,并且調(diào)用了s.join()昼捍,這樣Father線程要等到Son線程執(zhí)行完成后,才會執(zhí)行众辨《巳可以查看下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()進行等待,所以即使是子線程調(diào)用的join()鹃彻,而真實等待的是正在執(zhí)行的父進程。

線程優(yōu)先級

在操作系統(tǒng)中妻献,線程可以劃分優(yōu)先級蛛株,優(yōu)先級較高的線程被CPU優(yōu)先執(zhí)行。設置線程優(yōu)先級就是幫助“線程規(guī)劃器”確定下次選哪一個線程來優(yōu)先執(zhí)行育拨。
java中線程優(yōu)先級從1~10谨履,默認是5。Thead類提供了3個常量預定義優(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線程就有與A線程同樣的優(yōu)先級。
需要注意的是析蝴,高優(yōu)先級的線程總是大部分都會先執(zhí)行完成害捕,但是并不代表高優(yōu)先級的線程執(zhí)行完成后,再去執(zhí)行低優(yōu)先級的線程闷畸。高優(yōu)先級尝盼,只說明CPU盡量將執(zhí)行資源給優(yōu)先級比較高的線程。

守護線程

在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也不會執(zhí)行了");
    }
}
打印結(jié)果:
0
1
2
3
4
main線程退出渐苏,daemonThread也不會執(zhí)行了

因為DaemonThread是守護線程掀潮,而main線程為非守護線程,當main線程退出后琼富,daemonThread也會退出仪吧。

關(guān)注我

歡迎關(guān)注我的公眾號,會定期推送優(yōu)質(zhì)技術(shù)文章鞠眉,讓我們一起進步薯鼠、一起成長!
公眾號搜索:data_tc
或直接掃碼:??


歡迎關(guān)注我
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末械蹋,一起剝皮案震驚了整個濱河市出皇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哗戈,老刑警劉巖郊艘,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異唯咬,居然都是意外死亡纱注,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門胆胰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狞贱,“玉大人,你說我怎么就攤上這事蜀涨∠规遥” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵厚柳,是天一觀的道長氧枣。 經(jīng)常有香客問我,道長草娜,這世上最難降的妖魔是什么挑胸? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮宰闰,結(jié)果婚禮上茬贵,老公的妹妹穿的比我還像新娘。我一直安慰自己移袍,他們只是感情好解藻,可當我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葡盗,像睡著了一般螟左。 火紅的嫁衣襯著肌膚如雪啡浊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天胶背,我揣著相機與錄音巷嚣,去河邊找鬼。 笑死钳吟,一個胖子當著我的面吹牛廷粒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播红且,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼坝茎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了暇番?” 一聲冷哼從身側(cè)響起嗤放,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎壁酬,沒想到半個月后次酌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡舆乔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年和措,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜕煌。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诬留,靈堂內(nèi)的尸體忽然破棺而出斜纪,到底是詐尸還是另有隱情,我是刑警寧澤文兑,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布盒刚,位于F島的核電站,受9級特大地震影響绿贞,放射性物質(zhì)發(fā)生泄漏因块。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一籍铁、第九天 我趴在偏房一處隱蔽的房頂上張望涡上。 院中可真熱鬧,春花似錦拒名、人聲如沸吩愧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雁佳。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間糖权,已是汗流浹背堵腹。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留星澳,地道東北人疚顷。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像募判,于是被迫代替她去往敵國和親荡含。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 寫在前面的話: 這篇博客是我從這里“轉(zhuǎn)載”的届垫,為什么轉(zhuǎn)載兩個字加“”呢释液?因為這絕不是簡單的復制粘貼,我花了五六個小...
    SmartSean閱讀 4,711評論 12 45
  • 前言 多線程并發(fā)編程是Java編程中重要的一塊內(nèi)容装处,也是面試重點覆蓋區(qū)域误债,所以學好多線程并發(fā)編程對我們來說極其重要...
    嘟爺MD閱讀 7,305評論 21 272
  • 進程:正在執(zhí)行中的程序,其實是應用程序在內(nèi)存中運行的那片空間妄迁。 線程:進程中的一個執(zhí)行單元寝蹈,負責進程中程序的執(zhí)行。...
    七弦桐語閱讀 459評論 2 7
  • 簡介 本次主要介紹java多線程中的同步登淘,也就是如何在java語言中寫出線程安全的程序箫老。如何在java語言中解決非...
    小人物灌籃閱讀 468評論 0 1
  • 吃完pizza之后,發(fā)過朋友圈之后黔州,一陣陣的大笑之后耍鬓,又剩下了什么呢,六個可愛的室友流妻,在一起笑牲蜀,在一起嗨,哈哈大笑...
    Janice1閱讀 203評論 0 0