走進Java裏的線程世界

基礎概念

什麼是進程和線程

進程是程序運行資源分配的最小單位

  • 進程是操作系統進行資源分配的最小單位,其中資源包括:CPU、內存空間、磁盤IO等,同一進程中的多條線程共享該進程中的全部系統資源,而進程和進程之間是相互獨立的。進程是具有一定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位。
  • 進程是程序在計算機上的一次執行活動。當你運行一個程序,你就啓動了一個進程。顯然,程序是死的、靜態的,進程是活的、動態的。進程可以分爲系統進程和用戶進程。凡是用於完成操作系統的各種功能的進程就是系統進程,它們就是處於運行狀態下的操作系統本身,用戶進程就是所有由你啓動的進程。

線程是CPU調度的最小單位,必須依賴於進程而存在

  • 線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的、能獨立運行的基本單位。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),但是它可與同屬一個進程的其他的線程共享進程所擁有的全部資源。
  • 線程無處不在。任何一個程序都必須要創建線程,特別是Java不管任何程序都必須啓動一個main函數的主線程; Java Web開發裏面的定時任務、定時器、JSP和 Servlet、異步消息處理機制,遠程訪問接口RM等,任何一個監聽事件, onclick的觸發事件等都離不開線程和併發的知識。

CPU核心數和線程數的關係

  • 多核心:也指單芯片多處理器( Chip Multiprocessors,簡稱CMP)。CMP是由美國斯坦福大學提出的,其思想是將大規模並行處理器中的SMP(對稱多處理器)集成到同一芯片內,各個處理器並行執行不同的進程。這種依靠多個CPU同時並行地運行程序是實現超高速計算的一個重要方向,稱爲並行處理
  • 多線程: Simultaneous Multithreading,簡稱SMT。讓同一個處理器上的多個線程同步執行並共享處理器的執行資源。
  • 核心數、線程數:目前主流CPU都是多核的。增加核心數目就是爲了增加線程數,因爲操作系統是通過線程來執行任務的,一般情況下它們是1:1對應關係,也就是說四核CPU一般擁有四個線程。但 Intel引入超線程技術後,使核心數與線程數形成1:2的關係。當然,這裏的核心就是所謂的邏輯核心了。

查看計算機的邏輯核心數:

System.out.println(Runtime.getRuntime().availableProcessors());

CPU時間片輪轉機制

        我們平時在開發的時候,感覺並沒有受cpu核心數的限制,想啓動線程就啓動線程,哪怕是在單核CPU上,爲什麼?這是因爲操作系統提供了一種CPU時間片輪轉機制。

        時間片輪轉調度是一種最古老、最簡單、最公平且使用最廣的算法,又稱RR調度。每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。

        百度百科對CPU時間片輪轉機制原理解釋如下:
        時間片輪轉調度是一種最古老,最簡單,最公平且使用最廣的算法。每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。如果在時間片結束時進程還在運行,則CPU將被剝奪並分配給另一個進程。如果進程在時間片結束前阻塞或結束,則CPU當即進行切換。調度程序所要做的就是維護一張就緒進程列表,當進程用完它的時間片後,它被移到隊列的末尾。
        時間片輪轉調度中唯一有趣的一點是時間片的長度。從一個進程切換到另一個進程是需要一定時間的–保存和裝入寄存器值及內存映像,更新各種表格和隊列等。假如進程切換(process switch) - 有時稱爲上下文切換(context switch),需要5毫秒,再假設時間片設爲20毫秒,則在做完20毫秒有用的工作之後,CPU將花費5毫秒來進行進程切換。CPU時間的20%被浪費在了管理開銷上。
        爲了提高CPU效率,我們可以將時間片設爲500毫秒。這時浪費的時間只有1%。但考慮在一個分時系統中,如果有十個交互用戶幾乎同時按下回車鍵,將發生什麼情況?假設所有其他進程都用足它們的時間片的話,最後一個不幸的進程不得不等待5秒鐘才獲得運行機會。多數用戶無法忍受一條簡短命令要5秒鐘才能做出響應。同樣的問題在一臺支持多道程序的個人計算機上也會發生。
        結論可以歸結如下:時間片設得太短會導致過多的進程切換,降低了CPU效率;而設得太長又可能引起對短的交互請求的響應變差。將時間片設爲100毫秒通常是一個比較合理的折中。

        在CPU死機的情況下,其實大家不難發現當運行一個程序的時候把CPU給弄到了100%再不重啓電腦的情況下,其實我們還是有機會把它kill掉的,我想也正是因爲這種機制的緣故。

澄清並行和併發

        當談論併發的時候一定要加個單位時間,也就是說單位時間內併發量是多少?離開了單位時間其實是沒有意義的。
        俗話說,一心不能二用,這對計算機也一樣,原則上一個CPU只能分配給一個進程,以便運行這個進程。我們通常使用的計算機中只有一個CPU,也就是說只有一顆心,要讓它一心多用同時運行多個進程,就必須使用併發技術。實現併發技術相當複雜,最容易理解的是“時間片輪轉進程調度算法”。

綜合來說:

  • 併發:指應用能夠交替執行不同的任務,比如單CPU核心下執行多線程並非是同時執行多個任務,如果你開兩個線程執行,就是在你幾乎不可能察覺到的速度不斷去切換這兩個任務,已達到"同時執行效果",其實並不是的,只是計算機的速度太快,我們無法察覺到而已。
  • 並行:指應用能夠同時執行不同的任務。

兩者區別:一個是交替執行,一個是同時執行。
併發和並行

高併發編程的意義、好處和注意事項

由於多核多線程的CPU的誕生,多線程、高併發的編程越來越受重視和關注。
多線程可以給程序帶來如下好處:

  1. 充分利用CPU的資源
            從上面的CPU的介紹,可以看的出來,現在市面上沒有CPU的內核不使用多線程併發機制的,特別是服務器還不止一個CPU,如果還是使用單線程的技術做思路,明顯就out了。因爲程序的基本調度單元是線程,並且一個線程也只能在一個CPU的一個核的一個線程跑,如果你是個i3的CPU的話,最差也是雙核心4線程的運算能力:如果是一個線程的程序的話,那是要浪費3/4的CPU性能:如果設計一個多線程的程序的話,那它就可以同時在多個CPU的多個核的多個線程上跑,可以充分地利用CPU,減少CPU的空閒時間,發揮它的運算能力,提高併發量。
  2. 加快響應用戶的時間
            比如我們經常用的迅雷下載,都喜歡多開幾個線程去下載,誰都不願意用一個線程去下載,爲什麼呢?答案很簡單,就是多個線程下載快啊。我們在做程序開發的時候更應該如此,特別是我們做互聯網項目,網頁的響應時間若提升1s,如果流量大的話,就能增加不少轉換量。做過高性能web前端調優的都知道,要將靜態資源地址用兩三個子域名去加載,爲什麼?因爲每多一個子域名,瀏覽器在加載你的頁面的時候就會多開幾個線程去加載你的頁面資源,提升網站的響應速度。多線程,高併發真的是無處不在。
  3. 可以使你的代碼模塊化,異步化,簡單化
            例如我們實現電商系統,下訂單和給用戶發送短信、郵件就可以進行拆分,將給用戶發送短信、郵件這兩個步驟獨立爲單獨的模塊,並交給其他線程去執行。這樣既增加了異步的操作,提升了系統性能,又使程序模塊化,清晰化和簡單化。
    多線程應用開發的好處還有很多,大家在日後的代碼編寫過程中可以慢慢體會它的魅力。

多線程程序需要注意事項:

  1. 線程之間的安全性
            在同一個進程裏面的多線程是資源共享的,也就是都可以訪問同一個內存地址當中的一個變量。例如:若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全。若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
  2. 線程之間的死鎖
            爲了解決線程之間的安全性引入了Java的鎖機制,而一不小心就會產生Java線程死鎖的多線程問題,因爲不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個飢餓的人,他們必須共享刀叉並輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。
    假如線程A獲得了刀,而線程B獲得了叉。線程A就會進入阻塞狀態來等待獲得叉,而線程B則阻塞來等待線程A所擁有的刀。這只是人爲設計的例子,但儘管在運行時很難探測到,這類情況卻時常發生。
  3. 線程太多了會將服務器資源耗盡形成死機當機
            線程數太多有可能造成系統創建大量線程而導致消耗完系統內存以及CPU的“過渡切換”,造成系統的死機,那麼我們該如何解決這類問題呢?
            某些系統資源是有限的,如文件描述符。多線程程序可能耗盡資源,因爲每個線程都可能希望有一個這樣的資源。如果線程數相當大,或者某個資源的侯選線程數遠遠超過了可用的資源數則最好使用資源池。一個最好的示例是數據庫連接池。只要線程需要使用一個數據庫連接,它就從池中取出一個,使用以後再將它返回池中。資源池也稱爲資源庫。

認識Java裏的線程

Java程序天生就是多線程的
一個Java程序從main()方法開始執行,然後按照既定的代碼邏輯執行,看似沒有其他線程參與,但實際上Java程序天生就是多線程程序,因爲執行main()方法的是一個名稱爲main的線程。

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

/**
 *類說明:只有一個main方法的程序
 */
public class OnlyMain {
    public static void main(String[] args) {
        //Java 虛擬機線程系統的管理接口
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要獲取同步的monitor和synchronizer信息,僅僅獲取線程和線程堆棧信息
        ThreadInfo[] threadInfos =
                threadMXBean.dumpAllThreads(false, false);
        // 遍歷線程信息,僅打印線程ID和線程名稱信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] "
                    + threadInfo.getThreadName());
        }
    }
}
[6] Monitor Ctrl-Break //監控Ctrl-Break中斷信號的
[5] Attach Listener //內存dump,線程dump,類信息統計,獲取系統屬性等
[4] Signal Dispatcher  // 分發處理髮送給JVM信號的線程
[3] Finalizer  // 調用對象finalize方法的線程
[2] Reference Handler//清除Reference的線程
[1] main //main線程,用戶程序入口

線程的啓動與中止

啓動

啓動線程的方式
1、X extends Thread;,然後X.start
2、X implements Runnable;然後交給Thread運行

/**
 *類說明:新啓線程的方式
 */
public class NewThread {
	/*擴展自Thread類*/
	private static class UseThread extends Thread{
		@Override
		public void run() {
			super.run();
			// do my work;
			System.out.println("I am extended Thread");
		}
	}
	/*實現Runnable接口*/
	private static class UseRunnable implements Runnable{
		public void run() {
			// do my work;
			System.out.println("I am implements Runnable");
		}
	}
	

	public static void main(String[] args){
		new UseThread().start();
		new Thread(new UseRunnable()).start();

		new Thread(){
			@Override
			public void run() {
				super.run();
				// do my work;
				System.out.println("I am extended Thread");
			}
		}.start();
		new Thread(() -> {
			// do my work;
			System.out.println("I am implements Runnable");
		}).start();
		
	}
}

I am extended Thread
I am implements Runnable
I am extended Thread
I am implements Runnable

Thread和Runnable的區別

Thread纔是Java裏對線程的唯一抽象,Runnable只是對任務(業務邏輯)的抽象。Thread可以接受任意一個Runnable的實例並執行。

中止

線程自然終止
要麼是run執行完成了,要麼是拋出了一個未處理的異常導致線程提前結束。
stop()
暫停、恢復和停止操作對應在線程Thread的API就是suspend()、resume()和stop()。但是這些API是過期的,也就是不建議使用的。不建議使用的原因主要有:以suspend()方法爲例,在調用後,線程不會釋放已經佔有的資源(比如鎖),而是佔有着資源進入睡眠狀態,這樣容易引發死鎖問題。同樣,stop()方法在終結一個線程時不會保證線程的資源正常釋放,通常是沒有給予線程完成資源釋放工作的機會,因此會導致程序可能工作在不確定狀態下。正因爲suspend()、resume()和stop()方法帶來的副作用,這些方法才被標註爲不建議使用的過期方法。
中止
安全的中止則是其他線程通過調用某個線程A的interrupt()方法對其進行中斷操作, 中斷好比其他線程對該線程打了個招呼,“A,你要中斷了”,不代表線程A會立即停止自己的工作,同樣的A線程完全可以不理會這種中斷請求。因爲java裏的線程是協作式的,不是搶佔式的。線程通過檢查自身的中斷標誌位是否被置爲true來進行響應。
線程通過方法 isInterrupted() 來進行判斷是否被中斷,也可以調用靜態方法 Thread.interrupted() 來進行判斷當前線程是否被中斷,不過Thread.interrupted()會同時將中斷標識位改寫爲false。
如果一個線程處於了阻塞狀態(如線程調用了thread.sleep、thread.join、thread.wait等),則在線程在檢查中斷標示時如果發現中斷標示爲true,則會在這些阻塞方法調用處拋出InterruptedException異常,並且在拋出異常後會立即將線程的中斷標示位清除,即重新設置爲false。

/**
 *類說明:阻塞方法中拋出InterruptedException異常後,如果需要繼續中斷,需要手動再中斷一次
 */
public class HasInterrputException {

	private static class UseThread extends Thread{

		public UseThread(String name) {
			super(name);
		}

		@Override
		public void run() {
			while(!isInterrupted()) {
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) {
					System.out.println(Thread.currentThread().getName()
							+" in InterruptedException interrupt flag is "
							+isInterrupted());
					//資源釋放
//					interrupt();
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName()
						+ " I am extends Thread.");
			}
			System.out.println(Thread.currentThread().getName()
					+" interrupt flag is "+isInterrupted());
		}
	}

	public static void main(String[] args) throws InterruptedException {
		Thread endThread = new UseThread("HasInterrputEx");
		endThread.start();
		Thread.sleep(1000);
		endThread.interrupt();//中斷線程,其實設置線程的標識位true
	}
}

結果:
結果
如果將interrupt();這一行註釋取消掉:
結果
不建議自定義一個取消標誌位來中止線程的運行。因爲run方法裏有阻塞調用時會無法很快檢測到取消標誌,線程必須從阻塞調用返回後,纔會檢查這個取消標誌。這種情況下,使用中斷會更好,因爲,
一、一般的阻塞方法,如sleep等本身就支持中斷的檢查。
二、檢查中斷位的狀態和檢查取消標誌位沒什麼區別,用中斷位的狀態還可以避免聲明取消標誌位,減少資源的消耗。
注意:處於死鎖狀態的線程無法被中斷

深入理解run()和start()

Thread類是Java裏對線程概念的抽象,可以這樣理解:我們通過new Thread()其實只是new出一個Thread的實例,還沒有操作系統中真正的線程掛起鉤來。只有執行了start()方法後,才實現了真正意義上的啓動線程。
start()方法讓一個線程進入就緒隊列等待分配cpu,分到cpu後才調用實現的run()方法,start()方法不能重複調用,如果重複調用會拋出異常。
而run方法是業務邏輯實現的地方,本質上和任意一個類的任意一個成員方法並沒有任何區別,可以重複執行,也可以被單獨調用。

Java線程的生命週期

線程的生命週期

線程的優先級

在Java線程中,通過一個整型成員變量priority來控制優先級,優先級的範圍從1~10,在線程構建的時候可以通過setPriority(int)方法來修改優先級,默認優先級是5,優先級高的線程分配時間片的數量要多於優先級低的線程。
設置線程優先級時,針對頻繁阻塞(休眠或者I/O操作)的線程需要設置較高優先級,而偏重計算(需要較多CPU時間或者偏運算)的線程則設置較低的優先級,確保處理器不會被獨佔。在不同的JVM以及操作系統上,線程規劃會存在差異,有些操作系統甚至會忽略對線程優先級的設定。

線程的調度

線程調度是指系統爲線程分配CPU使用權的過程,主要調度方式有兩種:

  • 協同式線程調度(Cooperative Threads-Scheduling):線程執行的時間由線程本身來控制,線程把自己的工作執行完之後,要主動通知系統切換到另外一個線程上。使用協同式線程調度的最大好處是實現簡單,由於線程要把自己的事情做完後纔會通知系統進行線程切換,所以沒有線程同步的問題,但是壞處也很明顯,如果一個線程出了問題,則程序就會一直阻塞。
  • 搶佔式線程調度(Preemptive Threads-Scheduling):使用搶佔式線程調度的多線程系統,每個線程執行的時間以及是否切換都由系統決定。在這種情況下,線程的執行時間不可控,所以不會有一個線程導致整個進程阻塞的問題出現。

在Java中,Thread.yield()可以讓出CPU執行時間,但是對於獲取,線程本身是沒有辦法的。對於獲取CPU執行時間,線程唯一可以使用的手段是設置線程優先級,Java設置了10個級別的程序優先級,當兩個線程同時處於Ready狀態時,優先級越高的線程越容易被系統選擇執行。

Java中的線程是通過映射到操作系統的原生線程上實現的,所以線程的調度最終取決於操作系統,而操作系統級別,OS是以搶佔式調度線程,我們可以認爲線程是搶佔式的。Java虛擬機採用搶佔式調度模型,是指優先讓可運行池中優先級高的線程佔用CPU,如果可運行池中的線程優先級相同,那麼就隨機選擇一個線程,使其佔用CPU。處於運行狀態的線程會一直運行,直至它不得不放棄CPU。而且操作系統中線程的優先級有時並不能和Java中的一一對應,所以Java優先級並不是特別靠譜。但是在Java中,因爲Java沒有提供安全的搶佔式方法來停止線程,要安全的停止線程只能以協作式的方式。

守護線程

Daemon(守護)線程是一種支持型線程,因爲它主要被用作程序中後臺調度以及支持性工作。這意味着,當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。可以通過調用Thread.setDaemon(true)將線程設置爲Daemon線程。我們一般用不上,比如垃圾回收線程就是Daemon線程。
Daemon線程被用作完成支持性工作,但是在Java虛擬機退出時Daemon線程中的finally塊並不一定會執行。在構建Daemon線程時,不能依靠finally塊中的內容來確保執行關閉或清理資源的邏輯。

線程間的共享

synchronized內置鎖

線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那麼沒有一點兒價值,或者說價值很少,如果多個線程能夠相互配合完成工作,包括數據之間的共享,協同處理事情。這將會帶來巨大的價值。
Java支持多個線程同時訪問一個對象或者對象的成員變量,關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱爲內置鎖機制。

對象鎖和類鎖

對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。
但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,類鎖其實鎖的是每個類的對應的class對象。類鎖和對象鎖之間也是互不干擾的。

volatile,最輕量的同步機制

volatile保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

/**
 * 類說明:演示Volatile的提供的可見性
 */
public class VolatileCase {
    private  static boolean ready;
//    private  static volatile boolean ready;
    private static int number;

    private static class PrintThread extends Thread{
        @Override
        public void run() {
            System.out.println("PrintThread is running.......");
            while(!ready);
            System.out.println("number = "+number);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new PrintThread().start();
        Thread.sleep(1000);
        number = 51;
        ready = true;
        Thread.sleep(5000);
        System.out.println("main is ended!");
    }
}

結果

//    private  static boolean ready;
    private  static volatile boolean ready;

在這裏插入圖片描述
不加volatile時,子線程無法感知主線程修改了ready的值,從而不會退出循環,而加了volatile後,子線程可以感知主線程修改了ready的值,迅速退出循環。
但是volatile不能保證數據在多個線程下同時寫時的線程安全。該上鎖的還是得上鎖。
volatile最適用的場景:一個線程寫,多個線程讀

ThreadLocal辨析

與Synchonized的比較

ThreadLocal和Synchonized都用於解決多線程併發訪問。

  • Synchronized:利用鎖的機制,使變量或代碼塊在某一時該僅僅能被一個線程訪問。
  • ThreadLocal:爲每個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。

Spring的事務就藉助了ThreadLocal類。Spring會從數據庫連接池中獲得一個connection,然會把connection放進ThreadLocal中,也就和線程綁定了,事務需要提交或者回滾,只要從ThreadLocal中拿到connection進行操作。

爲何Spring的事務要藉助ThreadLocal類

以JDBC爲例,正常的事務代碼可能如下:
dbc = new DataBaseConnection();//第1行
Connection con = dbc.getConnection();//第2行
con.setAutoCommit(false);// //第3行
con.executeUpdate(…);//第4行
con.executeUpdate(…);//第5行
con.executeUpdate(…);//第6行
con.commit();////第7行
上述代碼,可以分成三個部分:
事務準備階段:第1~3行
業務處理階段:第4~6行
事務提交階段:第7行
可以很明顯的看到,不管我們開啓事務還是執行具體的sql都需要一個具體的數據庫連接。
現在我們開發應用一般都採用三層結構,如果我們控制事務的代碼都放在DAO(DataAccessObject)對象中,在DAO對象的每個方法當中去打開事務和關閉事務,當Service對象在調用DAO時,如果只調用一個DAO,那我們這樣實現則效果不錯,但往往我們的Service會調用一系列的DAO對數據庫進行多次操作,那麼,這個時候我們就無法控制事務的邊界了,因爲實際應用當中,我們的Service調用的DAO的個數是不確定的,可根據需求而變化,而且還可能出現Service調用Service的情況。
如果不使用ThreadLocal,代碼大概就會是這個樣子:
在這裏插入圖片描述在這裏插入圖片描述
但是需要注意一個問題,如何讓三個DAO使用同一個數據源連接呢?我們就必須爲每個DAO傳遞同一個數據庫連接,要麼就是在DAO實例化的時候作爲構造方法的參數傳遞,要麼在每個DAO的實例方法中作爲方法的參數傳遞。這兩種方式無疑對我們的Spring框架或者開發人員來說都不合適。爲了讓這個數據庫連接可以跨階段傳遞,又不顯示的進行參數傳遞,就必須使用別的辦法。
Web容器中,每個完整的請求週期會由一個線程來處理。因此,如果我們能將一些參數綁定到線程的話,就可以實現在軟件架構中跨層次的參數共享(是隱式的共享)。而JAVA中恰好提供了綁定的方法–使用ThreadLocal。
結合使用Spring裏的IOC和AOP,就可以很好的解決這一點。
只要將一個數據庫連接放入ThreadLocal中,當前線程執行時只要有使用數據庫連接的地方就從ThreadLocal獲得就行了。

ThreadLocal的使用

ThreadLocal類接口常用的4個方法:

  • void set(Object value)
    設置當前線程的線程局部變量的值。
  • public Object get()
    該方法返回當前線程所對應的線程局部變量。
  • public void remove()
    將當前線程局部變量的值刪除,目的是爲了減少內存的佔用,該方法是JDK 5.0新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量將自動被垃圾回收,所以顯式調用該方法清除線程的局部變量並不是必須的操作,但它可以加快內存回收的速度。
  • protected Object initialValue()
    返回該線程局部變量的初始值,該方法是一個protected的方法,顯然是爲了讓子類覆蓋而設計的。這個方法是一個延遲調用方法,在線程第1次調用get()或set(Object)時才執行,並且僅執行1次。ThreadLocal中的缺省實現直接返回一個null。

public final static ThreadLocal< String > RESOURCE = new ThreadLocal< String >();
RESOURCE代表一個能夠存放String類型的ThreadLocal對象。此時不論什麼一個線程能夠併發訪問這個變量,對它進行寫入、讀取操作,都是線程安全的。

有示例如下:

/**
 *類說明:演示ThreadLocal的使用
 */
public class UseThreadLocal {
	
	private static ThreadLocal<Integer> intLocal
            = ThreadLocal.withInitial(() -> 1);

    /**
     * 運行3個線程
     */
    public void StartThreadArray(){
        Thread[] runs = new Thread[3];
        for(int i=0;i<runs.length;i++){
            runs[i]=new Thread(new TestThread(i));
        }
        for(int i=0;i<runs.length;i++){
            runs[i].start();
        }
    }
    
    /**
     *類說明:測試線程,線程的工作是將ThreadLocal變量的值變化,並寫回,看看線程之間是否會互相影響
     */
    public static class TestThread implements Runnable{
        int id;
        public TestThread(int id){
            this.id = id;
        }
        public void run() {
            System.out.println(Thread.currentThread().getName()+":start");
            Integer s = intLocal.get();
            s = s+id;
            intLocal.set(s);
            System.out.println(Thread.currentThread().getName()
                    +":"+ intLocal.get());
        }
    }

    public static void main(String[] args){
    	new UseThreadLocal().StartThreadArray();
    }
}

結果:

Thread-1:start
Thread-2:start
Thread-0:start
Thread-1:2
Thread-2:3
Thread-0:1

實現解析

在這裏插入圖片描述

看一下ThreadLocal的源碼:

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

上面先取到當前線程,然後調用 getMap 方法獲取對應的 ThreadLocalMap,ThreadLocalMap 是ThreadLocal 的靜態內部類,然後 Thread 類中有一個這樣類型成員,所以 getMap 是直接返回Thread的成員。

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
       }
private void set(ThreadLocal<?> key, Object value) {
		...
        }
...

可以看到有個 Entry 內部靜態類,它繼承了 WeakReference ,是一個弱引用對象。Entry ThreadLocal 與副本對象,做成了 key-value 的形式。

引發的內存泄漏分析

預備知識

引用
Object o = new Object();
這個o,我們可以稱之爲對象引用,而new Object()我們可以稱之爲在內存中產生了一個對象實例。
當寫下 o=null時,只是表示o不再指向堆中object的對象實例,不代表這個對象實例不存在了。

  • 強引用:指在程序代碼之中普遍存在的,類似“Object obj=new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象實例。
  • 軟引用:用來描述一些還有用但並非必需的對象。僅軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象實例列進回收範圍之中進行回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。
  • 弱引用:用來描述非必需對象的,但是它的強度比軟引用更弱一些,僅被弱引用關聯的對象實例只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象實例。在JDK 1.2之後,提供了WeakReference類來實現弱引用。
  • 虛引用:也稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象實例是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象實例被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實現虛引用。
內存泄漏的現象

示例如下:

**
 * 類說明:ThreadLocal造成的內存泄漏演示
 */
public class ThreadLocalOOM {
    private static final int TASK_LOOP_SIZE = 500;
	//我們啓用一個線程池,大小固定爲5個線程
    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5,
            1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static class LocalVariable {
        private byte[] a = new byte[1024*1024*5];/*5M大小的數組*/
    }

    final static ThreadLocal<LocalVariable> localVariable
            = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        /*5*5=25*/
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                	//首先只簡單的在每個任務中new出一個數組
                    new LocalVariable();
                    //localVariable.set(new LocalVariable());
                    //localVariable.remove();
                }
            });

            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }

}

並將堆內存大小設置爲-Xmx256m:
在這裏插入圖片描述
結果:
在這裏插入圖片描述
可以看到內存的實際使用控制在25M左右:因爲每個任務中會不斷new出一個5M的數組,5*5=25M,這是很合理的。
當我們啓用了ThreadLocal以後:

//new LocalVariable();
localVariable.set(new LocalVariable());

結果:
在這裏插入圖片描述
內存佔用最高升至150M,一般情況下穩定在90M左右,那麼加入一個ThreadLocal後,內存的佔用真的會這麼多?
於是,我們加入一行代碼:

localVariable.remove();

結果:
在這裏插入圖片描述
可以看見最高峯的內存佔用也在25M左右,和我們不加ThreadLocal表現一樣。
這就充分說明,確實發生了內存泄漏。

分析

根據我們前面對ThreadLocal的分析,我們可以知道每個Thread 維護一個 ThreadLocalMap,這個映射表的 key 是 ThreadLocal實例本身,value 是真正需要存儲的 Object,也就是說 ThreadLocal 本身並不存儲值,它只是作爲一個 Key 來讓線程從 ThreadLocalMap 獲取 value。仔細觀察ThreadLocalMap,這個map是使用 ThreadLocal 的弱引用作爲 Key 的,弱引用的對象在 GC 時會被回收。
因此使用了ThreadLocal後,引用鏈如圖所示:
在這裏插入圖片描述
圖中的虛線表示弱引用。
這樣,當把 ThreadLocalRef 變量置爲 null 以後,只有一個虛引用引用指向 ThreadLocal 實例,所以Threadlocal 將會被 GC 回收。這樣一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry,就沒有辦法訪問這些 key 爲 null 的 Entry 的 value,如果當前線程再遲遲不結束的話,這些 key 爲 null 的Entry 的 value 就會一直存在一條強引用鏈:Current Thread Ref -> Current Thread -> ThreaLocalMap -> Entry -> value,而這塊 value 永遠不會被訪問到了,所以存在着內存泄露。
只有當前線程結束以後,Current Thread Ref 就不會存在棧中,強引用斷開,Current Thread、Map、 Entry、value 將全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 變量後,都調用它的remove()方法,清除數據。
其實考察ThreadLocal的實現,我們可以看見,無論是get()、set()在某些時候,調用了expungeStaleEntry方法用來清除Entry中Key爲null的Value,但是這是不及時的,也不是每次都會執行的,所以一些情況下還是會發生內存泄露。只有remove()方法中顯式調用了expungeStaleEntry方法。

從表面上看內存泄漏的根源在於使用了弱引用,但是另一個問題也同樣值得思考:爲什麼使用弱引用而不是強引用?
下面我們分兩種情況討論:
key 使用強引用:ThreadLocalMap 一直持有 ThreadLocal 的強引用,如果沒有手動置空這個強引用,ThreadLocal 的對象實例不會被回收,導致 ThreadLocal 和 Entry 一直不能被回收。
key 使用弱引用:ThreadLocalMap 持有 ThreadLocal 的弱引用,ThreadLocal 的對象如果不再被強引用指向被回收。Entry 不能被回收,但在下一次 ThreadLocalMap 調用 set,get,remove 都有機會被回收。
比較兩種情況,我們可以發現:由於 ThreadLocalMap 的生命週期跟 Thread 一樣長,如果都沒有手動刪除 Entry ,都會導致內存泄漏,但是使用弱引用可以讓 ThreadLocal 被回收。
因此,ThreadLocal 內存泄漏的根源是:由於 ThreadLocalMap 的生命週期跟 Thread 一樣長,如果沒有手動刪除 ThreadLocalMap 裏的 Entry 就會導致內存泄漏,而不是因爲弱引用。

總結

JVM利用設置 ThreadLocalMap 的 Entry 爲弱引用對象,來避免 ThreadLocal 內存泄露。
JVM利用調用 remove、get、set 方法的時候,回收弱引用對象 Entry,來避免 Entry 內存泄露 。
當ThreadLocal存儲很多 key 爲 null 的 Entry 的時候,而不再去調用 remove、get、set 方法,那麼將導致內存泄漏。
使用線程池+ ThreadLocal 時要小心,因爲這種情況下,線程是一直在不斷的重複運行的,從而也就造成了 value 可能造成累積的情況。

關於弱引用弱引用對象的區別,請看 有關“引用”、“對象”、“引用對象”的誤解和看過源碼說明後的理解

錯誤使用ThreadLocal導致線程不安全

示例如下:

/**
 * 類說明:ThreadLocal的線程不安全演示
 */
public class ThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);

    public void run() {
        //每個線程計數加一
        number.setNum(number.getNum()+1);
        //將其存儲到ThreadLocal中
        value.set(number);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //輸出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

}

結果:

Thread-3=5
Thread-0=5
Thread-1=5
Thread-4=5
Thread-2=5

爲什麼每個線程都輸出5而不是1?難道他們沒有獨自保存自己的Number副本嗎?爲什麼其他線程還是能夠修改這個值?
仔細考察ThreadLocal和Thead的代碼,我們發現ThreadLocalMap中保存的其實是對象的一個引用,這樣的話,當有其他線程對這個引用指向的對象實例做修改時,其實也同時影響了所有的線程持有的對象引用所指向的同一個對象實例。這也就是爲什麼上面的程序爲什麼會輸出一樣的結果:5個線程中保存的是同一Number對象的引用(因爲被static修飾了),在線程睡眠的時候,其他線程將num變量進行了修改,而修改的對象Number的實例是同一份,因此它們最終輸出的結果是相同的。
而上面的程序要正常的工作,應該的用法是讓每個線程中的ThreadLocal都應該持有一個新的Number對象(最簡單的方法是去掉static)。

線程間的協作

線程之間相互配合,完成某項工作,比如:一個線程修改了一個對象的值,而另一個線程感知到了變化,然後進行相應的操作,整個過程開始於一個線程,而最終執行又是另一個線程。前者是生產者,後者就是消費者,這種模式隔離了“做什麼”(what)和“怎麼做”(How),簡單的辦法是讓消費者線程不斷地循環檢查變量是否符合預期在while循環中設置不滿足的條件,如果條件滿足則退出while循環,從而完成消費者的工作。卻存在如下問題:
1)難以確保及時性。
2)難以降低開銷。如果降低睡眠的時間,比如休眠1毫秒,這樣消費者能更加迅速地發現條件變化,但是卻可能消耗更多的處理器資源,造成了無端的浪費。

等待/通知機制

是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。

wait,notify/notifyAll

  • notify():
    通知一個在對象上等待的線程,使其從wait方法返回,而返回的前提是該線程獲取到了對象的鎖,沒有獲得鎖的線程重新進入WAITING狀態。
  • notifyAll():
    通知所有等待在該對象上的線程。
  • wait()
    調用該方法的線程進入 WAITING狀態,只有等待另外線程的通知或被中斷纔會返回。需要注意,調用wait()方法後,會釋放對象的鎖
  • wait(long)
    超時等待一段時間,這裏的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回
  • wait (long,int)
    對於超時時間更細粒度的控制,可以達到納秒

等待和通知的標準範式

等待方遵循如下原則。
1)獲取對象的鎖。
2)如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
3)條件滿足則執行對應的邏輯。

 synchronized(對象){
 	while(條件不滿足){
 	對象.wait();
	 }
	//業務邏輯 
 }

通知方遵循如下原則。
1)獲得對象的鎖。
2)改變條件。
3)通知所有等待在對象上的線程。

synchronized(對象){
	//業務邏輯,改變條件
	對象.notify()/notifyAll();
}

在調用wait()、notify()系列方法之前,線程必須要獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait()方法、notify()系列方法。進入wait()方法後,當前線程釋放鎖,在從wait()返回前,線程與其他線程競爭重新獲得鎖, 執行notify()系列方法的線程退出調用了notifyAll的synchronized代碼塊的時候後,他們就會去競爭。如果其中一個線程獲得了該對象鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖後,其他的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。

notify和notifyAll應該用誰

儘可能用notifyall(),謹慎使用notify(),因爲notify()只會喚醒一個線程,我們無法確保被喚醒的這個線程一定就是我們需要喚醒的線程。

示例如下:

/**
 *類說明:測試wait/notifyAll
 */
public class TestWN {
    private static Express express = new Express(0,Express.CITY);

    /*檢查里程數變化的線程,不滿足條件,線程一直等待*/
    private static class CheckKm extends Thread{
        @Override
        public void run() {
        	express.waitKm();
        }
    }

    /*檢查地點變化的線程,不滿足條件,線程一直等待*/
    private static class CheckSite extends Thread{
        @Override
        public void run() {
        	express.waitSite();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<3;i++){
            new CheckSite().start();
        }
        for(int i=0;i<3;i++){
            new CheckKm().start();
        }

        Thread.sleep(1000);
        express.changeKm();
        Thread.sleep(5000);
        System.out.println("***********************");
        express.changeSite();
    }
}

/**
 *類說明:快遞實體類
 */
public class Express {
    public final static String CITY = "ShangHai";
    private int km;/*快遞運輸里程數*/
    private String site;/*快遞到達地點*/

    public Express(int km, String site) {
        this.km = km;
        this.site = site;
    }

    /* 變化公里數,然後通知處於wait狀態並需要處理公里數的線程進行業務處理*/
    public synchronized void changeKm(){
        this.km = 101;
        notifyAll();
    }

    /* 變化地點,然後通知處於wait狀態並需要處理地點的線程進行業務處理*/
    public  synchronized  void changeSite(){
        this.site = "BeiJing";
        notifyAll();
    }

    /*線程等待公里的變化*/
    public synchronized void waitKm(){
        while(this.km<100){
            try {
                wait();
                System.out.println("Check Site thread["
                                +Thread.currentThread().getId()
                        +"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the Km is "+this.km+",I will change db");
    }

    /*線程等待目的地的變化*/
    public synchronized void waitSite(){
        while(this.site.equals(CITY)){//快遞到達目的地
            try {
                wait();
                System.out.println("Check Site thread["+Thread.currentThread().getId()
                		+"] is be notified");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("the site is "+this.site+",I will call user");
    }
}

結果:

Check Site thread[17] is be notified
the Km is 101,I will change db
Check Site thread[16] is be notified
the Km is 101,I will change db
Check Site thread[15] is be notified
the Km is 101,I will change db
Check Site thread[14] is be notified
Check Site thread[13] is be notified
Check Site thread[12] is be notified
***********************
Check Site thread[12] is be notified
the site is BeiJing,I will call user
Check Site thread[13] is be notified
the site is BeiJing,I will call user
Check Site thread[14] is be notified
the site is BeiJing,I will call user

join()

把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行。比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。

調用yield()、sleep()、wait()、notify()方法對鎖有何影響

  • yield()
    使當前線程讓出CPU佔有權,但讓出的時間是不可設定的。也不會釋放鎖資源。注意:並不是每個線程都需要這個鎖的,而且執行yield( )的線程不一定就會持有鎖,我們完全可以在釋放鎖後再調用yield方法。
    所有執行yield()的線程有可能在進入到就緒狀態後會被操作系統再次選中馬上又被執行。
  • sleep()
    不會釋放鎖資源。
  • wait()
    會釋放鎖,而且當前被喚醒後,會重新去競爭鎖,鎖競爭到後纔會執行wait方法後面的代碼。
  • notify()
    對鎖無影響,線程只有在syn同步代碼執行完後纔會自然而然的釋放鎖,所以notify()系列方法一般都是syn同步代碼的最後一行。

【併發編程】目錄:

【併發編程】之走進Java裏的線程世界

【併發編程】之學會使用線程的併發工具類

【併發編程】之學會使用原子操作CAS

【併發編程】之深入理解顯式鎖和AQS

【併發編程】之一文徹底搞懂併發容器

【併發編程】之Java面試經常會問到的線程池,你搞清楚了嗎?

【併發編程】之Java併發安全知識點總結

【併發編程】之大廠很可能會問到的JMM底層實現原理

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