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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...