java並發:線程同步機制之Lock


一、初識Lock

  Lock是一個接口,提供了無條件的、可輪詢的、定時的、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的,其包路徑是:java.util.concurrent.locks.Lock,其核心方法是lock()、unlock()、tryLock(),實現類有ReentrantLock、ReentrantReadWriteLock.ReadLock、ReentrantReadWriteLock.WriteLock,下圖展示了Lock接口中定義的方法:

 

二、ReentrantLock

(1)初識ReentrantLock

  Java在過去很長一段時間只能通過synchronized關鍵字來實現互斥,它有一些缺點,比如你不能擴展鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java5通過Lock接口提供了更復雜的控制來解決這些問題,《Java並發編程實戰》一書有如下描述:

 

(2)示例

此處我們看下面這兩個例子,請注意其中ReentrantLock使用方式的區別:

(1)此處兩個方法之間的鎖是獨立

package com.test;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
public static void main(String[] args) {
final Countx ct = new Countx();
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
ct.get();
}
}.start();
}
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
ct.put();
}
}.start();
}
}
}

class Countx {

public void get() {
final ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 加鎖
System.out.println(Thread.currentThread().getName() + "get begin");
Thread.sleep(
1000L);// 模仿干活
System.out.println(Thread.currentThread().getName() + "get end");
lock.unlock();
// 解鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void put() {
final ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 加鎖
System.out.println(Thread.currentThread().getName() + "put begin");
Thread.sleep(
1000L);// 模仿干活
System.out.println(Thread.currentThread().getName() + "put end");
lock.unlock();
// 解鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

運行結果如下(每次運行結果都是不一樣的,仔細體會一下):

Thread-1get begin
Thread
-0get begin
Thread
-2put begin
Thread
-3put begin
Thread
-0get end
Thread
-3put end
Thread
-1get end
Thread
-2put end

 

(2)此處兩個方法之間使用相同的鎖

package com.test;

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
public static void main(String[] args) {
final Countx ct = new Countx();
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
ct.get();
}
}.start();
}
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
ct.put();
}
}.start();
}
}
}

class Countx {
final ReentrantLock lock = new ReentrantLock();

public void get() {
// final ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 加鎖
System.out.println(Thread.currentThread().getName() + "get begin");
Thread.sleep(
1000L);// 模仿干活
System.out.println(Thread.currentThread().getName() + "get end");
lock.unlock();
// 解鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void put() {
// final ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 加鎖
System.out.println(Thread.currentThread().getName() + "put begin");
Thread.sleep(
1000L);// 模仿干活
System.out.println(Thread.currentThread().getName() + "put end");
lock.unlock();
// 解鎖
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

運行結果如下(每次運行結果都是一樣的):

Thread-0get begin
Thread
-0get end
Thread
-1get begin
Thread
-1get end
Thread
-2put begin
Thread
-2put end
Thread
-3put begin
Thread
-3put end

 

 

三、ReadWriteLock

(1)初識ReadWriteLock

  Java中的ReadWriteLock是什么?一般而言,讀寫鎖是用來提升並發程序性能的鎖分離技術的成果,Java中的ReadWriteLock是Java5中新增的一個接口,提供了readLock和writeLock兩種鎖機制。一個ReadWriteLock維護一對關聯的鎖,一個用於只讀操作,一個用於寫,在沒有寫線程的情況下,一個讀鎖可能會同時被多個讀線程持有,寫鎖是獨占的。

我們來看一下ReadWriteLock的源碼:

public interface ReadWriteLock{
Lock readLock();
Lock writeLock();
}

  從源碼上面我們可以看出來ReadWriteLock並不是Lock的子接口,只不過ReadWriteLock借助Lock來實現讀寫兩個鎖並存、互斥的操作機制。在ReadWriteLock中每次讀取共享數據就需要讀取鎖,當需要修改共享數據時就需要寫入鎖,看起來好像是兩個鎖,但是並非如此。

  ReentrantReadWriteLock是ReadWriteLock在java.util里面唯一的實現類,主要使用場景是當有很多線程都從某個數據結構中讀取數據,而很少有線程對其進行修改。在這種情況下,允許讀取器線程共享訪問時合適的,寫入器線程必須是互斥訪問的,你可以使用JDK中的ReentrantReadWriteLock來實現這個規則。

ReentrantReadWriteLock的實現里面有以下幾個特性:

(1)公平性

(2)重入性

(3)鎖降級:寫線程獲取寫入鎖后可以獲取讀取鎖,然后釋放寫入鎖,這樣就可以從寫入鎖變成了讀取鎖,從而實現鎖降級的特性。

(4)鎖升級

(5)鎖獲取中斷

(6)條件變量

(7)重入數:讀取鎖和寫入鎖的數量最大分別是65535。它最多支持65535個寫鎖和65535個讀鎖。

 概括起來其實就是讀寫鎖的機制:

A、讀-讀不互斥

B、讀-寫互斥

C、寫-寫互斥

 

(2)示例

示例一:ReadLock和WriteLock單獨使用的情況

package demo.thread;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
public static void main(String[] args) {
final Count ct = new Count();
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
ct.get();
}
}.start();
}
for (int i = 0; i < 2; i++) {
new Thread() {
@Override
public void run() {
ct.put();
}
}.start();
}
}
}

class Count {
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

public void get() {
rwl.readLock().lock();
// 上讀鎖,其他線程只能讀不能寫,具有高並發性
try {
System.out.println(Thread.currentThread().getName()
+ " read start.");
Thread.sleep(
1000L);// 模擬干活
System.out.println(Thread.currentThread().getName() + "read end.");
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
rwl.readLock().unlock();
// 釋放寫鎖,最好放在finnaly里面
}
}

public void put() {
rwl.writeLock().lock();
// 上寫鎖,具有阻塞性
try {
System.out.println(Thread.currentThread().getName()
+ " write start.");
Thread.sleep(
1000L);// 模擬干活
System.out.println(Thread.currentThread().getName() + "write end.");
}
catch (InterruptedException e) {
e.printStackTrace();
}
finally {
rwl.writeLock().unlock();
// 釋放寫鎖,最好放在finnaly里面
}
}

}

運行結果如下:

Thread-1 read start.
Thread-0 read start.
Thread-1read end.
Thread-0read end.
Thread-3 write start.
Thread-3write end.
Thread-2 write start.
Thread-2write end.

從結果上面可以看的出來,讀的時候是並發的,寫的時候是有順序的帶阻塞機制的

 

實例二:ReadLock和WriteLock的復雜使用情況,模擬一個有讀寫數據的場景

private final Map<String, Object> map = new HashMap<String, Object>();// 假設這里面存了數據緩存
private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();

public Object readWrite(String id) {
Object value
= null;
rwlock.readLock().lock();
// 首先開啟讀鎖,從緩存中去取
try {
value
= map.get(id);
if (value == null) { // 如果緩存中沒有數據,釋放讀鎖,上寫鎖
rwlock.readLock().unlock();
rwlock.writeLock().lock();
try {
if (value == null) {
value
= "aaa"; // 此時可以去數據庫中查找,這里簡單的模擬一下
}
}
finally {
rwlock.writeLock().unlock();
// 釋放寫鎖
}
rwlock.readLock().lock();
// 然后再上讀鎖
}
}
finally {
rwlock.readLock().unlock();
// 最后釋放讀鎖
}
return value;
}

請一定要注意讀寫鎖的獲取與釋放順序。

 

(3)比較分析

ReentrantReadWriteLock與ReentrantLock的比較:

(1)相同點:都是一種顯式鎖,手動加鎖和解鎖,都很適合高並發場景

(2)不同點:ReentrantReadWriteLock是對ReentrantLock的復雜擴展,能適合更復雜的業務場景,ReentrantReadWriteLock可以實現一個方法中讀寫分離的鎖機制。而ReentrantLock加鎖解鎖只有一種機制

 

四、StampedLock

  Java8引入了一個新的讀寫鎖:StampedLock,這個鎖更快,而且它提供強大的樂觀鎖API,這意味着你能以一個較低的代價獲得一個讀鎖, 在這段時間希望沒有寫操作發生,當這段時間完成后,你可以查詢一下鎖,看在剛才這段時間是否有寫操作發生,然后你可以決定是否需要再試一次或升級鎖或放棄。

通常我們的同步鎖代碼如下:

synchronized(this){
// do operation
}

Java6提供的ReentrantReadWriteLock使用方式如下:

rwlock.writeLock().lock();
try {
// do operation
} finally {
rwlock.writeLock().unlock();
}

  ReentrantReadWriteLock、ReentrantLock和synchronized鎖都有相同的內存語義,不管怎么說synchronized代碼要更容易書寫,而ReentrantLock的代碼必須嚴格按照一定的方式來寫,否則就會造成嚴重的問題。StampedLock要比ReentrantReadWriteLock更加廉價,也就是消耗比較小,StampedLock控制鎖有三種模式(寫,讀,樂觀讀)

 

參考資料:

Java 8新特性StampedLock

(1)http://www.importnew.com/14941.html

(2)http://www.jdon.com/idea/java/java-8-stampedlock.html

 

===============深入學習===AbstractQueuedSynchronizer=================

此處貼出一些資源,后續會研究這部分內容

(1)http://ifeve.com/jdk1-8-abstractqueuedsynchronizer/

(2)http://ifeve.com/introduce-abstractqueuedsynchronizer/


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com