[高併發Java 二] 多線程基礎

1. 什麼是線程

線程是進程內的執行單元 java

某個進程當中都有若干個線程。 python

線程是進程內的執行單元。 設計模式

使用線程的緣由是,進程的切換是很是重量級的操做,很是消耗資源。若是使用多進程,那麼併發數相對來講不會很高。而線程是更細小的調度單元,更加輕量級,因此線程會較爲普遍的用於併發設計。 安全

在Java當中線程的概念和操做系統級別線程的概念是相似的。事實上,Jvm將會把Java中的線程映射到操做系統的線程區。 多線程

2. 線程的基本操做

2.1 線程狀態圖

上圖是Java中線程的基本操做。 併發

當new出一個線程時,其實線程並無工做。它只是生成了一個實體,當你調用這個實例的start方法時,線程才真正地被啓動。啓動後到Runnable狀態,Runnable表示該線程的資源等等已經被準備好,已經能夠執行了,可是並不表示必定在執行狀態,因爲時間片輪轉,該線程也可能此時並無在執行。對於咱們來講,該線程能夠認爲已經被執行了,可是是否真實執行,還得看物理cpu的調度。當線程任務執行結束後,線程就到了Terminated狀態。 jvm

有時候在線程的執行當中,不可避免的會申請某些鎖或某個對象的監視器,當沒法獲取時,這個線程會被阻塞住,會被掛起,到了Blocked狀態。若是這個線程調用了wait方法,它就處於一個Waiting狀態。進入Waiting狀態的線程會等待其餘線程給它notify,通知到以後由Waiting狀態又切換到Runnable狀態繼續執行。固然等待狀態有兩種,一種是無限期等待,直到被notify。一直則是有限期等待,好比等待10秒仍是沒有被notify,則自動切換到Runnable狀態 ide

2.2 新建線程

Thread thread = new Thread();
thread.start();
這樣就開啓了一個線程。

有一點須要注意的是 函數

Thread thread = new Thread();
thread.run();
直接調用run方法是沒法開啓一個新線程的。

start方法實際上是在一個新的操做系統線程上面去調用run方法。換句話說,直接調用run方法而不是調用start方法的話,它並不會開啓新的線程,而是在調用run的當前的線程當中執行你的操做。 高併發

Thread thread = new Thread("t1")
{
	@Override
	public void run()
	{
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName());
	}
};
thread.start();
若是調用start,則輸出是t1
Thread thread = new Thread("t1")
{
	@Override
	public void run()
	{
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName());
	}
};
thread.run();
若是是run,則輸出main。(直接調用run其實就是一個普通的函數調用而已,並無達到多線程的做用)

run方法的實現有兩種方式

第一種方式,直接覆蓋run方法,就如剛剛代碼中所示,最方便的用一個匿名類就能夠實現。

Thread thread = new Thread("t1")
{
	@Override
	public void run()
	{
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName());
	}
};
第二種方式
Thread t1=new Thread(new CreateThread3());
CreateThread3()實現了Runnable接口。

在張孝祥的視頻中,推薦第二種方式,稱其更加面向對象。

2.3 終止線程

  • Thread.stop() 不推薦使用。它會釋放全部monitor

在源碼中已經明確說明stop方法被Deprecated,在Javadoc中也說明了緣由。

緣由在於stop方法太過"暴力"了,不管線程執行到哪裏,它將會當即中止掉線程。

當寫線程獲得鎖之後開始寫入數據,寫完id = 1,在準備將name = 1時被stop,釋放鎖。讀線程得到鎖進行讀操做,讀到的id爲1,而name仍是0,致使了數據不一致。

最重要的是這種錯誤不會拋出異常,將很難被發現。

2.4 線程中斷

線程中斷有3種方法

public void Thread.interrupt() // 中斷線程 
public boolean Thread.isInterrupted() // 判斷是否被中斷 
public static boolean Thread.interrupted() // 判斷是否被中斷,並清除當前中斷狀態
什麼是線程中斷呢?

若是不瞭解Java的中斷機制,這樣的一種解釋極容易形成誤解,認爲調用了線程的interrupt方法就必定會中斷線程。 
其實,Java的中斷是一種協做機制。也就是說調用線程對象的interrupt方法並不必定就中斷了正在運行的線程,它只是要求線程本身在合適的時機中斷本身。每一個線程都有一個boolean的中斷狀態(不必定就是對象的屬性,事實上,該狀態也確實不是Thread的字段),interrupt方法僅僅只是將該狀態置爲true對於非阻塞中的線程, 只是改變了中斷狀態, 即Thread.isInterrupted()將返回true,並不會使程序中止; 

public void run(){//線程t1
   while(true){
      Thread.yield();
   }
}
t1.interrupt();
這樣使線程t1中斷,是不會有效果的,只是更改了中斷狀態位。

若是但願很是優雅地終止這個線程,就該這樣作

public void run(){ 
    while(true)
    { 
        if(Thread.currentThread().isInterrupted())
        { 
           System.out.println("Interruted!"); 
           break; 
        } 
        Thread.yield(); 
    } 
}

使用中斷,就對數據一致性有了必定的保證。

對於可取消的阻塞狀態中的線程, 好比等待在這些函數上的線程, Thread.sleep(), Object.wait(), Thread.join(), 這個線程收到中斷信號後, 會拋出InterruptedException, 同時會把中斷狀態置回爲false.

對於取消阻塞狀態中的線程,能夠這樣抒寫代碼:

public void run(){
    while(true){
        if(Thread.currentThread().isInterrupted()){
            System.out.println("Interruted!");
            break;
        }
        try {
           Thread.sleep(2000);
        } catch (InterruptedException e) {
           System.out.println("Interruted When Sleep");
           //設置中斷狀態,拋出異常後會清除中斷標記位
           Thread.currentThread().interrupt();
        }
        Thread.yield();
    }
}

2.5 線程掛起

掛起(suspend)和繼續執行(resume)線程

  • suspend()不會釋放鎖
  • 若是加鎖發生在resume()以前 ,則死鎖發生

這兩個方法都是Deprecated方法,不推薦使用。

緣由在於,suspend不釋放鎖,所以沒有線程能夠訪問被它鎖住的臨界區資源,直到被其餘線程resume。由於沒法控制線程運行的前後順序,若是其餘線程的resume方法先被運行,那則後運行的suspend,將一直佔有這把鎖,形成死鎖發生。

用如下代碼來模擬這個場景

package test;

public class Test
{
	static Object u = new Object();
	static TestSuspendThread t1 = new TestSuspendThread("t1");
	static TestSuspendThread t2 = new TestSuspendThread("t2");

	public static class TestSuspendThread extends Thread
	{
		public TestSuspendThread(String name)
		{
			setName(name);
		}

		@Override
		public void run()
		{
			synchronized (u)
			{
				System.out.println("in " + getName());
				Thread.currentThread().suspend();
			}
		}
	}

	public static void main(String[] args) throws InterruptedException
	{
		t1.start();
		Thread.sleep(100);
		t2.start();
		t1.resume();
		t2.resume();
		t1.join();
		t2.join();
	}
}
讓t1,t2同時爭奪一把鎖,爭奪到的線程suspend,而後再resume,按理來講,應該某個線程爭奪後被resume釋放了鎖,而後另外一個線程爭奪掉鎖,再被resume。

結果輸出是:

in t1
in t2
說明兩個線程都爭奪到了鎖,可是控制檯的紅燈仍是亮着的,說明t1,t2必定有線程沒有執行完。咱們dump出堆來看看


發現t2一直被suspend。這樣就形成了死鎖。

2.6 join和yeild

yeild是個native靜態方法,這個方法是想把本身佔有的cpu時間釋放掉,而後和其餘線程一塊兒競爭(注意yeild的線程仍是有可能爭奪到cpu,注意與sleep區別)。在javadoc中也說明了,yeild是個基本不會用到的方法,通常在debug和test中使用。

join方法的意思是等待其餘線程結束,就如suspend那節的代碼,想讓主線程等待t1,t2結束之後再結束。沒有結束的話,主線程就一直阻塞在那裏。

package test;

public class Test
{
	public volatile static int i = 0;

	public static class AddThread extends Thread
	{
		@Override
		public void run()
		{
			for (i = 0; i < 10000000; i++)
				;
		}
	}

	public static void main(String[] args) throws InterruptedException
	{
		AddThread at = new AddThread();
		at.start();
		at.join();
		System.out.println(i);
	}
}
若是把上述代碼的at.join去掉,則主線程會直接運行結束,i的值會很小。若是有join,打印出的i的值必定是10000000。

那麼join是怎麼實現的呢?

join的本質 

while(isAlive()) 
{ 
   wait(0); 
}
join()方法也能夠傳遞一個時間,意爲有限期地等待,超過了這個時間就自動喚醒。

這樣就有一個問題,誰來notify這個線程呢,在thread類中沒有地方調用了notify?

在javadoc中,找到了相關解釋。當一個線程運行完成終止後,將會調用notifyAll方法去喚醒等待在當前線程實例上的全部線程,這個操做是jvm本身完成的。

因此javadoc中還給了咱們一個建議,不要使用wait和notify/notifyall在線程實例上。由於jvm會本身調用,有可能與你調用指望的結果不一樣。

3. 守護線程

  • 在後臺默默地完成一些系統性的服務,好比垃圾回收線程、JIT線程就能夠理解爲守護線程。
  • 當一個Java應用內,全部非守護進程都結束時,Java虛擬機就會天然退出。

此前有寫過一篇python中如何實現,查看這裏

而Java中變成守護進程就相對簡單了。

Thread t=new DaemonT(); 
t.setDaemon(true); 
t.start();
這樣就開啓了一個守護線程。
package test;

public class Test
{
	public static class DaemonThread extends Thread
	{
		@Override
		public void run()
		{
			for (int i = 0; i < 10000000; i++)
			{
				System.out.println("hi");
			}
		}
	}

	public static void main(String[] args) throws InterruptedException
	{
		DaemonThread dt = new DaemonThread();
		dt.start();
	}
}
當線程dt不是一個守護線程時,在運行後,咱們能看到控制檯輸出hi

當在start以前加入

dt.setDaemon(true);
控制檯就直接退出了,並無輸出。

4. 線程優先級

Thread類中有3個變量定義了線程優先級。

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
package test;

public class Test
{
	public static class High extends Thread
	{
		static int count = 0;
		@Override
		public void run()
		{
			while (true)
			{
				synchronized (Test.class)
				{
					count++;
					if (count > 10000000)
					{
						System.out.println("High");
						break;
					}
				}
			}
		}
	}
	public static class Low extends Thread
	{
		static int count = 0;
		@Override
		public void run()
		{
			while (true)
			{
				synchronized (Test.class)
				{
					count++;
					if (count > 10000000)
					{
						System.out.println("Low");
						break;
					}
				}
			}
		}
	}

	public static void main(String[] args) throws InterruptedException
	{
		High high = new High();
		Low low = new Low();
		high.setPriority(Thread.MAX_PRIORITY);
		low.setPriority(Thread.MIN_PRIORITY);
		low.start();
		high.start();
	}
}
讓一個高優先級的線程和低優先級的線程同時爭奪一個鎖,看看哪一個最早完成。

固然並不必定是高優先級必定先完成。再屢次運行後發現,高優先級完成的機率比較大,可是低優先級仍是有可能先完成的。

5. 基本的線程同步操做

synchronized 和 Object.wait() Obejct.notify()

這一節內容詳情請看之前寫的一篇Blog

主要要注意的是

synchronized有三種加鎖方式:

  • 指定加鎖對象:對給定對象加鎖,進入同步代碼前要得到給定對象的鎖。
  • 直接做用於實例方法:至關於對當前實例加鎖,進入同步代碼前要得到當前實例的鎖。
  • 直接做用於靜態方法:至關於對當前類加鎖,進入同步代碼前要得到當前類的鎖。

做用於實例方法,則不要new兩個不一樣的實例

做用於靜態方法,只要類同樣就能夠了,由於加的鎖是類.class,能夠new兩個不一樣實例。

wait和notify的用法:

用什麼鎖住,就用什麼調用wait和notify

本文就不細說了。






系列:

[高併發Java 一] 前言

[高併發Java 二] 多線程基礎

[高併發Java 三] Java內存模型和線程安全

[高併發Java 四] 無鎖

[高併發Java 五] JDK併發包1

[高併發Java 六] JDK併發包2

[高併發Java 七] 併發設計模式

[高併發Java 八] NIO和AIO

[高併發Java 九] 鎖的優化和注意事項

[高併發Java 十] JDK8對併發的新支持

Reference:

1. http://www.jb51.net/article/32359.htm
相關文章
相關標籤/搜索