關於併發的思考--未完結待更新

併發的多面性

  1. 併發有更快的處理速度
  • 在單CPU的處理器上併發會造成性能的損耗,原因其實很簡單,無論操作系統使用時間片輪轉法還是FIFO的或者其他形式,在切換任務的時候都會增加上下文的切換,相對於單CPU單任務的話多了一個上下文的切換,這樣就造成了性能上的損耗,併發主要對於速度的提升是相對於多CPU而言的,在多CPU中同時將多個任務分配給多個CPU並行執行就大大的提高了程序的執行效率
  1. 在一些大型的軟件中多線程會使得軟件效率極大的提升的同時,也能夠使得程序設計得到極大的簡化,例如在一個遊戲中每個人物都有自己的相對獨立的意識,如果由單CPU支持的話就會出現每個人物的動作不一致的問題,但是如果由多任務來處理那麼理論上所有的人物都可以在規定的時間內完成規定的任務,使得動作協調一致.
    併發的壞處
    使用併發會導致更復雜的代碼

常見的java中線程的實現方式

  1. extent Thread 實現run方法

     public class ExtentThread extends Thread {
         String TAG = this.getClass().getCanonicalName();
         @Override
         public void run() {
             while (true) {
             Log.e(TAG, "run: " );
     
             }
         }
     }
    

使用

	ExtentThread extentThread =new ExtentThread();
	extentThread.start();

該種形式的線程實現方式優點如下
1.使用簡單
缺點
靈活性差

  1. implement Runable 實現run方法(其他的實現方式均爲該種的變體包括extent)

      public class ImplementThread implements Runnable {
     	String TAG = this.getClass().getCanonicalName();
       @Override
    	  public void run() {
         while (true){
             Log.e(TAG, "run: ");
         }
       }
     } 
    

使用

Thread thread =new Thread(new ImplementThread());
thread.start();

該種形式的線程實現方式優缺點如下

  1. 使用implements關鍵字,使得class可以繼承其他類,實現起來更靈活
  2. 實現較之於繼承稍顯複雜

線程讓步

在線程中如果我們完成了一件事,需要將CPU的控制權交付出去,可以使用Thread.yield,靜態方法Thread.yield的調用是對線程調度器的一種建議,告知線程調度器我已經執行完生命週期中的最重要的部分了,可以將CPU給其他的任務執行,但是線程調度器不一定會立即執行調度將CPU分配給其他的任務.
線程調機制是非確定的機制,也就是說如果現在存在五個線程同時運行五件原子時間(筆者目前還沒有想到如何實現,因爲線程的創建必然是有先後順序的,所以我們只討論理論上的情況),那麼理論上他可以產生5 * 4 * 3 * 2 * 1中不同的結果,同時由於線程調度器的存在我們也不用去關心線程是怎樣分配給CPU執行的,java的線程調度機制在內部已經很好的爲我們處理了

線程在執行完畢之前爲什麼不會被回收

new Thread(new Runnable() {
    @Override
    public void run() {
       
    }
}).start();

首先我們要了解的是爲什麼會有這個問題,對於一般的對象而言,如果沒有任何對象持有這個對象的引用那麼很快這個對象就會被java的垃圾回收期回收.

在上文中使用匿名類的方式new 出了一個線程的實例,雖然沒有任何對象持有對他引用,對於普通對象來說,一旦沒有任何對象持有對他的引用那麼他會被GC優先回收,但是對於線程就不一樣了,每個Thread都註冊了他自己,因此我們可以理解爲Thread本身持有了Thread的引用,所以在Thread的run執行完畢並且死亡之前GC是不會回收他的

在線程的構造過程中,創建的繼承或者實現類是不會實現一個線程的能力,要實現線程的行爲,必須顯式的將其附着到一個線程上,然後調用Tread.Start方法爲該線程執行必須的初始化的操作.這樣這個線程纔是真正的工作起來.

一種更好的創建線程的方式

創建一個線程還可以用java.util包中的Executor(執行器),Executor將會爲我們管理Thread對象,我們通過Executor.new***()方法可以創建不限個數或者指定個數的線程,例如CachedThreadPool會爲每一個任務都創建一個線程,這也就是說在不考慮系統的限制的情況下你每次來一個任務我都會創一個新的線程用於立即執行當前的任務,當然一旦存在線程被回收,那麼久不會去新建線程了,這種創建線程的方式會存在一個嚴重的問題–造成OOM.原因是當我短時間創建大量的線程的時候由於沒有指定最多創建的線程個數會導致儘可能多的創建線程,這會快速的消耗資源,噹噹前資源消耗完時系統拋出OOM.

而FixedThreadPool指定了線程的個數來執行所有提交的任務,當任務大於線程數時使用FIFO的形式,按任務的提交順序執行任務,噹噹前任務執行完畢之後開始執行下一個任務.直到所有的任務執行完畢.同時有了FixedThreadPool可以一次性預先執行代價高昂的線程分配,節省時間,同時由於線程的個數是優先的,所以也不會擔心資源的濫用

** 在任何線程池中,現有線程在有可能的情況下都會被自動複用 **

如何從任務中產生返回值

我們可以將Runnable看做一個單獨的任務,但是Runnable是不返回任何值的,如果希望任務結束的時候有返回值可以實現Callable接口而不是Runnable接口,這個接口通過調用call返回返回值,並且必須使用ExecutorService.submit()方法調用他,submit()方法會產生Future對象,這時候我們可以用Future的isDone來查看任務是否完成,當任務完成是我們可以利用get方法獲取結果,我們也可以直接使用get獲取任務結果,但是如果任務沒有完成,那麼就會被阻塞,直到任務產生結果.代碼如下所示:

//創建一個帶有返回值的任務
public class ThreadCall implements Callable<String> {
    @Override
    public String call() throws Exception {
        double pai = 0;
        for (int i = 0; i < 1000000; i=i+0) {
            pai = Math.PI*21.0*2.10;
            pai = pai+1;
        }
        return "call back" +pai;
    }

}
//使用
ExecutorService service = Executors.newCachedThreadPool();
    Future<String> submit = service.submit(new ThreadCall());
    try {
        boolean done = submit.isDone();//任務是否完成
        if (done) {
            String s = submit.get();//獲取任務完成後的返回值,如果此處任務一直沒有結束將會阻塞
            Log.e(TAG, "onCreate: " + s);
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }

我們可以使用簡單的sleep()來休眠任務,sleep()將會將線程中止指定的時間,
Thread.yield線程讓步,告知線程調度器現在是一個合適的時機將CPU讓給其他線程

你所誤解的volatile

volatile的特性

  1. 保證此變量對所有的線程是可見的,這裏的可見是指當一個線程修改了用volatile修飾的變量之後,變量的值對於其他線程來說是可以立即知道的

    • 這樣會使我們產生一種錯誤的感覺,如果使用volatile標記一個變量那麼對於這個變量的操作總是安全的.下面看一個簡單的例子

        public int addVolatile() {
            return a++;
        }
        public void startThread() {
            Thread[] threads = new Thread[20];
            for (int i = 0; i < 20; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i1 = 0; i1 < 10_00; i1++) {
      
                            addVolatile();
                        }
                    }
                });
                threads[i].start();
            }
            int b = 0;
            while (Thread.activeCount() > 1) {
                b++;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (b>10) {
                    break;
                }
                Log.e(TAG, Thread.activeCount()+"startThread: " + a);
            }
        }
         
        11-07 15:11:38.811 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:39.812 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:40.813 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:41.814 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:42.815 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:43.816 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:44.817 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:45.818 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:46.819 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
        11-07 15:11:47.820 7617-7617/com.mx.lhc.javatest E/com.mx.lhc.javatest.MainActivity: 5startThread: 10605
      
    • 通過結果我們可以看到,如果說通過volatile修飾的變量在併發的過程中能夠正確的被運算的話,那麼結果應該是20*1000,但是實際上我們計算出的結果遠遠低於這個值,這是因爲雖然被volatile標記的變量a是對所有線程可見的,但是java中的運算操作符不是原子操作,那麼也就是說存在這一一種情況,當線程A在做a++的操作中剛剛讀取完變量a的值,正準備做++操作的時候線程B做完了++操作將變量a寫到主存中,此時線程A繼續執行++然後將變量a數據更新到主存中,這樣就導致了a的值和我們預期的結果不一樣的情況.

    • 由於volatile變量只能保證可見性,所以在編碼的工程中我們一定要小心volatile,仔細的分辨當前是否需要通過枷鎖來保證原子性.

  2. 禁止指令重排序優化

    什麼叫指令重排序優化

    指令衝排序優化我們可以簡單的理解爲CPU爲了提高效率可能會不按照我們所寫的代碼順序一步一步的執行,而是根據jvm自己規則在不影響代碼最終結果的前提下對代碼進行重新排序,注意最重要的原則–不影響代碼最終運行結果,例如:

     int a = 0;①
     int b = 1; ②
     int c = a+b; ③
    

    在這種情況之下,就可能存在指令的重排序,① ②的執行順序是不會影響程序最終的結果的,所以可能存在先運行②在運行①的可能,即使這樣③的結果也不會變,這就是指令的重排序優化,當然這個例子無法體現出指令重排序優化的好處,但是如果步驟①是一個耗時的操作那麼經過指令的重排序程序整體的運行時間就變短了.

    同樣由於不能改變最終的運行結果,所以步驟③是無法再①②之前運行的因爲一旦這樣c的結果就發生了改變.

    但是volatile關鍵字告訴jvm不需要對該變量進行指令的重排序優化

    假設存在以下情況

     //運行在線程A中
     volatile boolean initialized = false;
     readFile();
     initialized = true;
     
     //運行在線程B中
     while(!initialized)
     	sleep();
     useFile();
    

    如果不用volatile修飾initialized那麼根據指令的重排序優化很有可能產生initialized先運行 readFile()後運行的情況,在這種情況下線程B就從sleep狀態解除,這時使用File便會出現異常.但是有了volatile關鍵之修飾initialized就不會在readFile()結束之前運行,這樣就保證了安全.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章