Java線程彙總

一、多線程介紹

多線程優勢

  1. 資源利用率好java

  2. 程序設計簡單算法

  3. 服務器響應更快數據庫

多線程缺點

  1. 設計更復雜編程

  2. 上下文切換的開銷緩存

  3. 增長資源消耗
    線程須要內存維護本地的堆棧,同時須要操做系統資源管理線程。安全

二、併發模型

併發系統能夠有多種併發模型,不一樣的併發模型在處理任務時,線程間的協做和交互的方式也不一樣。服務器

並行工做者

委託者將任務分配到不一樣的現場去執行,每一個工做者完成整個任務。工做者們並行運做在不一樣的線程上,甚至可能在不一樣的CPU上。如圖所示:
image_1bbo07gk5l631v4rb0n1vmu6389.png-13.8kB網絡

優勢:很容易理解和使用。
缺點:數據結構

  • 共享狀態會很複雜
    image_1bbo0g2541apr12q2eaa1fbie0cm.png-29.7kB多線程

共享的工做者常常須要訪問一些共享數據,不管是內存中的或者共享的數據庫中的。

在並行工做者模型中,線程須要以某種方式存取共享數據,以確保某個線程的修改可以對其餘線程可見。線程須要避免竟態,死鎖以及不少其餘共享狀態的併發性問題。

  • 無狀態的工做者
    共享狀態可以被系統中得其餘線程修改。因此工做者在每次須要的時候必須重讀狀態,以確保每次都能訪問到最新的副本,無論共享狀態是保存在內存中的仍是在外部數據庫中。工做者沒法在內部保存這個狀態(可是每次須要的時候能夠重讀)稱爲無狀態的。

每次都重讀須要的數據,將會致使速度變慢,特別是狀態保存在外部數據庫中的時候。

  • 任務順序是不肯定的
    做業執行順序是不肯定的。沒法保證哪一個做業最早或者最後被執行。

流水線模式

相似於工廠中生產線上的工人們那樣組織工做者。每一個工做者只負責做業中的部分工做。當完成了本身的這部分工做時工做者會將做業轉發給下一個工做者。每一個工做者在本身的線程中運行,而且不會和其餘工做者共享狀態。有時也被成爲無共享並行模型
image_1bbo0tfnh1dva6pk12jb8kjkcg13.png-6.7kB

一般使用非阻塞的IO來設計使用流水線併發模型的系統。非阻塞IO就是,一旦某個工做者開始一個IO操做的時候(好比讀取文件或從網絡鏈接中讀取數據),這個工做者不會一直等待IO操做的結束。IO操做速度很慢,因此等待IO操做結束很浪費CPU時間。此時CPU能夠作一些其餘事情。當IO操做完成的時候,IO操做的結果(好比讀出的數據或者數據寫完的狀態)被傳遞給下一個工做者。

image_1bbo143c1101e96r1jrrfuc1e0j1g.png-8.2kB

在實際過程當中,可能會是這樣:
image_1bbo18uoof41gqh1l6d1kv82111t.png-14.2kB

也多是這樣:
image_1bbo19kef1cuni78l031e9e6gi2a.png-14.9kB

固然還會有更復雜的設計,……

缺點: 代碼編寫複雜,追蹤某個做業到底被什麼代碼執行難度較大。
優勢:

  • 無需共享的狀態

工做者之間無需共享狀態,無需考慮全部因併發訪問共享對象而產生的併發性問題,基本上是一個單線程的實現。

  • 有狀態的工做者

當工做者知道了沒有其餘線程能夠修改它們的數據,工做者能夠變成有狀態的。對於有狀態,是指,能夠在內存中保存它們須要操做的數據,只需在最後將更改寫回到外部存儲系統。所以,有狀態的工做者一般比無狀態的工做者具備更高的性能。

  • 較好的硬件整合(Hardware Conformity)

當能肯定代碼只在單線程模式下執行的時候,一般可以建立更優化的數據結構和算法。單線程有狀態的工做者可以在內存中緩存數據,訪問緩存的數據變得更快。

  • 合理的做業順序

基於流水線併發模型實現的併發系統,在某種程度上是有可能保證做業的順序的。做業的有序性使得它更容易地推出系統在某個特定時間點的狀態。更進一步,你能夠將全部到達的做業寫入到日誌中去。一旦這個系統的某一部分掛掉了,該日誌就能夠用來重頭開始重建系統當時的狀態。按照特定的順序將做業寫入日誌,並按這個順序做爲有保障的做業順序。

Actors

在Actor模型中每一個工做者被稱爲actor。Actor之間能夠直接異步地發送和處理消息。Actor能夠被用來實現一個或多個像前文描述的那樣的做業處理流水線。下圖給出了Actor模型:
image_1bbo1f6vs1m7a1v3a5o0pufdq42n.png-9.8kB

Channels

工做者之間不直接進行通訊。相反,它們在不一樣的通道中發佈本身的消息(事件)。其餘工做者們能夠在這些通道上監聽消息,發送者無需知道誰在監聽。下圖給出了Channel模型:

image_1bbo1h1te7s11ca61qps1f8l2e834.png-14.1kB

channel模型對於來講彷佛更加靈活。一個工做者無需知道誰在後面的流水線上處理做業。只需知道做業(或消息等)須要轉發給哪一個通道。通道上的監聽者能夠隨意訂閱或者取消訂閱,並不會影響向這個通道發送消息的工做者。這使得工做者之間具備鬆散的耦合。

三、實現多線程方式

多線程實現方法有兩種:

  • 繼承Thread類

public class MyThread extends Thread {
   public void run(){
     System.out.println("MyThread running");
   }
}

//調用
MyThread myThread = new MyThread();
myTread.start();
  • 實現Runnble接口

public class MyRunnable implements Runnable {
   public void run(){
    System.out.println("MyRunnable running");
   }
}

//調用
Thread thread = new Thread(new MyRunnable());
thread.start();

實現Runnble接口比Thread類的優點:

  • 能夠避免Java單繼承帶來的侷限

  • 加強程序健壯性,可以被多個線程共享,代碼和數據是獨立的

  • 適合多個相同程序代碼的線程區處理同一資源

Thread中,start和run的區別:run是在當前線程運行,start是開闢新的線程運行!因此通常狀況下使用的是start!
執行完run()方法後,或在run()方法中return,線程便天然消亡。

線程中斷

當一個線程運行時,另外一個線程能夠調用對應的 Thread 對象的 interrupt()方法來中斷它,該方法只是在目標線程中設置一個標誌,表示它已經被中斷,並當即返回。這裏須要注意的是,若是隻是單純的調用 interrupt()方法,線程並無實際被中斷,會繼續往下執行。

sleep()方法的實現檢查到休眠線程被中斷,它會至關友好地終止線程,並拋出 InterruptedException 異常。

public class SleepInterrupt extends Object implements Runnable{  
    public void run(){  
        try{  
            System.out.println("in run() - about to sleep for 20 seconds");  
            Thread.sleep(20000);  
            System.out.println("in run() - woke up");  
        }catch(InterruptedException e){  
            System.out.println("in run() - interrupted while sleeping");  
            //處理完中斷異常後,返回到run()方法人口,  
            //若是沒有return,線程不會實際被中斷,它會繼續打印下面的信息  
            return;    
        }  
        System.out.println("in run() - leaving normally");  
    }  

    public static void main(String[] args) {  
        SleepInterrupt si = new SleepInterrupt();  
        Thread t = new Thread(si);  
        t.start();  
        //主線程休眠2秒,從而確保剛纔啓動的線程有機會執行一段時間  
        try {  
            Thread.sleep(2000);   
        }catch(InterruptedException e){  
            e.printStackTrace();  
        }  
        System.out.println("in main() - interrupting other thread");  
        //中斷線程t  
        t.interrupt();  
        System.out.println("in main() - leaving");  
    }  
}

若是將 catch 塊中的 return 語句註釋掉,則線程在拋出異常後,會繼續往下執行,而不會被中斷,從而會打印出leaving normally信息。

待決中斷

另一種狀況,若是線程在調用 sleep()方法前被中斷,那麼該中斷稱爲待決中斷,它會在剛調用 sleep()方法時,當即拋出 InterruptedException 異常。

public class PendingInterrupt extends Object {  
    public static void main(String[] args){  
        //若是輸入了參數,則在mian線程中中斷當前線程(亦即main線程)  
        if( args.length > 0 ){  
            Thread.currentThread().interrupt();  
        }   
        //獲取當前時間  
        long startTime = System.currentTimeMillis();  
        try{  
            Thread.sleep(2000);  
            System.out.println("was NOT interrupted");  
        }catch(InterruptedException x){  
            System.out.println("was interrupted");  
        }  
        //計算中間代碼執行的時間  
        System.out.println("elapsedTime=" + ( System.currentTimeMillis() - startTime));  
    }  
}

這種模式下,main 線程中斷它自身。除了將中斷標誌(它是 Thread 的內部標誌)設置爲 true 外,沒有其餘任何影響。線程被中斷了,但 main 線程仍然運行,main 線程繼續監視實時時鐘,並進入 try 塊,一旦調用 sleep()方法,它就會注意到待決中斷的存在,並拋出 InterruptException。

中斷狀態判斷

  • isInterrupted()方法判斷是否中斷

  • Thread.interrupted()方法判斷中斷狀態

join & yield

join 方法用線程對象調用,若是在一個線程 A 中調用另外一個線程 B 的 join 方法,線程 A 將會等待線程 B 執行完畢後再執行。

yield 能夠直接用 Thread 類調用,yield 讓出 CPU 執行權給同等級的線程,若是沒有相同級別的線程在等待 CPU 的執行權,則該線程繼續執行。

守護線程

Java有兩類線程:UserThread(用戶線程)、Daemon Thread(守護線程)。
用戶線程在前臺,守護線程在後臺運行,爲其餘前臺線程提供服務。當全部前臺線程都退出時,守護線程就會退出。若是有前臺線程仍然存活,守護線程就不會退出。
守護線程並不是只有虛擬機內部提供,用戶可使用Thread.setDaemon(true)方法設置爲當前線程爲守護線程。

  • setDaemon(true)必須在調用的線程的start()方法以前設置,不然會拋出異常。

  • 在守護線程中產生的新線程也是守護線程

線程阻塞

線程在如下四種狀態下會產生阻塞:

  1. 執行Thread.sleep()

  2. 當線程碰見wait()語句,它會一直阻塞到接到通知notify()

  3. 線程阻塞與不一樣的I/O的方式有多種。例:InputStreamread方法,一直阻塞到從流中讀取一個字節的數據爲知。

  4. 線程阻塞等待獲取某個對象鎖的訪問權限。

四、線程安全

定義:當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼這個類就是線程安全的!

競態條件 & 臨界區

當兩個線程競爭同一資源時,若是對資源的訪問順序敏感,就稱存在競態條件。
致使競態條件發生的代碼區稱做:臨界區。

下例中add()方法就是一個臨界區,它會產生競態條件。在臨界區中使用適當的同步就能夠避免競態條件。

public class Counter {
    protected long count = 0;
    public void add(long value){
        this.count = this.count + value;   
    }
}

數據安全

線程逃逸規則:若是一個資源的建立,使用,銷燬都在同一個線程內完成,且永遠不會脫離該線程的控制,則該資源的使用就是線程安全的。

屬性 描述 是否線程安全
局部變量 在棧中,不會被線程共享 線程安全
局部對象 引用所指的對象都存在共享堆中,對象不會被其它方法得到,也不會被非局部變量引用到 線程安全
對象成員 多個線程執行讀操做,或者每一個線程的對象都相互獨立 線程安全
局部對象 對象會被其它方法得到,或者被全局變量引用到 線程非安全
對象成員 存儲在堆上。若多個線程同時更新同一個對象的同一個成員 線程非安全

線程安全

當多個線程同時訪問同一個資源,而且其中的一個或者多個線程對這個資源進行了寫操做,纔會產生競態條件。多個線程同時讀同一個資源不會產生競態條件。

咱們能夠經過建立不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現線程安全,以下所示:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

若是非要對ImmutableValue進行操做的話,能夠建立新的實例進行隔離:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    //建立一個新的實例
    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    }
}

ImmutableValue能夠看作是線程安全的,可是若是別的類引用了ImmutableValue,就不能保證線程安全了。以下所示:

public void Calculator{
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue(){
        return currentValue;
    }

    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue;
    }

    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

即便Calculator類內部使用了一個不可變對象,但Calculator類自己仍是可變的,所以Calculator類不是線程安全的。換句話說:ImmutableValue類是線程安全的,但使用它的類不是。

五、同步(synchronized)

當多個線程訪問某個狀態變量,而且有線程執行寫入操做時,必須採用同步機制來協同這些線程對變量的訪問。

Java的主要同步機制有:

  1. synchronized關鍵字

  2. volatile類型變量

  3. 顯示鎖

  4. 原子變量

不管是同步方法,仍是同步塊都是隻針對同一個對象的多線程而言的,只有同一個對象產生的多線程,纔會考慮到同步方法或者是同步塊。

實例方法

Java實例方法同步是同步在對象上。這樣,每一個方法同步都同步在方法所屬的實例。只有一個線程可以在實例方法同步塊中運行。若是有多個實例存在,那麼一個線程一次能夠在一個實例同步塊中執行操做。一個實例一個線程。

public synchronized void add(int value){
    this.count += value;
 }

靜態方法同步

靜態方法的同步是指同步在該方法所在的類對象上。由於在Java虛擬機中一個類只能對應一個類對象,因此同時只容許一個線程執行同一個類中的靜態同步方法。

對於不一樣類中的靜態同步方法,一個線程能夠執行每一個類中的靜態同步方法而無需等待。無論類中的那個靜態同步方法是否被調用,一個類只能由一個線程同時執行。

public static synchronized void add(int value){
    count += value;
}

實例方法中的同步塊

有時你不須要同步整個方法,而是同步方法中的一部分。

public void add(int value){
    synchronized(this){
       this.count += value;
    }
}

示例使用Java同步塊構造器來標記一塊代碼是同步的。該代碼在執行時和同步方法同樣。在上例中,使用了「this」,即爲調用add方法的實例自己。在同步構造器中用括號括起來的對象叫作監視器對象。

靜態方法中的同步塊

和上面相似,下面是兩個靜態方法同步的例子。這些方法同步在該方法所屬的類對象上。

public class MyClass {
    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2);
       }
    }
  }

這兩個方法不容許同時被線程訪問。
若是第二個同步塊不是同步在MyClass.class這個對象上。那麼這兩個方法能夠同時被線程訪問。

六、線程通訊

線程通訊的目標是使線程間可以互相發送信號。另外一方面,線程通訊使線程可以等待其餘線程的信號。

經過共享對象通訊

線程間發送信號的一個簡單方式是在共享對象的變量裏設置信號值。

public class MySignal{
  protected boolean hasDataToProcess = false;

  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }

  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;
  }
}

線程A在一個同步塊裏設置boolean型成員變量hasDataToProcess爲true,線程B也在同步塊裏讀取hasDataToProcess這個成員變量。
線程A和B必須得到指向一個MySignal共享實例的引用,以便進行通訊。若是它們持有的引用指向不一樣的MySingal實例,那麼彼此將不能檢測到對方的信號。

忙等待(Busy Wait)

線程B運行在一個循環裏,等待線程A的一個可執行的信號。

protected MySignal sharedSignal = ...

...
while(!sharedSignal.hasDataToProcess()){
   //do nothing... busy waiting
}

wait(),notify()和notifyAll()

除非忙等待的時間特別短,不然會浪費CPU資源。合理的作法:讓等待線程進入睡眠或者非運行狀態,直到它接收到它等待的信號。

java.lang.Object 類定義了三個方法,wait()、notify()和notifyAll()來實現這個等待機制。

一個線程一旦調用了任意對象的wait()方法,就會變爲非運行狀態,直到另外一個線程調用了同一個對象的notify()方法。

爲了調用wait()或者notify(),線程必須先得到那個對象的鎖。也就是說,線程必須在同步塊裏調用wait()或者notify()。

在wait()/notify()機制中,不要使用全局對象,字符串常量等。應該使用對應惟一的對象

public class MonitorObject{
}

public class MyWaitNotify{

  MonitorObject myMonitorObject = new MonitorObject();

  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}

無論是等待線程仍是喚醒線程都在同步塊裏調用wait()和notify()。這是強制性的!一個線程若是沒有持有對象鎖,將不能調用wait(),notify()或者notifyAll()。不然,會拋出IllegalMonitorStateException異常。

一旦線程調用了wait()方法,它就釋放了所持有的監視器對象上的鎖。這將容許其餘線程也能夠調用wait()或者notify()。

被喚醒的線程必須從新得到監視器對象的鎖,才能夠退出wait()的方法調用,由於wait方法調用運行在同步塊裏面。若是多個線程被notifyAll()喚醒,那麼在同一時刻將只有一個線程能夠退出wait()方法,由於每一個線程在退出wait()前必須得到監視器對象的鎖。

丟失信號

notify()和notifyAll()方法不會保存調用它們的方法,若是方法被調用時,沒有線程處於等待狀態。通知信號事後便丟棄了。所以,若是一個線程先於被通知線程調用wait()前調用了notify(),等待的線程將錯過這個信號。在某些狀況下,這可能使線程錯過了喚醒信號,永遠在等待再也不醒來。

爲了不丟失信號,必須把它們保存在信號類裏。在MyWaitNotify的例子中,通知信號應被存儲在MyWaitNotify實例的一個成員變量裏。

public class MyWaitNotify2{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

在上述例子中,doNotify()方法在調用notify()前把wasSignalled變量設爲true。同時,留意doWait()方法在調用wait()前會檢查wasSignalled變量。

爲了不信號丟失,用一個變量來保存是否被通知過。在notify前,設置本身已經被通知過。在wait後,設置本身沒有被通知過,須要等待通知。。

假喚醒

線程有可能在沒有調用過notify()和notifyAll()的狀況下醒來。這就是所謂的假喚醒(spurious wakeups)。等待線程即便沒有收到正確的信號,也可以執行後續的操做。

爲了防止假喚醒,保存信號的成員變量將在一個while循環裏接受檢查,而不是在if表達式裏。這樣的一個while循環叫作自旋鎖。

public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

若是等待線程沒有收到信號就喚醒,wasSignalled變量將變爲false,while循環會再執行一次,促使醒來的線程回到等待狀態。

目前的JVM實現自旋會消耗CPU,若是長時間不調用doNotify方法,doWait方法會一直自旋,CPU會消耗太大。

七、TheadLocal

ThreadLocal類建立的變量只被同一個線程進行讀和寫操做。所以,儘管有兩個線程同時執行一段相同的代碼,並且這段代碼又有一個指向同一個ThreadLocal變量的引用,可是這兩個線程依然不能看到彼此的ThreadLocal變量域。

//建立一個ThreadLocal變量:每一個線程僅須要實例化一次便可。
//每一個線程只能看到私有的ThreadLocal實例,不一樣的線程在給ThreadLocal對象設置不一樣的值,也不能看到彼此的修改。
private ThreadLocal myThreadLocal = new ThreadLocal();

//設置、獲取數據
myThreadLocal.set("A thread local value");
String threadLocalValue = (String) myThreadLocal.get();

//建立泛型對象
private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();

myThreadLocal1.set("Hello ThreadLocal");
String threadLocalValues = myThreadLocal.get();

InheritableThreadLocal類是ThreadLocal的子類。爲了解決ThreadLocal實例內部每一個線程都只能看到本身的私有值,因此InheritableThreadLocal容許一個線程建立的全部子線程訪問其父線程的值。


引用

一、併發編程網-Java併發性和多線程
二、蘭亭風雨專欄

相關文章
相關標籤/搜索