Java學習之多線程

多線程相關總結:
萬字圖解Java多線程
Java(Android)線程池
面試官:說說多線程并發問題

一、概述:

1、線程是什么呢?
我們先來說一說比較熟悉的進程吧,之后就比較容易理解線程了。所謂進程,就是一個正在執行(進行)中的程序。每一個進程的執行都有一個執行順序,或者說是一個控制單元。簡單來說,就是你做一件事所要進行的一套流程。線程,就是進程中的一個獨立的控制單元;也就是說,線程是愛控制著進程的執行。一個進程至少有一個線程,并且線程的出現使得程序要有效率。打個比方說,在倉庫搬運貨物,一個人搬運和五個人搬運效率是不一樣的,搬運貨物的整個程序,就是進程;每一個人搬運貨物的過程,就是線程。

2、java中的線程:
在java中,JVM虛擬機啟動時,會有一個進程為java.exe,該程序中至少有一個線程負責java程序的執行;而且該程序運行的代碼存在于main方法中,該線程稱之為主線程。其實,JVM啟動時不止有一個線程(主線程),由于java是具有垃圾回收機制的,所以,在進程中,還有負責垃圾回收機制的線程。

3、多線程的意義:
透過上面的例子,可以看出,多線程有兩方面的意義:
1)提高效率。【運行更快,CPU資源利用更充分】
2)清除垃圾,解決內存不足的問題。

二、自定義線程:

線程有如此的好處,那要如何才能通過代碼自定義一個線程呢?其實,線程是通過系統創建和分配的,java是不能獨立創建線程的;但是,java是可以通過調用系統,來實現對進程的創建和分配的。java作為一種面向對象的編程語言,是可以將任何事物描述為對象,從而進行操作的,進程也不例外。我們通過查閱API文檔,知道java提供了對線程這類事物的描述,即Thread類。創建新執行線程有兩種方法:

1、創建線程方式

方式一:繼承Thread類

  • 局限性:
    單繼承的局限性
    任務中的成員變量不共享,加入static才能共享

1、步驟:
第一、定義類繼承Thread。
第二、復寫Thread類中的run方法。
第三、調用線程的start方法。分配并啟動該子類的實例。
start方法的作用:啟動線程,并調用run方法。

class Demo extends Thread  
{  
    public void run()  
    {  
        for (int i=0;i<60;i++)  
            System.out.println(Thread.currentThread().getName() + "demo run---" + i);  
    }  
}  
class Test2  
{  
    public static void main(String[] args)   
    {  
        Demo d1 = new Demo();//創建一個對象就創建好了一個線程  
        Demo d2 = new Demo();  
        d1.start();//開啟線程并執行run方法  
        d2.start();  
        for (int i=0;i<60;i++)  
            System.out.println("Hello World!---" + i);  
    }  
}

2、運行特點:
A.并發性:
我們看到的程序(或線程)并發執行,其實是一種假象。有一點需要明確:;在某一時刻,只有一個程序在運行(多核除外),此時cpu是在進行快速的切換,以達到看上去是同時運行的效果。由于切換時間是非常短的,所以我們可以認為是在并發進行。

B.隨機性:
在運行時,每次的結果不同。由于多個線程都在獲取cpu的執行權,cpu執行到哪個線程,哪個線程就會執行。可以將多線程運行的行為形象的稱為互相搶奪cpu的執行權。這就是多線程的特點,隨機性。執行到哪個程序并不確定。

3、覆蓋run方法的原因:
1)Thread類用于描述線程。該類定義了一個功能:用于存儲線程要運行的代碼,該存儲功能即為run方法。也就是說,Thread類中的run方法用于存儲線程要運行的代碼,就如同main方法存放的代碼一樣。

2)復寫run的目的:將自定義代碼存儲在run方法中,讓線程運行要執行的代碼。直接調用run,就是對象在調用方法。調用start(),開啟線程并執行該線程的run方法。如果直接調用run方法,只是將線程創建了,但未運行。

方式二:實現Runnable接口

  • 局限性:
    沒有返回值
    任務無法拋異常給調用者

1、步驟:
第一、定義類實現Runnable接口。
第二、覆蓋Runnable接口中的run方法。
第三、通過Thread類建立線程對象。要運行幾個線程,就創建幾個對象。
第四、將Runnable接口的子類對象作為參數傳遞給Thread類的構造函數。
第五、調用Thread類的start方法開啟線程,并調用Runnable接口子類的run方法。

//多個窗口同時賣票  
class Ticket implements Runnable  
{  
    private int tic = 20;  
    public void run()  
    {  
        while(true)  
        {  
            if (tic > 0)  
                System.out.println(Thread.currentThread().getName() + "sale:" + tic--);  
        }  
    }  
}  
  
class  TicketDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t);//創建一個線程  
        Thread t2 = new Thread(t);//創建一個線程  
        Thread t3 = new Thread(t);//創建一個線程  
        Thread t4 = new Thread(t);//創建一個線程  
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  
}

2、說明:
A.步驟2覆蓋run方法:將線程要運行的代碼存放在該run方法中。
B.步驟4:為何將Runnable接口的子類對象傳給Thread構造函數。因為自定義的run方法所屬對象為Runnable接口的子類對象,所以讓線程指定對象的run方法,就必須明確該run方法所屬的對象。

方式三:實現Callable接口

利用FutureTask執行任務

// 實現接口
class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        log.info("我是實現Callable的任務");
        return "success";
    }
}

// 執行
FutureTask<String> target = new FutureTask<>(new MyCallable());
new Thread(target).start();
log.info(target.get());

需要注意的是:局部變量在每一個線程中都獨有一份。

2、Thread類中的一些方法簡介:

在這簡單介紹幾個Thread中的方法:

1、線程名稱

  • 獲取線程名稱:getName()
    每個線程都有自己默認的名稱,
    也就是說,線程一為:Thread-0,線程二為:Thread-1。
    也可以獲取當前線程對象的名稱,通過currentThread().getName()
// 調用
對象.getName();
// 結果
Thread-編號(從0開始)
  • 設置線程名稱:setName()或構造函數
    可以通過setName()設置線程名稱,或者通過含有參數的構造函數直接顯式初始化線程的名稱。如:Test(String name)

2、線程的禮讓:

  • 優先級:setPriority()
    在Thread中,存在著1~10這十個執行級別;但是并不是優先級越高,就會一直執行這個線程,只是說會優先執行到這個線程,此后還是有其他線程會和此線程搶奪cpu執行權的。
    cpu比較忙時,優先級高的線程獲取更多的時間片
    cpu比較閑時,優先級設置基本沒用
    優先級是可以設定的,可通過setPriority()設定
//最低
 public final static int MIN_PRIORITY = 1;
//默認
 public final static int NORM_PRIORITY = 5;
// 最高
 public final static int MAX_PRIORITY = 10;

 // 方法的定義
 public final void setPriority(int newPriority) {
 }
  • yield()
    此方法可暫停當前線程,而執行其他線程。通過這個方法,可稍微減少線程執行頻率,達到線程都有機會平均被執行的效果。
    即讓運行中的線程切換到就緒狀態,重新爭搶cpu的時間片,爭搶時是否獲取到時間片看cpu的分配。
    如下示例:t2線程每次執行時進行了yield(),線程1執行的機會明顯比線程2要多。
// 方法的定義
public static native void yield();

Runnable r1 = () -> {
    int count = 0;
    for (;;){
       log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (;;){
        Thread.yield();
        log.info("            ---- 2>" + count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();

// 運行結果
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518

3、守護線程:setDaemon()

  • 默認情況下,java進程需要等待所有線程都運行結束,才會結束;
    有一種特殊線程叫守護線程,當所有的非守護線程都結束后,即使它沒有執行完,也會強制結束。
    默認的線程都是非守護線程。
  • 垃圾回收線程就是典型的守護線程
  • 可將一個線程標記為守護線程,直接調用setDaemon()方法。
    此方法需要在啟動前調用守護線程在這個線程結束后,會自動結束,則Jvm虛擬機也結束運行。
    ........  
      
    //守護線程(后臺線程),在啟動前調用。后臺線程自動結束  
    t1.setDaemon(true);  
    t2.setDaemon(true);  
    t1.start();  
    t2.start();  
  
     .........

4、線程的阻塞:

  • 阻塞方式:
    BIO阻塞,即使用了阻塞式的IO流
    sleep(long time) 讓線程休眠進入阻塞狀態
    a.join() 調用該方法的線程進入阻塞,等待a線程執行完恢復運行
    sychronizedReentrantLock 造成線程未獲得鎖進入阻塞狀態 (同步鎖章節細說)
    獲得鎖之后調用 wait() 方法 也會讓線程進入阻塞狀態 (同步鎖章節細說)
    LockSupport.park() 讓線程進入阻塞狀態 (同步鎖章節細說)
  • sleep()
    使線程休眠,會將運行中的線程進入阻塞狀態。當休眠時間結束后,重新爭搶cpu的時間片繼續運行
// 方法的定義 native方法
public static native void sleep(long millis) throws InterruptedException; 

try {
   // 休眠2秒
   // 該方法會拋出 InterruptedException異常 即休眠過程中可被中斷,被中斷后拋出異常
   Thread.sleep(2000);
 } catch (InterruptedException異常 e) {
 }
 try {
   // 使用TimeUnit的api可替代 Thread.sleep 
   TimeUnit.SECONDS.sleep(1);
 } catch (InterruptedException e) {
 }

  • join()【臨時加入線程】
    特點:當A線程執行到B線程方法時,A線程就會等待,B線程都執行完,A才會執行。join可用來臨時加入線程執行。
class Demo implements Runnable{  
    public void run(){  
        for(int x=0;x<90;x++){  
            System.out.println(Thread.currentThread().getName() + "----run" + x);  
        }  
    }  
}  
  
class  JoinDemo{  
    public static void main(String[] args)throws Exception{  
        Demo d = new Demo();  
        Thread t1 = new Thread(d);  
        Thread t2 = new Thread(d);  
        t1.start();       
        t2.start();  
        t1.join();//等t1執行完了,主線程才從凍結狀態恢復,和t2搶執行權。t2執不執行完都無所謂。  
        int n = 0;  
        for(int x=0;x<80;x++){  
            System.out.println(Thread.currentThread().getName() + "----main" + x);  
        }  
        System.out.println("Over");  
    }  
}

5、停止線程:

  • stop【過時】
    在Java1.5之后,就不再使用stop方法停止線程了。那么該如何停止線程呢?只有一種方法,就是讓run方法結束。
    開啟多線程運行,運行代碼通常為循環結構,只要控制住循環,就可以讓run方法結束,也就可以使線程結束。
    注: 特殊情況:當線程處于凍結狀態,就不會讀取標記,那么線程就不會結束。如下:
class StopThread implements Runnable{  
    private boolean flag = true;  
    public synchronized void run(){  
        while (flag){  
            try{  
                wait();  
            }catch (InterruptedException e) {  
                System.out.println(Thread.currentThread().getName() + "----Exception");  
                flag = false;  
            }  
            System.out.println(Thread.currentThread().getName() + "----run");  
        }  
    }  
    public void changeFlag(){  
        flag = false;  
    }  
}  
  
class  StopThreadDemo{  
    public static void main(String[] args) {  
        StopThread st = new StopThread();  
  
        Thread t1 = new Thread(st);  
        Thread t2 = new Thread(st);  
          
        t1.start();  
        t2.start();  
  
        int n = 0;  
        while (true){  
            if (n++ == 60){  
                st.changeFlag();  
                break;  
            }  
            System.out.println("Hello World!");  
        }  
    }  
}

這時,當沒有指定的方式讓凍結的線程回復打破運行狀態時,就需要對凍結進行清除。強制讓線程回復到運行狀態來,這樣就可以操作標記讓線程結束。

  • interrupt()
    1、此方法是為了讓線程中斷,但是并沒有結束運行,讓線程恢復到運行狀態,再判斷標記從而停止循環,run方法結束,線程結束。
    2、可以打斷sleep,wait,join等顯式的拋出InterruptedException方法的線程,但是打斷后,線程的打斷標記還是false
    isInterrupted() 獲取線程的打斷標記 ,調用后不會修改線程的打斷標記
    interrupted() 獲取線程的打斷標記,調用后清空打斷標記 即如果獲取為true 調用后打斷標記為false (不常用)
class StopThread implements Runnable{  
    private boolean flag = true;  
    public synchronized void run(){  
        while (flag){  
            try{  
                wait();  
            }catch (InterruptedException e){  
                System.out.println(Thread.currentThread().getName() + "----Exception");  
                flag = false;  
            }  
            System.out.println(Thread.currentThread().getName() + "----run");  
        }  
    }  
}  
  
class  StopThreadDemo{  
    public static void main(String[] args){  
        StopThread st = new StopThread();  
        Thread t1 = new Thread(st);  
        Thread t2 = new Thread(st);       
        t1.start();  
        t2.start();  
        int n = 0;  
        while (true){  
            if (n++ == 60){  
                t1.interrupt();  
                t2.interrupt();  
                break;  
            }  
            System.out.println("Hello World!");  
        }  
    }  
}

三、線程的運行狀態

1、系統線程的狀態

  • 初始狀態:創建線程對象時的狀態
  • 可運行狀態(就緒狀態):調用start()方法后進入就緒狀態,也就是準備好被cpu調度執行
  • 運行狀態:線程獲取到cpu的時間片,執行run()方法的邏輯
  • 阻塞狀態: 線程被阻塞,放棄cpu的時間片,等待解除阻塞重新回到就緒狀態爭搶時間片
  • 終止狀態: 線程執行完成或拋出異常后的狀態
線程的狀態

需要說明的是:

  • 阻塞狀態:具備運行資格,但是沒有執行權,必須等到cpu的執行權,才轉到運行狀態。
  • 凍結狀態:放棄了cpu的執行資格,cpu不會將執行權分配給這個狀態下的線程,必須被喚醒后,此線程要先轉換到阻塞狀態,等待cpu的執行權后,才有機會被執行到。

2、Thread類定義的線程狀態

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}
  • NEW 線程對象被創建
  • RUNNALE 線程調用了start()方法后進入該狀態,該狀態包含了三種情況
    • 就緒狀態 :等待cpu分配時間片
    • 運行狀態:進入Runnable方法執行任務
    • 阻塞狀態:BIO 執行阻塞式io流時的狀態
  • BLOCKED 沒獲取到鎖時的阻塞狀態(同步鎖章節會細說)
  • WAITING 調用wait()、join()等方法后的狀態
  • TIMED_WAITING 調用 sleep(time)、wait(time)、join(time)等方法后的狀態
  • TERMINATED 線程執行完成或拋出異常后的狀態
Thread線程狀態

四、上下文切換

CPU在多個線程間進行調度,運行時會進行上下文切換

發生切換場景:

  • 線程的cpu時間片用完
  • 垃圾回收
  • 線程自己調用了sleepyieldwaitjoinparksynchronizedlock 等方法

當發生上下文切換時,操作系統會保存當前線程的狀態,并恢復另一個線程的狀態,jvm中有塊內存地址叫程序計數器,用于記錄線程執行到哪一行代碼,是線程私有的。

五、多線程的安全問題:

在那個簡單的賣票小程序中,發現打印出了0、-1、-2等錯票,也就是說這樣的多線程在運行的時候是存在一定的安全問題的。

1、為什么會出現這種安全問題呢?

原因是當多條語句在操作同一線程共享數據時,一個線程對多條語句只執行了一部分,還未執行完,另一線程就參與進來執行了,導致共享數據發生錯誤。

以也就是說,由于cpu的快速切換,當執行線程一時,tic為1了,執行到if (tic > 0)的時候,cpu就可能將執行權給了線程二,那么線程一就停在這條語句了,tic還沒減1,仍為1;線程二也判斷if (tic > 0)是符合的,也停在這,以此類推。

當cpu再次執行線程一的時候,打印的是1號,執行線程二的時候,是2號票,以此類推,就出現了錯票的結果。其實就是多條語句被共享了,如果是一條語句,是不會出現此種情況的。

  • 問題根源:一行代碼編譯成字節碼的時候可能為多行,在多個線程上下文切換時就可能交錯執行。

2、線程安全

  • 線程安全:多線程調用同一個對象的臨界區的方法時,對象的屬性值一定不會發生錯誤,這就保證了線程安全。

線程安全的類一定所有的操作都線程安全嗎?
開發中經常會說到一些線程安全的類,如ConcurrentHashMap,線程安全指的是類里每一個獨立的方法是線程安全的,但是方法的組合就不一定是線程安全的。

成員變量和靜態變量是否線程安全?

  • 如果沒有多線程共享,則線程安全
  • 如果存在多線程共享
    • 多線程只有讀操作,則線程安全
    • 多線程存在寫操作,寫操作的代碼又是臨界區,則線程不安全

局部變量是否線程安全?

  • 局部變量是線程安全的
  • 局部變量引用的對象未必是線程安全的
    • 如果該對象沒有逃離該方法的作用范圍,則線程安全
    • 如果該對象逃離了該方法的作用范圍,比如:方法的返回值,需要考慮線程安全

3、那么該如何解決呢?(synchronized

對于多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可參與執行,就不會出現問題了。Java對于多線程的安全問題,提供了專業的解決方式,即同步代碼塊,可操作共享數據。

1、同步代碼塊

synchronized(對象)//對象稱為鎖旗標  
{  
    需要被同步的代碼  
}

其中的對象如同鎖,持有鎖的線程可在同步中執行,沒有鎖的線程,即使獲得cpu的執行權,也進不去,因為沒有獲取鎖,是進不去代碼塊中執行共享數據語句的。

同步的前提:

  • A.必須有兩個或兩個以上的線程
  • B.必須保證同步的線程使用同一個鎖。必須保證同步中只能有一個線程在運行。

好處與弊端:

  • 解決了多線程的安全問題(阻塞式的解決方案)。
  • 多個線程需要判斷鎖,較為消耗資源。
class Ticket implements Runnable  
{  
    private int tic = 100;  
    Object obj = new Object();  
    public void run()  
    {  
        while(true)  
        {  
            synchronized(obj)//任意的一個對象  
            {  
                //此兩句為共享語句  
                if (tic > 0)  
                    System.out.println(Thread.currentThread().getName() + "sale:" + tic--);  
            }     
        }  
    }  
}  
  
class  TicketDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t,"1");//創建第一個線程  
        Thread t2 = new Thread(t,"2");//創建第二個線程  
        //開啟線程  
        t1.start();  
        t2.start();  
    }  
}

2、同步函數
同步函數就是將修飾符synchronized放在返回類型的前面,下面通過同步函數給出多線程安全問題的具體解決方案:

1)目的:判斷程序中是否有安全問題,若有,該如何解決。
2)解決:
第一、明確哪些代碼是多線程的運行代碼
第二、明確共享數據
第三、明確多線程運行代碼中,哪些語句是操作共享數據的。

示例:

class Bank  
{  
    private int sum;//共享數據  
    //run中調用了add,所以其也為多線程運行代碼  
    public synchronized void add(int n)//同步函數,用synchronized修飾  
    {  
        //這有兩句操作,是操作共享數據的  
        sum += n;  
            System.out.println("sum" + sum);  
    }  
}  
  
class Cus implements Runnable  
{  
    private Bank b = new Bank();//共享數據  
    //多線程運行代碼run  
    public void run()  
    {  
        for (int i=0;i<3;i++)  
        {  
            b.add(100);//一句,不會分開執行,所以沒問題  
        }  
    }  
}  
  
class BankDemo   
{  
    public static void main(String[] args)   
    {  
        Cus c = new Cus();  
        Thread t1 = new Thread(c);  
        Thread t2 = new Thread(c);  
        t1.start();  
        t2.start();  
    }  
}

六、同步函數中的鎖:

1、非靜態同步函數中的鎖:this

函數需被對象調用,那么函數都有一個所屬的對象引用,就是this,因此同步函數使用的鎖為this。

測驗如下:

class Ticket implements Runnable  
{  
    private int tic = 100;  
    boolean flog = true;  
    public void run()  
    {  
        if (flog)  
        {  
            //線程一執行  
            while(true)  
            {  
                //如果對象為obj,則是兩個鎖,是不安全的;換成this,為一個鎖,會安全很多  
                synchronized(this)  
                {  
                    if (tic > 0)  
                        System.out.println(Thread.currentThread().getName() + "--cobe--:" + tic--);  
                }  
            }  
        }  
        //線程二執行  
        else  
            while(true)  
                show();  
    }  
    public synchronized void show()  
    {  
        if (tic > 0)  
            System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);  
    }  
}  
  
class ThisLockDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t);//創建一個線程  
        Thread t2 = new Thread(t);//創建一個線程  
        t1.start();  
        t.flog = false;//開啟線程一,即關閉if,讓線程二執行else中語句  
        t2.start();  
    }  
}

讓線程一執行打印cobe的語句,讓線程二執行打印show的語句。如果對象換位另一個對象obj,那將是兩個鎖,因為在主函數中創建了一個對象即Ticket t = new Ticket();,線程會共享這個對象調用的run方法中的數據,所以都是這個t對象在調用,那么,其中的對象應為this;否則就破壞了同步的前提,就會出現安全問題。

2、靜態同步函數中的鎖:

如果同步函數被靜態修飾后,經驗證,使用的鎖不是this了,因為靜態方法中不可定義this,所以,這個鎖不再是this了。靜態進內存時,內存中沒有本類對象,但是一定有該類對應的字節碼文件對象:類名.class;該對象的類型是Class。

所以靜態的同步方法使用的鎖是該方法所在類的字節碼文件對象,即類名.class。

示例:

>class Ticket implements Runnable  
{  
    //私有變量,共享數據  
    private static int tic = 100;  
    boolean flog = true;  
    public void run()  
    {  
        //線程一執行  
        if (flog)  
        {  
            while(true)  
            {  
                synchronized(Ticket.class)//不再是this了,是Ticket.class  
                {  
                    if (tic > 0)  
                        System.out.println(Thread.currentThread().getName() + "--obj--:" + tic--);  
                }  
            }  
        }  
        //線程二執行  
        else  
            while(true)  
                show();  
    }  
    public static synchronized void show()  
    {  
        if (tic > 0)  
            System.out.println(Thread.currentThread().getName() + "----show-----:" + tic--);  
    }  
}  
  
class StaticLockDemo  
{  
    public static void main(String[] args)   
    {  
        Ticket t = new Ticket();  
        Thread t1 = new Thread(t);//創建第一個線程  
        Thread t2 = new Thread(t);//創建第二個線程  
        t1.start();  
        t.flog = false;  
        t2.start();  
    }  
}

在之前,也提到過關于多線程的安全問題的相關知識,就是在單例設計模式中的懶漢式中,用到了鎖的機制。

七、多線程間的通信:

多線程間通信是線程之間進行交互的方式,簡單說就是存儲資源獲取資源。比如說倉庫中的貨物,有進貨的,有出貨的。還比如生產者和消費者的例子。這些都可以作為線程通信的實例。那么如何更好地實現通信呢?

先看下面的代碼:

/* 
線程間通信: 
等待喚醒機制:升級版 
生產者消費者  多個 
*/  
import java.util.concurrent.locks.*;  
  
class ProducerConsumerDemo{  
    public static void main(String[] args){  
        Resouse r = new Resouse();  
        Producer p = new Producer(r);  
        Consumer c = new Consumer(r);  
        Thread t1 = new Thread(p);  
        Thread t2 = new Thread(c);  
        Thread t3 = new Thread(p);  
        Thread t4 = new Thread(c);  
        t1.start();  
        t2.start();  
        t3.start();  
        t4.start();  
    }  
}  
  
class Resouse{  
    private String name;  
    private int count = 1;  
    private boolean flag =  false;   
    private Lock lock = new ReentrantLock();  
    private Condition condition_P = lock.newCondition();  
    private Condition condition_C = lock.newCondition();  
//要喚醒全部,否則都可能處于凍結狀態,那么程序就會停止。這和死鎖有區別的。  
    public void set(String name)throws InterruptedException{  
        lock.lock();  
        try{  
            while(flag)//循環判斷,防止都凍結狀態  
                condition_P.await();  
            this.name = name + "--" + count++;  
            System.out.println(Thread.currentThread().getName() + "..生成者--" + this.name);  
            flag = true;  
            condition_C.signal();  
        }finally{  
            lock.unlock();//釋放鎖的機制一定要執行  
        }         
    }  
    public void out()throws InterruptedException{  
        lock.lock();  
        try{  
            while(!flag)//循環判斷,防止都凍結狀態  
                condition_C.await();  
            System.out.println(Thread.currentThread().getName() + "..消費者." + this.name);  
            flag = false;  
            condition_P.signal();//喚醒全部  
        }finally{  
            lock.unlock();  
        }  
    }  
}  
  
class Producer implements Runnable{  
    private Resouse r;  
    Producer(Resouse r){  
        this.r = r;  
    }  
    public void run(){  
        while(true){  
            try{  
                r.set("--商品--");  
            }catch (InterruptedException e){}  
        }  
    }  
}  
  
class Consumer implements Runnable{  
    private Resouse r;  
    Consumer(Resouse r){  
        this.r = r;  
    }  
    public void run(){  
        while(true){  
            try{  
                r.out();  
            }catch (InterruptedException e){}  
        }  
    }  
}

1、等待喚醒機制:

1、顯式鎖機制和等待喚醒機制:
在JDK 1.5中,提供了改進synchronized的升級解決方案。將同步synchronized替換為顯式的Lock操作,將Object中的wait,notify,notifyAll替換成Condition對象,該對象可對Lock鎖進行獲取。這就實現了本方喚醒對方的操作。

在這里說明幾點:
1)、對于wait,notify和notifyAll這些方法都是用在同步中,也就是等待喚醒機制,這是因為要對持有監視器(鎖)的線程操作。所以要使用在同步中,因為只有同步才具有鎖。

2)、而這些方法都定義在Object中,是因為這些方法操作同步中的線程時,都必須表示自己所操作的線程的鎖,就是說,等待和喚醒的必須是同一把鎖。不可對不同鎖中的線程進行喚醒。所以這就使得程序是不良的,因此,通過對鎖機制的改良,使得程序得到優化。

3)、等待喚醒機制中,等待的線程處于凍結狀態,是被放在線程池中,線程池中的線程已經放棄了執行資格,需要被喚醒后,才有被執行的資格。

2、對于上面的程序,有兩點要說明:
1)、為何定義while判斷標記:
原因是讓被喚醒的線程再判斷一次。
避免未經判斷,線程不知是否應該執行,就執行本方的上一個已經執行的語句。如果用if,消費者在等著,兩個生成著一起判斷完flag后,cpu切換到其中一個如t1,另一個t3在wait,當t1喚醒凍結中的一個,是t3(因為它先被凍結的,就會先被喚醒),所以t3未經判斷,又生產了一個。而沒消費。

2)這里使用的是signal方法,而不是signalAll方法。是因為通過Condition的兩個對象,分別喚醒對方,這就體現了Lock鎖機制的靈活性。可以通過Contidition對象調用Lock接口中的方法,就可以保證多線程間通信的流暢性了。

對于多線程的知識,還需要慢慢積累,畢竟線程通信可以提高程序運行的效率,這樣就可以讓程序得到很大的優化。期待新知識······

八、線程池

1、簡述:

預先創建好一些線程,任務提交時直接執行,既可以節約創建線程的時間,又可以控制線程的數量。

2、線程池的好處

  • 降低資源消耗,通過池化思想,減少創建線程和銷毀線程的消耗,控制資源
  • 提高響應速度,任務到達時,無需創建線程即可運行
  • 提供更多更強大的功能,可擴展性高

3、線程池的主要流程

  • 流程包括:線程池創建、接收任務、執行任務、回收線程的步驟

  • 線程池的構造函數:

public ThreadPoolExecutor(int corePoolSize,  //核心線程數
                          int maximumPoolSize, //最大線程數
                          long keepAliveTime,  //救急線程的空閑時間
                          TimeUnit unit,  //救急線程的空閑時間單位
                          BlockingQueue<Runnable> workQueue,  //阻塞隊列
                          ThreadFactory threadFactory,  //創建線程的工廠,主要定義線程名
                          RejectedExecutionHandler handler  //拒絕策略
) {
  //......
}
  • 流程:

1、創建線程池后,線程池的狀態是RUNNABLE,該狀態下才能有下面的步驟
2、提交任務時,線程池會創建線程去處理任務
3、當線程池的工作線程數達到corePoolSize時,繼續提交任務會進入阻塞隊列
4、當阻塞隊列裝滿時,繼續提交任務,會創建救急線程來處理
5、當線程池中的工作線程數達到maximumPoolSize時,會執行拒絕策略
6、當線程取任務的時間達到keepAliveTime還沒有取到任務,工作線程數大于corePoolSize時,會回收該線程

  • 注意: 不是剛創建的線程是核心線程,后面創建的線程是非核心線程;線程是沒有核心非核心的概念的。
  • 拒絕策略

1、調用者拋出RejectedExecutionException (默認策略)
2、讓調用者運行任務
3、丟棄此次任務
4、丟棄阻塞隊列中最早的任務,加入該任務

  • 提交任務的方法
// 執行Runnable
public void execute(Runnable command) {
}

// 提交Callable
public <T> Future<T> submit(Callable<T> task) {
  if (task == null) throw new NullPointerException();
   // 內部構建FutureTask
  RunnableFuture<T> ftask = newTaskFor(task);
  execute(ftask);
  return ftask;
}

// 提交Runnable,指定返回值
public Future<?> submit(Runnable task) {
  if (task == null) throw new NullPointerException();
  // 內部構建FutureTask
  RunnableFuture<Void> ftask = newTaskFor(task, null);
  execute(ftask);
  return ftask;
} 

//  提交Runnable,指定返回值
public <T> Future<T> submit(Runnable task, T result) {
  if (task == null) throw new NullPointerException();
   // 內部構建FutureTask
  RunnableFuture<T> ftask = newTaskFor(task, result);
  execute(ftask);
  return ftask;
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
}

4、Execetors創建線程池

  • newFixedThreadPool(定長線程池)
    核心線程數 = 最大線程數 沒有救急線程
    阻塞隊列無界 可能導致oom
    可控制線程最大并發數,超出的線程會在隊列中等待
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
  • newCachedThreadPool(可緩存線程池)
    核心線程數是0,最大線程數無限制 ,救急線程60秒回收
    隊列采用 SynchronousQueue 實現,沒有容量,即放入隊列后沒有線程來取就放不進去
    可能導致線程數過多,cpu負擔太大
    如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • newSingleThreadExecutor(單線程化的線程池)
    核心線程數和最大線程數都是1,沒有救急線程,無界隊列 可以不停的接收任務
    將任務串行化 一個個執行, 使用包裝類是為了屏蔽修改線程池的一些參數 比如 corePoolSize
    如果某線程拋出異常了,會重新創建一個線程繼續執行
    可能造成oom
    用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行

現行大多數GUI程序都是單線程的。Android中單線程可用于數據庫操作,文件操作,應用批量安裝,應用批量刪除等不適合并發但可能IO阻塞性及影響UI線程響應的操作。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • newScheduledThreadPool(定長線程池)
    任務調度的線程池。可以指定延遲時間調用,可以指定隔一段時間調用
    支持定時及周期性任務執行
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

5、線程池的關閉

  • shutdown()
    會讓線程池狀態為shutdown,不能接收任務,但是會將工作線程和阻塞隊列里的任務執行完 相當于優雅關閉

  • shutdownNow()
    會讓線程池狀態為stop, 不能接收任務,會立即中斷執行中的工作線程,并且不會執行阻塞隊列里的任務, 會返回阻塞隊列的任務列表

6、線程池的使用

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

推薦閱讀更多精彩內容