一文看懂JUC多執行緒及高併發

蔡不菜丿發表於2020-04-05

本文主要介紹JUC多執行緒以及高併發

本文較長,可收藏,勿吃塵

如有需要,可以參考

如有幫助,不忘 點贊


一、Volatile

volatile 是Java虛擬機器提供的輕量級的同步機制

1)保證可見性
  • JMM模型的執行緒工作: 各個執行緒對主記憶體中共享變數X的操作都是各個執行緒各自拷貝到自己的工作記憶體操作後再協會主記憶體中。
  • 存在的問題: 如果一個執行緒A 修改了共享變數X的值還未寫回主記憶體,這是另外一個執行緒B又對記憶體中的一個共享變數X進行操作,但是此時執行緒A工作記憶體中的共享變數對執行緒B來說事並不可見的。這種工作記憶體與主記憶體延遲的現象就會造成了可見性的問題。
  • 解決(volatile): 當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值
public class Volatile{
    public static void main(Stirng[] args){
        testVolatile();
    }
    
    public static void testVolatile(){
       Test test = new Test();
        //第一個執行緒
        new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t first thread");
            try {
                //暫停3s
                TimeUnit.SECONDS.sleep(3);
                test.changeNum();
         		System.out.println(Thread.currentThread().getName() + "\t current value:" + test.n);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "threadAAA").start();
        //第二個執行緒是main執行緒
        while (test.n == 0) {
            //如果myData的num一直為零,main執行緒一直在這裡迴圈
			//System.out.println(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t now value:" + test.n);
    }
}
class Test{
    //int n = 0; //沒有加volatile 不保證可見性
    volatile int n = 0; //保證可見性
    public void changeNum(){
        this.n = 1;
    }
}
複製程式碼
2)不保證原子性
  • 原子性: 不可分割、完整性,即某個執行緒正在做某個具體業務時,中間不可以被加塞或者被分割,需要整體完整,要麼同時成功,要麼同時失敗
  • 解決方法:
  1. 加入synchronized
  2. 使用JUC下的AtomicInteger
public class Volatile {
    public static void main(String[] args) {
        atomicByVolatile();//驗證volatile不保證原子性
    }
    public static void atomicByVolatile(){
        Test test= new Test();
        for(int i = 1; i <= 20; i++){
            new Thread(() ->{
                for(int j = 1; j <= 1000; j++){
                    test.addSelf();
                    test.atomicAddSelf();
                }
            },"Thread "+i).start();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally num value is "+test.n);
        System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+test.atomicInteger);
    }
}
class Test  {
    volatile int n = 0;
    public void addSelf(){
        n++;
    }
    AtomicInteger atomicInteger = new AtomicInteger();//預設值為0
    public void atomicAddSelf(){
        atomicInteger.getAndIncrement();
    }
}
//列印結果:
/**
	main	 finally num value is 18864			**不保證原子性**
	main	 finally atomicnum value is 20000	**保證原子性**
*/
複製程式碼
3)禁止指令重排
  • 指令重排: 多執行緒環境中執行緒交替執行,由於編譯器優化重排的存在,兩個執行緒中使用的變數能否保證一致性是無法確定的,結果無法預測。
  • 指令重排過程: 原始碼 -> 編輯器優化的重排 -> 指令並行的重排 -> 記憶體系統的重排 ->最終執行的指令
  • 記憶體屏障作用:
  1. 保證特定操作的執行順序
  2. 保證某些變數的記憶體可見性(利用該特性實現volatile的記憶體可見性)

二、CAS

1)什麼是CAS
  1. CAS 全稱 => Compare-And-Set , 它是一條CPU併發源語
  2. 他的功能就是判斷記憶體某個位置的值是否為預期值,如果是則更新為新的值,這個過程是原子的。
  3. CAS併發源語體現在Java語言中就是sun.miscUnSafe類中的各個方法,呼叫UnSafe類中的CAS方法,JVM會幫我實現CAS彙編指令,這是一種完全依賴於硬體功能,通過它實現了原子操作,再次強調,由於CAS是一種系統源語,源語屬於作業系統用於範疇,是由若干個指令組成,用於完成某個功能的一個過程,並且源語的執行必須是連續的,在執行過程中不允許中斷,也即是說CAS是一條原子指令,不會造成所謂的資料不一致的問題
public class CASDemo{
	public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger();
        System.out.println(atomicInteger.compareAndSet(0,5));       //true
        System.out.println(atomicInteger.compareAndSet(0,2));       //false
        System.out.println(atomicInteger);                          //5
    }
}
複製程式碼
2)CAS原理
	public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);		//引出問題=》何為unsafe
    }
複製程式碼
3)何為UnSafe
  1. UnSafe是CAS的核心類,由於Java方法無法直接訪問底層,需要通過本地(native)方法來訪問,UnSafe相當於一個後面,基於該類可以直接操作額定的記憶體資料。UnSafe類在於sun.misc包中。其中內部方法可以向C的指標一樣直接操作記憶體,因為Java中CAS操作的助興依賴於UnSafe類的方法
  2. 變數 ValueOffset , 便是該變數在記憶體中偏移地址,因為UnSafe就是根據記憶體偏移地址來獲取資料的。
  3. 變數 value 和 volatile 修飾,保證了多執行緒之間的可見性。
4)CAS缺點
  1. 迴圈時間開銷很大
/**CAS中有個do while 方法 :如果CAS失敗,會一直進行嘗試,如果CAS長時間一直不成功,會給CPU帶來很大的開銷*/
 public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
複製程式碼
  1. 只能保證一個共享變數的原子性 當對一個共享變數執行操作的時候,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
  2. 存在ABA問題
5)ABA問題
  • 何為ABA問題: 在一個時間差的時段內會造成資料的變化。比如說一個執行緒AA從記憶體中取走A,這個時候另一個執行緒BB也從記憶體中取走A,這個時候A的值為X,然後執行緒BB將A的值改為Y,過一會又將A的值改為X,這個時候執行緒AA回來進行CAS操作發現記憶體中A的值仍然是X,因此執行緒AA操作成功。但是儘管執行緒AA的CAS操作成功,但是不代表這個過程就是沒問題的
  • 原子引用
public class ABADemo {
    public static void main(String[] args) {
        User u1 = new User("u1",18);
        User u2 = new User("u2",19);
        AtomicReference<User> atomicReference = new AtomicReference(u1);
        System.out.println(atomicReference.compareAndSet(u1,u2)+"\t"+atomicReference.get().getName());
        System.out.println(atomicReference.compareAndSet(u1,u2)+"\t"+atomicReference.get().getName());
    }
}

class User {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
	//省略get/set方法
}
複製程式碼
  • 解決(時間戳原子引用:AtomicStampedReference
 public static void main(String[] args) {
 
        System.out.println("====存在ABA問題");
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "執行緒A").start();
        new Thread(() -> {
            try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
            System.out.println(atomicReference.compareAndSet(100, 102) + "\t" + atomicReference.get());
        }, "執行緒2").start();
        
        System.out.println("====通過時間戳原子引用解決ABA問題====");
         new Thread(()->{
             int stamp1 = atomicStampedReference.getStamp();
             System.out.println(Thread.currentThread().getName()+"===第一次版本號:"+stamp1+"===值:"+atomicStampedReference.getReference());
             try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
             atomicStampedReference.compareAndSet(100,101,stamp1,stamp1+1); /**期望值,新值,期望版本號,新版本號**/
             int stamp2 = atomicStampedReference.getStamp();
             System.out.println(Thread.currentThread().getName()+"===第二次版本號:"+stamp2+"===值:"+atomicStampedReference.getReference());
             atomicStampedReference.compareAndSet(101,100,stamp2,stamp2+1);
             int stamp3 = atomicStampedReference.getStamp();
             System.out.println(Thread.currentThread().getName()+"===第三次版本號:"+stamp3+"===值:"+atomicStampedReference.getReference());
         },"執行緒3").start();

          new Thread(()->{
              int stamp4 = atomicStampedReference.getStamp();
              System.out.println(Thread.currentThread().getName()+"===第一次版本號:"+stamp4+"===值:"+atomicStampedReference.getReference());
              try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
              boolean result = atomicStampedReference.compareAndSet(100, 101, stamp4, stamp4 + 1);
              System.out.println(Thread.currentThread().getName()+"===是否修改成功:"+result+"===當前版本:"+atomicStampedReference.getStamp());
              System.out.println("===當前最新值:"+atomicStampedReference.getReference());
          },"執行緒4").start();
    }
複製程式碼

三、集合類不安全問題

1)故障現象

出現java.util.ConcurrentModificationException異常

在這裡插入圖片描述

2) 導致原因

併發爭搶修改導致

	public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
             new Thread(()->{
                 stringList.add(UUID.randomUUID().toString().substring(0,8));
                 System.out.println(stringList);
             },"執行緒"+i).start();
        }
    }
複製程式碼
3)解決方法
  • Vector :執行緒安全
  • Collections.synchronizedList(new ArrayList<>())
  • new CopyOnWriteArrayList<>()
    • List執行緒:new CopyOnWriteArrayList<>();
    • Set執行緒:new CopyOnWriteArraySet<>();
    • Set執行緒:ConcurrentHashMap();

四、鎖

1)公平鎖/非公平鎖

  • 定義:

    公平鎖: 是指多個執行緒按照申請鎖的順序來獲取鎖,類似於排隊,FIFO規則 非公平鎖: 是指在多執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲到鎖,在高併發的情況下,有可能造成優先順序反轉或者飢餓現象。

  • 兩者的區別:

    併發包ReentrantLock的建立可以指定函式的boolean型別來得到公平鎖或者非公平鎖,預設是非公平鎖

    公平鎖: 就是很公平,在併發環境中,每個執行緒在獲取鎖時會先檢視此鎖維護的等待佇列,如果為空,或者當前執行緒是等待佇列的第一個,就佔有鎖,否則就會加入到等待佇列中,以後會按照FIFO的規則從佇列中抽取到自己。 非公平鎖: 非公平鎖比較粗魯,上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖的那種方式。

    就 Java ReentrantLock 而言,通過建構函式指定該鎖是否是公平鎖, 預設 非公平鎖 ,非公平鎖的優點在於吞吐量比公平鎖大,就 synchronized 而言,它是一種非公平鎖。

2)可重入鎖(遞迴鎖)

  • 可重入鎖也稱之為遞迴鎖,指定是同一個執行緒外層函式獲得鎖之後,內層遞迴函式仍然能獲取該鎖的程式碼,在同一個執行緒在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。也就是說, 執行緒可以進入任何一個它已經擁有的鎖所同步著的程式碼塊

ReentrantLock/syschronized 就是一個典型的可重入鎖

  • ReentrantLock 舉例
public class ReenterLockDemo {
    public static void main(String[] args) {
        Rld rld = new Rld();
        Thread thread1 = new Thread(rld,"t1");
        Thread thread2 = new Thread(rld,"t2");
        thread1.start();
        thread2.start();
    }
}
class Rld implements Runnable {
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        get();
    }
    private void get() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t====get方法");
            set();
        } finally {
            lock.unlock();
        }

    }
    private void set() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t====set方法");
        } finally {
            lock.unlock();
        }
    }
}
//列印結果
/**
	t1	====get方法
	t1	====set方法
	t2	====get方法
	t2	====set方法
*/
複製程式碼
  • syschronized 舉例
public class ReenterLockDemo {
    public synchronized static void sendMsg(){
        System.out.println(Thread.currentThread().getName()+"\t"+"傳送簡訊");
        sendEmail();
    }
    public synchronized static void sendEmail(){
        System.out.println(Thread.currentThread().getName()+"\t"+"傳送郵件");
    }
    public static void main(String[] args) {
         new Thread(()->{
             sendMsg();
         },"t1").start();
        new Thread(()->{
            sendMsg();
        },"t2").start();
    }
}
//列印結果
/**
	t1	傳送簡訊
	t1	傳送郵件
	t2	傳送簡訊
	t2	傳送郵件
*/
複製程式碼

3)自旋鎖

  • 是指嘗試獲取鎖的執行緒不會立即阻塞,而是採用迴圈的方式去嘗試獲取鎖,這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗CPU。

上面的CAS問題中的unsafe 用到的就是自旋鎖。

	public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
複製程式碼
  • 例子
	/**
 * 實現自旋鎖
 * 自旋鎖好處,迴圈比較獲取知道成功位置,沒有類似wait的阻塞
 *
 * 通過CAS操作完成自旋鎖,A執行緒先進來呼叫mylock方法自己持有鎖5秒鐘,B隨後進來發現當前有執行緒持有鎖,不是null,所以只能通過自旋等待,知道A釋放鎖後B隨後搶到
 */
public class SpinLockDemo {
    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();
        new Thread(() -> {
            spinLockDemo.mylock();
            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        }, "Thread 1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (Exception e){
            e.printStackTrace();
        }

        new Thread(() -> {
            spinLockDemo.mylock();
            spinLockDemo.myUnlock();
        }, "Thread 2").start();
    }

    //原子引用執行緒
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    public void mylock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "\t come in");
        while (!atomicReference.compareAndSet(null, thread)) {
            System.out.println(Thread.currentThread().getName()+"wait...");
        }
    }

    public void myUnlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()");
    }
}
//列印結果
/**
	Thread 1	 come in
	Thread 2	 come in
	Thread 2	 wait...
	...		//會一直列印 Thread 2  wait... ,知道Thread 1 解鎖,然後會自己自旋
	Thread 1	 invoked myunlock()
	Thread 2	 invoked myunlock()
*/
複製程式碼

4)獨佔鎖(寫)/共享鎖(讀)/互斥鎖

  • 獨佔鎖: 指該鎖一次只能被一個執行緒所持有。對ReentrantLock和Synchronize而言都是獨佔鎖。
  • 共享鎖: 指該鎖可被多個執行緒所持有。

對ReentrantReadWriteLock而言,其讀鎖是共享鎖,其寫鎖是獨佔鎖。讀鎖的共享鎖可以保證併發度是非常高效的。讀寫,寫讀,寫寫的過程是互斥的。

  • 例子:
public class ReadWriteLockDemo {
    /**
     * 多執行緒同時操作,模擬高併發
     * 讀取共享資源應該同時進行(共享)
     * 如果有一個執行緒想去寫共享資源,就不應該有其他執行緒可以對該共享資源進行讀寫(獨佔)
     */
    public static void main(String[] args) {
        MyCache cache = new MyCache();
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                cache.put(temp + "", temp + "");
            }).start();
        }
        for (int i = 0; i < 5; i++) {
            final int temp = i;
            new Thread(() -> {
                cache.get(temp + "");
            }).start();
        }
    }
}
class MyCache {
    /**保證可見性*/
    private volatile Map<String, Object> map = new HashMap<>();

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();

    /**寫操作*/
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在寫入...");
            //模擬網路延遲
            try { TimeUnit.MICROSECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 寫入完成");
        } finally {
            writeLock.unlock();
        }
    }

    /**讀操作*/
    public void get(String key) {
        try {
            readLock.lock();
            System.out.println(Thread.currentThread().getName() + " 正在讀...");
            //模擬網路延遲
            try {TimeUnit.MICROSECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}
            Object res = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 讀取完成 :" + res);
        } finally {
            readLock.unlock();
        }
    }
}
//列印結果
/**
	Thread-0 正在寫入...
	Thread-0 寫入完成
	Thread-1 正在寫入...
	Thread-1 寫入完成
	Thread-2 正在寫入...
	Thread-2 寫入完成
	Thread-3 正在寫入...
	Thread-3 寫入完成
	Thread-4 正在寫入...
	Thread-4 寫入完成
	Thread-5 正在讀...
	Thread-6 正在讀...
	Thread-7 正在讀...
	Thread-9 正在讀...
	Thread-8 正在讀...
	Thread-5 讀取完成 :0
	Thread-9 讀取完成 :4
	Thread-6 讀取完成 :1
	Thread-8 讀取完成 :3
	Thread-7 讀取完成 :2
*/
複製程式碼

5)CountDownLatch

  • 讓一些執行緒阻塞直到另外一些執行緒完成後才別喚醒
  • CountDownLatch主要有兩個方法,當一個或多個執行緒呼叫await 方法時,呼叫執行緒會被阻塞,其他執行緒呼叫countDown 方法計數器減1(呼叫countDown 方法時執行緒不會阻塞),當計數器的值變為0,因呼叫await 方法被阻塞的執行緒會被喚醒,進而繼續執行。
  • 關鍵點: 1)await() 方法 2) countDown() 方法
  • 例子:一個教室有1個班長和若干個學生,班長要等所有學生都走了才能關門,那麼要如何實現。
//沒有使用CountDownLatch
public class CountDownLanchDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " 離開了教室...");
            }, i+"號學生").start();
        }
        System.out.println("========班長鎖門========");
    }

}
//列印結果
/**
	0號學生 離開了教室...
	4號學生 離開了教室...
	3號學生 離開了教室...
	2號學生 離開了教室...
	========班長鎖門========
	1號學生 離開了教室...
	5號學生 離開了教室...
*/
//可以看出班長走之後還有兩個學生被鎖在了教室

//=====解決方法=====
public static void main(String[] args) {
	   try {
	        CountDownLatch countDownLatch = new CountDownLatch(6);
	        for (int i = 0; i < 6; i++) {
	            new Thread(() -> {
	                countDownLatch.countDown();
	                System.out.println(Thread.currentThread().getName() + " 離開了教室...");
	            }, i + "號學生").start();
	        }
	        countDownLatch.await(); //這裡相當於擋住,在countDownLatch還沒有變0之前不能執行以下方法
	        System.out.println("========班長鎖門========");
	    } catch (InterruptedException e) {
	        e.printStackTrace();
	    }
}
//列印結果
/**
	0號學生 離開了教室...
	3號學生 離開了教室...
	2號學生 離開了教室...
	1號學生 離開了教室...
	4號學生 離開了教室...
	5號學生 離開了教室...
	========班長鎖門========
*/
//可以看出班長等學生都走了才鎖門
複製程式碼

6)CyclicBarrier

  • CyclicBarrier 的字面意思是可迴圈(Cyclic)使用的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫做同步點)時被阻塞,知道最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活,執行緒進入屏障通過CyclicBarrier的await()方法。
  • 例子:跟上面一樣,一個班級有六個學生,要等學生都離開後班長才能關門。
public static void main(String[] args) {
        CyclicBarrier cyclicBarrie = new CyclicBarrier(6, () -> {
            System.out.println("班長鎖門離開教室...");
        });

        for (int i = 0; i < 6; i++) {
            final int temp = i;
            new Thread(() -> {
                System.out.println("離開教室...");
                try {
                    cyclicBarrie.await();     //呼叫一次內部就會加1,與上面6呼應,等到6的時候就可以執行上面班長離開的方法
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, temp + "號學生").start();
        }
    }
 //列印結果
/**
	0號學生離開教室...
	4號學生離開教室...
	3號學生離開教室...
	2號學生離開教室...
	1號學生離開教室...
	5號學生離開教室...
	班長鎖門離開教室...
*/
//可以看出班長等學生都走了才鎖門
複製程式碼

CountDownLatch 和 CyclicBarrier 其實是相反的操作,一個是相減到0開始執行,一個是相加到指定值開始執行

7)Semaphore

  • 訊號量的主要使用者兩個目的,一個是用於共享資源的相互排斥使用 ,另一個是用於併發資源數的控制
  • 例子:搶車位問題,此時有六部車輛,但是隻有三個車位的問題。
public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(3); //模擬三個車位
        /**模擬六輛車*/
        for (int i = 1; i <= 6; i++) {
             new Thread(()->{
                 try {
                     semaphore.acquire();   //搶到車位  這時候只能進來三輛車,超過三輛車進不來,等待有車輛離開
                     System.out.println(Thread.currentThread().getName()+"\t 搶到車位");
                     try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
                     System.out.println(Thread.currentThread().getName()+"\t 停車2秒後,離開車位");
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 } finally {
                     semaphore.release();   //釋放車位資源
                 }
             },i + "號車輛").start();
        }
    }
//列印結果
/**
	1號車輛	 搶到車位
	3號車輛	 搶到車位
	2號車輛	 搶到車位
	2號車輛	 停車2秒後,離開車位
	1號車輛	 停車2秒後,離開車位
	3號車輛	 停車2秒後,離開車位
	4號車輛	 搶到車位
	5號車輛	 搶到車位
	6號車輛	 搶到車位
	6號車輛	 停車2秒後,離開車位
	5號車輛	 停車2秒後,離開車位
	4號車輛	 停車2秒後,離開車位
*/
複製程式碼

五、阻塞佇列

概念: 阻塞佇列,拆分為“阻塞”和“佇列”,所謂阻塞,在多執行緒領域,某些情況下會颳起執行緒(即執行緒阻塞),一旦條件滿足,被掛起的執行緒優先被自動喚醒。

在這裡插入圖片描述
Tread 1 往阻塞佇列中新增元素,Thread 2 往阻塞佇列中移除元素

  1. 當阻塞佇列是空時,從佇列中獲取元素的操作將會被阻塞。
  2. 當阻塞佇列是滿時,從佇列中新增元素的操作將會被阻塞。

1) 種類

  1. ArrayBlockingQueue: 是一個基於陣列結構 的有界阻塞佇列,此佇列按照FIFO(先進先出)規則排序。
  2. LinkedBlockingQueue: 是一個基於連結串列結構的有界阻塞佇列(大小預設值為Integer.MAX_VALUE),此佇列按照FIFO(先進先出)對元素進行排序,吞吐量通常要高於ArrayBlockingQueue。
  3. SynchronusQueue: 是一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue。
  4. PriorityBlockingQueue:支援優先順序排序的無界阻塞佇列
  5. DelayQueue:使用優先順序佇列實現的延遲無界阻塞佇列。
  6. LinkedTransferQueue:由連結串列結構組成的無界阻塞佇列。

吞吐量:SynchronusQueue > LinkedBlockingQueue > ArrayBlockingQueue

2) 使用好處

我們不需要關心什麼時候胡需要阻塞執行緒,什麼時候需要喚醒執行緒,因為BlockingQueure都一手給你辦好了。在concurrent包,釋出以前,在多執行緒環境下,我們必須自己去控制這些細節,尤其還要兼顧效率和執行緒安全, 而這會給我們的程式帶來不小的複雜度

3) 核心方法

方法型別 拋異常 特殊值 阻塞 超時
插入方法 add(o) offer(o) put(o) offer(o, timeout, timeunit)
移除方法 remove(o) poll() take() poll(timeout, timeunit)
檢查方法 element() peek() 不可用 不可用
  • 拋異常:如果操作不能馬上進行,則丟擲異常
  • 特殊值:如果操作不能馬上進行,將會返回一個特殊的值,一般是 true 或者 false
  • 一直阻塞:如果操作不能馬上進行,操作會被阻塞
  • 超時退出:如果操作不能馬上進行,操作會被阻塞指定的時間,如果指定時間沒執行,則返回一個特殊值,一般是 true 或者 false

4)用處

  • 生產者消費者模式
  • 執行緒池
  • 訊息中介軟體
  1. 生產者消費者模式--傳統版:
public class ShareData {
    private int stock = 0;
    private Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    /**生產*/
    private void produce() throws InterruptedException {
        lock.lock();
        try {
            while (stock > 0) {    //庫存量大於0時停止生成
                condition.await();
            }
            stock++;    //否則繼續生成
            System.out.println(Thread.currentThread().getName()+"\t生產者生產完畢,此時庫存:"+stock+"通知消費者消費");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    /**消費*/
    private void consume() throws InterruptedException {
        lock.lock();
        try {
            while (stock <1 ) {    //庫存不足等待生產
                condition.await();
            }
            stock--;    //否則繼續消費
            System.out.println(Thread.currentThread().getName()+"\t消費者消費完畢,此時庫存:"+stock+"通知生產者生產");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        /**初始庫存為0,兩個執行緒交替工作,一個生產一個消費*/
        ShareData shareData = new ShareData();
         new Thread(()->{
             for (int i = 1; i < 5; i++) {
                 try {
                     shareData.produce();
                 } catch (InterruptedException e) {}
             }
         },"執行緒A").start();
          new Thread(()->{
              for (int i = 1; i < 5; i++) {
                  try {
                      shareData.consume();
                  } catch (InterruptedException e) {}
              }
          },"執行緒B").start();
    }
}
//列印結果
/**
	執行緒A	生產者生產完畢,此時庫存:1通知消費者消費
	執行緒B	消費者消費完畢,此時庫存:0通知生產者生產
	執行緒A	生產者生產完畢,此時庫存:1通知消費者消費
	執行緒B	消費者消費完畢,此時庫存:0通知生產者生產
	執行緒A	生產者生產完畢,此時庫存:1通知消費者消費
	執行緒B	消費者消費完畢,此時庫存:0通知生產者生產
	執行緒A	生產者生產完畢,此時庫存:1通知消費者消費
	執行緒B	消費者消費完畢,此時庫存:0通知生產者生產
*/
複製程式碼
  1. 生產者消費者模式--阻塞佇列版
public class BlockingQueueDemo {

    /**
     * 預設開啟,進行生產消費工作
     */
    private volatile boolean flag = true;
    private AtomicInteger atomicInteger = new AtomicInteger();
    private BlockingQueue<String> blockingQueue;

    public BlockingQueueDemo(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }

    public void produce() throws InterruptedException {
        String data;
        boolean returnValue;
        while (flag) {
            data = atomicInteger.incrementAndGet() + "";
            returnValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS); //往佇列中放資料
            if (returnValue) {
                System.out.println(Thread.currentThread().getName() + "\t 插入佇列的資料為:" + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t 插入佇列的資料為:" + data + "失敗");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName()+"\t 停止標識 flag為:\t"+flag);
    }

    public void consume() throws InterruptedException {
        String result;
        while (flag) {
            result = blockingQueue.poll(2,TimeUnit.SECONDS);
            if (null == result || "".equalsIgnoreCase(result)) {
                flag = false;
                System.out.println(Thread.currentThread().getName()+"\t 沒有取到資料");
                return;
            }
            System.out.println(Thread.currentThread().getName()+"\t 消費者取到資料:"+result);
        }
    }
    public void stop() {
        flag = false;
    }
}
class TestDemo{
    public static void main(String[] args) {
        //建立一個容量為10的容器
        BlockingQueueDemo blockingQueueDemo = new BlockingQueueDemo(new ArrayBlockingQueue<>(10));
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t 生產執行緒啟動");
            try {
                blockingQueueDemo.produce();
            } catch (InterruptedException e){}
        },"生產者執行緒").start();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t 消費執行緒啟動");
            try {
                blockingQueueDemo.consume();
            } catch (InterruptedException e) {}
        },"消費者執行緒").start();
        try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println();
        System.out.println();
        System.out.println("停止工作");
        blockingQueueDemo.stop();
    }
}
複製程式碼

六、執行緒池

概念: 執行緒池做的工作主要是控制執行的執行緒的數量,處理過程中將任務加入佇列,然後線上程建立後啟動這些任務,如果執行緒超過了最大數量,超出的執行緒將排隊等候,等其他執行緒執行完畢,再從佇列中取出任務來執行。 特點:

  • 執行緒複用
  • 控制最大併發數
  • 管理執行緒

優點:

  • 降低資源消耗,通過重複利用自己建立的執行緒減低執行緒建立和銷燬造成的消耗。
  • 提高響應速度,當任務到達時,任務可不需要等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性,執行緒是稀缺西苑,如果無限制的建立,不僅會消耗系統資源,還會降低體統的穩定性,使用執行緒可以進行統一分配,調優和監控。

1)執行緒建立幾種方法

  • 繼承Thead
class ThreadDemo extends Thread{
    @Override
    public void run() {
        System.out.println("ThreadDemo 執行中...");
    }
    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
    }
}
複製程式碼
  • 實現 Runnable 介面
class RunnableDemo{
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("RunnableDemo 執行中...");
            }
        }).start();
    }
}
複製程式碼
  • 實現 Callable
public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return 1;
            }
        });
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
複製程式碼

2)架構說明

Java中的執行緒池使用過Excutor框架實現的,該框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor這幾個類。

在這裡插入圖片描述

3)重點了解

  • Executors.newFixedThreadPool()
    特點:
    1. 建立一個定長執行緒池,可控制執行緒的最大併發數,超出的執行緒會在佇列中等待。
    2. newFixedThreadPool 建立的執行緒池CorePoolSize和MaximumPoolSize是相等的,它使用的是LinkedBlockingQueue
    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads, 0, 
                    TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>());
        }
    複製程式碼
  • Executors.newSingleThreadExecutor()
    特點:
    1. 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務都按照指定的順序執行。
    2. newSingleThreadExecutor將corePoolSize和MaximumPoolSize都設定為1,它使用的是LinedBlockingQueue
    public static ExecutorService newSingleThreadExecutor() {
        return new ThreadPoolExecutor(1, 1, 0,
                TimeUnit.MICROSECONDS, new LinkedBlockingDeque<Runnable>());
    }
    複製程式碼
  • Executors.newCachedThreadPool()
    特點:
    1. 建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則建立新執行緒。
    2. newCacheThreadPool將corePoolsize設定為0,MaximumPoolSize設定為Integer.MAX_VALUE,它使用的是SynchronousQueue ,也就是說來了任務就建立執行緒執行,如果執行緒空閒超過60秒,就銷燬執行緒
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60,
                TimeUnit.SECONDS, new SynchronousQueue<>());
    }
    複製程式碼

4)七大引數

引數 作用
corePoolSize 執行緒池中常駐核心執行緒數
maximumPoolSize 執行緒池能夠容納同時執行的最大執行緒數,需大於1
keepAliveTime 多餘空閒執行緒的存活時間,當空間時間達到keepAliveTime值時,多餘的執行緒會被銷燬直到只剩下corePoolSize個執行緒為止
TimeUnit: keepAliveTime 時間單位
workQueue 阻塞任務佇列
threadFactory 表示生成執行緒池中工作執行緒的執行緒工廠,使用者建立新執行緒,一般用預設即可
RejectedExecutionHandler 拒絕策略,表示當執行緒佇列滿了並且工作執行緒大於執行緒池的最大顯示數(maximumPoolSize)時如何來拒絕
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> blockingQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
                maximumPoolSize < 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0) {
            throw new IllegalArgumentException("不合法配置");
        }
        if (blockingQueue == null ||
                threadFactory == null ||
                handler == null) {
            throw new NullPointerException();
        }
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.unit = unit;
        this.blocking = blockingQueue;
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
複製程式碼

5)執行緒池工作原理

image
image
image
例子:

(銀行)一家銀行總共有六個視窗(maximumPoolSize),週末開了三個視窗提供業務辦理(corePoolSize),上班期間來了3個人辦理業務,三個視窗能夠應付的過來,這個時候又來了1個,三個視窗便忙不過來了,,只好讓新來的客戶去等待區(workQueue)等待,接下來如果還有來客戶的話便讓客戶去等待區(workQueue)等待。但是如果等待區也坐滿了。業務經理(threadFactory)便通知剩下的視窗開啟來進行業務辦理,但是如果六個視窗都佔滿了,而且等待區也坐不下了。這個時候銀行便要考慮採用什麼方式(RejectedExecutionHandler)來拒絕客戶。時間慢慢的過去了,辦理業務的客戶也差不多走了,只剩下3個客戶在辦理。這個時候空閒了3個新增的視窗,他們便開始等待(keepAliveTime)一定時間,如果時間到了還沒有客戶來辦理業務的話,這3個新增視窗便可以關閉,回去休息。但是原來的三個視窗(corePoolSize)還得繼續開著。

6)拒絕策略

等待佇列已經排滿,再也塞不下新的任務,而且也達到了 maximumPoolSize 數量,無法繼續為新任務服務,這個時候我們便要採取拒絕策略機制合理的處理這個問題。 以下內建拒絕策略均實現了RejectExecutionHandler介面

  1. AbortPolicy(預設):

直接丟擲RejectedException異常來阻止系統正常執行。

  1. CallerRunPolicy:

“呼叫者執行” 一種調節機制,該策略既不會拋棄任務,也不會丟擲異常。執行緒呼叫執行該任務的 execute 本身。此策略提供簡單的反饋控制機制,能夠減緩新任務的提交速度。

  1. DiscardOldestPolicy:

拋棄佇列中等待最久的任務,然後把當前任務加入佇列中嘗試再次提交(如果再次失敗,則重複此過程)。

  1. DiscardPolicy:

直接丟棄任務,不予任何處理也不丟擲異常,如果允許任務丟失,這是最好的拒絕策略。


7)為何不用JDK建立執行緒池的方法

阿里巴巴 java 開發手冊 【強制】執行緒資源必須通過執行緒池提供,不允許在應用中自行顯示建立執行緒。說明:使用執行緒池的好處是減少在建立和銷燬執行緒上所消耗的時間以及系統資源的開銷,解決資源不足的問題。如果不使用執行緒池,有可能造成系統建立大量同類執行緒而導致消耗完記憶體或者“過度切換”的問題。 【強制】 執行緒池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。

  1. FixedThreadPoolSingleThreadPool:允許的請求佇列長度為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM。
  2. CacheThreadPoolScheduledThreadPool :允許建立執行緒的數量為Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致OOM。

自定義例子:

	/**
	  * 使用內建建立執行緒池
	  */
    private static void threadPoolInit() {
        /**一池5個執行緒*/
        //ExecutorService threadPool1 = Executors.newFixedThreadPool(5);

        /**一池1個執行緒*/
        //ExecutorService threadPool2 = Executors.newSingleThreadExecutor();

        /**一池N個執行緒*/
        ExecutorService threadPool = Executors.newCachedThreadPool();
        try {
            for (int i = 1; i <= 20; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 來辦理業務");
                });
                try {TimeUnit.MICROSECONDS.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
/**自定義執行緒池*/
    public static void main(String[] args) {
        ExecutorService customizeThreadPool = new ThreadPoolExecutor(
                2, 5, 1L, TimeUnit.SECONDS,
                new LinkedBlockingDeque<Runnable>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
                /**
                 *  new ThreadPoolExecutor.AbortPolicy();  預設丟擲異常
                 *  new ThreadPoolExecutor.CallerRunsPolicy();  回退呼叫者
                 *  new ThreadPoolExecutor.DiscardPolicy();     處理不了的不處理
                 */
        );

        try {
            for (int i = 1; i <= 20; i++) {
                customizeThreadPool.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t 來辦理業務");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            customizeThreadPool.shutdown();
        }
    }
複製程式碼

8)合理配置執行緒池

  • CPU密集型
    • 檢視本機CPU核數:Runtime.getRuntime().availableProcessors()
    • CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU需一直全速執行。
    • CPU密集任務只有在真正的多核CPU上才可能得到加速(通過多執行緒)
    • CPU密集型任務配置儘可能少的執行緒數量 => 公式:CPU核數+1個執行緒的執行緒池
  • IO密集型
    • 由於IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如CPU核數 * 2
    • IO密集型,是說明該任務需要大量的IO,即大量的阻塞。所以在單執行緒上執行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待上,所以要使用多執行緒可以大大的加速程式執行,即使在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
    • 配置執行緒公式:CPU核數 / 1-阻塞係數(0.8~0.9) =>如8核CPU:8 / 1 - 0.9 = 80個執行緒數

七、死鎖編碼及定位分析

1)是什麼

死鎖是指兩個或兩個以上的程式在執行過程中,因爭奪資源而造成的一種互相等待的現象,如果無外力的干涉那麼它們將無法推進下去,如果系統的資源充足,程式的資源請求都能夠得到滿足,死鎖出現的可能性就很低,否則會因爭奪有限的資源而陷入死鎖。

在這裡插入圖片描述

2)造成原因

  • 資源系統不足
  • 程式執行推進的順序不合適
  • 資源分配不當

例子:

public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA = "A鎖";
        String lockB = "B鎖";
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,2,1L,TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );
        threadPool.execute(()->{
            new HoldThread(lockA,lockB).run();
        });
        threadPool.execute(()->{
            new HoldThread(lockB,lockA).run();
        });
    }
}
class HoldThread implements Runnable{
    private String lockA;
    private String lockB;
    public HoldThread(String lockA,String lockB) {
        this.lockA = lockA;
        this.lockB = lockB;
    }
    @Override
    public void run() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName()+"\t 自己持有:"+lockA+"\t 嘗試獲取:"+lockB);
            try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName()+"\t 自己持有:"+lockB+"\t 嘗試獲取:"+lockA);
            }
        }
    }
}
複製程式碼

列印結果 陷入死鎖狀態:

在這裡插入圖片描述

3)解決方法

  • jps 命令定位程式編號

在這裡插入圖片描述

  • jstack 找到死鎖檢視

在這裡插入圖片描述


在這裡插入圖片描述

本文較長,能看到這裡的都是好樣的,成長之路學無止境 今天的你多努力一點,明天的你就能少說一句求人的話!

相關文章