Android的Java多執行緒部簡介和Synchronized學習總結

找瓶子的湯圓發表於2019-04-18
  • 執行緒使用

1. 建立後臺執行緒執行任務,大多數人(包括我)都會直接選擇
new Thread()
//或者
new Thread(new Runnable())
複製程式碼

之後用start()來啟動執行緒。跟程式碼會發現start()會執行start0()這個native方法,虛擬機器呼叫run方法。有Runnable就會呼叫傳入的runnable的run()實現,否則就會執行Thread中的run()方法。

2. ThreadFactory:工廠模式,方便做一些統一的初始化操作:
ThreadFactory factory=new ThreadFactory(){
    @Override
    public Thread newThread(Runnable r){
        return new Thread(r);
    }
    
}

Runnable runnable =new Runnable(){
    //```
}

Thread thread=factory.newThread(runnable);
thread.start();
Thread thread1=factory.newThread(runnable);
thread1.start();
複製程式碼
3. Executor:最常用到但是卻很少用的方法:
Runnable runnable =new Runnable(){
    @Override
    public void run(){
        //```
    }
}

Executor executor=Executors.newCachedThreadPool();
executor.executor(runnable);
executor.executor(runnable);
(ExecutorService)executor.shutdown();
複製程式碼

至少對於我,看起來是很少用,那為什麼說最常用呢?
AsyncTask,Cursor,Rxjava其實也是使用Executor進行執行緒操作的。

可以來看一下Executor,只是一個介面來通過execute()來指定執行緒工作

public interface Executor {
    void execute(Runnable command);
}

複製程式碼

對於Executor的擴充套件:ExecutorServive/Executors Executors.newCachedThreadPool()返回的又是一個ExecutorSevice extends Executor,對Executor做了一些擴充套件,主要關注的是shutdown()(保守型的結束)/shotdownNow()(立即結束),還有Future相關的submit(),這些後面會說。
newCachedThreadPool() 建立了一個帶快取的執行緒池,自動的進行執行緒的建立,快取,回收操作。
還有幾個別的方法:
newSingleThreadExecutor() 單一執行緒,用途較少。
newFixedThreadPool() 固定大小的執行緒池。 比如需要建立批量任務。 newScheduledThreadPool() 指定時間表,做個延時或者指定時間執行。
如果你想自定義執行緒池的時候就可以參考這幾個方法在app初始化的時候直接new ThreadPoolExecutor了。 執行緒完成後結束:就可以直接新增任務後執行shutdown()。關於執行緒的結束,寫在了後面的執行緒結束

ThreadPoolExecutor()的幾個引數

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(
        0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>());
    }
   
    public ThreadPoolExecutor(
    int corePoolSize,//初始執行緒池的大小/建立執行緒執行結束後回收到多少執行緒後不再回收
    int maximumPoolSize,//執行緒上線
    long keepAliveTime,//保持執行緒不被回收的等待時間
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,//阻塞佇列
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) {
    //```
    }

複製程式碼

寫到這裡的時候,maximumPoolSize這個引數,想起之前在自己寫一些圖片載入、快取的時候,開的執行緒總是會用CPU核心數來限制一下,比如2*CPU_CORE,以前不懂,會覺得大概是每個核分一個? 現在學到的:首先肯定不是為了一個核心來一個執行緒,畢竟一個cpu跑N多個執行緒,哪能就那麼剛剛好一個核心一個。
大概是可以保證程式碼在不同的機器上的CPU排程積極性差不多,比如單核的,就建立兩個執行緒,8核心的,就是16個執行緒。不然寫8個執行緒,單核心機器執行可能就會比較卡,8核心機器執行又會太少。

4. callable 可以簡單描述為有返回值的後臺執行緒,安卓端比較少用,就簡單記錄下,畢竟AsyncTask、Handler、RxJava都比這個好用
     Callable callable = new Callable<String>() {
            @Override
            public String call() throws Exception {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "找瓶子";
            }
        };
        ExecutorService executor = Executors.newCachedThreadPool();
        Future<String> future = executor.submit(callable);

        try {
            String result = future.get();
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
複製程式碼

是不是看起來也不是很麻煩?甚至還有一丟丟好用? 但是這個Future會阻塞執行緒,如果在主執行緒中使用的話,就需要不停地來檢視後臺是否執行結束

     while (true) {
            if (future.isDone()){
                try {
                    String result = future.get();
                } catch (ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
                try{
                    Thread.sleep(1000);//模擬主執行緒的任務,過一秒來看一看
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
複製程式碼

關於執行緒的結束,上面只是簡單說了下使用shutdown(),其實

  • 執行緒安全/同步問題:

大致的產生原因:作業系統對於cpu使用的時間片機制,導致某段程式碼某個執行緒在執行中會被暫停,其他執行緒繼續執行同一段程式碼/操作同一個資料(資源)的時候,可能帶來的資料錯誤。
eg:

class Test{
    
    private int x=0;
    private int y=0;

    private void ifEquals(int val){
        x=val;
        y=val
        if(x!=y){
            System.out.println("x: "+x+",y: "+y);
        }
    }

    public void testThread(){
        new Thread(){
            public void run(){
                for(int i=0;i<1_000_000_000;i++){
                ifEquals(i);
                }
            }
        }.start();
        
        new Thread(){
            public void run(){
                for(int i=0;i<1_000_000_000;i++){
                    ifEquals(i);
                }
            }
            }.start();
        }

}
複製程式碼

這還能不相等?
產生的原因其實簡單,上面也簡述過。就是在執行testThread的時候,兩個執行緒同時操作ifEquals方法:

  1. 執行緒1在操作到i=10,當前x=val=10時候被切換執行緒,此時y=val還未被執行緒1進行賦值
  2. 執行緒2進行執行程式碼,當進行到x=100,y=100,然後執行緒再次切換
  3. 執行緒1執行y=10,此時x的值已經是執行緒2修改過的100了,就會導致x!=y。

那麼知道了原因,解決的思路就變得簡單,x,y這兩步操作應該是變成一步操作即可。或者說一次ifEquals方法變成一步操作。所以JAVA提供了synchronized關鍵字,把這個關鍵字加在ifEquals這個方法,就使得其變成原子操作。 會對這個方法新增一個監視器Monitor這樣線上程1未執行完畢的時候,monitor不會被釋放,即使執行緒切換,執行緒2訪問到這個方法的時候,由於monitor未被釋放就會進入排隊等待,不會執行這個方法。

關於Synchronized關鍵字:現在給ifEquals增加Synchronized關鍵以後,上述程式碼再增加下面這個delVal()方法線上程中呼叫,x,y的值會出現問題嗎?依然會。
所以,看起來是對於方法的保護,實際上是對資源的保護,比如上面的例子,我們希望的其實並不是保護ifEquals方法,而是x,y的資源。

    private void delVal(int val){
        x=val-1;
        y=val-1;
    }
    
    String name;
    private Synchronized void setName(String val){
        name=val;
    }

複製程式碼

另一個問題就是在保護x/y的時候,同時也需要保護name的時候,如上對於兩個方法都加上Synchronized的時候也不方便,此時會導致當一個執行緒只是訪問到ifEquals方法的時候,另一個執行緒不能訪問setName。這就與我們一個執行緒操作x、y,另一個執行緒同步操作name的預期不符。原因是對方法新增synchronized會把整個Test類物件當做Monitor進行監視,也就是這些方法都會被同一個monitor進行監視。

那為什麼兩個方法操作的不是同一個資源,還會被保護呢?因為monitor不會對方法進行檢查,實際上我們synchronize方法的原因也不是為了讓方法不能被另一個執行緒呼叫,而是為了保護資源。這時候,synchronize方法就不符合我們多執行緒操作的預期,就需要我們自己手動來進行操作。所以就需要引入Synchronized程式碼塊。

    final Object numMonitor=new Object();
    final Object nameMonitor=new Object();
    //程式碼塊
    private void ifEquals(int val){
        synchronize(this){
            x=val;
            y=val
            if(x!=y){
                System.out.println("x: "+x+",y: "+y);
            }
        }
    }
     //程式碼塊
    private void delVal(int val){
         synchronize(this){
            x=val-1;
            y=val-1;
        }
    }
    
    //指定monitor
     private Synchronized void setName(String val){
     synchronize(nameMonitor){
            name=val;
        }
        
    }
    
   public void testThread(){
       //``````
   }
複製程式碼

synchronize(Object object),允許你指定object作為monitor來進行監視,比如我們可以把上面的this,換成numMonitor,這個時候,name和x、y就是兩個monitor進行,互不影響了。
synchronize的另一個作用:執行緒之間對於監視資源的資料同步

先解釋下java虛擬機器下面的資料操作:比如ifEquals這個方法,x、y讀到記憶體中,並不是cpu直接操作記憶體中的資料,而是由cpu單獨給一個儲存空間進行操作。我們都知道,現在用的記憶體條速度,比起cpu的操作速度有著極大的速度差,就像硬碟和記憶體的巨大速度差一樣,如果程式碼都是從硬碟執行,然後運算元據再寫回硬碟,肯定無法忍受。對於cpu也是一樣,ram的讀寫實在是太慢了,這時候就像使用記憶體來彌補速度差一樣,使用cpu的高速cache,來彌補記憶體和cpu匯流排之間的速度差。 基於以上的描述

開始操作的時候,

  1. x=0,y=0;
  2. thread1進行:x=5,y=5結束(線上程的cpu cache中),還沒有寫入記憶體的時候,
  3. thread2進行資料讀取,x=0,y=0;
synchronize關鍵字,就會保證cpu讀取賦值以後,再寫回記憶體中。來保證資料的正確
複製程式碼

但是帶來的結果就是,如果沒有cpu快取操作,這個x=5,y=5的操作會變得很慢。其實從上面的程式碼執行時間也能很明顯的看出區別,testThread()方法的執行時間,在有沒有對方法新增synchronize時會相差非常明顯。所以雖然會帶來執行緒之間的資料同步問題,當前的cache還是很有必要的。

安全
Safety 保證不會被改錯,改壞的安全,比如Thread Safety
Security 不被侵犯安全 https 的S
複製程式碼

死鎖

    private Synchronized void setName(String val){
        synchronize(nameMonitor){
            name=val;
                synchronize(numMonitor){
                    x=val;
                    y=val
                    if(x!=y){
                        System.out.println("x: "+x+",y: "+y);
                    }
                } 
            }
        }
    }
    
    private void ifEquals(int val){
        synchronize(numMonitor){
            x=val;
            y=val
            if(x!=y){
            System.out.println("x: "+x+",y: "+y);
            
            synchronize(nameMonitor){
                    name="haha";
                } 
            }
        }
    }
    
     

複製程式碼

Thread1 執行 ifEquals,numMonitor,然後cpu進行執行緒進行切換到Thread2 執行setName,持有nameMonitor,然後往下執行的時候,發現numMonitor被持有,Thread2進行等待,切換回Thread1,Thread1發現繼續執行,但是nameMonitor被持有,進入等待,這樣兩個執行緒就變成了互相持有對方需要的monitor進入互相等待,也就是死鎖。

樂觀悲觀鎖

跟執行緒安全不是很相關的鎖,更多的是,資料庫相關,並不是執行緒相關。

比如資料庫進行資料修改,需要先取出資料進行操作,再往裡寫,就會出現A操作寫資料,B操作也寫同一個資料,比如小明給我轉賬100,A操作出我的餘額X+100,正要寫入資料庫:餘額X+100,小王給我轉賬1並且先一步寫入了X+1,此時如果A操作繼續寫入餘額X+100就很明顯是錯誤的了。
解決這個問題的方式兩種:

  1. 悲觀鎖:對讀寫操作加鎖,A操作進行結束之前,B操作進行等待。是不是跟synchronize操作看起來一樣?
  2. 樂觀鎖:拿到資料的時候不加鎖,A操作進行寫入時,發現資料庫資料已經跟取出時候有了變化,那麼重新計算再寫入。

靜態方法的synchronize

1.給方法加,預設的monitor就是當前的Test.Class,這個類,不是這個物件 2.程式碼塊內部無法使用synchronize(this),應為這是靜態方法並不存在這個this。可以使用synchronize(Test.Class)

volatile

private volatile int a;

  1. 相當於一個小型的synchronized,使得對的操作具有原子性和同步性。多個執行緒進行讀寫操作不會導致記憶體中的資料亂改
  2. 但是對於double、long,由於比較長,那麼它本身不像int,其沒有原子性
  3. 對於基本型別有效,對於物件,只有保證本身的賦值操作有效(Dog dog=new Dog(“wangwang”)有效,對於dog.name="miaomiao"無效 )
  4. 我們都知道(int)a++ 實際上就是兩步操作,所以volatile 並不能保證a++的安全
    1. int temp=a+1;
    2. a=temp;
保證a++使用AtomicXXX
AtomicInteger a=new ActomicInteger(0);
a.getAndIncrement();
複製程式碼

讀寫鎖lock

比如Test中,增加一個方法

    private Lock lock=new ReentrantLock();
    
    private void reset(){
        lock.lock();
        //```
        lock.unlock();
    }

複製程式碼

但是如果在中間的方法中,出現了異常,後面的lock.unlock()無法執行,那就會導致一直被鎖(Monitor遇到異常會自動解開),所以需要手動處理:

    private void reset(){
        lock.lock();
        try{
           //```  
        }finally{
            lock.unlock();  
        }
    }

複製程式碼

看起來功能跟synchronized差不多?但是這麼麻煩用你幹啥?但是其實想一下,之前說執行緒同步的時候,都是在寫資料的時候出現問題,單純的讀取資料並不會出現問題,只有在寫入的時候,別人讀寫會導致出現問題,如果執行緒1讀取資料中切換執行緒,執行緒2也不能讀取,就會有效能的浪費,讀寫鎖就可以解決這個問題:

    private ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
    private ReentrantReadWriteLock.ReadLock readLock=lock.readLock();
    private ReentrantReadWriteLock.WriteLock writeLock=lock.writeLock();
    
    private void ifEquals(int val){
        writeLock.lock();
        try{
            x=val;
            y=val;
        }finally{
            writeLock.unlock()
        }
    }
    
    private readData(){
        readLock.lock();
        try{
            System.out.println("x: "+x+",y: "+y);
        }finally{
            readLock.unlock();
    }
        
    }
複製程式碼

這樣,有執行緒在呼叫ifEquals()的時候,別人不能讀也不能寫,readData()的時候,別人能跟我一起讀取資料。就有利於效能的提升。

  • 執行緒之間的互動

1. 從及基本的啟動/結束開始:
    Thread thread = new Thread() {
        @Override
        public void run() {
            for (int i = 0; i < 1_000_000_000; i++) {// 模擬一段耗時操作
                Log.d("========>", "找湯圓");
            }
        }
    };
    thread.start();
    Thread.sleep(100);
    thread.stop();
複製程式碼

這樣就在主執行緒完成了子執行緒的開始和終止,就基本的互動就是這樣。但是用的時候會發現,stop()。喵喵喵?不是很好用嗎?為什麼劃線不建議用了呢?
因為不可控,Thread.stop會直接結束,而不管你內部正在進行什麼操作(實際上主執行緒也確實不知道子執行緒在做什麼),這樣就帶來了不可控性。
但是比如我明知道進行A操作以後,後面的執行緒做的工作已經無意義了,需要節省資源終止執行緒要怎麼做呢? 用thread.interrupt(),但是這個interrupt只是做了一個標記,如果僅僅使用interrupt是沒有任何作用的,還需要子執行緒自己根據這個中斷狀態進行操作:

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1_000_000_000; i++) {
                if (isInterrupted()) {//不改變interrupt狀態
                // if(Thread.interrupted()) //這個方法會在使用之後重置interrupt的狀態
                //做執行緒結束的收尾工作
                    return;
                }
                Log.d("========>", "找湯圓"+i);
            }
        }
    });
    thread.start();
    try {
        Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    thread.interrupt();

複製程式碼

看到這個interrupt是不是會覺得這個詞線上程操作中有點熟悉?哎(二聲),這不是剛好是上面兩行sleep的時候要catch的InterruptedException麼?為什麼我就想讓執行緒sleep一下,要加入個try/catch呢?
原因有兩個:

  1. sleep方法會檢測當前的interrupt的狀態,如果當前執行緒已經被interrupt,則會拋InterruptedException
  2. 因為我們在使用interrupt的時候,需要注意interrupt會直接喚醒sleep,重置interrupt的狀態
    //對於Thread.interrupt()方法的部分註釋
     * If this thread is blocked in an invocation of the wait() or join() or sleep(),
     * methods of this class, then its interrupt status will be cleared and it
     * will receive an {@link InterruptedException}.
     
複製程式碼

所以中斷執行緒的時候需要考慮一下進行處理:

    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 1_000_000_000; i++) {
                if (Thread.interrupted()) {//false,100毫秒後才會變成ture
                    //進行自己的interrupt處理
                    return;
                }
                try {
                  Thread.sleep(2000);//睡得時候被執行interrupt,會直接喚醒進入中斷異常,
                    //如果不在下面catch進行處理,interrupt的值又會被重置,導致外部呼叫的interrupt相當於沒有發生
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //收尾工作
                    return;
                }
                Log.d("========>", "找湯圓"+i);
            }
        }
    });
    thread.start();
    try {
        Thread.sleep(100);//這裡是主執行緒睡了,
        //跟上面的子執行緒的sleep不一樣哦,只是為了模擬start()和interrupt之間中間有個時間差
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    thread.interrupt();

複製程式碼

那所以安卓中我只是想單純的讓執行緒sleep一下,不需要外部interrupt時候提供支援,不想寫try/catch不行嗎?

SystemClock.sleep(100);//小哥哥,瞭解一下這個
複製程式碼
2. wait(),join(),yield()的使用(Android較少用到):
  1. wait(),比如上面講到的死鎖中,存在的情況,雙方互相需要操作資源的時候發現monitor不在手裡,wait總是要和個synchronized一起出現,因為wait出現就是為了使用共同的資源
String name=null;

private synchronized void setName(){
    name="湯圓";
}

private synchronized String getName(){
    return name;
}

private void main(){
    new Thread() {
        @Override
        public void run() {
        //一些操作
        setName();
        }
    }.start();    
    new Thread() {
        @Override
        public void run() {
        //一些操作
        getname();
        }
    }.start();   
    
}

複製程式碼

由於兩個Thread不知道誰先執行完,所以可能出現getName先執行,但是getName獲取空的話又沒法進行操作,這時候怎麼辦呢?

private synchronized String getName(){
    while(name==null){}//一直乾等著,直到name不為空
    return name;
}
複製程式碼

但是這是個synchronized方法又會持有跟setname一樣的monitor,setName也被鎖住了成了死鎖,那怎麼做?

private synchronized String getName(){
    while(name==null){//使用wait的標準配套就是while判斷,而不是if,因為wait會被interrupt喚醒
    try{
        wait();//object方法
    }catch(InterruptedException e){
        //
        e.printStackTrace();
    }
    }//乾等著,直到不為空
    return name;
}
private synchronized void setName(){
    name="湯圓";
    notifyAll();//object方法,把monitor上的所有在等待的執行緒全部喚醒去看一下是否滿足執行條件了
}
複製程式碼

或者是進入頁面,需要請求多個介面,根據介面的資料來設定頁面設定資料,也是類似的情況。不過實際上一個Rxjava的zip操作就能解決大多數問題了。 實際上寫了這麼多,這種需求Rxjava的zip操作就解決了...

  1. join() 可能會存在一個thread1在執行的過程中,需要thread0來執行,等完全結束後再繼續執行該執行緒
private void main(){
    new Thread() {
        @Override
        public void run() {
        //一些操作
        try{
            thread0.join();//相當於自動notify的wait
            //Thread.yield();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        
        //一些操作
        getname();
        }
    };   
}

複製程式碼
  1. yield() 極少用,瞭解就行了,參考上面註釋掉的那行程式碼,讓出本次的cpu執行時間片給同優先順序的執行緒。

感謝&參考:扔物線

相關文章