一毅糟、為何寫(xiě)
最為一個(gè)Android開(kāi)發(fā)者红选,如果做得不夠深入可能為不會(huì)去處理多線程同步的問(wèn)題,稍微簡(jiǎn)單點(diǎn)可能使用一個(gè)線程池就可以搞定了留特,有關(guān)線程池的介紹可以參考我的另一篇文章:ExecutorService+LruCache+DiskLruCache用一個(gè)類打造簡(jiǎn)單的圖片加載庫(kù)
只是前段時(shí)間研究Android音視頻硬解碼纠脾,看到開(kāi)源項(xiàng)目中用到了線程同步,就是在視頻的YUV數(shù)據(jù)的暫存蜕青,和解碼為視頻并展示苟蹈,用到了兩個(gè)線程去做,一個(gè)線程收集視頻源數(shù)據(jù)右核,一個(gè)線程負(fù)責(zé)解碼并播放視頻慧脱,一個(gè)視頻數(shù)據(jù)池是兩個(gè)線程共享的,數(shù)據(jù)池滿了或者空了的時(shí)候兩個(gè)線程是要做出相應(yīng)處理的贺喝,這就涉及到線程同步了菱鸥。
學(xué)習(xí)、工作和生活的心態(tài)就要像向日葵躏鱼,就算是太陽(yáng)不在也要迎著月亮氮采!
二、名字講解
什么是線程同步染苛?
當(dāng)使用多個(gè)線程來(lái)訪問(wèn)同一個(gè)數(shù)據(jù)時(shí)鹊漠,非常容易出現(xiàn)線程安全問(wèn)題(比如多個(gè)線程都在操作同一數(shù)據(jù)導(dǎo)致數(shù)據(jù)不一致),所以我們用同步機(jī)制來(lái)解決這些問(wèn)題。
實(shí)現(xiàn)同步機(jī)制有兩個(gè)方法:
1茶行、同步代碼塊:
synchronized(同一個(gè)數(shù)據(jù)){} 同一個(gè)數(shù)據(jù):就是N條線程同時(shí)訪問(wèn)一個(gè)數(shù)據(jù)躯概。
2、同步方法:
public synchronized 數(shù)據(jù)返回類型方法名(){}
通過(guò)使用同步方法畔师,可非常方便的將某類變成線程安全的類娶靡,具有如下特征:
1,該類的對(duì)象可以被多個(gè)線程安全的訪問(wèn)看锉。
2姿锭,每個(gè)線程調(diào)用該對(duì)象的任意方法之后,都將得到正確的結(jié)果伯铣。
3艾凯,每個(gè)線程調(diào)用該對(duì)象的任意方法之后,該對(duì)象狀態(tài)依然保持合理狀態(tài)懂傀。
注:synchronized關(guān)鍵字可以修飾方法,也可以修飾代碼塊蜡感,但不能修飾構(gòu)造器蹬蚁,屬性等
※不要對(duì)線程安全類的所有方法都進(jìn)行同步恃泪,只對(duì)那些會(huì)改變共享資源方法的進(jìn)行同步。
線程通訊:
當(dāng)使用synchronized 來(lái)修飾某個(gè)共享資源時(shí)(分同步代碼塊和同步方法兩種情況),當(dāng)某個(gè)線程獲得共享資源的鎖后就可以執(zhí)行相應(yīng)的代碼段犀斋,直到該線程運(yùn)行完該代碼段后才釋放對(duì)該共享資源的鎖贝乎,讓其他線程有機(jī)會(huì)執(zhí)行對(duì)該共享資源的修改。當(dāng)某個(gè)線程占有某個(gè)共享資源的鎖時(shí)叽粹,如果另外一個(gè)線程也想獲得這把鎖運(yùn)行就需要使用wait() 和notify()/notifyAll()方法來(lái)進(jìn)行線程通訊了览效。
Java.lang.object 里的三個(gè)方法wait() notify() notifyAll()
wait()
導(dǎo)致當(dāng)前線程等待,直到其他線程調(diào)用同步監(jiān)視器的notify方法或notifyAll方法來(lái)喚醒該線程虫几。
wait(mills)
都是等待指定時(shí)間后自動(dòng)蘇醒锤灿,調(diào)用wait方法的當(dāng)前線程會(huì)釋放該同步監(jiān)視器的鎖定,可以不用notify或notifyAll方法把它喚醒辆脸。
notify()
喚醒在同步監(jiān)視器上等待的單個(gè)線程但校,如果所有線程都在同步監(jiān)視器上等待,則會(huì)選擇喚醒其中一個(gè)線程啡氢,選擇是任意性的状囱,只有當(dāng)前線程放棄對(duì)該同步監(jiān)視器的鎖定后,也就是使用wait方法后倘是,才可以執(zhí)行被喚醒的線程亭枷。
notifyAll()
喚醒在同步監(jiān)視器上等待的所有的線程。只用當(dāng)前線程放棄對(duì)該同步監(jiān)視器的鎖定后搀崭,也就是使用wait方法后,才可以執(zhí)行被喚醒的線程叨粘。
注意,notify方法一定要在synchronized同步里面調(diào)用门坷,還有做異常捕捉宣鄙。
原子操作:根據(jù)Java規(guī)范,對(duì)于基本類型的賦值或者返回值操作默蚌,是原子操作冻晤。但這里的基本數(shù)據(jù)類型不包括long和double, 因?yàn)镴VM看到的基本存儲(chǔ)單位是32位,而long 和double都要用64位來(lái)表示绸吸。所以無(wú)法在一個(gè)時(shí)鐘周期內(nèi)完成鼻弧。
自增操作(++)不是原子操作,因?yàn)樗婕暗揭淮巫x和一次寫(xiě)锦茁。
原子操作:由一組相關(guān)的操作完成攘轩,這些操作可能會(huì)操縱與其它的線程共享的資源,為了保證得到正確的運(yùn)算結(jié)果码俩,一個(gè)線程在執(zhí)行原子操作其間度帮,應(yīng)該采取其他的措施使得其他的線程不能操縱共享資源。
同步代碼塊:為了保證每個(gè)線程能夠正常執(zhí)行原子操作,Java引入了同步機(jī)制笨篷,具體的做法是在代表原子操作的程序代碼前加上synchronized標(biāo)記瞳秽,這樣的代碼被稱為同步代碼塊。
同步鎖:每個(gè)JAVA對(duì)象都有且只有一個(gè)同步鎖率翅,在任何時(shí)刻练俐,最多只允許一個(gè)線程擁有這把鎖。
當(dāng)一個(gè)線程試圖訪問(wèn)帶有synchronized(this)標(biāo)記的代碼塊時(shí)冕臭,必須獲得 this關(guān)鍵字引用的對(duì)象的鎖腺晾,在以下的兩種情況下,本線程有著不同的命運(yùn)辜贵。
1悯蝉、 假如這個(gè)鎖已經(jīng)被其它的線程占用,JVM就會(huì)把這個(gè)線程放到本對(duì)象的鎖池中念颈。本線程進(jìn)入阻塞狀態(tài)泉粉。鎖池中可能有很多的線程,等到其他的線程釋放了鎖榴芳,JVM就會(huì)從鎖池中隨機(jī)取出一個(gè)線程嗡靡,使這個(gè)線程擁有鎖,并且轉(zhuǎn)到就緒狀態(tài)窟感。
2讨彼、 假如這個(gè)鎖沒(méi)有被其他線程占用,本線程會(huì)獲得這把鎖柿祈,開(kāi)始執(zhí)行同步代碼塊哈误。 (一般情況下在執(zhí)行同步代碼塊時(shí)不會(huì)釋放同步鎖,但也有特殊情況會(huì)釋放對(duì)象鎖 如在執(zhí)行同步代碼塊時(shí)躏嚎,遇到異常而導(dǎo)致線程終止蜜自,鎖會(huì)被釋放;在執(zhí)行代碼塊時(shí)卢佣,執(zhí)行了鎖所屬對(duì)象的wait()方法重荠,這個(gè)線程會(huì)釋放對(duì)象鎖,進(jìn)入對(duì)象的等待池中)
線程同步的特征:
1虚茶、 如果一個(gè)同步代碼塊和非同步代碼塊同時(shí)操作共享資源戈鲁,仍然會(huì)造成對(duì)共享資源的競(jìng)爭(zhēng)。因?yàn)楫?dāng)一個(gè)線程執(zhí)行一個(gè)對(duì)象的同步代碼塊時(shí)嘹叫,其他的線程仍然可以執(zhí)行對(duì)象的非同步代碼塊婆殿。(所謂的線程之間保持同步,是指不同的線程在執(zhí)行同一個(gè)對(duì)象的同步代碼塊時(shí)罩扇,因?yàn)橐@得對(duì)象的同步鎖而互相牽制)
2婆芦、 每個(gè)對(duì)象都有唯一的同步鎖
3、 在靜態(tài)方法前面可以使用synchronized修飾符。
4寞缝、 當(dāng)一個(gè)線程開(kāi)始執(zhí)行同步代碼塊時(shí)癌压,并不意味著必須以不間斷的方式運(yùn)行,進(jìn)入同步代碼塊的線程可以執(zhí)行Thread.sleep()或執(zhí)行Thread.yield()方法荆陆,此時(shí)它并不釋放對(duì)象鎖,只是把運(yùn)行的機(jī)會(huì)讓給其他的線程集侯。
5被啼、 Synchronized聲明不會(huì)被繼承,如果一個(gè)用synchronized修飾的方法被子類覆蓋棠枉,那么子類中這個(gè)方法不在保持同步浓体,除非用synchronized修飾。
釋放對(duì)象的鎖:
1辈讶、 執(zhí)行完同步代碼塊就會(huì)釋放對(duì)象的鎖
2命浴、 在執(zhí)行同步代碼塊的過(guò)程中,遇到異常而導(dǎo)致線程終止贱除,鎖也會(huì)被釋放
3生闲、 在執(zhí)行同步代碼塊的過(guò)程中,執(zhí)行了鎖所屬對(duì)象的wait()方法月幌,這個(gè)線程會(huì)釋放對(duì)象鎖碍讯,進(jìn)入對(duì)象的等待池。
死鎖:
線程1獨(dú)占(鎖定)資源A扯躺,等待獲得資源B后捉兴,才能繼續(xù)執(zhí)行
同時(shí)
線程2獨(dú)占(鎖定)資源B,等待獲得資源A后录语,才能繼續(xù)執(zhí)行
這樣就會(huì)發(fā)生死鎖倍啥,程序無(wú)法正常執(zhí)行
如何避免死鎖
一個(gè)通用的經(jīng)驗(yàn)法則是:當(dāng)幾個(gè)線程都要訪問(wèn)共享資源A、B澎埠、C 時(shí)虽缕,保證每個(gè)線程都按照同樣的順序去訪問(wèn)他們。
注意:
1失暂、線程同步就是線程排隊(duì)彼宠。同步就是排隊(duì)。線程同步的目的就是避免線程“同步”執(zhí)行弟塞。
2凭峡、只有共享資源的讀寫(xiě)訪問(wèn)才需要同步。如果不是共享資源决记,那么就根本沒(méi)有同步的必要摧冀。
3、只有“變量”才需要同步訪問(wèn)。如果共享的資源是固定不變的索昂,那么就相當(dāng)于“常量”建车,線程同時(shí)讀取常量也不需要同步。至少一個(gè)線程修改共享資源椒惨,這樣的情況下缤至,線程之間就需要同步。
4康谆、多個(gè)線程訪問(wèn)共享資源的代碼有可能是同一份代碼领斥,也有可能是不同的代碼;無(wú)論是否執(zhí)行同一份代碼沃暗,只要這些線程的代碼訪問(wèn)同一份可變的共享資源月洛,這些線程之間就需要同步。
5孽锥、我們要盡量避免這種直接把synchronized加在函數(shù)定義上的偷懶做法嚼黔。因?yàn)槲覀円刂仆搅6取M降拇a段越小越好惜辑。synchronized控制的范圍越小越好唬涧。
同步鎖:
我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙韵丑。哪個(gè)線程獲取了這把鑰匙爵卒,才有權(quán)利訪問(wèn)該共享資源。
同步鎖不是加在共享資源上撵彻,而是加在訪問(wèn)共享資源的代碼段上钓株。
訪問(wèn)同一份共享資源的不同代碼段,應(yīng)該加上同一個(gè)同步鎖陌僵;如果加的是不同的同步鎖轴合,那么根本就起不到同步的作用,沒(méi)有任何意義碗短。
這就是說(shuō)受葛,同步鎖本身也一定是多個(gè)線程之間的共享對(duì)象。
三偎谁、生產(chǎn)者消費(fèi)者代碼示例
產(chǎn)品倉(cāng)庫(kù)
package com.danxx.javalib2;
import java.util.LinkedList;
import java.util.Queue;
/**
* 數(shù)據(jù)存儲(chǔ)倉(cāng)庫(kù)和操作
* 一個(gè)緩沖區(qū)总滩,緩沖區(qū)有最大限制,當(dāng)緩沖區(qū)滿
* 的時(shí)候巡雨,生產(chǎn)者是不能將產(chǎn)品放入到緩沖區(qū)里面的闰渔,
* 當(dāng)然,當(dāng)緩沖區(qū)是空的時(shí)候铐望,消費(fèi)者也不能從中拿出來(lái)產(chǎn)品冈涧,
* 這就涉及到了在多線程中的條件判斷
* Created by dawish on 2017/7/13.
*/
public class Storage {
private static volatile int goodNumber = 1;
private final static int MAX_SIZE = 20;
/**
* Queue操作解析:
* add 增加一個(gè)元索 如果隊(duì)列已滿茂附, 則拋出一個(gè)IIIegaISlabEepeplian異常
* remove 移除并返回隊(duì)列頭部的元素 如果隊(duì)列為空, 則拋出一個(gè)NoSuchElementException異常
* element 返回隊(duì)列頭部的元素 如果隊(duì)列為空督弓, 則拋出一個(gè)NoSuchElementException異常
* offer 添加一個(gè)元素并返回true 如果隊(duì)列已滿营曼, 則返回false
* poll 移除并返問(wèn)隊(duì)列頭部的元素 如果隊(duì)列為空, 則返回null
* peek 返回隊(duì)列頭部的元素 如果隊(duì)列為空愚隧, 則返回null
* put 添加一個(gè)元素 如果隊(duì)列滿蒂阱, 則阻塞
* take 移除并返回隊(duì)列頭部的元素 如果隊(duì)列為空, 則阻塞
*
*/
Queue<String> storage;
public Storage() {
storage = new LinkedList<String>();
}
/**
*
* @param dataValue
*/
public synchronized void put(String dataValue, String threadName){
if(storage.size() >= MAX_SIZE){
try {
goodNumber = 1;
super.wait(); //當(dāng)生產(chǎn)滿了后讓生產(chǎn)線程等待
return;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(dataValue + goodNumber++);
System.out.println(threadName + dataValue + goodNumber);
super.notify(); //每次添加一個(gè)數(shù)據(jù)就喚醒一個(gè)消費(fèi)等待的線程來(lái)消費(fèi)
}
/**
*
* @return
* @throws InterruptedException
*/
public synchronized String get(String threadName) {
if(storage.size() == 0){
try {
super.wait(); //當(dāng)產(chǎn)品倉(cāng)庫(kù)為空的時(shí)候讓消費(fèi)線程等待
System.out.println(threadName + "wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
super.notify(); //當(dāng)數(shù)據(jù)不為空的時(shí)候就喚醒一個(gè)生產(chǎn)線程來(lái)生產(chǎn)
String value = storage.remove();
return value;
}
}
生產(chǎn)者
package com.danxx.javalib2;
import java.util.UUID;
/**
* 生產(chǎn)者
* Created by dawish on 2017/7/13.
*/
public class Producer extends Thread{
private Storage storage;//生產(chǎn)者倉(cāng)庫(kù)
private String name="";
public Producer(Storage storage, String name) {
this.storage = storage;
this.name = name;
}
public void run(){
//生產(chǎn)者每隔1s生產(chǎn)1~100消息
long oldTime = System.currentTimeMillis();
while(true){
synchronized(storage){
if (System.currentTimeMillis() - oldTime >= 1000) {
oldTime = System.currentTimeMillis();
String msg = UUID.randomUUID().toString();
storage.put("-ID:" ,name);
}
}
}
}
}
消費(fèi)者
package com.danxx.javalib2;
/**
* 消費(fèi)者
* Created by dawish on 2017/7/13.
*/
public class Consumer extends Thread{
private Storage storage;//倉(cāng)庫(kù)
private String name="";
public Consumer(Storage storage, String name) {
this.storage = storage;
this.name = name;
}
public void run(){
while(true){
synchronized(storage){
//消費(fèi)者去倉(cāng)庫(kù)拿消息的時(shí)候狂塘,如果發(fā)現(xiàn)倉(cāng)庫(kù)數(shù)據(jù)為空蒜危,則等待
String data = storage.get(name);
if(data != null){
System.out.println(name +"-------------"+ data);
}
}
}
}
}
main方法
package com.danxx.javalib2;
/**
* Java中的多線程會(huì)涉及到線程間通信,常見(jiàn)的線程通信方式睹耐,例如共享變量、管道流等部翘,
* 這里我們要實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式硝训,也需要涉及到線程通信,不過(guò)這里我們用到了java中的
* wait()新思、notify()方法:
* wait():進(jìn)入臨界區(qū)的線程在運(yùn)行到一部分后窖梁,發(fā)現(xiàn)進(jìn)行后面的任務(wù)所需的資源還沒(méi)有準(zhǔn)備充分,
* 所以調(diào)用wait()方法夹囚,讓線程阻塞纵刘,等待資源,同時(shí)釋放臨界區(qū)的鎖荸哟,此時(shí)線程的狀態(tài)也從RUNNABLE狀態(tài)變?yōu)閃AITING狀態(tài)假哎;
* notify():準(zhǔn)備資源的線程在準(zhǔn)備好資源后,調(diào)用notify()方法通知需要使用資源的線程鞍历,
* 同時(shí)釋放臨界區(qū)的鎖舵抹,將臨界區(qū)的鎖交給使用資源的線程。
* wait()劣砍、notify()這兩個(gè)方法惧蛹,都必須要在臨界區(qū)中調(diào)用,即是在synchronized同步塊中調(diào)用刑枝,
* 不然會(huì)拋出IllegalMonitorStateException的異常香嗓。
* Created by dawish on 2017/7/14.
*/
public class MainApp {
public static void main(String[] args) {
Storage storage = new Storage();
Producer producer1 = new Producer(storage, "Producer-1");
Producer producer2 = new Producer(storage, "Producer-2");
Producer producer3 = new Producer(storage, "Producer-3");
Producer producer4 = new Producer(storage, "Producer-4");
Consumer consumer1 = new Consumer(storage, "Consumer-1");
Consumer consumer2 = new Consumer(storage, "Consumer-2");
producer1.start();
producer2.start();
producer3.start();
producer4.start();
consumer1.start();
consumer2.start();
}
}