1. 應(yīng)用背景
程序在設(shè)計(jì)當(dāng)中如果采取多線程操作的時(shí)候,如果操作的對(duì)象是一個(gè)的話,由于多個(gè)線程共享同一塊內(nèi)存空間,因此經(jīng)常會(huì)遇到數(shù)據(jù)安全訪問的問題,下面看一個(gè)經(jīng)典的問題,銀行取錢的問題:
1)、你有一張銀行卡,里面有5000塊錢,然后你到取款機(jī)取款,取出3000,當(dāng)正在取的時(shí)候,取款機(jī)已經(jīng)查詢到你有5000塊錢,然后正準(zhǔn)備減去300塊錢的時(shí)候
2)、你的老婆拿著那張銀行卡對(duì)應(yīng)的存折到銀行取錢,也要取3000.然后銀行的系統(tǒng)查詢,存折賬戶里還有6000(因?yàn)樯厦驽X還沒扣),所以它也準(zhǔn)備減去3000,
3)、你的卡里面減去3000,5000-3000=2000,并且你老婆的存折也是5000-3000=2000。
4)、結(jié)果,你們一共取了6000,但是卡里還剩下2000。
不難發(fā)現(xiàn),當(dāng)多個(gè)線程訪問同一數(shù)據(jù)并操作的時(shí)候非常容易出現(xiàn)類似的問題,。為了避免這樣的事情發(fā)生,我們要保證線程同步互斥,所謂同步互斥就是:并發(fā)執(zhí)行的多個(gè)線程在某一時(shí)間內(nèi)只允許一個(gè)線程在執(zhí)行以訪問共享數(shù)據(jù)。
2.同步互斥鎖
同步鎖原理:Java會(huì)為每個(gè)對(duì)象內(nèi)置同步鎖,通過使用synchronized來獲取一個(gè)對(duì)象的同步鎖,synchronized的使用方式,是在一段代碼塊中,加上synchronized(object){ ... }
當(dāng)線程首次執(zhí)行到synchronized語句塊時(shí)候會(huì)獲得對(duì)象的同步鎖(鎖最開始屬于對(duì)象,后被線程持有),在當(dāng)前線程不釋放同步鎖時(shí)候,其他線程獲取該對(duì)象同步鎖的行為是被阻塞的,直到該鎖被釋放。以下幾種情況下,線程才會(huì)釋放掉對(duì)象的同步鎖
1.線程執(zhí)行完synchronized修飾的語句塊。
2.線程主動(dòng)執(zhí)行wait()來釋放同步鎖。
同步鎖雖然可以解決多并發(fā)引起的數(shù)據(jù)安全問題,但是會(huì)在一定程度上影響程序運(yùn)行的效率,也會(huì)引起死鎖問題。因此慎重使用。下面是別人描述的死鎖,做引用。
死鎖:多個(gè)線程同時(shí)被阻塞,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放。由于線程被無限期地阻塞,因此程序不能正常運(yùn)行。簡(jiǎn)單的說就是:線程死鎖時(shí),第一個(gè)線程等待第二個(gè)線程釋放資源,而同時(shí)第二個(gè)線程又在等待第一個(gè)線程釋放資源。這里舉一個(gè)通俗的例子:如在人行道上兩個(gè)人迎面相遇,為了給對(duì)方讓道,兩人同時(shí)向一側(cè)邁出一步,雙方無法通過,又同時(shí)向另一側(cè)邁出一步,這樣還是無法通過。假設(shè)這種情況一直持續(xù)下去,這樣就會(huì)發(fā)生死鎖現(xiàn)象。
導(dǎo)致死鎖的根源在于不適當(dāng)?shù)剡\(yùn)用“synchronized”關(guān)鍵詞來管理線程對(duì)特定對(duì)象的訪問?!皊ynchronized”關(guān)鍵詞的作用是,確保在某個(gè)時(shí)刻只有一個(gè)線程被允許執(zhí)行特定的代碼塊,因此,被允許執(zhí)行的線程首先必須擁有對(duì)變量或?qū)ο蟮呐潘栽L問權(quán)。當(dāng)線程訪問對(duì)象時(shí),線程會(huì)給對(duì)象加鎖,而這個(gè)鎖導(dǎo)致其它也想訪問同一對(duì)象的線程被阻塞,直至第一個(gè)線程釋放它加在對(duì)象上的鎖。
一個(gè)死鎖的造成很簡(jiǎn)單,比如有兩個(gè)對(duì)象A 和 B 。第一個(gè)線程鎖住了A,然后休眠1秒,輪到第二個(gè)線程執(zhí)行,第二個(gè)線程鎖住了B,然后也休眠1秒,然后有輪到第一個(gè)線程執(zhí)行。第一個(gè)線程又企圖鎖住B,可是B已經(jīng)被第二個(gè)線程鎖定了,所以第一個(gè)線程進(jìn)入阻塞狀態(tài),又切換到第二個(gè)線程執(zhí)行。第二個(gè)線程又企圖鎖住A,可是A已經(jīng)被第一個(gè)線程鎖定了,所以第二個(gè)線程也進(jìn)入阻塞狀態(tài)。就這樣,死鎖造成了。
3.顯式鎖
為了符合Java面向?qū)ο蟮脑O(shè)計(jì)原則,在JDK1.5中,引入了顯示鎖的概念。
程序設(shè)計(jì)過程中如果提前發(fā)現(xiàn)可能會(huì)引起多線程數(shù)據(jù)訪問的安全問題,可以通過Lock lock =newReentrantLock();來獲取一個(gè)鎖,并將該鎖作為運(yùn)行參數(shù)傳入其中,在關(guān)鍵數(shù)據(jù)塊之前使用lock.lock();來控制對(duì)競(jìng)爭(zhēng)資源并發(fā)訪問的控制,顯式鎖的優(yōu)點(diǎn)是可以知道持有鎖的對(duì)象,比同步鎖也清晰好多。當(dāng)然使用完之后也要主動(dòng)釋放(lock.unlock())。
4.讀寫鎖
讀寫鎖是在顯示鎖的基礎(chǔ)上對(duì)讀寫進(jìn)行分離的一種鎖,可以認(rèn)為是為了提高并發(fā)效率的一種優(yōu)化。使用方法類比顯式鎖
初始化:ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
四種操作: rwl.readLock().lock(); ? rwl.readLock().unlock()
? ? ? ? ? ? ? ? ? ? ?rwl.writeLock().lock(); ?rwl.writeLock().unlock()
關(guān)于讀寫鎖之間的互斥:
1,讀鎖是排寫鎖操作的,讀鎖不排讀鎖操作,多個(gè)讀鎖可以并發(fā)不阻塞。即在讀鎖獲取后和讀鎖釋放之前,寫鎖并不能被任何線程獲得,多個(gè)讀鎖同時(shí)作用期間,試圖獲取寫鎖的線程都處于等待狀態(tài),當(dāng)最后一個(gè)讀鎖釋放后,試圖獲取寫鎖的線程才有機(jī)會(huì)獲取寫鎖。
2,寫鎖是排寫鎖、排讀鎖操作的。當(dāng)一個(gè)線程獲取到寫鎖之后,其他試圖獲取寫鎖和試圖獲取讀鎖的線程都處于等待狀態(tài),直到寫鎖被釋放。
3,在寫鎖狀態(tài)中,可以獲取讀鎖 即線程持有寫鎖的狀態(tài)下是可以繼續(xù)申請(qǐng)讀鎖的。即一線程同時(shí)持有讀鎖和寫鎖
4,讀鎖是不能夠獲得寫鎖的,如果要加寫鎖,本線程必須釋放所持有的讀鎖。
5.volatile
用volatile修飾的變量,線程在每次使用變量的時(shí)候,都會(huì)讀取變量修改后的最的值。volatile很容易被誤用,用來進(jìn)行原子性操作,可以看作是一種輕量級(jí)的synchronized,但是是盡量保證每次讀取的是最新的,并不絕對(duì)保證。