Java 多線程和高併發面試題

volatile

對 volatile的理解

volatile 是一種輕量級的同步機制。java

  1. 保證數據可見性
  2. 不保證原子性
  3. 禁止指令重排序

JMM

JMM(Java 內存模型)是一種抽象的概念,描述了一組規則或規範,定義了程序中各個變量的訪問方式。linux

JVM運行程序的實體是線程,每一個線程建立時 JVM 都會爲其建立一個工做內存,是線程的私有數據區域。JMM中規定全部變量都存儲在主內存,主內存是共享內存。線程對變量的操做在工做內存中進行,首先將變量從主內存拷貝到工做內存,操做完成後寫會主內存。不一樣線程間沒法訪問對方的工做內存,線程通訊(傳值)經過主內存來完成。小程序

JMM 對於同步的規定:api

  1. 線程解鎖前,必須把共享變量的值刷新回主內存
  2. 線程加鎖前,必須讀取主內存的最新值到本身的工做內存
  3. 加鎖解鎖是同一把鎖

JMM 的三大特性

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

原子性是不可分割,某個線程正在作某個具體業務時,中間不能夠被分割,要麼所有成功,要麼所有失敗。數組

重排序:計算機在執行程序時,爲了提升性能,編譯器和處理器經常對指令作重排序,源代碼通過編譯器優化重排序、指令並行重排序、內存系統的重排序以後獲得最終執行的指令。緩存

在單線程中保證程序最終執行結果和代碼執行順序執行結果一致。安全

多線程中線程交替執行,因爲重排序,兩個線程中使用的變量可否保證一致性沒法肯定,結果沒法肯定。bash

處理器在處理重排序時須要考慮數據的依賴性。服務器

volatile 實現禁止指令重排序,避免多線程環境下程序亂序執行。是經過內存屏障指令來執行的,經過插入內存屏障禁止在內存屏障後的指令執行重排序優化,並強制刷出緩存數據,保證線程能讀取到這些數據的最新版本。多線程

實例1:volatile 保證可見性

class MyData {
    //volatile int number = 0;//case2
    //int number=0; //case1
    public void change() {
        number = 60;
    }
}

public class VolatileDemo {
    public static void main(String[] args) {
        MyData data=new MyData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try{ TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
            data.change();
            System.out.println(Thread.currentThread().getName()+"\t updated number value:"+data.number);
        },"A").start();

        while(data.number==0){}
        System.out.println(Thread.currentThread().getName()+"\t over, get number:"+data.number);

    }
}
複製代碼

當咱們使用case1的時候,也就是number沒有volatile修飾的時候,運行結果:

A	 come in
A	 updated number value:60
複製代碼

而且程序沒有執行結束,說明在main線程中因爲不能保證可見性,一直在死循環。

當執行case2的時候:

A	 come in
A	 updated number value:60
main	 over, get number:60
複製代碼

保證了可見性,所以main成功結束。

實例2: volatile 不保證原子性

class MyData {
    volatile int number = 0;

    public void change() {
        number = 60;
    }

    public void addOne() {
        number++;
    }
}

public class VolatileDemo {
    public static void main(String[] args) {
        case2();
    }

    //驗證原子性
    public static void case2() {
        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.addOne();
                }
            }, String.valueOf(i)).start();
        }

        while(Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t number value:"+myData.number);
    }
}
複製代碼

最終輸出結果能夠發現並非 20000,且屢次輸出結果並不一致,所以說明 volatile 不能保證原子性。

如何保證原子性

  1. 加鎖:使用 synchronized 加鎖
  2. 使用 AtomicInteger

實例3:volatile 和 單例模式

DCL模式的單例模式

public class Singleton {

    private static Singleton instance=null;
    private Singleton(){
        System.out.println(Thread.currentThread().getName()+" constructor");
    }

    //DCL 雙端檢鎖機制
    public static Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null)
                    instance=new Singleton();
            }
        }
        return instance;
    }
}
複製代碼

DCL 機制不能徹底保證線程安全,由於有指令重排序的存在。

緣由在於instance = new Singleton(); 能夠分爲三步:

1. memory=allocate();//分配內存空間
2. instance(memory);//初始化對象
3. instance=memory;//設置instance指向分配的內存地址,分配成功後,instance!=null
複製代碼

因爲步驟2和步驟3不存在數據依賴關係,且不管重排序與否執行結果在單線程中沒有改變,所以這兩個步驟的重排序是容許的。也就是說指令重排序只會保證單線程串行語義的一致性(as-if-serial),可是不會關心多線程間的語義一致性。

所以,重排序以後,先執行3會致使instance!=null,可是對象還未被初始化。此時,別的線程在調用時,獲取了一個未初始化的對象。

所以,在聲明 instance 時,使用 volatile 進行修飾,禁止指令重排序。

private static volatile Singleton instance = null;
複製代碼

CAS

CAS 的全程是 CompareAndSwap,是一條 CPU 併發原語。它的功能是判斷內存某個位置的值是否爲預期值,若是是則更新爲新的值,這個過程是原子的

CAS 的做用是比較當前工做內存中的值和主內存中的值,若是相同則執行操做,不然繼續比較直到主內存和工做內存中的值一致爲止。主內存值爲V,工做內存中的預期值爲A,要修改的更新值爲B,當且僅當A和V相同,將V修改成B,不然什麼都不作。

CAS 底層原理:

在原子類中,CAS 操做都是經過 Unsafe 類來完成的。

//AtomicInteger i++
public final int getAndIncrement(){
    return unsafe.getAndAddInt(this,valueoffset,1);
}
複製代碼

其中 this 是當前對象, valueoffset 是一個 long ,表明地址的偏移量。

//AtomicInteger.java
private static final Unsafe unsfae=Unsafe.getUnsafe();//unsafe對象
private static final long valueOffset;//地址偏移量

static{
    try{
        valueoffset=unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value");
    }catch(Excepthion ex){throw new Error(ex);}
}

private volatile int value;//存儲的數值
複製代碼
  • Unsafe

Unsafe 類是 rt.jar 下的 sun.misc 包下的一個類,基於該類能夠直接操做特定內存的數據。

Java方法沒法直接訪問底層系統,須要使用 native 方法訪問,Unsafe 類的內部方法都是 native 方法,其中的方法能夠像C的指針同樣直接操做內存,Java 中的 CAS 操做的執行都依賴於 Unsafe 類的方法。

  • valueOffset

該變量表示變量值在內存中的偏移地址, Unsafe 就是根據內存偏移地址獲取數據的。

Unsafe類

CAS 併發源於體如今 Java 中就是 Unsafe 類的各個方法。調用該類中的 CAS 方法,JVM會幫咱們實現出 CAS 彙編指令,這是一種徹底依賴於硬件的功能。

原語是由若干條指令組成的,用於完成某個功能的過程。原語的執行必須是連續的,執行過程不容許被中斷。因此 CAS 是一條 CPU 的原子指令,不會形成數據不一致問題。

下邊是 AtomicInteger 中實現 i++ 功能所調用的 Unsafe 類的函數。

//unsafe.getAndAddInt
public final int getAndAddInt(Object var1,long var2,int var4){
    int var5;
    do{
        //獲取當前的值的地址
        var5=this.getIntVolatile(var1,var2);
        //var1表明對象,var2和var5分別表明當前對象的真實值和指望值,若是兩者相等,更新爲var5+var4
    }while(!this.compareAndSwapInt(var1,var2,var5,var5+var4);
    return var5;
}
複製代碼

在 getAndAddInt 函數中,var1 表明了 AtomicInteger 對象, var2 表明了該對象在內存中的地址, var4 表明了指望增長的數值。

首先經過 var1 和 var2 獲取到當前的主內存中真實的 int 值,也就是 var5。

而後經過循環來進行數據更改,當比較到真實值和對象的當前值相等,則更新,退出循環;不然再次獲取當前的真實值,繼續嘗試,直到成功。

在 CAS 中經過自旋而不是加鎖來保證一致性,同時和加鎖相比,提升了併發性。

具體情境來講:線程A和線程B併發執行 AtomicInteger 的自增操做:

  1. AtomicInteger 中的 value 原始值爲 3。主內存中 value 爲 3, 線程A和線程B的工做內存中有 value 爲 3 的副本;
  2. 線程 A 經過 getIntVolatile() 獲取到 value 的值爲3,並被掛起。
  3. 線程 B 也獲取到 value 的值爲3,而後執行 compareAndSwapInt 方法,比較到內存真實值也是 3,所以成功修改內存值爲4.
  4. 此時線程 A 繼續執行比較,發現對象中的 value 3 和主內存中的 value 4 不一致,說明已經被修改,A 從新進入循環。
  5. 線程 A 從新獲取 value,因爲 value 被 volatile 修飾,因此線程 A 此時 value 爲4,和主內存中 value 相等,修改爲功。

CAS的缺點

  1. 若是CAS失敗,會一直嘗試。若是CAS長時間不成功,會給CPU帶來很大的開銷。
  2. CAS 只能用來保證單個共享變量的原子操做,對於多個共享變量操做,CAS沒法保證,須要使用鎖。
  3. 存在 ABA 問題。

ABA問題

CAS 實現一個重要前提須要取出內存中某個時刻的數據並在當下時刻比較並替換,這個時間差會致使數據的變化。

線程1從內存位置V中取出A,線程2也從V中取出A,而後線程2經過一些操做將A變成B,而後又把V位置的數據變成A,此時線程1進行CAS操做發現V中仍然是A,操做成功。儘管線程1的CAS操做成功,可是不表明這個過程沒有問題。

這個問題相似於幻讀問題,經過新增版本號的機制來解決。在這裏可使用 AtomicStampedReference 來解決。

AtomicStampedReference

經過 AtomicStampedReference 來解決這個問題。

public class SolveABADemo {
    static AtomicStampedReference<Integer> atomicStampedReference=new AtomicStampedReference<>(100,1);

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

    new Thread(()->{
        int stamp=atomicStampedReference.getStamp();
        System.out.println(Thread.currentThread().getName()+"\t 版本號:"+stamp);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean ret=atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
        System.out.println(Thread.currentThread().getName()+"\t"+ret
            +" stamp:"+atomicStampedReference.getStamp()
            +" value:"+atomicStampedReference.getReference());
        },"t2").start();
    }
}
複製代碼
t1	 版本號:1
t2	 版本號:1
t1	 版本號:2
t1	 版本號:3
t2	false stamp:3 value:100
複製代碼

集合類的線程安全問題

ConcurrentModificationException

這個異常也就是併發修改異常,java.util.ConcurrentModificationException。

致使這個異常的緣由,是集合類自己是線程不安全的。

解決方案:

  1. 使用 Vector, Hashtable 等同步容器
  2. 使用 Collections.synchronizedxxx(new XX) 建立線程安全的容器
  3. 使用 CopyOnWriteList, CopyOnWriteArraySet, ConcurrentHashMap 等 j.u.c 包下的併發容器。

CopyOnWriteArrayList

底層使用了 private transient volatile Object[] array;

CopyOnWriteArrayList 採用了寫時複製、讀寫分離的思想。

public boolean add(E e){
    final ReentrantLock lock=this.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();
    }
}
複製代碼

添加元素時,不是直接添加到當前容器數組,而是複製到新的容器數組,向新的數組中添加元素,添加完以後將原容器引用指向新的容器。

這樣作的好處是能夠對該容器進行併發的讀,而不須要加鎖,由於讀時容器不會添加任何元素。

CopyOnWriteArraySet 自己就是使用 CopyOnWriteArrayList 來實現的。

Java鎖

公平鎖和非公平鎖

ReentrantLock 能夠指定構造函數的 boolean 類型獲得公平或非公平鎖,默認是非公平鎖,synchronized也是非公平鎖。

公平鎖是多個線程按照申請鎖的順序獲取鎖,是 FIFO 的。併發環境中,每一個線程在獲取鎖時先查看鎖維護的等待隊列,爲空則戰友,不然加入隊列。

非公平鎖是指多個線程不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。高併發狀況下可能致使優先級反轉或者飢餓現象。併發環境中,上來嘗試佔有鎖,嘗試失敗,再加入等待隊列。

可重入鎖(遞歸鎖)

可衝入鎖指的是同一線程外層函數獲取鎖以後,內層遞歸函數自動獲取鎖。也就是線程能進入任何一個它已經擁有的鎖所同步着的代碼塊

ReentrantLock 和 synchronized 都是可重入鎖。

可重入鎖最大的做用用來避免死鎖。

自旋鎖

自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式嘗試獲取鎖。好處是減小線程上下文切換的消耗,缺點是循環時會消耗CPU資源。

實現自旋鎖:

public class SpinLockDemo {
//使用AtomicReference<Thread>來更新當前佔用的 Thread
    AtomicReference<Thread> threadAtomicReference=new AtomicReference<>();

    public static void main(String[] args) {
        SpinLockDemo demo=new SpinLockDemo();
        new Thread(()->{
            demo.myLock();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            demo.myUnlock();
        },"t1").start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            demo.myLock();
            demo.myUnlock();
        },"t2").start();

    }

    public void myLock(){
        Thread thread=Thread.currentThread();
        System.out.println(Thread.currentThread().getName()+"\t come in");
        
        //若是當前佔用的線程爲null,則嘗試獲取更新
        while(!threadAtomicReference.compareAndSet(null,thread)){

        }
    }

    public void myUnlock(){
        Thread thread=Thread.currentThread();
        //釋放鎖,將佔用的線程設置爲null
        threadAtomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"\t unlocked");
    }
}

複製代碼

讀寫鎖

獨佔鎖:該鎖一次只能被一個線程持有,如 ReentrantLock 和 synchronized。

共享鎖:該鎖能夠被多個線程持有。

ReentrantReadWriteLock 中,讀鎖是共享鎖,寫鎖時獨佔鎖。讀讀共享保證併發性,讀寫互斥。

併發工具類

CountDownLatch

CountDownLatch 的做用是讓一些線程阻塞直到另一些線程完成一系列操做後才被喚醒。

CountDownLatch 在初始時設置一個數值,當一個或者多個線程使用 await() 方法時,這些線程會被阻塞。其他線程調用 countDown() 方法,將計數器減去1,當計數器爲0時,調用 await() 方法被阻塞的線程會被喚醒,繼續執行。

能夠理解爲,等你們都走了,保安鎖門。

CyclicBarrier

CyclicBarrier 是指能夠循環使用的屏障,讓一組線程到達一個屏障時被阻塞,直到最後一個線程到達屏障,屏障纔會開門,被屏障攔截的線程纔會繼續工做,線程進入屏障經過 await() 方法。

能夠理解爲,你們都到齊了,才能開會。

Semaphore

信號量用於:

  1. 多個共享資源的互斥使用
  2. 併發線程數的控制

能夠理解爲,多個車搶停車場的多個車位。當進入車位時,調用 acquire() 方法佔用資源。當離開時,調用 release() 方法釋放資源。

阻塞隊列

阻塞隊列首先是一個隊列,所起的做用以下:

  • 當阻塞隊列爲空,從隊列中獲取元素的操做將會被阻塞
  • 當阻塞隊列爲滿,向隊列中添加元素的操做將會被阻塞

試圖從空的阻塞隊列中獲取元素的線程將會被阻塞,直到其餘線程向空的隊列中插入新的元素。一樣的,試圖向已滿的阻塞隊列中添加新元素的線程一樣會被阻塞,直到其餘線程從隊列中移除元素使得隊列從新變得空閒起來並後序新增。

阻塞:阻塞是指在某些狀況下會掛起線程,即阻塞,一旦條件知足,被掛起的線程又會自動被喚醒。

優勢:BlockingQueue 能幫助咱們進行線程的阻塞和喚醒,而無需關心什麼時候須要阻塞線程,什麼時候須要喚醒線程。同時兼顧了效率和線程安全。

阻塞隊列的架構

BlokcingQueue 接口實現了 Queue 接口,該接口有以下的實現類:

  • ArrayBlockingQueue: 由數組組成的有界阻塞隊列
  • LinkedBlockingQueue: 由鏈表組成的有界阻塞隊列(默認大小爲 Integer.MAX_VALUE)
  • PriorityBlockingQueue:支持優先級排序的無界阻塞隊列
  • DelayQueue:使用優先級隊列實現的延遲無界阻塞隊列
  • SynchronousQueue: 不存儲元素的阻塞隊列,單個元素的隊列,同步提交隊列
  • LinkedTransferQueue:鏈表組成的無界阻塞隊列
  • LinkedBlockingDeque:鏈表組成的雙向阻塞隊列

阻塞隊列的方法

方法類型 拋出異常 特殊值 阻塞 超時
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
檢查 element() peek()
  • 拋出異常:當隊列滿,add(e)會拋出異常IllegalStateException: Queue full;當隊列空,remove()element()會拋出異常NoSuchElementException
  • 特殊值:offer(e)會返回 true/false。peek()會返回隊列元素或者null。
  • 阻塞:隊列滿,put(e)會阻塞直到成功或中斷;隊列空take()會阻塞直到成功。
  • 超時:阻塞直到超時後退出,返回值和特殊值中的狀況同樣。

生產者消費者模式

方式1. 使用Lock

class ShareData {
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws Exception {
        lock.lock();
        try {
            //判斷
            while (number != 0) {
                condition.await();
            }
            //幹活
            number++;
            System.out.println(Thread.currentThread().getName() + " produce\t" + number);
            //通知喚醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decrement()throws Exception{
        lock.lock();
        try {
            //判斷
            while (number == 0) {
                condition.await();
            }
            //幹活
            number--;
            System.out.println(Thread.currentThread().getName() + " consume\t" + number);
            //通知喚醒
            condition.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

/** * 一個初始值爲0的變量,兩個線程交替操做,一個加1一個減1,重複5次 * 1. 線程 操做 資源類 * 2. 判斷 幹活 通知 * 3. 防止虛假喚醒機制:判斷的時候要用while而不是用if */
public class ProduceConsumeTraditionalDemo {
    public static void main(String[] args) {
        ShareData data=new ShareData();

        new Thread(()->{
            for (int i = 0; i < 5 ; i++) {
                try {
                    data.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(()->{
            for (int i = 0; i < 5 ; i++) {
                try {
                    data.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}
複製代碼

打印結果

A produce	1
B consume	0
A produce	1
B consume	0
A produce	1
B consume	0
A produce	1
B consume	0
A produce	1
B consume	0
複製代碼

方法2:使用阻塞隊列

public class ProduceConsumeBlockingQueueDemo {
    public static void main(String[] args) {
        SharedData data=new SharedData(new ArrayBlockingQueue<>(10));
        new Thread(()-> {
            System.out.println(Thread.currentThread().getName() + "\t生產線程啓動");
            try {
                data.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Producer").start();
        new Thread(()-> {
            System.out.println(Thread.currentThread().getName() + "\t消費線程啓動");
            try {
                data.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"Consumer").start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        data.stop();
        System.out.println("中止");
    }
}

class SharedData{
    private volatile boolean FLAG=true;
    private AtomicInteger atomicInteger=new AtomicInteger();

    BlockingQueue<String> blockingQueue=null;

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

    public void produce() throws InterruptedException {
        String data=null;
        boolean ret;
        while(FLAG){
            data=""+atomicInteger.incrementAndGet();
            ret=blockingQueue.offer(data,2L,TimeUnit.SECONDS);
            if(ret){
                System.out.println(Thread.currentThread().getName()+"\t插入"+data+"成功");
            }else{
                System.out.println(Thread.currentThread().getName()+"\t插入"+data+"失敗");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println("生產結束,FLAG=false");
    }

    public void consume() throws InterruptedException {
        String ret=null;
        while(FLAG){
            ret=blockingQueue.poll(2L,TimeUnit.SECONDS);
            if(null==ret||ret.equalsIgnoreCase("")){
                System.out.println(FLAG=false);
                System.out.println(Thread.currentThread().getName()+"\t消費等待超時退出");
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t消費" + ret + "成功");
        }
    }

    public void stop(){
        FLAG=false;
    }


}

複製代碼

使用阻塞隊列+原子類+volatile變量的方式。 打印結果以下:

java.util.concurrent.ArrayBlockingQueue
Producer	生產線程啓動
Consumer	消費線程啓動
Producer	插入1成功
Consumer	消費1成功
Producer	插入2成功
Consumer	消費2成功
Producer	插入3成功
Consumer	消費3成功
中止
生產結束,FLAG=false
false
Consumer	消費等待超時退出
複製代碼

Synchronized 和 Lock 的區別

  1. 原始構成
    • Synchronized 是關鍵字,屬於JVM層面,底層是經過 monitorenter 和 monitorexit 完成,依賴於 monitor 對象來完成。因爲 wait/notify 方法也依賴於 monitor 對象,所以只有在同步塊或方法中才能調用這些方法。
    • Lock 是 java.util.concurrent.locks.lock 包下的,是 api層面的鎖。
  2. 使用方法
    • Synchronized 不須要用戶手動釋放鎖,代碼完成以後系統自動讓線程釋放鎖
    • ReentrantLock 須要用戶手動釋放鎖,沒有手動釋放可能致使死鎖。
  3. 等待是否能夠中斷
    • Synchronized 不可中斷,除非拋出異常或者正常運行完成
    • ReentrantLock 能夠中斷。一種是經過 tryLock(long timeout, TimeUnit unit),另外一種是lockInterruptibly()放代碼塊中,調用interrupt()方法進行中斷。
  4. 加鎖是否公平
    • synchronized 是非公平鎖
    • ReentrantLock 默認非公平鎖,能夠在構造方法傳入 boolean 值,true 表明公平鎖,false 表明非公平鎖。
  5. 鎖綁定多個 Condition
    • Synchronized 只有一個阻塞隊列,只能隨機喚醒一個線程或者喚醒所有線程。
    • ReentrantLock 用來實現分組喚醒,能夠精確喚醒。

案例:三個線程循環打印

class ShareData{
    private int number=1;
    private Lock lock=new ReentrantLock();

    public void printA(){
        lock.lock();
        Condition conditionA=lock.newCondition();
        try{
            while(number!=1){
                conditionA.await();
            }
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number=2;
            conditionA.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB(){
        lock.lock();
        Condition conditionB=lock.newCondition();
        try{
            while(number!=2){
                conditionB.await();
            }
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number=3;
            conditionB.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printC(){
        lock.lock();
        Condition conditionC=lock.newCondition();
        try{
            //判斷
            while(number!=3){
                conditionC.await();
            }
            //幹活
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName()+"\t"+i);
            }
            number=1;
            //通知
            conditionC.signal();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ShareData data=new ShareData();
        new Thread(() -> data.printA(),"A").start();
        new Thread(() -> data.printB(),"B").start();
        new Thread(() -> data.printC(),"C").start();
    }
}
複製代碼

線程池

建立線程

  1. 實現 Runnable 接口
  2. 實現 Callable 接口
  3. 繼承 Thread 類
  4. 使用線程池

Thread的構造函數中並無傳入 Callable 的方式,可是能夠傳入 Runnable 接口: Thread thread=new Thread(Runnable runnable, String name);。爲了使用 Callable 接口,咱們須要使用到 FutureTask 類。 FutureTask 類實現了 RunnableFuture 這一接口,而 RunnableFutre 又是 Future 的子接口,所以 FutureTask 能夠做爲參數使用上述的 Thread 構造函數。同時, FutureTask 自己構造函數能夠傳入 Callable 。

class MyThread implements Callable<Integer>{
    @Override
    public Integer call() {
        System.out.println("come in callable");
        return 2019;
    }
}
class Main{
    public static void main(String [] args){
        FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
        Thread t1=new Thread(futureTask,"A");
    }
}
複製代碼

線程池架構

除此以外,還有 Executors 工具類。

ThreadPoolExecutor

線程池有七大參數:

public ThreadPoolExecutor( int corePoolSize,//線程池常駐核心線程數 int maximumPoolSize,//線程池能容納同時執行最大線程數 long keepAliveTime,//多餘的空閒線程的存活時間,當前線程池線程數量超過core,空閒時間達到keepAliveTime,多餘空閒線程會被銷燬直到只剩下core個 TimeUnit unit, BlockingQueue<Runnable> workQueue,//被提交還沒有被執行的任務隊列 ThreadFactory threadFactory,//建立線程的線程工廠 RejectedExecutionHandler handler//拒絕策略 ) {...}
複製代碼

處理流程以下:

  1. 建立線程池,等待提交過來的任務請求。
  2. 添加請求任務
    • 若是運行線程數小於 corePoolSize,建立線程運行該任務
    • 若是運行線程數大於等於 corePoolSize,將任務放入隊列
    • 隊列滿,且運行線程數量小於 maximumPoolSize,建立非核心線程運行任務
    • 隊列滿,且運行線程數量大於等於 maximumPoolSize,線程池會啓動飽和拒絕策略執行。
  3. 線程完成任務,會從隊列中取下一個任務來執行
  4. 一個線程無事可作超過 keepAliveTime 時:
    • 若是當前運行線程數大於 corePoolSize,該線程被停掉
    • 線程池的全部任務完成後最終會收縮到 corePoolSize 的大小。

拒絕策略

在 JDK 中有四種內置的拒絕策略,均實現了 RejectedExecutionHandler 接口。

  • AbortPolicy: 直接拋出 RejectedExecutionException 異常,是默認的拒絕策略。
  • DiscardPolicy: 直接丟棄任務,不予處理也不拋出異常。若是容許任務丟失,是最好的處理策略。
  • DiscardOldestPolicy: 拋棄隊列中等待最久的任務,而後把當前任務加入隊列嘗試再次提交。
  • CallerRunsPolicy: 調用者運行。該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者。

三種經常使用線程池

  1. Executors.newFixedThreadPool(int)

建立固定容量的線程池,控制最大併發數,超出的線程在隊列中等待。

return new ThreadPoolExecutor(nThreads, nThreads, 
    0L, TimeUnit.MILLISECONDS, 
    new LinkedBlockingQueue<Runnable>());
複製代碼

其中 corePoolSize 和 maximumPoolSize 值是相等的,而且使用的是 LinkedBlockingQueue。

適用於執行長期的任務,性能比較高。

  1. Executors.newSingleThreadExecutor()

建立了一個單線程的線程池,只會用惟一的工做線程來執行任務,保證全部任務按照順序執行。

return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>()));
複製代碼

其中 corePoolSize 和 maximumPoolSize 都設置爲1,使用的也是 LinkedBlockingQueue。

適用於一個任務一個任務執行的場景。

  1. Executors.newCachedThreadPool()

建立了一個可緩存的線程池,若是線程池長度超過處理須要,能夠靈活回收空閒線程,沒有能夠回收的,則新建線程。

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
        60L, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());
複製代碼

設置 corePoolSize 爲0, maximumPoolSize 設置爲 Integer.MAX_VALUE,使用的是 SynchronousQueue。來了任務就建立線程執行,線程空閒超過60秒後銷燬。

適用於執行不少短時間異步的小程序或者負載比較輕的服務器。

工做中使用什麼樣的線程池

在阿里巴巴Java開發手冊中有以下規定:

  1. 線程資源必須經過線程池提供,不容許在應用中自行顯示建立線程。
    • 說明:使用線程池的好處是減小在建立和銷燬線程上消耗的時間和系統資源的開銷,解決資源不足的問題。若是不使用線程池,有可能形成系統建立大量同類線程致使消耗完內存或者過分切換。
  2. 線程池不容許使用 Executors 去建立,也就是不能使用上述的三種線程池,而是要經過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源韓進的風險。
    • FixedThreadPool 和 SingleThreadPool 都採用了 LinkedBlockingQueue,其容許的隊列長度爲 Integer.MAX_VALUE,可能堆積大量的請求,致使OOM。
    • CachedThreadPool 和 ScheduledThreadPool 容許建立的線程數量爲 Integer.MAX_VALUE,可能建立大量的線程,致使OOM。

如何設置線程池的線程數目

Runtime.getRuntime().availableProcessors()獲取當前設備的CPU個數。

  1. CPU密集型任務
    • CPU 密集的含義是任務須要大量的運算,而沒有阻塞,CPU一致全速運行
    • CPU 密集任務只有在真正的多核 CPU 上才能獲得加速(經過多線程),而在單核 CPU 上,不管開幾個模擬的多線程都不能獲得加速
    • CPU 密集型任務配置儘量少的線程數量,通常設置爲 CPU 核心數 + 1
  2. IO 密集型
    • IO 密集型,是指該任務須要大量的IO,大量的阻塞
    • 單線程上運行 IO 密集型的任務會致使浪費大量的 CPU 運算能力浪費在等待上
    • IO 密集型任務使用多線程能夠大大加速程序運行,利用了被浪費掉的阻塞時間
    • IO 密集型時,大部分線程都阻塞,須要多配置線程數,能夠採用CPU核心數 * 2,或者採用 CPU 核心數 / (1 - 阻塞係數),阻塞係數在0.8 ~ 0.9之間

死鎖

產生死鎖的緣由

死鎖是指兩個或兩個以上的進程在執行過程當中,由於爭奪資源形成的互相等待的現象。

死鎖須要滿族的四大條件以下:

  1. 互斥
  2. 循環等待
  3. 不可搶佔
  4. 佔有並等待

產生死鎖的主要緣由有:

  1. 系統資源不足
  2. 進程運行推動順序不當
  3. 資源分配不當

死鎖實例

class HoldLockThread implements Runnable{
    private String lock1;
    private String lock2;

    public HoldLockThread(String lock1, String lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        synchronized (lock1){
            System.out.println(Thread.currentThread().getName()+"\t持有"+lock1+"\t嘗試獲取"+lock2);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2){
                System.out.println(Thread.currentThread().getName()+"\t持有"+lock1+"\t嘗試獲取"+lock2);
            }
        }
    }
}

public class DeadLockDemo {
    public static void main(String[] args) {
        String lockA="lockA";
        String lockB="lockB";

        new Thread(new HoldLockThread(lockA,lockB),"Thread1").start();
        new Thread(new HoldLockThread(lockB,lockA),"Thread2").start();
    }
}

複製代碼

輸出以下結果,程序並無終止。

Thread2	持有lockB	嘗試獲取lockA
Thread1	持有lockA	嘗試獲取lockB
複製代碼

死鎖定位分析

使用 jps ,相似於 linux 中的 ps 命令。

在上述 java 文件中,使用 IDEA 中的 open In Terminal,或者在該文件目錄下使用 cmd 命令行工具。

首先使用 jps -l命令,相似於ls -l命令,輸出當前運行的 java 線程,從中能得知 DeadLockDemo 線程的線程號。

而後,使用jstack threadId來查看棧信息。輸出以下:

Java stack information for the threads listed above:
===================================================
"Thread2":
        at interview.jvm.deadlock.HoldLockThread.run(DeadLockDemo.java:22)
        - waiting to lock <0x00000000d6240328> (a java.lang.String)
        - locked <0x00000000d6240360> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)
"Thread1":
        at interview.jvm.deadlock.HoldLockThread.run(DeadLockDemo.java:22)
        - waiting to lock <0x00000000d6240360> (a java.lang.String)
        - locked <0x00000000d6240328> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
複製代碼
相關文章
相關標籤/搜索