1. 程式人生 > 實用技巧 >Java進階之執行緒

Java進階之執行緒

Java進階之執行緒

Java進階之執行緒

執行緒的概念

  • 可以同步一起執行
  • 執行緒和程序
    • 一個程序就是一個應用,而執行緒是在程序內部衍生出來的
    • 也可以說,執行緒是程序的最小組成單元
  • 並行和併發
    • 並行意味著一起開啟
    • 併發意味著不但一起開啟,同時CPU同時進行處理
    • 並行是併發產生的前序條件
    • 並行一般不會對機器產生較大壓力,但是併發會產生大量壓力
    • 降低執行緒的併發 RabbitMQ
      • 程式執行時間比較長--有效優化程式的執行時長(拆解)eg:訊息佇列
      • 底層一些機制 --排隊機制
  • 同步和非同步
    • 同步是指傳送一個請求,需要等待返回後才能傳送下一個請求,有等待過程
    • 非同步是指傳送一個請求後,不需要等待返回,隨時可以傳送下一個請求,即不需要等待
    • 同步是用於確保資源一次只能被一個執行緒使用,同步相比非同步,效能會相差三到四倍

執行緒執行機制

  • 就緒狀態(新生)
  • 執行狀態(執行)
  • 掛起狀態
    • 阻塞
    • 等待
    • 超時等待
  • 死亡狀態(終止)
  • 每次在執行java.exe時,都會有一個java虛擬機器程序啟動,執行程式帶程式碼的任務是由執行緒來完成的,所以,每一個執行緒都一個獨立的程式計數器 和方法呼叫棧
    • 程式計數器:也稱為PC暫存器,當執行緒執行一個方法時,程式計數器會指向方法區中的下一條要執行的位元組碼指令
    • 方法呼叫棧:簡稱方法棧,用來跟蹤執行緒執行中一系列的方法呼叫過程
  • 每當用Java命令啟動一個Java虛擬機器程序時,Java虛擬機器都會建立一個主執行緒,該執行緒也就是main,也就是誠如的入口

使用執行緒

    • 繼承Thread類,重寫run方法
    • 優點:程式碼簡單。
    • 缺點:無法繼承別的類
public class ExtendsThread extends Thread{
    private String name;
    public ExtendsThread(String name){
        this.name = name;
    }
    public void run(){
        for(int i = 1;i<=100;i++){
            System.out.println(name+"跑了"+i+"米");
        }
    }

    
public static void main(String[] args) { ExtendsThread extendsThread1 = new ExtendsThread("李麒麟"); ExtendsThread extendsThread2 = new ExtendsThread("吉祥物"); ExtendsThread extendsThread3 = new ExtendsThread("神獸"); extendsThread1.start(); extendsThread2.start(); extendsThread3.start(); } }
    • 實現Runnable介面,實現run方法
    • 優點:可以繼承其他類,實現同一介面的多個執行緒物件共享一個任務類物件,即多執行緒共享一份資源
    • 缺點:程式碼複雜,訪問當前執行緒必須要使用Thread.currentThread()方法
public class ImplementsRunnable implements Runnable{
    private String name;
    public ImplementsRunnable(String name){
        this.name = name;
    }
    @Override
    public void run() {
        for(int i = 1;i<=100;i++){
            System.out.println(name+"跑了"+i+"米");
        }
    }

    public static void main(String[] args) {
        ImplementsRunnable implementsRunnable1 = new ImplementsRunnable("起名字真是太難了");
        ImplementsRunnable implementsRunnable2 = new ImplementsRunnable("起名字真是超級難");
        ImplementsRunnable implementsRunnable3 = new ImplementsRunnable("起名字真是難爆了");
        Thread thread1 = new Thread(implementsRunnable1);
        Thread thread2 = new Thread(implementsRunnable2);
        Thread thread3 = new Thread(implementsRunnable3);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
    • 實現Callable介面
    • 優點:可以繼承其他類,多執行緒共享一份資源,還具有返回值,可以跑出返回值的異常
    • 缺點:程式碼複雜,訪問當前執行緒必須要使用Thread.currentThread()方法
    • 當需要呼叫get()方法時,如果執行緒還未執行完畢,則會阻塞到執行緒執行完畢後拿到返回值
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ImplementsCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (i = 0;i<20;i++){
            if(i == 10){
                break;
            }
        }
        return i;
    }

    public static void main(String[] args) {
        ImplementsCallable implementsCallable = new ImplementsCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(implementsCallable);
        Thread thread1 = new Thread(futureTask,"執行緒1");
        thread1.start();
        try {
            System.out.println(Thread.currentThread().getName()+" "+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 常用方法
    • 獲取執行緒名字:getName()
    • 獲取執行緒標識:getId()
    • 獲取執行緒優先順序:getPriority()
    • 更改執行緒優先順序:setPriority()
    • 判斷執行緒是否存活:isAlive()
    • 設定執行緒為守護執行緒:setDaemon(true)
    • 判斷執行緒是否為守護執行緒:isDaemon()
    • 設定執行緒名字:setName()
    • 讓執行緒進入阻塞狀態:sleep()
    • 啟動執行緒:start()
    • 執行緒體:run()
    • join(),當前執行的執行緒呼叫其他執行緒,讓自己進入阻塞狀態,當其他執行緒執行完畢,自己再繼續執行
    • sleep()方法進入阻塞狀態後,在執行時不會釋放物件鎖,如果被join()方法中斷,則會丟擲異常,同時清除中斷狀態
    • yield(),一般用於測試,提高執行緒併發效果,但執行完畢後會讓執行緒優先順序高的,或同等級的執行緒優先執行

執行緒鎖

    • 生產消費者模型
      • 執行緒AB共享固定大小緩衝區
      • 生產者:A產生資料存放到緩衝區
      • 消費者:B從緩衝區讀取資料計算



      • 在多執行緒中,如果生產者生產資料的速度過快,導致消費者資料無法快速使用完畢,生產者就必須等待消費者消費完畢資料後,再次生產資料,為了生產消費達到一定的動態平衡,就需要一個緩衝區存放生產者的資料,這就是所謂的生產者-消費者模式
      • 特點
      • 保證生產者在緩衝區已滿的時候不會再向緩衝區內寫入資料,消費者也不會在緩衝區為空的時候消費資料
      • 當緩衝區滿時,生產者會進入休眠,當消費者消耗緩衝區資料後,緩衝區有空餘空間,生產者會被喚醒,繼續往緩衝區中寫入資料,同理,當緩衝區為空時,消費者會進入休眠,當生產者將資料寫入緩衝區後,消費者會被喚醒,消費緩衝區內的資料進行計算。
    • 鎖機制
      • 互斥鎖synchronized --隱式鎖
        • 物件鎖synchronized("鎖A")
        • 類鎖synchronized(A.class) = public synchronized static void A(){}
        • synchronized是關鍵字,由虛擬機器自動釋放
        • 加鎖方式:方法鎖
public synchronized void sy(){
    //將方法加鎖
}
        • 加鎖方式:鎖塊
public class TestTrainTickets {
    static class TrainTickets implements Runnable{
        int trainTickents = 100;
        @Override
        public void run(){
            while (trainTickents>0){
                synchronized (this){
                    if(trainTickents<=0){
                        System.out.println(Thread.currentThread().getName()+"沒票了,請關注下次列車");
                        break;
                    }else{
                        System.out.println(Thread.currentThread().getName()+"出售車票第"+(100-trainTickents+1)+"張");
                        trainTickents--;
                    }
                    try {
                        Thread.sleep(new Random().nextInt(500)+1);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        TrainTickets trainTickets = new TrainTickets();
        Thread thread1 = new Thread(trainTickets,"一號視窗");
        Thread thread2 = new Thread(trainTickets,"二號視窗");
        Thread thread3 = new Thread(trainTickets,"三號視窗");
        Thread thread4 = new Thread(trainTickets,"四號視窗");
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}
      • 重入鎖ReentrantLock 顯示鎖
        • ReentrantLoct預設為非公平鎖,可以通過構造器更改
<!-- 
 public ReentrantLock(){
     //非公平鎖
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair){
    //true為公平鎖,false為非公平鎖
    sync = fair ? new FairSync() : new NonfairSync();
}
        • 非公平鎖NonfairSync()
        • 非公平鎖是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒優先獲取鎖,在高併發下,可能會出現優先順序翻轉(兩級反轉)的情況下
        • 公平鎖 FairSync()
        • 公平鎖顧名思義就是先來後到,按照申請順序公平的獲取鎖
        • lock是方法,加鎖後需要手動釋放鎖
        • 獲取鎖lock()
        • 釋放鎖unlock()
Lock lock = new ReentrantLock(true);//建立鎖為公平鎖
public void lock(){
    //加鎖
    lock.lock();
    //被鎖的內容    
    /釋放鎖
    lock.unlock();
}
    • 讀寫鎖read-write lock,又稱共享-獨佔鎖
      • 讀鎖,當讀寫鎖被加了讀鎖時,其他執行緒對該鎖加寫鎖會阻塞,加讀鎖會成功
      • 多個加入讀鎖的執行緒的程式之間可以並行訪問
      • 寫鎖,當讀寫鎖被加了寫鎖時,其他執行緒對該鎖加讀鎖或者寫鎖都會阻塞(不是失敗)
      • 寫鎖和讀鎖之間序列
      • 寫鎖和寫鎖之間序列
  • synchronized和lock的區別
    • Synchronized 內建的 Java 關鍵字;Lock 是一個 Java 類
    • Synchronized 無法判斷獲取鎖的狀態;Lock 可以判斷是否獲取到了鎖
    • Synchronized 會自動釋放;Lock 必須要手動釋放鎖!如果不釋放 會產生死鎖
    • Synchronized 執行緒 1(獲得鎖,阻塞),執行緒 2(等待獲取,死等);Lock 鎖就不一定會等待下去
    • Synchronized 可重入鎖,不可以中斷的,非公平;Lock 可重入鎖,可以判斷鎖,非公平(可以自己設定)
    • Synchronized 適合鎖少量的程式碼同步問題;Lock 適合鎖大量的同步程式碼!
  • ConncurrentHashMap
    • 支援高併發情況下的map容器,支援複合操作,採用分段鎖管理機制
    • Hashtbale是獨佔鎖,在高併發下效能不好,不支援符合操作
    • 使用Collections.synchronizedList(new ArrayList()),可以將一個非同步集合變成同步集合
  • CountDownLatch
    • 一個同步容器的輔助類,在完成一組正在其他執行緒中執行的操作之前,它允許一個或多個執行緒一直等待
    • 常用構造器:CountDownLatch(int count) 鎖存器的變數值
    • 常用方法
      • 執行緒喚醒:await() 當鎖存器變數不等於0,一直進行等待狀態,當鎖存器變數等於0,將等待的執行緒喚醒
      • 遞減鎖存器的值:countDown() 遞減鎖存器的值,直到變數為0,釋放所有正在等待的執行緒,用於finally塊中,防止有的執行緒一直在等待
  • 鎖的應用場景
    • 併發導致了一些資料不一致的情況下
    • 考慮先後順序的
  • 悲觀鎖和樂觀鎖
    • 悲觀鎖:悲觀的認為所有操作都會因為併發出現問題,所有操作必須序列
    • 樂觀鎖:樂觀的認為讀操作一般不會出現問題,寫操作才會,所以對讀操作不會產生序列效果,對寫操作不友好
  • 活鎖和死鎖
    • 死鎖發生在當兩個或多個執行緒一直在等待另一個執行緒持有的鎖或資源的時候。這會導致一個程式可能會被拖垮或者直接掛掉,因為執行緒們都不能繼續工作了。
    • 活鎖是另一個併發問題,它和死鎖很相似。在活鎖中,兩個或多個執行緒彼此間一直在轉移狀態,而不像我們上個例子中互相等待。結果就是所有執行緒都不能執行它們各自的任務。
  • 自旋鎖和阻塞鎖
    • 自旋鎖:是指當執行緒獲取鎖失敗的時候,去執行一個無意義的迴圈,迴圈結束後再重新去競爭鎖,如果競爭不到則繼續迴圈。整個過程中執行緒一直處於執行(running)狀態。
    • 阻塞鎖:和自旋鎖相對,指當執行緒獲取鎖失敗時,執行緒進入阻塞(blocking)狀態,當獲取相應的訊號時(喚醒,時間),進入執行緒的準備就緒狀態,準備就緒狀態的所有執行緒,通過競爭,進入執行狀態。

執行緒的通訊

  • 等待:wait()
  • 等待一定時間:wait(long)
  • 喚醒:notify()
  • 喚醒所有:notifyAll()
  • wait方法和sleep方法的區別
    • wait是Object類的,sleep是Thread類的方法
    • wait方法必須放在同步程式碼塊或同步方法在惠普那個,sleep不需要
    • wait方法必須有notify,notifyAll方法喚醒,才能繼續執行,sleep方法會在執行完時間段後,自動喚醒執行
    • wait方法必須妨礙notify方法的後面
    • wate方法可以被中斷,sleep如果被中斷會丟擲異常
    • wait在執行時會釋放鎖,sleep在執行過程中不會釋放鎖

執行緒池

  • 執行緒池的實現原理
    • 提供了一個執行緒的佇列,佇列中儲存著所有等待狀態的執行緒
    • 避免了建立與銷燬操作額外的開銷,提高了響應速度
    • 使用執行緒池可以很好地提高效能,執行緒池在系統啟動時即建立大量空閒的執行緒,程式將一個任務傳給執行緒池,執行緒池就會啟動一條執行緒來執行這個任務,執行結束以後,該執行緒並不會死亡,而是再次返回執行緒池中成為空閒狀態,等待執行下一個任務
    • 多執行緒執行時,系統不斷啟動和關閉執行緒,資源會過渡消耗,以及切換執行緒時會產生危險,從而導致系統資源的崩潰
  • 執行緒池的工作機制
    • 線上程池的程式設計模式下,任務提交給執行緒池,而不是直接提交給某個執行緒
    • 執行緒池在拿到任務後,在內部尋找有空閒的執行緒
    • 一個執行緒同時只能執行一個任務,但是可以向一個執行緒提交多個任務
  • 執行緒池體系結構
    • java.util.concurrent包
      • Excutor:負責執行緒的使用與排程的根介面
        • ExcutorService子介面:執行緒池的主要介面
          • ThreadPoolExecutor:執行緒池的實現類
          • ScheduledThreadPoolExecutor:繼承了ThreadPoolExecutor , 實現了ScheduledExecutorService介面,負責執行緒池中的執行緒排程
    • 有些時候,執行緒不允許使用Excutors去建立,而是通過ThreadPoolExecutor的方式,這樣的方式是為了規避資源耗盡的風險
    • Executors執行緒池物件的弊端
      • FixedThreadPool和SingleThreadPool:允許的請求佇列長度為Integer.MAX_VALUE,可能會堆積大量請求,導致OOM
      • CachedThreadPool和ScheduledThreadPool:允許建立的執行緒數量為Integer.MAX_VALUE,可能會建立大量執行緒,導致OOM
  • 常用執行緒池
    • ExecutorService
    • ExecutorService
    • ExecutorService
    • ScheduledExecutorService
  • 執行緒池的應用
    • 建立執行的執行緒池
    • submit():給執行緒池中的執行緒分配任務,返回值返回的是Future介面
    • shutdown()關閉執行緒池