天天看點

JAVA學習筆記——多線程

1.程序

  程序是正在運作的程式,是系統進行資源配置設定和調用的獨立機關,每一個程序都有他自己的記憶體空間和系統資源。

  多程序:單程序的計算機隻能一次做一件事,而多程序就可以在同一個時間段内執行多個任務,提高CPU的使用率。

  問:一邊玩遊戲一邊聽音樂是同時進行的嗎?

  并不是,因為單CPU在某個時間點上隻能進行一個任務,隻是CPU在做着程式間的高效切換而讓我們感覺是同時進行的。

2.線程

  在一個程序内又可以執行多個任務。而這每一個任務就可以看成是一個線程。

  線程是程式的執行單元、執行路徑,是程式使用CPU的最基本機關。

  

  單線程:程式隻有一條執行路徑

  多線程:程式有多條執行路徑

 

  多線程的意義?

  答:多線程的存在,不是為了提高程式的執行速度,其實是為了提高應用程式的使用率。

   程式的執行其實都是在搶CPU的資源,CPU的執行權。

   多個線程是在搶這個資源,而其中的某一個程序如果執行路徑比較多,就會有更高的幾率搶到CPU的執行權。

   我們不能保證哪一個線程能夠在哪個時刻搶到,是以線程的執行有随機性。

  并行:前者是邏輯上同時發生,指在某一個時間段内同時運作多個程式

  并發:後者是實體上同時發生,指在某一個時間點同時運作多個程式(高并發問題)

3.Java程式運作原理:

  由Java指令啟動JVM,JVM啟動就相當于啟動了一個程序。

  接着該程序建立了一個主線程去調用main方法。

  問:JVM虛拟機的啟動是單線程還是多線程的?

    多線程的

    原因是例如垃圾回收線程也要先啟動,否則很容易出現記憶體溢出。是以是多線程的。

4.如何實作多線程程式

  由于線程依賴于程序而存在,是以要實作多線程必須先建立一個程序。

  而程序是由系統建立的,是以必須去調用系統功能建立一個程序。

  而Java是無法直接調用系統功能的,是以需要去調用底層C/C++寫好的程式來實作多線程。是以Java提供了一些類供我們實作多線程。

5.Thread類:通過檢視API,可以發現有兩種實作多線程方式(實際上三種)

 (1)方式1:繼承Thread類

  步驟:

    A:自定義MyThread類繼承Thread類

    B:MyThread類重寫run()方法

      不是類中的所有代碼都需要被線程執行,為了區分哪些代碼能夠被線程執行,Java提供了Thread類中的run()用來包含那些被線程執行的代碼

    C:建立對象

    D:啟動線程

  注意:

    A:run()方法直接調用的話和普通方法一樣,都是單線程的,無論調用幾次。

    B:為了有多線程效果,應該是調用start()方法開啟線程。start()方法的作用有兩個,1是啟動線程,2是調用run()方法。

    C:一個線程對象的start()方法隻能同時調用一次,要想讓run()方法裡的代碼多線程運作,需要建立多個該線程對象并調用她們的start()方法。

    面試題:run()方法和start()方法有什麼差別?

      run():僅僅是封裝被線程執行的代碼,直接調用是普通方法

      start():首先啟動線程,再由Jvm去調用run()方法

  <1>如何擷取線程對象的名稱?

   public final String getName():擷取線程的名稱

  <2>如何設定線程對象的名稱?

   A(set方法):public final void setName(String name):設定線程的名稱

   B(構造方法):public MyThread(String name) {

           super(name); //Thread類有該方法

           } //重寫有參構造方法

  <3>針對不是Thread類的子類中如何擷取線程對象呢?

   public static Thread currentThread():傳回目前正在執行的線程對象

     用法:Thread.currentThread().getName()

  <4>線程排程

    線程有兩種排程模型:

    分時排程模型 所有線程輪流使用 CPU 的使用權,平均配置設定每個線程占用 CPU 的時間片

    搶占式排程模型 優先讓優先級高的線程使用 CPU,如果線程的優先級相同,那麼會随機選擇一個,優先級高的線程擷取的 CPU 時間片相對多一些。

    Java使用的是搶占式排程模型。

   設定優先級方法:

    public final int getPriority() :擷取線程的優先級數

    public final void setPriority(int newPriority):設定線程的優先級數

     線程預設優先級是5(NORMAL_PRIORITY)

     線程優先級的範圍是1-10(MIN_PRIORITY ~ MAX_PRIORITY)

     注意:線程優先級僅僅代表線程擷取CPU時間片的幾率高,要在次數比較多,多運作幾次的情況下才能看出效果。

     IllegalArgumentException:非法參數異常,表明向方法傳遞了一個不合法或者不正确的參數

  <5>線程控制

   線程休眠:public static void sleep(long millis):在指定的毫秒數内讓目前正在執行的線程休眠(暫停執行),此操作受到系統計時器和排程程式精度和準确性的影響。  

           Thread.sleep(1000); 休眠1秒

   線程加入:public final void join():等待該線程終止,這時候該線程其他線程對象暫停運作,等該線程結束再運作

   線程禮讓:public static void yield():暫停目前正在執行的線程對象,并執行其他線程。 作用是讓多個線程執行更加和諧,但是不能保證每個線程一人一次。

   守護線程:public final void setDaemon(boolean on):将線程标記為守護線程或者使用者線程。當正在運作的線程都是守護線程時,Java虛拟機自動退出。該方法必須線上程啟動前調用。

   中斷線程:public final void stop():讓線程停止,過時了,但是還可以用

        public void interrupt():中斷線程,把線程狀态終止,并抛出一個InterruptedException異常

  <6>線程的生命周期(4個狀态):

    建立:建立線程對象

    就緒:有執行資格,沒有執行權

    運作:有執行資格,有執行權

       阻塞:由于一些操作讓線程處于該狀态,沒有執行資格,沒有執行權

          而另一些操作可以激活線程,此時它處于就緒狀态

    死亡:線程對象變成垃圾,等待被回收。

     

JAVA學習筆記——多線程

 (2)方式二:實作Runnable接口

    步驟:

     A:自定義MyRunnable類實作接口Runnable接口

     B:重寫run()方法

     C:建立MyRunnable類的對象

     D:建立Thread類的對象,并把C步驟的對象作為構造參數傳遞

面試題:為什麼有了第一種方式還要來第二種方式?

  A:可以避免java單繼承帶來的局限性

  B:第二種方式适合多個相同程式的代碼

5.線程安全性

  隻要線程裡的操作不是原子性的(不可拆分的),那麼在對線程進行延遲操作(sleep())時,就有可能存在安全性問題。

  <1>解決線程安全問題的基本思想

    首先想為什麼出現問題?(也是我們判斷是否有問題的标準)

      A:是否是多線程環境

      B:是否有共享資料

      C:是否有多條語句操作共享資料

    如何解決多線程安全問題呢?

      基本思想:讓程式沒有安全問題的環境。

    怎麼實作呢?

      把多個語句操作共享資料的代碼給鎖起來,讓任意時刻隻能有一個線程執行即可。(同步)

  

  <2>同步代碼塊

    A:格式:

      synchronized(對象){

       需要同步的代碼;

      }

    B:同步可以解決安全問題的根本原因就在那個對象上。該對象如同鎖的功能。每次隻能有一個程序同時通路該同步代碼塊。

    C:同步代碼塊的鎖對象可以是 任意對象 

    D:同步方法的格式以及鎖對象問題

       同步方法是指把同步關鍵字加在方法上 private synchronized void method(){}

       同步方法的鎖是什麼呢? —— this ,因為每個方法都自帶一個隐藏對象this

    E:靜态方法及鎖對象問題

       靜态方法的鎖對象是什麼呢?———類的位元組碼檔案 類.class

  <3>同步的特點

    同步的前提:存在多個線程

      解決問題的時候要注意:多個線程使用的是同一個鎖對象

    同步的好處:同步的出現解決了多線程的安全問題。

    同步的弊端:當線程相當多時,因為每個線程都會去判斷同步上的鎖,這是很耗費資源的,無形中會降低程式的運作效率。

  <4>線程安全類(同步)

    StringBuffer sb = new StringBuffer() ;

    Vector<String> v = new Vector<String>() ; //我們并不用它

    Hashtable<String, String> h = new Hashtable<String, String>() ;

    //如何建立安全的List?

    List<String> list1 = new ArrayList<String>() ; //線程不安全

    List<String> list2 = Collection.synchronizedList(new ArrayList<String>()) ; //線程安全

6.Lock鎖

  雖然已經可以了解同步代碼塊和同步方法的鎖對象的問題,但是我們并沒有直接看到在哪邊加上了鎖,在哪裡釋放了鎖。

  為了更清晰的表達如何加鎖和釋放鎖,JDK5提供了一個新的鎖對象Lock。

  加鎖:void lock():擷取鎖

  解鎖:void lock():釋放鎖

  

7.死鎖

  (1)同步弊端

    A:效率低

    B:如果出現了同步嵌套,就容易産生死鎖問題

  (2)死鎖問題及其代碼

    是指兩個或者兩個以上的線程在執行的過程中,因争奪資源産生的一種互相等待現象

8.線程間的通信:不同種類的線程針對同一個資源的操作

  當不同種類的線程針對同一個資源進行操作的時候容易出現線程安全問題

  解決方案:

    A:不同種類的線程都要加鎖

    B:不同種類的線程加的鎖要是同一把(對象是同一個)

    

JAVA學習筆記——多線程

    等待喚醒:

     Object類中提供了三種方法:(設定一個flag來判斷資源狀态)

       wait():等待,線程立即釋放鎖

       notify():喚醒單個線程,并不代表可以立馬執行,還得搶CPU資源

       notify():喚醒所有線程

     為什麼這些方法不定義在Thread中呢?

       因為這些方法的調用必須通過鎖對象調用,而我們剛才試用的鎖對象是任意鎖對象

       是以這些方法必須定義在Object類裡。

9.線程組:把多個線程組合到一起,它可以對一批線程進行分組管理,Java允許程式直接對線程組進行控制。

  線程預設的線程組是main

  構造方法:

    ThreadGroup(String name) :構造一個新線程組。

  方法:

     String getName() :傳回此線程組的名稱。

     void setDaemon(boolean daemon) :設定此線程組的所有線程為守護線程(背景線程)

     void destroy() :銷毀此線程組及其所有子組。

  

10.線程池

  程式啟動一個新線程成本是比較高的,因為它涉及到要與作業系統進行互動。而使用線程池可以很好的提高性能,尤其是當程式中要建立大量生存期很短的線程時,更應該考慮使用線程池。

  線程池裡的每一個線程代碼結束後,并不會死亡,而是再次回到線程池中成為空閑狀态,等待下一個對象來使用。

  在JDK5之前,我們必須手動實作自己的線程池,從JDK5開始,Java内置支援線程池

  JDK5新增了一個Executors工廠類來産生線程池,有如下幾個方法

    public static ExecutorService newCachedThreadPool()

    public static ExecutorService newFixedThreadPool(int nThreads)

    public static ExecutorService newSingleThreadExecutor()

  這些方法的傳回值是ExecutorService對象,該對象表示一個線程池,可以執行Runnable對象或者Callable對象代表的線程。它提供了如下方法

  Future< T> submit(Runnable task)

  < T> Future< T> submit(Callable< T> task)

  

  (1)如何實作線程池?

   A:建立一個線程池對象,控制要建立幾個線程對象

   public static ExecutorService newFixedThreadPool(int nThreads):nThreads表示幾個線程

   B:這種線程池的線程可以執行Runnable對象或者Callable對象代表的線程

     即 做一個類實作Runnable接口

   C:調用如下方法,添加線程到線程池

   Future< T> submit(Runnable task)

   D:結束線程池(如果不結束線程池,線程池裡的線程隻是閑置,但會一直存在)

    void shutdown() : 啟動一次順序關閉,執行以前送出的任務,但不接受新任務。

//建立一個線程池對象
        ExecutorService pool = Executors.newFixedThreadPool() ;

        //調用submit()方法
        pool.submit(new MyRunnable()) ;
        pool.submit(new MyRunnable()) ;

        //結束線程
        pool.shutdown();
           

11.方式3:Callable方式

  帶泛型傳回值的多線程方式,但是它僅限于線程池存在。

  步驟和剛才示範線程池執行Runnable對象的差不多。

  好處:

    A:可以有傳回值

    B:可以抛出異常

  弊端:

    A:代碼比較複雜,是以一般不用

   submit()方法傳回的Future< T> 這個中括号裡面的T是傳回值的類型,該傳回值可通過Future對象調用submit()方法來得到。

   Future< Integer> f1 = pool.submit(new MyCallable(100)) ;

   Integer i = f1.get() ;

12.匿名内部類方式使用多線程,如下:

/*
         * 繼承Thread類方式
         */
        new Thread() {
            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            };
        }.start();

        /*
         * 實作Runnable接口
         */
        new Thread(new Runnable() {

            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            }
        }) {
        }.start();

        /*
         * 面試題:這種情況下會報錯嗎?不會的話是運作誰的start()方法?
         */
        new Thread(new Runnable() {

            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            }
        }) {
            public void run() {
                for (int i = ; i < ; i++) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + i);
                }
            };
        }.start();
           

13.定時器

  依賴Timer和TimerTask這兩個類:

  Timer:定時

    public Timer():建立一個新計時器。相關的線程不 作為守護程式運作。

    public void schedule(TimerTask task , long delay):安排在指定延遲後執行指定的任務。(delay作為毫秒值,1000是1秒,即1秒後執行task任務)

    public void schedule(TimerTask task, long delay, long period):安排指定的任務從指定的延遲後開始進行重複的固定延遲執行。(第一次執行在delay時候執行任務task,然後在每隔period後執行一次task)

    public void cancel():終止此計時器,丢棄所有目前已安排的任務。

  TimerTask:任務

14.多線程常見面試題

  (1)多線程有幾種實作方案?分别是哪幾種?

    兩種。

    繼承Thread類

    實作Runnable接口

    擴充一種:實作Callable接口。這個得和線程池結合。

   (2):同步有幾種方式,分别是什麼?

    兩種。

    同步代碼塊

    同步方法

    

   (3):啟動一個線程是run()還是start()?它們的差別?

     run():封裝了被線程執行的代碼,直接調用僅僅是普通方法的調用

     start():啟動線程,并由JVM自動調用run()方法

   (4):sleep()和wait()方法的差別

     sleep():必須指定時間;不釋放鎖。

     wait():可以不指定時間,也可以指定時間;釋放鎖。

   (5):為什麼wait(),notify(),notifyAll()等方法都定義在Object類中

     因為這些方法的調用是依賴于鎖對象的,而同步代碼塊的鎖對象是任意鎖。

    而Object代碼任意的對象,是以,定義在這裡面。

   (6):線程的生命周期圖

     建立 – 就緒 – 運作 – 死亡

     建立 – 就緒 – 運作 – 阻塞 – 就緒 – 運作 – 死亡

     建議:畫圖解釋。

繼續閱讀