線程安全問題
雖然多線程編程極大地提高了效率,但是也會帶來一定的隱患。舉一個例子:我們要兩個線程修改并交替打印變量a
public class VolatileDemo {
int a = 0;
public void addNum() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;//注意這里
}
public static void main(String[] args) {
final VolatileDemo volatileDemo = new VolatileDemo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
volatileDemo.addNum();
System.out.println("num=" + volatileDemo.a);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
volatileDemo.addNum();
System.out.println("num=" + volatileDemo.a);
}
}
}).start();
}
}
還是這個例子,兩個線程同時操作一個變量a,但是打印出來的a的最終結(jié)果一定會是200嗎?答案不是。可能每次的結(jié)果都不一樣。最終結(jié)果小于等于200。這就是最經(jīng)典的一個線程安全問題了。兩個線程操作一個變量可能兩個線程同事拿到了變量的副本假設(shè)a=1,線程一,和線程二同時修改了變量副本兩個線程中a=2了,這時候線程1和線程二再次將變量刷新到主內(nèi)存中,等于說兩個操作打印出同一個數(shù)字了,按照我們的意愿,這時候主內(nèi)存中變量a應(yīng)該等于3,但是還是等于2。
這個就是線程安全問題,即多個線程同時訪問一個資源時,會導(dǎo)致程序運(yùn)行結(jié)果并不是想看到的結(jié)果。。
- 由于每個線程執(zhí)行的過程是不可控的,所以很可能導(dǎo)致最終的結(jié)果與實際上的愿望相違背或者直接導(dǎo)致程序出錯。
如何解決線程安全問題?
基本上所有的并發(fā)模式在解決線程安全問題時,都采用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當(dāng)訪問完臨界資源后釋放鎖,讓其他線程繼續(xù)訪問。
在Java中,提供了兩種方式來實現(xiàn)同步互斥訪問:synchronized和Lock。
synchronized
synchronized用于多線程設(shè)計,有了synchronized關(guān)鍵字,多線程程序的運(yùn)行結(jié)果將變得可以控制。synchronized關(guān)鍵字用于保護(hù)共享數(shù)據(jù)。
synchronized實現(xiàn)同步的機(jī)制:synchronized依靠"鎖"機(jī)制進(jìn)行多線程同步,"鎖"有2種,一種是對象鎖,一種是類鎖。
互斥鎖
概念
能到達(dá)到互斥訪問目的的鎖。
舉個例子:假設(shè)我和老王要去上廁所,但是廁所只有一個,于是我進(jìn)了廁所,這時候老王就要像個SB一樣在門口等待了。
在Java中,可以使用synchronized關(guān)鍵字來標(biāo)記一個方法或者代碼塊,當(dāng)某個線程調(diào)用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執(zhí)行完畢或者代碼塊執(zhí)行完畢,這個線程才會釋放該對象的鎖,其他線程才能執(zhí)行這個方法或者代碼塊。
synchronized的使用
synchronized可以修飾一個方法
在這個時候,synchronized獲取的是該類的對象鎖。等于說我同一個對象中的兩個方法被synchronized修飾,那么這兩個方法都是互斥的。第一個線程再訪問的第一個方法的時候,第二個線程也必須要等待第一個線程完成了,才能訪問第二個方法。
public synchronized void addNum() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;//注意這里
}
那么上面的代碼加上一個synchronized。運(yùn)行結(jié)果就一定了,最終打印出來的結(jié)果是200了。
舉例:
public class TestSynchronized {
public synchronized void method1() throws InterruptedException {
System.out.println("method1 begin at:" + System.currentTimeMillis());
Thread.sleep(6000);
System.out.println("method1 end at:" + System.currentTimeMillis());
}
public synchronized void method2() throws InterruptedException {
while(true) {
System.out.println("method2 running");
Thread.sleep(200);
}
}
static TestSychronized instance = new TestSychronized();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
instance.method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=1; i<4; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 still alive");
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
instance.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
運(yùn)行結(jié)果:thread2一直等到thread1中的method1執(zhí)行完了之后才執(zhí)行method2,說明method1和method2互斥
synchronized {修飾代碼塊}的作用不僅于此,synchronized void method{}整個函數(shù)加上synchronized塊,效率并不好。在函數(shù)內(nèi)部,可能我們需要同步的只是小部分共享數(shù)據(jù),其他數(shù)據(jù),可以自由訪問,這時候我們可以用 synchronized(表達(dá)式){//語句}更加精確的控制。
synchronized可以修飾代碼塊
當(dāng)修飾代碼塊的時候鎖的就是傳入的對象,this只的是當(dāng)前類。
如下:
public void addNum() {
synchronized(this){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;
}
}
synchronized修飾靜態(tài)方法
synchronized還可以修飾靜態(tài)方法,這個時候鎖的對象就是類對象,下面兩種例子效果都相同:
public void addNum(){
synchronized(Obl.class)
}
}
public static synchronized void addNum() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;//注意這里
}
例子:
public class TestSychronized {
public synchronized static void method1() throws InterruptedException {
System.out.println("method1 begin at:" + System.currentTimeMillis());
Thread.sleep(6000);
System.out.println("method1 end at:" + System.currentTimeMillis());
}
public synchronized static void method2() throws InterruptedException {
while(true) {
System.out.println("method2 running");
Thread.sleep(200);
}
}
static TestSychronized instance1 = new TestSychronized();
static TestSychronized instance2 = new TestSychronized();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
instance1.method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=1; i<4; i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1 still alive");
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
instance2.method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
}
}
運(yùn)行結(jié)果:thread2一直等到thread1中的method1執(zhí)行完了之后才執(zhí)行method2,說明method1和method2互斥
總結(jié)
synchronized是java中的一個關(guān)鍵字,也就是說是Java語言內(nèi)置的特性。synchronized既能保證原子性,又能保證一致性。synchronized鎖的同一個對象的時候,其他線程不能訪問該對象中的其他的synchronized修飾的方法或者代碼塊,他們是互斥的。雖然synchronized可以保證線程的同步,但是在訪問線程非常多的情況下,性能低下。
如果一個代碼塊被synchronized修飾了,當(dāng)一個線程獲取了對應(yīng)的鎖,并執(zhí)行該代碼塊時,其他線程便只能一直等待,等待獲取鎖的線程釋放鎖,而這里獲取鎖的線程釋放鎖只會有兩種情況:
獲取鎖的線程執(zhí)行完了該代碼塊,然后線程釋放對鎖的占有;
線程執(zhí)行發(fā)生異常,此時JVM會讓線程自動釋放鎖。
那么如果這個獲取鎖的線程由于要等待IO或者其他原因(比如調(diào)用sleep方法)被阻塞了,但是又沒有釋放鎖,其他線程便只能干巴巴地等待,試想一下,這多么影響程序執(zhí)行效率。
因此就需要有一種機(jī)制可以不讓等待的線程一直無期限地等待下去(比如只等待一定的時間或者能夠響應(yīng)中斷),通過Lock就可以辦到。
Lock
lock是一個接口,它有如下方法:
public interface Lock {
void lock();//加鎖
void lockInterruptibly() throws InterruptedException;//加可中斷鎖
boolean tryLock();//加鎖成功返回true,失敗返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加鎖成功返回true,失敗等待一段時間后若仍無法加鎖則返回false,可響應(yīng)中斷
void unlock();//解鎖
Condition newCondition(); //返回一個Condition,利用它可以如和Synchronizd配合使用的wait()和notify()一樣對線程阻塞和喚醒,不同的是一個lock可以有多個condition.
}
Lock方法
lock()
用來獲取鎖。如果鎖已被其他線程獲取,則等待。
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
tryLock()
用來獲取鎖。如果鎖已被其他線程獲取,則返回false,否則返回true。不會進(jìn)行等待。
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
tryLock(long time, TimeUnit unit)
與tryLock()方法類似,只不過區(qū)別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內(nèi)如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true。
lockInterruptibly()
當(dāng)通過這個方法去獲取鎖時,如果線程正在等待獲取鎖,則這個線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)。也就使說,當(dāng)兩個線程同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那么對線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
ReentrantLock(Lock接口實現(xiàn)類)
是一個獨占鎖,與sychronized類似
ReadWriteLock
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一個用來獲取讀鎖,一個用來獲取寫鎖。也就是說將文件的讀寫操作分開,分成2個鎖來分配給線程,從而使得多個線程可以同時進(jìn)行讀操作。下面的ReentrantReadWriteLock實現(xiàn)了ReadWriteLock接口。
ReentrantReadWriteLock(ReadWriteLock實現(xiàn)類)
ReentrantReadWriteLock里面提供了很多豐富的方法,不過最主要的有兩個方法:readLock()和writeLock()用來獲取讀鎖和寫鎖。
ReentrantReadWriteLock里面的鎖主體就是一個Sync,也就是FairSync或者NonfairSync,所以說實際上只有一個鎖,只是在獲取讀取鎖和寫入鎖的方式上不一樣。
ReentrantReadWriteLock里面有兩個類:ReadLock/WriteLock,這兩個類都是Lock的實現(xiàn)。
如果有一個線程已經(jīng)占用了讀鎖,則此時其他線程如果要申請寫鎖,則申請寫鎖的線程會一直等待釋放讀鎖。但是如果其他線程要申請讀鎖,那么不需要等待依然能申請的到讀鎖。這很大的優(yōu)化了性能。
如果有一個線程已經(jīng)占用了寫鎖,則此時其他線程如果申請寫鎖或者讀鎖,則申請的線程會一直等待釋放寫鎖。
小結(jié)
ReentrantReadWriteLock相比ReentrantLock的最大區(qū)別是:ReentrantReadWriteLock的讀鎖是共享鎖,任何線程都可以獲取,而寫鎖是獨占鎖。ReentrantLock不論讀寫,是獨占鎖。
Lock和synchronized的選擇
Lock和synchronized有以下幾點不同:
- Lock是一個接口,而synchronized是Java中的關(guān)鍵字,synchronized是內(nèi)置的語言實現(xiàn);
- synchronized在發(fā)生異常時,會自動釋放線程占有的鎖,因此不會導(dǎo)致死鎖現(xiàn)象發(fā)生;而Lock在發(fā)生異常時,如果沒有主動通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用Lock時需要在finally塊中釋放鎖;
- Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行,使用synchronized時,等待的線程會一直等待下去,不能夠響應(yīng)中斷;
- 通過Lock可以知道有沒有成功獲取鎖,而synchronized卻無法辦到。
- Lock可以提高多個線程進(jìn)行讀操作的效率。
在性能上來說,如果競爭資源不激烈,兩者的性能是差不多的,而當(dāng)競爭資源非常激烈時(即有大量線程同時競爭),此時Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說,在具體使用時要根據(jù)適當(dāng)情況選擇。
鎖的概念相關(guān)介紹
- 可重入鎖
如果鎖具備可重入性,則稱作為可重入鎖。像synchronized和ReentrantLock都是可重入鎖
- 可中斷鎖
可中斷鎖:顧名思義,就是可以interrupt()中斷的鎖。
在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。
如果某一線程A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖,可能由于等待時間過長,線程B不想等待了,想先處理其他事情,我們可以讓它中斷自己或者在別的線程中中斷它,這種就是可中斷鎖。
在前面演示lockInterruptibly()的用法時已經(jīng)體現(xiàn)了Lock的可中斷性。
- 公平鎖
公平鎖即盡量以請求鎖的順序來獲取鎖。比如同是有多個線程在等待一個鎖,當(dāng)這個鎖被釋放時,等待時間最久的線程(最先請求的線程)會獲得該所,這種就是公平鎖。
非公平鎖即無法保證鎖的獲取是按照請求鎖的順序進(jìn)行的。這樣就可能導(dǎo)致某個或者一些線程永遠(yuǎn)獲取不到鎖。
在Java中,synchronized就是非公平鎖,它無法保證等待的線程獲取鎖的順序。
而對于ReentrantLock和ReentrantReadWriteLock,它默認(rèn)情況下是非公平鎖,但是可以設(shè)置為公平鎖。
- 讀寫鎖
讀寫鎖將對一個資源(比如文件)的訪問分成了2個鎖,一個讀鎖和一個寫鎖。
正因為有了讀寫鎖,才使得多個線程之間的讀操作不會發(fā)生沖突,提高了程序的性能。
ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現(xiàn)了這個接口。
可以通過readLock()獲取讀鎖,通過writeLock()獲取寫鎖。