多線程併發編程面試常考

對象在內存中的內存佈局

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();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章