從線程池到synchronized關鍵字詳解

線程池 BlockingQueue synchronized volatile

前段時間看了一篇關於"一名3年工做經驗的程序員應該具有的技能"文章,倍受打擊。不少熟悉而又陌生的知識讓我懷疑本身是一個假的程序員。本章從線程池,阻塞隊列,synchronized 和 volatile關鍵字,wait,notify方法實現線程之間的通信,死鎖,常考面試題。將這些零碎的知識整合在一塊兒。以下圖所示。java

學習流程圖:
學習流程圖
技術:Executors,BlockingQueue,synchronized,volatile,wait,notify
說明:文章學習思路:線程池---->隊列---->關鍵字---->死鎖---->線程池實戰
源碼:https://github.com/ITDragonBl...git

線程池

線程池,顧名思義存放線程的池子,能夠類比數據庫的鏈接池。由於頻繁地建立和銷燬線程會給服務器帶來很大的壓力。若能將建立的線程再也不銷燬而是存放在池中等待下一個任務使用,能夠不只減小了建立和銷燬線程所用的時間,提升了性能,同時還減輕了服務器的壓力。程序員

線程池的使用

初始化線程池有五個核心參數,分別是 corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue。還有兩個默認參數 threadFactory, handler
corePoolSize:線程池初始核心線程數。初始化線程池的時候,池內是沒有線程,只有在執行任務的時會建立線程。
maximumPoolSize:線程池容許存在的最大線程數。若超過該數字,默認提示RejectedExecutionException異常
keepAliveTime:當前線程數大於核心線程時,該參數生效,其目的是終止多餘的空閒線程等待新任務的最長時間。即指定時間內將還未接收任務的線程銷燬。
unit:keepAliveTime 的時間單位
workQueue:緩存任務的的隊列,通常採用LinkedBlockingQueue。
threadFactory:執行程序建立新線程時使用的工廠,通常採用默認值。
handler:超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序,通常採用默認值。github

線程池工做流程

開始,游泳館來了一名學員,因而館主安排一個教練負責培訓這名學員;
而後,游泳館來了六名學員,可館主只招了五名教練,因而有一名學員被安排到休息室等待;
後來,游泳館來了十六名學員,休息室已經滿了,館主覈算了開支,預計最多可招十名教練;
最後,游泳館只來了十名學員,館主對教練說,若是半天內接不到學員的教練就能夠走了;
結果,游泳館沒有學員,關閉了。
在接收任務前,線程池內是沒有線程。只有當任務來了纔開始新建線程。當任務數大於核心線程數時,任務進入隊列中等待。若隊列滿了,則線程池新增線程直到最大線程數。再超過則會執行拒絕策略。面試

線程池的三種關閉

shutdown: 線程池再也不接收任務,等待線程池中全部任務完成後,關閉線程池。經常使用
shutdownNow: 線程池再也不接收任務,忽略隊列中的任務,嘗試中斷正在執行的任務,返回未執行任務列表,關閉線程池。慎用
awaitTermination: 線程池能夠繼續接收任務,當任務都完成後,或者超過設置的時間後,關閉線程池。方法是阻塞的,考慮使用數據庫

線程池的種類

1 newSingleThreadExecutor() 單線程線程池
初始線程數和容許最大線程數都是一,keepAliveTime 也就失效了,隊列是無界阻塞隊列。該線程池的主要做用是負責緩存任務。數組

2 newFixedThreadPool(n) 固定大小線程池
初始線程數和容許最大線程數相同,且大小自定義,keepAliveTime 也就失效了,隊列是無界阻塞隊列。符合大部分業務要求,經常使用。緩存

3 newCachedThreadPool() 無緩存無界線程池
初始線程數爲零,最大線程數爲無窮大,keepAliveTime 60秒類終止空閒線程,隊列是無緩存無界隊列。適合任務數很少的場景,慎用。安全

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 線程池
 * 優點,類比數據庫的鏈接池
 * 1. 頻繁的建立和銷燬線程會給服務器帶來很大的壓力
 * 2. 若建立的線程不銷燬而是留在線程池中等待下次使用,則會很大地提升效率也減輕了服務器的壓力
 * 
 * 三種workQueue策略
 * 直接提交 SynchronousQueue
 * 無界隊列 LinkedBlockingQueue
 * 有界隊列 ArrayBlockingQueue
 * 
 * 四種拒絕策略
 * AbortPolicy : JDK默認,超出 MAXIMUM_POOL_SIZE 放棄任務拋異常 RejectedExecutionException
 * CallerRunsPolicy : 嘗試直接調用被拒絕的任務,若線程池被關閉,則丟棄任務
 * DiscardOldestPolicy : 放棄隊列最前面的任務,而後從新嘗試執被拒絕的任務。若線程池被關閉,則丟棄任務
 * DiscardPolicy : 放棄不能執行的任務但不拋異常
 */
public class ThreadPoolExecutorStu {
    
    // 線程池中初始線程個數
    private final static Integer CORE_POOL_SIZE = 3;
    // 線程池中容許的最大線程數
    private final static Integer MAXIMUM_POOL_SIZE = 8;
    // 當線程數大於初始線程時。終止多餘的空閒線程等待新任務的最長時間
    private final static Long KEEP_ALIVE_TIME = 10L;
    // 任務緩存隊列 ,即線程數大於初始線程數時先進入隊列中等待,此數字能夠稍微設置大點,避免線程數超過最大線程數時報錯。或者直接用無界隊列
    private final static ArrayBlockingQueue<Runnable> WORK_QUEUE = new ArrayBlockingQueue<Runnable>(5);
    
    public static void main(String[] args) {
        Long start = System.currentTimeMillis();
        /**
         * ITDragonThreadPoolExecutor 耗時 1503
         * ITDragonFixedThreadPool 耗時 505
         * ITDragonSingleThreadExecutor 語法問題報錯,
         * ITDragonCachedThreadPool 耗時506
         * 推薦使用自定義線程池,或newFixedThreadPool(n)
         */
        ThreadPoolExecutor threadPoolExecutor = ITDragonThreadPoolExecutor();
        for (int i = 0; i < 8; i++) {    // 執行8個任務,若超過MAXIMUM_POOL_SIZE則會報錯 RejectedExecutionException
            MyRunnableTest myRunnable = new MyRunnableTest(i);
            threadPoolExecutor.execute(myRunnable);
            System.out.println("線程池中如今的線程數目是:"+threadPoolExecutor.getPoolSize()+",  隊列中正在等待執行的任務數量爲:"+  
                    threadPoolExecutor.getQueue().size());
        }
        // 關掉線程池 ,並不會當即中止(中止接收外部的submit任務,等待內部任務完成後才中止),推薦使用。 與之對應的是shutdownNow,不推薦使用
        threadPoolExecutor.shutdown();    
        try {
            // 阻塞等待30秒關掉線程池,返回true表示已經關閉。和shutdown不一樣,它能夠接收外部任務,而且還阻塞。這裏爲了方便統計時間,因此選擇阻塞等待關閉。
            threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("耗時 : " + (System.currentTimeMillis() - start));
    }
    
    // 自定義線程池,開發推薦使用
    public static ThreadPoolExecutor ITDragonThreadPoolExecutor() {
        // 構建一個,初始線程數量爲3,最大線程數據爲8,等待時間10分鐘 ,隊列長度爲5 的線程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MINUTES, WORK_QUEUE);
        return threadPoolExecutor;
    }
    
    /**
     * 固定大小線程池
     * corePoolSize初始線程數和maximumPoolSize最大線程數同樣,keepAliveTime參數不起做用,workQueue用的是無界阻塞隊列
     */
    public static ThreadPoolExecutor ITDragonFixedThreadPool() {
        ExecutorService executor = Executors.newFixedThreadPool(8);
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
        return threadPoolExecutor;
    }
    
    /**
     * 單線程線程池
     * 等價與Executors.newFixedThreadPool(1);
     */
    public static ThreadPoolExecutor ITDragonSingleThreadExecutor() {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
        return threadPoolExecutor;
    }
    
    /**
     * 無界線程池
     * corePoolSize 初始線程數爲零
     * maximumPoolSize 最大線程數無窮大
     * keepAliveTime 60秒類將沒有被用到的線程終止
     * workQueue SynchronousQueue 隊列,無容量,來任務就直接新增線程
     * 不推薦使用
     */
    public static ThreadPoolExecutor ITDragonCachedThreadPool() {
        ExecutorService executor = Executors.newCachedThreadPool();
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
        return threadPoolExecutor;
    }
    
}

class MyRunnableTest implements Runnable {
    private Integer num;    // 正在執行的任務數
    public MyRunnableTest(Integer num) {
        this.num = num;
    }
    public void run() {
        System.out.println("正在執行的MyRunnable " + num);
        try {
            Thread.sleep(500);// 模擬執行事務須要耗時
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("MyRunnable " + num + "執行完畢");
    }
}

隊列

隊列,是一種數據結構。大部分的隊列都是以FIFO(先進先出)的方式對各個元素進行排序的(PriorityBlockingQueue是根據優先級排序的)。隊列的頭移除元素,隊列的末尾插入元素。插入的元素建議不能爲null。Queue主要分兩類,一類是高性能隊列 ConcurrentLinkedQueue;一類是阻塞隊列 BlockingQueue。本章重點介紹BlockingQueue服務器

ConcurrentLinkedQueue

ConcurrentLinkedQueue性能好於BlockingQueue。是基於連接節點的無界限線程安全隊列。該隊列的元素遵循先進先出的原則。不容許null元素。

BlockingQueue

ArrayBlockingQueue: 基於數組的阻塞隊列,在內部維護了一個定長數組,以便緩存隊列中的數據對象。並無實現讀寫分離,也就意味着生產和消費不能徹底並行。是一個有界隊列
LinkedBlockingQueue:基於列表的阻塞隊列,在內部維護了一個數據緩衝隊列(由一個鏈表構成),實現採用分離鎖(讀寫分離兩個鎖),從而實現生產者和消費者操做的徹底並行運行。是一個無界隊列,
SynchronousQueue: 沒有緩衝的隊列,生存者生產的數據直接會被消費者獲取並消費。若沒有數據就直接調用出棧方法則會報錯。

三種隊列使用場景
newFixedThreadPool 線程池採用的隊列是LinkedBlockingQueue。其優勢是無界可緩存,內部實現讀寫分離,併發的處理能力高於ArrayBlockingQueue
newCachedThreadPool 線程池採用的隊列是SynchronousQueue。其優勢就是無緩存,接收到的任務都可直接處理,再次強調,慎用!
併發量不大,服務器性能較好,能夠考慮使用SynchronousQueue。
併發量較大,服務器性能較好,能夠考慮使用LinkedBlockingQueue。
併發量很大,服務器性能沒法知足,能夠考慮使用ArrayBlockingQueue。系統的穩定最重要。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import org.junit.Test;

/**
 * 阻塞隊列
 * ArrayBlockingQueue        :有界
 * LinkedBlockingQueue       :無界
 * SynchronousQueue          :無緩衝直接用
 * 非阻塞隊列
 * ConcurrentLinkedQueue     :高性能
 */
public class ITDragonQueue {
    
    /**
     * ArrayBlockingQueue : 基於數組的阻塞隊列實現,在內部維護了一個定長數組,以便緩存隊列中的數據對象。
     * 內部沒有實現讀寫分離,生產和消費不能徹底並行,
     * 長度是須要定義的,
     * 能夠指定先進先出或者先進後出,
     * 是一個有界隊列。
     */
    @Test
    public void ITDragonArrayBlockingQueue() throws Exception {  
        ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5); // 能夠嘗試 隊列長度由3改到5  
        array.offer("offer 插入數據方法---成功返回true 不然返回false");  
        array.offer("offer 3秒後插入數據方法", 3, TimeUnit.SECONDS);  
        array.put("put 插入數據方法---但超出隊列長度則阻塞等待,沒有返回值");  
        array.add("add 插入數據方法---但超出隊列長度則提示 java.lang.IllegalStateException"); //  java.lang.IllegalStateException: Queue full  
        System.out.println(array);
        System.out.println(array.take() + " \t還剩元素 : " + array);   // 從頭部取出元素,並從隊列裏刪除,若隊列爲null則一直等待
        System.out.println(array.poll() + " \t還剩元素 : " + array);   // 從頭部取出元素,並從隊列裏刪除,執行poll 後 元素減小一個
        System.out.println(array.peek() + " \t還剩元素 : " + array);   // 從頭部取出元素,執行peek 不移除元素
    }
    
    /**
     * LinkedBlockingQueue:基於列表的阻塞隊列,在內部維護了一個數據緩衝隊列(該隊列由一個鏈表構成)。
     * 其內部實現採用讀寫分離鎖,能高效的處理併發數據,生產者和消費者操做的徹底並行運行
     * 能夠不指定長度,
     * 是一個無界隊列。
     */
    @Test
    public void ITDragonLinkedBlockingQueue() throws Exception {
        LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>();
        queue.offer("1.無界隊列");
        queue.add("2.語法和ArrayBlockingQueue差很少");
        queue.put("3.實現採用讀寫分離");
        List<String> list = new ArrayList<String>();
        System.out.println("返回截取的長度 : " + queue.drainTo(list, 2));
        System.out.println("list : " + list);
    }
    
    /**
     * SynchronousQueue:沒有緩衝的隊列,生存者生產的數據直接會被消費者獲取並消費。
     */
    @Test
    public void ITDragonSynchronousQueue() throws Exception {
        final SynchronousQueue<String> queue = new SynchronousQueue<String>();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("take , 在沒有取到值以前一直處理阻塞  : " + queue.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        Thread.sleep(2000);
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                queue.add("進值!!!");
            }
        });
        thread2.start();    
    }

    /**
     * ConcurrentLinkedQueue:是一個適合高併發場景下的隊列,經過無鎖的方式,實現了高併發狀態下的高性能,性能好於BlockingQueue。
     * 它是一個基於連接節點的無界限線程安全隊列。該隊列的元素遵循先進先出的原則。頭是最早加入的,尾是最後加入的,不容許null元素。
     * 無阻塞隊列,沒有 put 和 take 方法
     */
    @Test
    public void ITDragonConcurrentLinkedQueue() throws Exception {
        ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();  
        queue.offer("1.高性能無阻塞");
        queue.add("2.無界隊列");
        System.out.println(queue);
        System.out.println(queue.poll() + " \t  : " + queue);   // 從頭部取出元素,並從隊列裏刪除,執行poll 後 元素減小一個
        System.out.println(queue.peek() + " \t  : " + queue);   // 從頭部取出元素,執行peek 不移除元素
    }
    
}

關鍵字

關鍵字是爲了線程安全服務的,哪什麼是線程安全呢?當多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的
線程安全的兩個特性:原子性可見性。synchronized 同步,原子性。volatile 可見性。wait,notify 負責多個線程之間的通訊。

synchronized

synchronized 能夠在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或"臨界區",若一個線程想要執行synchronized修飾的代碼塊,首先要
step1 嘗試得到鎖
step2 若是拿到鎖,執行synchronized代碼體內容
step3 若是拿不到鎖,這個線程就會不斷的嘗試得到這把鎖,直到拿到爲止,並且是多個線程同時去競爭這把鎖。
注*(線程多了也就是會出現鎖競爭的問題,多個線程執行的順序是按照CPU分配的前後順序而定的,而並不是代碼執行的前後順序)

synchronized 能夠修飾方法,修飾代碼塊,這些都是對象鎖。若和static一塊兒使用,則升級爲類鎖。
synchronized 鎖是能夠重入的,當一個線程獲得了一個對象的鎖後,再次請求此對象時是能夠再次獲得該對象的鎖。鎖重入的機制,也支持在父子類繼承的場景。
synchronized 同步異步,一個線程獲得了一個對象的鎖後,其餘線程是能夠執行非加鎖的方法(異步)。可是不能執行其餘加鎖的方法(同步)。
synchronized 鎖異常,當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。

/**
 * synchronized 關鍵字,能夠修飾方法,也能夠修飾代碼塊。建議採用後者,經過減少鎖的粒度,以提升系統性能。
 * synchronized 關鍵字,若是以字符串做爲鎖,請注意String常量池的緩存功能和字符串改變後鎖是否的狀況。
 * synchronized 鎖重入,當一個線程獲得了一個對象的鎖後,再次請求此對象時是能夠再次獲得該對象的鎖。
 * synchronized 同異步,一個線程得到鎖後,另一個線程能夠執行非synchronized修飾的方法,這是異步。若另一個線程執行任何synchronized修飾的方法則須要等待,這是同步
 * synchronized 類鎖,用static + synchronized 修飾則表示對整個類進行加鎖
 */
public class ITDragonSynchronized {
    
    private void thisLock () {  // 對象鎖  
        synchronized (this) {  
            System.out.println("this 對象鎖!");  
        }  
    }  
      
    private void classLock () {  // 類鎖  
        synchronized (ITDragonSynchronized.class) {  
            System.out.println("class 類鎖!");  
        }  
    }  
      
    private Object lock = new Object();  
    private void objectLock () {  // 任何對象鎖  
        synchronized (lock) {  
            System.out.println("object 任何對象鎖!");  
        }  
    }  
      
    private void stringLock () {  // 字符串鎖,注意String常量池的緩存功能  
        synchronized ("string") { // 用 new String("string")  t4 和 t5 同時進入。用string t4完成後,t5在開始
            try {  
                for(int i = 0; i < 3; i++) {  
                    System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !");  
                    Thread.sleep(500);       
                }  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
      
    private String strLock = "lock";  // 字符串鎖改變  
    private void changeStrLock () {  
        synchronized (strLock) {  
            try {  
                System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !");  
                strLock = "changeLock";  
                Thread.sleep(500);  
                System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !");  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
    
    private synchronized void method1() {  // 鎖重入
        System.out.println("^^^^^^^^^^^^^^^^^^^^ method1");  
        method2();  
    }  
    private synchronized void method2() {  
        System.out.println("-------------------- method2");  
        method3();  
    }  
    private synchronized void method3() {  
        System.out.println("******************** method3");  
    }  
    
    private synchronized void syncMethod() {  
        try {  
            System.out.println(Thread.currentThread().getName() + " synchronized method!");  
            Thread.sleep(2000);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
      
    // 若次方法也加上了synchronized,就必須等待t1線程執行完後,t2才能調用,兩個synchronized塊之間具備互斥性,synchronized塊得到的是一個對象鎖,鎖定的是整個對象
    private void asyncMethod() {  
        System.out.println(Thread.currentThread().getName() + " asynchronized method!");  
    } 
      
    // static + synchronized 修飾則表示類鎖,打印的結果是thread1線程先執行完,而後在執行thread2線程。若沒有被static修飾,則thread1和 thread2幾乎同時執行,同時結束
    private synchronized void classLock(String args) {
        System.out.println(args + "start......");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(args + "end......");
    }
    
    public static void main(String[] args) throws Exception {  
        final ITDragonSynchronized itDragonSynchronized = new ITDragonSynchronized();  
        System.out.println("------------------------- synchronized 代碼塊加鎖 -------------------------");
        Thread thread1 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.thisLock();  
            }  
        });  
        Thread thread2 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.classLock();  
            }  
        });  
        Thread thread3 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.objectLock();  
            }  
        });  
        thread1.start();  
        thread2.start();  
        thread3.start();  
        Thread.sleep(2000);
        System.out.println("------------------------- synchronized 字符串加鎖 -------------------------");
        // 若是字符串鎖,用new String("string") t4,t5線程是能夠獲取鎖的,若是直接使用"string" ,若鎖不釋放,t5線程一直處理等待中  
        Thread thread4 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.stringLock();  
            }  
        }, "t4");  
        Thread thread5 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.stringLock();  
            }  
        }, "t5");  
        thread4.start();  
        thread5.start();  
          
        Thread.sleep(3000);
        System.out.println("------------------------- synchronized 字符串變鎖 -------------------------");
        // 字符串變了,鎖也會改變,致使t7線程在t6線程未結束後變開始執行,但一個對象的屬性變了,不影響這個對象的鎖。  
        Thread thread6 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.changeStrLock();  
            }  
        }, "t6");  
        Thread thread7 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.changeStrLock();  
            }  
        }, "t7");  
        thread6.start();  
        thread7.start(); 
        
        Thread.sleep(2000);
        System.out.println("------------------------- synchronized 鎖重入 -------------------------");
        Thread thread8 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.method1();  
            }  
        }, "t8");  
        thread8.start(); 
        Thread thread9 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                SunClass sunClass = new SunClass();  
                sunClass.sunMethod();  
            }  
        }, "t9");  
        thread9.start(); 
        
        Thread.sleep(2000);
        System.out.println("------------------------- synchronized 同步異步 -------------------------");
        Thread thread10 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.syncMethod();  
            }  
        }, "t10");  
        Thread thread11 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                itDragonSynchronized.asyncMethod();  
            }  
        }, "t11");  
        thread10.start(); 
        thread11.start(); 
        
        Thread.sleep(2000);
        System.out.println("------------------------- synchronized 同步異步 -------------------------");
        ITDragonSynchronized classLock1 = new ITDragonSynchronized();
        ITDragonSynchronized classLock2 = new ITDragonSynchronized();
        Thread thread12 = new Thread(new Runnable() {
            @Override
            public void run() {
                classLock1.classLock("classLock1");
            }
        });
        thread12.start();
        Thread thread13 = new Thread(new Runnable() {
            @Override
            public void run() {
                classLock2.classLock("classLock2");
            }
        });
        thread13.start();
    }  
    
    // 有父子繼承關係的類,若是都使用了synchronized 關鍵字,也是線程安全的。  
    static class FatherClass {  
        public synchronized void fatherMethod(){  
            System.out.println("#################### fatherMethod");  
        }  
    }  
      
    static class SunClass extends FatherClass{  
        public synchronized void sunMethod() {  
            System.out.println("@@@@@@@@@@@@@@@@@@@@ sunMethod");  
            this.fatherMethod();
        }  
    }  
}

volatile

volatile 關鍵字雖然不具有synchronized關鍵字的原子性(同步)但其主要做用就是使變量在多個線程中可見。也就是可見性。
用法很簡單,直接用來修飾變量。由於其不具有原子性,能夠用Atomic類代替。美中不足的是多個Atomic類也不具有原子性,因此還須要synchronized來修飾。
volatile 關鍵字工做原理
每一個線程都有本身的工做內存,若是線程須要用到一個變量的時,會從主內存拷貝一份到本身的工做內存中。從而提升了效率。每次執行完線程後再將變量從工做內存同步回主內存中。
這樣就存在一個問題,變量在不一樣線程中可能存在不一樣的值。若是用volatile 關鍵字修飾變量,則會讓線程的執行引擎直接從主內存中獲取值。
volatile關鍵字

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * volatile 關鍵字主要做用就是使變量在多個線程中可見。
 * volatile 關鍵字不具有原子性,但Atomic類是具有原子性和可見性。
 * 美中不足的是多個Atomic類不具有原子性,仍是須要synchronized 關鍵字幫忙。
 */
public class ITDragonVolatile{
    
    private volatile boolean flag = true;  
    private static volatile int count;   
    private static AtomicInteger atomicCount = new AtomicInteger(0); // 加 static 是爲了不每次實例化對象時初始值爲零
    
    //    測試volatile 關鍵字的可見性
    private void volatileMethod() {
        System.out.println("thread start !");  
        while (flag) {  // 若是flag爲true則一直處於阻塞中,
        }  
        System.out.println("thread end !");
    }
    
    //    驗證volatile 關鍵字不具有原子性
    private int volatileCountMethod() {
        for (int i = 0; i < 10; i++) {
            // 第一個線程還未將count加到10的時候,就可能被另外一個線程開始修改。可能會致使最後一次打印的值不是1000
            count++ ;    
        }  
        return count;
    }
    
    //    驗證Atomic類具備原子性
    private int atomicCountMethod() {
        for (int i = 0; i < 10; i++) {  
            atomicCount.incrementAndGet();  
        }  
        // 若最後一次打印爲1000則表示具有原子性,中間打印的信息多是受println延遲影響。
        return atomicCount.get();// 若最後一次打印爲1000則表示具有原子性
    }
    
    // 驗證多個 Atomic類操做不具有原子性,加synchronized關鍵字修飾便可
    private synchronized int multiAtomicMethod(){
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        atomicCount.addAndGet(1);
        atomicCount.addAndGet(2);
        atomicCount.addAndGet(3);
        atomicCount.addAndGet(4); 
        return atomicCount.get(); //若具有原子性,則返回的結果必定都是10的倍數,需屢次運行才能看到結果
    }
  
    /**
     * volatile 關鍵字可見性緣由
     * 這裏有兩個線程    :一個是main的主線程,一個是thread的子線程
     * jdk線程工做流程    :爲了提升效率,每一個線程都有一個工做內存,將主內存的變量拷貝一份到工做內存中。線程的執行引擎就直接從工做內存中獲取變量。
     * So 問題來了        :thread線程用的是本身的工做內存,主線程將變量修改後,thread線程不知道。這就是數據不可見的問題。
     * 解決方法        :變量用volatile 關鍵字修飾後,線程的執行引擎就直接從主內存中獲取變量。
     * 
     */
    public static void main(String[] args) throws InterruptedException {  
//        測試volatile 關鍵字的可見性
        /*ITDragonVolatile itDragonVolatile = new ITDragonVolatile();  
        Thread thread = new Thread(itDragonVolatile);
        thread.start();
        Thread.sleep(1000);  // 等線程啓動了,再設置值
        itDragonVolatile.setFlag(false);  
        System.out.println("flag : " + itDragonVolatile.isFlag());*/  
        
//        驗證volatile 關鍵字不具有原子性 和 Atomic類具備原子性
        final ITDragonVolatile itDragonVolatile = new ITDragonVolatile();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 100; i++) {
            threads.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    // 中間打印的信息多是受println延遲影響,請看最後一次打印的結果
                    System.out.println(itDragonVolatile.multiAtomicMethod());
                }
            }));
        }
        for(Thread thread : threads){
            thread.start();
        }
    }  
      
    public boolean isFlag() {  
        return flag;  
    }  
  
    public void setFlag(boolean flag) {  
        this.flag = flag;  
    }  

}

wait,notify

使用 wait/ notify 方法實現線程間的通訊,模擬BlockingQueue隊列。有兩點須要注意:
1)wait 和 notify 必需要配合 synchronized 關鍵字使用
2)wait方法是釋放鎖的, notify方法不釋放鎖。
線程通訊概念:線程是操做系統中獨立的個體,但這些個體若是不通過特殊的處理,就不能成爲一個總體,線程之間的通訊就成爲總體的必用方法之一。

import java.util.LinkedList;  
import java.util.concurrent.atomic.AtomicInteger;

/**
 * synchronized 能夠在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或"臨界區",通常給代碼塊加鎖,經過減少鎖的粒度從而提升性能。
 * Atomic* 是爲了彌補volatile關鍵字不具有原子性的問題。雖然一個Atomic*對象是具有原子性的,但不能確保多個Atomic*對象也具有原子性。
 * volatile 關鍵字不具有synchronized關鍵字的原子性其主要做用就是使變量在多個線程中可見。
 * wait / notify
 * wait() 使線程阻塞運行,notify() 隨機喚醒等待隊列中等待同一共享資源的一個線程繼續運行,notifyAll() 喚醒全部等待隊列中等待同一共享資源的線程繼續運行。
 * 1)wait 和 notify 必需要配合 synchronized 關鍵字使用
 * 2)wait方法是釋放鎖的, notify方法不釋放鎖
 */
public class ITDragonMyQueue {
    
    //1 須要一個承裝元素的集合   
    private LinkedList<Object> list = new LinkedList<Object>();  
    //2 須要一個計數器 AtomicInteger (保證原子性和可見性)
    private AtomicInteger count = new AtomicInteger(0);  
    //3 須要制定上限和下限  
    private final Integer minSize = 0;  
    private final Integer maxSize ;  
      
    //4 構造方法  
    public ITDragonMyQueue(Integer size){  
        this.maxSize = size;  
    }  
      
    //5 初始化一個對象 用於加鎖  
    private final Object lock = new Object();  
      
    //put(anObject): 把anObject加到BlockingQueue裏,若是BlockQueue沒有空間,則調用此方法的線程被阻斷,直到BlockingQueue裏面有空間再繼續.  
    public void put(Object obj){  
        synchronized (lock) {  
            while(count.get() == this.maxSize){  
                try {  
                    lock.wait();          // 當Queue沒有空間時,線程被阻塞 ,這裏爲了區分,命名爲wait1
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
            list.add(obj);              //1 加入元素  
            count.incrementAndGet();      //2 計數器累加  
            lock.notify();              //3 新增元素後,通知另一個線程wait2,隊列多了一個元素,能夠作移除操做了。 
            System.out.println("新加入的元素爲: " + obj);  
        }  
    }  
      
    //take: 取走BlockingQueue裏排在首位的對象,若BlockingQueue爲空,阻斷進入等待狀態直到BlockingQueue有新的數據被加入.  
    public Object take(){  
        Object ret = null;  
        synchronized (lock) {  
            while(count.get() == this.minSize){  
                try {  
                    lock.wait();          // 當Queue沒有值時,線程被阻塞 ,這裏爲了區分,命名爲wait2
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
            ret = list.removeFirst();      //1 作移除元素操做  
            count.decrementAndGet();      //2 計數器遞減  
            lock.notify();              //3 移除元素後,喚醒另一個線程wait1,隊列少元素了,能夠再添加操做了  
        }  
        return ret;  
    }  
      
    public int getSize(){  
        return this.count.get();  
    }  
      
    public static void main(String[] args) throws Exception{  
        final ITDragonMyQueue queue = new ITDragonMyQueue(5);  
        queue.put("a");  
        queue.put("b");  
        queue.put("c");  
        queue.put("d");  
        queue.put("e");  
        System.out.println("當前容器的長度: " + queue.getSize());  
        Thread thread1 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                queue.put("f");  
                queue.put("g");  
            }  
        },"thread1");  
        Thread thread2 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                System.out.println("移除的元素爲:" + queue.take());  // 移除一個元素後再進一個,而並不是同時移除兩個,進入兩個元素。
                System.out.println("移除的元素爲:" + queue.take());  
            }  
        },"thread2");  
        thread1.start();  
        Thread.sleep(2000);
        thread2.start();  
    }  
}

死鎖

死鎖是一個很糟糕的狀況,鎖遲遲不能解開,其餘線程只能一直處於等待阻塞狀態。好比線程A擁有鎖一,卻還想要鎖二。線程B擁有鎖二,卻還想要鎖一。兩個線程各執己見,兩個線程將永遠等待。
排查:
第一步:控制檯輸入jps用於得到當前JVM進程的pid
第二步:jstack pid 用於打印堆棧信息
第三步:解讀,"Thread-1" 是線程的名字,prio 是線程的優先級,tid 是線程id, nid 是本地線程id, waiting to lock 等待去獲取的鎖,locked 本身擁有的鎖。

"Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37)
        - waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object)
        - locked <0x00000000ecfdf9e0> (a java.lang.Object)
        at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54)
        at java.lang.Thread.run(Thread.java:748)
/**
 * 死鎖: 線程A擁有鎖一,卻還想要鎖二。線程B擁有鎖二,卻還想要鎖一。兩個線程各執己見,兩個線程將永遠等待。
 * 避免: 在設計階段,瞭解鎖的前後順序,減小鎖的交互數量。
 * 排查: 
 * 第一步:控制檯輸入 jps 用於得到當前JVM進程的pid
 * 第二步:jstack pid 用於打印堆棧信息 
 * "Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
 * - waiting to lock <0x00000000ecfdf9d0> - locked <0x00000000ecfdf9e0> 
 * "Thread-0" #10 prio=5 os_prio=0 tid=0x0000000055ff0800 nid=0x1b14 waiting for monitor entry [0x0000000056c7f000]
 * - waiting to lock <0x00000000ecfdf9e0> - locked <0x00000000ecfdf9d0> 
 * 能夠看出,兩個線程持有的鎖都是對方想要獲得的鎖(得不到的永遠在騷動),並且最後一行也打印了 Found 1 deadlock.
 */
public class ITDragonDeadLock {
    
    private final Object left = new Object();
    private final Object right = new Object();
    
    public void leftRight(){
        synchronized (left) {
            try {
                Thread.sleep(2000); // 模擬持有鎖的過程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (right) {
                System.out.println("leftRight end!");
            }
        }
    }
    
    public void rightLeft(){
        synchronized (right) {
            try {
                Thread.sleep(2000); // 模擬持有鎖的過程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (left) {
                System.out.println("rightLeft end!");
            }
        }
    }
    
    public static void main(String[] args) {
        ITDragonDeadLock itDragonDeadLock = new ITDragonDeadLock();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                itDragonDeadLock.leftRight();
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                itDragonDeadLock.rightLeft();
            }
        });
        thread2.start();
    }

}

多線程案例

如有Thread一、Thread二、Thread三、Thread4四條線程分別統計C、D、E、F四個盤的大小,全部線程都統計完畢交給Thread5線程去作彙總,應當如何實現

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 如有Thread一、Thread二、Thread三、Thread4四條線程分別統計C、D、E、F四個盤的大小,全部線程都統計完畢交給Thread5線程去作彙總,應當如何實現?
 * 思考:彙總,說明要把四個線程的結果返回給第五個線程,若要線程有返回值,推薦使用callable。Thread和Runnable都沒返回值
 */
public class ITDragonThreads {  
    
    public static void main(String[] args) throws Exception {  
        // 無緩衝無界線程池
        ExecutorService executor = Executors.newFixedThreadPool(8); 
        // 相對ExecutorService,CompletionService能夠更精確和簡便地完成異步任務的執行
        CompletionService<Long> completion = new ExecutorCompletionService<Long>(executor);  
        CountWorker countWorker = null;  
        for (int i = 0; i < 4; i++) { // 四個線程負責統計
            countWorker = new CountWorker(i+1);  
            completion.submit(countWorker);  
        }  
        // 關閉線程池
        executor.shutdown();  
        // 主線程至關於第五個線程,用於彙總數據
        long total = 0;  
        for (int i = 0; i < 4; i++) { 
            total += completion.take().get(); 
        }  
        System.out.println(total / 1024 / 1024 / 1024 +"G");  
    }  
}  
  
class CountWorker implements Callable<Long>{  
    private Integer type;  
    public CountWorker() {
    }
    public CountWorker(Integer type) {
        this.type = type;
    }

    @Override  
    public Long call() throws Exception {  
        ArrayList<String> paths = new ArrayList<>(Arrays.asList("c:", "d:", "e:", "f:"));
        return countDiskSpace(paths.get(type - 1));  
    }  
    
    // 統計磁盤大小
    private Long countDiskSpace (String path) {  
        File file = new File(path);  
        long totalSpace = file.getTotalSpace();  
        System.out.println(path + " 總空間大小 : " + totalSpace / 1024 / 1024 / 1024 + "G");  
        return totalSpace;
    }  
}

查考面試題

1 常見建立線程的方式和其優缺點
(1)繼承Thread類 (2)實現Runnable接口
優缺點:實現一個接口比繼承一個類要靈活,減小程序之間的耦合度。缺點就是代碼多了一點。

2 start()方法和run()方法的區別
start方法能夠啓動線程,而run方法只是thread的一個普通方法調用。

3 多線程的做用
(1)發揮多核CPU的優點,提升CPU的利用率(2)防止阻塞,提升效率

4 什麼是線程安全
當多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的。

5 線程安全級別
(1)不可變(2)絕對線程安全(3)相對線程安全(4)線程非安全

6 如何在兩個線程之間共享數據
線程之間數據共享,其實能夠理解爲線程之間的通訊,能夠用wait/notify/notifyAll 進行等待和喚醒。

7 用線程池的好處
避免頻繁地建立和銷燬線程,達到線程對象的重用,提升性能,減輕服務器壓力。使用線程池還能夠根據項目靈活地控制併發的數目。

8 sleep方法和wait方法有什麼區別
sleep方法和wait方法均可以用來放棄CPU必定的時間,sleep是thread的方法,不會釋放鎖。wait是object的方法,會釋放鎖。

總結

1 線程池核心參數有 初始核心線程數,線程池運行最大線程數,空閒線程存活時間,時間單位,任務隊列。
2 隊列是一種數據結構,主要有兩類 阻塞隊列BlockingQueue,和非阻塞高性能隊列ConcurrentLinkedQueue。
3 線程安全的兩個特性,原子性和可見性。synchronized 關鍵字具有原子性。volatile 關鍵字具有可見性。
4 單個Atomic類具有原子性和可見性,多個Atomic類不具有原子性,須要synchronized 關鍵字修飾。
5 兩個線程持有的鎖都是對方想要獲得的鎖時容易出現死鎖的狀況,從設計上儘可能減小鎖的交互。

本章到這裏就結束了,涉及的知識點比較多,請參考流程圖來學習。若有什麼問題能夠指出。喜歡的朋友能夠點個"推薦"

相關文章
相關標籤/搜索