Android 多線程編程初探
Android 中的多線程其實就是 Java SE 中的多線程,只是為了方便使用,android 封裝了一些類,如 AsyncTask、HandlerThread 等,在日常的開發過程中,我們往往需要去執行一些耗時的操作,例如發起網絡請求,考慮到網速等其他外在的因素,服務器可能不會立刻響應我們的請求,如果不將這條操作放到子線程中去執行,就會造成主線程被阻塞,今天我們就從多線程的基礎來一起探討
一、線程的基本用法
對於 Andorid 多線程來說我們最新接觸到的就是 Thread 和 Runnable 通常我們如下來啟動一個新的線程
1)繼承自 Thread 來創建子線程
定義一個線程只需要新建一個類繼承自 Thread,然後重寫父類的 run 方法即可
[java] view plain copy
- /**
- * 繼承 Thread 創建子線程
- */
- class MyThread extends Thread {
- @Override
- public void run() {
- //處理具體的邏輯
- }
- }
那麽如何啟動這個線程呢?只需要 new 出 MyThread 的實例,然後調用它的 start() 方法,這樣 run() 方法中的代碼就會運行在子線程,如下:
[java] view plain copy
- new MyThread().start();
2)實現 Runnable 接口來創建子線程
繼承方式耦合性高,更多時候我們會選擇實現 Runnable 接口的方式來實現一個子線程,如下:
[java] view plain copy
- /**
- * 實現 Runnable 接口創建子線程
- */
- class MyThread implements Runnable {
- @Override
- public void run() {
- //處理具體的邏輯
- }
- }
如果使用這種寫法,啟動線程的方法也需要相應的改變,如下:
[java]
- MyThread myThread = new MyThread();
- new Thread(myThread).start();
Thread 構造函數接受一個 Runnable 參數,我們 new 出的 MyThread 正是一個實現了 Runnable 接口的對象,所以可以直接將它傳入到 Thread 的構造函數裏,接著就和上面的一樣了,調用 Thread 的 start() 方法,run() 方法中的代碼就會在子線程當中運行了
3)直接使用匿名類來創建子線程
[java] view plain copy
- new Thread(new Runnable() {
- @Override
- public void run() {
- //處理具體的邏輯
- }
- }).start();
如果你不想再定義一個類去實現 Runnable 接口,也可以使用如上匿名類的方式來實現,這種實現方式也是我們最常用到的
以上這些就是我們來創建子線程時使用的不方式,基本和 Java 中一樣
4)Thread 和 Runnable 的區別
實際上 Thread 也是一個 Runnable,它實現了 Runnable 接口,在Thread類中有一個 Runnable 類型的 target字段,代表要被執行在這個子線程中的任務,代碼如下:
Thread 部分源碼:
[java] view plain copy
- public class Thread implements Runnable {
- /* What will be run. */
- private Runnable target;
- /* The group of this thread */
- private ThreadGroup group;
- private String name;
- /*
- * The requested stack size for this thread, or 0 if the creator did
- * not specify a stack size. It is up to the VM to do whatever it
- * likes with this number; some VMs will ignore it.
- */
- private long stackSize;
- /**
- * 初始化 Thread 並且將Thread 添加到 ThreadGroup 中
- *
- * @param g
- * @param target
- * @param name
- * @param stackSize
- */
- private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
- java.lang.Thread parent = currentThread();
- //group 參數為空,則獲取當前線程的才線程組
- if (g == null) {
- g = parent.getThreadGroup();
- }
- this.group = g;
- this.name = name;
- //設置 target
- this.target = target;
- /* Stash the specified stack size in case the VM cares */
- this.stackSize = stackSize;
- }
- public Thread() {
- init(null, null, null, 0);
- }
- public Thread(Runnable target) {
- init(null, target, null, 0);
- }
- public Thread(ThreadGroup group, Runnable target) {
- init(group, target, null, 0);
- }
- public Thread(Runnable target, String name) {
- init(null, target, name, 0);
- }
- public Thread(ThreadGroup group, Runnable target, String name) {
- init(group, target, name, 0);
- }
- public Thread(ThreadGroup group, Runnable target, String name,
- long stackSize) {
- init(group, target, name, stackSize);
- }
- /**
- * 啟動一個新的線程,如果target 不為空則執行 target 的 run 函數
- * 否者執行當前對象的run()方法
- */
- public synchronized void start() {
- //調用 nativeCreate 啟動新線程
- nativeCreate(this, stackSize, daemon);
- started = true;
- }
- @Override
- public void run() {
- if (target != null) {
- target.run();
- }
- }
- }
上面是 Thread 的部分源碼,我們看到其實 Thread 也實現了 Runnable 接口,最終被線程執行的是 Runnable,而非 Thread,Thread 只是對 Runnable 的包裝,並且通過一些狀態對 Thread 進行管理與調度,Runnable 定義了可執行的任務它只有一個無返回值的 run() 函數,如下:
[java] view plain copy
- public interface Runnable {
- /**
- * When an object implementing interface <code>Runnable</code> is used
- * to create a thread, starting the thread causes the object‘s
- * <code>run</code> method to be called in that separately executing
- * thread.
- * <p>
- * The general contract of the method <code>run</code> is that it may
- * take any action whatsoever.
- *
- * @see java.lang.Thread#run()
- */
- public abstract void run();
- }
當啟動一個線程時,如果 Thread 的 Target 不為空時,則會在子線程中執行這個 target 的 run() 函數,否則虛擬機就會執行該線程自身的 run() 函數
二、線程的 wait、sleep、join、yield
Thread 的基本用法相對來說比較簡單,通常就是復寫 run() 函數,然後調用線程的 start() 方法啟動線程,接下來我們來看看 wait、sleep、join、yield 的區別
1)wait
當一個線程執行到 wait() 方法時,它就進入到一個和該對象相關的等待池中,同時釋放對象的機鎖,使得其它線程可以訪問,用戶可以使用 notify、nitifyAll 或者指定睡眠時間來喚醒當前等待池中的線程,註意:wait()、notify()、notifyAll() 必須放在 Synchronized 塊中,否則會拋出異常
2)sleep
該函數是 Thread 的靜態函數,作用是使調用線程進入睡眠狀態,因為 sleep() 是 Thread 類的靜態方法,因此它不能改變對象鎖,所以當在一個 Synchronized 塊中調用 sleep() 方法時,線程雖然休眠了,但是對象的鎖並沒有被釋放,其它線程無法訪問這個對象,即使睡眠也持有對象的鎖
3)join
等待目標線程執行完之後再繼續執行
4)yield
線程禮讓,目標線程由運行狀態轉換為就緒狀態,也就是讓出執行權限,讓其它線程可以優先執行,但其他線程能否優先執行未知
三、線程池
當我們需要頻繁的創建多個線程進行耗時操作時,每次都經過 new Thread 實現並不是一種好的方式,每次 new Thread 新建和銷毀對象的性能較差,線程缺乏統一管理,可能無限制新建線程,相互之間競爭,可能占用過多系統資源導致死鎖,並且缺乏定時執行,定期執行,線程中斷等功能,Java 提供了 4 種線程池,它能夠有效的管理,調度線程,避免過多的浪費系統資源,它的優點如下:
1)重用存在的線程,減少對象創建,銷毀的開銷
2)可有效控制最大並發線程數,提高系統資源的使用率,同時避免過多的資源競爭,避免堵塞
3)提供定時執行、定期執行、單線程、並發數控制等功能
簡單來說,線程池原理簡單的解釋就是會創建多個線程並進行管理,提交給線程的任務會被線程池指派給其中的線程進行執行,通過線程池的統一調度、管理使得多線程使用更加簡單、高效,如下圖:
線程池都實現了 ExecutorService 接口,改接口定義了線程池需要實現的接口,它的實現有 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor,ThreadPoolExecutor 也就是我們運用最多的線程池實現,ScheduledThreadPoolExecutor 用於周期性執行任務,通常我們都不會直接通過 new 的形式來創建線程池,由於創建參數過程相對復雜一些,因此 JDK 給我們提供了一個 Executor 工廠類來簡化這個過程
線程池的使用準則:
1)不要對那些同步等待的其他任務結果的任務排隊,這可能會導致死鎖,在死鎖中所有所有線程都被一些任務所占用,這些任務依次等待排隊任務的結果,而這些任務又無法執行,因為所有的線程處於忙碌狀態
2)理解任務,要有效的調整線程池的大小,你需要理解正在排隊的任務以及它們正在做什麽
3)調整線程池的大小基本上就是避免兩類錯誤,線程太少或線程太多,幸運的是對於大多數應用程序來說,太多和太少之間的余地相當寬
今天介紹的基本都是一些理論的東西,有的看起來可能沒勁,大家就當了解一下吧,又到周五了,祝大家周末愉快
參考:郭神第一行代碼,Android 進階
Android 多線程編程初探