線程&鎖未完成----2021年1月19日19:17:45

: 記錄我學習到的關於線程與鎖的相關知識------@author 愛跳舞的小碼農 Bboy_fork

線程

依稀還記得背過一道面試題,實現線程的方式有哪些? 當時。。。

興致勃勃的背誦瞭如下

代碼

new Thread(()->{
   
   
    for (int i = 0; i < 1000; i++) {
   
   
        System.out.println("線程1----"+ i);
    }
}).start();

代碼

new Runnable() {
   
   
   @Override
   public void run() {
   
   
       for (int i = 0; i < 1000; i++) {
   
   
           System.out.println("線程2----"+ i);
       }
   }
}.run();

代碼

new Callable() {
   
   
    @Override
    public Object call() throws Exception {
   
   
        for (int i = 0; i < 1000; i++) {
   
   
            System.out.println("線程3----"+ i);
        }
        return null;
    }
}.call();

面試官:還有麼?
我: 恩,沒了,就這些。

面試官:那 callablerunable 的異同有哪些?
膨脹:
    相同——都是接口。都可以編寫多線程程序。都採用Thread.start()啓動線程
    不同——Runnable沒有返回值;Callable可以返回執行結果,是個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果;
面試官:好的。
面試官:線程狀態有哪些?
面試官:線程有哪些終止方式?
面試官:線程間如何通信?什麼是線程封閉。






當時沒感覺 現在想來 面試官內心: 小夥很一( la )般( ji )啊

很好 讓我們重新審視這些題

線程的相關使用及面試題

線程基礎

上代碼!
點開Thread類

public 
class Thread implements Runnable {
   
   

通過查看Thread類的構造方法能看到…

public Thread(Runnable target) {
   
   
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

以及:

    /**
     * If this thread was constructed using a separate
     * <code>Runnable</code> run object, then that
     * <code>Runnable</code> object's <code>run</code> method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of <code>Thread</code> should override this method.
     *
     * @see     #start()
     * @see     #stop()
     * @see     #Thread(ThreadGroup, Runnable, String)
     */
    @Override
    public void run() {
   
   
        if (target != null) {
   
   
            target.run();
        }
    }

還有:

    /**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
   
   

很好 Thread看起來就是皮包公司了 實際上還是 Runable 在幹活
準確的說 是幹Runable 手中的活
run方法什麼也沒做就是調一下Runable中的run,start最終也還是調一下run( 註釋裏寫的很清楚 )

那麼再點開 Runable 和 Callable

@FunctionalInterface
public interface Runnable {
   
   
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
   
   
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

很好! 簡潔的一匹 ,看不出來啥,讓我們正常使用一下。

Runnable:

public void runnableTest(){
   
   
    Runnable runnable = new Runnable() {
   
   
        @Override
        public void run() {
   
   
            System.out.println("runable 任務1 開始");
            try {
   
   Thread.sleep(4000L);}
            catch (InterruptedException e) {
   
   e.printStackTrace();}
            System.out.println("runable 任務1 結束");
        }
    };
    Runnable runnable2 = new Runnable() {
   
   
        @Override
        public void run() {
   
   
            System.out.println("runable 任務2 開始");
            try {
   
   Thread.sleep(3000L);}
            catch (InterruptedException e) {
   
   e.printStackTrace(); }
            System.out.println("runable 任務2 結束");
        }
    };

    new Thread(runnable).start();
    new Thread(runnable2).start();
}

Callable

public void callableTest() throws Exception {
   
   
     Callable callable1 = new Callable() {
   
   
         @Override
         public String call() {
   
   
             System.out.println("callable 任務1 開始");
             try {
   
   Thread.sleep(4000L);}
             catch (InterruptedException e) {
   
   e.printStackTrace();}
             System.out.println("callable 任務1 結束");
             return "任務1返回值";
         }
     };
     Callable callable2 = new Callable() {
   
   
         @Override
         public String call() {
   
   
             System.out.println("callable 任務2 開始");
             try {
   
   Thread.sleep(3000L);}
             catch (InterruptedException e) {
   
   e.printStackTrace(); }
             System.out.println("callable 任務2 結束");
             return "任務2返回值";
         }
     };

     FutureTask futureTask = new FutureTask<>(callable1);
     FutureTask futureTask2 = new FutureTask<>(callable2);
     new Thread(futureTask).start();
     new Thread(futureTask2).start();

     Object o = futureTask.get();
     Object o1 = futureTask2.get();
     System.out.println(o.toString() + o1.toString());
 }

很好!到這裏我們可以得出結論:
Runable還是比較稀鬆平常的 我們將要跑的代碼 放在run方法裏 然後交由Thread執行。
但是 Callable 呢?爲什麼還要再來一個FutureTask ?爲什麼他能夠獲取到返回值和異常?
好的 讓我們再點開她的源碼:


package java.util.concurrent;
import java.util.concurrent.locks.LockSupport;

public class FutureTask<V> implements RunnableFuture<V> {
   
   
    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;
    private Callable<V> callable;
......
    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask(Callable<V> callable) {
   
   
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
......
    /**
     * @throws CancellationException {@inheritDoc}
     */
    public V get() throws InterruptedException, ExecutionException {
   
   
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
......
    public void run() {
   
   
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
   
   
            Callable<V> c = callable;
            if (c != null && state == NEW) {
   
   
                V result;
                boolean ran;
                try {
   
   
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
   
   
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
   
   
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }

這就完了? 就這樣實現了?
你沒看懂?好的 我們重新寫一個 ForkFutureTask
首先確定我們要實現的結果 也就是模仿 copy 盜版 剽竊創意
以達到這樣的效果:


Callable callable = new Callable() {
   
   
    @Override
    public String call() throws Exception {
   
   
        return testNullDemoService.selectA(Id);
    }
};

ForkFutureTask futureTask = new ForkFutureTask(callable);
ForkFutureTask futureTask2 = new ForkFutureTask(() -> {
   
   
    return testNullDemoService.selectB(Id);
});

new Thread(futureTask).start();
new Thread(futureTask2).start();

讓我們仔細分析一下 FutureTask 的源碼
首先是構造 把傳進來的 Callable 放到 private Callable callable; 中
很常規的操作。
run方法:
    先是一頓猛的一看,還有點看不懂的操作,然後 try 塊中:



result = c.call();

很好,看來執行也就這樣了,實現問題不大;

下一個問題就是:
     它能拿到 返回值 ! 所以就產生了這樣的問題:
假如任務還沒執行完成 主線程就使用get方法獲取結果,怎麼辦?
     理智: 當然是等待、睡眠、阻塞;總之 停下來
那等待到什麼時候?由誰來喚醒?
     理智: 當然是等到任務執行完成,再喚醒它 繼續執行get方法,返回




好的 於是乎有了它:

import java.util.concurrent.Callable;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.LockSupport;

/**
 * 我自己的FutureTask
 * */
public class ForkFutureTask<T> implements Runnable {
   
   
    private Callable<T> callable;
    private int status = 0;
    LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

    //callable 內 業務代碼 執行完畢後的返回值
    T result;

    public ForkFutureTask(Callable<T> callable){
   
   
        this.callable = callable;
    }

    @Override
    public void run(){
   
   
        try {
   
   
            result = callable.call();
        } catch (Exception e) {
   
   
            e.printStackTrace();
        }finally {
   
   
            status = 1;
        }
        while (true){
   
   
            Thread waiter = waiters.poll();
            if(waiter == null){
   
   
                break;
            }
            LockSupport.unpark(waiter);
        }
    }

    public T get(){
   
   
        while (status != 1){
   
   
            waiters.add(Thread.currentThread());
            LockSupport.park(Thread.currentThread());
        }
        //1 代表結束
        return result;
    }
}

我們在進入get方法時如果還未執行完成 就將目前調用get方法這個線程加入等待隊列中,
等待call中執行完成,再unpark喚醒它。

是不是感覺沒那麼難,線程間的喚醒就這樣實現了,FutureTask也簡單的實現了…
小結:
     我們重寫 Runable和Callable中的方法 交由 Thread 執行 / FutureTask 包裝過後再 Thread ,用以實現開闢線程,來加速單個訪問的處理速度,FutureTask 也沒什麼大不了,不過是一個實現了Runable 的類 用來包裝任務。bala bala。。。。。。

常見面試題補充

弱智面試題:線程狀態有哪些?

這是送分題啊,直接掏出源碼:

/*
 * @since   JDK1.0
 */
public class Thread implements Runnable {
   
   
    public enum State {
   
   
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WAITING,
        TERMINATED;
    }

線程有哪些終止方式?

stop、interrupt、標誌位

stop會強行終止線程 後續的操作都不幹了 直接斃掉。所以 肯定有線程安全問題吖。
而且JDK早就不建議這麼幹了。

interrupt是官方推薦使用的。 正常interrupt 雖然還是終止了 但是後續的方法會執行。
當線程在調用wait()、wait(long) 活join sleep方法被阻塞,interrupt還是會生效,中斷狀態將清除,拋出InterruptedException。

標誌位,說來也是取巧

    private volatile static boolean flag = true;

    public static void main (String[] args) throws InterruptedException {
   
   
        new Thread(()->{
   
   
            try {
   
   
                while (flag){
   
   
                    System.out.println("run 1s");
                    Thread.sleep(1000L);
                }
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        }).start();

        Thread.sleep(3000L);
        flag = false;

線程間如何通信?什麼是線程封閉

線程之間協同、共享及通信。當然分原始辦法和正常辦法。
① 文件共享(就是寫到文件裏嘛 easy)
② 網絡共享()
③ 變量共享(共享變量不多說)
④ jdk提供的協調線程API



suspend/resume

帶鎖 所以死鎖的方式:【其成功的方式】:正常的調用,就是去掉鎖,但是還要考慮 執行順序的問題因爲順序不對也容易鎖住。

public void test01() throws InterruptedException {
   
   

    Thread consumerThread = new Thread(() -> {
   
   
        System.out.println("消費者線程內");
        synchronized (this) {
   
   
            System.out.println("消費者等待");
            Thread.currentThread().suspend();
            System.out.println("消費者被喚醒");
        }
        System.out.println("消費者執行完成");
    });
    consumerThread.start();
    System.out.println("生產者開始執行");
    Thread.sleep(2000L);
    System.out.println("生產者執行完成");
    synchronized (this){
   
   
        System.out.println("生產者試圖喚醒 通知 消費者");
        consumerThread.resume();
    }
}
/*
生產者開始執行
消費者線程內
消費者等待
生產者執行完成
*/
wait/notify

正常:

public void test02() throws InterruptedException {
   
   
    new Thread(() -> {
   
   
        while (box == null){
   
   
            synchronized (this){
   
   
                System.out.println("消費者線程:box裏沒東西 則進入等待");
                try {
   
   
                    this.wait();
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
            }
        }
        System.out.println("消費者線程:有對象,直接結束");
    }).start();

    System.out.println("生產者線程:生產對象");
    Thread.sleep(3000L);
    box = new Object();
    synchronized (this){
   
   
        this.notifyAll();
        System.out.println("生產者: 全部喚醒-->通知消費者");
    }
}

/*
生產者線程:生產對象
消費者線程:box裏沒東西 則進入等待
生產者: 全部喚醒-->通知消費者
消費者線程:有對象,直接結束
*/

因爲線程執行順序,會導致程序永久等待

public void test03() throws InterruptedException {
   
   
    new Thread(()->{
   
   
        System.out.println("消費者:判斷");
        while (box == null){
   
   
            System.out.println("生產者判斷爲空 處理...");
            try {
   
   
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
            synchronized (this){
   
   
                try {
   
   
                    System.out.println("消費者: 進入等待");
                    this.wait();
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
            }
        }
        System.out.println("已被喚醒,執行完成");
    }).start();

    Thread.sleep(1000L);
    box = new Object();
    synchronized (this){
   
   
        this.notifyAll();
        System.out.println("生產者: 喚醒全部 --> 通知消費者");
    }
}
/*
消費者:判斷
生產者判斷爲空 處理...
生產者: 喚醒全部 --> 通知消費者
消費者: 進入等待
*/

即: wait notifyAll 解決了 鎖的問題 進入wait 會自動釋放鎖, 但是仍有執行順序的問題
會導致一直等待

park/unpark

採用令牌的機制,並不會出現因爲執行順序 而出現死鎖。
例:

public void test04() throws InterruptedException {
   
   
    Thread consumerThread = new Thread(() -> {
   
   
        while (box == null){
   
   
            System.out.println("消費者: 進入等待");
            LockSupport.park();
        }
        System.out.println("解鎖 執行完畢");
    });
    consumerThread.start();

    Thread.sleep(3000L);
    box = new Object();
    System.out.println("生產者:已創建對象 並準備通知消費者");
    LockSupport.unpark(consumerThread);
    System.out.println("生產者:已通知");
}
/*
消費者: 進入等待
生產者:已創建對象 並準備通知消費者
生產者:已通知
解鎖 執行完畢
*/


因爲park & unpark 等待的時候 不會釋放鎖, 所以會有這種死鎖:

public void test05() throws InterruptedException {
   
   
    Thread consumerThread = new Thread(() -> {
   
   
        while (box == null){
   
   
            System.out.println("消費者: 進入等待");
            synchronized (this){
   
   
                LockSupport.park();
            }
        }
        System.out.println("消費者: 執行結束");
    });
    consumerThread.start();

    Thread.sleep(3000L);
    box = new Object();
    synchronized (this){
   
   
        LockSupport.unpark(consumerThread);
        System.out.println("生產者:通知消費者");
    }
}
/*
消費者: 進入等待
*/


unpark 不會疊加
弄多次unpark 但是 park第一次可以執行, 但第二次就會等待了
重:
使用 if 進行判斷 是有風險的 (我的代碼中已經改爲while)
它存在一個僞喚醒的可能, 這個是由更底層的原因導致的 在java的api中有說明,
官方推薦: 使用while循環進行判斷。





然後… … …! 很就容易就說完了,因爲這裏真的沒那麼多東西可說,但是你可以拽着面試官說啊!
比如方向1:
     FutureTask裏面真的沒啥好說的,要說它相對於Runnable就是解決了主線程對子線程的感知,就用到了比較常見的生產者-消費者模型。。。
【模型 代碼設計 】【先放着 再議 再議 寫這裏就跑題了,可以以後加個鏈接】
又或方向2:
利用這兩種 able 我們就可以創建線程…但是!線程也是資源!線程的創建也是消耗!爲了優化,爲了節約,爲了合理的限制,有了線程池這東西…池嘛…




線程池的基本使用

類型 名稱 描述
接口 public interface Executor { 最上層接口,定義了執行任務的方法execute
接口 public interface ExecutorService extends Executor { 繼承了Executor接口,拓展了Callable、Future、關閉方法
接口 public interface ScheduledExecutorService extends ExecutorService { 繼承了ExecutorService,增加了定時任務相關方法
實現類 public class ThreadPoolExecutor extends AbstractExecutorService {---------------------分割線---------------------public abstract class AbstractExecutorService implements ExecutorService { 基礎、標準的線程池實現
實現類 public class ScheduledThreadPoolExecutor extends ThreadPoolExecutor implements ScheduledExecutorService { 繼承了ThreadPoolExecutor,實現了ScheduledExecutorService中相關定時任務的方法

常見的有這些接口和這兩個實現類,我們就能夠一般簡單的創建一個線程池了。

    /**
     * 核心:5 最大:10   無界隊列,   超出核心線程數量的線程存活時間 5s,
     * 無界隊列 就是會把這個最大數量 弄得沒有意義,因爲他會一直緩存隊列中 等待執行。
     * */
    private void threadPoolExecutorTest1(){
   
   
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
        testCommon(threadPoolExecutor);
    }

以及使用線程池:

    @SneakyThrows
    public void testCommon(ThreadPoolExecutor threadPoolExecutor){
   
   
        for (int i = 0; i < 15; i++) {
   
   
            int n = i;
            threadPoolExecutor.submit(new Runnable() {
   
   
                @SneakyThrows
                @Override
                public void run() {
   
   
                    System.out.println("開始執行:"+ n );
                    Thread.sleep(3000L);
                    System.out.println("執行結束:"+ n );
                }
            });
            System.out.println("任務提交成功"+i);
        }
        
        Thread.sleep(500L);
        System.out.println("當前線程池線程數量爲:" + threadPoolExecutor.getPoolSize());
        System.out.println("當前線程池等待的數量爲:" + threadPoolExecutor.getQueue().size());
        Thread.sleep(15000L);
        System.out.println("當前線程池線程數量爲:" + threadPoolExecutor.getPoolSize());
        System.out.println("當前線程池等待的數量爲:" + threadPoolExecutor.getQueue());
    }

當然 這裏可以詳細說一下構造方法中的參數的具體含義:
分別是:核心線程數量、最大線程數量、超出核心線程數量的線程存活時間、其時間單位、緩存隊列。
當然還有其他重載構造 傳入ThreadFactory、RejectedExecutionHandler 啥的,不常用、不管、不想看。

當然,說完基礎的實現肯定就會說到其簡便的工具——Executors圖片描描描描描述
它給了幾個方法,創建一個 Single線程池、Fixed線程池、Cache線程池 等等。

        //創建一個固定大小、任務隊列容量無界的線程池。核心線程數=最大線程數。
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        //創建一個大小無界的緩衝線程池。它的任務隊列是一個同步隊列。任務加入到池中,如果池中有空閒線程,則用空閒線程執行,如無則創建新線程執行。
        //池中的線程空閒超過60秒,將被小鬼釋放。線程數隨任務的多少變化。適用於執行耗時較小的一部任務。池的核心線程數=0,最大線程數=integer.MAX_VALUE
        ExecutorService executorService1 = Executors.newCachedThreadPool();

        //只有一個線程來執行無界隊列的單一線程池。該線程池確保任務按加入的順序一個一個依次執行。當唯一的線程因任務異常中止時,將創建一個新的線程來繼續執行後續的任務。
        //與newFixedThreadPool(1)區別在於,單一線程池的池大小在newSingle中硬編碼,不能再改變。
        ExecutorService executorService2 = Executors.newSingleThreadExecutor();

        //能定時執行任務的線程池,該池的核心線程數由參數指定,最大線程數=integer.MAX_VALUE
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);

點進去後…

    public static ExecutorService newFixedThreadPool(int nThreads) {
   
   
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

    public static ExecutorService newCachedThreadPool() {
   
   
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

    public static ExecutorService newSingleThreadExecutor() {
   
   
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
   
   
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
   
   
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

好的、看到這裏 不難看出:Executors 不過是做了幾個創建線程池的工作;
例如
     緩存線程池:不過是沒有核心線程,最大值不限制(Integer.MAX_VALUE),無界隊列,創建出來的線程60s沒有被複用則會刪掉。
     計劃線程池:創建指定核心線程數、最大線程數不限(Integer.MAX_VALUE),0納秒回收,無界隊列。


值得一提的是: 最大線程數並不是直接意義上的最大線程數,線程數達到核心線程數後,則會分配向隊列中,隊列中存放不下的則會查看最大線程數,嘗試增加線程處理。最後是處理完畢,查看等待時間,銷燬線程。

代碼舉例:

    /**
     * 此,等待隊列爲3 自然兩個任務就會被拒絕執行。
     * */
    private void threadPoolExecutorTest2(){
   
   
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
   
   
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
   
   
                System.out.println("有任務被拒絕執行了");
            }
        });
        testCommon(threadPoolExecutor);
    }
結果:
任務提交成功0
開始執行:0
任務提交成功1
開始執行:1
任務提交成功2
開始執行:2
任務提交成功3
開始執行:3
任務提交成功4
任務提交成功5
開始執行:4
任務提交成功6
任務提交成功7
任務提交成功8
任務提交成功9
開始執行:8
任務提交成功10
開始執行:9
開始執行:10
任務提交成功11
開始執行:11
任務提交成功12
有任務被拒絕執行了
任務提交成功13
有任務被拒絕執行了
任務提交成功14
開始執行:12
當前線程池線程數量爲:10
當前線程池等待的數量爲:3
執行結束:4
執行結束:2
執行結束:1
執行結束:0
開始執行:7
執行結束:8
執行結束:9
執行結束:3
開始執行:6
開始執行:5
執行結束:11
執行結束:12
執行結束:10
執行結束:6
執行結束:7
執行結束:5
當前線程池線程數量爲:5
當前線程池等待的數量爲:[]

妥!線程池的使用看起來也說得很清晰嘍。

思考:架構師門會不會將它封裝/限制,線程畢竟是挺珍惜的資源嘛,肯定不能讓你隨便就new線程池不是。
根據和某架構師交流得到信息:
在實際開發中:很少有對線程池的限制
一是本身業務邏輯很多都是不需要 / 無法使用多線程的。比如後續操作需要用到前面操作返回的數據。
二就算有多線程的使用必要,現在前後端分離,多數也是前端同時調用多個達到多線程的效果/或者說是處理速度。
三、真真正正需要用到的時候… 寫唄 反正出問題能定位到你【囧】
所以、說一句。線程作爲項目裏的資源,注意節約使用哦





到這裏 線程就沒什麼好說的了 然後就需要引導!往鎖那裏 拐!
(正常說完線程 面試官可能也會往鎖這裏拐 畢竟鎖就是爲了給多線程數據做的解決方案)
     心機: 常規使用難度不大 幹就完了,但是併發量小的時候還能玩,多了就不美麗了。所以 鎖就有存在的必要了…balabala 反正就是話術 不僅是你沒啥好講的 需要轉到鎖上來,面試官肯定也想順便問問鎖的問題。

volatile

這個關鍵字基本都用過,我們也都知道:   它不是鎖!
它是不能夠保證線程安全的,它解決了變量可見性的問題

問題來了,什麼是可見性的問題呢?
它是怎麼解決可見性的問題的呢? 上圖!
在這裏插入圖片描述
CPU有緩存:
     L1 Cache(一級緩存):是CPU第一層高速緩存,分爲數據緩存和指令緩存。一般服務器CPU的L1緩存容量通常在32-4096KB。
     L2 由於L1級高速緩存容量的限制,爲了再次提高CPU的運算速度,在CPU外部放置一高速存儲器,即二級緩存。
     L3 現在都是內置的。而它的實際作用是:進一步降低內存延遲,同時提升大數據量計算時處理器的性能。多核共享一個L3緩存





【問到就簡單說說,詳細了沒用,也記不住】
多CPU讀取同樣的數據進行緩存,進行運算。最終以哪個CPU爲準?L3 有解決方案:
MESI協議:
它規定每條緩存有個狀態位,同時定義下面四個狀態。
修改態(Modified) —— 此cache行已被修改過(髒行),內容已不同於主存,爲此cache專有;
專有態(Exclusive) —— 此cache行內容同於主存,但不出現於其他cache中;
共享態(Shared) —— 此cache行內容同於主存,但也出現於其它cache中;
無效態(Invalid) —— 此cache行內容無效(空行)。






好的我們解釋了CPU有緩存這一事實,那麼就可以直接說了,我們讀到的數據,很可能是緩存哦,當然 更多的是可能其他線程修改 我們並不知道它修改了 所以就是錯誤的數據 ~ so,就有了可見性的問題,我們見到的可能並不是真實的數據。
小結:volatile關鍵字通過使用MESI協議 使用總線嗅探技術 來更新其他cpu中緩存的值,使新修改的值被讀取,使舊值失效。保證了我們讀到的是最新,正確的值,即可見性 讀 的保證。

另一方面:三種指令重排

【java編程語言的語義允許Java編譯器和微處理器進行執行優化,這些優化導致了與其交互的代碼不再同步,從而導致看似矛盾的行爲】

編譯器優化重排:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
指令級的並行重排:現代處理器採用了指令級並行技術來將多條指令重疊指令。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
內存系統的重排:對於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。
指令重排
栗子重述: 當CPU寫緩存時發現緩存區塊正在被其他CPU佔用,爲提高性能。可能將後面的讀先執行。它本身是有限制的,即as-if-serial語義:不管怎麼重排序,程序的執行結果不能被改變。編譯器,runtime和處理器都必須遵守as-if-serial語義。
也就是說:編譯器和處理器不會對存在數據依賴關係的操作重排序。單個CPU當然沒毛病,但是多核多線程中,沒有因果關聯的指令,無從判斷,可能出現亂序執行,導致程序運行結果錯誤。




而volatile就是在生成的指令序列前後插入內存屏障(Memory Barries)來禁止處理器重排序。
四種內存屏障:

屏障類型 簡稱 指令示例 說明
LoadLoad Barriers 讀·讀 屏障 Load1;LoadLoad;Load2 (Load1代表加載數據,Store1表示刷新數據到內存) 確保Load1數據的狀態鹹魚Load2及所有後續裝載指令的裝載。
StoreStore Barriers 寫·寫屏障 Store1;StoreStore;Store2 確保Store1數據對其他處理器可見(刷新到內存) 先於Store2及所有後續存儲指令的存儲。
LoadStore Barriers 讀·寫 屏障 Load1;LoadStore;Store2 確保Load1數據裝載先於Store2及所有後續的存儲指令刷新到內存
StoreLoad Barriers 寫·讀 屏障 Store1;StoreLoad;Load2 確保Store1數據對其他處理器變得可見(指刷新到內存) 先於Load2及所有後續裝載指令的裝載。 StoreLoad Barriers會使該屏障之前的所有內存訪問指令(存儲和裝載指令) 完成之後,才指令該屏障之後的內存訪問指令。

所以:volatile寫:

在每個volatile寫操作前面插入一個StoreStore(寫-寫 屏障)。
在每個volatile寫操作後面插入一個StoreLoad(寫-讀 屏障)。(圖畫的醜 見諒 不諒也忍着)
volatile寫 如何插入內存屏障

所以:volatile讀:

在每個volatile讀操作後面插入一個LoadLoad(讀-讀 屏障)。

在每個volatile讀操作後面插入一個LoadStore(讀-寫 屏障)。
在這裏插入圖片描述
到這裏 我們知道了volatile關鍵字都做了什麼並且它解決了什麼問題。

小結 回到原本:什麼是volatile關鍵字?:
即: 對CPU的兩種優化:緩存和指令重排序雖然提高了CPU的性能,但是帶來了這樣那樣的問題。而通過volatile關鍵字我們屏蔽了這兩種優化 保證了變量的可見性。

synchronized

非公平、獨享鎖、悲觀鎖…
不多逼逼 簡單的很 直接上代碼:

	public synchronized void update(){
   
   i++;}
	//等價於
    public void updateBlock(){
   
   
        synchronized (this){
   
   i++;}
    }
    public static synchronized void staticUpdate(){
   
   i++;}
	//等價於
    public static void staticUpdateBlock(){
   
   
        synchronized (Solution.class){
   
   i++;}
    }

總說:給***對象上鎖,思考:

鎖在哪了? 怎麼鎖的?

答: 使用openJDK可以查看:org.openjdk.jol.info.ClassLayout

	ClassLayout layout= ClassLayout.parseInstance(t001);
	System.out.println(layout.toPrintable());

能夠看到其對象頭中的情況

com.julyDemo.test13.test001 object internals:
 OFFSET  SIZE                     TYPE DESCRIPTION                               VALUE
      0     4                          (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4                          (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                          (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4                      int test001.intttt                            5
     16     4         java.lang.String test001.tempStr1                          null
     20     4         java.lang.String test001.tempStr2                          null
     24     4         java.lang.String test001.tempStr3                          (object)
     28     4   com.julyDemo.test13.BB test001.b1                                (object)
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

好的。什麼是對象頭?

上圖!
sync原理

對象頭裏有什麼呢?

Mark Word 標誌位
Class Metadata Address 字面意思,地址
Array Length 數組長度;當其爲數組時,標記數組長度,平時就是0。 沒啥用,或者說用處不大

那麼標誌位中有什麼呢?
對象頭裏有什麼呢
mark World:描述當前狀態
標誌位裏有什麼


所以 我們有這麼幾種鎖

偏向鎖
輕量級鎖
重量級鎖…
具體是怎麼操作的呢? 好 上圖! 鎖升級流程:
鎖升級
JDK6以後,默認開啓偏向鎖。
可通過JVM參數: -XX:-UseBiasedLocking 來禁用偏向鎖開啓,若偏向鎖開啓,只有一個線程搶鎖,可獲取到偏向鎖。
    流程如上圖,解讀:
1、線程發現開啓偏向鎖( 0 / 1)①,則獲取偏向鎖,
2、新加入線程爭搶,升級爲輕量級鎖,沒有搶到的線程進行自旋搶鎖,
3、自旋到一定,仍未搶到,升級爲重量鎖,執行完後重量鎖降爲未鎖定。或搶到則繼續輕量鎖,最終降級爲未鎖定狀態。【重量直接降爲未鎖定,不是也不能降爲輕量再未鎖定】









    小結 :偏向鎖,就是單線程情況 加鎖解鎖比較快。多線程來爭搶時候也不影響。這就是一種錦上添花的優化。它本身是無鎖狀態、是樂觀鎖的體現。

ObjectMonitor(對象監視器)裏都有什麼呢?
對象創建的時候 會出現這麼一個Monitor 具體什麼時間出現的。哪行代碼 是真的沒找到。
但在這個對象監視器裏都有:
ObjectMonitor裏有什麼
巴拉巴拉一大堆。我們這裏用到的,就 entryList、waitSet之類的用來記錄當前。
所以 補充一個重量級鎖:的運行鎖定情況:
    重量級鎖的時候:mark word變爲指向對象監視器的地址。
Monitor中屬性owner:中保存着現在持鎖的線程(例:t1)。
EntryList:(鎖池)先進先出原則,其中保存着自旋後未搶到鎖的線程(例:t2、t3)。







wait方法語義: 釋放鎖

例1:
當t1正常執行完畢 —— 會執行monitor Exit 方法,隨後t2線程(因爲先進先出)獲得到鎖…
當t1執行了wait方法:—— t1進入waitSet ,owner=null。所以我們說 wait方法 釋放了鎖。
假設現在又執行了notfly():t1會和t2形成競爭(t3由於先進先出不具備競爭條件) 。t1 勝出:則t1再次獲得owner 一切又回到當年模樣。t2勝出:t2獲得owner,而t1則進入EntryList中。(所以我們又說synchronized他不是公平鎖)【因爲比如t1運行完畢釋放。同時t2準備爭搶,這時有新線程t4 t5 t6來搶鎖,那麼很明顯 t2不一定能搶到…所以】


synchronized鎖在方法上呢?
   它是一種隱式的控制,不是通過字節碼指令來控制的。是通過調用方法和返回這個方法時候來控制。詳細點:所有的方法呢,有一個方法常量池,在其中,通過“方法表結構”來區分這個方法是否是同步的。調用的時候呢回去檢查這個標誌ACC_SYNC 如果這個被設置了,也就是說這個方法時sync的了,那就先去持有Monitor,再執行方法,再去完成。不管正常完成異常完成,都會釋放掉這個Monitor。
但是我們絕大多數的時候 都會是持有對象鎖,極少會使用到方法上的鎖。所以不太需要。

輕量級鎖和重量級鎖的區別?
輕量級鎖時,未搶到的線程在自旋嘗試搶鎖。【佔用CPU資源】
重量級鎖時,搶owner,未搶到的線程會進入entryList掛起等待(這不也是自旋麼)【這樣就會對CPU的佔用沒那麼高】。

或許還有獨佔鎖、讀寫鎖、(非)/公平鎖、balabala

是的沒錯,但都是從不同角度來解讀 / (劃分) 鎖的。
比如sync是不是獨佔鎖呢?是的。
sync 和 ReentrantLock 都是非公平鎖。
    我們知道:所謂鎖的公平與否 就是線程們 是否按照申請鎖的順序來獲取鎖。先來後到,即爲公平。sync哪裏不公平了呢?答:sync重量鎖加鎖方式:鎖升級
比如所說的讀寫鎖…一番講述後我們可以自然的過渡到lock接口ReentrantLock等。。。
當然 這裏可以順便被問到簡單的面試題:
有哪些鎖分類:(很簡單不多贅述)





鎖分類:
    自旋鎖:是指當一個線程在獲取鎖的時候,如果鎖已經被其他線程獲取,那麼該線程將循環等待,然後不斷的判斷鎖是否能夠被成功獲取,知道獲取到鎖纔會退出循環。
    樂觀鎖:假定沒有衝突,在修改數據時如果發現數據和之前獲取的不一致,則讀最新數據,修改後嘗試修改 unsafe——
    悲觀鎖:假定會發生併發衝突,同步所有對數據的相關操作,從讀數據就開始上鎖。
    獨享鎖:給資源加上寫鎖,線程可以修改資源,其他線程不能再加鎖;(單寫)
    共享鎖:給資源加上讀鎖後只能讀不能改,其他線程也只能加讀鎖,不能加寫鎖;(多讀)
    可重入鎖、不可重入鎖:線程拿到一把鎖之後,可以自由進入同一把鎖所同步的其他代碼。
    公平鎖、非公平鎖:爭搶鎖的順序,如果是按先來後到,則爲公平。






LOCK

查看他們的結構和繼承實現關係我們能得出:
locks這包裏接口就兩個,Lock接口 和 ReadWriteLock接口,很簡單就這麼幾個方法,

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
/*
 * @since 1.5
 * @author Doug Lea
 */
public interface Lock {
   
   

    void lock();

    void lockInterruptibly() throws InterruptedException;
    
    boolean tryLock();

    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    void unlock();

    Condition newCondition();
}

以及:

package java.util.concurrent.locks;

/**
 * @since 1.5
 * @author Doug Lea
 */
public interface ReadWriteLock {
   
   
    /**
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * @return the lock used for writing
     */
    Lock writeLock();
}

而我們常用得,就是它的一些實現類。
Lock接口實現關係

接口 Lock:

它的最直接實現就是ReentrantLock(可重入鎖)了。

public class ReentrantLock implements Lock, java.io.Serializable {
   
   

接口ReadWriteLock:

ReentrantReadWriteLock (讀寫鎖)

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
   
   
    /**
     * The lock returned by method {@link ReentrantReadWriteLock#writeLock}.
     */
    public static class WriteLock implements Lock, java.io.Serializable {
   
   
    ......
    
    /**
     * The lock returned by method {@link ReentrantReadWriteLock#readLock}.
     */
    public static class ReadLock implements Lock, java.io.Serializable {
   
   

所以,可以說 我們使用的都是他給我們實現的Lock接口而已。

方法 描述
void lock() 一直獲取鎖,直到獲取到爲止
boolean tryLock() 嘗試獲取獲取不到就算了
boolean tryLock(long time,TimeUnit unit) throws InterruptedException 時間內嘗試,過了還沒拿到就算了
void lockInterruptibly() throws InterruptedException 可被外部中斷(他嘗試獲取鎖,如果線程被interrupt中斷了 那麼他就不等了)
void unlock() 解鎖
Condition newCondition() 返回Condition()

方法看起來都很簡單,那麼 Condition 是什麼呢?
    Condition中有一些方法。它本身是一個類似於監視器的東西。我們來看看使用一下。
Condition :

package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Date;

/**
 * @since 1.5
 * @author Doug Lea
 */
public interface Condition {
   
   

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    boolean awaitUntil(Date deadline) throws InterruptedException;

    void signal();

    void signalAll();
}
/**
 * ConditionTest
 * */
public class ConditionTest {
   
   

    private static Lock lock = new ReentrantLock();
    private static Condition condition =  lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
   
   
        Thread thread = new Thread() {
   
   
            @Override
            public void run() {
   
   
                try {
   
   
                    System.out.println("子線程休眠8s 後嘗試獲取鎖");
                    Thread.sleep(8000L);
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
                lock.lock();//將線程鎖定

                try {
   
   
                    System.out.println("當前線程" + Thread.currentThread().getName() + "獲得鎖");
                    condition.await();
                    System.out.println("當前線程" + Thread.currentThread().getName() + "開始執行");
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }finally {
   
   
                    lock.unlock();
                }
            }
        };

        thread.start();
        Thread.sleep(2000L);
        System.out.println("休眠2s,來控制線程");
        lock.lock();
        System.out.println("mian方法獲得鎖 並且馬上嘗試condition.signal");
        //但是這樣容易出現死鎖。 即:如果signal方法先於await調用的話
        condition.signal();//直接使用 會報錯 java.lang.IllegalMonitorStateException 因爲主線程拿不到這個鎖 所以前後加lock()unlock()
        lock.unlock();
    }
}

好的 好像明白了 來使用一下,簡單的實現一個阻塞隊列:
其中內容就用list來代替

public class Test {
   
   
    public static void main (String[] args) throws InterruptedException {
   
   
        ForkBlockingQueue forkBlockingQueue = new ForkBlockingQueue(6);
        new Thread(){
   
   
            @Override
            public void run() {
   
   
                for (int i = 0; i <20 ; i++) {
   
   
                    forkBlockingQueue.put("x"+i);
                }
            }
        }.start();

        Thread.sleep(1000L);
        System.out.println("開始取元素");
        for (int i = 0; i <10 ; i++) {
   
   
            forkBlockingQueue.take();
            Thread.sleep(2000);

        }
    }
}
class ForkBlockingQueue{
   
   

    List<Object> list = new ArrayList<>();
    private Lock lock = new ReentrantLock();
    private Condition putCondition = lock.newCondition(); //condition可以有多個,針對不同的操作放入不同condition,相當於等待隊列
    private Condition takeCondition = lock.newCondition();
    private int length;

    public ForkBlockingQueue(int length){
   
   
        this.length =length;
    }

    public void  put(Object obj){
   
   
        lock.lock(); 
        try{
   
   
            while (true) {
   
   
                if (list.size() < length) {
   
    //集合的長度不能超過規定的長度,才能向裏面放東西
                    list.add(obj);
                    System.out.println("隊列中放入元素:"+obj);
                    takeCondition.signal();
                    return;
                } else {
   
    //如果放不進去,就該阻塞. --利用condition實現
                    putCondition.await();//掛起
                }
            }
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        } finally {
   
   
            lock.unlock();
        }
    }

    public Object take(){
   
   
        lock.lock();
        try{
   
   
            while (true) {
   
   
                if (list.size() > 0) {
   
   
                    Object obj = list.remove(0);
                    System.out.println("隊列中取得元素:"+obj);
                    putCondition.signal();
                    return obj;
                } else {
   
   
                    takeCondition.await();
                }
            }
        } catch (InterruptedException e) {
   
   
            e.printStackTrace();
        } finally {
   
   
            lock.unlock();
            return null;
        }
    }
}

很好 使用起來看起來沒什麼問題 在試試lock 比如面試題:ReentrantLock 重複加鎖的問題。
答案當然很簡單:加鎖必然解鎖,不然肯定拿不到 能拿到的 都是本線程,也就是所謂的可重入鎖,解鎖之前必須加鎖 也就是不能沒有鎖去解鎖,不然會拋異常。
所以按照這樣的思想。我們就可以去嘗試實現一個自己的讀寫鎖了(其實是偷窺源碼和學習之後)
分析:
    ①:需要一個類似於sync中owner的東西來記錄是哪個線程拿到了鎖;(比如AtomicReference < Thread > )
    ②:需要一個count之類的數字來記錄這個線程加了多少次;(這個數字肯定要線程安全 比如AtomicInteger)
    ③:需要一個容器來存儲等待的線程,比如阻塞隊列(LinkedBlockingQueue);
    ④:實現Lock接口 其中方法。(lock ~ tryLock ~ unLock 等)
    ⑤:現實的 ReentrantLock 可以通過構造進行設置是否爲公平鎖,默認非公平
    ⑥:人話連起來就是:線程來了 判斷/嘗試對count進行CAS操作 (0,1) count的值代表了當前是否有線程獲取了鎖,owner記錄着這個線程,沒搶到的等待線程進入等待隊列阻塞,鎖釋放則喚醒頭部的線程來搶鎖,搶鎖機制同爲一個線程則可以多次獲得即count++,釋放鎖時候先-- 爲0順便清空owner。
實現 + 測試:









/**
 * 嘗試自己搞一個ReentrantLock
 * */
public class ForkReentrantLock implements Lock {
   
   

    //存放當前佔有鎖的線程 owner
    AtomicReference<Thread> owner = new AtomicReference<>();

    //重入鎖 所以要有記錄加鎖的次數。
    AtomicInteger count = new AtomicInteger();

    //未能佔有的線程,肯定要有一個等待隊列
    private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

    @Override
    public boolean tryLock() {
   
   
        int ct = count.get();
        if(ct ==0){
   
   //沒有被佔用
            if(count.compareAndSet(ct,ct+1)){
   
   
                owner.set(Thread.currentThread());
                return true;
            }
        }else {
   
   // 被佔用
            if(Thread.currentThread() == owner.get()){
   
    // 如果當前線程 就是佔有鎖的線程
                count.compareAndSet(ct,ct+1);
                return true;
            }
        }
        return false;
    }

    @Override
    public void lock() {
   
   
        if(!tryLock()){
   
   
            //加入等待隊列
            waiters.offer(Thread.currentThread());

            while (true){
   
   
                Thread head = waiters.peek();
                if(head == Thread.currentThread()){
   
   
                    if(!tryLock()){
   
   
                        LockSupport.park();
                    }else {
   
   
                        waiters.poll();
                        return;
                    }
                }else {
   
   
                    LockSupport.park();
                }
            }
        }
    }

    public boolean tryUnLock(){
   
   
        if(owner.get()!=Thread.currentThread()){
   
   //嘗試解鎖的 不是佔有鎖的線程
            throw new IllegalMonitorStateException();
        }else {
   
   //如果是
            int ct = count.get();
            int nextc = ct -1;
            count.set(nextc);

            if(nextc == 0){
   
   
                owner.compareAndSet(Thread.currentThread(),null);
                return true;
            }else {
   
   
                return false;
            }
        }
    }

    @Override
    public void unlock() {
   
   
        if(tryUnLock()){
   
   
            Thread head = waiters.peek();
            if(head != null){
   
   
                LockSupport.unpark(head);
            }
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
   
   

    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
   
   
        return false;
    }

    @Override
    public Condition newCondition() {
   
   
        return null;
    }
}

public class ForkReentrantLockTest {
   
   
    volatile static int i=0;
    static ForkReentrantLock lock = new ForkReentrantLock();
    public static void add(){
   
   
        lock.lock();
        i++;
        lock.unlock();
    }
    public static void main(String args[]) throws InterruptedException {
   
   
        for (int i=0; i<10; i++){
   
   
            new Thread(){
   
   
                @Override
                public void run() {
   
   
                    for (int j=0; j< 10000; j++){
   
   
                        //System.out.println(currentThread().getName()+ "....");
                        add();
                    }
                    System.out.println("done...");
                }
            }.start();
        }
        Thread.sleep(5000L);
        System.out.println(i);

    }
}

成功
那好 那我們再來看看ReadWriteLock
我們都知道:它維護一對關聯鎖 分讀和寫,讀可以多個讀線程同時持有,寫排他。並且同一時間不能被不同線程持有。 適合讀操作多餘寫入操作的場景,改進互斥鎖的性能…

一個小點: 讀寫鎖的鎖降級是——寫鎖降級成爲讀鎖,
       即 持有寫鎖的同時,再獲取讀鎖,隨後釋放寫鎖的過程。
寫鎖是線程獨佔,讀鎖是共享,所以寫->讀是降級(而讀-》寫是不能實現的 【因爲很多線程持有 怎麼實現嘛…】)

所以先使用一下:

/**
 * 讀寫鎖使用demo 將hashMap變成線程安全
 * */
public class ReadWriteLockTest {
   
   
    private final Map<String,Object> map = new HashMap<String,Object>();

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock read = readWriteLock.readLock();
    private final Lock write = readWriteLock.writeLock();

    public Object get(String key){
   
   
        read.lock();
        try {
   
   
            return map.get(key);
        }finally {
   
   
            read.unlock();
        }
    }

    public void put(String key,Object value){
   
   
        write.lock();
        try {
   
   
            map.put(key,value);
        }finally {
   
   
            write.unlock();
        }
    }

    public Object remove(String key){
   
   
        write.lock();
        try {
   
   
            return map.remove(key);
        }finally {
   
   
            write.unlock();
        }
    }
}

然後看看源碼 分析一下原理。怎麼實現讀寫鎖呢? 很好理解 ReentrantLock時是一個count,現在弄兩個count來分別用作readCount 和writeCount 不就完了嘛。當然了 在源碼中 是使用一個int值來進行記錄,即int有32位,16位記一個。
就這些麼?當然不是。

什麼是AQS呢?

AQS實現:ReentrantLock

源碼中這樣定義 AbstractQueuedSynchronizer :

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
   
   
    /**
     * Synchronization implementation for ReentrantReadWriteLock.
     * Subclassed into fair and nonfair versions.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
   
   

字面意思:抽象同步隊列。它就是使用的 模板方法模式
我的理解:模板方法幫你實現大部分 並且搭出框架,你使用的時候,只需要少部分的細節需要實現即可。
即: 定義一個算法的骨架,將骨架中的特定步驟延遲到子類中(注:特定步驟由子類實現)。
模板方法模式使得子類可以不改變算法的結構即可重新定義算法的某些特定步驟。


在這裏呢,AQS實現對同步狀態的管理,以及對阻塞線程排隊等待通知等一系列的處理。

所以 我們可以模仿弄一個MyAQS 例如 這樣:

/**
 * 提取出來的公共代碼,主要實現AQS的功能
 * @author Bboy_fork
 * @date 2021年1月11日21:36:56
 * */
public class AQSDemo {
   
   
    AtomicInteger readCount = new AtomicInteger();
    AtomicInteger writeCount = new AtomicInteger();

    //獨佔鎖 擁有者
    AtomicReference<Thread> owner = new AtomicReference<>();

    //等待隊列
    public volatile LinkedBlockingQueue<WaitNode> waiters = new LinkedBlockingQueue<WaitNode>();

    public class WaitNode{
   
   
        int type = 0;   //0 爲想獲取獨佔鎖的線程,  1爲想獲取共享鎖的線程 //ReadWriteLock用一個int值存儲了兩個count值

        Thread thread = null;
        int arg = 0;

        public WaitNode(Thread thread, int type, int arg){
   
   
            this.thread = thread;
            this.type = type;
            this.arg = arg;
        }
    }

    //獲取獨佔鎖
    public void lock() {
   
   
        int arg = 1;
        //嘗試獲取獨佔鎖,若成功,退出方法,    若失敗...
        if (!tryLock(arg)){
   
   
            //標記爲獨佔鎖
            WaitNode waitNode = new WaitNode(Thread.currentThread(), 0, arg);
            waiters.offer(waitNode);    //進入等待隊列

            //循環嘗試拿鎖
            for(;;){
   
   
                //若隊列頭部是當前線程
                WaitNode head = waiters.peek();
                if (head!=null && head.thread == Thread.currentThread()){
   
   
                    if (!tryLock(arg)){
   
         //再次嘗試獲取 獨佔鎖
                        LockSupport.park();     //若失敗,掛起線程
                    } else{
   
        //若成功獲取
                        waiters.poll();     //  將當前線程從隊列頭部移除
                        return;         //並退出方法
                    }
                }else{
   
     //若不是隊列頭部元素
                    LockSupport.park();     //將當前線程掛起
                }
            }
        }
    }

    //釋放獨佔鎖
    public boolean unlock() {
   
   
        int arg = 1;

        //嘗試釋放獨佔鎖 若失敗返回true,若失敗...
        if(tryUnlock(arg)){
   
   
            WaitNode next = waiters.peek(); //取出隊列頭部的元素
            if (next !=null){
   
   
                Thread th = next.thread;
                LockSupport.unpark(th);     //喚醒隊列頭部的線程
            }
            return true;                //返回true
        }
        return false;
    }

    //嘗試獲取獨佔鎖
    public boolean tryLock(int acquires) {
   
   
        throw new UnsupportedOperationException();
    }

    //嘗試釋放獨佔鎖
    public boolean tryUnlock(int releases) {
   
   
        throw  new UnsupportedOperationException();
    }

代碼中添加 UnsupportedOperationException() 來讓使用者自行實現這個方法。
      當然 abstract 也行。。。

最後 掏出源碼欣(折)賞(磨)一下

package java.util.concurrent.locks;
public class ReentrantReadWriteLock
//想啥呢 自己去翻源碼
//它裏面用的是一個鏈表,再有就是之前提到的用一個值去記錄。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章