免費(fèi)學(xué)習(xí)推薦:java基礎(chǔ)教程
Lock鎖與生產(chǎn)者消費(fèi)者問(wèn)題
- 傳統(tǒng)Synchronized鎖
- Lock鎖
- Synchronized和lock鎖的區(qū)別
- 傳統(tǒng)的生產(chǎn)者和消費(fèi)者問(wèn)題
- Lock版的生產(chǎn)者和消費(fèi)者問(wèn)題
- Condition實(shí)現(xiàn)精準(zhǔn)通知喚醒
傳統(tǒng)Synchronized鎖
實(shí)現(xiàn)一個(gè)基本的售票例子:
/* 真正的多線程開(kāi)發(fā),公司中的開(kāi)發(fā),降低耦合性 線程就是一個(gè)單獨(dú)的資源類,沒(méi)有任何附屬的操作 1.屬性,方法 * */public class SaleTicketDemo1 { public static void main(String[] args) { //并發(fā),多個(gè)線程操作同一個(gè)資源類,把資源類丟入線程 Ticket ticket=new Ticket(); //Runnable借口是一個(gè)FunationalInterface函數(shù)式接口,接口可以new,jdk1.8以后,lamda表達(dá)式()->{代碼} new Thread(()->{ for(int i=0;i<60;i++){ ticket.sale(); } },"A").start(); new Thread(new Runnable() { @Override public void run() { for(int i=0;i<60;i++){ ticket.sale(); } } },"B").start(); new Thread(()->{ for(int i=0;i<60;i++){ ticket.sale(); } },"C").start(); }}//資源類 OOPclass Ticket{ //屬性,方法 private int number=50; //賣票的方式 //synchronized本質(zhì):隊(duì)列,所 public synchronized void sale(){ if(number>0){ System.out.println(Thread.currentThread().getName()+"賣出了"+(number--)+"票,剩余"+number); } }}
注意,這里面用到了lambda表達(dá)式,lambda表達(dá)式詳細(xì)描述見(jiàn)Java基礎(chǔ)-Lambda表達(dá)式
這是使用傳統(tǒng)的synchronized實(shí)現(xiàn)并發(fā),synchronized的本質(zhì)就是隊(duì)列,鎖。就好比食堂排隊(duì)。如果沒(méi)有排隊(duì),就會(huì)很亂。只有給一個(gè)人服務(wù)完成了,另一個(gè)人才能接收到服務(wù)。
Lock鎖
之前已經(jīng)說(shuō)道,JVM提供了synchronized關(guān)鍵字來(lái)實(shí)現(xiàn)對(duì)變量的同步訪問(wèn)以及用wait和notify來(lái)實(shí)現(xiàn)線程間通信。在jdk1.5以后,JAVA提供了Lock類來(lái)實(shí)現(xiàn)和synchronized一樣的功能,并且還提供了Condition來(lái)顯示線程間通信。
Lock類是Java類來(lái)提供的功能,豐富的api使得Lock類的同步功能比synchronized的同步更強(qiáng)大。
在java.util. Concurrent包中,里面有3個(gè)接口,Condition,lock(標(biāo)準(zhǔn)鎖)。ReadWriteLock鎖(讀寫鎖)
Lock實(shí)現(xiàn)提供比使用synchronized方法和語(yǔ)句可以獲得的更廣泛的鎖定操作。 它們?cè)试S更靈活的結(jié)構(gòu)化,可能具有完全不同的屬性,并且可以支持多個(gè)相關(guān)聯(lián)的對(duì)象Condition。
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
lock()表示加鎖,unlock()表示解鎖
JDK官方文檔中解釋
所有已知實(shí)現(xiàn)類:
ReentrantLock可重入鎖
ReentrantReadWriteLock.ReadLock 讀鎖
ReentrantReadWriteLock.writeLock寫鎖
先說(shuō)ReentrantLock實(shí)現(xiàn)類:
ReentrantLock底層源碼構(gòu)造函數(shù)
公平鎖:十分公平,可以先來(lái)后到。但是問(wèn)題如果一個(gè)3s和一個(gè)3h的進(jìn)程到達(dá),3h先,那么3s等3h,實(shí)際上也不利。
非公平鎖:十分不公平,可以插隊(duì)(默認(rèn))
之后,我們會(huì)具體解釋。
怎么用,用之前加鎖,用之后解鎖
//lock鎖三部曲
//1.new ReentranLock();構(gòu)造
//2.Lock.lock();加鎖
//3.finally();解鎖
public class SaleTicketDemo2 { public static void main(String[] args) { //并發(fā),多個(gè)線程操作同一個(gè)資源類,把資源類丟入線程 Ticket ticket=new Ticket(); //Runnable借口是一個(gè)FunationalInterface函數(shù)式接口,接口可以new,jdk1.8以后,lamda表達(dá)式()->{代碼} new Thread(()->{for(int i=0;i<60;i++)ticket.sale();},"A").start(); new Thread(()->{for(int i=0;i<60;i++)ticket.sale();},"B").start(); new Thread(()->{for(int i=0;i<60;i++)ticket.sale();},"C").start(); }}//資源類 OOP//lock鎖三部曲//1.new ReentranLock();//2.Lock.lock();加鎖//3.finally();解鎖class Ticket2{ //屬性,方法 private int number=50; //賣票的方式 //synchronized本質(zhì):隊(duì)列,所 Lock lock=new ReentrantLock(); public void sale(){ lock.lock(); try { //業(yè)務(wù)代碼 if(number>0){ System.out.println(Thread.currentThread().getName()+"賣出了"+(number--)+"票,剩余"+number); } } catch (Exception e) { // TODO: handle exception }finally{ lock.unlock(); } }}
Synchronized和lock鎖的區(qū)別
1.synchronized是內(nèi)置的java關(guān)鍵字,lock是一個(gè)Java類
2.synchronized無(wú)法判斷獲取鎖的狀態(tài),lock可以判斷是否獲取到了鎖
3.synchronized會(huì)自動(dòng)釋放鎖(a–),lock必須要手動(dòng)釋放鎖!如果不釋放鎖,會(huì)導(dǎo)致死鎖
4.Synchronized線程1(獲得鎖,阻塞),線程2(等待,傻傻的等)
lock.tryLock()嘗試獲取鎖,不一定會(huì)一直等待下去
5.Synchronized可重入鎖,不可以中斷的,非公平鎖。Lock,可重入鎖,可以判斷鎖,公平與非公平可以自己設(shè)置(可以自己設(shè)置)
6.synchronized適合少量的代碼同步問(wèn)題,lock鎖適合鎖大量的同步代碼
synchornized鎖對(duì)象和同步代碼塊方法
傳統(tǒng)的生產(chǎn)者和消費(fèi)者問(wèn)題
傳統(tǒng)的生產(chǎn)者和消費(fèi)者是基于Object類的wait、notify方法和synchronized關(guān)鍵字來(lái)實(shí)現(xiàn)的。
在面試的時(shí)候,手寫生產(chǎn)者消費(fèi)者代碼是很常見(jiàn)的事情。
面試筆試經(jīng)典問(wèn)題:
單例模式+排序算法+生產(chǎn)者消費(fèi)者+死鎖
生產(chǎn)者消費(fèi)者問(wèn)題synchronized版
線程之間的通信問(wèn)題:生產(chǎn)者和消費(fèi)者問(wèn)題 等待喚醒,通知喚醒
線程交替執(zhí)行 A B 操作同一個(gè)變量number=0
A num+1
B num-1
注意:加鎖的方法中,執(zhí)行的思路是判斷等待+業(yè)務(wù)+通知
package testConcurrent;/* 線程之間的通信問(wèn)題:生產(chǎn)者和消費(fèi)者問(wèn)題 等待喚醒,通知喚醒 線程交替執(zhí)行 A B 操作同一個(gè)變量number=0 A num+1 B num-1 * */public class A { public static void main(String[] args) { Data data =new Data(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"B").start(); }}//判斷等待+業(yè)務(wù)+通知class Data{ //數(shù)字。資源類 private int number=0; //+1。多線程的情況下一定要加鎖。 public synchronized void increment() throws InterruptedException{ //判斷是否需要等待,如果不需要,就需要干活進(jìn)行業(yè)務(wù)操作 if(number!=0){ //等于1的時(shí)候,需要等待,緩沖區(qū)只有1個(gè)空位置 //等待操作 this.wait(); } number++; //進(jìn)行業(yè)務(wù)操作 System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他線程,我+1完畢了 this.notify(); } //-1 public synchronized void decrement() throws InterruptedException{ if(number==0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他線程,我-1完畢了 this.notify(); }}
如圖,基本可以實(shí)現(xiàn)所要求的功能,但是這樣還會(huì)出現(xiàn)問(wèn)題,如果此時(shí)我再加上了兩個(gè)線程,則
new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"C").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"D").start();
這里結(jié)果中出現(xiàn)了2,輸出結(jié)果出現(xiàn)了問(wèn)題。為什么呢?
為什么if判斷會(huì)出現(xiàn)問(wèn)題:
if判斷只判斷一次。因?yàn)閕f判斷了之后,就已經(jīng)進(jìn)入了代碼的等待那一行,這時(shí),在wait下的線程可能有多個(gè),甚至包括生產(chǎn)者和消費(fèi)者。有可能某個(gè)生產(chǎn)者執(zhí)行完了之后,喚醒的是另一個(gè)生產(chǎn)者。
在我們的官方文檔中就給出了解釋
public final void wait(long timeout) throws InterruptedException
導(dǎo)致當(dāng)前線程等待,直到另一個(gè)線程調(diào)用此對(duì)象的notify()方法或notifyAll()方法,或指定的時(shí)間已過(guò)。
線程也可以喚醒,而不會(huì)被通知,中斷或超時(shí),即所謂的虛假喚醒 。 雖然這在實(shí)踐中很少會(huì)發(fā)生,但應(yīng)用程序必須通過(guò)測(cè)試應(yīng)該使線程被喚醒的條件來(lái)防范,并且如果條件不滿足則繼續(xù)等待。 換句話說(shuō),等待應(yīng)該總是出現(xiàn)在循環(huán)中,就像這樣:
synchronized (obj) { while (<condition does not hold>) obj.wait(timeout); ... // Perform action appropriate to condition }
注意點(diǎn):防止虛假喚醒問(wèn)題。
我們代碼中用的是if判斷,而應(yīng)該用while判斷
package testConcurrent;/* 線程之間的通信問(wèn)題:生產(chǎn)者和消費(fèi)者問(wèn)題 等待喚醒,通知喚醒 線程交替執(zhí)行 A B 操作同一個(gè)變量number=0 A num+1 B num-1 * */public class A { public static void main(String[] args) { Data data =new Data(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"B").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.increment(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"C").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { data.decrement(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } },"D").start(); }}//判斷等待+業(yè)務(wù)+通知class Data{ //數(shù)字。資源類 private int number=0; //+1 public synchronized void increment() throws InterruptedException{ //判斷是否需要等待,如果不需要,就需要干活進(jìn)行業(yè)務(wù)操作 while(number!=0){ //等于1的時(shí)候,需要等待,緩沖區(qū)只有1個(gè)空位置 //等待操作 this.wait(); } number++; //進(jìn)行業(yè)務(wù)操作 System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他線程,我+1完畢了 this.notify(); } //-1 public synchronized void decrement() throws InterruptedException{ while(number==0){ //等待 this.wait(); } number--; System.out.println(Thread.currentThread().getName()+"=>"+number); //通知其他線程,我-1完畢了 this.notify(); }}
Lock版的生產(chǎn)者和消費(fèi)者問(wèn)題
在synchronized版本中,我們使用了wait和notify來(lái)實(shí)現(xiàn)線程之間的同步
在lock中,
此時(shí)synchronized被lock替換了,那么wait和notify用什么來(lái)替換呢?
我們?cè)诠俜轿臋njava.util.concurrent.locks 中,找到Lock類,然后在底部找到了
Condition newCondition()
返回一個(gè)新Condition綁定到該實(shí)例Lock實(shí)例。
在等待條件之前,鎖必須由當(dāng)前線程保持。 呼叫Condition.await()將在等待之前將原子釋放鎖,并在等待返回之前重新獲取鎖。
然后我們?cè)賮?lái)了解Condition類
Condition 將 Object 監(jiān)視器方法(wait、notify 和 notifyAll)分解成截然不同的對(duì)象,以便通過(guò)將這些對(duì)象與任意 Lock 實(shí)現(xiàn)組合使用,為每個(gè)對(duì)象提供多個(gè)等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和語(yǔ)句的使用,Condition 替代了 Object 監(jiān)視器方法的使用。
一個(gè)Condition實(shí)例本質(zhì)上綁定到一個(gè)鎖。 要獲得特定Condition實(shí)例的Condition實(shí)例,請(qǐng)使用其newCondition()方法。
我們可以看到,使用的時(shí)候new一個(gè)Condition對(duì)象。然后用await替代wait,signal替換notify
代碼實(shí)現(xiàn)
//判斷等待+業(yè)務(wù)+通知
class Data2{ //數(shù)字。資源類 private int number=0; Lock lock=new ReentrantLock(); Condition condition=lock.newCondition(); //+1 public void increment() throws InterruptedException{ try { lock.lock(); //業(yè)務(wù)代碼 while(number!=0){ //等待 condition.await(); } number++; System.out.println(Thread.currentThread().getName()+"=>"+number); condition.signalAll(); //通知 } catch (Exception e) { // TODO: handle exception }finally{ lock.unlock(); } } //-1 public void decrement() throws InterruptedException{ try { lock.lock(); //業(yè)務(wù)代碼 while(number!=1){ //等待 condition.await(); } number--; System.out.println(Thread.currentThread().getName()+"=>"+number); condition.signalAll(); //通知 } catch (Exception e) { // TODO: handle exception }finally{ lock.unlock(); } } }
注意:主函數(shù)部分于最上面的代碼一樣。
這時(shí)候雖然說(shuō)是正確的,但是它是一個(gè)隨機(jī)分布的狀態(tài),現(xiàn)在我們希望它有序執(zhí)行,即A執(zhí)行完了執(zhí)行B,B執(zhí)行C,C完了執(zhí)行D。即精準(zhǔn)通知。
Condition實(shí)現(xiàn)精準(zhǔn)通知喚醒
Condition實(shí)現(xiàn)精準(zhǔn)的通知和喚醒
我們構(gòu)造三個(gè)線程,要求A執(zhí)行完了執(zhí)行B,B執(zhí)行完了執(zhí)行C,C執(zhí)行完了執(zhí)行D.
代碼思想:
//加多個(gè)監(jiān)視器,通過(guò)監(jiān)視器來(lái)判斷喚醒的是哪一個(gè)人
//設(shè)置多個(gè)同步監(jiān)視器,每個(gè)監(jiān)視器監(jiān)視一個(gè)線程
//實(shí)例:生產(chǎn)線,下單->支付->交易->物流
package testConcurrent;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/* A執(zhí)行完調(diào)用B,B執(zhí)行完調(diào)用C,C執(zhí)行完調(diào)用A * */public class C { public static void main(String[] args) { Data3 data=new Data3(); new Thread(()->{ for(int i=0;i<10;i++){ data.printA(); } },"A").start(); new Thread(()->{ for(int i=0;i<10;i++){ data.printB(); } },"B").start(); new Thread(()->{ for(int i=0;i<10;i++){ data.printC(); } },"C").start(); }}class Data3{ //資源類 private Lock lock=new ReentrantLock(); //加多個(gè)監(jiān)視器,通過(guò)監(jiān)視器來(lái)判斷喚醒的是哪一個(gè)人 //設(shè)置多個(gè)同步監(jiān)視器,每個(gè)監(jiān)視器監(jiān)視一個(gè)線程 //實(shí)例:生產(chǎn)線,下單->支付->交易->物流 private Condition condition1=lock.newCondition(); private Condition condition2=lock.newCondition(); private Condition condition3=lock.newCondition(); private int number=1; //1A 2B 3C public void printA(){ lock.lock(); try { //業(yè)務(wù),判斷->執(zhí)行->通知 while(number!=1){ //等待 condition1.await(); } System.out.println(Thread.currentThread().getName()+"=>AAAAAAA"); //喚醒,喚醒指定的人,B number=2; //精準(zhǔn)喚醒 condition2.signal(); } catch (Exception e) { // TODO: handle exception }finally{ lock.unlock(); } } public void printB(){ lock.lock(); try { //業(yè)務(wù),判斷->執(zhí)行->通知 while(number!=2){ //等待 condition2.await(); } System.out.println(Thread.currentThread().getName()+"=>BBBBBBB"); //喚醒,喚醒指定的人,C number=3; //精準(zhǔn)喚醒 condition3.signal(); } catch (Exception e) { // TODO: handle exception }finally{ lock.unlock(); } } public void printC(){ lock.lock(); try { //業(yè)務(wù),判斷->執(zhí)行->通知 while(number!=3){ //等待 condition3.await(); } System.out.println(Thread.currentThread().getName()+"=>CCCCCCC"); //喚醒,喚醒指定的人,A number=1; //精準(zhǔn)喚醒 condition1.signal(); } catch (Exception e) { // TODO: handle exception }finally{ lock.unlock(); } }}
相關(guān)學(xué)習(xí)推薦:java基礎(chǔ)