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ì)象的訪問局义。“synchronized”關(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ì)保證棋凳。