Android IPC機制(二)用Messenger進行進程間通信

本文首發于微信公眾號「后廠技術官」

查看更多

分享到 評論

Android IPC機制(一)開啟多進程

本文首發于微信公眾號「后廠技術官」

查看更多

分享到 評論

Android Design Support Library(二)用NavigationView實現抽屜菜單界面

本文首發于微信公眾號「后廠技術官」

查看更多

分享到 評論

Android Design Support Library(一)用TabLayout實現類似網易選項卡動態滑動效果

本文首發于微信公眾號「后廠技術官」

查看更多

分享到 評論

Java并發編程(二)同步

如果你的java基礎較弱,或者不大了解java多線程請先看這篇文章java多線程(一)線程定義、狀態和屬性

同步一直是java多線程的難點,在我們做android開發時也很少應用,但這并不是我們不熟悉同步的理由。希望這篇文章能使更多的人能夠了解并且應用java的同步。
在多線程的應用中,兩個或者兩個以上的線程需要共享對同一個數據的存取。如果兩個線程存取相同的對象,并且每一個線程都調用了修改該對象的方法,這種情況通常成為競爭條件。
競爭條件最容易理解的例子就是:比如火車賣票,火車票是一定的,但賣火車票的窗口到處都有,每個窗口就相當于一個線程,這么多的線程共用所有的火車票這個資源。并且無法保證其原子性,如果在一個時間點上,兩個線程同時使用這個資源,那他們取出的火車票是一樣的(座位號一樣),這樣就會給乘客造成麻煩。解決方法為,當一個線程要使用火車票這個資源時,我們就交給它一把鎖,等它把事情做完后在把鎖給另一個要用這個資源的線程。這樣就不會出現上述情況。

1. 鎖對象
synchronized關鍵字自動提供了鎖以及相關的條件,大多數需要顯式鎖的情況使用synchronized非常的方便,但是等我們了解ReentrantLock類和條件對象時,我們能更好的理解synchronized關鍵字。ReentrantLock是JAVA SE 5.0引入的, 用ReentrantLock保護代碼塊的結構如下:

mLock.lock();
try{
...
}
finally{
mLock.unlock();
}

這一結構確保任何時刻只有一個線程進入臨界區,一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句。當其他線程調用lock時,它們則被阻塞直到第一個線程釋放鎖對象。把解鎖的操作放在finally中是十分必要的,如果在臨界區發生了異常,鎖是必須要釋放的,否則其他線程將會永遠阻塞。

2. 條件對象
進入臨界區時,卻發現在某一個條件滿足之后,它才能執行。要使用一個條件對象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程,條件對象又稱作條件變量。
我們來看看下面的例子來看看為何需要條件對象

假設一個場景我們需要用銀行轉賬,我們首先寫了銀行的類,它的構造函數需要傳入賬戶數量和賬戶金額

public class Bank {
private double[] accounts;
    private Lock bankLock;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    }

接下來我們要提款,寫一個提款的方法,from是轉賬方,to是接收方,amount轉賬金額,結果我們發現轉賬方余額不足,如果有其他線程給這個轉賬方再存足夠的錢就可以轉賬成功了,但是這個線程已經獲取了鎖,它具有排他性,別的線程也無法獲取鎖來進行存款操作,這就是我們需要引入條件對象的原因。

public void transfer(int from,int to,int amount){
     bankLock.lock();
     try{
         while (accounts[from]<amount){
             //wait
         }
     }finally {
         bankLock.unlock();
     }
 }

一個鎖對象擁有多個相關的條件對象,可以用newCondition方法獲得一個條件對象,我們得到條件對象后調用await方法,當前線程就被阻塞了并放棄了鎖

public class Bank {
private double[] accounts;
    private Lock bankLock;
    private Condition condition;
    public Bank(int n,double initialBalance){
        accounts=new double[n];
        bankLock=new ReentrantLock();
        //得到條件對象
        condition=bankLock.newCondition();
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount) throws InterruptedException {
        bankLock.lock();
        try{
            while (accounts[from]<amount){
                //阻塞當前線程,并放棄鎖
                condition.await();
            }
        }finally {
            bankLock.unlock();
        }
    }
}

等待獲得鎖的線程和調用await方法的線程本質上是不同的,一旦一個線程調用的await方法,他就會進入該條件的等待集。當鎖可用時,該線程不能馬上解鎖,相反他處于阻塞狀態,直到另一個線程調用了同一個條件上的signalAll方法時為止。當另一個線程準備轉賬給我們此前的轉賬方時,只要調用condition.signalAll();該調用會重新激活因為這一條件而等待的所有線程。
當一個線程調用了await方法他沒法重新激活自身,并寄希望于其他線程來調用signalAll方法來激活自身,如果沒有其他線程來激活等待的線程,那么就會產生死鎖現象,如果所有的其他線程都被阻塞,最后一個活動線程在解除其他線程阻塞狀態前調用await,那么它也被阻塞,就沒有任何線程可以解除其他線程的阻塞,程序就被掛起了。
那何時調用signalAll呢?正常來說應該是有利于等待線程的方向改變時來調用signalAll。在這個例子里就是,當一個賬戶余額發生變化時,等待的線程應該有機會檢查余額。

public void transfer(int from,int to,int amount) throws InterruptedException {
       bankLock.lock();
       try{
           while (accounts[from]<amount){
               //阻塞當前線程,并放棄鎖
               condition.await();
           }
           //轉賬的操作
           ...
           condition.signalAll();
       }finally {
           bankLock.unlock();
       }
   }

當調用signalAll方法時并不是立即激活一個等待線程,它僅僅解除了等待線程的阻塞,以便這些線程能夠在當前線程退出同步方法后,通過競爭實現對對象的訪問。還有一個方法是signal,它則是隨機解除某個線程的阻塞,如果該線程仍然不能運行,那么則再次被阻塞,如果沒有其他線程再次調用signal,那么系統就死鎖了。

3. Synchronized關鍵字
Lock和Condition接口為程序設計人員提供了高度的鎖定控制,然而大多數情況下,并不需要那樣的控制,并且可以使用一種嵌入到java語言內部的機制。從Java1.0版開始,Java中的每一個對象都有一個內部鎖。如果一個方法用synchronized關鍵字聲明,那么對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須獲得內部的對象鎖。
換句話說,

public synchronized void method(){

}

等價于

public void method(){
this.lock.lock();
try{

}finally{
this.lock.unlock();
}

上面銀行的例子,我們可以將Bank類的transfer方法聲明為synchronized,而不是使用一個顯示的鎖。
內部對象鎖只有一個相關條件,wait方法添加到一個線程到等待集中,notifyAll或者notify方法解除等待線程的阻塞狀態。也就是說wait相當于調用condition.await(),notifyAll等價于condition.signalAll();

我們上面的例子transfer方法也可以這樣寫:

public synchronized void transfer(int from,int to,int amount)throws InterruptedException{
    while (accounts[from]<amount) {
        wait();
    }
    //轉賬的操作
    ...
    notifyAll();   
    }

可以看到使用synchronized關鍵字來編寫代碼要簡潔很多,當然要理解這一代碼,你必須要了解每一個對象有一個內部鎖,并且該鎖有一個內部條件。由鎖來管理那些試圖進入synchronized方法的線程,由條件來管理那些調用wait的線程。

4. 同步阻塞
上面我們說過,每一個Java對象都有一個鎖,線程可以調用同步方法來獲得鎖,還有另一種機制可以獲得鎖,通過進入一個同步阻塞,當線程進入如下形式的阻塞:

synchronized(obj){

}

于是他獲得了obj的鎖。再來看看Bank類

public class Bank {
private double[] accounts;
private Object lock=new Object();
   public Bank(int n,double initialBalance){
        accounts=new double[n];
        for (int i=0;i<accounts.length;i++){
            accounts[i]=initialBalance;
        }
    }
    public void transfer(int from,int to,int amount){
        synchronized(lock){
          //轉賬的操作
            ...
        }
    }
}

在此,lock對象創建僅僅是用來使用每個Java對象持有的鎖。有時開發人員使用一個對象的鎖來實現額外的原子操作,稱為客戶端鎖定。例如Vector類,它的方法是同步的。現在假設在Vector中存儲銀行余額

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
}

Vecror類的get和set方法是同步的,但是這并未對我們有所幫助。在第一次對get調用完成以后,一個線程完全可能在transfer方法中被被剝奪運行權,于是另一個線程可能在相同的存儲位置存入了不同的值,但是,我們可以截獲這個鎖

  public void transfer(Vector<Double>accounts,int from,int to,int amount){
  synchronized(accounts){
  accounts.set(from,accounts.get(from)-amount);
  accounts.set(to,accounts.get(to)+amount;
  }
}

客戶端鎖定(同步代碼塊)是非常脆弱的,通常不推薦使用,一般實現同步最好用java.util.concurrent包下提供的類,比如阻塞隊列。如果同步方法適合你的程序,那么請盡量的使用同步方法,他可以減少編寫代碼的數量,減少出錯的幾率,如果特別需要使用Lock/Condition結構提供的獨有特性時,才使用Lock/Condition。

分享到 評論

Android5.x Toolbar和Palette應用解析

本文首發于微信公眾號「后廠村碼農」

查看更多

分享到 評論

Android5.x Notification應用解析

本文首發于微信公眾號「后廠村碼農」

查看更多

分享到 評論

Android5.x CardView 應用解析

本文首發于微信公眾號「后廠村碼農」

查看更多

分享到 評論

Android5.x RecyclerView 應用解析

本文首發于微信公眾號「后廠村碼農」

查看更多

分享到 評論

Java并發編程(三)volatile域

前言

有時僅僅為了讀寫一個或者兩個實例域就使用同步的話,顯得開銷過大,volatile關鍵字為實例域的同步訪問提供了免鎖的機制。如果聲明一個域為volatile,那么編譯器和虛擬機就知道該域是可能被另一個線程并發更新的。再講到volatile關鍵字之前我們需要了解一下內存模型的相關概念以及并發編程中的三個特性:原子性,可見性和有序性。

查看更多

分享到 評論

Java并發編程(一)線程定義、狀態和屬性

一 、線程和進程

1. 什么是線程和進程的區別:
線程是指程序在執行過程中,能夠執行程序代碼的一個執行單元。在java語言中,線程有四種狀態:運行 、就緒、掛起和結束。
進程是指一段正在執行的程序。而線程有時也被成為輕量級的進程,他是程序執行的最小單元,一個進程可以擁有多個線程,各個線程之間共享程序的內功空間(代碼段、數據段和堆空間)及一些進程級的資源(例如打開的文件),但是各個線程都擁有自己的棧空間。
2. 為何要使用多進程
在操作系統級別上來看主要有以下幾個方面:

  • 使用多線程可以減少程序的響應時間,如果某個操作和耗時,或者陷入長時間的等待,此時程序講不會響應鼠標和鍵盤等的操作,使用多線程后可以把這個耗時的線程分配到一個單獨的線程去執行,從而使程序具備了更好的交互性。
  • 與進程相比,線程創建和切換開銷更小,同時多線程在數據共享方面效率非常高。
  • 多CPU或者多核計算機本身就具備執行多線程的能力,如果使用單個進程,將無法重復利用計算機資源,造成資源的巨大浪費。在多CPU計算機使用多線程能提高CPU的利用率。
  • 使用多線程能簡化程序的結構,使程序便于理解和維護

二、創建線程
多線程的實現一般有以下三種方法其中前兩種為最常用的方法:
1. 繼承Thread類,重寫run()方法
Thread本質上也是實現了Runnable接口的一個實例。需要注意的是調用start()方法后并不是是立即的執行多線程的代碼,而是使該線程變為可運行態,什么時候運行多線程代碼是由操作系統決定的。
以下是主要步驟:
(1)定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執行體。
(2)創建Thread子類的實例,即創建了線程對象。
(3)調用線程對象的start()方法來啟動該線程。

public class TestThread extends Thread{ 
    public void run() {
            System.out.println("Hello World");
        }  
    public static void main(String[] args) {
        Thread mThread = new TestThread();
        mThread.start(); 
    } 
}

2. 實現Runnable接口,并實現該接口的run()方法
以下是主要步驟:
(1)自定義類并實現Runnable接口,實現run()方法。
(2)創建Thread子類的實例,用實現Runnable接口的對象作為參數實例化該Thread對象。
(3)調用Thread的start()方法來啟動該線程。

public class TestRunnable implements Runnable {
    public void run() { 
            System.out.println("Hello World");
        } 
}

public class TestRunnable {
    public static void main(String[] args) {
        TestRunnable mTestRunnable = new TestRunnable();      
        Thread mThread = new Thread(mTestRunnable);
        mThread.start(); 
    } 
}

3. 實現Callable接口,重寫call()方法
Callable接口實際是屬于Executor框架中的功能類,Callable接口與Runnable接口的功能類似,但提供了比Runnable更強大的功能,主要表現為以下的3點:
(1)Callable可以在任務接受后提供一個返回值,Runnable無法提供這個功能。
(2)Callable中的call()方法可以拋出異常,而Runnable的run()方法不能拋出異常。
(3)運行Callable可以拿到一個Future對象,Future對象表示異步計算的結果,他提供了檢查計算是否完成的方法。由于線程屬于異步計算模型,因此無法從別的線程中得到函數的返回值,在這種情況下就可以使用Future來監視目標線程調用call()方法的情況,但調用Future的get()方法以獲取結果時,當前線程就會阻塞,直到call()方法的返回結果。

public class TestCallable {  
    //創建線程類
    public static class MyTestCallable  implements Callable {  
        public String call() throws Exception {  
             retun "Hello World";
            }  
        }  
public static void main(String[] args) {  
        MyTestCallable mMyTestCallable= new MyTestCallable();  
        ExecutorService mExecutorService = Executors.newSingleThreadPool();  
        Future mfuture = mExecutorService.submit(mMyTestCallable);  
        try { 
        //等待線程結束,并返回結果
            System.out.println(mfuture.get());  
        } catch (Exception e) {  
           e.printStackTrace();
        } 
    }  
}

上述程序的輸出結果為:Hello World

在這三種方式中,一般推薦實現Runnable接口的方式,其原因是:首先,Thread類定義了多種方法可以被派生類使用重寫,但是只有run()方法是必須被重寫的,實現這個線程的主要功能,這也是實現Runnable接口需要的方法。其次,一個類應該在他們需要加強或者修改時才會被繼承。因此如果沒有必要重寫Thread類的其他方法,那么在這種情況下最好是用實現Runnable接口的方式。

三、中斷線程
當線程的run()方法執行方法體中的最后一條語句后,并經由執行return語句返回時,或者出現在方法中沒有捕獲的異常時線程將終止。在java早期版本中有一個stop方法,其他線程可以調用它終止線程,但是這個方法現在已經被棄用了。
interrupt方法可以用來請求終止線程,當一個線程調用interrupt方法時,線程的中斷狀態將被置位。這是每個線程都具有的boolean標志,每個線程都應該不時的檢查這個標志,來判斷線程是否被中斷。
要想弄清線程是否被置位,可以調用Thread.currentThread().isInterrupted():

while(!Thread.currentThread().isInterrupted()){
do something
}

但是如果一個線程被阻塞,就無法檢測中斷狀態。這是產生InterruptedException的地方。當一個被阻塞的線程(調用sleep或者wait)上調用interrupt方法。阻塞調用將會被InterruptedException中斷。
如果每次迭代之后都調用sleep方法(或者其他可中斷的方法),isInterrupted檢測就沒必要也沒用處了,如果在中斷狀態被置位時調用sleep方法,它不會休眠反而會清除這一狀態并拋出InterruptedException。所以如果在循環中調用sleep,不要去檢測中斷狀態,只需捕獲InterruptedException。
在很多發布的代碼中會發現InterruptedException被抑制在很低的層次上:

void myTask(){
...
try{
sleep(50)
}catch(InterruptedException e){
...
}
}

不要這樣做,如果不認為catch中做一處理有什么好處的話,有兩種合理的選擇:

  • 在catch中調用Thread.currentThread().interrup()來設置中斷狀態。調用者可以對其進行檢測
  • 更好的選擇用throw InterruptedException標記你的方法,不采用try語句塊來捕獲已成。這樣調用者可以捕獲這個異常:
void myTask()throw InterruptedException{
sleep(50)
}

四、線程的狀態

(1). 新建狀態(New):新創建了一個線程對象。
(2). 就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位于可運行線程池中,變得可運行,等待獲取CPU的使用權。
(3). 運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
(4). 阻塞狀態(Blocked):阻塞狀態是線程因為某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分三種:

  • 等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
  • 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池中。
  • 其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。
    (5). 死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命周期。

    這里寫圖片描述

五、線程的優先級和守護線程

1. 線程優先級
在java中,每一個線程有一個優先級,默認情況下,一個線程繼承它父類的優先級。可以用setPriority方法提高或降低任何一個線程優先級。可以將優先級設置在MIN_PRIORITY(在Thread類定義為1)與MAX_PRIORITY(在Thread類定義為10)之間的任何值。線程的默認優先級為NORM_PRIORITY(在Thread類定義為5)。
盡量不要依賴優先級,如果確實要用,應該避免初學者常犯的一個錯誤。如果有幾個高優先級的線程沒有進入非活動狀態,低優先級線程可能永遠也不能執行。每當調度器決定運行一個新線程時,首先會在具有搞優先級的線程中進行選擇,盡管這樣會使低優先級的線程完全餓死。

2. 守護線程

調用setDaemon(true);將線程轉換為守護線程。守護線程唯一的用途就是為其他線程提供服務。計時線程就是一個例子,他定時發送信號給其他線程或者清空過時的告訴緩存項的線程。當只剩下守護線程時,虛擬機就退出了,由于如果只剩下守護線程,就沒必要繼續運行程序了。
另外JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在做數據庫應用時候,使用的數據庫連接池,連接池本身也包含著很多后臺線程,監控連接個數、超時時間、狀態等等。

分享到 評論
广东11选5高手群