Android中的執行緒使用與Java有何不同?
目錄
Java中的執行緒
- Java中如何建立執行緒
- Java中的執行緒同步問題(synchronized關鍵字,lock, wait,notify,notifyall)
- Java中保證成員變數訪問的同步和原子操作
- Java中如何終止執行緒
Android中的執行緒
- 執行緒間的通訊Handler,Looper,MessageQueue
- ThreadLocal
- Asynctask引起記憶體洩漏
下面開始正文
Java中的執行緒
提起android中去使用執行緒,我們首先必須搞懂java中執行緒的一些基本概念.
1, Java中如何建立執行緒
在java中建立執行緒的幾種方式
第一種: 直接new Thread
Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.print("執行執行緒"); } }); thread.start();
第二種: 執行緒工廠
ThreadFactory factory = new ThreadFactory() { @Override public Thread newThread(@NonNull Runnable r) { return new Thread(r, "執行緒名字"); } }; Runnable runnable = new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " started!"); } }; Thread thread1 = factory.newThread(runnable); thread1.start();
第三種: 執行緒池
Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Thread with Runnable started!"); } }; Executor executor = Executors.newCachedThreadPool(); executor.execute(runnable);
2,Java中的執行緒同步問題
在使用執行緒過程,難免會遇到執行緒同步的問題,首先必須清楚執行緒不同步是如何產生的,然後再來看看解決方法.
下面看一段示例程式碼,它會出現一個情況就是執行緒不同步.
public static class ThreadTest{ private static int x,y; public static void startThread1(){ Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100_00_00; i++){ x = i; y = i; } if (x != y){ System.out.println("x != y x = "+x+" y = "+y); } } }); thread.start(); } public static void startThread2(){ Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100_00_00; i++){ x = i; y = i; } if (x != y){ System.out.println("x != y x = "+x+" y = "+y); } } }); thread.start(); } }
我們執行startThread1()和startThread2()會輸出
x != y x = 394076 y = 396613
這是因為出現了執行緒的不同步才產生的異常現象, 這是線上程1執行過程中,執行緒2也在執行導致多執行緒操作x,y,從而x y 不相等.
為了避免執行緒的不同步,java中引入了synchronized關鍵字. 下面對需要執行緒同步的程式碼塊加入synchronized關鍵字.
public static class ThreadTest{ private static int x,y; public static void startThread1(){ Thread thread = new Thread(new Runnable() { @Override public void run() { synchronized (ThreadDemoActivity.class){ for (int i = 0; i < 100_00_00; i++){ x = i; y = i; } if (x != y){ System.out.println("x != y x = "+x+" y = "+y); } } } }); thread.start(); } public static void startThread2(){ Thread thread = new Thread(new Runnable() { @Override public void run() { synchronized (ThreadDemoActivity.class){ for (int i = 0; i < 100_00_00; i++){ x = i; y = i; } if (x != y){ System.out.println("x != y x = "+x+" y = "+y); } } } }); thread.start(); } }
這下就不會有前面提到的執行緒不同步的問題了. synchronized關鍵字有幾種不同的使用方法
synchronized關鍵字加到方法前面
public synchronized void demo(){ //... }
synchronized定義程式碼塊
private Object lock = new Object(); public void demo(){ synchronized (lock){ //... } }
synchronized都會去關聯到一個鎖物件,前者加到方法前面的synchronized,它的鎖物件是類的物件,後者則是使用自定義的鎖物件,前者更加方便,但是它只能指定一個類物件作為鎖物件,後者更加靈活自由,可以定義同步程式碼塊,自定義鎖物件(就像剛開始的示例程式碼中使用了ThreadDemoActivity.class作為鎖物件,當然也可以自定義一個物件作為鎖物件)
synchronized除了解決上面提到的執行緒互斥訪問問題,還會會解決另外一個問題資料的同步問題. 當我們在程式碼中有一個成員變數
x = 1
一個執行緒a要對x = 5賦值,它需要分三個步驟,一個是拷貝x成員變數到它執行緒所屬的記憶體區域,二是把x的值賦成5,三是把x = 5放回原有的記憶體區域. 因為這樣做會提高效率。synchronized在解決執行緒間同步問題的時候也順帶解決了這個資料的同步問題。
所以總結一下:
synchronized解決了兩個問題, 問題一資料訪問的互斥,問題二是資料的同步問題
為了解決執行緒間的同步問題,除了使用synchronized關鍵字之外,還可以使用lock來解決,不過它用起來會比較麻煩。通常的程式碼格式是:
private Lock lock = new ReentrantLock(); public void demo(){ lock.lock(); //同步程式碼塊 lock.unlock(); }
用得比較常見的地方的讀寫鎖,在一個執行緒寫的時候不允許別的執行緒寫和讀,但是在一個執行緒讀的情況下,執行別的執行緒讀,但是不能寫。
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); private ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); private int x = 1; public void write(){ writeLock.lock(); x = 5; writeLock.unlock(); } public void print(){ readLock.lock(); System.out.print("x = "+x); readLock.unlock(); }
關於wait, notify和notifyAll的使用.
在程式設計中,我們會有一個執行緒必須滿足一個條件才能繼續執行的需求,而這個條件是另外一個執行緒去達成的,可以看下下面程式碼
private String shareMsg = null; private synchronized void initshareMsg(){ shareMsg = "share msg"; } private synchronized void printshareMsg(){ while (shareMsg != null){ System.out.print(shareMsg); } } public void run(){ Thread thread_1 = new Thread(() -> { initshareMsg(); }); Thread thread_2 = new Thread(() -> { printshareMsg(); }); thread_2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread_1.start(); }
很顯然上面的程式碼會出現死鎖的現象,printshareMsg會拿到鎖一直迴圈下去,initshareMsg永遠拿不到monitor,所以不能執行, 我們可以通過wait和notify的配合使用來達到我們想要的效果. 修改後程式碼如下.
private String shareMsg = null; private synchronized void initshareMsg(){ shareMsg = "share msg"; notify();//放棄monitor,通知之前wait的執行緒,繼續執行 } private synchronized void printshareMsg(){ while (shareMsg == null){ try { wait();//等待,放棄monitor,讓別的執行緒獲得monitor } catch (InterruptedException e) { e.printStackTrace(); } System.out.print(shareMsg); } } public void run(){ Thread thread_1 = new Thread(() -> { initshareMsg(); }); Thread thread_2 = new Thread(() -> { printshareMsg(); }); thread_2.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread_1.start(); }
那麼notifyall又有什麼用?如果有多個執行緒都處於wait狀態,那麼僅僅呼叫notify只能喚醒一個執行緒,而notifyall可以喚醒所有處於wait的執行緒來競爭monitor.
3, Java中保證成員變數訪問的同步和原子操作
關鍵字:volatile, Atomic相關類
上面提到了,synchronized解決了資料訪問的互斥和資料的同步問題,但是單獨對一個成員變數來說不能加synchronized關鍵字,而我們僅僅對一個成員變數做資料訪問的互斥和資料的同步加上synchronized關鍵字又會感覺太麻煩,目前就有一個很好解決這個問題的方法是使用
volatile關鍵字和Atomic相關類,它們的作用容易混淆.
volatile主要用來解決的是資料的同步問題

如上圖,未加volatile的情況下執行緒b獲取到x的值可能出現x = 1的情況,加上volatile關鍵字就可以避免這個問題.
Atomic相關類主要解決的問題是像a++這樣的操作,a++這個操作其實分成了兩步,一步是r = a+1, 第二步是 a = r. 這顯然不是一個原子操作,使用AtomicInteger則把a++封裝成為一個原子操作.
AtomicInteger a = new AtomicInteger(); a.incrementAndGet();
a++不能靠volatile保證原子操作,volatile解決的是同步問題Atomic相關的類解決的原子操作問題.
4, Java中如何終止執行緒
通常我們開啟一個執行緒的後,會有終止一個執行緒的需求,那麼在java中是如何去終止執行緒呢?可以通過呼叫
thread.stop()
它可以立即終止執行緒,但是它有個不好的地方是在於,立即終止是存在一定的風險,因為執行緒正常執行過程中立即終止會導致程式出現異常。所以stop這個方法是被棄用的,而正規終止執行緒的方式是使用interrupt去終止執行緒.
Thread thread = new Thread(){ @Override public void run() { super.run(); for (int i = 0; i < 100; i++){ if (isInterrupted()){ //執行緒終止,收尾操作 } } } }; thread.interrupt();
通過isInterrupted判斷外界是否呼叫了執行緒終止,從而進行一些收尾操作後終止執行緒,這樣顯然更加安全. 另外Thread還有一個判斷終止的介面是Thread.interrupt(),它和isInterrupted()的區別是Thread.interrupt()呼叫後會把中斷標誌位重置,意思就是Thread.interrupt()第一次呼叫後是true,第二次呼叫後就是false了.
另外我們在使用Thread.sleep通常會丟擲InterruptedException
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
這個InterruptedException,就是線上程休眠過程中呼叫interrupt就會丟擲該異常.
Android中的執行緒
1,執行緒間的通訊Handler,Looper,MessageQueue
Looper
其實Android中執行緒和java執行緒區別就是,Android提供了一種建立無限迴圈執行緒的模式, 就是looper機制,我們來看看如何在java中去建立無限迴圈的執行緒.
private void run(){//執行入口 CustomizableThread customizableThread = new CustomizableThread(); //設定任務 customizableThread.setTask(new Runnable() { @Override public void run() { System.out.print("執行任務"); } }); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //退出迴圈 customizableThread.quit(); } /** * 建立一個無限迴圈的執行緒 */ class CustomizableThread extends Thread { private Runnable task; private boolean quit; synchronized void setTask(Runnable task){ this.task = task; } synchronized void quit(){ quit = true; } @Override public void run() { super.run(); while (!quit){ synchronized (this){ if (task != null){ task.run(); task = null; } } } } }
上面這種無限迴圈的執行緒就是Android經常提到的Looper的原型了. 在Android中它把上面提到的無限迴圈的機制寫成了一個 Looper物件,大致如下(當然實際程式碼肯定比這個複雜很多,這裡只是為了說明Looper到底起到了什麼作用)
class Looper { private Runnable task; private boolean quit; synchronized void setTask(Runnable task){ this.task = task; } synchronized void quit(){ quit = true; } public void loop(){ while (!quit){ synchronized (this){ if (task != null){ task.run(); task = null; } } } } }
另外looper會把這個task做成一個佇列的形式,那就是MessageQueue了,那麼Handler又是起到什麼作用呢,Handler它其實一個關聯一個looper的物件,用於像looper中MessageQueue傳送訊息,大致的模型如下:

ThreadLocal
接下來說下ThreadLocal,ThreadLocal是用來存放執行緒獨立的物件,什麼是執行緒獨立的執行緒物件,我們知道執行緒之間是可以共享記憶體的,
public class ThreadLocalDemo { private Integer mInteger = new Integer(1);//執行緒之間是共享的. private void run(){ Thread thread1 = new Thread(new Runnable() { @Override public void run() { mInteger = 2; } }); thread1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.print(mInteger); } }); thread2.start(); } }
那麼如果使用ThreadLocal的話就可以做到記憶體的獨立。
public class ThreadLocalDemo { private Integer mInteger = new Integer(1); static ThreadLocal<Integer> sThreadLocal1 = new ThreadLocal<>(); static ThreadLocal<Integer> sThreadLocal2 = new ThreadLocal<>(); public void run(){ Thread thread1 = new Thread(new Runnable() { @Override public void run() { sThreadLocal1.set(mInteger); Integer integer = sThreadLocal1.get(); integer = 3; System.out.println("ThreadLocal thread1 integer "+integer); } }); thread1.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Thread thread2 = new Thread(new Runnable() { @Override public void run() { sThreadLocal2.set(mInteger); Integer integer = sThreadLocal2.get(); System.out.println("ThreadLocal thread2 integer "+integer); } }); thread2.start(); } }
I/System.out: ThreadLocal thread1 integer 3 I/System.out: ThreadLocal thread2 integer 1
上面結果輸出可以說明thread1對integer的賦值只對thread1生效.
實際上在android中looper就是用ThreadLocal進行存放的,這樣可以做到每個執行緒之間looper是獨立的.
public final class Looper { static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } //其餘程式碼省略... }
Asynctask引起記憶體洩漏
什麼是記憶體洩漏,記憶體洩漏是GC Root引用鏈中有無用的物件。這裡的GC Root有3種
1,正在執行的執行緒
2,static變數
3,native引用到的
Asynctask實際上是後臺啟動執行緒,返回到ui執行緒的一個過程,它常常會引用到外部的Activity引用,導致Activity的記憶體洩漏,但是這是暫時的,通常情況下Asynctask不會長期引起記憶體洩漏.
最後
在現在這個金三銀四的面試季,我自己在網上也蒐集了很多資料做成了文件和架構視訊資料免費分享給大家【 包括高階UI、效能優化、架構師課程、NDK、Kotlin、混合式開發(ReactNative+Weex)、Flutter等架構技術資料 】,希望能幫助到您面試前的複習且找到一個好的工作,也節省大家在網上搜索資料的時間來學習。
資料獲取方式:加入Android架構交流QQ群聊:513088520 ,進群即領取資料!!!
點選連結加入群聊【Android移動架構總群】:加入群聊

資料大全