溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

深入淺析java中的鎖

發(fā)布時間:2020-11-12 15:45:37 來源:億速云 閱讀:99 作者:Leah 欄目:編程語言

這篇文章給大家介紹深入淺析java中的鎖,內(nèi)容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

在并發(fā)編程中,經(jīng)常遇到多個線程訪問同一個 共享資源 ,這時候作為開發(fā)者必須考慮如何維護數(shù)據(jù)一致性,在java中synchronized關(guān)鍵字被常用于維護數(shù)據(jù)一致性。synchronized機制是給共享資源上鎖,只有拿到鎖的線程才可以訪問共享資源,這樣就可以強制使得對共享資源的訪問都是順序的,因為對于共享資源屬性訪問是必要也是必須的,下文會有具體示例演示。

一.java中的鎖

一般在java中所說的鎖就是指的內(nèi)置鎖,每個java對象都可以作為一個實現(xiàn)同步的鎖,雖然說在java中一切皆對象, 但是鎖必須是引用類型的,基本數(shù)據(jù)類型則不可以 。每一個引用類型的對象都可以隱式的扮演一個用于同步的鎖的角色,執(zhí)行線程進入synchronized塊之前會自動獲得鎖,無論是通過正常語句退出還是執(zhí)行過程中拋出了異常,線程都會在放棄對synchronized塊的控制時自動釋放鎖。 獲得鎖的唯一途徑就是進入這個內(nèi)部鎖保護的同步塊或方法 。

正如引言中所說,對共享資源的訪問必須是順序的,也就是說當多個線程對共享資源訪問的時候,只能有一個線程可以獲得該共享資源的鎖,當線程A嘗試獲取線程B的鎖時,線程A必須等待或者阻塞,直到線程B釋放該鎖為止,否則線程A將一直等待下去,因此java內(nèi)置鎖也稱作互斥鎖,也即是說鎖實際上是一種互斥機制。

根據(jù)使用方式的不同一般我們會將鎖分為對象鎖和類鎖,兩個鎖是有很大差別的,對象鎖是作用在實例方法或者一個對象實例上面的,而類鎖是作用在靜態(tài)方法或者Class對象上面的。一個類可以有多個實例對象,因此一個類的對象鎖可能會有多個,但是每個類只有一個Class對象,所以類鎖只有一個。 類鎖只是一個概念上的東西,并不是真實存在的,它只是用來幫助我們理解鎖定的是實例方法還是靜態(tài)方法區(qū)別的 。

在java中實現(xiàn)鎖機制不僅僅限于使用synchronized關(guān)鍵字,還有JDK1.5之后提供的Lock,Lock不在本文討論范圍之內(nèi)。一個synchronized塊包含兩個部分:鎖對象的引用,以及這個鎖保護的代碼塊。如果作用在實例方法上面,鎖就是該方法所在的當前對象,靜態(tài)synchronized方法會從Class對象上獲得鎖。

二.synchronized使用示例

1.多窗口售票

假設(shè)一個火車票售票系統(tǒng),有若干個窗口同時售票,很顯然在這里票是作為多個窗口的共享資源存在的,由于座位號是確定的,因此票上面的號碼也是確定的,我們用多個線程來模擬多個窗口同時售票,首先在不使用synchronized關(guān)鍵字的情況下測試一下售票情況。

先將票本身作為一個共享資源放在單獨的線程中,這種作為共享資源存在的線程很顯然應該是實現(xiàn)Runnable接口,我們將票的總數(shù)num作為一個入?yún)魅耄看紊梢粋€票之后將num做減法運算,直至num為0即停止,說明票已經(jīng)售完了,然后開啟多個線程將票資源傳入。

public class Ticket implements Runnable{
   private int num;//票數(shù)量
   private boolean flag=true;//若為false則售票停止
   public Ticket(int num){
   this.num=num;
   }
   @Override
   public void run() {
   while(flag){
   ticket();
   }
   }
   private void ticket(){
   if(num<=0){
   flag=false;
   return;
   }
   try {
   Thread.sleep(20);//模擬延時操作
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   //輸出當前窗口號以及出票序列號
   System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--);
   }
  }
  public class MainTest {
   public static void main(String[] args) {
   Ticketticket = new Ticket(5);
   Threadwindow01 = new Thread(ticket, "窗口01");
   Threadwindow02 = new Thread(ticket, "窗口02");
   Threadwindow03 = new Thread(ticket, "窗口03");
   window01.start();
   window02.start();
   window03.start();
   }
  }

 程序的輸出結(jié)果如下:

  窗口02售出票序列號:5
  窗口03售出票序列號:4
  窗口01售出票序列號:5
  窗口02售出票序列號:3
  窗口01售出票序列號:2
  窗口03售出票序列號:2
  窗口02售出票序列號:1
  窗口03售出票序列號:0
  窗口01售出票序列號:-1

從上面程序運行結(jié)果可以看出不但票的序號有重號而且出票數(shù)量也不對,這種售票系統(tǒng)比12306可要爛多了,人家在繁忙的時候只是刷不到票而已,而這里的售票系統(tǒng)倒好了,出票比預計的多了而且會出現(xiàn)多個人爭搶做同一個座位的風險。如果是單個售票窗口是不會出現(xiàn)這種問題,多窗口同時售票就會出現(xiàn)爭搶共享資源因此紊亂的現(xiàn)象,解決該現(xiàn)象也很簡單,就是在ticket()方法前面加上synchronized關(guān)鍵字或者將ticket()方法的方法體完全用synchronized塊包括起來。

//方式一
  private synchronized void ticket(){
   if(num<=0){
   flag=false;
   return;
   }
   try {
   Thread.sleep(20);//模擬延時操作
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--);
  }
  //方式二
  private void ticket(){
   synchronized (this) {
   if (num <= 0) {
   flag = false;
   return;
   }
   try {
   Thread.sleep(20);//模擬延時操作
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   System.out.println(Thread.currentThread().getName() + "售出票序列號:" + num--);
   }
  }

再看一下加入synchronized關(guān)鍵字的程序運行結(jié)果:

  窗口01售出票序列號:5
  窗口03售出票序列號:4
  窗口03售出票序列號:3
  窗口02售出票序列號:2
  窗口02售出票序列號:1

從這里可以看出在實例方法上面加上synchronized關(guān)鍵字的實現(xiàn)效果跟對整個方法體加上synchronized效果是一樣的。 另外一點需要注意加鎖的時機也非常重要 ,本示例中ticket()方法中有兩處操作容易出現(xiàn)紊亂,一個是在if語句模塊,一處是在num–,這兩處操作本身都不是原子類型的操作,但是在使用運行的時候需要這兩處當成一個整體操作,所以synchronized將整個方法體都包裹在了一起。如若不然,假設(shè)num當前值是1,但是窗口01執(zhí)行到了num–,整個操作還沒執(zhí)行完成,只進行了賦值運算還沒進行自減運算,但是窗口02已經(jīng)進入到了if語句模塊,此時num還是等于1,等到窗口02執(zhí)行到了輸出語句的時候,窗口01的num–也已經(jīng)將自減運算執(zhí)行完成,這時候窗口02就會輸出序列號0的票。再者如果將synchronized關(guān)鍵字加在了run方法上面,這時候的操作不會出現(xiàn)紊亂或者錯誤,但是這種加鎖方式無異于單窗口操作,當窗口01拿到鎖進入run()方法之后,必須等到flag為false才會將語句執(zhí)行完成跳出循環(huán),這時候的num就已經(jīng)為0了,也就是說票已經(jīng)被售賣完了,這種方式摒棄了多線程操作,違背了最初的設(shè)計原則-多窗口售票。

2.懶漢式單例模式

創(chuàng)建單例模式有很多中實現(xiàn)方式,本文只討論懶漢式創(chuàng)建。在Android開發(fā)過程中單例模式可以說是最常使用的一種設(shè)計模式,因為它操作簡單還可以有效減少內(nèi)存溢出。下面是懶漢式創(chuàng)建單例模式一個示例:

public class Singleton {
   private static Singletoninstance;
   private Singleton() {
   }
   public static SingletongetInstance() {
   if (instance == null) {
   instance = new Singleton();
   }
   return instance;
   }
  }

如果對于多窗口售票邏輯已經(jīng)完全明白了的話就可以看出這里的實現(xiàn)方式是有問題的,我們可以簡單的創(chuàng)建幾個線程來獲取單例輸出對象的hascode值。

  com.sunny.singleton.Singleton@15c330aa
  com.sunny.singleton.Singleton@15c330aa
  com.sunny.singleton.Singleton@41aff40f

在多線程模式下發(fā)現(xiàn)會出現(xiàn)不同的對象,這種單例模式很顯然不是我們想要的,那么根據(jù)上面多窗口售票的邏輯我們在getInstance()方法上面加上一個synchronized關(guān)鍵字,給該方法加上鎖,加上鎖之后可以避免多線程模式下生成多個不同對象,但是同樣會帶來一個效率問題,因為不管哪個線性進入getInstance()方法都會先獲得鎖,然后再次釋放鎖,這是一個方面,另一個方面就是只有在第一次調(diào)用getInstance()方法的時候,也就是在if語句塊內(nèi)才會出現(xiàn)多線程并發(fā)問題,而我們卻索性將整個方法都上鎖了。討論到這里就引出了另外一個問題,究竟是synchronized方法好還是synchronized代碼塊好呢? 有一個原則就是鎖的范圍越小越好 ,加鎖的目的就是將鎖進去的代碼作為原子性操作,因為非原子操作都不是線程安全的,因此synchronized代碼塊應該是在開發(fā)過程中優(yōu)先考慮使用的加鎖方式。

public static SingletongetInstance() {
   if (instance == null) {
   synchronized (Singleton.class) {
   instance = new Singleton();
   }
   }
   return instance;
  }

這里也會遇到類似上面的問題,多線程并發(fā)下回生成多個實例,如線程A和線程B都進入if語句塊,假設(shè)線程A先獲得鎖,線程B則等待,當new一個實例后,線程A釋放鎖,線程B獲得鎖后會再次執(zhí)行new語句,同樣不能保證單例要求,那么下面代碼再來一個null判斷,進行雙重檢查上鎖呢?

public static SingletongetInstance() {
   if (instance == null) {
   synchronized (Singleton.class) {
   if(instance==null){
   instance = new Singleton();
   }
   }
   }
   return instance;
  }

該模式就是雙重檢查上鎖實現(xiàn)的單例模式,這里在代碼層面我們已經(jīng) 基本 保證了線程安全了,但是還是有問題的, 雙重檢查鎖定的問題是:并不能保證它會在單處理器或多處理器計算機上順利運行。雙重檢查鎖定失敗的問題并不歸咎于 JVM 中的實現(xiàn)bug,而是歸咎于java平臺內(nèi)存模型。內(nèi)存模型允許所謂的“無序?qū)懭搿?,這也是這些習語失敗的一個主要原因。 更為詳細的介紹可以參考 Java單例模式中雙重檢查鎖的問題 。所以單例模式創(chuàng)建比較建議使用惡漢式創(chuàng)建或者靜態(tài)內(nèi)部類方式創(chuàng)建。

3.synchronized不具有繼承性

我們可以通過一個簡單的demo驗證這個問題,在一個方法中順序的輸出一系列數(shù)字,并且輸出該數(shù)字所在的線程名稱,在父類中加上synchronized關(guān)鍵字,子類重寫父類方法測試一下加上synchronized關(guān)鍵字和不加關(guān)鍵字的區(qū)別即可。

public class Parent {
   public synchronized void test() {
   for (int i = 0; i < 5; i++) {
   System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   }
   }
  }

子類繼承父類Parent,重寫test()方法.

public class Child extends Parent {
   @Override
   public void test() {
   for (int i = 0; i < 5; i++) {
   System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   }
   }
  }

測試代碼如下:

final Child c = new Child();
  new Thread() {
   public void run() {
   c.test();
   };
  }.start();
  new Thread() {
   public void run() {
   c.test();
   };
  }.start();

 輸出結(jié)果如下:

  Parent Thread-0:0 Child Thread-0:0
  Parent Thread-0:1 Child Thread-1:0
  Parent Thread-0:2 Child Thread-0:1
  Parent Thread-0:3 Child Thread-1:1
  Parent Thread-0:4 Child Thread-0:2
  Parent Thread-1:0 Child Thread-1:2
  Parent Thread-1:1 Child Thread-0:3
  Parent Thread-1:2 Child Thread-1:3
  Parent Thread-1:3 Child Thread-0:4
  Parent Thread-1:4 Child Thread-1:4

通過輸出信息可以知道,父類Parent中會將單個線程中序列號輸出完成才會執(zhí)行另一個線程中代碼,但是子類Child中確是兩個線程交替輸出數(shù)字,所以synchronized不具有繼承性。

4.死鎖示例

死鎖是多線程開發(fā)中比較常見的一個問題。若有多個線程訪問多個資源時,相互之間存在競爭,就容易出現(xiàn)死鎖。下面就是一個死鎖的示例,當一個線程等待另一個線程持有的鎖時,而另一個線程也在等待該線程鎖持有的鎖,這時候兩個線程都會處于阻塞狀態(tài),程序便出現(xiàn)死鎖。

public class Thread01 extends Thread{
   private Object resource01;
   private Object resource02;
   public Thread01(Object resource01, Object resource02) {
   this.resource01 = resource01;
   this.resource02 = resource02;
   }
   @Override
   public void run() {
   synchronized(resource01){
   System.out.println("Thread01 locked resource01");
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   synchronized (resource02) {
   System.out.println("Thread01 locked resource02");
   }
   }
   }
  }
  public class Thread02 extends Thread{
   private Object resource01;
   private Object resource02;
   public Thread02(Object resource01, Object resource02) {
   this.resource01 = resource01;
   this.resource02 = resource02;
   }
   @Override
   public void run() {
   synchronized(resource02){
   System.out.println("Thread02 locked resource02");
   try {
   Thread.sleep(500);
   } catch (InterruptedException e) {
   e.printStackTrace();
   }
   synchronized (resource01) {
   System.out.println("Thread02 locked resource01");
   }
   }
   }
  }
  public class MainTest {
   public static void main(String[] args) {
   final Object resource01="resource01";
   final Object resource02="resource02";
   Thread01thread01=new Thread01(resource01, resource02);
   Thread02thread02=new Thread02(resource01, resource02);
   thread01.start();
   thread02.start();
   }
  }

執(zhí)行上面的程序就會一直等待下去,出現(xiàn)死鎖。當線程Thread01獲得resource01的鎖后,等待500ms,然后嘗試獲取resource02的鎖,但是此時resouce02鎖已經(jīng)被Thread02持有,同樣Thread02也等待了500ms嘗試獲取resouce01鎖,但是該所已經(jīng)被Thread01持有,這樣兩個線程都在等待對方所有的資源,造成了死鎖。

三.其它

關(guān)鍵字synchronized具有鎖重入功能,當一個線程已經(jīng)持有一個對象鎖后,再次請求該對象鎖時是可以得到該對象的鎖的,這種方式是必須的,否則在一個synchronized方法內(nèi)部就沒有辦法調(diào)用該對象的另外一個synchronized方法了。鎖重入是通過為每個所關(guān)聯(lián)一個計數(shù)器和一個占有它的線程,當計數(shù)器為0時,認為鎖是未被占有的。線程請求一個未被占有的鎖時,JVM會記錄鎖的占有者,并將計數(shù)器設(shè)置為1。如果同一個線程再次請求該鎖,計數(shù)器會遞增,每次占有的線程退出同步代碼塊時計數(shù)器會遞減,直至減為0時鎖才會被釋放。

在聲明一個對象作為鎖的時候要注意字符串類型鎖對象,因為字符串有一個常量池,如果不同的線程持有的鎖是具有相同字符的字符串鎖時,兩個鎖實際上同一個鎖。

關(guān)于深入淺析java中的鎖就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI