3萬字長文帶你複習JUC

在複習JUC之前,先搞明白什麼情況下才能線程安全,也就是線程安全所具備的條件

  1. 原子性
  2. 有序性
  3. 可見性

那從上面這幾個角度來考慮Synchronized和volatile的區別?

  • Synchronized可以保證原子性,有序性,可見性
  • volatile可以保證有序性和可見性,但是不能保證原子性

還有一種類是絕對安全的,它就是不可變類(immutable),它的不可變也就意味着不能進行寫操作,那自然是安全的了。
先說說final,它可以修飾變量,變量的值爲不可變,不過僅僅是於簡單類型。如果是一個複雜類型的話,final就不能防止其內部的操作了,如下例子所示

@Slf4j
@NotThreadSafe
public class Immutable1 {
    private final static Integer a = 1;
    private final static String b = "2";
    private final static Map<Integer, Integer> map = Maps.newHashMap();

    static {
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);

    }

    public static void main(String[] args) {

//        a=2;  不可以賦值
//      map = Maps.newHashMap(); 這個引用不能指向其他對象
        map.put(1, 3);
        log.info("{}", map.get(1));

    }

結果如下
在這裏插入圖片描述
對於Integer是不能進行賦值操作的,可以看到final對於引用類型的變量是鎖住了那個引用類型的地址。
那怎麼可以實現對象的不可變呢?
對於集合來說可以調用Collections裏面的不可變方法,如下代碼

@Slf4j
@ThreadSafe
public class Immutable2 {
    private final static Integer a = 1;
    private final static String b = "2";
    private static Map<Integer, Integer> map = null;

    static {
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        //使用Collections.unmodifiableMap()生成不可變對象
        map = Collections.unmodifiableMap(map);

    }

    public static void main(String[] args) {
        //如果強制進行put就會報錯
        map.put(1, 3);
        log.info("{}", map.get(1));

    }

}

可以看到出現了異常,對於不可變對象也可以使用第三方的工具包等
在這裏插入圖片描述

也就是說volatile是一種輔助鎖進行工作的一種機制!它並不能代替鎖來保證多線程環境下的線程安全

下面是我從網上找到的一份JUC的圖,下面就按照這個圖來進行復習
在這裏插入圖片描述
可以從圖中看到JUC下主要分爲五大部分

  • collections
  • atomic
  • locks
  • tools
  • executor

但是實際上的JUC包不是這樣的,這樣只是爲了模塊分類
在這裏插入圖片描述
下面就按照邏輯模塊來複習其中的重點

Collections

在Collections模塊中,比較常見的的幾個集合有
大體的分可以分爲Concurrentxxx,Blockxxx,CopyOnWritexxx

  • Concurrent開頭的一般都是弱一致性的容器(fail-fast),比如統計size的準確度是有限的,沒有像COW一樣的修改開銷
  • Blocking中典型的就是阻塞隊列,如ArrayBlockingQueue(有界),LinkedBlockingQueue(內部邏輯是根據有界來編寫的),PriorityBlockingQueue
  • CopyOnWrite:利用快照技術,寫時複製

下面就挑裏面典型的來說一說

CurrentHashMap的基本原理

在CurrentHashMap中採用的CAS+鎖來保證的線程安全。它的默認容量和負載因子,樹形化,剪枝基本都和HashMap相同。它與JDK7不同的是它不再採用分段鎖,而是採用的bucket鎖,縮小的鎖的粒度,它把鎖放在了頭部,從而保證每個bucket的線程安全
下面以put爲例
在瞭解put前需要了解幾個變量

    static final int MOVED     = -1; // 標誌證在擴容
    static final int TREEBIN   = -2; // 標誌是樹
    private transient volatile int sizeCtl;  // -1代表初始化,-N代表有N-1個線程正在擴容    正數/0表示沒有被初始化
   //put
    public V put(K key, V value) {
        return putVal(key, value, false);
    }


    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // 1.合法性檢測
        if (key == null || value == null) throw new NullPointerException();
        // 2.對key進行位干擾,目的是爲了讓hash更均勻
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 遍歷哈希表  table就是Node數組,可以把數組的每個元素看爲一個bucket
        for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
            ConcurrentHashMap.Node<K,V> f; int n, i, fh;
            // 3.如果table未初始化,就進行初始化
            if (tab == null || (n = tab.length) == 0)
                // 初始化根據sizeCtl和循環CAS來進行的
                tab = initTable();
            // 4.如果該bucket爲null就是要以CAS無鎖方式進行put  i = (n - 1) & hash是用來取模
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                        new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 5.如果正在擴容,就加快轉移
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);

            else {
                V oldVal = null;
                // 鎖住頭
                synchronized (f) {
                    // 6.如果是鏈表就遍歷如果重複key就替換否則就尾插  並記錄binCount
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (ConcurrentHashMap.Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                ConcurrentHashMap.Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        // 7.如果是樹,就替換value或插入新的節點
                        else if (f instanceof ConcurrentHashMap.TreeBin) {
                            ConcurrentHashMap.Node<K,V> p;
                            binCount = 2;
                            if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }

                if (binCount != 0) {
                    // 8.如果binCount到達率樹形化的閾值就進行樹化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    // 9.如果是替換操作就返回value
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 10.如果是添加Node,就給節點數+1,,並檢查是否需要擴容,需要擴容就幫助擴容(增加線程)  
        addCount(1L, binCount);
        return null;
    }


initTable()方法

  /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    // 進行table初始化操作
    private final ConcurrentHashMap.Node<K,V>[] initTable() {
        ConcurrentHashMap.Node<K,V>[] tab; int sc;
        // 1.循環進行初始化
        while ((tab = table) == null || tab.length == 0) {
            // 2.如果有線程去處理了就不管了,說明其他CAS已經成功,讓出cpu
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            // 3.沒有進行初始化就CAS操作,如果滿足就進入
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    // 4. 根據默認容量進行初始化
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

Blocking

阻塞隊列一般可以用於生產者消費者模型中去,比如下面的例子

package com.wrial.main.example.collection;
/*
 * @Author  Wrial
 * @Date Created in 12:06 2020/3/20
 * @Description 使用阻塞隊列實現生產者消費者模型
 */

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ConsumerAndProductorByBlockQueue {

    private static ArrayBlockingQueue queue = new ArrayBlockingQueue(3);

    public static void main(String[] args) {


        Consumer consumer = new Consumer(queue);
        Productor productor = new Productor(queue);
        new Thread(consumer).start();
        new Thread(productor).start();


    }

    static class Consumer implements Runnable {

        private BlockingQueue queue;

        Consumer(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    Thread.sleep(3000);
                    queue.put(i);
                    System.out.println("put->" + i);
                }
                //100標誌結束
                queue.put(100);
                System.out.println("put結束標誌" + 100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    static class Productor implements Runnable {
        private BlockingQueue queue;

        Productor(BlockingQueue queue) {
            this.queue = queue;
        }


        @Override
        public void run() {
            int i;
            try {
                while ((i = (int) queue.take()) != 100) {
                    System.out.println("take->" + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}


在這裏插入圖片描述

CopyOnWrite

翻譯過來就是就是寫時複製, 在往集合中添加數據的時候,先拷貝存儲的數組,然後添加元素到拷貝好的數組中,然後用現在的數組去替換成員變量的數組,由此可以看到它必然會很費時,在寫的多的不建議使用它相關的。

鎖相關

在開始正題之前,先了解一下Synchronized,AQS,Lock之間的關係

  • Synchronized是JVM層面的鎖,是程序員不可控的,它會有自己的鎖膨脹機制
  • AQS是一個同步器,比如在下面說的這些同步工具基本都是基於AQS的
  • Lock是程序級別的鎖的接口,提供tryLock,lock,unlock,newCondition等方法,可以和AQS實現ReentrantLock

說到JUC的鎖必然是不能少了Lock接口,Lock接口規定了程序鎖的規範


public interface Lock {
    
    void lock();

    void lockInterruptibly() throws InterruptedException;

    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();
    
    Condition newCondition();
}

實現Lock接口的類也有很多ReentrantLock(可重入鎖),ReadWriteLock(讀寫分離鎖),StampedLock(可以看做是對讀寫鎖的改進)等等

使用Lock自定義一個簡單鎖

package com.wrial.main.example.myLock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 定義一個自己的鎖
 */
public class Mylock1 implements Lock {

    private boolean isLock = false;

    //爲了處理多線程,加上Synchronized
    @Override
    public synchronized void lock() {

        //不能用if,使用while自旋
        while (isLock) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        isLock = true;

    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public synchronized void unlock() {

        isLock = false;
        notify();
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

使用Lock接口實現一個可重入鎖

package com.wrial.main.example.myLock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/*
 * 實現可重入鎖,根據當前鎖的狀態,如果鎖被佔用,那就判斷當前線程是不是佔用鎖的線程,否則就wait,釋放鎖的時候進行notify
 *
 * */
public class MyLock2 implements Lock {

    private boolean isLock = false;
    private Thread lockBy = null;
    private int lockCount = 0;

    @Override
    public void lock() {

        Thread currentThread = Thread.currentThread();
        while (isLock && lockBy != currentThread) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        isLock = true;
        lockBy = currentThread;
        lockCount++;
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    //必須要加synchronized
    @Override
    public synchronized void unlock() {
        if (lockBy == Thread.currentThread()) {
            lockCount--;
            if (lockCount == 0) {
                notify();
                isLock = false;
                lockBy = null;
            }
        }

    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

AQS全稱AbstractQueueSynchronizer同步發生器,是輕量級鎖的基礎,它用過內置的同步隊列(FIFO)來完成共享資源的管理工作。
要想明白AQS,那就得先明白裏面的內容,先看看它對節點的定義

static final class Node {
        // 共享模型
        static final Node SHARED = new Node();
        // 獨佔模型
        static final Node EXCLUSIVE = null;

        // 代表取消的(中斷或已完成),它不再會到隊列中去,會被垃圾回收機制回收
        static final int CANCELLED =  1;
        // 表示該節點後續有阻塞的節點
        static final int SIGNAL    = -1;
        // 條件下阻塞
        static final int CONDITION = -2;
        // 共享時頭結點的狀態
        static final int PROPAGATE = -3;

簡單的畫了畫圖,表示了AQS裏的隊列維護過程,第一個是投,它不會存任何的進程信息,它主要是記錄head和tail的指針,如果有新的Node進來就會插入到末尾,刪除就是刪除鏈表節點。這些Node是對線程信息的包裝。當鎖可用的時候它會喚醒裏面的線程進行工作。AQS中還有一個十分重要的變量,那就是state,它的含義就是已獲得鎖的線程lock的次數
在這裏插入圖片描述
下面就是自己使用AQS和Lock實現自定義的可重入鎖

package com.wrial.main.example.myLock;

/*
 * 使用AQS來實現可重入鎖
 * */

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MyLock4 implements Lock {


    private class Helper extends AbstractQueuedSynchronizer {


        //1.獲取狀態

        //2.拿到狀態,CAS判斷(雙重檢測),並設置爲獨佔鎖

        //3.判斷是不是當前線程,要是當前線程的話就可重入

        @Override
        protected boolean tryAcquire(int arg) {

            Thread t = Thread.currentThread();
            int state = getState();
            // 說明無鎖,就CAS進行取,若成功就設置爲獨佔鎖
            if (state == 0) {
                if (compareAndSetState(0, arg)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            // 如果當前線程已經那到鎖,那就state+1進行重入
            }else if (t == getExclusiveOwnerThread()){
                setState(state+1);
                return true;
            }
            return false;
        }


        //1.先看是不是當前線程,如果不是就不能釋放,拋出異常

        //2.如果是的話,判斷state,如果state-1(arg)=0,那就可以釋放


        @Override
        protected boolean tryRelease(int arg) {
            //鎖的獲取和釋放需要一一對應,那麼調用這個方法的線程,一定是當前線程
            if(Thread.currentThread() != getExclusiveOwnerThread()){
                throw new RuntimeException();
            }
            int state = getState() - arg;
            setState(state);
            // 如果state爲0,說明獨佔鎖被取消了
            if(state == 0){
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;

        }

        //ConditionObject不能在外部使用是AQS裏的
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    Helper helper = new Helper();

    @Override
    public void lock() {
        helper.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        helper.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1, unit.toNanos(time));
    }

    @Override
    public void unlock() {
        helper.release(1);
    }

    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }

}

使用讀寫鎖案例:

package com.wrial.main.example.myLock;


import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReentrantReadWriteLockExample {


    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private final Map<String, Data> map = new TreeMap();

    private final Lock readLock = readWriteLock.readLock();

    private final Lock writeLock = readWriteLock.writeLock();

    public Data getData(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Set<String> getKeySet() {
        readLock.lock();
        try {
            return map.keySet();
        } finally {
            readLock.unlock();
        }
    }

    public Data put(String key, Data value) {
        writeLock.lock();
        try {
            return map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    static class Data {
        public int getValue() {
            return 1;
        }


    }

    public void testReadLock() {
        Lock readLock = readWriteLock.readLock();
        Data data = new Data();
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "準備讀" + data.getValue());
            readLock.lock();
            // 在此不釋放鎖
            System.out.println(Thread.currentThread() + "正在讀");

        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "準備讀" + data.getValue());
            readLock.lock();
            // 在此不釋放鎖
            System.out.println(Thread.currentThread() + "正在讀");

        }).start();
    }

    public void testWriteLock() {
        Lock writeLock = readWriteLock.writeLock();
        Data data = new Data();
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "準備寫" + data.getValue());
            writeLock.lock();
            // 在此不釋放鎖
            System.out.println(Thread.currentThread() + "正在寫");

        }).start();
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "準備寫" + data.getValue());
            writeLock.lock();
            // 在此不釋放鎖
            System.out.println(Thread.currentThread() + "正在寫");

        }).start();
    }

    public static void main(String[] args) {

        ReentrantReadWriteLockExample reentrantReadWriteLockExample = new ReentrantReadWriteLockExample();
        reentrantReadWriteLockExample.testReadLock();
//        reentrantReadWriteLockExample.testWriteLock();
        
    }

}

讀鎖可以重複加,寫鎖只能獨佔,如下

在這裏插入圖片描述在這裏插入圖片描述

當然所的搭配還有讀-寫,寫-讀,經測試過讀寫鎖是互斥的。

Atomic工具包

關於Atomic工具包使用方法都是相同的,它底層的原理還是利用的CAS進行數據更新,從而保證數據更新操作的原子性
這個程序是通過CountDownLatch控制打印次數五千的案例,此處並沒有使用到Atomic進行add

@Slf4j
@NotThreadSafe
public class CountExample1 {

    public static int clientTotal = 5000;

    public static int threadTotal = 200;

    public static int count = 0;


    public static void main(String[] args) throws InterruptedException {
        //線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信號量 控制最多可同時執行的個數(在此處的測試可以忽略它的存在)
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {

                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });

        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("cout:{}", count);

    }

    private static void add() {
        count++;
    }
}

可以看到結果如下,打印的結果不是5000
在這裏插入圖片描述
在下面程序使用AtomicInteger來進行自增

package com.wrial.main.example.count;


import com.wrial.main.annotations.NotThreadSafe;
import com.wrial.main.annotations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 併發模擬測試
 * 原子性Atomci包
 * 可以和CountExample進行對比,這個是5000,而CountExample是一個變化的
 * 僅僅將int換爲AtomicInteger互斥訪問後就不會出錯了
 */

@Slf4j
@ThreadSafe
public class CountExample2 {

    public static int clientTotal = 5000;

    public static int threadTotal = 200;

    //    public static int count = 0;
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        //線程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //信號量
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            executorService.execute(() -> {

                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });

        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("cout:{}", count.get());

    }

    private static void add() {
//        count++;
        count.incrementAndGet();//先增加再get
//        count.getAndIncrement();//先get後increment

    }
}

打印結果一直是5000
在這裏插入圖片描述
這就是Atomicxx的用法,它是利用CAS保證了原子性,也可以從此例子中進一步驗證自增操作並不是具有原子性的

工具類

在JUC下的工具類有很多,比如

  • CountDownLatch:正如其名,每執行一個任務就進行一次countdown。使用場景:劃分任務由多個線程進行執行,例如採用多線程進行數據採集,可以通過它規定進行採集的次數等
  • Semaphore:信號量,可用於控制同時資源訪問,使用場景:連接池的可連接數等
  • CyclicBarrier:內存屏障,它允許線程相互等待,等到達執行點之後再進行執行。它的意義和CountDownLatch很相似,但是又有不同。它的計數可以重置,而CountDownLatch不能重置。使用場景:可以用CountDownLatch的地方就可以用它,它比前者更強大,可以對阻塞數進行統計
  • Exchanger:顧名思義,它就是用來交換數據的。使用場景:可以提供一個同步點,讓兩個線程可以交換數據,必須成對出現

CountdownLatch使用,完成200個任務就停止


@Slf4j
public class CountDownLatchExample {

    private static final int count = 200;

    public static void test(int threadNum) throws InterruptedException {

        Thread.sleep(100);
        log.info("{}", threadNum);
        Thread.sleep(100);


    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exc = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(count);
        for (int i = 1; i <= count; i++) {
            final int num = i;
            exc.execute(() -> {
                try {
                    test(num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //保證肯定會執行,在其他情況可以在某些條件下才會countdown
                    countDownLatch.countDown();
                }
            });
        }
        //可以保證前面的執行完,對count=0進行校驗
        countDownLatch.await();
        log.info("finish");
        exc.shutdown();

    }

}

Semaphore信號量實現併發訪問控制,案例如下
控制信號量值爲10,執行任務需要一秒,可以在控制檯中看到明顯的停頓(而且是每隔10條任務),足以說明信號量的作用

package com.wrial.main.example.aqs;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.*;

/*
 * 信號量的使用
 * */
@Slf4j
public class SemaphoreExample1 {

    private static final int conutThrad = 30;

    public static void test(int count) throws InterruptedException {
        Thread.sleep(1000);
        log.info("{}", count);
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService exc = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(10);//定義資源運行最大併發量爲10
        for (int i = 0; i < conutThrad; i++) {
            final int count = i;
            exc.execute(() -> {
                try {
                    semaphore.acquire();//獲得一個許可,也可以獲取多個許可
                    test(count);
                    semaphore.release();//釋放一個許可,也可以釋放多個許可
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        exc.shutdown();


    }

}


它的tryAcquire也可以對其設置超時時間等參數,如下例子
設置信號量大小爲3,我們每次都獲取三個,每執行一個任務需要1s,也就是說最多可以執行五個任務

package com.wrial.main.example.aqs;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/*
 * 信號量的使用
 * */
@Slf4j
public class SemaphoreExample2{

    private static final int conutThrad = 30;

    public static void test(int count) throws InterruptedException {
        Thread.sleep(1000);
        log.info("{}", count);
    }

    public static void main(String[] args) throws InterruptedException {

        ExecutorService exc = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(3);//定義資源運行最大併發量爲3
        for (int i = 0; i < conutThrad; i++) {
            final int count = i;
            exc.execute(() -> {
                try {
                    //能獲取到信號量的就可以使用,獲取不到就捨棄
//                    if (semaphore.tryAcquire()) {
//                        test(count);
//                        semaphore.release();//釋放一個許可,也可以釋放多個許可
//                    }
                    //可以傳入參數,設置信號量的超時時間,針對於多線程
//                    if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
//                        test(count);
//                        semaphore.release();
//                    }
                    if (semaphore.tryAcquire(3,5, TimeUnit.SECONDS)) {
                        test(count);
                        semaphore.release(3);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        exc.shutdown();


    }

}

演示結果如下:
在這裏插入圖片描述

CyclicBarrier的使用案例,每五個線程之間到達屏障點,先到的等待後面的然後全到了之後就繼續執行

@Slf4j
public class CyclicBarrierExample1 {

    // 規定爲5,每五個線程comming,就等待一次再going
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    private static int threadCount = 20;

    public static void runAndWait(int num) throws Exception {
        Thread.sleep(100);
        log.info("{} is comming", num);
        cyclicBarrier.await();
        log.info("{} is going", num);

    }

    public static void main(String[] args) throws Exception {

        ExecutorService exe = Executors.newCachedThreadPool();
        for (int i = 0; i < threadCount; i++) {
            Thread.sleep(100);
            final int num = i;
            exe.execute(() -> {
                try {
                    runAndWait(num);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        exe.shutdown();
    }

}

部分結果如下圖
在這裏插入圖片描述
爲了進一步驗證,我將threadCount調整爲18,發現後三個一直在等待,但是後面已經沒有了
在這裏插入圖片描述
解決它的方法是什麼了,可以給它設置超時時間,超時就不再等待
帶有超時處理的CyclcBarrier



@Slf4j
public class CyclicBarrierExample2 {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

    private static int threadCount = 20;

    public static void runAndWait(int num) throws Exception {
        Thread.sleep(100);
        log.info("{} is comming", num);
        try {
            cyclicBarrier.await(3, TimeUnit.SECONDS);
        }catch (RuntimeException| BrokenBarrierException e){
            log.warn("RuntimeException| BrokenBarrierException",e);
        }
        log.info("{} is going", num);

    }

    public static void main(String[] args) throws Exception {

        ExecutorService exe = Executors.newCachedThreadPool();
        for (int i = 0; i < threadCount; i++) {
            Thread.sleep(1000);
            final int num = i;
            exe.execute(() -> {
                try {
                    runAndWait(num);
                } catch (Exception e) {

                }
            });
        }

        exe.shutdown();
    }

}

可以看到它等不到就拋出異常,自己走了
在這裏插入圖片描述
前面說到CyclcBarrier可以重置,可以傳入一個Runnable接口作爲重置標誌,代碼如下

package com.wrial.main.example.aqs;

/*
 * 使用CyclicBarrier,並傳入Runable
 * */

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.*;

@Slf4j
public class CyclicBarrierExample3 {

    //傳入的Runnable是在到達屏障後,第一個執行
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
      log.info("—————————————————————————————————— ");
    });

    private static int threadCount = 20;

    public static void runAndWait(int num) throws Exception {
        Thread.sleep(100);
        log.info("{} is comming", num);
        cyclicBarrier.await();
        log.info("{} is going", num);

    }

    public static void main(String[] args) throws Exception {

        ExecutorService exe = Executors.newCachedThreadPool();
        for (int i = 0; i < threadCount; i++) {
            Thread.sleep(1000);
            final int num = i;
            exe.execute(() -> {
                try {
                    runAndWait(num);
                } catch (Exception e) {

                }
            });
        }

        exe.shutdown();
    }

}

演示效果:
在這裏插入圖片描述
Exchanger使用案例

package com.wrial.main.example.aqs;
/*
 * @Author  Wrial
 * @Date Created in 17:45 2020/3/20
 * @Description Exchanger使用案例
 */

import java.io.IOException;
import java.util.concurrent.Exchanger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ExchangerExample {


    public void start() throws  IOException {
        int count = 5;
        //new一個Exchanger
        Exchanger<Integer> exchanger = new Exchanger<>();
        ExecutorService executorService = Executors.newFixedThreadPool(count);
        // 使用i作爲休眠時間和各自的數據
        for (int i = 0; i < count; i++) {
            executorService.execute(new Worker(i, exchanger,i));
        }
        System.in.read();
    }

    class Worker extends Thread {
        Integer value;
        Integer sleepTime;
        Exchanger<Integer> exchanger;

        public Worker(Integer sleepTime, Exchanger<Integer> exchanger,Integer value) {
            this.sleepTime = sleepTime;
            this.exchanger = exchanger;
            this.value = value;
        }

        @Override
        public void run() throws IllegalArgumentException {
            try {
                System.out.println(Thread.currentThread().getName() + " 準備執行");
                TimeUnit.SECONDS.sleep(sleepTime);
                System.out.println(Thread.currentThread().getName() + " 等待交換");
                Integer value = exchanger.exchange(this.value);
                System.out.println(Thread.currentThread().getName() + " 交換得到數據爲:" + value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }


    public static void main(String[] args) throws IOException, InterruptedException {
        new ExchangerExample().start();
    }
}


演示結果如下:
在這裏插入圖片描述
可以發現Exchanger不能孤立存在,thread-5一直在等線程和它交換,並且它進行交換的是相鄰的兩個線程的數據!

線程池

在前面也用到了線程池,下面就簡單瞭解一下線程池的種類,和線程池的結構
爲什麼要有線程池?
原因很簡單,就和數據庫連接池一個道理,因爲建立線程和銷燬線程對資源消耗很大,因此可以採用線程池來進行線程複用,更爲重要的一點是線程池提供了管理線程更簡便的方法。
我們經常去使用Executors工具類去創建我們想要的線程池,如下圖
在這裏插入圖片描述
裏面有固定大小的,也有可變大小的,也有可調度的,也有單線程的還有單線程可調度的線程池供我們去使用 ,結構是什麼呢?怎麼去工作的呢?下面就來聊一聊線程池
在說線程池之前先看看執行器,也就是Executor接口,在這個接口中只有一個方法,那就是execute方法,傳入的是一個Runnable接口,代碼如下

public interface Executor {

    void execute(Runnable command);
}

可以從上面那幅圖看到線程池從調度管理方面可以分爲可調度的線程池和不可調度的線程池

  • ExecutorService:不可調度的
  • ScheduledExecutorService:可調度的

進入Executors源碼可以看到創建線程池的方式大致分爲三種

  1. ThreadPoolExecutor
  2. ForkJoinPool
  3. ScheduledThreadPoolExecutor

其中1和2創建出的是不可調度的,3創建出來的是可調度的
代碼如下

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }

    public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1, threadFactory));
    }

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

    public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

下面着重於對最常用的ThreadPoolExecutor進行解釋
下圖爲ThreadPoolExecutor的UML圖,可以看到它的繼承和實現關係
在這裏插入圖片描述
要通過ThreadPoolExecutor創建一個線程池,需要的七大核心參數,瞭解核心參數也是瞭解線程池的根本,如下圖
在這裏插入圖片描述
核心參數有

  1. 核心線程數量:分配核心工作線程的數量
  2. 最大線程池容量:線程池最大能維護的線程數量
  3. 保持存活時間:規定非核心線程的最大的空閒時間
  4. 時間單元:規定使用的時間的單位
  5. 工作隊列(阻塞隊列):用於排隊的阻塞隊列
  6. 線程工廠:用於生產線程
  7. 拒絕策略:線程池拒絕任務採取的策略

它也對拒絕策略做了規定,也就是RejectedExecutionHandler的所有子類,拒絕策略有如下四種
在這裏插入圖片描述

  1. ThreadPoolExecutor.AbortPolicy:是默認的拒絕策略,就拒絕執行是拋出異常
  2. ThreadPoolExecutor.CallerRunsPolicy:由調用線程來完成任務(誰調用的就找誰)
  3. ThreadPoolExecutor.DiscardOldestPolicy:拋棄最老的任務(最先執行),然後重新提交被拒絕的線程
  4. ThreadPoolExecutor.DiscardPolicy:丟棄任務,只是默默的丟棄,也不會報異常

線程池的excute方法

  public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * 可以大致分爲下面五個步驟:
         * 1.判斷當前執行任務的線程是否小於核心線程,如果小於核心線程就創建一個新的線程執行work
         * 2.如果核心線程已滿,那就判斷workQueue滿不滿,等待隊列不滿的話就加入到等待隊列被調度
         * 3.如果workQueue滿了,那就會去檢查,當前執行任務線程數是否大於maxiumPoolSize
         * 4.如果不大於maxiumPoolSize,那就創建一個新的線程加入到線程池
         * 5.如果大於maxiumPoolSize那就是拒絕策略用到的地方了(根據自己的拒絕策略進行執行)
         *
       
        int c = ctl.get();
        //1
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //2
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
              
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //3  4
        else if (!addWorker(command, false))
        //5
            reject(command);
    }

因此結合其他代碼不難分析出線程池的工作流程,下面是我畫出的流程圖
在這裏插入圖片描述
這就是ThreadPoolExecutor的工作流程
下面也稍微說一說其他的兩種線程池
ForkJoinPool是根據併發裏面的ForkJoin框架而來的,核心思想就是分治,先將任務拆分,然後執行,然後合併。一般在處理比較大的數據的時候效率比較高,但是CPU佔有率也會急劇飆升。
下面就說說帶有調度的線程池,它其實是ThreadPoolExecutor的子類,它只是把阻塞隊列換成了延時隊列,從而實現調度,如下圖
在這裏插入圖片描述
說完線程池就最後來說說Callable和Future
說之前呢,先將Callable和我們經常用的Runnable進行一個對比
Callable接口

public interface Callable<V> {
     //如果不能完成就拋出一樣,完成就返回V
    V call() throws Exception;
}

Runnable接口


public interface Runnable {
    public abstract void run();
}

經過對比可以發現Runnable接口是不能進行回調的,也可以這麼說,Runnable是一個同步任務,Callable是一個異步任務
那說完Callable,那Future是幹嘛的呢?通過下面的代碼可以發現Future其實就是一個異步任務的管理,可以取消,判斷是否完成,get等操作

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

因此就可以根據此進行異步任務的編寫,代碼如下


@Slf4j
public class FutureExample {


    static class MyCallable implements Callable {

        @Override
        public String call() throws Exception {
            log.info("Do Some Things in callable!");
            Thread.sleep(4000);
            return "Callable Things Done";
        }
    }

    public static void main(String[] args) throws Exception {

        ExecutorService exe = Executors.newCachedThreadPool();
        Future future = exe.submit(new MyCallable());
        log.info("do some thing in main");
        //get方法如果獲取不到就一直阻塞
        String mes = (String) future.get();
        log.info("{}", mes);
        log.info("main done");
        exe.shutdown();
    }

}

結果如下,可以看到get執行完的時候和Thread.Sleep前,剛好差了4秒,可以看出get操作獲取不到就是阻塞狀態。
在這裏插入圖片描述
——————————————————————————————————————————

此文到此結束!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章