1. 程式人生 > >JUC多執行緒及高併發

JUC多執行緒及高併發

一、請你談談對volatile的理解

Package java.util.concurrent---> AtomicInteger  Lock ReadWriteLock

1、volatile是java虛擬機器提供的輕量級的同步機制

保證可見性、不保證原子性、禁止指令重排

  1. 保證可見性

    當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看到修改的值

    當不新增volatile關鍵字時示例:

    package com.jian8.juc;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * 1驗證volatile的可見性
     * 1.1 如果int num = 0,number變數沒有新增volatile關鍵字修飾
     * 1.2 添加了volatile,可以解決可見性
     */
    public class VolatileDemo {
    
        public static void main(String[] args) {
            visibilityByVolatile();//驗證volatile的可見性
        }
    
        /**
         * volatile可以保證可見性,及時通知其他執行緒,主實體記憶體的值已經被修改
         */
        public static void visibilityByVolatile() {
            MyData myData = new MyData();
    
            //第一個執行緒
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t come in");
                try {
                    //執行緒暫停3s
                    TimeUnit.SECONDS.sleep(3);
                    myData.addToSixty();
                    System.out.println(Thread.currentThread().getName() + "\t update value:" + myData.num);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }, "thread1").start();
            //第二個執行緒是main執行緒
            while (myData.num == 0) {
                //如果myData的num一直為零,main執行緒一直在這裡迴圈
            }
            System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myData.num);
        }
    }
    
    class MyData {
        //    int num = 0;
        volatile int num = 0;
    
        public void addToSixty() {
            this.num = 60;
        }
    }
    

    輸出結果:

    thread1	 come in
    thread1	 update value:60
    //執行緒進入死迴圈
    

    當我們加上volatile關鍵字後,volatile int num = 0;輸出結果為:

    thread1	 come in
    thread1	 update value:60
    main	 mission is over, num value is 60
    //程式沒有死迴圈,結束執行
    
  1. ==不保證原子性==

    原子性:不可分割、完整性,即某個執行緒正在做某個具體業務時,中間不可以被加塞或者被分割,需要整體完整,要麼同時成功,要麼同時失敗

    驗證示例(變數新增volatile關鍵字,方法不新增synchronized):

    package com.jian8.juc;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * 1驗證volatile的可見性
     *  1.1 如果int num = 0,number變數沒有新增volatile關鍵字修飾
     * 1.2 添加了volatile,可以解決可見性
     *
     * 2.驗證volatile不保證原子性
     *  2.1 原子性指的是什麼
     *      不可分割、完整性,即某個執行緒正在做某個具體業務時,中間不可以被加塞或者被分割,需要整體完整,要麼同時成功,要麼同時失敗
     */
    public class VolatileDemo {
    
        public static void main(String[] args) {
    //        visibilityByVolatile();//驗證volatile的可見性
            atomicByVolatile();//驗證volatile不保證原子性
        }
        
        /**
         * volatile可以保證可見性,及時通知其他執行緒,主實體記憶體的值已經被修改
         */
    	//public static void visibilityByVolatile(){}
        
        /**
         * volatile不保證原子性
         * 以及使用Atomic保證原子性
         */
        public static void atomicByVolatile(){
            MyData myData = new MyData();
            for(int i = 1; i <= 20; i++){
                new Thread(() ->{
                    for(int j = 1; j <= 1000; j++){
                        myData.addSelf();
                        myData.atomicAddSelf();
                    }
                },"Thread "+i).start();
            }
            //等待上面的執行緒都計算完成後,再用main執行緒取得最終結果值
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            while (Thread.activeCount()>2){
                Thread.yield();
            }
            System.out.println(Thread.currentThread().getName()+"\t finally num value is "+myData.num);
            System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+myData.atomicInteger);
        }
    }
    
    class MyData {
        //    int num = 0;
        volatile int num = 0;
    
        public void addToSixty() {
            this.num = 60;
        }
    
        public void addSelf(){
            num++;
        }
        
        AtomicInteger atomicInteger = new AtomicInteger();
        public void atomicAddSelf(){
            atomicInteger.getAndIncrement();
        }
    }
    

    執行三次結果為:

    //1.
    main	 finally num value is 19580	
    main	 finally atomicnum value is 20000
    //2.
    main	 finally num value is 19999
    main	 finally atomicnum value is 20000
    //3.
    main	 finally num value is 18375
    main	 finally atomicnum value is 20000
    //num並沒有達到20000
    
  1. 禁止指令重排

    有序性:在計算機執行程式時,為了提高效能,編譯器和處理器常常會對**==指令做重拍==**,一般分以下三種

    graph LR
    	原始碼 --> id1["編譯器優化的重排"]
    	id1 --> id2[指令並行的重排]
    	id2 --> id3[記憶體系統的重排]
    	id3 --> 最終執行的指令
    	style id1 fill:#ff8000;
    	style id2 fill:#fab400;
    	style id3 fill:#ffd557;
    

    單執行緒環境裡面確保程式最終執行結果和程式碼順序執行的結果一致。

    處理器在進行重排順序是必須要考慮指令之間的**==資料依賴性==**

    ==多執行緒環境中執行緒交替執行,由於編譯器優化重排的存在,兩個執行緒中使用的變數能否保證一致性時無法確定的,結果無法預測==

    重排程式碼例項:

    宣告變數:int a,b,x,y=0

    JUC多執行緒及高併發

    執行緒2
    x = a; y = b;
    b = 1; a = 2;
    結 果 x = 0 y=0

    如果編譯器對這段程式程式碼執行重排優化後,可能出現如下情況:

    執行緒1 執行緒2
    b = 1; a = 2;
    x= a; y = b;
    結 果 x = 2 y=1

    這個結果說明在多執行緒環境下,由於編譯器優化重排的存在,兩個執行緒中使用的變數能否保證一致性是無法確定的

    volatile實現禁止指令重排,從而避免了多執行緒環境下程式出現亂序執行的現象

    ==記憶體屏障==(Memory Barrier)又稱記憶體柵欄,是一個CPU指令,他的作用有兩個:

    1. 保證特定操作的執行順序
    2. 保證某些變數的記憶體可見性(利用該特性實現volatile的記憶體可見性)

    由於編譯器和處理器都能執行指令重排優化。如果在之零件插入一i奧Memory Barrier則會告訴編譯器和CPU,不管什麼指令都不能和這條Memory Barrier指令重排順序,也就是說==通過插入記憶體屏障禁止在記憶體屏障前後的指令執行重排序優化==。記憶體屏障另外一個作用是強制刷出各種CPU的快取資料,因此任何CPU上的執行緒都能讀取到這些資料的最新版本。

    graph TB
        subgraph 
        bbbb["對Volatile變數進行讀操作時,<br>回在讀操作之前加入一條load屏障指令,<br>從記憶體中讀取共享變數"]
        ids6[Volatile]-->red3[LoadLoad屏障]
        red3-->id7["禁止下邊所有普通讀操作<br>和上面的volatile讀重排序"]
        red3-->red4[LoadStore屏障]
        red4-->id9["禁止下邊所有普通寫操作<br>和上面的volatile讀重排序"]
        red4-->id8[普通讀]
        id8-->普通寫
        end
        subgraph 
        aaaa["對Volatile變數進行寫操作時,<br>回在寫操作後加入一條store屏障指令,<br>將工作記憶體中的共享變數值重新整理回到主記憶體"]
        id1[普通讀]-->id2[普通寫]
        id2-->red1[StoreStore屏障]
        red1-->id3["禁止上面的普通寫和<br>下面的volatile寫重排序"]
        red1-->id4["Volatile寫"]
        id4-->red2[StoreLoad屏障]
        red2-->id5["防止上面的volatile寫和<br>下面可能有的volatile讀寫重排序"]
        end
        style red1 fill:#ff0000;
        style red2 fill:#ff0000;
        style red4 fill:#ff0000;
        style red3 fill:#ff0000;
        style aaaa fill:#ffff00;
        style bbbb fill:#ffff00;
    

2、JMM(java記憶體模型)

JMM(Java Memory Model)本身是一種抽象的概念,並不真實存在,他描述的時一組規則或規範,通過這組規範定義了程式中各個變數(包括例項欄位,靜態欄位和構成陣列物件的元素)的訪問方式。

JMM關於同步的規定:

  1. 執行緒解鎖前,必須把共享變數的值重新整理回主記憶體
  2. 執行緒加鎖前,必須讀取主記憶體的最新值到自己的工作記憶體
  3. 加鎖解鎖時同一把鎖

由於JVM執行程式的實體是執行緒,而每個執行緒建立時JVM都會為其建立一個工作記憶體(有的成為棧空間),工作記憶體是每個執行緒的私有資料區域,而java記憶體模型中規定所有變數都儲存在**==主記憶體==,主記憶體是貢獻記憶體區域,所有執行緒都可以訪問,==但執行緒對變數的操作(讀取賦值等)必須在工作記憶體中進行,首先概要將變數從主記憶體拷貝到自己的工作記憶體空間,然後對變數進行操作,操作完成後再將變數寫回主記憶體,==不能直接操作主記憶體中的變數,各個執行緒中的工作記憶體中儲存著主記憶體的==變數副本拷貝==**,因此不同的執行緒件無法訪問對方的工作記憶體,執行緒間的通訊(傳值)必須通過主記憶體來完成,期間要訪問過程如下圖:

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

3、你在那些地方用過volatile

當普通單例模式在多執行緒情況下:

public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 構造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        //構造方法只會被執行一次
//        System.out.println(getInstance() == getInstance());
//        System.out.println(getInstance() == getInstance());
//        System.out.println(getInstance() == getInstance());

        //併發多執行緒後,構造方法會在一些情況下執行多次
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, "Thread " + i).start();
        }
    }
}

其構造方法在一些情況下會被執行多次

解決方式:

  1. 單例模式DCL程式碼

    DCL (Double Check Lock雙端檢鎖機制)在加鎖前和加鎖後都進行一次判斷

        public static SingletonDemo getInstance() {
            if (instance == null) {
                synchronized (SingletonDemo.class) {
                    if (instance == null) {
                        instance = new SingletonDemo();
                    }
                }
            }
            return instance;
        }
    

    大部分執行結果構造方法只會被執行一次,但指令重排機制會讓程式很小的機率出現構造方法被執行多次

    ==DCL(雙端檢鎖)機制不一定執行緒安全==,原因時有指令重排的存在,加入volatile可以禁止指令重排

    原因是在某一個執行緒執行到第一次檢測,讀取到instance不為null時,instance的引用物件可能==沒有完成初始化==。instance=new SingleDemo();可以被分為一下三步(虛擬碼):

    memory = allocate();//1.分配物件記憶體空間
    instance(memory);	//2.初始化物件
    instance = memory;	//3.設定instance執行剛分配的記憶體地址,此時instance!=null
    

    步驟2和步驟3不存在資料依賴關係,而且無論重排前還是重排後程序的執行結果在單執行緒中並沒有改變,因此這種重排優化時允許的,如果3步驟提前於步驟2,但是instance還沒有初始化完成

    但是指令重排只會保證序列語義的執行的一致性(單執行緒),但並不關心多執行緒間的語義一致性。

    ==所以當一條執行緒訪問instance不為null時,由於instance示例未必已初始化完成,也就造成了執行緒安全問題。==

  2. 單例模式volatile程式碼

    為解決以上問題,可以將SingletongDemo例項上加上volatile

    private static volatile SingletonDemo instance = null;
    

二、CAS你知道嗎

1、compareAndSet----比較並交換

AtomicInteger.conpareAndSet(int expect, indt update)

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

第一個引數為拿到的期望值,如果期望值沒有一致,進行update賦值,如果期望值不一致,證明資料被修改過,返回fasle,取消賦值

例子:

package com.jian8.juc.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 1.CAS是什麼?
 * 1.1比較並交換
 */
public class CASDemo {
    public static void main(String[] args) {
       checkCAS();
    }

    public static void checkCAS(){
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data is " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2014) + "\t current data is " + atomicInteger.get());
    }
}

輸出結果為:

true	 current data is 2019
false	 current data is 2019

2、CAS底層原理?對Unsafe的理解

比較當前工作記憶體中的值和主記憶體中的值,如果相同則執行規定操作,否則繼續比較知道主記憶體和工作記憶體中的值一直為止

  1. atomicInteger.getAndIncrement();

        public final int getAndIncrement() {
            return unsafe.getAndAddInt(this, valueOffset, 1);
        }
    
  2. Unsafe

    • 是CAS核心類,由於Java方法無法直接訪問地層系統,需要通過本地(native)方法來訪問,Unsafe相當於一個後門,基於該類可以直接操作特定記憶體資料。Unsafe類存在於sun.misc包中,其內部方法操作可以像C的指標一樣直接操作記憶體,因為Java中CAS操作的執行依賴於Unsafe類的方法。

      Unsafe類中的所有方法都是native修飾的,也就是說Unsafe類中的方法都直接呼叫作業系統底層資源執行相應任務

    • 變數valueOffset,表示該變數值在記憶體中的偏移地址,因為Unsafe就是根據記憶體便宜地址獲取資料的

    • 變數value用volatile修飾,保證多執行緒之間的可見性

  3. CAS是什麼

    CAS全稱呼Compare-And-Swap,它是一條CPU併發原語

    他的功能是判斷記憶體某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的。

    CAS併發原語體現在JAVA語言中就是sun.misc.Unsafe類中各個方法。呼叫Unsafe類中的CAS方法,JVM會幫我們實現CAS彙編指令。這是一種完全依賴於硬體的功能,通過他實現了原子操作。由於CAS是一種系統原語,原語屬於作業系統用語範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的,在執行過程中不允許被中斷,也就是說CAS是一條CPU的原子指令,不會造成資料不一致問題。

    //unsafe.getAndAddInt
        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;
        }
    

    var1 AtomicInteger物件本身

    var2 該物件的引用地址

    var4 需要變動的資料

    var5 通過var1 var2找出的主記憶體中真實的值

    用該物件前的值與var5比較;

    如果相同,更新var5+var4並且返回true,

    如果不同,繼續去之然後再比較,直到更新完成

3、CAS缺點

  1. ** 迴圈時間長,開銷大**

    例如getAndAddInt方法執行,有個do while迴圈,如果CAS失敗,一直會進行嘗試,如果CAS長時間不成功,可能會給CPU帶來很大的開銷

  2. 只能保證一個共享變數的原子操作

    對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性

  3. ABA問題

三、原子類AtomicInteger的ABA問題?原子更新引用?

1、ABA如何產生

CAS演算法實現一個重要前提需要去除記憶體中某個時刻的資料並在當下時刻比較並替換,那麼在這個時間差類會導致資料的變化。

比如執行緒1從記憶體位置V取出A,執行緒2同時也從記憶體取出A,並且執行緒2進行一些操作將值改為B,然後執行緒2又將V位置資料改成A,這時候執行緒1進行CAS操作發現記憶體中的值依然時A,然後執行緒1操作成功。

==儘管執行緒1的CAS操作成功,但是不代表這個過程沒有問題==

2、如何解決?原子引用

示例程式碼:

package juc.cas;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicRefrenceDemo {
    public static void main(String[] args) {
        User z3 = new User("張三", 22);
        User l4 = new User("李四", 23);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
    }
}

@Getter
@ToString
@AllArgsConstructor
class User {
    String userName;
    int age;
}

輸出結果

true	User(userName=李四, age=23)
false	User(userName=李四, age=23)

3、時間戳的原子引用

新增機制,修改版本號

package com.jian8.juc.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * ABA問題解決
 * AtomicStampedReference
 */
public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("=====以下時ABA問題的產生=====");
        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "Thread 1").start();

        new Thread(() -> {
            try {
                //保證執行緒1完成一次ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
        }, "Thread 2").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("=====以下時ABA問題的解決=====");

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本號" + stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本號" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本號" + atomicStampedReference.getStamp());
        }, "Thread 3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本號" + stamp);
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);

            System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t當前最新實際版本號:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t當前最新實際值:" + atomicStampedReference.getReference());
        }, "Thread 4").start();
    }
}

輸出結果:

=====以下時ABA問題的產生=====
true	2019
=====以下時ABA問題的解決=====
Thread 3	第1次版本號1
Thread 4	第1次版本號1
Thread 3	第2次版本號2
Thread 3	第3次版本號3
Thread 4	修改是否成功false	當前最新實際版本號:3
Thread 4	當前最新實際值:100

四、我們知道ArrayList是執行緒不安全的,請編寫一個不安全的案例並給出解決方案

HashSet與ArrayList一致 HashMap

HashSet底層是一個HashMap,儲存的值放在HashMap的key裡,value儲存了一個PRESENT的靜態Object物件

1、執行緒不安全

package com.jian8.juc.collection;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * 集合類不安全問題
 * ArrayList
 */
public class ContainerNotSafeDemo {
    public static void main(String[] args) {
        notSafe();
    }

    /**
     * 故障現象
     * java.util.ConcurrentModificationException
     */
    public static void notSafe() {
        List<String> list = new ArrayList<>();
        for (int i = 1; i <= 30; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, "Thread " + i).start();
        }
    }
}

報錯:

Exception in thread "Thread 10" java.util.ConcurrentModificationException

2、導致原因

併發正常修改導致

一個人正在寫入,另一個同學來搶奪,導致資料不一致,併發修改異常

3、解決方法:**CopyOnWriteArrayList

List<String> list = new Vector<>();//Vector執行緒安全
List<String> list = Collections.synchronizedList(new ArrayList<>());//使用輔助類
List<String> list = new CopyOnWriteArrayList<>();//寫時複製,讀寫分離

Map<String, String> map = new ConcurrentHashMap<>();
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

CopyOnWriteArrayList.add方法:

CopyOnWrite容器即寫時複製,往一個元素新增容器的時候,不直接往當前容器Object[]新增,而是先將當前容器Object[]進行copy,複製出一個新的容器Object[] newElements,讓後新的容器新增元素,新增完元素之後,再將原容器的引用指向新的容器setArray(newElements),這樣做可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會新增任何元素,所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器

	public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

五、公平鎖、非公平鎖、可重入鎖、遞迴鎖、自旋鎖?手寫自旋鎖

1、公平鎖、非公平鎖

  1. 是什麼

    公平鎖就是先來後到、非公平鎖就是允許加塞,Lock lock = new ReentrantLock(Boolean fair); 預設非公平。

    • **==公平鎖==**是指多個執行緒按照申請鎖的順序來獲取鎖,類似排隊打飯。

    • **==非公平鎖==**是指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒優先獲取鎖,在高併發的情況下,有可能會造成優先順序反轉或者節現象。

  2. 兩者區別

    • 公平鎖:Threads acquire a fair lock in the order in which they requested it

      公平鎖,就是很公平,在併發環境中,每個執行緒在獲取鎖時,會先檢視此鎖維護的等待佇列,如果為空,或者當前執行緒就是等待佇列的第一個,就佔有鎖,否則就會加入到等待佇列中,以後會按照FIFO的規則從佇列中取到自己。

    • 非公平鎖:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested.

      非公平鎖比較粗魯,上來就直接嘗試佔有額,如果嘗試失敗,就再採用類似公平鎖那種方式。

  3. other

    對Java ReentrantLock而言,通過建構函式指定該鎖是否公平,磨粉是非公平鎖,非公平鎖的優點在於吞吐量比公平鎖大

    對Synchronized而言,是一種非公平鎖

2、可重入所(遞迴鎖)

  1. 遞迴鎖是什麼

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

  2. ReentrantLock/Synchronized 就是一個典型的可重入鎖

  3. 可重入鎖最大的作用是避免死鎖

  4. 程式碼示例

    package com.jian8.juc.lock;
    
    ####
        public static void main(String[] args) {
            Phone phone = new Phone();
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread 1").start();
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread 2").start();
        }
    }
    class Phone{
        public synchronized void sendSMS()throws Exception{
            System.out.println(Thread.currentThread().getName()+"\t -----invoked sendSMS()");
            Thread.sleep(3000);
            sendEmail();
        }
    
        public synchronized void sendEmail() throws Exception{
            System.out.println(Thread.currentThread().getName()+"\t +++++invoked sendEmail()");
        }
    }
    
    package com.jian8.juc.lock;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class ReentrantLockDemo {
        public static void main(String[] args) {
            Mobile mobile = new Mobile();
            new Thread(mobile).start();
            new Thread(mobile).start();
        }
    }
    class Mobile implements Runnable{
        Lock lock = new ReentrantLock();
        @Override
        public void run() {
            get();
        }
    
        public void get() {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName()+"\t invoked get()");
                set();
            }finally {
                lock.unlock();
            }
        }
        public void set(){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName()+"\t invoked set()");
            }finally {
                lock.unlock();
            }
        }
    }
    
    

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

  1. 概念

    • 獨佔鎖:指該鎖一次只能被一個執行緒所持有,對ReentrantLock和Synchronized而言都是獨佔鎖

    • 共享鎖:只該鎖可被多個執行緒所持有

      ReentrantReadWriteLock其讀鎖是共享鎖,寫鎖是獨佔鎖

    • 互斥鎖:讀鎖的共享鎖可以保證併發讀是非常高效的,讀寫、寫讀、寫寫的過程是互斥的

  2. 程式碼示例

    package com.jian8.juc.lock;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    /**
     * 多個執行緒同時讀一個資源類沒有任何問題,所以為了滿足併發量,讀取共享資源應該可以同時進行。
     * 但是
     * 如果有一個執行緒象取寫共享資源來,就不應該自由其他執行緒可以對資源進行讀或寫
     * 總結
     * 讀讀能共存
     * 讀寫不能共存
     * 寫寫不能共存
     */
    public class ReadWriteLockDemo {
        public static void main(String[] args) {
            MyCache myCache = new MyCache();
            for (int i = 1; i <= 5; i++) {
                final int tempInt = i;
                new Thread(() -> {
                    myCache.put(tempInt + "", tempInt + "");
                }, "Thread " + i).start();
            }
            for (int i = 1; i <= 5; i++) {
                final int tempInt = i;
                new Thread(() -> {
                    myCache.get(tempInt + "");
                }, "Thread " + i).start();
            }
        }
    }
    
    class MyCache {
        private volatile Map<String, Object> map = new HashMap<>();
        private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    
        /**
         * 寫操作:原子+獨佔
         * 整個過程必須是一個完整的統一體,中間不許被分割,不許被打斷
         *
         * @param key
         * @param value
         */
        public void put(String key, Object value) {
            rwLock.writeLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t正在寫入:" + key);
                TimeUnit.MILLISECONDS.sleep(300);
                map.put(key, value);
                System.out.println(Thread.currentThread().getName() + "\t寫入完成");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                rwLock.writeLock().unlock();
            }
    
        }
    
        public void get(String key) {
            rwLock.readLock().lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t正在讀取:" + key);
                TimeUnit.MILLISECONDS.sleep(300);
                Object result = map.get(key);
                System.out.println(Thread.currentThread().getName() + "\t讀取完成: " + result);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                rwLock.readLock().unlock();
            }
    
        }
    
        public void clear() {
            map.clear();
        }
    }
    

4、自旋鎖

  1. spinlock

    是指嘗試獲取鎖的執行緒不會立即阻塞,而是==採用迴圈的方式去嘗試獲取鎖==,這樣的好處是減少執行緒上下文切換的消耗,缺點是迴圈會消耗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;
        }
    

    手寫自旋鎖:

    package com.jian8.juc.lock;
    
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicReference;
    
    /**
     * 實現自旋鎖
     * 自旋鎖好處,迴圈比較獲取知道成功位置,沒有類似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(3);
            }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)) {
    
            }
        }
    
        public void myUnlock() {
            Thread thread = Thread.currentThread();
            atomicReference.compareAndSet(thread, null);
            System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()");
        }
    }
    
    

六、CountDownLatch/CyclicBarrier/Semaphore使用過嗎

1、CountDownLatch(火箭發射倒計時)

  1. 它允許一個或多個執行緒一直等待,知道其他執行緒的操作執行完後再執行。例如,應用程式的主執行緒希望在負責啟動框架服務的執行緒已經啟動所有的框架服務之後再執行

  2. CountDownLatch主要有兩個方法,當一個或多個執行緒呼叫await()方法時,呼叫執行緒會被阻塞。其他執行緒呼叫countDown()方法會將計數器減1,當計數器的值變為0時,因呼叫await()方法被阻塞的執行緒才會被喚醒,繼續執行

  3. 程式碼示例:

    package com.jian8.juc.conditionThread;
    
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
    
    public class CountDownLatchDemo {
        public static void main(String[] args) throws InterruptedException {
    //        general();
            countDownLatchTest();
        }
    
        public static void general(){
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t上完自習,離開教室");
                }, "Thread-->"+i).start();
            }
            while (Thread.activeCount()>2){
                try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            }
            System.out.println(Thread.currentThread().getName()+"\t=====班長最後關門走人");
        }
    
        public static void countDownLatchTest() throws InterruptedException {
            CountDownLatch countDownLatch = new CountDownLatch(6);
            for (int i = 1; i <= 6; i++) {
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName()+"\t被滅");
                    countDownLatch.countDown();
                }, CountryEnum.forEach_CountryEnum(i).getRetMessage()).start();
            }
            countDownLatch.await();
            System.out.println(Thread.currentThread().getName()+"\t=====秦統一");
        }
    }
    
    

2、CyclicBarrier(集齊七顆龍珠召喚神龍)

  1. CycliBarrier

    可迴圈(Cyclic)使用的屏障。讓一組執行緒到達一個屏障(也可叫同步點)時被阻塞,知道最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活,執行緒進入屏障通過CycliBarrier的await()方法

  2. 程式碼示例:

    package com.jian8.juc.conditionThread;
    
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class CyclicBarrierDemo {
        public static void main(String[] args) {
            cyclicBarrierTest();
        }
    
        public static void cyclicBarrierTest() {
            CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
                System.out.println("====召喚神龍=====");
            });
            for (int i = 1; i <= 7; i++) {
                final int tempInt = i;
                new Thread(() -> {
                    System.out.println(Thread.currentThread().getName() + "\t收集到第" + tempInt + "顆龍珠");
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }, "" + i).start();
            }
        }
    }
    
    

3、Semaphore訊號量

可以代替Synchronize和Lock

  1. 訊號量主要用於兩個目的,一個是用於多個共享資源的互斥作用,另一個用於併發執行緒數的控制

  2. 程式碼示例:

    搶車位示例

    package com.jian8.juc.conditionThread;
    
    import java.util.concurrent.Semaphore;
    import java.util.concurrent.TimeUnit;
    
    public class SemaphoreDemo {
        public static void main(String[] args) {
            Semaphore semaphore = new Semaphore(3);//模擬三個停車位
            for (int i = 1; i <= 6; i++) {//模擬6部汽車
                new Thread(() -> {
                    try {
                        semaphore.acquire();
                        System.out.println(Thread.currentThread().getName() + "\t搶到車位");
                        try {
                            TimeUnit.SECONDS.sleep(3);//停車3s
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "\t停車3s後離開車位");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        semaphore.release();
                    }
                }, "Car " + i).start();
            }
        }
    }
    
    

七、阻塞佇列

  • **==ArrayBlockingQueue==**是一個基於陣列結構的有界阻塞佇列,此佇列按FIFO原則對元素進行排序
  • **==LinkedBlockingQueue==**是一個基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue
  • **==SynchronousQueue==**是一個不儲存元素的阻塞佇列,滅個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於

1、佇列和阻塞佇列

  1. 首先是一個佇列,而一個阻塞佇列再資料結構中所起的作用大致如下圖

    graph LR
    Thread1-- put -->id1["阻塞佇列"]
    subgraph BlockingQueue
    	id1
    end
    id1-- Take -->Thread2
    蛋糕師父--"放(櫃滿阻塞)"-->id2[蛋糕展示櫃]
    subgraph 櫃
    	id2
    end
    id2--"取(櫃空阻塞)"-->顧客
    

    執行緒1往阻塞佇列中新增元素,而執行緒2從阻塞佇列中移除元素

    當阻塞佇列是空是,從佇列中==獲取==元素的操作會被阻塞

    當阻塞佇列是滿時,從佇列中==新增==元素的操作會被阻塞

    試圖從空的阻塞佇列中獲取元素的執行緒將會被阻塞,知道其他的執行緒網空的佇列插入新的元素。

    試圖網已滿的阻塞佇列中新增新元素的執行緒同樣會被阻塞,知道其他的執行緒從列中移除一個或者多個元素或者完全清空佇列後使佇列重新變得空閒起來並後續新增

2、為什麼用?有什麼好處?

  1. 在多執行緒領域:所謂阻塞,在某些情況下會掛起執行緒,一旦滿足條件,被掛起的執行緒又會自動被喚醒

  2. 為什麼需要BlockingQueue

    好處時我們不需要關心什麼時候需要阻塞執行緒,什麼時候需要喚醒執行緒,因為這一切BlockingQueue都給你一手包辦了

    在concurrent包釋出以前,在多執行緒環境下,==我們每個程式設計師都必須自己控制這些細節,尤其還要兼顧效率和執行緒安全==,而這回給我們程式帶來不小的複雜度

3、BlockingQueue的核心方法

方法型別 丟擲異常 特殊值 阻塞 超時
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take poll(time,unit)
檢查 element() peek() 不可用 不可用
方法型別 status
丟擲異常 當阻塞佇列滿時,再往佇列中add會拋IllegalStateException: Queue full
當阻塞佇列空時,在網佇列裡remove會拋NoSuchElementException
特殊值 插入方法,成功true失敗false
移除方法,成功返回出佇列的元素,佇列裡沒有就返回null
一直阻塞 當阻塞佇列滿時,生產者執行緒繼續往佇列裡put元素,佇列會一直阻塞執行緒知道put資料或響應中斷退出
當阻塞佇列空時,消費者執行緒試圖從佇列take元素,佇列會一直阻塞消費者執行緒知道佇列可用。
超時退出 當阻塞佇列滿時,佇列會阻塞生產者執行緒一定時間,超過限時後生產者執行緒會退出

4、架構梳理+種類分析

  1. 種類分析

    • ==ArrayBlockingQueue==:由資料結構組成的有界阻塞佇列。
    • ==LinkedBlockingQueue==:由連結串列結構組成的有界(但大小預設值為Integer.MAX_VALUE)阻塞佇列。
    • PriorityBlockingQueue:支援優先順序排序的無界阻塞佇列。
    • DelayQueue:使用優先順序佇列實現的延遲無界阻塞佇列。
    • ==SychronousQueue==:不儲存元素的阻塞佇列,也即單個元素的佇列。
    • LinkedTransferQueue:由連結串列結構組成的無界阻塞佇列。
    • LinkedBlockingDeque:由歷覽表結構組成的雙向阻塞佇列。
  2. SychronousQueue

    • 理論:SynchronousQueue沒有容量,與其他BlockingQueue不同,SychronousQueue是一個不儲存元素的BlockingQueue,每一個put操作必須要等待一個take操作,否則不能繼續新增元素,反之亦然。

    • 程式碼示例

      package com.jian8.juc.queue;
      
      import java.util.concurrent.BlockingQueue;
      import java.util.concurrent.SynchronousQueue;
      import java.util.concurrent.TimeUnit;
      
      /**
       * ArrayBlockingQueue是一個基於陣列結構的有界阻塞佇列,此佇列按FIFO原則對元素進行排序
       * LinkedBlockingQueue是一個基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue
       * SynchronousQueue是一個不儲存元素的阻塞佇列,滅個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於
       * 1.佇列
       * 2.阻塞佇列
       * 2.1 阻塞佇列有沒有好的一面
       * 2.2 不得不阻塞,你如何管理
       */
      public class SynchronousQueueDemo {
          public static void main(String[] args) throws InterruptedException {
              BlockingQueue<String> blockingQueue = new SynchronousQueue<>();
              new Thread(() -> {
                  try {
                      System.out.println(Thread.currentThread().getName() + "\t put 1");
                      blockingQueue.put("1");
                      System.out.println(Thread.currentThread().getName() + "\t put 2");
                      blockingQueue.put("2");
                      System.out.println(Thread.currentThread().getName() + "\t put 3");
                      blockingQueue.put("3");
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }, "AAA").start();
              new Thread(() -> {
                  try {
                      TimeUnit.SECONDS.sleep(5);
                      System.out.println(Thread.currentThread().getName() + "\ttake " + blockingQueue.take());
                      TimeUnit.SECONDS.sleep(5);
                      System.out.println(Thread.currentThread().getName() + "\ttake " + blockingQueue.take());
                      TimeUnit.SECONDS.sleep(5);
                      System.out.println(Thread.currentThread().getName() + "\ttake " + blockingQueue.take());
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }, "BBB").start();
          }
      }
      
      

5、用在哪裡

  1. 生產者消費者模式

    • 傳統版

      package com.jian8.juc.queue;
      
      import java.util.concurrent.locks.Condition;
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      
      /**
       * 一個初始值為零的變數,兩個執行緒對其交替操作,一個加1一個減1,來5輪
       * 1. 執行緒  操作  資源類
       * 2. 判斷  幹活  通知
       * 3. 防止虛假喚起機制
       */
      public class ProdConsumer_TraditionDemo {
          public static void main(String[] args) {
              ShareData shareData = new ShareData();
              for (int i = 1; i <= 5; i++) {
                  new Thread(() -> {
                      try {
                          shareData.increment();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  }, "ProductorA " + i).start();
              }
              for (int i = 1; i <= 5; i++) {
                  new Thread(() -> {
                      try {
                          shareData.decrement();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  }, "ConsumerA  " + i).start();
              }
              for (int i = 1; i <= 5; i++) {
                  new Thread(() -> {
                      try {
                          shareData.increment();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  }, "ProductorB " + i).start();
              }
              for (int i = 1; i <= 5; i++) {
                  new Thread(() -> {
                      try {
                          shareData.decrement();
                      } catch (Exception e) {
                          e.printStackTrace();
                      }
                  }, "ConsumerB  " + i).start();
              }
          }
      }
      
      class ShareData {//資源類
          private int number = 0;
          private Lock lock = new ReentrantLock();
          private Condition condition = lock.newCondition();
      
          public void increment() throws Exception {
              lock.lock();
              try {
                  //1.判斷
                  while (number != 0) {
                      //等待不能生產
                      condition.await();
                  }
                  //2.幹活
                  number++;
                  System.out.println(Thread.currentThread().getName() + "\t" + number);
                  //3.通知
                  condition.signalAll();
              } catch (Exception e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
          }
      
          public void decrement() throws Exception {
              lock.lock();
              try {
                  //1.判斷
                  while (number == 0) {
                      //等待不能消費
                      condition.await();
                  }
                  //2.消費
                  number--;
                  System.out.println(Thread.currentThread().getName() + "\t" + number);
                  //3.通知
                  condition.signalAll();
              } catch (Exception e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
          }
      }
      
      
  • 阻塞佇列版

    package com.jian8.juc.queue;
    
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class ProdConsumer_BlockQueueDemo {
        public static void main(String[] args) {
            MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10));
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t生產執行緒啟動");
                try {
                    myResource.myProd();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Prod").start();
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t消費執行緒啟動");
                try {
                    myResource.myConsumer();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Consumer").start();
    
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("5s後main叫停,執行緒結束");
            try {
                myResource.stop();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    class MyResource {
        private volatile boolean flag = true;//預設開啟,進行生產+消費
        private AtomicInteger atomicInteger = new AtomicInteger();
    
        BlockingQueue<String> blockingQueue = null;
    
        public MyResource(BlockingQueue<String> blockingQueue) {
            this.blockingQueue = blockingQueue;
            System.out.println(blockingQueue.getClass().getName());
        }
    
        public void myProd() throws Exception {
            String data = null;
            boolean retValue;
            while (flag) {
                data = atomicInteger.incrementAndGet() + "";
                retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
                if (retValue) {
                    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=false,生產結束");
        }
    
        public void myConsumer() throws Exception {
            String result = null;
            while (flag) {
                result = blockingQueue.poll(2, TimeUnit.SECONDS);
                if (null == result || result.equalsIgnoreCase("")) {
                    flag = false;
                    System.out.println(Thread.currentThread().getName() + "\t超過2s沒有取到蛋糕,消費退出");
                    System.out.println();
                    return;
                }
                System.out.println(Thread.currentThread().getName() + "\t消費佇列" + result + "成功");
            }
        }
    
        public void stop() throws Exception {
            flag = false;
        }
    }
    
  1. 執行緒池

  2. 訊息中介軟體

6、synchronized和lock有什麼區別?用新的lock有什麼好處?請舉例

區別

  1. 原始構成

    • synchronized時關鍵字屬於jvm

      monitorenter,底層是通過monitor物件來完成,其實wait/notify等方法也依賴於monitor物件只有在同步或方法中才能掉wait/notify等方法

      monitorexit

    • Lock是具體類,是api層面的鎖(java.util.)

  2. 使用方法

    • sychronized不需要使用者取手動釋放鎖,當synchronized程式碼執行完後系統會自動讓執行緒釋放對鎖的佔用
    • ReentrantLock則需要使用者去手動釋放鎖若沒有主動釋放鎖,就有可能導致出現死鎖現象,需要lock()和unlock()方法配合try/finally語句塊來完成
  3. 等待是否可中斷

    • synchronized不可中斷,除非丟擲異常或者正常執行完成
    • ReentrantLock可中斷,設定超時方法tryLock(long timeout, TimeUnit unit),或者lockInterruptibly()放程式碼塊中,呼叫interrupt()方法可中斷。
  4. 加鎖是否公平

    • synchronized非公平鎖
    • ReentrantLock兩者都可以,預設公平鎖,構造方法可以傳入boolean值,true為公平鎖,false為非公平鎖
  5. 鎖繫結多個條件Condition

    • synchronized沒有
    • ReentrantLock用來實現分組喚醒需要要喚醒的執行緒們,可以精確喚醒,而不是像synchronized要麼隨機喚醒一個執行緒要麼喚醒全部執行緒。
    package com.jian8.juc.lock;
    
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * synchronized和lock區別
     * <p===lock可繫結多個條件===
     * 對執行緒之間按順序呼叫,實現A>B>C三個執行緒啟動,要求如下:
     * AA列印5次,BB列印10次,CC列印15次
     * 緊接著
     * AA列印5次,BB列印10次,CC列印15次
     * 。。。。
     * 來十輪
     */
    public class SyncAndReentrantLockDemo {
        public static void main(String[] args) {
            ShareData shareData = new ShareData();
            new Thread(() -> {
                for (int i = 1; i <= 10; i++) {
                    shareData.print5();
                }
            }, "A").start();
            new Thread(() -> {
                for (int i = 1; i <= 10; i++) {
                    shareData.print10();
                }
            }, "B").start();
            new Thread(() -> {
                for (int i = 1; i <= 10; i++) {
                    shareData.print15();
                }
            }, "C").start();
        }
    
    }
    
    class ShareData {
        private int number = 1;//A:1 B:2 C:3
        private Lock lock = new ReentrantLock();
        private Condition condition1 = lock.newCondition();
        private Condition condition2 = lock.newCondition();
        private Condition condition3 = lock.newCondition();
    
        public void print5() {
            lock.lock();
            try {
                //判斷
                while (number != 1) {
                    condition1.await();
                }
                //幹活
                for (int i = 1; i <= 5; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
                //通知
                number = 2;
                condition2.signal();
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void print10() {
            lock.lock();
            try {
                //判斷
                while (number != 2) {
                    condition2.await();
                }
                //幹活
                for (int i = 1; i <= 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
                //通知
                number = 3;
                condition3.signal();
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void print15() {
            lock.lock();
            try {
                //判斷
                while (number != 3) {
                    condition3.await();
                }
                //幹活
                for (int i = 1; i <= 15; i++) {
                    System.out.println(Thread.currentThread().getName() + "\t" + i);
                }
                //通知
                number = 1;
                condition1.signal();
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

八、執行緒池用過嗎?ThreadPoolExecutor談談你的理解

1、Callable介面的使用

package com.jian8.juc.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

/**
 * 多執行緒中,第三種獲得多執行緒的方式
 */
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //FutureTask(Callable<V> callable)
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread2());

        new Thread(futureTask, "AAA").start();
//        new Thread(futureTask, "BBB").start();//複用,直接取值,不要重啟兩個執行緒
        int a = 100;
        int b = 0;
        //b = futureTask.get();//要求獲得Callable執行緒的計算結果,如果沒有計算完成就要去強求,會導致堵塞,直到計算完成
        while (!futureTask.isDone()) {//當futureTask完成後取值
            b = futureTask.get();
        }
        System.out.println("*******Result" + (a + b));
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
    }
}

class MyThread2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable come in");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return 1024;
    }
}

2、為什麼使用執行緒池

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

  2. 主要特點

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

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

3、執行緒池如何使用

  1. 架構說明

    Java中的執行緒池是通過Executor框架實現的,該框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor

    graph BT
    	類-Executors
    	類-ScheduledThreadPoolExecutor-->類-ThreadPoolExecutor
    	類-ThreadPoolExecutor-->類-AbstractExecutorService
    	類-AbstractExecutorService-.->介面-ExecutorService
    	類-ScheduledThreadPoolExecutor-.->介面-ScheduledExecutorService
    	介面-ScheduledExecutorService-->介面-ExecutorService
    	介面-ExecutorService-->介面-Executor
    
  2. 編碼實現

    實現有五種,Executors.newScheduledThreadPool()是帶時間排程的,java8新推出Executors.newWorkStealingPool(int),使用目前機器上可用的處理器作為他的並行級別

    重點有三種

    • Executors.newFixedThreadPool(int)