Java多線程創(chuàng)建和常用方法

本文主要介紹線程的定義,創(chuàng)建,使用,停止,狀態(tài)圖和常用方法。
主要用于概念掃盲和梳理。
多進(jìn)程是指操作系統(tǒng)能同時(shí)運(yùn)行多個(gè)任務(wù)(程序)。
多線程是指在同一程序中有多個(gè)順序流在執(zhí)行。

先說并發(fā)

1. 為什么要用到并發(fā)

一直以來,硬件的發(fā)展極其迅速,也有一個(gè)很著名的"摩爾定律",可能會(huì)奇怪明明討論的是并發(fā)編程為什么會(huì)扯到了硬件的發(fā)展,這其中的關(guān)系應(yīng)該是多核CPU的發(fā)展為并發(fā)編程提供的硬件基礎(chǔ)。摩爾定律并不是一種自然法則或者是物理定律,它只是基于認(rèn)為觀測(cè)數(shù)據(jù)后,對(duì)未來的一種預(yù)測(cè)。
按照所預(yù)測(cè)的速度,我們的計(jì)算能力會(huì)按照指數(shù)級(jí)別的速度增長(zhǎng),不久以后會(huì)擁有超強(qiáng)的計(jì)算能力,正是在暢想未來的時(shí)候,2004年,Intel宣布4GHz芯片的計(jì)劃推遲到2005年,然后在2004年秋季,Intel宣布徹底取消4GHz的計(jì)劃,也就是說摩爾定律的有效性超過了半個(gè)世紀(jì)戛然而止

但是,聰明的硬件工程師并沒有停止研發(fā)的腳步,他們?yōu)榱诉M(jìn)一步提升計(jì)算速度,而不是再追求單獨(dú)的計(jì)算單元,而是將多個(gè)計(jì)算單元整合到了一起,也就是形成了多核CPU。短短十幾年的時(shí)間,家用型CPU,比如Intel i7就可以達(dá)到4核心甚至8核心。而專業(yè)服務(wù)器則通??梢赃_(dá)到幾個(gè)獨(dú)立的CPU,每一個(gè)CPU甚至擁有多達(dá)8個(gè)以上的內(nèi)核。因此,摩爾定律似乎在CPU核心擴(kuò)展上繼續(xù)得到體驗(yàn)。因此,多核的CPU的背景下,催生了并發(fā)編程的趨勢(shì),通過并發(fā)編程的形式可以將多核CPU的計(jì)算能力發(fā)揮到極致,性能得到提升。
頂級(jí)計(jì)算機(jī)科學(xué)家Donald Ervin Knuth如此評(píng)價(jià)這種情況:在我看來,這種現(xiàn)象(并發(fā))或多或少是由于硬件設(shè)計(jì)者無計(jì)可施了導(dǎo)致的,他們將摩爾定律的責(zé)任推給了軟件開發(fā)者。
另外,在特殊的業(yè)務(wù)場(chǎng)景下先天的就適合于并發(fā)編程。比如在圖像處理領(lǐng)域,一張1024X768像素的圖片,包含達(dá)到78萬6千多個(gè)像素。即時(shí)將所有的像素遍歷一邊都需要很長(zhǎng)的時(shí)間,面對(duì)如此復(fù)雜的計(jì)算量就需要充分利用多核的計(jì)算的能力。又比如當(dāng)我們?cè)?strong>網(wǎng)上購(gòu)物時(shí),為了提升響應(yīng)速度,需要拆分,減庫(kù)存,生成訂單等等這些操作,就可以進(jìn)行拆分利用多線程的技術(shù)完成。面對(duì)復(fù)雜業(yè)務(wù)模型,并行程序會(huì)比串行程序更適應(yīng)業(yè)務(wù)需求,而并發(fā)編程更能吻合這種業(yè)務(wù)拆分 。

2. 并發(fā)編程缺點(diǎn)

  • 頻繁的上下文切換
    時(shí)間片是CPU分配給各個(gè)線程的時(shí)間,因?yàn)闀r(shí)間非常短,所以CPU不斷通過切換線程,讓我們覺得多個(gè)線程是同時(shí)執(zhí)行的,時(shí)間片一般是幾十毫秒。而每次切換時(shí),需要保存當(dāng)前的狀態(tài)起來,以便能夠進(jìn)行恢復(fù)先前狀態(tài),而這個(gè)切換時(shí)非常損耗性能,過于頻繁反而無法發(fā)揮出多線程編程的優(yōu)勢(shì)。

通常減少上下文切換可以采用無鎖并發(fā)編程,CAS算法,使用最少的線程和使用協(xié)程。

  1. 無鎖并發(fā)編程:可以參照concurrentHashMap鎖分段的思想,不同的線程處理不同段的數(shù)據(jù),這樣在多線程競(jìng)爭(zhēng)的條件下,可以減少上下文切換的時(shí)間。
  2. CAS算法,利用Atomic下使用CAS算法來更新數(shù)據(jù),使用了樂觀鎖,可以有效的減少一部分不必要的鎖競(jìng)爭(zhēng)帶來的上下文切換
  3. 使用最少線程:避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多的線程,這樣會(huì)造成大量的線程都處于等待狀態(tài)
  4. 協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個(gè)任務(wù)間的切換
  • 線程安全

3. 一些概念

3.1 同步與異步

同步和異步通常用來形容一次方法調(diào)用。同步方法調(diào)用一開始,調(diào)用者必須等待被調(diào)用的方法結(jié)束后,調(diào)用者后面的代碼才能執(zhí)行。而異步調(diào)用,指的是,調(diào)用者不用管被調(diào)用方法是否完成,都會(huì)繼續(xù)執(zhí)行后面的代碼,當(dāng)被調(diào)用的方法完成后會(huì)通知調(diào)用者。
比如,在超時(shí)購(gòu)物,如果一件物品沒了,你得等倉(cāng)庫(kù)人員跟你調(diào)貨,直到倉(cāng)庫(kù)人員跟你把貨物送過來,你才能繼續(xù)去收銀臺(tái)付款,這就類似同步調(diào)用。而異步調(diào)用了,就像網(wǎng)購(gòu),你在網(wǎng)上付款下單后,什么事就不用管了,該干嘛就干嘛去了,當(dāng)貨物到達(dá)后你收到通知去取就好。

3.2 并發(fā)與并行

并發(fā)和并行是十分容易混淆的概念。并發(fā)指的是多個(gè)任務(wù)交替進(jìn)行,而并行則是指真正意義上的“同時(shí)進(jìn)行”。實(shí)際上,如果系統(tǒng)內(nèi)只有一個(gè)CPU,而使用多線程時(shí),那么真實(shí)系統(tǒng)環(huán)境下不能并行,只能通過切換時(shí)間片的方式交替進(jìn)行,而成為并發(fā)執(zhí)行任務(wù)。
真正的并行也只能出現(xiàn)在擁有多個(gè)CPU的系統(tǒng)中。

3.3 阻塞和非阻塞

阻塞和非阻塞通常用來形容多線程間的相互影響,比如一個(gè)線程占有了臨界區(qū)資源,那么其他線程需要這個(gè)資源就必須進(jìn)行等待該資源的釋放,會(huì)導(dǎo)致等待的線程掛起,這種情況就是阻塞,而非阻塞就恰好相反,它強(qiáng)調(diào)沒有一個(gè)線程可以阻塞其他線程,所有的線程都會(huì)嘗試地往前運(yùn)行。

3.4 臨界區(qū)

臨界區(qū)用來表示一種公共資源或者說是共享數(shù)據(jù),可以被多個(gè)線程使用。但是每個(gè)線程使用時(shí),一旦臨界區(qū)資源被一個(gè)線程占有,那么其他線程必須等待。

進(jìn)程與線程

進(jìn)程--資源分配的最小單位

  • 每個(gè)進(jìn)程都有獨(dú)立的代碼和數(shù)據(jù)空間(進(jìn)程上下文)
  • 一個(gè)進(jìn)程包含多個(gè)線程
  • 進(jìn)程間的切換會(huì)有較大的開銷

線程--cpu調(diào)度的最小單位

  • 同一類線程共享代碼和數(shù)據(jù)空間,
  • 每個(gè)線程有獨(dú)立的運(yùn)行棧和程序計(jì)數(shù)器(PC)
  • 線程切換開銷小

其他

  • 多進(jìn)程是指操作系統(tǒng)能同時(shí)運(yùn)行多個(gè)任務(wù)(程序)。
  • 多線程是指在同一程序中有多個(gè)順序流在執(zhí)行。
  • 線程和進(jìn)程一樣分為五個(gè)階段:創(chuàng)建、就緒、運(yùn)行、阻塞、終止。

線程創(chuàng)建、使用、停止

創(chuàng)建線程

  • 在Java中要想實(shí)現(xiàn)多線程,有三種手段,一種是繼承Thread類,另外一種是實(shí)現(xiàn)Runnable接口,三是實(shí)現(xiàn)Callable 接口。
    實(shí)現(xiàn) Runnable 和 Callable 接口的類只能當(dāng)做一個(gè)可以在線程中運(yùn)行的任務(wù),不是真正意義上的線程,因此最后還需要通過 Thread 來調(diào)用??梢哉f任務(wù)是通過線程驅(qū)動(dòng)從而執(zhí)行的。

1. 繼承Thread 類,覆蓋run方法(推薦使用Runable)

  • run()方法是多線程程序的一個(gè)約定,所有的多線程代碼都在run方法里面。
        class PrimeThread extends Thread {
         long minPrime;
         PrimeThread(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }
  • 我們復(fù)寫的是run()方法,但是執(zhí)行的卻是start()方法。
  • start()方法的調(diào)用后并不是立即執(zhí)行多線程代碼,而是使得該線程變?yōu)榭蛇\(yùn)行態(tài)(Runnable),什么時(shí)候運(yùn)行是由操作系統(tǒng)決定的。
     PrimeThread p = new PrimeThread(143);
     p.start();

2. 實(shí)現(xiàn)Runnable接口,覆蓋run方法

     class PrimeRun implements Runnable {
         long minPrime;
         PrimeRun(long minPrime) {
             this.minPrime = minPrime;
         }

         public void run() {
             // compute primes larger than minPrime
              . . .
         }
     }

在啟動(dòng)的多線程的時(shí)候,需要先通過Thread類的構(gòu)造方法Thread(Runnable target)構(gòu)造出對(duì)象,然后調(diào)用Thread對(duì)象的start()方法來運(yùn)行多線程代碼。

     PrimeRun p = new PrimeRun(143);
     new Thread(p).start();

    new Thread(new PrimeRun(143)).start();

實(shí)際上所有的多線程代碼都是通過運(yùn)行Thread的start()方法來運(yùn)行的。因此,不管是擴(kuò)展Thread類還是實(shí)現(xiàn)Runnable接口來實(shí)現(xiàn)多線程,最終還是通過Thread的對(duì)象的API來控制線程的,熟悉Thread類的API是進(jìn)行多線程編程的基礎(chǔ)。

3. 實(shí)現(xiàn) Callable 接口

與 Runnable 相比,Callable 可以有返回值,返回值通過FutureTask 進(jìn)行封裝。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        // ...
    }
    public static void main(String[]  args) {
        MyCallable mc = new MyCallable();
        FutureTask<Integer> ft = new FutureTask<>(mc);
        Thread thread = new Thread(ft);
        thread.start();
        System.out.println(ft.get());
    }
}

Thread和Runnable的區(qū)別

實(shí)現(xiàn)Runnable接口比繼承Thread類所具有的優(yōu)勢(shì):

  1. 適合多個(gè)相同的程序代碼的線程去處理同一個(gè)資源
  2. 可以避免java中的單繼承的限制
  3. 增加程序的健壯性,代碼可以被多個(gè)線程共享,代碼和數(shù)據(jù)獨(dú)立
  4. 線程池只能放入實(shí)現(xiàn)Runable或callable類線程,不能直接放入繼承Thread的類

使用線程

方法

類別 方法簽名 簡(jiǎn)介
構(gòu)造方法 thread() -
- thread(String name) -
- thread(Runnable target) -
- thread(Runnable target,String name) -
常用方法 void start() 啟動(dòng)線程
- static void sleep(long millis) 休眠
- static void sleep(long millis,int nanos) 休眠
- void join() 使其他線程等待當(dāng)前線程終止
- void join(long millis) 使其他線程等待當(dāng)前線程終止
- void join(long millis,int nanos) 使其他線程等待當(dāng)前線程終止
- static void yield( ) 當(dāng)前運(yùn)行線程釋放處理器資源
獲取線程引用 static Thread currendThread() 返回當(dāng)前運(yùn)行的線程引用

注意事項(xiàng)

  • main方法其實(shí)也是一個(gè)線程。在java中所以的線程都是同時(shí)啟動(dòng)的,至于什么時(shí)候,哪個(gè)先執(zhí)行,完全看誰先得到CPU的資源。
  • 在java中,每次程序運(yùn)行至少啟動(dòng)2個(gè)線程。一個(gè)是main線程,一個(gè)是垃圾收集線程。

停止線程

錯(cuò)誤方法

  • stop()方法。
    它會(huì)使線程戛然而止,我們不知道它完成了什么工作,沒有完成什么工作,因此是錯(cuò)誤的。
  • interrupt()方法。
    不要使用interrupt()方法,因?yàn)?strong>在線程run()中調(diào)用sleep(),join(),yeild()方法時(shí),中斷狀態(tài)會(huì)被清除,isinterrupe=false

如何正確的停止線程

使用退出標(biāo)志。

volatile boolean keepRunning = true;

代碼示例

public class ActorThread extends Thread {//繼承Thread創(chuàng)建線程
    // 覆蓋run------------------------------------------------------
    public void run() {
        System.out.println(getName() + "是一個(gè)演員!");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning) {
            System.out.println(getName() + "登臺(tái)演出:" + (++count));
            //標(biāo)注法停止線程-----------------------------------------------
            if (count == 15) {
                keepRunning = false;
            }
            //測(cè)試sleep()-----------------------
            if (count % 5 == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(getName() + "的演出結(jié)束了!");
    }

    public static void main(String[] args) {
        Thread actor = new ActorThread();
        actor.setName("Mr. Thread");
        actor.start();
        Thread actressThread = new Thread(new Actress(), "Ms. Runnable");
        actressThread.start();
    }

}

class Actress implements Runnable {//實(shí)現(xiàn)Runnable接口創(chuàng)建線程
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "是一個(gè)演員!");
        int count = 0;
        boolean keepRunning = true;
        while (keepRunning) {
            System.out.println(Thread.currentThread().getName() + "登臺(tái)演出:" + (++count));
            if (count == 15) {
                keepRunning = false;
            }
            if (count % 5 == 0) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "的演出結(jié)束了!");
    }
}

輸出

測(cè)試sleep方法與停止線程

線程狀態(tài)轉(zhuǎn)換

線程狀態(tài)轉(zhuǎn)換
  1. 新建狀態(tài)(New):新創(chuàng)建了一個(gè)線程對(duì)象。
  2. 就緒狀態(tài)(Runnable):線程對(duì)象創(chuàng)建后,其他線程調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線程位于可運(yùn)行線程池中,變得可運(yùn)行,等待獲取CPU的使用權(quán)。
  3. 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU,執(zhí)行程序代碼。
  4. 阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄CPU使用權(quán),暫時(shí)停止運(yùn)行。直到線程進(jìn)入就緒狀態(tài),才有機(jī)會(huì)轉(zhuǎn)到運(yùn)行狀態(tài)。阻塞的情況分三種:
    • 等待阻塞:運(yùn)行的線程執(zhí)行wait()方法,JVM會(huì)把該線程放入等待池中。(wait會(huì)釋放持有的鎖)
    • 同步阻塞:運(yùn)行的線程在獲取對(duì)象的同步鎖時(shí),若該同步鎖被別的線程占用,則JVM會(huì)把該線程放入鎖池中。
    • 其他阻塞:運(yùn)行的線程執(zhí)行sleep()或join()方法,或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài)。當(dāng)sleep()狀態(tài)超時(shí)、join()等待線程終止或者超時(shí)、或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài)。(sleep是不會(huì)釋放持有的鎖)
  5. 死亡狀態(tài)(Dead):線程執(zhí)行完了或者因異常退出了run()方法,該線程結(jié)束生命周期。

常用函數(shù)說明

sleep(long millis):線程暫停執(zhí)行

在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠。
sleep() 可能會(huì)拋出 InterruptedException。因?yàn)楫惓2荒芸缇€程傳播回 main() 中,因此必須在本地進(jìn)行處理。線程中拋出的其它異常也同樣需要在本地進(jìn)行處理。

join(long millis):指等待t線程一段時(shí)間(或終止)

  • join是Thread類的一個(gè)方法,啟動(dòng)線程后直接調(diào)用,即join()的作用是:“等待該線程(終止)”。
  • 目的
    在很多情況下,主線程生成并起動(dòng)了子線程,如果子線程里要進(jìn)行大量的耗時(shí)的運(yùn)算,主線程往往將于子線程之前結(jié)束,但是如果主線程處理完其他的事務(wù)后,需要用到子線程的處理結(jié)果,也就是主線程需要等待子線程執(zhí)行完成之后再結(jié)束,這個(gè)時(shí)候就要用到j(luò)oin()方法了。

yield():暫停當(dāng)前正在執(zhí)行的線程對(duì)象,并執(zhí)行其他線程。

  • yield()應(yīng)該做的是讓當(dāng)前運(yùn)行線程回到可運(yùn)行狀態(tài),以允許具有相同優(yōu)先級(jí)的其他線程獲得運(yùn)行機(jī)會(huì)。
  • 目的
    使用yield()的目的是讓相同優(yōu)先級(jí)的線程之間能適當(dāng)?shù)妮嗈D(zhuǎn)執(zhí)行。但是,實(shí)際中無法保證yield()達(dá)到讓步目的,因?yàn)樽尣降木€程還有可能被線程調(diào)度程序再次選中。

wait()

本小節(jié)轉(zhuǎn)載自使用wait/notify/notifyAll實(shí)現(xiàn)線程間通信的幾點(diǎn)重要說明

用于線程間通信

在Java中,可以通過配合調(diào)用Object對(duì)象的wait()方法和notify()方法或notifyAll()方法來實(shí)現(xiàn)線程間的通信。

語(yǔ)義

在線程中調(diào)用wait()方法,將阻塞等待其他線程的通知(其他線程調(diào)用notify()方法或notifyAll()方法),在線程中調(diào)用notify()方法或notifyAll()方法,將通知其他線程從wait()方法處返回。

Object是所有類的超類,它有5個(gè)方法組成了等待/通知機(jī)制的核心:notify()、notifyAll()、wait()、wait(long)和wait(long,int)。在Java中,所有的類都從Object繼承而來,因此,所有的類都擁有這些共有方法可供使用。而且,由于他們都被聲明為final,因此在子類中不能覆寫任何一個(gè)方法。

使用中需要注意

  1. wait() --- "我去等待了"
 public final void wait()  throws InterruptedException,IllegalMonitorStateException 
  • 該方法用來將當(dāng)前線程置入休眠狀態(tài),直到接到通知或被中斷為止。在調(diào)用wait()之前,線程必須要獲得該對(duì)象的對(duì)象級(jí)別鎖,即只能在同步方法或同步塊中調(diào)用wait()方法。
  • 進(jìn)入wait()方法后,當(dāng)前線程釋放鎖。在從wait()返回前,線程與其他線程競(jìng)爭(zhēng)重新獲得鎖。如果調(diào)用wait()時(shí),沒有持有適當(dāng)?shù)逆i,則拋出IllegalMonitorStateException,它是RuntimeException的一個(gè)子類,因此,不需要try-catch結(jié)構(gòu)。
  1. notify()---"大家來爭(zhēng)我"
public final native void notify() throws IllegalMonitorStateException
  • 該方法也要在同步方法或同步塊中調(diào)用,即在調(diào)用前,線程也必須要獲得該對(duì)象的對(duì)象級(jí)別鎖,的如果調(diào)用notify()時(shí)沒有持有適當(dāng)?shù)逆i,也會(huì)拋出IllegalMonitorStateException
  • 該方法用來通知那些可能等待該對(duì)象的對(duì)象鎖的其他線程。如果有多個(gè)線程等待,則線程規(guī)劃器任意挑選出其中一個(gè)wait()狀態(tài)的線程來發(fā)出通知,并使它等待獲取該對(duì)象的對(duì)象鎖(notify后,當(dāng)前線程不會(huì)馬上釋放該對(duì)象鎖,wait所在的線程并不能馬上獲取該對(duì)象鎖,要等到程序退出synchronized代碼塊后,當(dāng)前線程才會(huì)釋放鎖,wait所在的線程也才可以獲取該對(duì)象鎖),但不驚動(dòng)其他同樣在等待被該對(duì)象notify的線程們。當(dāng)?shù)谝粋€(gè)獲得了該對(duì)象鎖的wait線程運(yùn)行完畢以后,它會(huì)釋放掉該對(duì)象鎖,此時(shí)如果該對(duì)象沒有再次使用notify語(yǔ)句,則即便該對(duì)象已經(jīng)空閑,其他wait狀態(tài)等待的線程由于沒有得到該對(duì)象的通知,會(huì)繼續(xù)阻塞在wait狀態(tài),直到這個(gè)對(duì)象發(fā)出一個(gè)notify或notifyAll。這里需要注意:它們等待的是被notify或notifyAll,而不是鎖。這與下面的notifyAll()方法執(zhí)行后的情況不同。
  1. notifyAll()
     public final native void notifyAll() throws IllegalMonitorStateException

該方法與notify()方法的工作方式相同,重要的一點(diǎn)差異是:
notifyAll使所有原來在該對(duì)象上wait的線程統(tǒng)統(tǒng)退出wait的狀態(tài)(即全部被喚醒,不再等待notify或notifyAll,但由于此時(shí)還沒有獲取到該對(duì)象鎖,因此還不能繼續(xù)往下執(zhí)行),變成等待獲取該對(duì)象上的鎖,一旦該對(duì)象鎖被釋放(notifyAll線程退出調(diào)用了notifyAll的synchronized代碼塊的時(shí)候),他們就會(huì)去競(jìng)爭(zhēng)。如果其中一個(gè)線程獲得了該對(duì)象鎖,它就會(huì)繼續(xù)往下執(zhí)行,在它退出synchronized代碼塊,釋放鎖后,其他的已經(jīng)被喚醒的線程將會(huì)繼續(xù)競(jìng)爭(zhēng)獲取該鎖,一直進(jìn)行下去,直到所有被喚醒的線程都執(zhí)行完畢。

  1. wait(long)和wait(long,int)
    顯然,這兩個(gè)方法是設(shè)置等待超時(shí)時(shí)間的,后者在超值時(shí)間上加上ns,精度也難以達(dá)到,因此,該方法很少使用。對(duì)于前者,如果在等待線程接到通知或被中斷之前,已經(jīng)超過了指定的毫秒數(shù),則它通過競(jìng)爭(zhēng)重新獲得鎖,并從wait(long)返回。另外,需要知道,如果設(shè)置了超時(shí)時(shí)間,當(dāng)wait()返回時(shí),我們不能確定它是因?yàn)榻拥搅送ㄖ€是因?yàn)槌瑫r(shí)而返回的,因?yàn)閣ait()方法不會(huì)返回任何相關(guān)的信息。但一般可以通過設(shè)置標(biāo)志位來判斷,在notify之前改變標(biāo)志位的值,在wait()方法后讀取該標(biāo)志位的值來判斷,當(dāng)然為了保證notify不被遺漏,我們還需要另外一個(gè)標(biāo)志位來循環(huán)判斷是否調(diào)用wait()方法。
  2. 深入理解:
    如果線程調(diào)用了對(duì)象的wait()方法,那么線程便會(huì)處于該對(duì)象的
    等待池中,等待池中的線程不會(huì)去競(jìng)爭(zhēng)該對(duì)象的鎖。
    當(dāng)有線程調(diào)用了對(duì)象的notifyAll()方法(喚醒所有wait線程)或notify()方法(只隨機(jī)喚醒一個(gè)wait線程),被喚醒的的線程便會(huì)進(jìn)入該對(duì)象的鎖池中,鎖池中的線程會(huì)去競(jìng)爭(zhēng)該對(duì)象鎖。
    優(yōu)先級(jí)高的線程競(jìng)爭(zhēng)到對(duì)象鎖的概率大,假若某線程沒有競(jìng)爭(zhēng)到該對(duì)象鎖,它還會(huì)留在鎖池中,唯有線程再次調(diào)用wait()方法,它才會(huì)重新回到等待池中。而競(jìng)爭(zhēng)到對(duì)象鎖的線程則繼續(xù)往下執(zhí)行,直到執(zhí)行完了synchronized代碼塊,它會(huì)釋放掉該對(duì)象鎖,這時(shí)鎖池中的線程會(huì)繼續(xù)競(jìng)爭(zhēng)該對(duì)象鎖。

wait和sleep區(qū)別

共同點(diǎn):

  1. 他們都是在多線程的環(huán)境下,都可以在程序的調(diào)用處阻塞指定的毫秒數(shù),并返回
  2. wait()和sleep()都可以通過interrupt()方法 打斷線程的暫停狀態(tài) ,從而使線程立刻拋出InterruptedException。 (不建議使用該方法)
  • 如果線程A希望立即結(jié)束線程B,則可以對(duì)線程B對(duì)應(yīng)的Thread實(shí)例調(diào)用interrupt方法。如果此刻線程B正在wait/sleep /join,則線程B會(huì)立刻拋出InterruptedException,在catch() {} 中直接return即可安全地結(jié)束線程。
  • 需要注意的是,InterruptedException是線程自己從內(nèi)部拋出的,并不是interrupt()方法拋出的。對(duì)某一線程調(diào)用 interrupt()時(shí),如果該線程正在執(zhí)行普通的代碼,那么該線程根本就不會(huì)拋出InterruptedException。但是,一旦該線程進(jìn)入到 wait()/sleep()/join()后,就會(huì)立刻拋出InterruptedException 。

不同點(diǎn):

  1. 所屬的類
    Thread類的方法:sleep(),yield()等
    Object的方法:wait()和notify()等
  2. 是否釋放鎖
    每個(gè)對(duì)象都有一個(gè)鎖來控制同步訪問。Synchronized關(guān)鍵字可以和對(duì)象的鎖交互,來實(shí)現(xiàn)線程的同步。
    sleep方法沒有釋放鎖,而wait方法釋放了鎖,使得其他線程可以使用同步控制塊或者方法。
  3. 使用限制
    wait,notify和notifyAll只能在同步控制方法或者同步控制塊里面使用,而sleep可以在任何地方使用 。

sleep()和yield()的區(qū)別

  • sleep 方法使當(dāng)前運(yùn)行中的線程睡眼一段時(shí)間,進(jìn)入不可運(yùn)行狀態(tài),這段時(shí)間的長(zhǎng)短是由程序設(shè)定的,yield 方法使當(dāng)前線程讓出 CPU 占有權(quán),但讓出的時(shí)間是不可設(shè)定的。實(shí)際上,yield()方法對(duì)應(yīng)了如下操作:先檢測(cè)當(dāng)前是否有相同優(yōu)先級(jí)的線程處于同可運(yùn)行狀態(tài),如有,則把 CPU 的占有權(quán)交給此線程,否則,繼續(xù)運(yùn)行原來的線程。所以yield()方法稱為“退讓”,它把運(yùn)行機(jī)會(huì)讓給了同等優(yōu)先級(jí)的其他線程
  • 另外,sleep 方法允許較低優(yōu)先級(jí)的線程獲得運(yùn)行機(jī)會(huì),但 yield() 方法執(zhí)行時(shí),當(dāng)前線程仍處在可運(yùn)行狀態(tài),所以,不可能讓出較低優(yōu)先級(jí)的線程些時(shí)獲得 CPU 占有權(quán)。在一個(gè)運(yùn)行系統(tǒng)中,如果較高優(yōu)先級(jí)的線程沒有調(diào)用 sleep 方法,又沒有受到 I\O 阻塞,那么,較低優(yōu)先級(jí)線程只能等待所有較高優(yōu)先級(jí)的線程運(yùn)行結(jié)束,才有機(jī)會(huì)運(yùn)行。

三線程打印ABC

要求

java實(shí)現(xiàn)三個(gè)線程A B C:A線程打印10次A,B線程打印10次B,C線程打印10次C,要求線程同時(shí)運(yùn)行,交替打印10次ABC。

方法一.使用synchronized() + wait(),nitify()

  • 題意
    問題為三線程間的同步喚醒操作,主要的目的就是ThreadA->ThreadB->ThreadC→ThreadA……循環(huán)執(zhí)行三個(gè)線程。為了控制線程執(zhí)行的順序,那么就必須要確定喚醒、等待的順序,所以每一個(gè)線程必須同時(shí)持有兩個(gè)對(duì)象鎖,才能繼續(xù)執(zhí)行。一個(gè)對(duì)象鎖是prev,就是前一個(gè)線程所持有的對(duì)象鎖。還有一個(gè)就是自身對(duì)象鎖。
  • 思路
    主要的思想就是,為了控制執(zhí)行的順序,必須要先持有prev鎖,也就是前一個(gè)線程要釋放自身對(duì)象鎖,再去申請(qǐng)自身對(duì)象鎖,兩者兼?zhèn)鋾r(shí)打印字母,之后首先調(diào)用self.notifyAll()釋放自身對(duì)象鎖,喚醒下一個(gè)等待線程,再調(diào)用prev.wait()釋放prev對(duì)象鎖,終止當(dāng)前線程,等待循環(huán)結(jié)束后再次被喚醒。

程序運(yùn)行的主要過程就是
A線程最先運(yùn)行,持有C,A對(duì)象鎖,后釋放A,C鎖,喚醒B;
線程B等待A鎖,再申請(qǐng)B鎖,后打印B,再釋放B,A鎖,喚醒C;
線程C等待B鎖,再申請(qǐng)C鎖,后打印C,再釋放C,B鎖,喚醒A……

  • 注意
    為了避免JVM啟動(dòng)ThreadA、ThreadB、ThreadC三個(gè)線程順序的不確定性。需要讓A,B,C三個(gè)線程以確定的順序啟動(dòng),中間加一段sleep()確保前一個(gè)線程已啟動(dòng)。

代碼實(shí)現(xiàn)

public class ABC {
    public static class ThreadPrinter implements Runnable {
        private String name;
        private Object prev;//前一個(gè)對(duì)象的鎖
        private Object self;//自己的鎖

        private ThreadPrinter(String name, Object prev, Object self) {
            this.name = name;
            this.prev = prev;
            this.self = self;
        }

        @Override
        public void run() {
            int count = 10;
            while (count > 0) {// 多線程并發(fā),不能用if,必須用循環(huán)測(cè)試等待條件,避免虛假喚醒
                synchronized (prev) { // 先獲取 prev 鎖
                    synchronized (self) {// 再獲取 self 鎖
                        System.out.print(name);
                        count--;
                        self.notifyAll();// 先釋放 self,喚醒其他線程競(jìng)爭(zhēng)self鎖
                    }
                    try {
                        prev.wait(); // 再釋放 prev,休眠等待喚醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        ThreadPrinter pa = new ThreadPrinter("A", c, a);
        ThreadPrinter pb = new ThreadPrinter("B", a, b);
        ThreadPrinter pc = new ThreadPrinter("C", b, c);

        new Thread(pa).start();
        Thread.sleep(10);
        new Thread(pb).start();
        Thread.sleep(10);
        new Thread(pc).start();
        Thread.sleep(10);
    }
}

輸出

打印結(jié)果

方法二. 使用Lock

代碼實(shí)現(xiàn)

import java.util.concurrent.locks.*;

public class ABC_Lock {
    private static Lock lock = new ReentrantLock();//通過Lock鎖來保證線程的訪問的互斥
    private static int state = 0;//控制ABC的打印語(yǔ)句是否執(zhí)行,獲取Lock之后的判斷
 
    static class ThreadA extends Thread {
        @Override
        public void run() {
            int count = 10;
            while(count > 0) {
                try {
                    lock.lock();
                    while (state % 3 == 0) {//多線程并發(fā),不能用if,必須用循環(huán)測(cè)試等待條件,避免虛假喚醒
                        System.out.print("A");
                        state++;
                        count--;
                    }
                } finally {
                    lock.unlock();// lock()和unlock()操作結(jié)合try/catch使用
                }
            }
        }
    }
 
    static class ThreadB extends Thread {
        @Override
        public void run() {
            int count = 10;
            while(count > 0) {
                try {
                    lock.lock();
                    while (state % 3 == 1) {//多線程并發(fā),不能用if,必須用循環(huán)測(cè)試等待條件,避免虛假喚醒
                        System.out.print("B");
                        state++;
                        count--;
                    }
                } finally {
                    lock.unlock();// lock()和unlock()操作結(jié)合try/catch使用
                }
            }
        }
    }
 
    static class ThreadC extends Thread {
        @Override
        public void run() {
            int count = 10;
            while(count > 0){
                try {
                    lock.lock();
                    while (state % 3 == 2) {//多線程并發(fā),不能用if,必須用循環(huán)測(cè)試等待條件,避免虛假喚醒
                        System.out.print("C");
                        state++;
                        count--;
                    }
                } finally {
                    lock.unlock();// lock()和unlock()操作結(jié)合try/catch使用
                }
            }
        }
    }
 
    public static void main(String[] args) {
        new ThreadA().start();
        new ThreadB().start();
        new ThreadC().start();
    }
}

輸出

打印結(jié)果

參考文章
并發(fā)編程的優(yōu)缺點(diǎn)
java多線程入門學(xué)習(xí)(一)
Java多線程學(xué)習(xí)(吐血超詳細(xì)總結(jié))
oracle官方文檔

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

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

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