1. 程式人生 > 實用技巧 >多執行緒併發程式設計面試常考

多執行緒併發程式設計面試常考

物件在記憶體中的記憶體佈局

sychronized鎖住物件後該物件的鎖狀態升級過程:new - 無鎖態 - 偏向鎖 - 輕量級鎖/自旋鎖/無鎖 (CAS)- 重量級鎖 - GC標記資訊


執行緒的幾個狀態
  • NEW(新建狀態)
  • Runnable
    • Ready(就緒狀態,執行緒被放在等待佇列中,等著被CPU執行)
    • Running(執行狀態,被扔到CPU中執行)
    • Blocked
    • Waiting
    • TimedWaiting
  • Terminated(終止態)
三種新建執行緒的方法
  • 實現Thread
  • 實現Runnable介面
  • 執行緒池
執行緒的常用方法:
  • sleep(),沉睡一段時間(當前執行緒回到就緒狀態),這段時間CPU執行其它執行緒
  • yield(),和sleep()類似,讓出CPU,當前執行緒回到就緒狀態。使用很少見
  • join(),通知其它執行緒獲得CPU執行,比如在t1執行緒內執行t2.join(),意思就是t1執行緒通知t2執行緒執行,自己回到就緒狀態。
Synchronized講解

synchronized實現過程:(不能禁止指令重排)
  • Java程式碼:synchronized
  • monitorentermoniterexit
  • 執行過程中自動升級(偏向鎖、自旋鎖、重量級鎖)
  • 更底層的實現lock comxchg
volatile講解:
  • 保證變數的各執行緒可見性/資料一致性 (多個執行緒要用到變數時,重新去記憶體拿)
  • 禁止CPU指令重排(在單執行緒沒問題,多執行緒就會出現問題。為什麼要指令重排,其實就是因為CPU太快了,而訪問記憶體比訪問快取又慢了太多)
    • 舉個例子:物件的初始化三個步驟Person p = new Person("zeng", 24);
      • 申請物件Person的記憶體,這個時候給例項變數設定了預設值,比如name = null; age = 0;
      • 呼叫該物件的建構函式進行真正的初始化例項變數name = "zeng"; age = 24;
      • 返回物件Personp
  • volatile不能實現synchronized的原子性操作
    • 比如定義一個變數volatile int count = 0;
      10個執行緒分別count++加1000次,最終的count不一定會是10000,因為這裡的count++並不是一個原子性操作,它包含好幾個指令,所以為了要實現整個的count++原子性操作,也就是必須要使用sychronizedcount++加鎖。
再注意一些問題:
  • 在用synchronized鎖住一個物件時,這個時候不能將這個引用去指向另一個物件
  • 不要用synchronized去鎖一個String、Integer等基本資料型別的封裝類的物件
CAS(無鎖優化/自旋):
  • CompareAndSwap
  • Java裡面java.util.concurrnet.atomic.AtomicXXX開頭的類都是使用CAS自旋鎖實現的。內部都是使用UnSafe這個類的compareAndSet等操作實現執行緒安全地修改值
    • 舉個例子:AtomicInteger count = new AtomicInteger(0);在上面的volatile的討論中,count++如果不加sychronized鎖會導致非原子性操作,但這裡直接使用AtomicInteger即可實現執行緒可見、原子性操作,將count++到10000。並且不需要volatile、synchronized
  • ABA問題(1變為2又被變為1),加版本號version
  • 所有的JavaCAS的操作基本上都是用的UnSafe這個類,這個UnSafe使Java語言有了像C++的直接操作JVM記憶體的能力。
ReentrantLock(可重入鎖,公平鎖(預設是非公平鎖))本身底層也是CAS
  • 可以替代synchronized,替換方法:lock.lock();
  • 可以通過lock.interupt的方法將該鎖設定為可以通過interup方法喚醒正在wait的執行緒
  • 相比上個特點,synchronized的執行緒,wait之後必須通過其它執行緒的notify()才能喚醒
  • 如果設定為公平鎖,那麼執行緒在搶一個資源時,會進入優先佇列排隊按先後順序等待
  • synchronized是非公平鎖
  • synchronized自動加鎖解鎖,ReentrantLock手動加鎖解鎖lock.lock()
  • 底層實現:ReentrantLock是CAS的實現,synchronized底層是有鎖的升級過程(4種)
CountDownLatch鎖(倒計時完了繼續執行(門栓))
CyclicBarrier鎖(當執行緒數目到達某個數目(柵欄值)時,繼續執行後面的事物)
Phase鎖(階段鎖,CyclicBarrier的升級版本,有多個階段,比如結婚現場有7個人,先7人到達現場,再7人吃完飯,再xxxxx)
ReadWriteLock(共享鎖、排他鎖、多個執行緒可以一起執行)
Semaphore(訊號量,用於限流(僅允許幾個執行緒同時工作))
Exchanger(兩個執行緒執行時交換值)
LockSupport(可以通過park()方法隨時將執行緒停止,並通過unpark()方法隨時讓某執行緒就緒)
面試題1:定義兩個執行緒,A執行緒往容器裡放資料,B執行緒監測容器容量為5時,停止執行
  • 有3種方法
  • 使用wait()notify()方法的組合。這個很重要
  • 使用門栓鎖CountDownLatch
  • 使用LockSupport直接park()unpark()
面試題2:順序列印A1B2C3……
面試題3:生產者消費者問題
版本1 通過synchronized、wait()、notify()實現
package zr.thread;

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

/*
    生產者與消費者實現1

    寫一個固定容量同步容器,擁有put和get方法, 以及getCount方法
    能夠支援2個生產者執行緒以及10個消費者執行緒的阻塞呼叫

    使用wait()和notifyAll()來實現

    這個方法是有瑕疵的,因為使用notifyAll()會喚醒所有的其它等待佇列的執行緒,包括生產者、消費者
    有沒有辦法只喚醒生產者,或者只喚醒消費者?

 */

/**
 * @author ZR
 * @Classname MyContainer1
 * @Description 生產者消費者最簡單寫法
 * @Date 2020/9/12 21:02
 */
public class MyContainer1<T> {
    final private LinkedList<T> lists = new LinkedList<>();
    // 最多10個元素
    final private int MAX = 10;
    private int count = 0;

    // 因為++count所以要加synchronized
    public synchronized void put(T t){
        // 想想為什麼用while而不是if
        while(lists.size() == MAX){
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        lists.add(t);
        ++count;
        // 通知所有消費者執行緒消費
        // 這個方法其實是有點小瑕疵的,因為notifyAll()會叫醒所有的其它wait()執行緒,也包括了另一個生產者
        this.notifyAll();
    }

    // 因為--count所以要加synchronized
    public synchronized T get(){
        T t = null;
        while(lists.size() == 0){
            try{
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        t = lists.removeFirst();
        --count;
        // 通知生產者進行生產
        // 這個方法其實是有點小瑕疵的,因為notifyAll()會叫醒所有的其它wait()執行緒,也包括了其它消費者
        this.notifyAll();
        return t;
    }

    public static void main(String[] args){
        MyContainer1<String> c = new MyContainer1<>();
        // 啟動消費者執行緒
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                for(int j = 0; j < 5; j++)
                    System.out.println(c.get());
            }, "customer" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 啟動生產者執行緒
        for(int i = 0; i < 2; i++){
            new Thread(()->{
                for(int j = 0; j < 25; j++)
                    c.put(Thread.currentThread().getName() + " " + j);
            }, "producer" + i).start();
        }
    }
}
版本2 通過ReentrantLock實現
package zr.thread;

import com.sun.org.glassfish.external.statistics.CountStatistic;

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

/**
 * @author ZR
 * @Classname MyContainer2
 * @Description TODO
 * @Date 2020/9/12 21:27
 */
public class MyContainer2<T> {

    final private LinkedList<T> lists = new LinkedList<>();
    // 最多10個元素
    final private int MAX = 10;
    private int count = 0;

    private Lock lock = new ReentrantLock();
    // Condition的本質就是等待佇列,在這裡生產者在生產者的佇列,消費者在消費者的佇列
    // 在Container1例中,等待佇列只有一個,生產者和消費者都在裡邊兒
    private Condition producer = lock.newCondition();
    private Condition customer = lock.newCondition();

    public void put(T t){
        try {
            // 需要手動加鎖
            lock.lock();
            while(lists.size() == MAX)
                producer.await();

            lists.add(t);
            ++count;
            // 通知消費者執行緒進行消費
            customer.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 手動解鎖
            lock.unlock();
        }
    }

    public T get(){
        T t = null;
        try {
            lock.lock();
            while(lists.size() == 0)
                customer.await();

            t = lists.removeFirst();
            --count;
            // 通知生產者執行緒生產
            producer.signalAll();
        } catch (InterruptedException e){
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return t;
    }


    public static void main(String[] args){
        MyContainer2<String> c = new MyContainer2<>();
        // 啟動消費者執行緒
        for(int i = 0; i < 10; i++){
            new Thread(()->{
                for(int j = 0; j < 5; j++)
                    System.out.println(c.get());
            }, "customer" + i).start();
        }

        try {
            TimeUnit.SECONDS.sleep(2);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 啟動生產者執行緒
        for(int i = 0; i < 2; i++){
            new Thread(()->{
                for(int j = 0; j < 25; j++)
                    c.put(Thread.currentThread().getName() + " " + j);
            }, "producer" + i).start();
        }
    }
}