在Android常常會用到多線程,雖然多線程提升了性能,但也帶來了一些複雜性:java
1.須要使用Java中處理併發編程模型編程
2.須要確保在多線程環境中的數據一致性(同步)安全
3.須要設置任務的執行策略多線程
線程基本概念:
其實軟件運行的本質是指示硬件去作一些操做(包括展現一張圖片,存儲數據等),這些指令有代碼實現,由CPU按順序執行,線程其實就是這些指令的高級定義。從應用的角度來看,一個線程是沿着Java的代碼路徑順序執行的,在一個線程上按順序執行的代碼路徑被稱爲一個任務,一個線程能夠順序執行一個或者多個任務。併發
線程的執行:app
在Android中線程由java.lang.Thread來表明,當Thread調用start時,開始運行任務,當任務被執行完畢而且沒有更多任務時,thread中止。thread的存活時間取決於任務的長短。Thread支持實現了java.lang.Runnable 接口的任務,最簡單的例子:ide
private class MyTask implements Runnable { public void run() { int i = 0;//變量保存在線程本地堆棧上 } } Thread myThread = new Thread(new MyTask()); myThread.start();
從操做系統層面看,一個線程擁有一個指令指針和一個棧指針,指令指針每次都會指向下一條要執行的指令,而棧指針指向一塊線程私有的內存區域(不能被其餘線程讀取),用來存儲線程本地數據。Cpu每次只是執行一條指令,可是系統一般有不少進程或者線程須要同時執行,好比adnroid同時運行多個app.若是按順序執行每個任務,那排到最後的任務也太慘了,用戶須要等待好久。爲了讓用戶以爲應用是同時在運行的,cpu就得在多個線程中分享運行時間(一個cpu實質上每次仍是運行一個線程,但因爲線程切換的時間很短,因此用戶感受不出來)。這就須要有個調度策略決定哪一個線程要立刻被運行以及運行多長時間,調度策略能夠有不少種,但一般使用線程優先級來調度。高優先級線程會優先於低優先級線程被調用,並被賦予更多的運行時間.在java中線程的優先級從1(最低優先級)到10(最高優先級),若是不設置的話,默認爲5:
myThread.setPriority(8)
可是僅僅根據優先級來調度的話,那麼低優先級的線程可能沒法得到足夠的運行時間(飢餓線程),所以,調度策略還會考慮每一個線程的處理時間,以此來更改運行的線程,線程的更改就是所謂的上下文切換,上下文切換會記錄當前線程暫停運行時的數據和狀態,以便下一次切換回來時恢復原先的狀態。兩個同時運行的線程 運行在一個處理器中,被分割成執行間隔的例子,以下所示:性能
Thread T1 = new Thread(new MyTask()); T1.start(); Thread T2 = new Thread(new MyTask()); T2.start();
每一個調度的點都消耗一些時間,用於cpu計算線程切換,在圖中這個時間表示爲C這個時間段this
多線程應用:
對於多線程來講,由於應用代碼能夠分割成多個操做步驟,因此看起來就像是並行運行同樣。若是執行線程的數量超過處理器的數量,那其實還不算真正的並行,只是經過上下文切換實現線程的分割運行,實際上每條指令仍是順序運行的。多線程雖然提升了運行效率,也帶來了一些代價,包括增長了複雜度、增長了內存佔用、運行順序的不肯定性,這些都須要應用程序去管理。spa
資源佔用:
線程在處理器和內存方面會帶來開銷。每一個線程都會分配一個私有內存區域,主要用來存儲該線程的本地變量和方法運行時的參數。只要線程是存活的,它就會佔據必定的系統資源,即便它此時處於空閒狀態或者阻塞狀態。而處理器佔用是指,在每次的上文切換中,處理器須要計算、存儲和恢復線程狀態,越多的上下文切換,對性能的影響也就越大。
增長複雜度:
對於單線程來講,因爲代碼的執行是有序的,因此咱們分析代碼行爲時是很容易的。可是一旦到多線程,由於線程的執行順序和執行時間是不肯定的,咱們在處理的時候是很容易出錯的,而且一旦出錯,調試起來也很麻煩。
數據的不一樣步:
多線程執行順序的不肯定,致使對數據訪問順序的不肯定性,若是一個變量被2個以上的線程共享,每一個線程均可以改變它的值,那麼這個最終的值是很差把握的。舉個例子:線程t1和t2都可以修改變量sharedResource,訪問的順序是不肯定的,它可能先被加或先被減。
public class RaceCondition { int sharedResource = 0; public void startTwoThreads() { Thread t1 = new Thread(new Runnable() { @Override public void run() { sharedResource++; } }); t1.start(); Thread t2 = new Thread(new Runnable() { @Override public void run() { sharedResource--; } }); t2.start(); } }
sharedResource暴露了一個競爭條件,它的結果會隨着線程執行順序的不一樣而不一樣,咱們無法保證t1和t2哪一個會先改變sharedResource的值。在這個例子中,更爲細緻的順序是二進制指令的順序,修改一塊內存區域的值時包括讀、修改、寫入三個操做,而這三個操做都不是原子操做。線程的上下文切換可能在這三個指令之間發生,這樣sharedResource的最終結果就取決於兩個線程的6個指令操做的順序,結果可能爲0,-1或者1.第一個結果發生在第一個線程在第二個線程讀取sharedResource以前寫入。後兩個結果發生在最早讀取的都是初始化值0,最後的寫入操做決定了最後的結果。由於有些數據的讀寫不該該被中斷否則可能會出現上述狀況,因此對於這樣的一些數據應該在代碼中提供原子區域代碼塊(原子操做,不被中斷),若是一個線程運行原子區域中的代碼,其餘想要訪問相同代碼塊的線程將會被阻塞,直到沒有線程在運行這個代碼塊。所以,Java中的原子區域是互斥的,由於它只容許訪問一個線程,原子區域的建立有不少方法,最多見的就是使用關鍵字synchronized:
synchronized (this) { sharedResource++; }
線程安全:
讓多個線程共享相同的對象是一個很快捷的線程交互方式,但也引起了上文提到的線程安全的問題。若是一個被多個線程訪問的對象,每次被線程訪問時都是一個正確的狀態,那就是線程安全的,這能夠經過同步來實現,同步能夠保證在一塊代碼塊內當前只能有一個線程執行該代碼塊,這樣的代碼塊稱爲臨界區,而且它只能是原子操做。在java中同步是經過鎖機制來實現的,判斷臨界區代碼是否已經有線程在執行了,若是已經有線程在執行,其餘想要執行該代碼塊的線程將會被阻塞。Android中線程鎖機制包括:
1.對象鎖:synchronized 關鍵字
2.顯式鎖定:java.util.concurrent.locks.ReentrantLock和java.util.concurrent.locks.ReentrantReadWriteLock
對象鎖和java監聽器:
synchronized關鍵字在每一個Java對象中隱含可用的對象鎖上運行,對象鎖是互斥,因此保證了當前只有一個線程在運行關鍵代碼塊,對象鎖相似於一個監聽器,java監聽器能夠有三種狀態建模:
掛起:
線程在等待監聽器被另外一個線程釋放
運行:
惟一的一個線程持有該監聽器,而且正在運行臨界區
等待:
線程在運行完臨界區的所有代碼以前自願放棄對監聽器的持有,讓其餘線程運行臨界區,自身等待被系統喚起從而再次擁有該監聽器
線程在這三種狀態中轉換的效果圖:
當線程運行被對象鎖保護的代碼塊時,根據監聽器的不一樣狀態,線程會處於不一樣的過渡狀態:
1.進入監聽器:一個線程想要訪問被對象鎖保護的代碼塊,它已經進入了監視器中,但若是已經有線程擁有對象鎖,那這個線程會被掛起,等待
2.請求鎖:若是當前沒有線程擁有這個監視器,那麼一個被阻塞的線程會去請求獲取鎖,而且運行代碼塊,但若是有多個線程同時被阻塞,系統會經過調度策略來判斷哪一個線程該得到這個監視器
3.釋放鎖而且等待:線程經過調用 Object.wait() 將本身掛起,一般是由於在運行時有些條件還沒有知足(好比io讀取未結束)
4.在被喚起以後請求鎖:等待的線程一般在其餘線程調用 Object.notify() 或者 Object.notifyAll()後被喚起,並經過系統調度,能夠再次得到監視器。
5.釋放鎖而且離開監視器:當代碼塊執行完畢後,線程離開,釋放鎖和監視器,以便其餘線程能夠獲取。
下面的代碼分別表示以上吳5種狀態在代碼中的位置.
synchronized (this) { // (1) // Execute code (2) wait(); // (3) // Execute code (4) } // (5)
對象鎖的結果不一樣級別:
1方法級別:
synchronized void changeState() { sharedResource++; }
2.代碼塊級別:
void changeState() { synchronized(this) { sharedResource++; } }
3.使用其餘對象的對象鎖的代碼塊級別:
private final Object mLock = new Object(); void changeState() { synchronized(mLock) { sharedResource++; } }
4.對封閉類實例的內部鎖進行操做的方法級別:
synchronized static void changeState() { staticSharedResource++; }
5.在封閉類實例的對象鎖上運行的塊級別:
static void changeState() { synchronized(this.getClass()) { staticSharedResource++; } }
代碼塊級別和方法級別代碼中的this是同一個對象,可是使用代碼塊級別你能夠更加精確地控制臨界區,只關心你實際想要保護的狀態,咱們應該儘量地縮小原子操做的範圍,過大的原子操做範圍會下降應用的性能。
咱們還能夠在一個類中使用其餘對象的對象鎖,一個應用應該儘量地使用一個鎖保護他的每個狀態,所以,若是一個類中有多個獨立的狀態,最好須要多個鎖來提升性能.
使用顯式的鎖機制:
若是須要更加高級的鎖機制,可使用ReentrantLock和 ReentrantReadWriteLock替代synchronized,例子:
int sharedResource; private ReentrantLock mLock = new ReentrantLock(); public void changeState() { mLock.lock(); try { sharedResource++; } finally { mLock.unlock(); } }
synchronized關鍵字和ReentrantLock具備相同的語義:若是另外一個線程已經進入該區域,這方式都會阻塞全部嘗試執行臨界區的線程,這是一種防護性的策略,它們假設全部的併發訪問都是有問題的,可是多線運行多線程同時讀取一個共享變量是沒有害處的。所以,synchronized和ReentrantLock可能過分保護了.ReentrantReadWriteLock 運行多線程併發讀取,可是不容許邊讀邊寫以及同時寫入:
int sharedResource; private ReentrantReadWriteLock mLock = new ReentrantReadWriteLock(); public void changeState() { mLock.writeLock().lock(); try { sharedResource++; } finally { mLock.writeLock().unlock(); } } public int readState() { mLock.readLock().lock(); try { return sharedResource; } finally { mLock.readLock().unlock(); } }
ReentrantReadWriteLock 相對比較複雜,在判斷是否線程是該執行仍是該阻塞上會比synchronized和ReentrantLock花更多的時間,所以在使用上須要有個取捨。一般較好的策略是當多線程有不少讀取操做而且不多的寫入操做時,選擇ReentrantReadWriteLock會比較好。