java中的鎖

来源:http://www.cnblogs.com/5207/archive/2016/09/28/5917353.html
-Advertisement-
Play Games

java中有哪些鎖 這個問題在我看了一遍<java併發編程>後盡然無法回答,說明自己對於鎖的概念瞭解的不夠。於是再次翻看了一下書里的內容,突然有點打開腦門的感覺。看來確實是要學習的最好方式是要帶著問題去學,並且解決問題。 在java中鎖主要兩類:內部鎖synchronized和顯示鎖java.uti ...


java中有哪些鎖

這個問題在我看了一遍<java併發編程>後盡然無法回答,說明自己對於鎖的概念瞭解的不夠。於是再次翻看了一下書里的內容,突然有點打開腦門的感覺。看來確實是要學習的最好方式是要帶著問題去學,並且解決問題。

在java中鎖主要兩類:內部鎖synchronized和顯示鎖java.util.concurrent.locks.Lock。但細細想這貌似總結的也不太對。應該是由java內置的鎖和concurrent實現的一系列鎖。

為什麼這說,因為在java中一切都是對象,而java對每個對象都內置了一個鎖,也可以稱為對象鎖/內部鎖。通過synchronized來完成相關的鎖操作。

而因為synchronized的實現有些缺陷以及併發場景的複雜性,有人開發了一種顯式的鎖,而這些鎖都是由java.util.concurrent.locks.Lock派生出來的。當然目前已經內置到了JDK1.5及之後的版本中。

synchronized

首先來看看用的比較多的synchronized,我的日常工作中大多用的也是它。synchronized是用於為某個代碼塊的提供鎖機制,在java的對象中會隱式的擁有一個鎖,這個鎖被稱為內置鎖(intrinsic)或監視器鎖(monitor locks)。線程在進入被synchronized保護的塊之前自動獲得這個鎖,直到完成代碼後(也可能是異常)自動釋放鎖。內置鎖是互斥的,一個鎖同時只能被一個線程持有,這也就會導致多線程下,鎖被持有後後面的線程會阻塞。正因此實現了對代碼的線程安全保證了原子性。

可重入

既然java內置鎖是互斥的而且後面的線程會導致阻塞,那麼如果持有鎖的線程再次進入試圖獲得這個鎖時會如何呢?比如下麵的一種情況:

public class BaseClass {
    public synchronized void do() {
        System.out.println("is base");
    }

}

public class SonClass extends BaseClass {
    public synchronized void do() {
      System.out.println("is son");
      super.do();
    }

}


SonClass son = new SonClass();
son.do();

此時派生類的do方法除了會首先會持有一次鎖,然後在調用super.do()的時候又會再一次進入鎖並去持有,如果鎖是互斥的話此時就應該死鎖了。

但結果卻不是這樣的,這是因為內部鎖是具有可重入的特性,也就是鎖實現了一個重入機制,引用計數管理。當線程1持有了對象的鎖a,此時會對鎖a的引用計算加1。然後當線程1再次獲得鎖a時,線程1還是持有鎖a的那麼計算會加1。當然每次退出同步塊時會減1,直到為0時釋放鎖。

synchronized的一些特點

修飾代碼的方式

  • 修飾方法
public class BaseClass {
    public synchronized void do() {
        System.out.println("is base");
    }

}

這種就是直接對某個方法進行加鎖,進入這個方法塊時需要獲得鎖。

  • 修飾代碼塊
public class BaseClass {
    private static Object lock = new Object();
    public void do() {
        synchronized (lock) {
            System.out.println("is base");
        }
    }

}

這裡就將鎖的範圍減少到了方法中的部分代碼塊,這對於鎖的靈活性就提高了,畢竟鎖的粒度控制也是鎖的一個關鍵問題。

對象鎖的類型

經常看到一些代碼中對synchronized使用比較特別,看一下如下的代碼:

public class BaseClass {
    private static Object lock = new Object();
    public void do() {
        synchronized (lock) {
        }
    }
    
    public synchronized void doVoid() {
    }
    
    public synchronized static void doStaticVoid() {
    }
    
    public  static void doStaticVoid() {
        synchronized (BaseClass.class) {
        
        }
    }    

}

這裡出現了四種情況:修飾代碼塊,修飾了方法,修飾了靜態方法,修飾BaseClass的class對象。那這幾種情況會有什麼不同呢?

  • 修飾代碼塊

這種情況下我們創建了一個對象lock,在代碼中使用synchronized(lock)這種形式,它的意思是使用lock這個對象的內置鎖。這種情況下就將鎖的控制交給了一個對象。當然這種情況還有一種方式:

public void do() {
    synchronized (this) {
        System.out.println("is base");
    }
}

使用this的意思就是當前對象的鎖。這裡也道出了內置鎖的關鍵,我提供一把鎖來保護這塊代碼,無論哪個線程來都面對同一把鎖咯。

  • 修飾對象方法

這種直接修飾在方法是咱個情況?其實和修飾代碼塊類似,只不過此時預設使用的是this,也就是當前對象的鎖。這樣寫起代碼來倒也比較簡單明確。前面說過了與修飾代碼塊的區別主要還是控制粒度的區別。

  • 修飾靜態方法

靜態方法難道有啥不一樣嗎?確實是不一樣的,此時獲取的鎖已經不是this了,而this對象指向的class,也就是類鎖。因為Java中的類信息會載入到方法常量區,全局是唯一的。這其實就提供了一種全局的鎖。

  • 修飾類的Class對象

這種情況其實和修改靜態方法時比較類似,只不過還是一個道理這種方式可以提供更靈活的控制粒度。

小結

通過這幾種情況的分析與理解,其實可以看內置鎖的主要核心理念就是為一塊代碼提供一個可以用於互斥的鎖,起到類似於開關的功能。

java中對內置鎖也提供了一些實現,主要的特點就是java都是對象,而每個對象都有鎖,所以可以根據情況選擇用什麼樣的鎖。

java.util.concurrent.locks.Lock

前面看了synchronized,大部分的情況下差不多就夠啦,但是現在系統在併發編程中複雜性是越來越高,所以總是有許多場景synchronized處理起來會比較費勁。或者像<java併發編程>中說的那樣,concurrent中的lock是對內部鎖的一種補充,提供了更多的一些高級特性。

java.util.concurrent.locks.Lock簡單分析

這個介面抽象了鎖的主要操作,也因此讓從Lock派生的鎖具備了這些基本的特性:無條件的、可輪循的、定時的、可中斷的。而且加鎖與解鎖的操作都是顯式進行。下麵是它的代碼:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

ReentrantLock

ReentrantLock就是可重入鎖,連名字都這麼顯式。ReentrantLock提供了和synchronized類似的語義,但是ReentrantLock必須顯式的調用,比如:

public class BaseClass {
    private Lock lock = new ReentrantLock();

    public void do() {
        lock.lock();
        try {
        //....
        } finally {
          lock.unlock();
        }
        
    }

}

這種方式對於代碼閱讀來說還是比較清楚的,只不過有個問題,就是如果忘了加try finally或忘 了寫lock.unlock()的話導致鎖沒釋放,很有可能導致一些死鎖的情況,synchronized就沒有這個風險。

  • trylock

ReentrantLock是實現Lock介面,所以自然就擁有它的那些特性,其中就有trylock。trylock就是嘗試獲取鎖,如果鎖已經被其他線程占用那麼立即返回false,如果沒有那麼應該占用它並返回true,表示拿到鎖啦。

另一個trylock方法裡帶了參數,這個方法的作用是指定一個時間,表示在這個時間內一直嘗試去獲得鎖,如果到時間還沒有拿到就放棄。

因為trylock對鎖並不是一直阻塞等待的,所以可以更多的規避死鎖的發生。

  • lockInterruptibly

lockInterruptibly是線上程獲取鎖時優先響應中斷,如果檢測到中斷拋出中斷異常由上層代碼去處理。這種情況下就為一種輪循的鎖提供了退出機制。為了更好理解可中斷的鎖操作,寫了一個demo來理解。

package com.test;

import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

public class TestLockInterruptibly {
	static ReentrantLock lock = new ReentrantLock();
	
	public static void main(String[] args) {
		Thread thread1 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				try {
					doPrint("thread 1 get lock.");
					do123();
					doPrint("thread 1 end.");
					
				} catch (InterruptedException e) {
					doPrint("thread 1 is interrupted.");
				}
			}
		});

		Thread thread2 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				try {
					doPrint("thread 2 get lock.");
					do123();
					doPrint("thread 2 end.");					
				} catch (InterruptedException e) {
					doPrint("thread 2 is interrupted.");
				}
			}
		});
		
		thread1.setName("thread1");
		thread2.setName("thread2");
		thread1.start();
		try {
			Thread.sleep(100);//等待一會使得thread1會在thread2前面執行			
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		thread2.start();
	}
	
	
	private static void do123() throws InterruptedException {
		lock.lockInterruptibly();
		doPrint(Thread.currentThread().getName() + " is locked.");
		try {
			doPrint(Thread.currentThread().getName() + " doSoming1....");
			Thread.sleep(5000);//等待幾秒方便查看線程的先後順序
			doPrint(Thread.currentThread().getName() + " doSoming2....");

			doPrint(Thread.currentThread().getName() + " is finished.");
		} finally {
			lock.unlock();
		}
	}
	
	private static void doPrint(String text) {
		System.out.println((new Date()).toLocaleString() + " : " + text);
	}
}

上面代碼中有兩個線程,thread1比thread2更早啟動,為了能看到拿鎖的過程將上鎖的代碼sleep了5秒鐘,這樣就可以感受到前後兩個線程進入獲取鎖的過程。最終上面的代碼運行結果如下:

2016-9-28 15:12:56 : thread 1 get lock.
2016-9-28 15:12:56 : thread1 is locked.
2016-9-28 15:12:56 : thread1 doSoming1....
2016-9-28 15:12:56 : thread 2 get lock.
2016-9-28 15:13:01 : thread1 doSoming2....
2016-9-28 15:13:01 : thread1 is finished.
2016-9-28 15:13:01 : thread1 is unloaded.
2016-9-28 15:13:01 : thread2 is locked.
2016-9-28 15:13:01 : thread2 doSoming1....
2016-9-28 15:13:01 : thread 1 end.
2016-9-28 15:13:06 : thread2 doSoming2....
2016-9-28 15:13:06 : thread2 is finished.
2016-9-28 15:13:06 : thread2 is unloaded.
2016-9-28 15:13:06 : thread 2 end.

可以看到,thread1先獲得鎖,一會thread2也來拿鎖,但這個時候thread1已經占用了,所以thread2一直到thread1釋放了鎖後才拿到鎖。

**這段代碼說明lockInterruptibly後面來獲取鎖的線程需要等待前面的鎖釋放了才能獲得鎖。**但這裡還沒有體現出可中斷的特點,為此增加一些代碼:

thread2.start();
try {
	Thread.sleep(1000);			
} catch (InterruptedException e) {
	e.printStackTrace();
}	
//1秒後把線程2中斷
thread2.interrupt();

在thread2啟動後調用一下thread2的中斷方法,好吧,先跑一下代碼看看結果:

2016-9-28 15:16:46 : thread 1 get lock.
2016-9-28 15:16:46 : thread1 is locked.
2016-9-28 15:16:46 : thread1 doSoming1....
2016-9-28 15:16:46 : thread 2 get lock.
2016-9-28 15:16:47 : thread 2 is interrupted. <--直接就響應了線程中斷
2016-9-28 15:16:51 : thread1 doSoming2....
2016-9-28 15:16:51 : thread1 is finished.
2016-9-28 15:16:51 : thread1 is unloaded.
2016-9-28 15:16:51 : thread 1 end.

和前面的代碼相比可以發現,thread2正在等待thread1釋放鎖,但是這時thread2自己中斷了,thread2後面的代碼則不會再繼續執行。

ReadWriteLock

顧名思義就是讀寫鎖,這種讀-寫鎖的應用場景可以這樣理解,比如一波數據大部分時候都是提供讀取的,而只有比較少量的寫操作,那麼如果用互斥鎖的話就會導致線程間的鎖競爭。如果對於讀取的時候大家都可以讀,一旦要寫入的時候就再將某個資源鎖住。這樣的變化就很好的解決了這個問題,使的讀操作可以提高讀的性能,又不會影響寫的操作。

一個資源可以被多個讀者訪問,或者被一個寫者訪問,兩者不能同時進行。

這是讀寫鎖的抽象介面,定義一個讀鎖和一個寫鎖。

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

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

在JDK里有個ReentrantReadWriteLock實現,就是可重入的讀-寫鎖。ReentrantReadWriteLock可以構造為公平的或者非公平的兩種類型。如果在構造時不顯式指定則會預設的創建非公平鎖。在非公平鎖的模式下,線程訪問的順序是不確定的,就是可以闖入;可以由寫者降級為讀者,但是讀者不能升級為寫者。

如果是公平鎖模式,那麼選擇權交給等待時間最長的線程,如果一個讀線程獲得鎖,此時一個寫線程請求寫入鎖,那麼就不再接收讀鎖的獲取,直到寫入操作完成。

  • 簡單的代碼分析 在ReentrantReadWriteLock里其實維護的是一個sync的鎖,只是看起來語義上像是一個讀鎖和寫鎖。看一下它的構造函數:
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

//讀鎖的構造函數
protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}
//寫鎖的構造函數
protected WriteLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

可以看到實際上讀/寫鎖在構造時都是引用的ReentrantReadWriteLock的sync鎖對象。而這個Sync類是ReentrantReadWriteLock的一個內部類。總之讀/寫鎖都是通過Sync來完成的。它是如何來協作這兩者關係呢?

//讀鎖的加鎖方法
public void lock() {
    sync.acquireShared(1);
}

//寫鎖的加鎖方法
public void lock() {
    sync.acquire(1);
}

區別主要是讀鎖獲得的是共用鎖,而寫鎖獲取的是獨占鎖。這裡有個點可以提一下,就是ReentrantReadWriteLock為了保證可重入性,共用鎖和獨占鎖都必須支持持有計數和重入數。而ReentrantLock是使用state來存儲的,而state只能存一個整形值,為了相容兩個鎖的問題,所以將其劃分了高16位和低16位分別存共用鎖的線程數量或獨占鎖的線程數量或者重入計數。

其他

寫了一大篇感覺要寫下去篇幅太長了,還有一些比較有用的鎖:

  • CountDownLatch

就是設置一個同時持有的計數器,而調用者調用CountDownLatch的await方法時如果當前的計數器不為0就會阻塞,調用CountDownLatch的release方法可以減少計數,直到計數為0時調用了await的調用者會解除阻塞。

  • Semaphone

信號量是一種通過授權許可的形式,比如設置100個許可證,這樣就可以同時有100個線程同時持有鎖,如果超過這個量後就會返回失敗。

 

 

註:此文章為原創,歡迎轉載,請在文章頁面明顯位置給出此文鏈接! 若您覺得這篇文章還不錯請點擊下右下角的推薦,非常感謝! http://www.cnblogs.com/5207
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、前言 Jdom是什麼? Jdom是一個開源項目,基於樹形結構,利用純java的技術對XML文檔實現解析,生成,序列化以及多種操作。它是直接為java編程服務,利用java語言的特性(方法重載,集合),把SAX和DOM的功能結合起來,儘可能的把原來解析xml變得簡單,我們使用Jdom解析xml會是 ...
  • 一、HDFS讀過程 1.1 HDFS API 讀文件 1 Configuration conf = new Configuration(); 2 FileSystem fs = FileSystem.get(conf); 3 Path file = new Path("demo.txt"); 4 F ...
  • 分析: mysql_fetch_row,這個函數是從結果集中取一行作為枚舉數據,從和指定的結果標識關聯的結果集中取得一行數據並作為數組返回。每個結果的列儲存在一個數組的單元中,偏移量從 0 開始。 註意,這裡是從0開始偏移,也就是說不能用欄位名字來取值,只能用索引來取值,所以如下代碼是取不到值的: ...
  • 此篇講的是MyEclipse9工具提供的支持搭建自加包有代碼也是相同:用戶登錄與註冊的例子,表欄位只有name,password. SSH,xml方式搭建文章鏈接地址:http://www.cnblogs.com/wkrbky/p/5912810.html 一、Hibernate(數據層)的搭建: ...
  • 來到機房刷了一道水(bian’tai)題。題目思想非常簡單易懂(我的做法實際上參考了Evensgn 範學長,在此多謝範學長了) 題目擺上: 1044: [HAOI2008]木棍分割 Description 有n根木棍, 第i根木棍的長度為Li,n根木棍依次連結了一起, 總共有n-1個連接處. 現在允 ...
  • 一、統一資源定位地址(URL) (1)網路地址 在網路上,電腦是通過網路地址標識。網路地址通常有兩種表示方法,第一種表示方法通常採用4個整數組成,例如: 166.111.4.100表示某一網站伺服器的主頁地址。 第二種方法是通過功能變數名稱表示網路地址,例如: www.aaaa.edu.cn表示某一學校的 ...
  • 最近一直在忙其他事情,FOL停了好久,汗。。。 1、上個月幫朋友搞了個微信的公眾號,然後因為公眾號要做些用戶管理的,又去把簡訊驗證這塊做了一下,用的是阿裡大於的服務。期間被sign碼拖了兩天,總算是搞定了。等下把代碼分享一下。 2、公眾號的事情剩下一些頁面的工作沒做,因為朋友那邊一直沒提供頁面內容, ...
  • 題意不難理解,看了後就能得出下列式子: (A+C*x-B)mod(2^k)=0 即(C*x)mod(2^k)=(B-A)mod(2^k) 利用模線性方程(線性同餘方程)即可求解 模板直達車 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...