Java多線程常見案例分析線程池與單例模式及阻塞隊列

一、單例模式

設計模式:軟件設計模式

是一套被反復使用、多數人知曉、經過分類編目、代碼設計經驗的總結。使用設計模式是為瞭可重用代碼、讓代碼更容易被他人理解、保證代碼可靠性、程序的重用性。

單例模式:是設計模式的一種。保證某個類在程序中隻存在唯一一份實例,不會創建出多個實例。單例模式的具體實現分為“懶漢”和“餓漢”兩種。

構造方法必須是私有的,保證該類不能在類外被隨便創建。

1、餓漢模式

類加載的同時,創建實例。(缺點是無論是否使用都會創建對象,比較占空間)

//類加載的時候創建對象,確保隻能有一個實例對象
class Singleton {
    private static Singleton instance = new Singleton();
    //私有的構造方法
    private Singleton() {}
    //隻能通過getInstance()方法獲取到同一個實例對象
    public static Singleton getInstance() {
        return instance;
    }
}

2、懶漢模式(單線程)

類加載的時候不創建實例,第一次使用的時候才創建實例。

(缺點:線程不安全,如果存在多個線程並發並行執行,可能創建多個實例,所以隻適用於單線程)

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        //第一次使用時,創建實例
        if (instance == null) {
            instance = new Singleton();
        }
        //後面使用時,直接返回第一次創建的實例
        return instance;
    }
}

3、懶漢模式(多線程)

上面的懶漢模式存在線程安全問題,如果多個線程同時調用getInstance()方法,可能創建多個實例。所以在多線程時,我們需要使用synchronized改善線程安全問題。

class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    //加鎖保證不會有多個線程同時訪問改代碼塊
    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

對於以上代碼,雖然保證瞭線程安全,但是對於懶漢模式,隻有在第一次調用時才會創建實例,大多數境況下隻進行讀操作,如果對代碼塊整體加鎖,程序執行的效率會大大降低。我們可以對上面的程序進一步優化,對於讀操作,我們使用volatile修飾變量;隻給寫操作的代碼塊加上鎖即可。

【單例模式懶漢模式多線程的進一步優化】雙重if判定

class Singleton {
    //使用volatile修飾變量
    private static volatile Singleton instance = null;
    private Singleton() {};
    public static Singleton getInstance() {
        if (instance == null) {
            //隻給寫操作的相關代碼加鎖
            synchronized (Singleton.class) {
                //需要雙重if判斷,防止在多線程中加鎖前instance發生變化
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

寫操作加鎖,保證線程安全;

如果已經實例化,進行讀操作,保證多個線程並發並行執行,保證效率。

二、阻塞隊列

阻塞隊列是什麼?

阻塞隊列是一種特殊的隊列。也遵守“先進先出”的原則。

阻塞隊列是一種線程安全的數據結構:

  • 當隊列滿的時候,繼續入隊隊列就會阻塞,知道有其他線程從隊列中取走元素;
  • 當隊列空的時候,繼續出隊也會阻塞,直到其他線程往隊列中插入元素。

阻塞隊列的一個經典應用場景就是“生產者消費者模型”。

標準庫中的阻塞隊列:

  • BlockingQueue是一個接口,真是實現的是類是:LinkedBlockingQueue。
  • put方法用於阻塞式的入隊列,take用於阻塞式的出隊列。
  • BlockingQueue也有offer、poll、peek方法,但是不具有阻塞特性。

阻塞隊列的實現

  • 通過循環隊列實現;
  • 使用synchronized進行加鎖控制
  • put插入元素,如果隊列滿瞭,就進行wait(要在循環中進行wait,多線程情況下可能喚醒多個線程,所以喚醒後隊列可能還是滿的)
  • take取出元素,如果隊列為空,就wait(循環中wait)
public class BlockingQueue{
    //使用循環數組來實現阻塞隊列
    private int[] array;
    //隊列中已經存放元素的個數
    private int size;
    //放入元素的下標
    private int putIndex;
    //取元素的下標
    private int takeIndex;
    //在構造方法中指定隊列的大小
    public BlockingQueue(int capacity){
        array=new int[capacity];
    }
    /*放元素:需要保證線程安全,如果隊列滿瞭,線程進入等待*/
    public synchronized void put(int m) throws InterruptedException {
        //隊列滿,線程等待
        if(size==array.length){
            //需要註意的是,進行等待的是當顯得實例對象,不是類對象
            this.wait();
        }
        //放元素,同時更新下標
        array[putIndex]=m;
        putIndex=(putIndex+1)%array.length;
        size++;
        //通知等待的線程
        notifyAll();
    }
    /*取元素:保證線程安全。如果隊列為空,線程等待*/
    public synchronized int take() throws InterruptedException {
        //隊列為空,線程等待
        if(size==0){
            this.wait();
        }
        //取元素,同時更新下標
        int ret=array[takeIndex];
        takeIndex=(takeIndex+1)%array.length;
        size--;
        //通知等待的線程
        notifyAll();
        return ret;
    }
}

生產者消費者模型

生產者消費者模型就是通過一個容器來解決生產者和消費者之間的強耦合問題。

生產者和消費者之間不直接通信,而通過阻塞隊列來實現通訊,所以生產者生產完數據不需要等待消費者來處理,直接扔給阻塞隊列。消費者也不需要去找生產者,而是直接從阻塞隊列中取。

  • 阻塞隊列相當於一個緩沖區,平衡瞭消費者和生產者的處理能力;
  • 阻塞隊列也能使生產者和消費者之間“解耦”。

耦合和解耦:

  • 耦合指的是兩個類之間聯系的緊密程度。強耦合(表示類之間存在著直接的關系)。弱耦合(在兩個類的中間加入一層,將原來的之間關系變成間接關系,使得兩個類對中間層是強耦合,兩個類之間變成瞭弱耦合。
  • 解耦:降低耦合度,也就是將強耦合變成弱耦合的過程。

三、線程池

池:字符串常量池(類似緩存)、數據庫連接池等

線程池:初始化的時候就創建一定數量的線程【不同的從線程池的阻塞隊列中取任務(消費者)】【在其他線程中提交任務到線程池(生產者)】

優點:

線程的創建和銷毀都有一定的代價,使用線程池就可以重復使用線程來執行多組任務。(如果線程不再使用,並不是真正的將線程釋放,而是放到一個“池子”中,下次如果需要用到線程直接從池子中取,不必通過系統來創建)

1、創建線程池的的方法

(1)ThreadPoolExecutor

提供瞭更多的可選參數,可以進一步細化線程池行為的設定。

以第三個構造方法為例:

  1. corePoolSize:表示核心線程的數量
  2. maximumPoolSize:最大線程數(核心線程+臨時線程)
  3. keepAliveTime:允許臨時線程空閑的時間(如果超過該時間臨時線程還是沒有任務執行,就被銷毀)
  4. unit: keepaliveTime的時間單位
  5. workQueue:傳遞任務的阻塞隊列
  6. threadFactory:規定創建線程的標準
  7. RejectedExecutionHandler:拒絕策略,如果阻塞隊列已滿,再傳進來任務該怎麼辦

【1】AbortPolicy():超過負荷,直接拋出異常(默認的拒絕策略,使用其他不帶拒絕策略的構造方法時的默認參數)

【2】CallerRunsPolicy():調用者負責處理

【3】DiscardOldestPolicy():丟棄隊列中最老的任務

【4】DiscardPolicy():丟棄新來的任務

創建線程池如下:

        //使用ThreadPoolExecutor創建線程池
        ThreadPoolExecutor threadPool1=new ThreadPoolExecutor(
                5,
                10,
                3,
                //自由線程無任務時最大存活時間單位:分
                TimeUnit.MINUTES,
                //一般不使用無邊界的阻塞隊列,內存有限
                new ArrayBlockingQueue<>(100),
                //規定創建線程的標準
                Executors.defaultThreadFactory(),
                //拒絕策略:一般最多使用CallerRunsPolicy(),或自己實現
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

(2)Executors(快捷創建線程池的API)

Executors創建線程的幾種方式:

  • newFixedThreadPool:創建固定線程數的線程池(沒有臨時線程)
  • newCachedThreadPool:創建線程數目動態增長的線程池(緩存的線程池,沒有核心線程,全是臨時線程)
  • newSingleThreadExecutor:創建隻包含單個線程的線程池
  • newScheduledThreadPool:設定延遲時間後執行任務,或者定期執行命令(計劃線程池)

創建線程池如下:

        //Executors的四種創建線程的方法
        //沒有臨時線程的線程池
        ExecutorService threadPool2= Executors.newFixedThreadPool(10);
        //線程數目動態增長的線程池
        ExecutorService threadPool3=Executors.newCachedThreadPool();
        //創建單個線程的線程池
        ExecutorService threadPool4=Executors.newSingleThreadExecutor();
        //計劃線程池
        ExecutorService threadPool5=Executors.newScheduledThreadPool(7);

2、線程池的工作流程

線程池工作流程

使用線程池:

創建線程池

提交任務:

【1】submit(Runnable task)

【2】execute(Runnable task)

到此這篇關於Java多線程常見案例分析線程池與單例模式及阻塞隊列的文章就介紹到這瞭,更多相關Java線程池內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: