Android中的程序和線程
- Android中的一個應用程式一般就對應着一個程序,多程序的情況可以參考 Android 多程序通信之幾個基本問題
- Android中更常見的是多線程的情況,一個應用程式中一般都有包括UI線程等多個線程。Android中規定網絡通路必須在子線程中進行,而操作更新UI則隻能在UI線程。
- 常見的網絡請求庫,如OkHttp、Volly等都為我們封裝好了線程池,是以我們在進行網絡請求時一般不是很能直覺地感受到建立線程以及切換線程的過程。
- 線程是一種很寶貴的資源,要避免頻繁建立銷毀線程,一般推薦用線程池來管理線程。
線程的狀态
線程可能存在6種不同的狀态:新建立(New)、可運作(Runnable)、阻塞狀态(Blocked)、等待狀态(Waiting)、限期等待(Timed Waiting)、終止狀态(Terminated)
- 新建立(New):建立後但還未啟動的線程(還沒有調用start方法)處于這種狀态
- 可運作(Runnable):一旦調用了start方法,線程就處于這種狀态。需要注意的是此時線程可能正在執行,也可能在等待CPU配置設定執行的時間
- 阻塞狀态(Blocked):表示線程被鎖阻塞,等待擷取到一個排他鎖。在程式等待進入同步區域時,線程将進入這種狀态
- 等待狀态(Waiting):處于這種狀态的線程不會被配置設定CPU執行時間,它們要等待被其他線程顯示地喚醒。調用以下方法會讓線程進入這種狀态:
- 沒有設定Timeout參數的Object.wait()方法
- 沒有設定Timeout參數的Thread.join()方法
- 限期等待(Timed Waiting):與等待狀态(Waiting)不同的是,處于這種狀态的線程不需要等待其它線程喚醒,在一定時間之後會由系統喚醒。調用以下方法會讓線程進入這種狀态:
- Thread.sleep()方法
- 設定了Timeout參數的Object.wait()方法
- 設定了Timeout參數的Thread.join()方法
- 終止狀态(Terminated):表示線程已經執行完畢。導緻線程終止有2種情況:
- 線程的run方法執行完畢,正常退出
- 因為一個沒有捕獲的異常而終止了run方法
建立線程
建立線程一般有如下幾種方式:繼承Thread類;實作Runnable接口;實作Callable接口
- 繼承Thread類,重寫run方法
public class TestThread extends Thread {
@Override
public void run() {
System.out.println("Hello World");
}
public static void main(String[] args) {
Thread mThread = new TestThread();
mThread.start();
}
}
- 實作Runnable接口,并實作run方法
public class TestRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello World");
}
public static void main(String[] args) {
TestRunnable mTestRunnable = new TestRunnable();
Thread mThread = new Thread(mTestRunnable);
mThread.start();
}
}
- 實作Callable接口,重寫call方法
- Callable可以在任務接受後提供一個傳回值而Runnable不行
- Callable的call方法可以抛出異常,Runnable的run方法不行
- 運作Callable可以拿到一個Future對象,表示計算的結果,通過Future的get方法可以拿到異步計算的結果,不過目前線程會阻塞。
public class TestCallable {
public static class MyTestCallable implements Callable<String> {
@Override
public String call() throws Exception {
//call方法可以提供傳回值,而Runnable不行
return "Hello World";
}
}
public static void main(String[] args) {
MyTestCallable myTestCallable = new MyTestCallable();
//手動建立線程池
ExecutorService executorService = new ThreadPoolExecutor(1,1,0L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10));
//運作callable可以拿到一個Future對象
Future future = executorService.submit(myTestCallable);
try {
//等待線程結束,future.get()方法會使目前線程阻塞
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
- 以上三種方式就是常見的建立線程的方式。推薦使用實作Runnable接口的方法。
線程中斷
- 當一個線程調用interrupt方法時,線程的中斷辨別為将被設定成true
- 通過Thread.currentThread().isInterrupted()方法可以判斷線程是否應該被中斷
- 可以通過調用Thread.interrupted()對中斷标志位進行複位(設定為false)
- 如果一個線程處于阻塞狀态,線程在檢查中斷标志位時如果發現中斷标志位為true,則會在阻塞方法處抛出InterruptedException異常,并且在抛出異常前會将中斷标志位複位,即重新設定為false
- 不要在代碼底層捕獲InterruptedException異常後不做處理
同步的幾種方法
同步的方式一般有如下3種:volatile關鍵字、synchronized關鍵字、重入鎖ReentrantLock
volatile關鍵字
- volatile關鍵字實作多線程安全關鍵在于它的可見性特性,但它需要滿足一些條件才能保證線程安全,具體可以檢視文章 深入了解Java虛拟機(八)之Java記憶體模型
- 在用volatile關鍵字來實作多線程安全時需要注意volatile不保證原子性,也就是不能用于一些自增、自減等操作,也不能用于一些不變式中,自增、自減比較好了解,下面看看不變式的情況
public class VolatileTest {
private volatile int lower,upper;
public int getLower() {
return lower;
}
public void setLower(int value) {
if (value > upper) {
throw new IllegalArgumentException();
}
this.lower = value;
}
public int getUpper() {
return upper;
}
public void setUpper(int value) {
if (value < lower) {
throw new IllegalArgumentException();
}
this.upper = value;
}
}
- 上面的例子中,如果初始值是(0,5),線程A調用setLower(4),線程B調用setUpper(3),顯然最後結果就會變成(4,3)了
- volatile使用的場景常見的有作為狀态标志以及DCL單例模式
synchronized關鍵字和重入鎖ReentrantLock
- synchronized關鍵字比較常見,可以用于同步方法也可以用于同步代碼塊,一般推薦用同步方法,同步代碼塊的安全性不高。
- 重入鎖ReentrantLock相比synchronized提供了一些獨有的特性:可以綁定多個解鎖的條件Condition、可以實作公平鎖、可以設定放棄等待擷取鎖的時間。
public class ReentrantLockTest {
private Lock mLock = new ReentrantLock();
//true,表示實作公平鎖
<!--private Lock mLock = new ReentrantLock(true);-->
private Condition condition;
private void thread1() throws InterruptedException{
mLock.lock();
try {
condition = mLock.newCondition();
condition.await();
System.out.println("thread1:Hello World");
}finally {
mLock.unlock();
}
}
private void thread2() throws InterruptedException{
mLock.lock();
try {
System.out.println("thread2:Hello World");
condition.signalAll();
}finally {
mLock.unlock();
}
}
}
- 一個ReentrantLock有多個相關的Condition,調用Condition的await方法會讓目前線程進入該條件的等待集并阻塞,直到另一個線程調用了同一個條件的signalAll方法激活因為這個條件而進入阻塞的所有線程
- 一般線程同步用得比較多的還是synchronized同步方法和一些java.util.concurrent包提供的一些類
如何安全的終止線程
雖然我們一般都是利用線程池來管理線程而不會直接顯示地建立線程,但是作為線程相關知識的一部分,我們還是要了解如何安全地終止一個線程。
要安全地終止一個線程,一般有2種方法:中斷和标志位
(1)利用中斷來終止線程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
//do something
}
}
});
//當我們調用Thread的interrupt方法時,線程就會退出循環停止了。
thread.interrupt();
(2)通過标志位
private static class MyRunnable implements Runnable {
//控制線程的标志位,需要用 volatile關鍵字
private volatile boolean on = true;
@Override
public void run() {
while (on) {
//do something
}
}
public void cancel() {
on = false;
}
}
//啟動線程
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
//終止線程
myRunnable.cancel();
歡迎關注我的微信公衆号,期待與你一起學習,一起交流,一起成長!