多個線程同時對同一個對象進(jìn)行讀寫操作,很容易會出現(xiàn)一些難以預(yù)料的問題萤厅。所以很多時候我們需要給代碼塊加鎖,同一時刻只允許一個線程對某個對象進(jìn)行操作夯辖。多線程之所以會容易引發(fā)一些難以發(fā)現(xiàn)的bug,很多時候是寫代碼的程序員對線程鎖不熟悉或者干脆就沒有在必要的地方給線程加鎖導(dǎo)致的拾徙。這里我想總結(jié)一下java多線程中的各種鎖的作用和用法,還有容易踩的坑滞诺。
這篇文章里面有很多的文字和代碼都來自于《實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計》这刷。它真的是一本很不錯的書,建議大家有空可以去看一下。
synchronized關(guān)鍵字
synchronized的作用
關(guān)鍵字synchronized的作用是實(shí)現(xiàn)線程間的同步鼻弧。它的工作是對同步的代碼加鎖,使得每一次,只能有一個線程進(jìn)入同步塊,從而保證線程間的安全性设江。
關(guān)鍵字synchronized可以有多張用法,這里做一個簡單的整理:
指定加鎖對象:對給定對象加鎖,進(jìn)入同步代碼前要獲取給定對象的鎖。
直接作用于實(shí)例方法:相當(dāng)于給當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼塊前要獲得當(dāng)前實(shí)例的鎖温数。
直接作用于靜態(tài)方法:相當(dāng)于對當(dāng)前類加鎖,進(jìn)入同步代碼前要獲取當(dāng)前類的鎖绣硝。
下面來分別說一下上面的三點(diǎn):
假設(shè)我們有下面這樣一個Runnable,在run方法里對靜態(tài)成員變量sCount自增10000次:
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
sCount++;
}
}
}
假設(shè)我們在兩個Thread里面同時跑這個Runnable:
Count count = new Count();
Thread t1 = new Thread(count);
Thread t2 = new Thread(count);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Count.getCount());
得到的結(jié)果并不是20000,而是一個比20000小的數(shù),如14233蜻势。
這是為什么呢撑刺?假設(shè)兩個線程分別讀取sCount為0,然后各自技術(shù)得到sCount為1,并先后寫入這個結(jié)果,因此,雖然sCount++執(zhí)行了2次,但是實(shí)際sCount的值只增加了1。
我們可以用指定加鎖對象的方法解決這個問題,這里因?yàn)閮蓚€Thread跑的是同一個Count實(shí)例,所以可以直接給this加鎖:
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (this) {
sCount++;
}
}
}
}
我們也可以給實(shí)例方法加鎖,這種方式和上面那一種的區(qū)別就是給this加鎖,鎖的區(qū)域比較小,兩個線程交替執(zhí)行sCount++操作,而給方法加鎖的話,先拿到鎖的線程會連續(xù)執(zhí)行1000次sCount自增,然后再釋放鎖給另一個線程握玛。
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public synchronized void run() {
for (int i = 0; i < 10000; i++) {
sCount++;
}
}
}
synchronized直接作用于靜態(tài)方法的用法和上面的給實(shí)例方法加鎖類似,不過它是作用于靜態(tài)方法:
class Count implements Runnable {
private static int sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
private static synchronized void increase() {
sCount++;
}
}
等待(wait)和通知(notify)
Object有兩個很重要的接口:Object.wait()和Object.notify()
當(dāng)在一個對象實(shí)例上調(diào)用了wait()方法后,當(dāng)前線程就會在這個對象上等待够傍。直到其他線程調(diào)用了這個對象的notify()方法或者notifyAll()方法甫菠。notifyAll()方法與notify()方法的區(qū)別是它會喚醒所有正在等待這個對象的線程,而notify()方法只會隨機(jī)喚醒一個等待該對象的線程。
wait()冕屯、notify()和notifyAll()都需要在synchronized語句中使用:
class MyThread extends Thread {
private Object mLock;
public MyThread(Object lock) {
this.mLock = lock;
}
@Override
public void run() {
super.run();
synchronized (mLock) {
try {
mLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("in MyThread");
}
}
}
Object lock = new Object();
MyThread t = new MyThread(lock);
t.start();
System.out.println("before sleep");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep");
synchronized (lock) {
lock.notify();
}
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
從上面的例子可以看出來,在調(diào)用wait()方法實(shí)際上已經(jīng)釋放了對象的鎖,所以在其他線程中才能獲取到這個對象的鎖,從而進(jìn)行notify操作寂诱。而等待的線程被喚醒后又需要重新獲得對象的鎖。
synchronized容易犯的隱蔽錯誤
是否給同一個對象加鎖
在用synchronized給對象加鎖的時候需要注意加鎖是不是同一個,如將代碼改成這樣:
Thread t1 = new Thread(new Count());
Thread t2 = new Thread(new Count());
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Count.getCount());
因?yàn)閮蓚€線程跑的是不同的Count實(shí)例,所以用給指定對象加鎖和給實(shí)例方法加鎖的方法都不能避免兩個線程同時對靜態(tài)成員變量sCount進(jìn)行自增操作安聘。
但是如果用第三種作用于靜態(tài)方法的寫法,就能正確的加鎖痰洒。
是否給錯誤的對象加鎖
如我們將sCount的類型改成Integer,并且在sCount++的時候直接對sCount加鎖會發(fā)生什么事情呢(畢竟我們會很自然的給要操作的對象加鎖來實(shí)現(xiàn)線程同步)?
class Count implements Runnable {
private static Integer sCount = 0;
public static int getCount() {
return sCount;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
synchronized (sCount) {
sCount++;
}
}
}
}
Count count = new Count();
Thread t1 = new Thread(count);
Thread t2 = new Thread(count);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Count.getCount());
最后的得到的結(jié)果仍然是比20000小的值浴韭。
這是為什么呢丘喻?《實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計》中給出的解釋是這樣的:
在Java中,Integer使用不變對象。也就是對象一旦被創(chuàng)建,就不可能被修改念颈。也就是說,如果你有一個Integer代表1,那么它就永遠(yuǎn)是1,你不可能改變Integer的值,使它位泉粉。那如果你需要2怎么辦呢?也很簡單,新建一個Integer,并讓它表示2即可榴芳。
也就是說sCount在真實(shí)執(zhí)行時變成了:
sCount = Integer.valueOf(sCount.intValue()+1);
進(jìn)一步看Integer.valueOf()嗡靡,我們可以看到:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
所以在多個線程中,由于sCount一直在變,并不是同一個對象,所以兩個線程的加鎖可能加在了不同的Integer對象上,并沒有真正的鎖住代碼塊。
我再舉一個例子:
public void increase(Integer integer){
integer++;
}
在外面這樣調(diào)用它,并不會使得傳入的Integer增加:
Integer i = 0;
increase(i);
重入鎖
ReentrantLock的意思是Re-Entrant-Lock也就是重入鎖,它的特點(diǎn)就是在同一個線程中可以重復(fù)加鎖,只需要解鎖同樣的次數(shù)就能真正解鎖:
class MyThread extends Thread {
private ReentrantLock mLock = new ReentrantLock();
@Override
public void run() {
super.run();
mLock.lock();
System.out.println("outside");
mLock.lock();
System.out.println("inside");
mLock.unlock();
mLock.unlock();
}
}
事實(shí)上synchronized也是可重入的,比如下面的代碼同樣是可以正常退出的:
class MyThread extends Thread {
@Override
public void run() {
super.run();
synchronized (this) {
System.out.println("outside");
synchronized (this) {
System.out.println("inside");
}
}
}
}
與synchronized相比,重入鎖需要程序員手動調(diào)用加鎖和解鎖,也因?yàn)槿绱?重入鎖對邏輯控制的靈活性要遠(yuǎn)遠(yuǎn)好于synchronized窟感。
重入鎖可以完全替代synchronized關(guān)鍵字讨彼。在JDK 5.0的早起版本中,重入鎖的性能遠(yuǎn)遠(yuǎn)好于synchronized。但從JDK 6.0開始,JDK在synchronized做了大量優(yōu)化,使得兩者的性能差距并不大肌括。
ReentrantLock是可中斷的
對于synchronized,如果它在等待鎖,那么它就只有兩個狀態(tài):獲得鎖繼續(xù)執(zhí)行或者保持等待点骑。但是對于重入鎖,就有了另外一種可能,那就是重入鎖在等待的時候可以被中斷:
class MyThread extends Thread {
private ReentrantLock mLock = new ReentrantLock();
@Override
public void run() {
super.run();
try {
mLock.lockInterruptibly();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(mLock.isHeldByCurrentThread()){
mLock.unlock();
}
}
}
}
ReentrantLock可以設(shè)置等待限時
ReentrantLock.tryLock()方法可以給等待鎖設(shè)置最長等待時間,如果在設(shè)置的時間結(jié)束之前獲取到鎖就會返回true,否則返回false:
class MyThread extends Thread {
private ReentrantLock mLock = new ReentrantLock();
@Override
public void run() {
super.run();
try {
if (mLock.tryLock(2, TimeUnit.SECONDS)) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (mLock.isHeldByCurrentThread()) {
mLock.unlock();
}
}
}
}
ReentrantLock.tryLock()也可以不帶參數(shù)直接運(yùn)行。在這種情況下,當(dāng)前線程會嘗試獲得鎖,如果鎖并未被其他線程占用,則申請鎖會成功,并立即返回true谍夭。如果鎖被其他線程占用,則當(dāng)前線程不會進(jìn)行等待,而是立即返回false黑滴。
ReentrantLock可以設(shè)置公平鎖
大多數(shù)情況下,鎖的申請是非公平的。也就是說,線程1首先請求了鎖A紧索,接著線程2也請求了鎖A袁辈。那么當(dāng)鎖A可用時,是線程1可以獲得鎖還是線程2可以獲得鎖呢?這是不一定的珠漂。系統(tǒng)只是會從這個鎖的等待隊列里隨機(jī)挑選一個:
class MyThread extends Thread {
private ReentrantLock mLock;
public MyThread(String name, ReentrantLock lock) {
super(name);
this.mLock = lock;
}
@Override
public void run() {
super.run();
while (true) {
mLock.lock();
System.out.println(Thread.currentThread().getName() + "獲得鎖");
mLock.unlock();
}
}
}
ReentrantLock lock = new ReentrantLock();
MyThread t1 = new MyThread("t1", lock);
t1.start();
MyThread t2 = new MyThread("t2", lock);
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
打印如下:
t1獲得鎖
t2獲得鎖
t2獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t2獲得鎖
t2獲得鎖
t1獲得鎖
t1獲得鎖
synchronized產(chǎn)生的鎖也是非公平的晚缩。但如果使用ReentrantLock(boolean fair)構(gòu)造函數(shù)創(chuàng)建ReentrantLock,并且傳入true。則該重入鎖是公平的:
ReentrantLock lock = new ReentrantLock(true);
MyThread t1 = new MyThread("t1", lock);
t1.start();
MyThread t2 = new MyThread("t2", lock);
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
打印如下:
t1獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t1獲得鎖
t2獲得鎖
t1獲得鎖
需要注意的是實(shí)現(xiàn)公平鎖必然要求系統(tǒng)維護(hù)一個有序隊列,所以公平鎖的實(shí)現(xiàn)成本較高,性能也相對低下,因此,默認(rèn)情況下,鎖是非公平的媳危。
ReentrantLock可以與Condition配合使用
Condition和之前講過的Object.wait()還有Object.notify()的作用大致相同:
class MyThread extends Thread {
private ReentrantLock mLock;
private Condition mCondition;
public MyThread(ReentrantLock lock, Condition condition) {
this.mLock = lock;
this.mCondition = condition;
}
@Override
public void run() {
super.run();
mLock.lock();
try {
mCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
mLock.unlock();
System.out.println("in MyThread");
}
}
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
MyThread t = new MyThread(lock, condition);
t.start();
System.out.println("before sleep");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep");
lock.lock();
condition.signal();
lock.unlock();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Condition的操作需要在ReentrantLock.lock()和ReentrantLock.unlock()之間進(jìn)行的荞彼。
ReentrantLock.newCondition()可以創(chuàng)建一個Condition。Condition.await()方法相當(dāng)于Object.wait()方法,而Condition.signal()方法相當(dāng)于Object.notify()方法待笑。當(dāng)然它也有對應(yīng)的Condition.signalAll()方法鸣皂。
同樣的在調(diào)用Condition.await()之后,線程占用的鎖會被釋放。這樣在Condition.signal()方法調(diào)用的時候才獲取到鎖。
需要注意的是Condition.signal()方法調(diào)用之后,被喚醒的線程因?yàn)樾枰匦芦@取鎖寞缝。所以需要等到調(diào)用Condition.signal()的線程釋放了鎖(調(diào)用ReentrantLock.unlock())之后才能繼續(xù)執(zhí)行癌压。
Condition接口的基本方法如下,它提供了限時等待、不可中斷的等待之類的操作:
void await() throws InterruptedException;
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
void signal();
void signalAll();
信號量
信號量為多線程協(xié)作提供了更為強(qiáng)大的控制方法荆陆。廣義上說,信號量是對鎖的拓展滩届。無論是synchronize還是重入鎖,一次都只運(yùn)行一個線程訪問一個資源,而信號鎖則可以指定多個線程,同時訪問某一個資源。
像下面的代碼, MyRunnable被加鎖的代碼塊一次會被5個線程執(zhí)行:
public class MyRunnable implements Runnable {
private Semaphore mSemaphore;
public MyRunnable(Semaphore semaphore) {
mSemaphore = semaphore;
}
@Override
public void run() {
try {
mSemaphore.acquire();
Thread.sleep(2000);
System.out.println("thread " + Thread.currentThread().getId() + " working");
mSemaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 19; i++) {
new Thread(new MyRunnable(semaphore)).start();
}
Thread t = new Thread(new MyRunnable(semaphore));
t.start();
t.join();
Semaphore.acquire()方法嘗試獲得一個準(zhǔn)入許可被啼。如無法獲得,線程就會等待帜消。而Semaphore.release()則在線程訪問資源結(jié)束后,釋放一個許可。
Semaphore有下面的一些常用方法:
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long timeout, TimeUnit unit)
public void release()
其他的一些鎖
讀寫鎖
讀寫鎖(ReadWriteLock)是JDK5中提供的分離鎖浓体。讀寫分離鎖可以有效的減少鎖競爭券犁。
讀寫鎖允許多個線程同時讀,但是寫寫操作和讀寫操作就需要相互等待了。讀寫鎖的訪問約束如下:
讀 | 寫 | |
---|---|---|
讀 | 非阻塞 | 阻塞 |
寫 | 阻塞 | 阻塞 |
讀寫操作在某些特定操作下可以提高程序的性能,如下面的代碼汹碱。如果使用重入鎖,需要十一秒左右才能運(yùn)行完:
public class Data {
private String mData = "data";
private ReentrantLock mLock = new ReentrantLock();
public String readData(){
mLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("read data : " + mData);
String data = mData;
mLock.unlock();
return data;
}
public void writeData(String data){
mLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mData = data;
System.out.println("write data : " + mData);
mLock.unlock();
}
}
final Data data = new Data();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
data.readData();
}
}).start();
}
Thread write = new Thread(new Runnable() {
@Override
public void run() {
data.writeData("update data");
}
});
write.start();
但是如果將重入鎖改成讀寫鎖的話只需要兩秒左右就能完成:
public class Data {
private String mData = "data";
private ReadWriteLock mLock = new ReentrantReadWriteLock();
private Lock mReadLock = mLock.readLock();
private Lock mWriteLock = mLock.writeLock();
public String readData(){
mReadLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String data = mData;
System.out.println("read data : " + mData);
mReadLock.unlock();
return data;
}
public void writeData(String data){
mWriteLock.lock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("write data : " + mData);
mData = data;
mWriteLock.unlock();
}
}
倒計時器粘衬、循環(huán)柵欄
倒計時器(CountDownLatch)和循環(huán)柵欄(CyclicBarrier)因?yàn)楸容^不常用,所以這里就不講了,有興趣的同學(xué)可以自己去看一下《實(shí)戰(zhàn)Java高并發(fā)程序設(shè)計》這本書。