Android的Java多線程和Synchronized學習總結

  • 線程的使用方式

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

以後用start()來啓動線程。跟代碼會發現start()會執行start0()這個native方法,虛擬機調用run方法。有Runnable就會調用傳入的runnable的run()實現,不然就會執行Thread中的run()方法。java

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()的幾個參數bash

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();
                }
            }
        }
複製代碼

ok,線程的常見使用方式就基本上說完了。使用的時候咱們會常常遇到多個線程操做同一個資源的問題,好比A給B轉帳的同時,B,C又同時給A轉帳,那麼應該怎麼處理呢?這時候就要說到第二節的線程安全問題了。app

  • 線程安全/同步問題:

1. 產生緣由:

操做系統對於cpu使用的時間片機制,致使某段代碼某個線程在執行中會被暫停,其餘線程繼續執行同一段代碼/操做同一個數據(資源)的時候,可能帶來的數據錯誤。
eg:ide

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。
2. 解決辦法:Synchronized

那麼知道了緣由,解決的思路就變得簡單,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;
    }

複製代碼
3. 關於監視器Monitor

另外一個問題就是在保護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的另外一個做用:線程之間對於監視資源的數據同步

4.靜態方法的synchronize

1.給方法加,默認的monitor就是當前的Test.Class,這個類,不是這個對象 2.代碼塊內部沒法使用synchronize(this),應爲這是靜態方法並不存在這個this。可使用synchronize(Test.Class)

5. 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;

6. 保證a++使用AtomicXXX

AtomicInteger a=new ActomicInteger(0);
a.getAndIncrement();
複製代碼

7. 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仍是頗有必要的。

8. 關於安全

不少時候咱們在說安全,線程安全,網絡安全,數據安全,可是其實這是不同的「安全」:

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操做進行寫入時,發現數據庫數據已經跟取出時候有了變化,那麼從新計算再寫入。

讀寫鎖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執行時間片給同優先級的線程。

感謝&參考:扔物線

相關文章
相關標籤/搜索