1 synchronized的缺陷
synchronized是java中的一個關鍵字,也就是說是Java語言內置的特性茧妒。那么為什么會出現(xiàn)Lock呢萧吠?
在上面一篇文章中,我們了解到如果一個代碼塊被synchronized修飾了桐筏,當一個線程獲取了對應的鎖纸型,并執(zhí)行該代碼塊時,其他線程便只能一直等待梅忌,等待獲取鎖的線程釋放鎖狰腌,而這里獲取鎖的線程釋放鎖只會有兩種情況:
1)獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有牧氮;
2)線程執(zhí)行發(fā)生異常琼腔,此時JVM會讓線程自動釋放鎖。
那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調用sleep方法)被阻塞了踱葛,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下洲赵,這多么影響程序執(zhí)行效率叠萍。
因此就需要有一種機制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應中斷),通過Lock就可以辦到抄腔。
再舉個例子:當有多個線程讀寫文件時绵患,讀操作和寫操作會發(fā)生沖突現(xiàn)象织狐,寫操作和寫操作會發(fā)生沖突現(xiàn)象旺嬉,但是讀操作和讀操作不會發(fā)生沖突現(xiàn)象邪媳。
但是采用synchronized關鍵字來實現(xiàn)同步的話废赞,就會導致一個問題:
如果多個線程都只是進行讀操作唉地,所以當一個線程在進行讀操作時,其他線程只能等待無法進行讀操作耕拷。
因此就需要一種機制來使得多個線程都只是進行讀操作時,線程之間不會發(fā)生沖突既峡,通過Lock就可以辦到。
另外传惠,通過Lock可以知道線程有沒有成功獲取到鎖。這個是synchronized無法辦到的盼砍。
總結一下浇坐,也就是說Lock提供了比synchronized更多的功能擒贸。但是要注意以下幾點:
1)Lock不是Java語言內置的,synchronized是Java語言的關鍵字,因此是內置特性回右。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徊都,它表示兩個鎖,一個是讀操作相關的鎖几睛,稱為共享鎖;一個是寫相關的鎖夯接,稱為排他鎖晴弃,描述如下:
線程進入讀鎖的前提條件:
沒有其他線程的寫鎖肝匆,
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個。
線程進入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
而讀寫鎖有以下三個重要的特性:
(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ù)據,只能有一個線程能寫該數(shù)據豁鲤,但可以有多個線程同時讀該數(shù)據秽誊。
ReadWriteLock rwl = new ReentrantReadWriteLock();
private Object data = null;//共享數(shù)據,只能有一個線程能寫數(shù)據琳骡,但可以有多個線程讀該數(shù)據
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 鎖的相關概念介紹
在前面介紹了Lock的基本使用楣号,這一節(jié)來介紹一下與鎖相關的幾個概念最易。
3.1 可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖炫狱。像synchronized和ReentrantLock都是可重入鎖藻懒,可重入性在我看來實際上表明了鎖的分配機制:基于線程的分配,而不是基于方法調用的分配视译。舉個簡單的例子嬉荆,當一個線程執(zhí)行到某個synchronized方法時,比如說method1酷含,而在method1中會調用另外一個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已經持有了該對象的鎖,而又在申請獲取該對象的鎖粥庄,這樣就會線程A一直等待永遠不會獲取到的鎖丧失。
而由于synchronized和Lock都具備可重入性,所以不會發(fā)生上述現(xiàn)象惜互。
3.2 可中斷鎖
可中斷鎖:顧名思義布讹,就是可以相應中斷的鎖。
在Java中训堆,synchronized就不是可中斷鎖描验,而Lock是可中斷鎖。
如果某一線程A正在執(zhí)行鎖中的代碼坑鱼,另一線程B正在等待獲取該鎖膘流,可能由于等待時間過長,線程B不想等待了鲁沥,想先處理其他事情呼股,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖画恰。
在前面演示lockInterruptibly()的用法時已經體現(xiàn)了Lock的可中斷性彭谁。
3.3 公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖允扇,當這個鎖被釋放時马靠,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖蔼两。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進行的甩鳄。這樣就可能導致某個或者一些線程永遠獲取不到鎖。
在Java中额划,synchronized就是非公平鎖妙啃,它無法保證等待的線程獲取鎖的順序。
而對于ReentrantLock和ReentrantReadWriteLock俊戳,它默認情況下是非公平鎖揖赴,但是可以設置為公平鎖。
看一下這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中也有類似的方法燥滑,同樣也可以設置為公平鎖和非公平鎖。不過要記住阿逃,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";
}
}