Java併發編程基礎(一)

多線程得狀態

NEW

建立一個線程對象,此時得線程尚未真正得存在,只是建立了一個通常得對象,在這個對象調用了start()以後將進入了就緒得狀態RUNNABLE.java

RUNNABLE

建立得線程對象只有調用start()以後才正式得建立了一個線程,這個狀態就是就緒狀態,此狀態得線程能夠得到CPU的調度(也就是線程得到了執行的資格),RUNNABLE狀態得線程只能意外得終止或者進入RUNNING狀態,不會進入BLOCK或者TERMINATED狀態,由於這些狀態的須要調用wait和sleep,或者其餘的block的IO操做,須要得到CPU的執行權才能夠。算法

RUNNING

一旦CPU經過輪詢的或者其餘的方式從任務可執行的隊列中選中了線程,那麼此時它纔是真正的執行本身的邏輯代碼,RUNNING狀態的其實也是RUNNABLED狀態,可是反過來不成立。RUNNING狀態能夠在一下狀態切換。數組

1.TERMINATED: 使用stop(),可是JDK不推薦使用。緩存

2.BLOCKED狀態,好比sleep,或者wait()進入waitSet中,或者進行阻塞IO操做(好比網路數據的讀寫),或者獲取某個鎖資源,從而進入了該鎖的阻塞隊列中進入了BLOCKED狀態.網絡

3.RUNNABLE:CPU的調度器的輪詢,使得哎線程放棄執行,進入RUNNABLE狀態,或者該線程主動的調用了yield方法,放棄CPU的執行權.數據結構

BLOCKED

由上可知進入BLOCKED狀態的緣由。線程進入BLOCKED狀態能夠在如下幾種狀態切換。多線程

1.TERMINATED:調用stop()不推薦或者JVM Crash.app

2.RUNNABLE:線程阻塞的操做結束,進入了RUNNABLE狀態;線程完成指定的休眠,進入RUNNABLE;Wait中的線程渠道了某個鎖資源,進入RUNNABLE;如今在阻塞的過程被打斷,調用了interrupt()。xss

TERMINATED

TERMINATED是一個線程的最終狀態,線程進入TERMINATED狀態,意味着該線程的整個生命週期都結束了,下列的狀況將會是線程進入TERMIATED狀態.ide

1.線程運行正常結束,結束生命週期

2.線程運行出錯的意外結束。

3.JVM Crash,致使全部的線程都結束。

線程的start()源碼解析。

start()的源代碼以下:

public synchronized void start() {
		/**
		 * This method is not invoked for the main method thread or "system"
		 * group threads created/set up by the VM. Any new functionality added
		 * to this method in the future may have to also be added to the VM.
		 *
		 * A zero status value corresponds to state "NEW".
		 */
		if (threadStatus != 0)
			throw new IllegalThreadStateException();

		/* Notify the group that this thread is about to be started
		 * so that it can be added to the group's list of threads
		 * and the group's unstarted count can be decremented. */
		group.add(this);

		boolean started = false;
		try {
			start0();
			started = true;
		} finally {
			try {
				if (!started) {
					group.threadStartFailed(this);
				}
			} catch (Throwable ignore) {
				/* do nothing. If start0 threw a Throwable then
				  it will be passed up the call stack */
			}
		}
	}

由上面的代碼能夠知道,實際的調用啓動的方式是start0(),該方法是一個本地方法private native void start0();除此以外咱們還能夠知道,剛建立未啓動的線程的threadStatus=0,不能屢次啓動Thread,不然會提示java.lang.IllegalThreadStateException,啓動的線程會被加入到一個線程組中,進入terminated狀態的線程不能在其回到runnable和runing狀態.

模板方法在多線程的使用

[@Override](https://my.oschina.net/u/1162528)
	public void run() {
		if (target != null) {
			target.run();
		}
	}

由上面這段程序能夠知道,若是在構建Thread對象的時候傳入了Runnable,那麼run方法就是調用了Runnable的run,不然咱們就須要重寫Thread的run(). 例子:

public class TemplateDemo {
//至關於start(),負責編寫算法結構
public final void modify(String msg){
	System.out.println("-----------------------");
	showInfo(msg);
	System.out.println("-----------------------");
}
//詳單與run
public void showInfo(String msg){

}
	public static void main(String[] args) {
		TemplateDemo demo = new TemplateDemo(){
			[@Override](https://my.oschina.net/u/1162528)
			public void showInfo(String msg) {
				System.out.println("content:"+msg);
			}
		};
		demo.modify("hello world");
	}
}

執行結果: https://oscimg.oschina.net/oscnet/dd6ee5272a33f2181bdb54c0c1fb89ef7b2.jpg

線程的構造函數

線程的命名

在多線程的環境,爲線程起一個特殊的名字是頗有必要的,能有助於咱們對問題發容排查和線程的追蹤。

線程的默認命名

public Thread() {
		init(null, null, "Thread-" + nextThreadNum(), 0);
	}

	private static int threadInitNumber;
	private static synchronized int nextThreadNum() {
		return threadInitNumber++;
	}

由上面的源碼能夠知道Thread的命名規則是Thread-i.

修改線程的名字

在線程建立以後,啓動以前,咱們有機會去修改線程的名字,可是若是線程已經啓動了,就沒法修改線程的名字,由以下的代碼可得知:

public final synchronized void setName(String name) {
		checkAccess();
		if (name == null) {
			throw new NullPointerException("name cannot be null");
		}

		this.name = name;
		//非NEW狀態的線程是沒法啓動的
		if (threadStatus != 0) {
			setNativeName(name);
		}
	}

線程的父子關係

由下面的代碼片斷能夠知道,任何線程的建立都是創建在其餘線程的基礎之上。

private void init(ThreadGroup g, Runnable target, String name,
					  long stackSize, AccessControlContext acc,
					  boolean inheritThreadLocals) {
		if (name == null) {
			throw new NullPointerException("name cannot be null");
		}

		this.name = name;
		//獲取當前的線程
		Thread parent = currentThread();
		SecurityManager security = System.getSecurityManager();

Thread與ThreadGroup

在Thread的構造函數中,能夠顯示的指定線程的Group,也就是ThreadGroup

SecurityManager security = System.getSecurityManager();
		if (g == null) {
			/* Determine if it's an applet or not */

			/* If there is a security manager, ask the security manager
			   what to do. */
			if (security != null) {
				g = security.getThreadGroup();
			}

			/* If the security doesn't have a strong opinion of the matter
			   use the parent thread group. */
			if (g == null) {
				//獲取父線程的線程組
				g = parent.getThreadGroup();
			}
		}

由上能夠知道建立的當前線程會被加入當前父線程的線程組。

例子:

public class ThreadGroupDemo {

		public static void main(String[] args) {
			Thread t1 = new Thread(new Runnable() {
				[@Override](https://my.oschina.net/u/1162528)
				public void run() {

				}
			});
			t1.start();
			ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
			System.out.println("mainGroup:"+threadGroup.getName());
			Thread t2 = new Thread(new ThreadGroup("demo"), new Runnable() {
				[@Override](https://my.oschina.net/u/1162528)
				public void run() {

				}
			});
			System.out.println("t1 threadGroup:"+t1.getThreadGroup().getName());
			System.out.println("t2 threadGroup:"+t2.getThreadGroup().getName());
		}
	}

執行結果:

Thread與Runnable

Thread負責線程本省的職責和控制,而Runnable則負責邏輯執行單元的部分,這裏就再也不贅述。

Thread與JVM虛擬機棧.

在Thread的構造函數中有一個參數stackSize,經過官方的文檔可知,通常狀況下,建立線程的時候不會手動的執行棧內存的地址空間字節數組,統一經過xss參數進行設置便可,經過上面的刮北風王菲的文檔的描述,咱們不難發現stacksize越大則表明着正在線程方法調用遞歸的深度就越深,stacksize越小則表明着建立的線程數量越多,固然了,這個參數對平臺的依賴仍是比較高的。經過對虛擬機的屢次的參數調整,以下圖所示:

獲得入以下的表格:

JVM內存結構

1.程序計數器

不管任何語言,其實都是須要哦有操做系統經過控制總線向CPU發送機器指令,程序計數器在JVM中所起的做用就是用於存放當前線程接下來將要執行的字節碼指令,分支,循環,跳轉,異常處理等信息.在任什麼時候候,一個處理器只執行其中一個線程中的指令,爲了可以在CPU時間片輪狀切換上下文以後順利回到正確的執行位置,每條線程都須要具有一個獨立的程序計數器,各個線程之間互相不影響,所以JVM將此塊內u才能區域涉及成線程私有的。

2.Java虛擬機棧 Java虛擬機棧也是線程私有的,它的生命週期與線程相同,是在JVM運行時所建立的,在線程中,方法執行的時候都會建立一個名爲棧幀(stack frame)的數據結構,主要用於存放局部變量表,操做棧,動態連接,方法出口等信息.每一個線程在建立的時候,JVM都回爲其建立對應的虛擬機棧,虛擬機棧的大小可能夠經過-xss來配置,方法的調用時棧幀壓入和彈出的過程,等同的虛擬機棧若是局部變量表等佔用內存越小則可唄壓入的棧幀就回越多,反之則可唄壓入的棧幀就回越少,通常棧幀將內存的大小稱爲寬度,而張震的數量則稱爲虛擬機棧的深度。

3.本地方法棧

Java提供了調用本地方法的接口(Java Native Interface),也就是C/C++程序,在線程的執行過程,常常回碰到調用JNI方法的狀況,好比網絡通訊,文件操做的底層,甚至是String的intern等都是JNI方法,JVM爲本地方法所劃分的內存區域即是本地方法棧,這塊內存區域其自由度很是高,徹底靠不一樣的JVM廠商來實現,Java虛擬機規範併爲給出強制的規定,一樣它也是線程私有的內存區域。

4.堆內存

堆內存是JVM中最大的一塊內存區域,被全部的線程所共享,Java在運行期間建立的全部對象幾乎都存在該內存區域,該內存區域也是垃圾回收器重點照顧的區域,所以有些時候堆內存被稱爲GC堆。堆內存還被細分爲新生代,老年代,更細分爲Eden區,From Survivor區和To Survivor區。

5.方法區 方法區也是被多線程共享的內存區域,它主要用於存儲已經被虛擬加載的類信息,常量,經他變量,即便編譯器(JIT)編譯後的代碼等數據,雖然在Java虛擬機規範中,將堆內存劃分爲堆內存的一個邏輯分區,可是被稱爲非堆,甚至有時被稱爲"持久代",在HotSpot JVM中,方法區仍是回被細化爲持久代和代碼緩存區,代碼緩存區主要存儲編譯後的本地代碼.

守護線程

守護線程是一類比較特殊的線程,通常用於處理一些後要的工做,像JDK的垃圾回收線程。在正常的狀況下,若JVM中沒有一個非守護線程,則JVM的進程會退出。

例子:

public class DeamonThreadDemo {
	public static void main(String[] args) throws InterruptedException {
		Thread thread = new Thread(()->{
			while (true){
				try {
				Thread.sleep(1);
				}catch (Exception e){
					e.printStackTrace();
				}
			}
		});
		//設置守護線程
//        thread.setDaemon(true);
		thread.start();
		Thread.sleep(2_000L);
		System.out.println("Main thread finished lifecycle.");
	}
}

上面的代碼若是沒有放開註釋的那行,那麼運行就不會結束,JVM不會退出,若是放開了註釋的那行,那麼JVM會正常的退出.由此能夠知道,守護線程具有自動結束生命週期的特性,而非守護線程不具有這個特色。能夠做爲後臺服務做用,可是在父線程退出以後也可以正常的結束,那麼守護線程就是個很好的選擇。

線程API

線程Sleep

Sleep是使線程進入休眠狀態,休眠有一個很是重要的特徵,拿就是其不會放棄監視器monitor鎖的全部權。 sleep有兩種重載的方法:

public static native void sleep(long millis) throws InterruptedException;//須要填充時間毫秒
public static void sleep(long millis, int nanos)//須要毫秒和納秒

例子:

public class ThreadSleepDemo {


	public static void main(String[] args) {
	new Thread(()->{
		long startTime = System.currentTimeMillis();
		sleep(3_000L);
		long endTime = System.currentTimeMillis();
		System.out.println(String.format("total spend %d ms",(endTime-startTime)));
	}).start();
		long startTime = System.currentTimeMillis();
		sleep(3_000L);
		long endTime = System.currentTimeMillis();
		System.out.println(String.format("Main thread total spend %d ms",(endTime-startTime)));
	}
	private static void sleep(long ms){
		try {
			Thread.sleep(ms);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

執行結果:

使用TimeUnit替代了Thread.sleep

再JDK1.5以後,JDK引入一個枚舉類TimeUnit,其對sleep方法提供了很好的封裝,使用它能夠省去時間但未的換算步驟,例如須要休眠5小時30分25秒100毫秒:

TimeUnit.HOURS.sleep(5);
		TimeUnit.MINUTES.sleep(30);
		TimeUnit.SECONDS.sleep(25);
		TimeUnit.MILLISECONDS.sleep(100);

上面的代碼比單位換算要直觀的多了。

線程yield

yield方法屬於一種啓發式的方法,其會提醒調度器我願意放棄當前的CPU資源,若是CPU資源不緊張,則會忽略這種提醒。調用yield方法會使當前的線程從Running狀態切花不能到Runnbale狀態,通常不經常使用。

public class ThreadYieldDemo {

	public static void main(String[] args) {
		IntStream.range(0,2).mapToObj(ThreadYieldDemo::create).forEach(Thread::start);
	}
	private static Thread create(int index){
		return new Thread(()->{
		   if (index==0)
			   Thread.yield();//放棄CPU資源
			System.out.println(index);
		});
	}
}

執行的結果多是1 0或者是0 1出現,不肯定,由於這個yield方法只是一個提示,CPU執不執行是不肯定的。

yield和sleep

再JDK5之前的版本種,yield方法事實上是i調研了sleep(0),可是他們之間存在着本質的區別。

  • sleep會致使當前線程暫停知道的時間,沒有CPU時間片的消耗。
  • yield只是對CPU調度器的一個提示,若是CPU調度器沒有忽略這個提示,它會致使線程上下文的切換。
  • sleep會使用線程短暫的block,會再給定的時間內釋放CPU資源。
  • yield會使Running狀態的Thread進入Runnable狀態(若是CPU調度器沒有忽略這個提示語的話)
  • sleep幾乎百分之百的完成了給定時間的休眠,而yield的提示並不能一旦擔保。
  • 一個線程sleep另一個線程interrupt會捕獲到終端八年信號,而yield不會。

設置線程優先級

  • public final void setPriority(int newPrioprity)爲線程指定優先級。
  • public final int getPriority()獲取線程的優先級。

在線程上設置優先級是一個提示的做用,並非,設置了就必定獲得:

  1. 對於root用戶,它會hint操做系統你想要設置的優先級,不然會忽略。
  2. 若是CPU比較忙,設置優先級可能會得到更多的CPU時間片,可是閒時有限即得搞底幾乎不會有任何做用。

setPriority(int priority)的源碼以下:

public final void setPriority(int newPriority) {
		ThreadGroup g;
		checkAccess();
		if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
			throw new IllegalArgumentException();
		}
		if((g = getThreadGroup()) != null) {
			if (newPriority > g.getMaxPriority()) {
				newPriority = g.getMaxPriority();
			}
			setPriority0(priority = newPriority);
		}
	}

從源碼能夠知道線程的最小優先級時1,最大的優先級時10,因此設置的時候只能在1-10之間(固然咱們也能夠設置優先級的大小範圍,可是隻能在線程組內)。默認的線程優先級時5,子線程的優先級依賴父線程的優先級:

public class ThreadPriorityDemo {

	public static void main(String[] args) {
		Thread t1 = new Thread();
		System.out.println("t1 priority:"+t1.getPriority());
		Thread t2 = new Thread(()->{
			Thread t3 = new Thread();//子線程的優先級依賴父線程的優先級
			System.out.println("t3 priority:"+t3.getPriority());

		});
		t2.setPriority(6);
		t2.start();
		System.out.println("t2 priority:"+t2.getPriority());
	}
}

執行結果:

獲取當前線程

public static Thread currentThread()用戶返回當前執行線程得引用,這個方法雖然很簡單,可是使用很是普遍。

public class CurrentThreadDemo {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			[@Override](https://my.oschina.net/u/1162528)
			public void run() {
				System.out.println("1currentThread:"+Thread.currentThread().getName());
			}
		}).start();
		System.out.println("2currentThread:"+Thread.currentThread().getName());
	}
}

執行結果:

設置線程上下文類加載器

  • public ClassLoader getContextClassLoader()獲取線程上下文得類加載器,簡單來講,就是這個線程是由那個類加載器加載得,若是在沒有此u該線程的上下文加載器的狀況下,則保持與父線程統一的類加載器。

  • public void setContextClassLoader(ClassLoader cl)設置改線程的類加載器,這個方法能夠打破JAVA類加企的父委託機制,有時候該方法也被成爲JAVA類加載器的後門。

線程interrupt

線程interrupt是一個很是重要的API,也是常用的反法國,與線程中斷相關的API

  • public void interrupt()該方法會打斷當前進入阻塞狀態的線程(例如調用了sleep,wait,join,InterruptibleChannel的IO操做,Selector的wakeup方法,都會使線程進入阻塞狀態),打斷線程阻塞並不表明生命週期的結束,僅僅是打斷了當前的阻塞狀態。一旦線程被打斷都會收到一個稱爲InterruptedException的異常,這個異常就像一個signal(信號)同樣通知當前線程被打斷了(注意,死亡狀態的線程不能對其中斷)

例子:

public class ThreadInterruptDemo {

	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(()->{
			try {
				TimeUnit.MINUTES.sleep(1);
			} catch (InterruptedException e) {
				System.out.println("receive interrupt msg");
			}
		});
		t1.start();
		TimeUnit.MILLISECONDS.sleep(2);
		t1.interrupt();//中斷
	}
}

執行回結果:

  • public boolean isInterrupted() isInterrupted是Thread的一個成員方法,它主要判斷當前線程是否被中斷,該方法僅僅是堆interrupt表示的一個判斷,並不會影響表示發生任何標識發生任何改變,這個與咱們即將學習到的interrupted是存在差異的。

例子:

public class ThreadInterruptedDemo {

	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(){
			@Override
			public void run() {
				//這裏使用循環而不是使用sleep是由於sleep是可中斷的(會收到中斷信號,將中斷的信號),會干擾到程序運行的結果
		 /*       while (true){
					//....
				}*/
				try {
					TimeUnit.MILLISECONDS.sleep(1);
				} catch (InterruptedException e) {
					System.out.printf("I am be interrupted ? %s\n",isInterrupted());
				}
			}
		};

		t1.start();
		TimeUnit.MILLISECONDS.sleep(2);
		System.out.printf("Thread is interrupted? %s\n",t1.isInterrupted());
		t1.interrupt();
		System.out.printf("Thread is interrupted? %s\n",t1.isInterrupted());
	}
}

若是打開註釋的地方,去掉下面的線程休眠,就會出現 false true的結果,反之都是false,由於interrupt會將標識重置。

  • public static boolean interrupted() interrupted是一個靜態方法,雖然其餘也用於判斷當前線程是否被中斷,可是它和成員方法isInterrupted是有區別的,調用該方法會直接查出掉線程的interrupt標識,須要注意的是,若是當前線程被打斷了,那麼第一次調用的interrupted方法會返回true,而且當即擦除interrupt標識,第二次會包括之後的調用永遠會返回false,除非在此期間線程又被一次的打斷。

例子:

@Test
	public void testInterrupted() throws InterruptedException {
		Thread t1 = new Thread(){
			@Override
			public void run() {
				while (true)
				System.out.println(Thread.interrupted());
			}
		};
		t1.setDaemon(true);
		t1.start();
		TimeUnit.MILLISECONDS.sleep(2);
		t1.interrupt();
	}

執行結果:

線程join

Thread的join()與sleep同樣是可中斷的方法,也就是說,若是由其餘的線程可執行對點給錢線程的interrupt操做,它也會捕抓到中斷的信號,而且擦除線程的interrupt標識,Thread的API爲咱們提供了以下的三個接口:

public final void join() throws InterruptedException

public final synchronized void join(long millis)

public final synchronized void join(long millis, int nanos)

join某個線程1,回事當前線程2進入等待,知道線程1運行結束生命週期,或者到達給定的時間,那麼再此期間2線程是出於BLOCKED的,而不是1線程。

public class ThreadJoinDemo {

	public static void main(String[] args) throws InterruptedException {
		List<Thread> threadList = IntStream.range(1,3)
				.mapToObj(ThreadJoinDemo::create).collect(Collectors.toList());
		 threadList.forEach(Thread::start);
		for (Thread t:threadList) {
			//若是註釋當前的代碼,會使得三個線程交替出現,
			// 不然閒使得當前的兩個線程交替出現,等這兩個線程執行完成以後再執行主線程
			t.join();
		}
		for (int i=10;i>0;i--){
			System.out.println(Thread.currentThread().getName()+"#"+i);
			shortSleep();
		}
	}

	private static Thread create(int seq) {
		return new Thread(()->{
			for (int i=0;i<10;i++){
				System.out.println(Thread.currentThread().getName()+"#"+i);
				shortSleep();
			}
		});
	}
	private static void shortSleep(){
		try {
			TimeUnit.SECONDS.sleep(1);
		}catch (InterruptedException e){
		e.printStackTrace();
		}
	}
}

運行結果:

關閉一個線程

JDK裏面的stop方法是能夠關閉的,可是已經被JDK廢棄掉的,JDK不推薦使用了,該關閉方法再關閉線程是可能不會是方法監視器鎖。因此須要採用更加合理的線程關閉方法.

正常關閉

1.線程結束生命週期正常結束 線程運行結束,完成了本身的使命以後,就會正常的而退出,若是線程種的任何耗時比較短,或者時間可控,那麼方法天然會正常的結束.

2.捕抓中斷信號關閉線程

使用new Thread的方式建立線程,這種方式看似很簡單,其實它的派生成本是比較高得,所以一個線程種每每會循環的執行某個任務,好比心跳檢查,不斷得接收網絡得消息報文。系統決定退出得時候,能夠藉助中斷線程得方式使其退出。 例子:

public class ThreadInterruptExitDemo {

	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread() {
			@Override
			public void run() {
				System.out.println("I will start work");
				while (!isInterrupted()) {
					System.out.println("I am working");
				}
				System.out.println("I will be exiting");
			}
		};
		t.start();
		TimeUnit.MILLISECONDS.sleep(10);
		System.out.println("I will be shutdown.");
		t.interrupt();
	}
}

執行結果: I will start work I am working ... I will be shutdown. I am working I am working I will be exiting

使用volatile開關控制

因爲線程得interrupt標識極可能被擦除,或者邏輯單元種不會調用任何中斷方法,因此volatile修飾得開發flag關閉線程也是一種很好得方式.

例子:

static class MyTask extends Thread{
		private volatile boolean closed=false;
		@Override
		public void run() {
			System.out.println("I will start work");
			while (!closed && !isInterrupted()) {
				System.out.println("I am working");
			}
			System.out.println("I will be exiting");
		}
		public void close(){
			this.closed=true;
			this.interrupt();
		}
	}
	public static void main(String[] args) throws InterruptedException {
		MyTask t = new MyTask();
		t.start();
		TimeUnit.MILLISECONDS.sleep(1);
		System.out.println("System will be shutdown.");
		t.close();
	}

執行結果: I will start work I am working I am working ... System will be shutdown. I am working I will be exiting

異常退出

再一個線程得執行單元種,是不容許拋出checked異常得,不論Thread得run方法仍是Runnable的run方法,若是線程的再運行過程當中式須要捕獲checked異常而且判斷是否還有運行下取的必要,那麼此時能夠將checked異常封裝成unckecked異常(RuntimeException)拋出進而結束線程的生命週期.

進程假死

可使用jps和jstack等jdk自帶的工具查看.

相關文章
相關標籤/搜索