線程與鎖模型
線程與鎖模型是比較原始的一種處理并發(fā)的方式,主要是對(duì)底層硬件的運(yùn)行過程形式化,這是它的優(yōu)點(diǎn)也是缺點(diǎn)。
線程與鎖模型非常直接,幾乎所有的編程語(yǔ)言都提供了支持,但是如果不了解該模型,那么程序會(huì)很容易出錯(cuò),而且難以維護(hù)。
為什么需要鎖
我們先來看一段多線程的代碼:
public class Counting {
public static void main( String[] args) throws InterruptedException {
class Counter {
private int count = 0;
public void increment() { ++count; }
public int getCount() { return count;}
}
final Counter counter = new Counter();
class CountingThread extends Thread {
public void run() {
for(int x = 0; x < 10000; x++)
counter.increment();
}
}
CountingThread t1 = new CountingThread();
CountingThread t2 = new CountingThread();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
這是創(chuàng)建了兩個(gè)線程t1與t2,每一個(gè)線程都調(diào)用了counter.increment(),10000次,看上去特別簡(jiǎn)單,但是每次運(yùn)行都會(huì)有不同的結(jié)果,這是因?yàn)樵诓僮鱟ounter對(duì)象的時(shí)候發(fā)生了競(jìng)態(tài)條件。
競(jìng)態(tài)條件是指代碼的行為取決于各操作的時(shí)序。
如果不理解我們先看一下java編譯器是如何處理的++count的
//獲取值
getfield #2
//將常量i 1 進(jìn)棧
iconst_1
//加i
iadd
//更新值
putfield#2
問題就出在這里,如果同時(shí)調(diào)用increment(),兩個(gè)線程在獲取值的時(shí)候是同一個(gè)值如100,那么放回去的時(shí)候雖然操作了兩次increment(),但是實(shí)際結(jié)果是101。
synchronize
java中遇到這種問題可以有一種的解決辦法,進(jìn)行同步(synchronize)訪問。
只需要在之前的代碼中這么改一下:
······
public synchronized void increment() { ++count; }
······
那么在線程使用increment函數(shù)的時(shí)候會(huì)獲得該函數(shù)的鎖,其他線程將不能訪問,直到該線程返回時(shí)釋放鎖。
現(xiàn)在因?yàn)樵黾恿送降拇a,執(zhí)行都會(huì)獲得正確的結(jié)果--20000。
但是synchronize也會(huì)帶來很多坑,下面一一介紹。
亂序編譯
static boolan isReady = false
static int number = 0;
static Thread t1 = new Thread() {
public void run() {
number = 100;
isReady = true;
}
static Thread t2 = new Thread() {
public void run() {
if (isReady)
System.out.println(number);
else
System.out.println("not ready");
}
想想看如果同時(shí)運(yùn)行上面的代碼會(huì)發(fā)生什么事?
結(jié)果:
- 打印not ready
- 打印100。
- 打印0。 (為什么?)
為什么 number = 100; isReady = true;語(yǔ)句發(fā)生了顛倒?
但是事實(shí)上是有可能發(fā)生的:
- 編譯器的靜態(tài)優(yōu)化會(huì)打亂。
- JVM的動(dòng)態(tài)優(yōu)化會(huì)打亂。
- 硬件可以通過亂序執(zhí)行來優(yōu)化性能。
但實(shí)際上還有更糟糕的,有時(shí)候一個(gè)線程產(chǎn)生的修改可能對(duì)于另外一個(gè)線程來講是不可見的。
從常識(shí)上來說,無論是編譯器,JVM還是硬件都不應(yīng)該改變代碼的原有邏輯,這里我們需要有明確的標(biāo)準(zhǔn)來知道可能會(huì)發(fā)生什么,那就是Java內(nèi)存模型。
在Java內(nèi)存模型中還說明了上面一個(gè)問題的答案:
如果讀線程與寫線程不進(jìn)行同步,就不能保證可見性。
所以除了increment()之外,也應(yīng)該對(duì)getCount()方法同步,不然可能會(huì)得到一個(gè)已經(jīng)失效的值。
死鎖
有一個(gè)著名的問題--哲學(xué)家進(jìn)餐問題

如圖。
哲學(xué)家的狀態(tài)可能是「思考」也可能是「饑餓」,如果是饑餓,他就會(huì)將兩邊的筷子拿起來,并且進(jìn)餐一段時(shí)間。進(jìn)餐結(jié)束后哲學(xué)家就會(huì)返回筷子。
那么代碼可以這么寫
synchronized(左邊的筷子) {
synchronized(右邊的筷子)
}
這樣會(huì)出現(xiàn)一個(gè)問題,如果所有的哲學(xué)家在某個(gè)時(shí)刻,將左邊的筷子都拿起來,就都不能拿到右邊的筷子了,而且也不能釋放左邊的筷子,這樣程序就會(huì)一直卡組,這就是死鎖。
如果解決?
我們可以給筷子設(shè)置編號(hào),只能先拿小的,然后拿大的。
或者給哲學(xué)家拿筷子的順序進(jìn)行設(shè)置。
雖然可以解決但是依然暴露出synchronized的問題。
方法內(nèi)部的陷阱
private synchronized void update {
for (Person person: persons)
person.eat();
}
這段邏輯看上去沒有問題,方法加上了synchronized,所以多線程使用的時(shí)候也會(huì)同步訪問。
但實(shí)際上也是有一個(gè)陷阱,在eat方法這個(gè)地方。
因?yàn)閷?duì)eat方法不了解,所以可能eat方法中也調(diào)用了synchronized函數(shù),這樣就是使用了兩把鎖,就像之前的哲學(xué)家進(jìn)餐問題一樣,可能會(huì)發(fā)生死鎖。
解決的辦法是將persons拷貝一份:
private void update {
ArrayList<Person> personsCopy;
synchronized(this) {
personCopy = (ArrayList<Person>)persons.clone();
for (Person person: persons)
person.eat();
}
這樣調(diào)用方法的時(shí)候不需要加鎖,而且也減少了持有鎖的時(shí)間。
總結(jié)
- 對(duì)共享變量需要同步化。
- 讀線程和寫線程需要同步化。
- 按照約定的全局順序來獲得多把鎖。
- 持有鎖的時(shí)候盡量不要調(diào)用外部方法。
- 持有鎖的時(shí)間應(yīng)該盡量短。