多線程入門學習-第一課(多線程併發理解)

        本篇是我最近在《Thinking in java》一書多線程的概念部分的一些學習心得,由於只是重點看了這幾個概念性的東西,對應後面的內容還沒有深入學習,所以有什麼不足的地方希望大家多多原諒,本文旨在讓不瞭解多線程的人能夠了解什麼是多線程,如果有什麼地方講的不好,希望大家能多多指點,謝謝。希望對沒有接觸過多線程的人有點幫助。
 
       本節要通過一個例子來簡單介紹一下多線程,及使用多線程的原因。首先,我們來看幾個重要的概念性的問題。說到多線程不得不提併發。那麼到底什麼是併發?先說一下平時我們寫的程序是怎麼樣的,平時寫的程序大都是順序執行的,那麼什麼叫做順序執行呢?舉個例子來說,現在你寫了個程序,這個程序很簡單就100行代碼,然後他從第一行順序運行到了最後一行結束,這就是順序執行。

     在講併發之前,我想先講一下另外一個非常重要的概念“任務”。什麼是任務?其實任務這個東西可大可小,就看你從什麼角度去劃分他。就說這100行代碼,你可以把它看成是一個任務。當然你也可以把這個100的任務拆分成多個小任務,我們就拆分成10個小任務,假設這10個小任務都互不相干,然後從1到10給這個任務編號,那麼順序執行的程序他就會從1依次執行到10。而併發的程序也就是多線程寫的程序是怎麼執行的呢?從宏觀上看,他有可能是10個任務同時進行,這就是所謂的併發。但事實確並不是這樣的,假設只要一個處理器,也就是說同一時刻只有一個任務可以進處理。(事實上同一時刻處理器只執行一個指令纔是,而一個任務是有很多指令所組成的,這裏我們把這些個任務都想象成原子操作,爲什麼可以看出原子操作,你可以這麼認爲:通過特殊指令可以讓處理器順序執行完這個任務的一系列指令,也就是說每個小任務內部是順序執行的)。

     既然一個時刻只有一個任務在進行那麼前面爲什麼又說多個任務同時進行呢?這裏就又涉及到另外一個概念----阻塞。什麼是阻塞?我們假設“任務1”在處理到一半由於某種原因不在執行下去了(這個原因是程序控制之外的,比如說I/O操作就是等待你從控制檯輸入信息),這個時候怎麼辦?假如是順序執行的程序,他就會 一直這麼等着,也就是說其他任務在任務1執行完之前是不會去執行的。但是併發操作就不一樣了,這個時候通過某種指令告訴處理器任務1在等待了,其他的任務可以搶佔這個處理器去執行自己的任務,當然只是其中一個不可能9個同時執行。因爲執行速度是非常快的,宏觀上看你會覺得他們是同時進行的,又或者說換一種角度來看,我們把在等待的時間也算成是執行任務的時間,那麼他們也是同時進行任務的,只是cpu在處理的時候只處理一個任務。也就是說這裏的併發指的是什麼?這裏的併發指的是多個任務可以同時進行,但每個任務的cpu處理階段只能有一個任務在進行。

     那麼到底是如何進行併發操作的呢?這裏就又要引入另外一個新的概念----線程。什麼是線程?線程有什麼用?我是這樣理解:我們用線程來包裝需要執行的任務,然後告訴處理器來處理我包裝的任務,從微觀的角度來講,它也一個是一系列指令,這些指令告訴處理器需要執行哪些代碼(當然這些代碼也只是一系列指令而已)。並且這個線程一旦通知cpu去處理任務後,在這個任務結束之前他是不會再包裝其他任務了。那其他任務想執行怎麼辦?那就再起一個線程去處理其他某個任務唄。這樣子你的程序裏就會存在多個線程,也就是所謂的多線程啦

          從性能角度來看,假如沒有阻塞,那麼就沒有多線程的必要,此時若是使用多線程還增加可個上下文切換(從一個任務切換到另一個任務)的時間。而當出現阻塞的時候如果不使用多線程,那麼程序勢必會停止那裏等待,這個時候就是在浪費時間了。而假如使用了多線程,這個等待的線程就繼續讓他等待着,而另外一個線程可以繼續執行他的工作。其實就cpu的處理而言,同一時刻其實只執行了一個任務,只是當順序執行的程序中有出現了因爲阻塞而等待的情況時,就跳過這個情況,然後繼續執行下去,然後那個阻塞好了,就繼續加入到cpu的搶佔當中。誰搶到了,誰就執行。舉個例子來說:這就好比你要燒菜,加入按部就班的燒菜要怎麼做?那麼我們現在做一道蛋湯。首先,第一步我要把雞蛋攪碎,第二步:切一些配料,第三步把水放入鍋中燒,第四步,等水燒開後把蛋和配料放入水中。然後等他煮好就好了。這裏看起來一切都很正常,其實在燒水的時候,我們有一段等水燒開的時間,這段時間你是空閒的。這並不是一個高效的做法,假如我們先燒水,然後在燒水的這段時間攪碎雞蛋和切配料,然後水燒開後放入水中煮,其實這樣子做就相當於併發執行了你要做的事情,充分利用了空閒的時間,而你自己人就相當於執行任務的cpu。
          從以上的舉例中我們可以得出這樣一個結論,爲了能夠充分利用阻塞時的空閒時間,我們可以選擇多線程操作。在代碼中的示例我們可以這樣來考慮,這裏我講的是一種思路,其思路的宗旨是通過比較來學習理解多線程。有兩個任務,任務1:非阻塞,任務2:阻塞;兩種執行方法:1:順序執行,2:併發執行。然後分別取一個任務和一種執行方法進行組合測試。我把這方法叫做排列組合法,這樣可以清楚的知道各種情況下會怎麼樣,下面我附上簡單的測試代碼:

import java.util.concurrent.TimeUnit;
//倒計時
public class LiftOff implements Runnable {
       private int countDown = 10;//倒計時數
       private static int taskCount = 0; //多個對象共用這個變量
       private  final int id = taskCount++;//標誌是哪個線程在執行的這個
       public LiftOff() {
             super();
      }
       public LiftOff(int countDown) {
             super();
             this.countDown = countDown;
      }
       //打印計時數
       public String status() {
             return "#" + id + "(" + ( countDown > 0 ? countDown : "liftOff") + ")" ;
      }
       //用於測試不併發
       public void run2() {
             while (countDown -- > 0) {
                  System. out.println(status());
                   try {
                        TimeUnit. MILLISECONDS.sleep(1000);
                  } catch (InterruptedException e) {
                        e.printStackTrace();
                  }
            }
      }
       //測試併發執行
       public void run() {
             while (countDown -- > 0) {
                  System. out.println(status());
                   try {
                        TimeUnit. MILLISECONDS.sleep(100);
                  } catch (InterruptedException e) {
                        e.printStackTrace();
                  }
            }
      }

       public static void main(String[] args) {
             //併發
             for (int i = 0; i < 5; i++) {
                   new Thread(new LiftOff()).start();
                  System. out.println("diligent" );
            }
//          //不併發
//          for( int i = 0; i < 5; i++){
//                new LiftOff().run2();
//                System.out.println("diligent");
//          }
      }
}
下面來分析一下這個類:
     這個類非常簡單,其實就是一個倒計時的類。該類繼承了Runnable接口,表示是一個任務,繼承該接口需要實現run方法。run()內的就是一個要執行的任務,簡單點說就是要在新開啓的線程中跑的東西。而status方法說白了就是一個返回當前計數的方法。現在這個要執行的具體任務,也就是run方法內要執行的內(輸出當前的計數直到他小於0,並且每打印一次就休眠1秒)而main方法裏面有一個for循環用來執行5遍這個倒計時的任務。每次執行這個任務都重新創建一個線程Thread來執行這個任務。main其實也是跑在一個線程裏面。接下來我們用極限的思想來考慮這個問題:

     首先我們來羅列一下我們要分析的這個問題所涉及到的元素:1.執行這個任務的總時間我們記爲t1,2.兩次創建線程的時間間隔我們記爲t2。然後讓我們來比較一下這兩段時間。第一種情況:t1無窮小,趨向於0,記t1->0,t2無窮大,記t2->∞,這就意味着這個任務執行速度會非常之快,近乎是一個原子操作了,所有這個時候系統同時最多也就存在兩個線程。其實這種情況已經沒有多起一個線程去執行他的必要了,因爲就這麼順序執行也不會出現明顯的阻塞狀態。然後第二中情況:t1->∞、t2->0,這個會導致怎樣的情況呢?此時執行倒計時的任務的線程會同時存在,還有一個main主線程。然後這6個線程會如何運行?首先要明確一點,那就是當同時存在這樣六個線程的時候,在同一時刻必定只有一個任務在執行(這裏討論的都是單cpu的情況),可以認爲這六個線程擠在那裏等待執行各自的任務。然後一個任務他執行好了或者是等待了,其他的線程就會搶佔資源來執行自己的任務,可以理解爲搶佔cpu來執行自己的任務,誰搶到誰執行,這個如果優先級都一樣那麼搶到資源的概率是一樣的,所有到底誰執行任務是存在不確定性的。但有一點是可以確定的,那就是每個線程都有自己的任務,而這個任務是不會變的,並且即使自己的任務完成了,也不會管其他線程的任務。

     然後來總結一下上面的內容:
          1:有阻塞才併發
          2:併發具有不確定性(那個線程搶到資源執行任務是隨機的)
          3:每個線程執行的任務是明確的

    再補充一點就是不要爲了併發而併發,能順序執行的就不要多線程執行,啓用線程本身就需要額外的開銷,首先你啓用一個線程需要花費時間,然後任務之前的切換執行也需要時間,還有後續將會的併發存在的一些隱患如死鎖。只有當真正需要併發執行的時候再去併發,並且還要小心編寫,這些在後續都會講到。
    最後總結下分析問題的小技巧:一個是上面我用到排列組合法,先從一個特例出發找出問題所在,然後把問題分解,然後提煉出分解的元素,考慮每個元素本身存在的不同情況,在排列組合出多種實際問題的情況。
          另一個就是可以用極限的思想看問題,然後找出問題所在。
發佈了5 篇原創文章 · 獲贊 6 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章