一、爲何寫
最爲一個Android開發者,如果做得不夠深入可能爲不會去處理多線程同步的問題,稍微簡單點可能使用一個線程池就可以搞定了,有關線程池的介紹可以參考我的另一篇文章:ExecutorService+LruCache+DiskLruCache用一個類打造簡單的圖片加載庫
只是前段時間研究Android音視頻硬解碼,看到開源項目中用到了線程同步,就是在視頻的YUV數據的暫存,和解碼爲視頻並展示,用到了兩個線程去做,一個線程收集視頻源數據,一個線程負責解碼並播放視頻,一個視頻數據池是兩個線程共享的,數據池滿了或者空了的時候兩個線程是要做出相應處理的,這就涉及到線程同步了。
學習、工作和生活的心態就要像向日葵,就算是太陽不在也要迎着月亮!
二、名字講解
什麼是線程同步?
當使用多個線程來訪問同一個數據時,非常容易出現線程安全問題(比如多個線程都在操作同一數據導致數據不一致),所以我們用同步機制來解決這些問題。
實現同步機制有兩個方法:
1、同步代碼塊:
synchronized(同一個數據){} 同一個數據:就是N條線程同時訪問一個數據。
2、同步方法:
public synchronized 數據返回類型方法名(){}
通過使用同步方法,可非常方便的將某類變成線程安全的類,具有如下特徵:
1,該類的對象可以被多個線程安全的訪問。
2,每個線程調用該對象的任意方法之後,都將得到正確的結果。
3,每個線程調用該對象的任意方法之後,該對象狀態依然保持合理狀態。
注:synchronized關鍵字可以修飾方法,也可以修飾代碼塊,但不能修飾構造器,屬性等
※不要對線程安全類的所有方法都進行同步,只對那些會改變共享資源方法的進行同步。
線程通訊:
當使用synchronized 來修飾某個共享資源時(分同步代碼塊和同步方法兩種情況),當某個線程獲得共享資源的鎖後就可以執行相應的代碼段,直到該線程運行完該代碼段後才釋放對該共享資源的鎖,讓其他線程有機會執行對該共享資源的修改。當某個線程佔有某個共享資源的鎖時,如果另外一個線程也想獲得這把鎖運行就需要使用wait() 和notify()/notifyAll()方法來進行線程通訊了。
Java.lang.object 裏的三個方法wait() notify() notifyAll()
wait()
導致當前線程等待,直到其他線程調用同步監視器的notify方法或notifyAll方法來喚醒該線程。
wait(mills)
都是等待指定時間後自動甦醒,調用wait方法的當前線程會釋放該同步監視器的鎖定,可以不用notify或notifyAll方法把它喚醒。
notify()
喚醒在同步監視器上等待的單個線程,如果所有線程都在同步監視器上等待,則會選擇喚醒其中一個線程,選擇是任意性的,只有當前線程放棄對該同步監視器的鎖定後,也就是使用wait方法後,纔可以執行被喚醒的線程。
notifyAll()
喚醒在同步監視器上等待的所有的線程。只用當前線程放棄對該同步監視器的鎖定後,也就是使用wait方法後,纔可以執行被喚醒的線程。
注意,notify方法一定要在synchronized同步裏面調用,還有做異常捕捉。
原子操作:根據Java規範,對於基本類型的賦值或者返回值操作,是原子操作。但這裏的基本數據類型不包括long和double, 因爲JVM看到的基本存儲單位是32位,而long 和double都要用64位來表示。所以無法在一個時鐘週期內完成。
自增操作(++)不是原子操作,因爲它涉及到一次讀和一次寫。
原子操作:由一組相關的操作完成,這些操作可能會操縱與其它的線程共享的資源,爲了保證得到正確的運算結果,一個線程在執行原子操作其間,應該採取其他的措施使得其他的線程不能操縱共享資源。
同步代碼塊:爲了保證每個線程能夠正常執行原子操作,Java引入了同步機制,具體的做法是在代表原子操作的程序代碼前加上synchronized標記,這樣的代碼被稱爲同步代碼塊。
同步鎖:每個JAVA對象都有且只有一個同步鎖,在任何時刻,最多隻允許一個線程擁有這把鎖。
當一個線程試圖訪問帶有synchronized(this)標記的代碼塊時,必須獲得 this關鍵字引用的對象的鎖,在以下的兩種情況下,本線程有着不同的命運。
1、 假如這個鎖已經被其它的線程佔用,JVM就會把這個線程放到本對象的鎖池中。本線程進入阻塞狀態。鎖池中可能有很多的線程,等到其他的線程釋放了鎖,JVM就會從鎖池中隨機取出一個線程,使這個線程擁有鎖,並且轉到就緒狀態。
2、 假如這個鎖沒有被其他線程佔用,本線程會獲得這把鎖,開始執行同步代碼塊。 (一般情況下在執行同步代碼塊時不會釋放同步鎖,但也有特殊情況會釋放對象鎖 如在執行同步代碼塊時,遇到異常而導致線程終止,鎖會被釋放;在執行代碼塊時,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池中)
線程同步的特徵:
1、 如果一個同步代碼塊和非同步代碼塊同時操作共享資源,仍然會造成對共享資源的競爭。因爲當一個線程執行一個對象的同步代碼塊時,其他的線程仍然可以執行對象的非同步代碼塊。(所謂的線程之間保持同步,是指不同的線程在執行同一個對象的同步代碼塊時,因爲要獲得對象的同步鎖而互相牽制)
2、 每個對象都有唯一的同步鎖
3、 在靜態方法前面可以使用synchronized修飾符。
4、 當一個線程開始執行同步代碼塊時,並不意味着必須以不間斷的方式運行,進入同步代碼塊的線程可以執行Thread.sleep()或執行Thread.yield()方法,此時它並不釋放對象鎖,只是把運行的機會讓給其他的線程。
5、 Synchronized聲明不會被繼承,如果一個用synchronized修飾的方法被子類覆蓋,那麼子類中這個方法不在保持同步,除非用synchronized修飾。
釋放對象的鎖:
1、 執行完同步代碼塊就會釋放對象的鎖
2、 在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放
3、 在執行同步代碼塊的過程中,執行了鎖所屬對象的wait()方法,這個線程會釋放對象鎖,進入對象的等待池。
死鎖:
線程1獨佔(鎖定)資源A,等待獲得資源B後,才能繼續執行
同時
線程2獨佔(鎖定)資源B,等待獲得資源A後,才能繼續執行
這樣就會發生死鎖,程序無法正常執行
如何避免死鎖
一個通用的經驗法則是:當幾個線程都要訪問共享資源A、B、C 時,保證每個線程都按照同樣的順序去訪問他們。
注意:
1、線程同步就是線程排隊。同步就是排隊。線程同步的目的就是避免線程“同步”執行。
2、只有共享資源的讀寫訪問才需要同步。如果不是共享資源,那麼就根本沒有同步的必要。
3、只有“變量”才需要同步訪問。如果共享的資源是固定不變的,那麼就相當於“常量”,線程同時讀取常量也不需要同步。至少一個線程修改共享資源,這樣的情況下,線程之間就需要同步。
4、多個線程訪問共享資源的代碼有可能是同一份代碼,也有可能是不同的代碼;無論是否執行同一份代碼,只要這些線程的代碼訪問同一份可變的共享資源,這些線程之間就需要同步。
5、我們要儘量避免這種直接把synchronized加在函數定義上的偷懶做法。因爲我們要控制同步粒度。同步的代碼段越小越好。synchronized控制的範圍越小越好。
同步鎖:
我們可以給共享資源加一把鎖,這把鎖只有一把鑰匙。哪個線程獲取了這把鑰匙,纔有權利訪問該共享資源。
同步鎖不是加在共享資源上,而是加在訪問共享資源的代碼段上。
訪問同一份共享資源的不同代碼段,應該加上同一個同步鎖;如果加的是不同的同步鎖,那麼根本就起不到同步的作用,沒有任何意義。
這就是說,同步鎖本身也一定是多個線程之間的共享對象。
三、生產者消費者代碼示例
產品倉庫
package com.danxx.javalib2;
import java.util.LinkedList;
import java.util.Queue;
/**
* 數據存儲倉庫和操作
* 一個緩衝區,緩衝區有最大限制,當緩衝區滿
* 的時候,生產者是不能將產品放入到緩衝區裏面的,
* 當然,當緩衝區是空的時候,消費者也不能從中拿出來產品,
* 這就涉及到了在多線程中的條件判斷
* 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 增加一個元索 如果隊列已滿, 則拋出一個IIIegaISlabEepeplian異常
* remove 移除並返回隊列頭部的元素 如果隊列爲空, 則拋出一個NoSuchElementException異常
* element 返回隊列頭部的元素 如果隊列爲空, 則拋出一個NoSuchElementException異常
* offer 添加一個元素並返回true 如果隊列已滿, 則返回false
* poll 移除並返問隊列頭部的元素 如果隊列爲空, 則返回null
* peek 返回隊列頭部的元素 如果隊列爲空, 則返回null
* put 添加一個元素 如果隊列滿, 則阻塞
* take 移除並返回隊列頭部的元素 如果隊列爲空, 則阻塞
*
*/
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(); //當生產滿了後讓生產線程等待
return;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(dataValue + goodNumber++);
System.out.println(threadName + dataValue + goodNumber);
super.notify(); //每次添加一個數據就喚醒一個消費等待的線程來消費
}
/**
*
* @return
* @throws InterruptedException
*/
public synchronized String get(String threadName) {
if(storage.size() == 0){
try {
super.wait(); //當產品倉庫爲空的時候讓消費線程等待
System.out.println(threadName + "wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
super.notify(); //當數據不爲空的時候就喚醒一個生產線程來生產
String value = storage.remove();
return value;
}
}
生產者
package com.danxx.javalib2;
import java.util.UUID;
/**
* 生產者
* Created by dawish on 2017/7/13.
*/
public class Producer extends Thread{
private Storage storage;//生產者倉庫
private String name="";
public Producer(Storage storage, String name) {
this.storage = storage;
this.name = name;
}
public void run(){
//生產者每隔1s生產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);
}
}
}
}
}
消費者
package com.danxx.javalib2;
/**
* 消費者
* Created by dawish on 2017/7/13.
*/
public class Consumer extends Thread{
private Storage storage;//倉庫
private String name="";
public Consumer(Storage storage, String name) {
this.storage = storage;
this.name = name;
}
public void run(){
while(true){
synchronized(storage){
//消費者去倉庫拿消息的時候,如果發現倉庫數據爲空,則等待
String data = storage.get(name);
if(data != null){
System.out.println(name +"-------------"+ data);
}
}
}
}
}
main方法
package com.danxx.javalib2;
/**
* Java中的多線程會涉及到線程間通信,常見的線程通信方式,例如共享變量、管道流等,
* 這裏我們要實現生產者消費者模式,也需要涉及到線程通信,不過這裏我們用到了java中的
* wait()、notify()方法:
* wait():進入臨界區的線程在運行到一部分後,發現進行後面的任務所需的資源還沒有準備充分,
* 所以調用wait()方法,讓線程阻塞,等待資源,同時釋放臨界區的鎖,此時線程的狀態也從RUNNABLE狀態變爲WAITING狀態;
* notify():準備資源的線程在準備好資源後,調用notify()方法通知需要使用資源的線程,
* 同時釋放臨界區的鎖,將臨界區的鎖交給使用資源的線程。
* wait()、notify()這兩個方法,都必須要在臨界區中調用,即是在synchronized同步塊中調用,
* 不然會拋出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();
}
}