1 synchronized的缺陷
synchronized是java中的一個關(guān)鍵字,也就是說是Java語言內(nèi)置的特性。那么為什么會出現(xiàn)Lock呢?
在上面一篇文章中,我們了解到如果一個代碼塊被synchronized修飾了,當一個線程獲取了對應的鎖,并執(zhí)行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
1)獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有;
2)線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖。
那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下,這多么影響程序執(zhí)行效率。
因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到。
再舉個例子:當有多個線程讀寫文件時,讀操作和寫操作會發(fā)生沖突現(xiàn)象,寫操作和寫操作會發(fā)生沖突現(xiàn)象,但是讀操作和讀操作不會發(fā)生沖突現(xiàn)象。
但是采用synchronized關(guān)鍵字來實現(xiàn)同步的話,就會導致一個問題:
如果多個線程都只是進行讀操作,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作。
因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發(fā)生沖突,通過Lock就可以辦到。
另外,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的。
總結(jié)一下,也就是說Lock提供了比synchronized更多的功能。但是要注意以下幾點:
1)Lock不是Java語言內(nèi)置的,synchronized是Java語言的關(guān)鍵字,因此是內(nèi)置特性。Lock是一個類,通過這個類可以實現(xiàn)同步訪問;
2)Lock和synchronized有一點非常大的不同,采用synchronized不需要用戶去手動釋放鎖,當synchronized方法或者synchronized代碼塊執(zhí)行完之后,系統(tǒng)會自動讓線程釋放對鎖的占用;而Lock則必須要用戶去手動釋放鎖,如果沒有主動釋放鎖,就有可能導致出現(xiàn)死鎖現(xiàn)象。
2 Lock
2.1 ReentrantLock
public class LockTest {
public static void main(String[] args) {
new LockTest().init();
}
private void init() {
final Outputer outputer = new Outputer();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.output("kpioneer");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
outputer.output("Jack");
}
}
}).start();
}
static class Outputer {
Lock lock = new ReentrantLock();
public void output(String name) {
int len = name.length();
lock.lock();
/**
*
*不管程序運行是否異常都要釋放鎖
* 否則再也進入不到改頁面
*/
try {
for (int i = 0; i < len; i++) {
System.out.print(name.charAt(i));
}
System.out.println();
} finally {
lock.unlock();
}
}
}
}
kpioneer
Jack
kpioneer
Jack
kpioneer
Jack
Jack
kpioneer
Jack
kpioneer
省略...
2.2 ReentrantReadWriteLock
實中有這樣一種場景:對共享資源有讀和寫的操作,且寫操作沒有讀操作那么頻繁。在沒有寫操作的時候,多個線程同時讀一個資源沒有任何問題,所以應該允許多個線程同時讀取共享資源;但是如果一個線程想去寫這些共享資源,就不應該允許其他線程對該資源進行讀和寫的操作了。
針對這種場景,JAVA的并發(fā)包提供了讀寫鎖ReentrantReadWriteLock,它表示兩個鎖,一個是讀操作相關(guān)的鎖,稱為共享鎖;一個是寫相關(guān)的鎖,稱為排他鎖,描述如下:
線程進入讀鎖的前提條件:
沒有其他線程的寫鎖,
沒有寫請求或者有寫請求,但調(diào)用線程和持有鎖的線程是同一個。
線程進入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
而讀寫鎖有以下三個重要的特性:
(1)公平選擇性:支持非公平(默認)和公平的鎖獲取方式,吞吐量還是非公平優(yōu)于公平。
(2)重進入:讀鎖和寫鎖都支持線程重進入。
(3)鎖降級:遵循獲取寫鎖、獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成為讀鎖。
public class ReadWriteLockTest {
public static void main(String[] args) {
final Queue q = new Queue();
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
while (true) {
q.get();
}
}
}.start();
new Thread() {
@Override
public void run() {
while (true) {
q.put(new Random().nextInt(10000));
}
}
}.start();
}
}
}
class Queue {
//共享數(shù)據(jù),只能有一個線程能寫該數(shù)據(jù),但可以有多個線程同時讀該數(shù)據(jù)。
ReadWriteLock rwl = new ReentrantReadWriteLock();
private Object data = null;//共享數(shù)據(jù),只能有一個線程能寫數(shù)據(jù),但可以有多個線程讀該數(shù)據(jù)
public void get() {
//上讀鎖,其他線程只能讀不能寫
rwl.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " be ready to read data!");
Thread.sleep((long) (Math.random() * 1000));
System.out.println(Thread.currentThread().getName() + " have read data :" + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwl.readLock().unlock();
}
}
public void put(Object data) {
//上寫鎖,不允許其他線程讀也不允許寫
rwl.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " be ready to write data!");
Thread.sleep((long) (Math.random() * 1000));
this.data = data;
System.out.println(Thread.currentThread().getName() + " have write data: " + data);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwl.writeLock().unlock();
}
}
}
Thread-0 be ready to read data!
Thread-2 be ready to read data!
Thread-2 have read data :null
Thread-0 have read data :null
Thread-1 be ready to write data!
Thread-1 have write data: 4664
Thread-1 be ready to write data!
Thread-1 have write data: 1849
Thread-3 be ready to write data!
Thread-3 have write data: 75
Thread-3 be ready to write data!
Thread-3 have write data: 8222
Thread-3 be ready to write data!
Thread-3 have write data: 3056
Thread-3 be ready to write data!
Thread-3 have write data: 6114
Thread-3 be ready to write data!
Thread-3 have write data: 6376
省略...
3 鎖的相關(guān)概念介紹
在前面介紹了Lock的基本使用,這一節(jié)來介紹一下與鎖相關(guān)的幾個概念。
3.1 可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配,而不是基于方法調(diào)用的分配。舉個簡單的例子,當一個線程執(zhí)行到某個synchronized方法時,比如說method1,而在method1中會調(diào)用另外一個synchronized方法method2,此時線程不必重新去申請鎖,而是可以直接執(zhí)行方法method2。
看下面這段代碼就明白了:
class MyClass {
public synchronized void method1() {
method2();
}
public synchronized void method2() {
}
}
上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執(zhí)行到了method1,此時線程A獲取了這個對象的鎖,而由于method2也是synchronized方法,假如synchronized不具備可重入性,此時線程A需要重新申請鎖。但是這就會造成一個問題,因為線程A已經(jīng)持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。
而由于synchronized和Lock都具備可重入性,所以不會發(fā)生上述現(xiàn)象。
3.2 可中斷鎖
可中斷鎖:顧名思義,就是可以相應中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
在前面演示lockInterruptibly()的用法時已經(jīng)體現(xiàn)了Lock的可中斷性。
3.3 公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對于ReentrantLock和ReentrantReadWriteLock,它默認情況下是非公平鎖,但是可以設(shè)置為公平鎖。
看一下這2個類的源代碼就清楚了:
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
另外在ReentrantLock類中定義了很多方法,比如:
isFair() //判斷鎖是否是公平鎖
isLocked() //判斷鎖是否被任何線程獲取了
isHeldByCurrentThread() //判斷鎖是否被當前線程獲取了
hasQueuedThreads() //判斷是否有線程在等待該鎖
在ReentrantReadWriteLock中也有類似的方法,同樣也可以設(shè)置為公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock并未實現(xiàn)Lock接口,它實現(xiàn)的是ReadWriteLock接口。
3.4 讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發(fā)生沖突。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。
public class CacheDemo {
private Map<String, Object> cache = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public static void main(String[] args) {
}
public Object getData(String key) {
Object value = null;
//首先開啟讀鎖,從緩存中去取
readWriteLock.readLock().lock();
try {
value = cache.get(key);
//如果緩存中沒有釋放讀鎖,上寫鎖
if (value == null) {
//對應queryDB()
readWriteLock.readLock().unlock();
//讀鎖必須unlock之后才能獲取寫鎖
readWriteLock.writeLock().lock();
try {
//對應queryDB()
value = queryDB();
} finally {
//釋放寫鎖
readWriteLock.writeLock().unlock();
}
//然后再上讀鎖
readWriteLock.readLock().lock();
}
} finally {
//最后釋放讀鎖
readWriteLock.readLock().unlock();
}
return value;
}
public Object queryDB() {
return "aaaa";
}
}