一、引子
對於Java併發的鎖結構,我們常常使用的是synchonized
結構,而由於其靈活度較低,所以在Java-5後提出了Lock
接口,以及AbstractQueuedSynchronizer
抽象類供我們方便且安全地來實現自定義鎖結構,下面從代碼出發來開始這篇文章的閱讀。
本文就兩個實現方式來闡述“生產者-消費者模式”背景下的鎖應用,第一種方式是使用Lock
接口的自定義實現類來實現,第二種方式是使用synchronized
關鍵字來實現。願讀者在兩種不同的實現方式對比中發現各自使用的特點。
二、Lock接口實現
需求:設計一個同步工具:該工具在同一時刻, 只允許至多兩個線程同時訪問, 超過兩個線程的訪問將被阻塞, 我們將這個同步工具命名爲TwinsLock
。並且以生產者和消費者的角度來驗證此鎖是否成功編寫。
package concurrency_basic.chapter16_自定義同步組件;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count);
}
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current,
newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
}
public void lock() {
sync.acquireShared(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
public void unlock() {
sync.releaseShared(1);
}
@Override
public Condition newCondition() {
return null;
}
//以下是TwinsLock類是否成功編寫的測試代碼
public static void main(String[] args) {
final Lock lock = new TwinsLock();
class Worker extends Thread {
public void run() {
lock.lock();
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
// 啓動10個線程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.start();
}
}
}
控制檯輸出:
Thread-0
Thread-1
Thread-2
Thread-5
Thread-6
Thread-4
Thread-7
Thread-3
Thread-8
Thread-9
三、代碼分析
首先,我們由控制檯輸出可見,我們的確成功地創建了一個最多支持兩個線程同時工作的共享鎖。
但是如果要求讀者朋友不加基礎地直接理解以上代碼,恐怕對於部分人有所難度。所以我先介紹一下由jdk1.5之後提供的鎖設計模式,學了這個之後,理解代碼相對容易非常多了。下面我以ReentrantLock
類作爲一個例子來說明自定義鎖的設計模式。
我們先不管右下角的Condition
接口。先看看其餘接口以及類在鎖構造過程中所起到的作用:
Lock
接口:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;
AQS
抽象類:同步器面向的是鎖的實現者,它簡化了鎖的實現方式, 隱藏了同步狀態管理、 線程的排隊、 等待與喚醒等底層操作。
所以上述代碼的邏輯是:首先我們寫一個靜態的內部類Sync
,其需要繼承AQS
抽象類。其主要功能是:重寫AQS
類內部獲取資源方法:tryAcquireShared
以及釋放資源的方法:tryReleaseShared
,資源獲取和釋放中涉及了同步器狀態的變化,而狀態的變化需要調用AQS內部提供了CAS方法:compareAndSetState
。而AQS中涉及線程排隊、休眠、喚醒等操作代碼我們並不需要實現,我們所需做的就是關於線程獲得到資源/釋放資源時,修改同步器的狀態,而這個狀態將決定線程是否被喚醒,是否將嘗試搶奪資源的鎖放入等待隊列並休眠。
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState();
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current,
newCount)) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState();
int newCount = current + returnCount;
if (compareAndSetState(current, newCount)) {
return true;
}
}
}
而Lock
方法的實現TwinsLock
類的相關方法重寫,只需簡單地調用AQS
抽象類實現:Sync
靜態內部類對象的若干方法即可:
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
其中acquireShared(1)
以及releaseShared(1)
方法的入口參數值的大小可以認爲是對線程資源消耗程度的描述,在這個類中,我們可以認爲其都會消耗同步器中資源單位1(總共資源單位爲2),如果將值改爲2,相當於兩個資源會被一個線程鎖佔據,這樣一來,鎖就只允許只有一個線程佔據資源,進行執行了。
四、傳統方式實現生產者消費者模式
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
/**
* @author Fisherman
*/
public class TraditionalLock {
private static final Object MONITOR = new Object();
private AtomicInteger number = new AtomicInteger(2);
public void lock() {
synchronized (MONITOR) {
while (number.get() <= 0) {
try {
MONITOR.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number.decrementAndGet();
}
}
public void unlock() {
synchronized (MONITOR) {
number.incrementAndGet();
MONITOR.notifyAll();
}
}
public static void main(String[] args) {
final TraditionalLock lock = new TraditionalLock();
class Worker extends Thread {
public void run() {
lock.lock();
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
Thread.sleep(100);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
// 啓動10個線程
for (int i = 0; i < 10; i++) {
Worker w = new Worker();
w.start();
}
}
}
控制檯輸出:
Thread-1
Thread-0
Thread-8
Thread-9
Thread-2
Thread-7
Thread-4
Thread-3
Thread-5
Thread-6
可見我們使用傳統方式也實現了限制線程運行數目爲2的生產者消費者模式,但是與Lock
接口實現的鎖相比,顯然傳統的實現方式在lock
、unlock
方法需要更多的細節。需要設置同步監視器,需要額外調用 wait
/notifyAll
方法,並且由於使用了synchronized
進行上鎖,所以在資源消耗上比CAS實現的同步操作更加耗費內存資源。綜上所述,使用Lock
接口實現的自定義鎖更加靈活、耗費更少的資源、對開發者更加友善。