天天看點

JUC(Java.util.concurrent)常見元件

這裡寫目錄标題

    • ReentrantLock
    • semaphore
    • CountDownLatch
    • 原子類
    • 線程安全的集合

ReentrantLock

ReentrantLock 和 synchronized 一樣,也是一個可重入鎖。

  • ReentrantLock 使用方法
import java.util.concurrent.locks.ReentrantLock;

/**
兩個線程操作一個數
*/
public class MyReentrantLock {

    static int count;
    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                reentrantLock.lock();
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
                reentrantLock.unlock();
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                reentrantLock.lock();
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
                reentrantLock.unlock();
            }
        };
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count);
    }

}

           
  • 差別(參考 ReenTrantLock 與synchronized 差別)
  1. ReenTrantLock 是 JDK(java 源碼包) 實作的,而 synchronized 是關鍵字,通過 JVM 底層源碼(c++) 實作;
  2. ReenTrantLock 自己實作加鎖(lock)和解鎖(unlock),而 synchronized 是全套服務,加鎖解鎖自己實作;
  3. ReenTrantLock 中有 tryLock ,當線程阻塞後,可以手動實作等待時間,或者直接傳回(tryLock() 方法中可以設定等待時間,或者不設定,是以可以避免進入核心态的阻塞狀态,線程進入核心态就是不可控的,往往進入核心态意味着效率更低。是以設計鎖就是避免線程進入核心态)(為什麼有 synchronized 還要有 ReenTrantLock 的原因);

semaphore

semaphore(信号量),簡單點來了解,信号量就是一個計數器,表示可用資源的個數。

怎麼來了解這個 計數器 呢?

類似于停車場入口顯示的車位剩餘量,當一輛車離開停車場,那麼車位剩餘量+1,如果一輛車進入停車場,那麼車位剩餘量-1,如果車位剩餘量為 0 ,那麼車不能挺進去。

semaphore 也是這樣,涉及到兩個操作

P :申請一個可用資源(可用資源-1,也就是剩餘車位數量-1)

V :釋放一個可用資源(可用資源+1,也就是剩餘車位數量+1)

如果信号量為 0 ,也就是可用資源個數是 0,那麼就會阻塞,直到其他線程釋放資源。

注意:而 信号量 + 1 或者 - 1 操作都是 原子性 的。

基于 信号量 的了解,是以可以設計一個鎖

  1. 信号量隻有 0 和 1;
  2. 如果一個線程進行 P 操作,那麼信号量 -1,此時如果其他線程想使用此資源,需要阻塞等待;
  3. 如果線程釋放了資源,那麼信号量 +1,此時阻塞線程可以申請資源。

也可以實作一個 阻塞隊列

需要三個 semaphore,一個代表鎖,一個代表資源的個數,一個代表空位的個數;

CountDownLatch

類似于一個計數器,但是有不同,舉個例子。

比如下載下傳一個遊戲,需要多個線程下載下傳。那麼線程 1 下載下傳好了目前的檔案,而其他線程還沒下載下傳好,是以并不算完全下載下傳成功,等待所有線程都下載下傳成功後,才算真的下載下傳完成。

原子類

JUC(Java.util.concurrent)常見元件

原子類能夠保證操作資料時 CPU 的指令是原子的。

  • 示例
import java.util.concurrent.atomic.AtomicInteger;

public class MyAtomic {
    static AtomicInteger atomicInteger = new AtomicInteger();
    static int num = 0;

    /**
     * 執行 5 次自增對比結果
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            doIncrement();
            System.out.println("atomicInteger"+i + "= " + atomicInteger);
            System.out.println("num"+i + "= " + num);
            System.out.println("--------分割線---------");
            atomicInteger = new AtomicInteger();
            num = 0;
        }
    }

    /**
     * 建立兩個線程對一個數進行修改
     * @throws InterruptedException
     */
    public static void doIncrement() throws InterruptedException {
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    atomicInteger.incrementAndGet();
                    num++;
                }
            }
        };
        thread1.start();
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    atomicInteger.incrementAndGet();
                    num++;
                }
            }
        };
        thread2.start();
        thread1.join();
        thread2.join();
    }
}


           
  • 結果
atomicInteger1= 100001
num1= 63105
--------分割線---------
atomicInteger2= 100001
num2= 91873
--------分割線---------
atomicInteger3= 100001
num3= 91147
--------分割線---------
atomicInteger4= 100001
num4= 92904
--------分割線---------
atomicInteger5= 100001
num5= 88765
           

由結果得知,原子類 AtomicInteger 是線程安全的

線程安全的集合

平時刷題用的一些集合都是線程不安全的,例如

  1. ArrayList
  2. LinkedList
  3. HashSet
  4. HashMap

當然也有安全的集合,例如

5. Stack

6. Vector

7. HashTable(目前沒用到過)

針對線程不安全的集合,在 Java.util.concurrent 這個包中包含了許多線程安全的集合(标記了部分)

JUC(Java.util.concurrent)常見元件

當然,也可以自己實作線程安全的集合,那就是通過加 synchronized 關鍵字。而 HashTable 就是使用了這個方法,保證線程安全的。

而為什麼有了 HashTable 還要有 concurrentHashMap ?(HashTable 和 concurrentHashMap 的差別)

直接加 synchronized 關鍵字必然會導緻效率很低,而 concurrentHashMap 是通過什麼方式保證線程安全呢?

  1. 每個連結清單/紅黑樹加鎖的方式來保證線程安全(這種分法是 jdk1.8之後的分法,而之前是幾個連結清單/紅黑樹加一個鎖),也就是說,如果線程之間修改的不是同一個資料,那麼就不會導緻線程不安全。
  2. 針對讀操作,不加鎖,雖然可能會導緻一個線程正在修改資料的時候,另外一個線程剛好讀這個資料,導緻線程不安全。當然可以用讀寫鎖來進行優化。
  3. 内部廣泛使用 CAS 操作來提高效率,例如擷取元素個數的時候,沒加鎖,直接 CAS;
  4. 針對擴容進行了優化,假如某一個線程觸發了擴容,那麼當其他線程進行操作的時候,就會一起進行擴容。而 HashTable 則是觸發擴容的線程單獨進行擴容。

繼續閱讀