多线程与高并发(四):LockSupport,高频面试题,AQS源码,以及源码阅读方法论
補充幾道面試題
- 鎖升級過程:無鎖、偏向鎖、輕量級鎖、重量級鎖
- StampedLock 自己看一下
- 面試題:syn和Reentrantlock的區別?
LockSupport
LockSupport.park()當前線程停止
t.start()
LockSupport.unpark(t);讓線程繼續運行
原來想要停止一個線程,需要把線程加在某個鎖上
之前想要叫醒一個線程,需要notify
現在unpark可以先于park調用并且有效,而之前wait不能先于notify調用
LockSupport的park是怎么實現的?仍然是Unsafe類。
淘寶面試題1
實現一個容器,提供兩個方法 add,size ,寫兩個線程:
線程1,添加10個元素到容器中
線程2,實時監控元素個數,當元素個數到5個時,線程2給出提示并結束
- volatile 最好不要被用來修飾引用值。經過我們的測試,如果引用值指向的對象發生了改變,另外的線程是觀察不到的。
方法1(原題是這個方法,讓你填空,填notify還是wait)
/*** 曾經的面試題:(淘寶?)* 實現一個容器,提供兩個方法,add,size* 寫兩個線程,線程1添加10個元素到容器中,線程2實現監控元素的個數,當個數到5個時,線程2給出提示并結束* * 給lists添加volatile之后,t2能夠接到通知,但是,t2線程的死循環很浪費cpu,如果不用死循環,該怎么做呢?* * 這里使用wait和notify做到,wait會釋放鎖,而notify不會釋放鎖* 需要注意的是,運用這種方法,必須要保證t2先執行,也就是首先讓t2監聽才可以* * 閱讀下面的程序,并分析輸出結果* 可以讀到輸出結果并不是size=5時t2退出,而是t1結束時t2才接收到通知而退出* 想想這是為什么?* * notify之后,t1必須釋放鎖,t2退出后,也必須notify,通知t1繼續執行* 整個通信過程比較繁瑣* @author mashibing*/ package com.mashibing.juc.c_020_01_Interview;import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit;public class T04_NotifyFreeLock {//添加volatile,使t2能夠得到通知volatile List lists = new ArrayList();public void add(Object o) {lists.add(o);}public int size() {return lists.size();}public static void main(String[] args) {T04_NotifyFreeLock c = new T04_NotifyFreeLock();final Object lock = new Object();new Thread(() -> {synchronized(lock) {System.out.println("t2啟動");if(c.size() != 5) {try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("t2 結束");//通知t1繼續執行lock.notify();}}, "t2").start();new Thread(() -> {System.out.println("t1啟動");synchronized(lock) {for(int i=0; i<10; i++) {c.add(new Object());System.out.println("add " + i);if(c.size() == 5) {lock.notify();//釋放鎖,讓t2得以執行try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}, "t1").start();} }方法2(感覺這種解法有點偏離題意了,這是在同一個線程中進行add操作和size操作了)
/*** 曾經的面試題:(淘寶?)* 實現一個容器,提供兩個方法,add,size* 寫兩個線程,線程1添加10個元素到容器中,線程2實現監控元素的個數,當個數到5個時,線程2給出提示并結束* <p>* 給lists添加volatile之后,t2能夠接到通知,但是,t2線程的死循環很浪費cpu,如果不用死循環,該怎么做呢?* <p>* 這里使用wait和notify做到,wait會釋放鎖,而notify不會釋放鎖* 需要注意的是,運用這種方法,必須要保證t2先執行,也就是首先讓t2監聽才可以* <p>* 閱讀下面的程序,并分析輸出結果* 可以讀到輸出結果并不是size=5時t2退出,而是t1結束時t2才接收到通知而退出* 想想這是為什么?* <p>* notify之后,t1必須釋放鎖,t2退出后,也必須notify,通知t1繼續執行* 整個通信過程比較繁瑣* <p>* 使用Latch(門閂)替代wait notify來進行通知* 好處是通信方式簡單,同時也可以指定等待時間* 使用await和countdown方法替代wait和notify* CountDownLatch不涉及鎖定,當count的值為零時當前線程繼續運行* 當不涉及同步,只是涉及線程通信的時候,用synchronized + wait/notify就顯得太重了* 這時應該考慮countdownlatch/cyclicbarrier/semaphore** @author mashibing*/ package com.mashibing.juc.c_020_01_Interview;import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport;//TODO park unparkpublic class T07_LockSupport_WithoutSleep {// 添加volatile,使t2能夠得到通知volatile List lists = new ArrayList();public void add(Object o) {lists.add(o);}public int size() {return lists.size();}static Thread t1 = null, t2 = null;public static void main(String[] args) {T07_LockSupport_WithoutSleep c = new T07_LockSupport_WithoutSleep();t1 = new Thread(() -> {System.out.println("t1啟動");for (int i = 0; i < 10; i++) {c.add(new Object());System.out.println("add " + i);if (c.size() == 5) {LockSupport.unpark(t2);LockSupport.park();}}}, "t1");t2 = new Thread(() -> {System.out.println("t2啟動");//if (c.size() != 5) {LockSupport.park();//}System.out.println("t2 結束");LockSupport.unpark(t1);}, "t2");t2.start();t1.start();} }變例
- 用兩個線程交叉打印A1B2C3…Z26
- 用三個線程交叉打印1-100
淘寶面試題2:經典的生產者-消費者模型
寫一個固定容量同步容器,擁有put和get方法,以及getCount方法,能夠支持2個生產者線程以及10個消費者線程的阻塞調用。
(使用waut和notify/notifyAll來實現)
方法1
建議把這種寫法背過
/*** 面試題:寫一個固定容量同步容器,擁有put和get方法,以及getCount方法,* 能夠支持2個生產者線程以及10個消費者線程的阻塞調用* <p>* 使用wait和notify/notifyAll來實現** @author mashibing*/ package com.mashibing.juc.c_021_01_interview;import java.util.LinkedList; import java.util.concurrent.TimeUnit;public class MyContainer1<T> {final private LinkedList<T> lists = new LinkedList<>();final private int MAX = 10; //最多10個元素private int count = 0;public synchronized void put(T t) {while (lists.size() == MAX) { //想想為什么用while而不是用if?try {this.wait(); //effective java} catch (InterruptedException e) {e.printStackTrace();}}lists.add(t);++count;this.notifyAll(); //通知消費者線程進行消費}public synchronized T get() {T t = null;while (lists.size() == 0) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}t = lists.removeFirst();count--;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());}, "c" + 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);}, "p" + i).start();}} }方法2:使用ReentrantLock創建兩個Condition,本質上是兩個等待隊列
一定要動手寫!
/*** 面試題:寫一個固定容量同步容器,擁有put和get方法,以及getCount方法,* 能夠支持2個生產者線程以及10個消費者線程的阻塞調用* <p>* 使用wait和notify/notifyAll來實現* <p>* 使用Lock和Condition來實現* 對比兩種方式,Condition的方式可以更加精確的指定哪些線程被喚醒** @author mashibing*/ package com.mashibing.juc.c_021_01_interview;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;public class MyContainer2<T> {final private LinkedList<T> lists = new LinkedList<>();final private int MAX = 10; //最多10個元素private int count = 0;private Lock lock = new ReentrantLock();private Condition producer = lock.newCondition();private Condition consumer = lock.newCondition();public void put(T t) {try {lock.lock();while (lists.size() == MAX) { //想想為什么用while而不是用if?producer.await();}lists.add(t);++count;consumer.signalAll(); //通知消費者線程進行消費} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public T get() {T t = null;try {lock.lock();while (lists.size() == 0) {consumer.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());}, "c" + 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);}, "p" + i).start();}} }源碼閱讀規則
閱讀源碼不是一件容易的事!
- 理解別人的思路,要先讓自己的思維達到別人的高度
- 讀懂思路就可以了,不要讀得太細
- 框架源碼有很多設計模式(坦克一期:23種設計模式)
- 不要直接順序地把一整個類從頭讀到尾
跑不起來不讀:如果通過IDE自動跳轉的方式,直接跟靜態的源碼的話,由于多態的特性,你可能會跳到父類的實現中去,看不到真正的子類的實現。
無關細節略過:不要過于關心邊界條件
嘗試自己畫圖:嗯,畫圖!IDEA可以直接生成類圖,或者你可以自己畫,類似于:
AQS(CLH的變種)
AQS的底層是CAS+Volitile
公平鎖:上來先排隊
非公平鎖:上來直接搶鎖
state 根據子類不同的實現,取不同的意義。
AQS類中有一個內部類Node,里面裝的是它的成員變量Thread。很多Node組成一個雙向鏈表,就是一個等待隊列。
非公平的時候,如果搶不到鎖,就進入隊列繼續等待。
如果搶到了,就用CAS的方式設定當前線程為獨占這把鎖的線程。
總結
以上是生活随笔為你收集整理的多线程与高并发(四):LockSupport,高频面试题,AQS源码,以及源码阅读方法论的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM从入门到精通(三):热加载的实现原
- 下一篇: Redis实战(三):Redis的Lis