21天學會Java之(Java SE第十二篇):多線程、Lambda表達式

多線程是Java語言的重要特性,大量應用於網絡編程和服務器端程序的開發。最常見的UI界面的底層原理、操作系統底層原理都大量使用了多線程技術。本篇中僅初步講解多線程的普通應用,並無深入剖析。由於JUC包的內容過多,過於深奧,本人水平有限,本文中也不擴展敘寫,希望在對於併發編程有更深一步的理解之後填上這個坑。

多線程的基本概念

對於線程的理解,我們需要先理解程序、進程以及線程的概念。

程序是一個靜態的概念,一般對應於操作系統中的一個可執行文件,例如,打開用於敲代碼的idea的可執行文件。打開idea可執行文件,將會加載該程序到內存中並開始執行它,於是就產生了“進程”,而我們打開了多個可執行文件,這就產生了多個進程。

對於多任務,多進程大多數人應該就特別熟悉,我們打開電腦上的任務管理器/活動監視器,我們就能看到一大堆進程,這是操作系統的一種能力,看起來可以在同一時刻運行多個程序。例如,我們在敲代碼的時候能同時用音樂軟件聽歌。而如今,人們往往都有多CPU多計算機,但是併發執行的進程數目並不受限於CPU數目。操作系統會爲每個進程分配CPU的時間片,給人並行處理的感覺。

多線程程序在更低一層擴展了多任務多概念:單個程序看起來在同時完成多個任務。每個任務在一個線程中執行,線程是控制線程的簡稱。如果一個程序可以同時運行多個線程,則稱這個程序是多線程的程序。

而多線程和多進程的本質區別在於每個進程都擁有自己的一套變量,而線程則共享數據。而這樣就會涉及線程安全的問題,下文會介紹這個問題。不過對於共享變量使線程之間的通信比進程之間的通信更有效、更容易。此外,在操作系統中,與進程相比較,線程更“輕量級”,創建、撤銷一個線程比啓動新進程的開銷要小得多,所以線程又被稱爲輕量級進程。

Java中如何實現多線程

Java中使用多線程非常的簡單。下文將會介紹如何創建和使用線程。

通過繼承Thread類實現多線程

繼承Thread類實現多線程的步驟如下:

  1. 在Java中負責實現線程功能的類是java.lang.Thread類。
  2. 可以通過創建Thread的實例來創建新的線程。
  3. 每個線程都是通過某個特定的Thread對象所對應的方法run()來完成其操作的,方法run()稱爲線程體。
  4. 通過調用Thread類的start()方法來啓動一個線程。

可以參考以下代碼理解:

/**
 * 創建線程的方式一:
 * 1.創建:繼承Thread並且重寫run方法
 * 2.啓動:創建子類對象並且運行start方法
 * @author Eddie
 *
 */
public class StartThread extends Thread {
	//程序入口點
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("一邊聽歌......");
		}	
	}
	
	public static void main(String[] args) {
		//創建子類對象
		StartThread st = new StartThread();
		//啓動線程
		st.start();  //不保證立即運行,靠cpu調用
//		st.run();  //僅調用普通的run方法
		for (int i = 0; i < 20; i++) {
			System.out.println("一邊敲代碼......");
		}
	}
}

這種方法的缺點是:如果類已經繼承一個類,則無法繼承Thread類(Java只能繼承一個父類)。

通過Runnable接口實現多線程

在實際開發中,更多的是通過Runnable接口實現的多線程。這種方式完美解決了繼承Thread類的缺點,在實現Runnable接口的同時還可以繼承某個類。所以實現Runnable接口的方式要通用一些。

可以參考以下代碼理解:

/**
 * 創建線程的方式二:
 * 1.創建:實現Runnable並且重寫run方法
 * 2.啓動:創建實現類對象和Thread對象並且運行start方法
 * 推薦:避免單繼承的侷限性,優先使用接口
 * 方便共享資源
 * @author Eddie
 *
 */
public class StartRunnable implements Runnable {
	//線程入口點
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			System.out.println("一邊聽歌......");
		}
	}
	
	public static void main(String[] args) {
//		//創建子類對象
//		StartRunnable sr = new StartRunnable();
//		Thread t=new Thread(sr);
//		//啓動線程
//		t.start();  //不保證立即運行,靠cpu調用
//		st.run();  //僅調用普通的run方法
		
		new Thread(new StartRunnable()).start();  //同樣可以使用匿名對象的方式來使用子類
		
		for (int i = 0; i < 20; i++) {
			System.out.println("一邊敲代碼......");
		}
	}
}

線程狀態和生命週期

線程狀態

一個成對象在它的生命週期內,需要經歷5個狀態,如下圖所示:

線程生命週期圖

  1. 新生狀態

    用new關鍵字建立一個線程對象後,該線程對象處於新生狀態。處於新生狀態的線程有自己的內存空間,通過調用start方法進入就緒狀態。

  2. 就緒狀態

    處於就緒狀態的線程已經具備了運行條件,但是還沒有被分配到CPU,處於“線程就緒隊列”,等待系統爲其分配CPU。就緒狀態並不是執行狀態,當系統選定一個等待執行的Thread對象後,它就會進入執行狀態。一旦獲得哦CPU,線程就進入運行狀態並自動調用其run方法。下列4種原因會導致線程進入就緒狀態:

    • 新建線程:調用start()方法,進入就緒狀態。
    • 阻塞線程:阻塞解除,進入就緒狀態。
    • 運行線程:調用yield()方法,直接進入就緒狀態。
    • 運行線程:JVM將CPU資源從本線程切換到其他線程。
  3. 運行狀態

    在運行狀態的線程執行其run方法中的代碼,直到因調用其他方法而終止,或等待某資源產生阻塞或完成任務死亡。如果在給定的時間片內沒有執行結束,線程就會被系統換下來並回到就緒狀態,也可能由於某些“導致阻塞的事件”而進入阻塞狀態。

  4. 阻塞狀態

    阻塞是指暫停一個線程的執行以等待某個條件發生(如其資源就緒)。有4種原因會導致阻塞:

    • 執行sleep(int millsecond)方法,使當前線程休眠,進入阻塞狀態。當指定的時間到了之後,線程進入就緒狀態。
    • 執行wait()方法,使當前線程進入阻塞狀態。當使用notify()方法喚醒這個線程後,它進入就緒狀態。
    • 當線程運行時,某個操作進入阻塞狀態,例如執行I/O流操作(read()/write()方法本身就是阻塞的方法)。只有當引起該操作阻塞的原因消失後,線程才進入就緒狀態。
    • join()線程聯合:當某個線程等待另一個線程執行結束並能繼續執行時,使用join()方法。
  5. 死亡狀態

    死亡狀態是線程生命週期中的最後一個階段。線程死亡的原因有兩個:一個是正常運行的線程完成了它run()方法內的全部工作;另外一個是線程被強制終止,如通過執行~~stop()destroy()~~方法來終止一個線程(stop()/destroy()方法已經被JDK廢棄,不推薦使用)。當一個線程進入死亡狀態以後,就不能回到其他狀態了。

終止線程的常用方式

上文中提到stop()/destroy()方法已經被JDK廢棄,不推薦使用。當我們需要終止線程的時候通常的做法是提供一個boolean類型的終止變量,當這個變量置爲false時,終止線程的運行。可以參考以下代碼:

/**
 * 終止線程
 * 1.線程正常執行完畢/2.外部干涉,加入標識(這邊所要使用的方法)
 * @author Eddie
 *
 */
public class TerminateThread implements Runnable{
	//加入標識 標記線程體是否可以運行
	private boolean flag=true;
	private String name;
	
	public TerminateThread() {
	}
	public TerminateThread(String name) {
		super();
		this.name = name;
	}
	public String getName() {
		return name;
	}
	@Override
	public void run() {
		int i=0;
		//關聯標識
		while (flag) {
			System.out.println(name+"運行:"+(i++)+"次。");
		}
	}
	
	//對外提供改變標識的方法。
	public void stop() {
		this.flag=false;
	}
	
	public static void main(String[] args) {
		TerminateThread tt = new TerminateThread("線程");  //新生狀態
		new Thread(tt).start();  //就緒狀態
		
		for (int i = 0; i < 99; i++) {
			System.out.println("主線程運行了:"+i+"次。");
			if (i==66) {
				System.out.println(tt.getName()+"STOP!");
				tt.stop();
			}
		}
	}
}

暫停線程執行的常用方法

暫停線程的常用方法有sleep()和yield(),這兩個方法的區別如下:

  • sleep()方法可以讓正在運行的線程進入阻塞狀態,直到休眠時間滿了,進入就緒狀態。
  • yield()方法可以讓正在運行的線程直接進入就緒狀態,讓出CPU的使用權。

sleep()方法使用的示範代碼:

public class BlockedSleep {
	public static void main(String[] args) {
		StateThread t1 = new StateThread();
		StateThread t2 = new StateThread();
		t1.start();
		t2.start();
	}
}
//這裏爲了簡潔實用繼承的方式實現多線程
class StateThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			System.out.println(this.getName() + ":" + i);
			try {
				Thread.sleep(1000);  //調用線程的sleep()方法
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

yield()方法使用的示範代碼:

public class BlockedYield {
	public static void main(String[] args) {
		StateThread t1 = new StateThread();
		StateThread t2 = new StateThread();
		t1.start();
		t2.start();
	}
}
//這裏爲了簡潔實用繼承的方式實現多線程
class StateThread extends Thread {
	@Override
	public void run() {
		for (int i = 0; i < 99; i++) {
			System.out.println(this.getName() + ":" + i);
			Thread.yield();  //調用線程的yield()方法
		}
	}
}

以上代碼可以自己copy進IDE運行看下運行結果,sleep()方法中我們可以感覺到每條結果輸出之前的延遲,這是因爲Thread.sleep(1000)語句在起作用。而在yield()方法中,代碼可以引起線程的切換,但運行沒有明顯延遲。

聯合(合併)線程的使用方法

線程A運行期間,可以調用線程B的join()方法,讓線程B和線程A聯合。這樣,線程A就必須等待線程B執行完畢,才能繼續執行。用以下一個例子來說明一下join()方法的使用:

/**
 * join:合併線程,插隊線程
 * @author Eddie
 *
 */
public class BlockedJoin02 {
	public static void main(String[] args) {
		new Thread(new father()).start();
	}
}

class father implements Runnable{
	@Override
	public void run() {
		System.out.println("爸爸想抽菸了。");
		System.out.println("拿錢叫兒子去買菸。");
		Thread sonThread=new Thread(new son()); 
		sonThread.start();
		try {
			sonThread.join();  //調用join()方法
			System.out.println("拿到了煙,把零錢給兒子。");
		} catch (InterruptedException e) {
			e.printStackTrace();
			System.out.println("兒子走丟了,出門找兒子。");
		}
	}
}

class son implements Runnable{
	@Override
	public void run() {
		System.out.println("兒子拿了錢,出門買菸。!");
		System.out.println("路過了遊戲廳。");
		for (int i = 0; i <= 10; i++) {
			System.out.println("在遊戲廳裏呆了"+i+"秒。");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("走出遊戲廳去便利店買菸");
		System.out.println("回家把煙給爸爸。");
	}
}

Lambda表達式

Lambda表達式是一個可傳遞的代碼塊,可以在以後執行一次或多次。本文僅簡單介紹一下如何使用Lambda表達式,以及Lambda在多線程中的使用,更詳細的內容可以翻閱相關的書籍。

Lambda表達式的推導

public class LambdaTest01 {
	//非靜態內部類
	class Like2 implements ILike{
		@Override
		public void lambda() {
			System.out.println("I like lambda2");
		}
	}
	//靜態內部類
	static class Like3 implements ILike{
		@Override
		public void lambda() {
			System.out.println("I like lambda3");
		}
	}
	
	public static void main(String[] args) {
		//外部類
		ILike like=new Like1();
		like.lambda();
		//非靜態內部類
		like =new LambdaTest01().new Like2();
		like.lambda();
		//靜態內部類
		like =new Like3();
		like.lambda();
		
		//局部內部類
		class Like4 implements ILike{
			@Override
			public void lambda() {
				System.out.println("I like lambda4");
			}
		}
		like=new Like4();
		like.lambda();
		
		//匿名內部類
		like=new ILike() {
			@Override
			public void lambda() {
				System.out.println("I like lambda5");
			}
		};
		like.lambda();
		
		//Lambda表達式
		like=()-> {
				System.out.println("I like lambda5");
			};
		like.lambda();
		
//		Lambda推導必須存在類型
//		()-> {
//			System.out.println("I like lambda5");
//		}.lambda();
		
	}
}
//接口中只能有一個要實現的方法
interface ILike{
	void lambda();
}
//外部類
class Like1 implements ILike{
	@Override
	public void lambda() {
		System.out.println("I like lambda1");
	}
}

Lambda表達式參數的簡化過程

public class LambdaTest02 {
	public static void main(String[] args) {
		ILove love=(String a)-> {
				System.out.println("I like lambda-->"+a);
			};
		love.lambda("普通Lambda表達式");
		
		//可以去掉參數類型
		love=(a)-> {
			System.out.println("I like lambda-->"+a);
		};
		love.lambda("去掉參數類型");
		
		//只有一個參數括號可以省略
		love=a-> {
			System.out.println("I like lambda-->"+a);
		};
		love.lambda("省略參數括號");
		
		//只有一行代碼可以省略花括號
		love=a->System.out.println("I like lambda-->"+a);
		love.lambda("省略花括號");
		
	}
}

interface ILove{
	void lambda(String a);
}
//外部類
//class Love1 implements ILove{
//	@Override
//	public void lambda(String a) {
//		System.out.println("I like lambda-->"+a);
//	}
//}

Lambda表達式返回值的簡化過程

public class LambdaTest03 {
	public static void main(String[] args) {
		//普通的Lambda表達式
		IInsterest insterest=(int a1, int b1)-> {
			System.out.println("I like lambda-->"+(a1+b1));
			return a1+b1;
			};
		insterest.lambda(1, 1);
		
		//去掉參數類型(去掉的話需要全部去掉,僅去掉一個不可行)
		insterest=(a1, b1)-> {
			System.out.println("I like lambda-->"+(a1+b1));
			return a1+b1;
			};
		insterest.lambda(2, 2);
		
		/*
		 * 有兩個參數不可省略參數的括號
		 * 有兩行代碼不可省略花括號
		 */
		
		//如果只有一行代碼,並且有返回值可以省略return;
		insterest=(a1, b1)->a1+b1;
		//返回了一個int數值
		System.out.println(insterest.lambda(6, 6));
		
		insterest=(a1, b1)->100;
		//返回了一個int數值
		System.out.println(insterest.lambda(100, 100));
	}
}
interface IInsterest{
	int lambda(int a,int b);
}
//class Insterest implements IInsterest{
//	@Override
//	public int lambda(int a1, int b1) {
//		System.out.println("I like lambda-->"+(a1+b1));
//		return a1+b1;
//	}
//}

Lambda表達式簡化線程(用一次)的使用

public class LambdaThread01 {
	//靜態內部類
	static class Test1 implements Runnable{
		@Override
		public void run() {
			for (int i = 0; i < 2; i++) {
				System.out.println("一邊聽歌1");
			}
		}
	}
	//非靜態內部類
	class Test2 implements Runnable{
		@Override
		public void run() {
			for (int i = 0; i < 2; i++) {
				System.out.println("一邊聽歌2");
			}
		}
	}
	
	public static void main(String[] args) {
		//靜態內部類
		new Thread(new Test1()).start();
		//非靜態內部類
		new Thread(new LambdaThread01().new Test2()).start();
		
		//局部內部類
		class Test3 implements Runnable{
			@Override
			public void run() {
				for (int i = 0; i < 2; i++) {
				System.out.println("一邊聽歌3");
				}
			}
		}
		
		new Thread(new Test3()).start();
		
		//匿名內部類
		new Thread(new Runnable() {
			@Override
			public void run() {
				for (int i = 0; i < 2; i++) {
					System.out.println("一邊聽歌4");
					}
			}
		}).start();
		
		//jdk8簡化   Lambda表達式
		//因爲Thread裏只能傳入一個實現Runable接口的實現類並且Runable僅需要實現一個run()方法
		new Thread(()-> {
				for (int i = 0; i < 2; i++) {
					System.out.println("一邊聽歌5");
				}
			}
		).start();
		
		for (int i = 0; i < 5; i++) {
			System.out.println("66666666666");
		}
	}
}

使用Lambda表達式簡化多線程

/**
 * 使用Lambda表達式簡化多線程
 * Lambda表達式避免匿名內部類定義過多
 * 其實質屬於函數式編程的概念
 * @author Eddie
 *
 */
public class LambdaThread02 {
	public static void main(String[] args) {
		new Thread(()->{
			for (int i = 0; i < 10; i++) {
				System.out.println("一邊聽歌...");
			}
		}).start();
		
		new Thread(()->System.out.println("正在學習Lambda表達式")).start();
		
		for (int i = 0; i < 10; i++) {
			System.out.println("一邊寫代碼...");
		}
	}
}

線程的常用方法

線程也是對象,系統爲線程定義了很多方法、優先級、名字等,以便對多線程進行有效地管理。

線程常用的方法

線程的常用方法如下表所示:

方法 功能
getState() 獲得線程當前的狀態
isAlive() 判斷線程是否還“活着”,即線程是否還未終止
getPriority() 獲得線程的優先級數值
setPriority() 設置線程的優先級數值
setName() 給線程設置一個名字
getName() 獲得線程的名字
currentThread() 取得當前正在運行的線程對象,也就是取得自己本身
setDaemon(boolean on) 將線程設置成守護線程

使用getState()方法觀察線程狀態

public class AllState {
	public static void main(String[] args) {
		Thread t=new Thread(()->{
			for (int i = 0; i <5; i++) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("模擬線程");
			}
		});
		//觀察狀態
		State state=t.getState();
		System.out.println(state);  //NEW
		
		t.start();
		state=t.getState();
		System.out.println(state);  //RUNNABLE
		
//		while (state!=State.TERMINATED) {
//			try {
//				Thread.sleep(200);
//			} catch (InterruptedException e) {
//				e.printStackTrace();
//			}
//			state=t.getState();
//			System.out.println(state);  //TIMED_WAITING
//		}
		
		while (true) {
			//活動的線程數
			int threadNum=Thread.activeCount();
			if (threadNum==1) {
				break;
			}
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			state=t.getState();
			System.out.println(state);  //TIMED_WAITING
		}
		state=t.getState();
		System.out.println(state);    //TERMINATED
	}
}

線程的優先級

/**
 * 線程的優先級1-10
 * 1.NORM_PRIORITY 5 默認
 * 2.MIN_PRIORITY 1
 * 3.MAX_PRIORITY 10
 * 概率,不代表絕對的先後順序
 * @author Eddie
 *
 */
public class PriorityTest {
	public static void main(String[] args) {
		
		MyPriority mp=new MyPriority();
		Thread t1=new Thread(mp,"百度");
		Thread t2=new Thread(mp,"阿里");
		Thread t3=new Thread(mp,"騰訊");
		Thread t4=new Thread(mp,"頭條");
		Thread t5=new Thread(mp,"美團");
		Thread t6=new Thread(mp,"滴滴");
		
		//設置優先級需要在線程啓動前
		t1.setPriority(Thread.MAX_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);
		t3.setPriority(Thread.MAX_PRIORITY);
		t4.setPriority(Thread.MIN_PRIORITY);
		t5.setPriority(Thread.MIN_PRIORITY);
		t6.setPriority(Thread.MIN_PRIORITY);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
		t5.start();
		t6.start();
		System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
	}
}

class MyPriority implements Runnable{

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
		Thread.yield();
	}
}

其他方法的示例

/**
 * 其他方法
 * isAlive:線程是否還或者
 * Thread.currentThread():當前線程
 * setName.getName:設置和獲取代理線程的名稱
 * @author Eddie
 *
 */
public class InfoTest {
	public static void main(String[] args) {
		System.out.println(Thread.currentThread().isAlive());
		MyInfo myInfo=new MyInfo("戰鬥機");
		Thread t=new Thread(myInfo);
		t.setName("公雞");
		t.start();
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(t.isAlive());
	}
}

class MyInfo implements Runnable{
	private String name;
	
	public MyInfo(String name) {
		super();
		this.name = name;
	}

	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+"-->"+name);
	}
}

守護線程

/**
 * 守護線程:是爲用戶線程服務的;JVM停止不用等待守護線程執行完畢
 * 線程默認用戶線程 JVM等待用戶線程執行完畢纔會停止
 * @author Eddie
 *
 */
public class DaemonTest {
	public static void main(String[] args) {
		God god=new God();
		You you=new You();
		Thread t=new Thread(god);
		t.setDaemon(true);  //將用戶線程設置爲守護線程
		t.start();
		new Thread(you).start();
		
	}
}

class You implements Runnable{
	@Override
	public void run() {
		for (int i = 1; i <=365*100; i++) {
			System.out.println("Happy Life"+i+"days.");
		}
		System.out.println("die...");
	}
}

class God implements Runnable{
	@Override
	public void run() {
		while (true) {
			System.out.println("Bless you...");
		}
	}
}

線程同步

在處理多線程問題時,如果多個線程同時訪問同一個對象,並且某些線程還想修改這個對象時,就需要用到“線程同步”機制。加入線程同步後,我們稱爲這是線程安全的;線程安全在併發時保證數據的準確性、效率儘可能高。

線程同步的概念

線程同步其實就是一種等待機制,多個需要同時訪問此對象的線程進入這個對象的等待池形成隊列,等待前面的線程使用完畢後,下一個線程繼續使用。

用一個取款機的例子來看下未使用線程同步的情況下會發生的情況:

public class UnsafeTest02 {
	public static void main(String[] args) {
		Account account=new Account(100, "百萬賬戶");
		ATM atm01=new ATM(account, 80);
		ATM atm02=new ATM(account, 70);
		new Thread(atm01,"自己").start();
		new Thread(atm02,"老婆").start();
	}
}

//賬戶
class Account{
	private int total_assets;  //賬戶總資產
	private String account_name;  //賬戶名字
	public Account(int total_assets, String account_name) {
		super();
		this.total_assets = total_assets;
		this.account_name = account_name;
	}
	public int getTotal_assets() {
		return total_assets;
	}
	public void setTotal_assets(int total_assets) {
		this.total_assets = total_assets;
	}
	public String getAccount_name() {
		return account_name;
	}
	public void setAccount_name(String account_name) {
		this.account_name = account_name;
	}
}
//模擬取款
class ATM implements Runnable{
	private Account account;  //取款賬戶
	private int withdrawMoney;  //取款金額
	private int pocketMoney;  //口袋的錢
	public ATM(Account account, int withdrawMoney) {
		super();
		this.account = account;
		this.withdrawMoney = withdrawMoney;
	}

	@Override
	public void run() {
		if (account.getTotal_assets()<withdrawMoney) {
			return;
		}
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		account.setTotal_assets(account.getTotal_assets()-withdrawMoney);
		pocketMoney+=withdrawMoney;
		System.out.println(Thread.currentThread().getName()+":"+pocketMoney);
		System.out.println(account.getAccount_name()+":"+account.getTotal_assets());
	}
}

由於沒有使用線程同步機制,即使我們在線程中判斷了剩餘餘額,但是同樣會使兩個人都取款成功,這就叫做線程不安全。

實現線程同步

由於同一進程的多個線程共享同一塊存儲空間,這在帶來方便的同時,也帶來了訪問衝突問題。Java語言提供了專門機制來解決這種衝突,有效避免了同一個數據對象被多個線程同時訪問造成的問題。這套機制就是使用synchronized關鍵字,它包括兩種用法:synchronized方法和synchronized塊。

  1. synchronized方法

    通過在方法聲明中加入synchronized關鍵字來聲明此方法,語法格式如下:

    public synchronized void accessVal(int newVal);
    

    synchronized方法控制對“對象的類成員變量”的訪問:每個對象對應一把鎖,每個synchronized方法都必須獲得調用該方法的對象的鎖才能執行,否則所屬線程阻塞。方法一旦執行,就獨佔該鎖,直到從該方法返回時纔將鎖釋放,此後被阻塞的線程方能獲得該鎖,重新進入可執行狀態。

  2. synchronized塊

    synchronized方法的缺陷是,若將一個大的方法聲明爲synchronized將會大大影響程序的工作效率。

    爲此,Java提供了更好的解決辦法,就是使用synchronized塊。synchronized塊可以讓人們精確地控制具體的“成員變量”,縮小同步的範圍,提高效率。且synchronized塊可以指定鎖的對象,synchronized方法則只能鎖本對象。

    通過synchronized關鍵字可聲明synchronized塊,語法格式如下:

    synchronized(synObject){
        //允許訪問控制的代碼
    }
    

將以上取款機的例子加入線程同步:

public class SynBlock01 {
	public static void main(String[] args) {
		Account account=new Account(200, "百萬賬戶");
		SynATM my = new SynATM(account,80);
		SynATM wife = new SynATM(account,90);
		new Thread(my,"自己").start();
		new Thread(wife,"妻子").start();
	}
}
//賬戶
class Account{
	private int total_assets;  //賬戶總資產
	private String account_name;  //賬戶名字
	public Account(int total_assets, String account_name) {
		super();
		this.total_assets = total_assets;
		this.account_name = account_name;
	}
	public int getTotal_assets() {
		return total_assets;
	}
	public void setTotal_assets(int total_assets) {
		this.total_assets = total_assets;
	}
	public String getAccount_name() {
		return account_name;
	}
	public void setAccount_name(String account_name) {
		this.account_name = account_name;
	}
}
class SynATM implements Runnable{
	private Account account;
	private int drawingMoney;
	private int money;
	public SynATM(Account account, int drawingMoney) {
		super();
		this.account = account;
		this.drawingMoney = drawingMoney;
	}

	@Override
	public void run() {
		//提高性能,判斷賬戶是否有錢或者取的錢是否超過賬戶餘額,滿足條件直接返回,不需要運行同步塊
		if (account.getTotal_assets()<=0 || account.getTotal_assets()<drawingMoney) {
			return;
		}
		//同步塊:目標鎖定account
		synchronized (account) {
			account.setTotal_assets(account.getTotal_assets()-drawingMoney);
			money+=drawingMoney;
			System.out.println(Thread.currentThread().getName()+"錢包餘額:"+money);
			System.out.println(account.getAccount_name()+"餘額:"+account.getTotal_assets());
		}
	}
}

synchronized (account)意味着線程需要獲得account對象的“鎖”纔有資格運行同步塊中的代碼。Account對象的“鎖”也稱爲“互斥鎖”,在同一時刻只能被一個線程使用。A線程擁有鎖,則可以調用“同步塊”中的代碼;B線程沒有鎖,則進入account對象的“鎖池隊列”等待,直到A線程使用完畢釋放了account對象的鎖,B線程得到鎖纔可以調用“同步塊”中的代碼。

synchronized方法、synchronized塊和線程不安全的例子

以下是買票的例子:

public class SynBlock03 {
	public static void main(String[] args) {
		Syn12306 web12306 = new Syn12306();
		
		new Thread(web12306,"黃牛").start();
		new Thread(web12306,"yellow牛").start();
		new Thread(web12306,"ticket_scalper").start();
	}
}

class Syn12306 implements Runnable{
	//票數
	private int ticketNums=10;
	private boolean flag=true;
	
	@Override
	public void run() {
		while (flag) {
			test5();
			try {
				Thread.sleep(20);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	//線程安全,範圍太大-->性能效率低下:同步方法,鎖定的是SynWeb對象
	public synchronized void test1() {
		if (ticketNums<=0) {
			flag=false;
			return;
		}
		//模擬網絡延遲
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
	//線程安全,範圍太大-->性能效率低下:同步塊,鎖定this對象,即SynWeb對象
	public void test2() {
		synchronized(this) {
			if (ticketNums<=0) {
				flag=false;
				return;
			}
			//模擬網絡延遲
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
	//線程不安全:同步塊,鎖定ticketNums對象的屬性在變
	public void test3() {
		synchronized((Integer)ticketNums) {
			if (ticketNums<=0) {
				flag=false;
				return;
			}
			//模擬網絡延遲
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
	//線程不安全:同步塊
	public void test4() {
		//僅鎖定下面一部分,線程不安全
		synchronized(this) {
			if (ticketNums<=0) {
				flag=false;
				return;
			}
		}
		//模擬網絡延遲
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
	}
	//線程安全:儘可能鎖定合理的範圍(不是指代碼 指數據的完整性)
	//double checking 
	public void test5() {
		if (ticketNums<=0) {  //考慮的是沒有票的情況
			flag=false;
			return;
		}
		//僅鎖定下面一部分,線程不安全
		synchronized(this) {
			if (ticketNums<=0) {  //考慮的是最後一張票的情況
				flag=false;
				return;
			}
			//模擬網絡延遲
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-->"+ticketNums--);
		}
	}
}

死鎖及解決方案

死鎖的概念

“死鎖”指的是多個線程各自佔有一些共享資源,並且互相等待得到其他線程佔有的資源才能繼續,從而導致兩個或者多個線程都在等待對方釋放資源,停止執行的情形。

因此,某一個同步塊需要同時擁有“兩個以上對象的鎖”時,就可能會發生“死鎖”的問題。用以下一個例子來描述下死鎖的形成:

public class DeadLock {
	public static void main(String[] args) {
		new Thread(new MarkUp("大丫", true)).start();
		new Thread(new MarkUp("二丫", false)).start();
	}
}
//鏡子
class Mirror{
}
//口紅
class Lipstick{
}
//化妝
class MarkUp implements Runnable{
	//不管幾個對象只有一份
	static Mirror mirror=new Mirror();
	static Lipstick lipstick=new Lipstick();
	private String girl;
	private boolean flag;
	
	public MarkUp(String girl, boolean flag) {
		this.girl = girl;
		this.flag = flag;
	}

	@Override
	public void run() {
		markup();
	}
	//相互持有對方的對象鎖
	private void markup() {
		if (flag) {
			synchronized (mirror) {  //先將鏡子鎖上
				System.out.println(this.girl+"照鏡子。");
				//1秒後,塗口紅
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (lipstick) {  //然後將口紅鎖上
					System.out.println(this.girl+"塗口紅。");
				}
			}
		}else {
			synchronized (lipstick) {  //先將口紅鎖上
				System.out.println(this.girl+"塗口紅。");
				//2秒後,照鏡子
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				synchronized (mirror) {  //然後將鏡子鎖上
					System.out.println(this.girl+"照鏡子。");
				}
			}
		}
	}
}

執行後,兩個線程都在等對方的資源,都處於停滯狀態。

死鎖的解決方法

死鎖是由於“同步塊需要同時持有多個對象鎖”造成的。要解決這個問題,就是同一個代碼塊不要同時持有兩個對象鎖。如上面的死鎖例子,可以修改如下:

public class DeadLock {
	public static void main(String[] args) {
		new Thread(new MarkUp("大丫", true)).start();
		new Thread(new MarkUp("二丫", false)).start();
	}
}
//鏡子
class Mirror{
}
//口紅
class Lipstick{
}
//化妝
class MarkUp implements Runnable{
	//不管幾個對象只有一份
	static Mirror mirror=new Mirror();
	static Lipstick lipstick=new Lipstick();
	private String girl;
	private boolean flag;
	
	public MarkUp(String girl, boolean flag) {
		this.girl = girl;
		this.flag = flag;
	}

	@Override
	public void run() {
		markup();
	}
	//相互持有對方的對象鎖
	private void markup() {
		if (flag) {
			synchronized (mirror) {  //先將鏡子鎖上
				System.out.println(this.girl+"照鏡子。");
				//1秒後,塗口紅
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				/*
				synchronized (lipstick) {  //然後將口紅鎖上
					System.out.println(this.girl+"塗口紅。");
				}*/
			}
			synchronized (lipstick) {  //然後將口紅鎖上
				System.out.println(this.girl+"塗口紅。");
			}
		}else {
			synchronized (lipstick) {  //先將口紅鎖上
				System.out.println(this.girl+"塗口紅。");
				//2秒後,照鏡子
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				/*
				synchronized (mirror) {  //然後將鏡子鎖上
					System.out.println(this.girl+"照鏡子。");
				}*/
			}
			synchronized (mirror) {  //然後將鏡子鎖上
				System.out.println(this.girl+"照鏡子。");
			}
		}
	}
}

題外內容(與線程同步有相關性)

以下內容與線程同步有相關性,僅寫了幾個例子來描述。

CAS:比較並交換

public class CAS {
	//庫存
	private static AtomicInteger stock=new AtomicInteger(5);
	public static void main(String[] args) {
		for (int i = 0; i < 6; i++) {
			new Thread(new Customer()).start();
		}
	}
	
	public static class Customer implements Runnable{
		@Override
		public void run() {
			synchronized (stock) {
				//模擬延遲
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				Integer left=stock.get();
				if (left<1) {
					System.out.println(Thread.currentThread().getName()+"沒搶到,沒有庫存了");
					return;
				}
				System.out.println(Thread.currentThread().getName()+"搶到了,第"+left+"件商品,剩餘"+left+"件商品。");
				stock.set(left-1);
			}
		}
	}
}

指令重排

public class HappenBefore {
	private static int a=0;  //變量1
	private static boolean flag=false;  //變量2
	public static void main(String[] args) throws InterruptedException {
		for (int i = 0; i < 100; i++) {
			a=0;
			flag=false;
			//線程1 讀取數據
			Thread t1=new Thread(()->{
				a=1;
				flag=true;
			});
			//線程2 更改數據
			Thread t2=new Thread(()->{
				if (flag) {
					a*=1;
				}
				//指令重排
				if (a==0) {
					System.out.println("Happen before,a->"+a);
				}
			});
			
			t1.start();
			t2.start();
			
			t1.join();
			t2.join();
		}
	}
}

可重入鎖:鎖可以延續使用

public class LockTest {
	public void test() {
		//第一次獲得鎖
		synchronized (this) {
			while (true) {
				//第二次獲得同樣的鎖
				synchronized (this) {
					System.out.println("ReentrantLock");
				}
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public static void main(String[] args) {
		new LockTest().test();
	}
}

不可重入鎖:鎖不可以延續使用

public class LockTest01 {
	Lock lock=new Lock();
	
	public void a() {
		lock.lock();
		doSomething();
		lock.unLock();
	}
	//不可重入
	public void doSomething() {
		lock.lock();
		//............
		lock.unLock();
	}
	
	public static void main(String[] args) {
		new LockTest01().a();
		new LockTest01().doSomething();
	}
}

class Lock{
	//是否佔用
	private boolean isLocked=false;
	//使用鎖
	public synchronized void lock() {
		while (isLocked) {
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		isLocked=true;
	}
	//釋放鎖
	public synchronized void unLock() {
		isLocked=false;
		notify();
	}
}

volatile關鍵字

volatile用於保證數據的同步,也就是可見性(不保證原子性),可以參考以下例子:

public class ValatileTest {
	private volatile static int num=0;
	public static void main(String[] args) {
		new Thread(()->{
			while (num==0) {  //此處不要編寫代碼
				
			}
		}).start();
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		num=1;
	}
}

線程併發協作(生產者-消費者模式)

生產者-消費者模式的基本概念

多線程環境下,經常需要多個線程能夠併發和協作。這是,就需要了解一個重要的多線程併發協作模型“生產者-消費者模式”;

  • 什麼是生產者。生產者指的是負責生產數據的模塊(這裏的模塊指的可能是方法、對象、線程、進程等)。
  • 什麼是消費者。消費者指的是負責處理數據的模塊(這裏的模塊指的可能是方法、對象、線程、進程等)。
  • 什麼是緩衝區。消費者不能直接使用生產者的數據,它們之間有個“緩衝區”。生產者將生產好數據放入“緩衝區”,消費者從“緩衝區”拿出要處理的數據。

緩衝區是實現併發操作的核心。緩衝區設置有如下3個好處:

  • 實現線程的併發協作:有了緩衝區以後,生產者線程只需要往緩衝區裏面放置數據,而不需要管消費者消費的情況;同樣,消費者只需要從緩衝區拿出數據處理即可,不需要考慮生產者生產的情況。這樣,就從邏輯上實現了“生產者線程”和“消費者線程”的分離。
  • 解耦了生產者和消費者。生產者不需要和消費者直接打交道。
  • 解決忙閒不均,提高效率。生產者生產數據慢時,但在緩衝區仍有數據,不影響消費者消費;消費者處理數據慢時,生產者仍然可以繼續往緩衝區裏面放置數據。

而生產者-消費者模式主要有兩種實現方法:管程法以及信號燈法。

線程併發協作(線程通信)的使用情景

  1. 生產者和消費者共享同一個資源,並且生產者和消費者之間相互依賴,互爲條件。
  2. 對於生產者,沒有生產產品之前,消費者要進入等待狀態。而生產了產品之後,又需要馬上通知消費者消費。
  3. 對於消費者,在消費之後,要通知生產者已經消費結束,需要繼續生產新產品以供消費。
  4. 在生產者-消費者問題中,僅適用synchronized是不夠的。synchronized可以阻止併發更新同一個共享資源,雖然實現了同步,但它不能用來實現不同線程之間的消息傳遞(通信),這就需要用到線程通信的方法了。

線程通信的常用方法

方法名 作用
final void wait() 表示線程一直等待,直到得到其他線程通知
void wait(long timeout) 線程等待指定毫秒參數的時間
final void wait(long timeout,int nanos) 線程等待指定毫秒、微秒的時間
final void notify() 喚醒一個處於等待狀態的線程
final void notifyAll() 換新同一個對象上所有調用wait()方法的線程,優先級別高的線程優先運行
  • 注意事項: 以上方法均是java.lang.Object類的方法,只能在同步方法或者同步塊中使用,否則會拋出異常。

在實際開發中,尤其是“架構設計”中會大量使用“生產者-消費者”模式。初學者僅需瞭解作用即可,如果想深入理解架構這一部分內容是相當重要的。

生產者消費者實現方法

以下是生產者-消費者模式的實現方法的實例,可結合概念以及註釋理解。

管程法

public class CoTest01 {
	public static void main(String[] args) {
		SynContainer container=new SynContainer();
		new Thread(new Producer(container)).start();
		new Thread(new Consumer(container)).start();
	}
}
//生產者
class Producer implements Runnable{
	private SynContainer container;
	public Producer(SynContainer container) {
		this.container = container;
	}
	@Override
	public void run() {
		//生產
		for (int i = 0; i < 100; i++) {
			System.out.println("生產第"+(i+1)+"個麪包");
			container.push(new Bread(i));
		}
	}
}
//消費者
class Consumer implements Runnable{
	private SynContainer container;
	public Consumer(SynContainer container) {
		this.container = container;
	}
	@Override
	public void run() {
		//消費
		for (int i = 0; i < 100; i++) {
			System.out.println("買了"+(container.get().getId()+1)+"個麪包");
		}
	}
}
//緩衝區
class SynContainer{
	Bread[] breads=new Bread[10];
	private int count =0;
	//存儲 生產
	public synchronized void push(Bread bread) {
		//緩衝區(庫存)滿了停止消費
		if (count==breads.length) {
			try {
				this.wait();  //線程阻塞 停止生產,消費者通知生產解除阻塞
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//容器未滿可以生產
		breads[count]=bread;
		count++;
		//this.notify();
		this.notifyAll();  //生產了商品可以通知生產者恢復消費了
	}
	//獲取 消費
	public synchronized Bread get() {
		//緩衝區爲空(沒有面包)就需要停止消費
		if (count==0) {
			try {
				this.wait();  //線程阻塞 停止消費,生產者通知消費解除阻塞
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		//沒有數據只能等待
		count--;
		//this.notify();
		this.notifyAll();  //消費了商品可以通知生產者恢復生產了
		return breads[count];
	}
}
//麪包
class Bread{
	private int id;

	public int getId() {
		return id;
	}

	public Bread(int i) {
		super();
		this.id = i;
	}
}

信號燈法

public class CoTest02 {
	public static void main(String[] args) {
		Tv tv=new Tv();
		new Thread(new Actor(tv)).start();
		new Thread(new Audience(tv)).start();
	}
}
//生產者 演員
class Actor implements Runnable{
	private Tv tv;
	public Actor(Tv tv) {
		this.tv = tv;
	}

	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			if (i%2==0) {
				this.tv.play("牛逼");
			} else {
				this.tv.play("666");
			}
		}
	}
}

//消費者 觀衆
class Audience implements Runnable{
	private Tv tv;
	public Audience(Tv tv) {
		this.tv = tv;
	}
	@Override
	public void run() {
		for (int i = 0; i < 20; i++) {
			this.tv.watch();
		}
	}
}
//同一個資源 電視
class Tv{
	private String voice;
	//信號燈:true表示演員表演,觀衆等待;false表示觀衆等待,演員表演
	private boolean flag=true;
	
	public synchronized void play(String voice){
		//演員等待
		if (!flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("演員說了:"+voice);
		this.voice=voice;
		this.notifyAll();  //喚醒
		this.flag=!this.flag;  //切換標誌
	}
	
	public synchronized void watch() {
		//觀衆等待
		if (flag) {
			try {
				this.wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		System.out.println("觀衆聽到了:"+this.voice);
		this.notifyAll();  //喚醒
		this.flag=!this.flag;  //切換標誌
	}
}

任務定時調度

任務定時調度在項目開發中經常用到。在實際開發中可以使用quanz任務框架來開發,也可以使用Timer和Timertask類實現同樣的功能。

通過Timer和TimerTask類可以實現定時啓動某個線程,通過線程執行某個任務的功能。

Timer和Timertask類

  1. java.util.Timer

    在這種方式中,Timer類的作用類似於鬧鐘的功能,也就是定時或者每隔一定時間觸發一次線程。其實,Timer是JDK中提供的一個定時器工具。使用的時候會在主線程之外起一個單獨的線程執行指定的計劃任務,可以指定執行一次或者反覆執行多次,起到類似鬧鐘的作用。

  2. java.util.TimerTask

    TimerTask類是一個抽象類,該類實現了Runnable接口,所以該類具備多線程能力。在這種實現方式中,通過繼承TimerTask使用該類獲得多線程的能力,將需要多線程執行的代碼書寫在run方法內部,然後通過Timer類啓動線程的執行。

可以參考以下例子理解:

public class TimerTest {
	public static void main(String[] args) {
		Timer timer=new Timer();
		//執行安排
		//timer.schedule(new MyTimer(), 3000);  //3000毫秒後執行1次
		//timer.schedule(new MyTimer(), 3000,1000);  //3000毫秒後執行,然後每隔1000毫秒執行一次
		Calendar calendar=new GregorianCalendar(2020,05,06,20,45,00);  //傳入一個時間(注意月份0-11)
		//timer.schedule(new MyTimer(), calendar.getTime());  //按預定的時間執行一次
		timer.schedule(new MyTimer(), calendar.getTime(), 1000);  //按預定的時間執行,然後每隔1000毫秒執行一次
	}
}
//任務類
class MyTimer extends TimerTask{
	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println("放空大腦。。。");
		}
		System.out.println("----------END-------------");
	}
}

在實際使用中,一個Timer可以啓動任意多個TimerTask實現的線程,但是多個線程之間會存在阻塞。所以如果多個線程之間需要完全獨立的話,最好還是一個Timer啓動一個TimerTask。

Quartz的簡單例子

使用Quartz框架我們可以到Quartz官網下載開源文件,本文僅描述一個簡單的例子,如果想深入瞭解可以查看文件中的API文檔以及源碼。

首先我們需要一個創建一個任務的對象:

import java.util.Date;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloJob implements Job {

    public HelloJob() {
    }

    public void execute(JobExecutionContext context)
        throws JobExecutionException {
    	System.out.println("------start-------");
        System.out.println("Hello World! - " + new Date());
        System.out.println("------end-------");
    }
}

以下是一個簡單使用例子:

import static org.quartz.DateBuilder.evenSecondDateAfterNow;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Date;

/**
 * Quartz學習入門
 * @author WHZ
 *
 */
public class SimpleExample {

  public void run() throws Exception {
    //1、創建Scheduler工廠
    SchedulerFactory sf = new StdSchedulerFactory();
    //2、從工廠中獲取調度器
    Scheduler sched = sf.getScheduler();


    //3、創建JobDetail(任務)
    JobDetail job = newJob(HelloJob.class).withIdentity("job1", "group1").build();

    //時間
    //Date runTime = evenMinuteDate(new Date());  //下一分鐘
    Date runTime = evenSecondDateAfterNow();  //下一秒
    //4、觸發器(觸發條件)
    //Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).build();
    Trigger trigger = newTrigger().withIdentity("trigger1", "group1").startAt(runTime).  //按設定的時間開始運行
    		withSchedule(simpleSchedule().withIntervalInSeconds(5).withRepeatCount(3)).build();  //間隔5秒,重複3次
    
    //5、註冊任務和觸發條件
    sched.scheduleJob(job, trigger);
    
    //6、啓動
    sched.start();

    
    try {
      //5秒後停止(該線程總共運行的時間)
      Thread.sleep(30L * 1000L);
    } catch (Exception e) {
    }
    //7、停止
    sched.shutdown(true);
  }

  public static void main(String[] args) throws Exception {

    SimpleExample example = new SimpleExample();
    example.run();

  }
}

實際開發中,可以使用該開源框架更加方便實現任務的定時調度,實際上該框架底層原理就是Timer和TimerTask類的內容,想要深入瞭解可以嘗試閱讀QUARTZ框架的源碼。

結語

本篇到此完結,多線程的內容在Java中是極其深奧的一部分。礙於本人水平有限,本文中沒有描述JUC包的內容,可以參考相關的API文檔以及書籍來學習。而對於更加複雜的系統級程序設計,建議參考更高級的參考文獻。希望看到這裏的讀者能點個贊給個關注,祝各位早日年薪百萬!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章