【Java】【線程同步】Java線程同步全API詳解及代碼實測

前言

這篇博客重點在於講解API功能細節,對於同步異步還沒有清晰概念,或從未使用過線程同步相關API的同學,請先自行補全入門基礎,這裏不再累述

由於Java爲線程API都設置了強制異常檢查,所以編程時需要編寫大量的try-catch代碼,爲了節省這些無意義代碼,讓博客更簡潔清晰,我們封裝了一個Threads工具類,來屏蔽這些異常檢查代碼。比如:

  • Threads.post(runnable)相當於new Thread(runnable).start()
  • Threads.sleep()相當於Thread.sleep()
  • Threads.yield()相當於Thread.yield()
  • Threads.wait(lock)相當於lock.wait()
  • Threads.notify(lock)相當於lock.notify()
  • Threads.join(thread)相當於thread.join()
  • Threads.interrupt(thread)相當於thread.interrupt()

博客中提到的lock對象,均是指作爲同步鎖的對象,任意對象都可以作爲同步鎖

線程狀態

出於線程管理和描述的需要,我們必須清楚地定義出線程的所有狀態或生命週期

  • 新建狀態(NEW):thread剛被new出來,還沒有執行start方法
  • 就緒狀態(READY):thread已經在運行,但是尚未獲得CPU或同步鎖資源
  • 運行狀態(RUNNABLE):thread獲得了CPU和同步鎖資源,正在執行代碼
  • 阻塞狀態(BLOCKED):thread因爲sychronized,sleep,wait,join等原因,暫時停止執行(等待條件成立時,就會復原到就緒狀態)
  • 中止狀態(TERMINATED):thread的run方法執行完畢,任務完成,線程結束

Thread.sleep(long ms)

讓當前線程休眠一段時間,達到指定時間後,再繼續執行後續代碼

synchronized(lock) {…}

同步塊,表示在{…}塊作用域內,當前線程獲得lock對象的訪問和修改權限,也可以形象地說成是,當前線程獲得lock對象鎖
在{…}塊結束之前,其它線程無法訪問或修改lock對象,需要一直等待

這樣說其實並不嚴謹,這只是最簡單的情況,通過Thread.wait等方法可以讓同步塊暫時讓出對線鎖,馬上就會提到

synchronized function() {…}

同步方法,同一時間只有一個線程可以進入被synchronized修飾的方法,只有當前線程退出方法後,其它線程才能進入,相當於是一個方法鎖

lock.wait()

只有在同步塊中,lock被作爲同步鎖時,才能使用此方法
這個方法讓當前線程臨時讓出lock的所有權,當前線程進入阻塞狀態
直到其它線程調用了lock.notify()或lock.notifyAll()通知資源已經釋放,纔會恢復到就緒狀態,重新競爭資源

代碼測試


	//同步鎖
	final Object lock = new Object();
	
	//啓動線程A
	Threads.post(() -> {
	    synchronized (lock) {   //1.由於線程B休眠,所以線程A先進入同步塊,獲得lock所有權
	        Threads.sleep(5000); //2.線程A休眠,保持對lock的所有權
	        System.out.println("A1");
	        Threads.waits(lock);    //4.線程A讓出lock所有權,等待其它線程notify,再重新競爭lock所有權
	        System.out.println("A2");   //由於兩個線程都沒有調用notify,所以A2和B2永遠不會打印,一直阻塞在wait處
	    }
	});
	
	//啓動線程B
	Threads.post(() -> {
	    Threads.sleep(2000);
	    synchronized (lock) {   //3.由於lock被線程A佔有,進入阻塞狀態,等待lock資源
	        System.out.println("B1");   //5.線程B獲得lock所有權
	        Threads.waits(lock);    //6.線程B讓出lock所有權,等待其它線程notify,再重新競爭lock所有權
	        System.out.println("B2");
	    }
	});

lock.wait(long ms)

同wait()功能一樣,但是增加了超時處理
當超出timeout指定的時間時,即使其它線程沒有調用lock.notify(),線程也會自動進入就緒狀態,重新競爭lock資源

代碼測試


	//同步鎖
	final Object lock = new Object();
	
	//啓動線程A
	Threads.post(() -> {
	    synchronized (lock) {   //1.由於線程B休眠,所以線程A先進入同步塊,獲得lock所有權
	        Threads.sleep(5000); //2.線程A休眠,保持對lock的所有權
	        System.out.println("A1");
	        Threads.waits(lock, 5000);    //4.線程A讓出lock所有權,等待其它線程notify或超時,再重新競爭lock所有權
	        System.out.println("A2");   //5.wait超時,線程自動切換到就緒狀態,獲得lock所有權
	    }
	});
	
	//啓動線程B
	Threads.post(() -> {
	    Threads.sleep(2000);
	    synchronized (lock) {   //3.由於lock被線程A佔有,進入阻塞狀態,等待lock資源
	        System.out.println("B1");   //5.線程B獲得lock所有權
	        Threads.waits(lock);    //6.線程B讓出lock所有權,等待其它線程notify,再重新競爭lock所有權
	        System.out.println("B2");   //由於線程A沒有調用notify,所以B2永遠不會打印
	    }
	});

sleep和wait方法的區別

  • sleep是Thread類的方法,wait是Object類的方法
  • sleep(休眠)是當前線程什麼都不做,和其它線程互不影響,沒有如何互動
  • wait(等待)則是交出同步鎖所有權,並且等待其它線程的通知,wait是圍繞着同步鎖進行的線程間互動

lock.notify()

通知另一個正處於wait狀態的線程退出wait狀態,進入就緒狀態,開始競爭lock資源
如果有多個線程都處於wait狀態,具體哪個線程被通知是不確定的,由系統調度決定
還有一點非常重要的就是,線程調用notify,並不意味着就立刻釋放lock鎖,僅僅是通知其它線程而已,其它線程也只是開始競爭資源,並不代表可以立刻得到lock資源,只有notify的線程退出同步塊之後,lock資源纔會被釋放,其它線程纔有可能搶到資源
一般建議將notify放在同步塊的最後一行代碼執行,因爲就算提前執行也沒用,反而容易引起誤會或重複調用

lock.notifyAll()

和nofify方法功能一致,只不過nofify方法只通知一個線程,而notifyAll通知所有處於wait狀態的線程

代碼測試


	//同步鎖
	final Object lock = new Object();
	
	//由於多個線程是併發運行的,沒法控制哪個先執行,這樣就不方便測試
	//但是我們可以通過休眠,來控制有效代碼的執行順序
	//我們讓三個線程分別休眠1s,2s,3s,這樣代碼執行順序就是A-B-C
	
	//啓動線程A
	Threads.post(() -> {
	    Threads.sleep(100);
	    synchronized (lock) {
	        System.out.println("A1");   //1.線程A獲得同步鎖,開始執行代碼
	        Threads.wait(lock); //2.線程A交出同步鎖,進入wait狀態
	        System.out.println("A2");
	    }
	});
	
	//啓動線程B
	Threads.post(() -> {
	    Threads.sleep(200);
	    synchronized (lock) {
	        Threads.sleep(500);    //3.線程B獲得同步鎖,開始執行代碼
	        System.out.println("B1");
	        Threads.wait(lock); //5.線程B交出同步鎖,進入wait狀態
	        System.out.println("B2");
	    }
	});
	
	//啓動線程C
	Threads.post(() -> {
	    Threads.sleep(300);
	    synchronized (lock) {   //4.由於同步鎖被B佔有,無法進入同步塊
	        System.out.println("C1");   //6.A和B都交出了同步鎖,線程C進入同步塊,獲得同步鎖
	        Threads.notify(lock);   //7.通知其中一個wait線程退出阻塞狀態,進入就緒狀態
	        Threads.sleep(500);    //8.由於線程C仍持有同步鎖,wait線程只能繼續等待,雖然它已經是就緒狀態
	        System.out.println("C2");
	    }   //9.線程C釋放同步鎖,wait線程獲得同步鎖,繼續執行代碼,但由於只通知了一個線程,A2和B2只有一個會打印
	});

Thread.yield()

它告訴系統,可以讓當前線程先放棄lock資源,從運行狀態轉入就緒狀態,從而讓其它線程有獲得lock資源的機會
它是一個建議性的API,並不能保證其它線程一定能得到lock資源
因爲就緒狀態的線程仍然會競爭資源,可能剛剛讓出lock資源,馬上又給自己搶到了,但是這樣至少保證了其它線程有得到lock資源的可能性
一般在我們不想壟斷資源,也不想永遠在其它線程之後執行,想讓不同線程隨機自由競爭的時候,就可以使用這個API
由於它是建議性的,運行效果也是隨機的,一般我們並沒有必要調用這個方法,往往我們都是出於"完美主義"的想法,想"公平對待"不同線程,纔會去使用它

值得注意的是,Thread.sleep()和Thread.yield()都是靜態方法,屬於Thread類的方法,而不是屬於某個對象的方法,它們的操作對象都是當前線程

thread.setPriority(int priority)

提到了Thread.yield方法,就順便提下thread.setPriority方法,它也是一個建議性的方法
它爲線程設置不同的優先級,取值範圍爲1-10,數值越大優先級越高
高優先級線程有更高概率獲得CPU資源和鎖資源,但這不是一定生效的,只是給CPU的一個建議

thread.join()

讓另一個線程先執行,執行完再回到當前線程繼續執行,這個thread一般是其它線程
這個方法不涉及同步鎖,僅僅是控制多個線程的執行順序,但也會讓線程進入阻塞狀態

thread.join(long ms)

和thread.join()功能一致,但是增加了超時限制,達到指定時間,即使其它線程未執行完畢,也會繼續執行

代碼測試


	//同步鎖
	final Object lock = new Object();
	
	//創建線程B
	Thread t2 = new Thread(() -> {
	    Threads.sleep(3000);    //3.線程B優先執行,線程A等待
	    System.out.println("B1");
	    System.out.println("B2");
	});    //4.線程B執行完畢
	
	//創建線程A
	Thread t1 = new Thread(() -> {
	    System.out.println("A1");   //1.由於線程B在休眠,所以A先執行
	    Threads.join(t2);   //2.等待B執行完畢,再繼續執行
	    System.out.println("A2");    //5.線程A繼續執行
	});
	
	//啓動線程
	t1.start();
	t2.start();

線程阻塞的幾種情景

  • 由於sychronized無法獲得對線鎖,進入阻塞狀態,其它線程讓出同步鎖時,即可打破阻塞狀態
  • 由於sleep方法,主動進入阻塞狀態,達到超時時間,即可退出阻塞狀態
  • 由於wait方法,主動進入阻塞狀態,收到其它線程的notify,或達到超時時間,即可打破阻塞狀態
  • 由於join方法,主動進入阻塞狀態,其它線程執行完畢,或達到超時時間,即可打破阻塞狀態

thread.interrupt()

中斷一個處於阻塞狀態的線程,並拋出一個InterruptedException
注意兩點,一是隻能中斷處於阻塞狀態的線程,不能中斷處於正常運行狀態的線程,二是中斷後只是拋出一個異常,並不是直接讓線程停止
如果我們正確處理了這個異常,線程還是會繼續往下執行,當然,我們也可以在捕獲到異常時,通過代碼讓線程return或跳到最後一行,從而達到停止線程的目的

注意,interrupt()方法只對sleep,wait,join方法引起的阻塞狀態有效,對sychronized同步鎖造成的阻塞無效

代碼測試


	//同步鎖
	final Object lock = new Object();
	
	//創建線程C
	Thread t3 = new Thread(() -> {
	    synchronized (lock) {
	        Threads.sleep(100000);
	    }
	});
	
	//創建線程A
	Thread t1 = new Thread(() -> {
	    System.out.println("A1");   //1.線程A和C先運行,因爲線程B在休眠
	    try {
	        Threads.join(t3);   //2.等待線程C運行完畢,由於C睡眠時間很長,A會長時間阻塞
	    } catch (Exception e) {     //6.線程A被打斷,拋出InterruptedException異常
	        System.out.println("InterruptedException");
	        return;     //7.捕獲異常,return結束線程,也可以不結束,取決於代碼
	    }
	    System.out.println("A2");   //8.由於線程結束,A2不會被打印
	});
	
	//創建線程B
	Thread t2 = new Thread(() -> {
	    Threads.sleep(1000);
	    System.out.println("B1");   //3.線程B開始運行
	    t1.interrupt();     //4.打斷線程A的阻塞狀態
	    System.out.println("B2");   //5.線程B繼續執行
	});
	
	
	//啓動線程
	t1.start();
	t2.start();
	t3.start();

thread.stop()

強制無條件立刻終止一個線程
這個方法比interrupt更加暴力,它可以在任何情景下立刻終止一個線程,不管線程run方法是否執行完,不管線程是否處於阻塞狀態,或線程是否擁有同步鎖,它都會立刻生效
這是個已經被廢棄的方法,因爲它的結束方式就決定了,這個接口與生俱來的危險性
比如這個線程使用了同時向一個List和一個Map對象寫數據,並給讀寫操作加鎖了,結果這個線程給List寫入了數據,剛準備給Map寫數據的時候,線程就被強制結束了,當其它線程再使用List和Map的時候,使用的已經是不同步的數據了
但是如果確定線程操作不涉及線程同步或內存泄露等問題,使用stop方法還是不錯的選擇的,畢竟它是唯一一個讓線程立即結束的方法,非常簡單

代碼測試


	//創建線程A
	Thread t1 = new Thread(() -> {
	    while (true) {
	        long millis = System.currentTimeMillis();
	        System.out.println(millis);
	    }
	});
	
	//創建線程B
	Thread t2 = new Thread(() -> {
	    Thread.sleeps(2000);
	    t1.stop();
	});
	
	//啓動線程
	t1.start();
	t2.start();

Java源碼中對Thread.State的定義

我們在文章的剛開始,就已經講解過線程狀態的定義,但是我們是從線程工作原理的角度來講的,它適用於所有語言

Java在Thread也定義了一個名爲State的內部類,可以通過thread.getState()來獲取線程的State,Thread源碼中定義的線程狀態則和我們上面定義的有所差異

我們先來看下Java源碼


	package java.lang;

	public class Thread implements Runnable {

	    public enum State {
	        NEW, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
	    }

		private volatile int threadStatus = 0;
	
	    public State getState() {
	        return sun.misc.VM.toThreadState(threadStatus);
	    }
	}

通過源碼我們可以看到以下區別

  • Java源碼中沒有定義就緒狀態,因爲就緒狀態只存在一瞬間,如果競爭資源成功,馬上就會進入運行狀態,如果失敗,則會立刻轉入阻塞狀態,從代碼實現的角度來說,一瞬間的狀態是沒實際意義的,因爲它的值馬上就會發生變化
  • Java源碼中將阻塞狀態分爲了三種:BLOCKED, WAITING, TIMED_WAITING
  • sychronized引起的同步鎖阻塞,用BLOCKED表示
  • wait(),join()引起的無限等待阻塞,用WAITING表示
  • wait(ms),join(ms),sleep(ms)引起的限時等待阻塞,用TIMED_WAITING表示
  • 可以看到,和我們在文章的劃分其實本質上是一樣的,只是表述上的區別。因爲源碼是爲了實現Thread接口功能所設計的,它必須區分每種具體的狀態,才能實現wait,notify這些功能

Java內存模型和volatile關鍵字

Java內存模型我們在這裏不詳解,只是爲了介紹volatile而簡單提下
在Java的內存模型中,爲了保證速度,每個CPU都有一個高速緩存,線程使用變量時,首先訪問的並不是內存中的值,而是CPU緩存中的值。這樣就會出現一個問題,當多個線程使用不同CPU的時候,是從不同的CPU緩存中取值,這樣就有可能會出現變量值不一致的情況
volatile關鍵字能夠保證被其修飾的變量,在數值被改變時,能及時地反映到內存和各個CPU緩存中,這樣就能每個線程獲取到的值是最新的

volatile只能保證取值賦值語句在多線程情況下可以正確執行,並不能保證其它情景下變量值也是同步的,甚至是非常簡單的語句都不行

volatile的作用非常有限,我們看下這個例子就知道了


	private volatile int sum = 0//這就是我們要舉例的語句,非常之簡單
	//但是volatile關鍵字不能保證這條語句能夠正確地執行
	sum++;

	//volatile只能保證下面這種形式的語句能夠按照預期的結果執行
	sum = 100;
	int result = sum;

	//sum++其實相當於以下語句
	sum = sum + 1;
	//再進一步,它其實相當於這樣的語句
	int temp = sum;
	sum = temp + 1;

	//這就是問題的關鍵所在
	//volatile可以保證int temp = sum的正確性
	//volatile也可以保證sum = temp + 1的正確性
	//但是這是對CPU來說,是兩次運算,在兩次運算之間,其它線程可能已經修改了sum的值
	
	//比如我們有10000個線程都在這些sum++這個簡單的代碼
	//我們的線程第一個拿到了sum的值,它的初始值爲0,於是
	//int temp = 0
	//sum = temp + 1 = 1;
	//但是在這兩句之間,其它的線程可能已經讓sum自增了100次,sum的最新值已經變成了100
	//而我們卻還在用舊的sum值在做自增運算,得到1,然後賦值給sum,覆蓋了其它線程的運算結果

	//這顯然不是我們所預期的結果
	//問題的關鍵就在於,sum++是複合操作,它其實相當於多個運算語句
	//而多個運算語句之間,其它線程是有可能插入進來先執行的,讓我們的操作變得無意義
	//所以正如前面所說的,volatile只能保證基本取值賦值語句的正確性

	//從這個例子我們可以看出,volatile的功能其實極其有限
	//不知道大家有沒有悟出來一個結論:
	//volatile其實並不是用來解決線程間的語句同步問題的,而是用來解決CPU之間的變量值同步問題

有了sychronized還需要volatile嗎

看來上節的說明,不知道大家有沒有自己悟出來這樣一點:
volatile其實並不是用來解決線程間的語句同步問題的,而是用來解決CPU之間的變量值同步問題

sychronized關鍵字用於解決線程間的語句同步問題,它將同步塊作爲一個整體,其它線程只有在整個同步塊都退出時,纔有可能修改或訪問同步變量
除此之外,其實sychronized關鍵字也具有保證CPU之間變量值同步的功能,sychronized在進入同步塊時,會清空所有的CPU緩存,從主內存中重新獲得變量值,sychronized在退出同步塊時,會將最新的變量值同步到主內存當中

雖然sychronized的功能要強大於volatile,但是由於sychronized是阻塞式的,它會影響到代碼的執行速度,一個線程在執行,其它線程就要等待。而且sychronized本身在實現上,就比volatile更加複雜,運行效率更低

所以在一些簡單的情況下,比如一個線程只寫值,另一個線程只讀值,也不用關心多線程下語句的執行順序時,使用volatile就足夠了

代碼測試

我們通過代碼來測試下,沒有volatile關鍵字,會不會出現變量值不同步的問題


	@SuppressWarnings("all")
	public class Hello {
	
	    public static Integer value = 0;
	
	    public static void main(String[] args) {
	
	        //線程A先執行,value=0
	        Threads.post(() -> {
	            while (true)
	                if (value != 0)
	                    System.out.println("Value Change");
	            //永遠不會打印,說明線程B的修改沒有反映到線程A中
	            //如果我們在value前加上volatile修飾,則馬上打印,說明volatile確實具有同步數值的作用
	
	            //另外,即使我們不使用volatile關鍵字
	            //如果我們在while (true)裏面添加sychronized或sleep語句,發現也會打印語句
	            //這說明,進入sychronized同步塊或執行sleep語句後,會自動同步數值

            	//注意:這個測試代碼其實是有講究的,我們不能直接通過打印value的值去測試
	            //因爲System.out.println方法本身內部就包含了sychronized代碼在裏面
	            //如果直接打印,必然會造成變量同步,這樣是測不出真實結果的
	        });
	
	        //線程B後執行,修改value的值
	        Threads.post(() -> {
	            Threads.sleep(200);
	            while (true)
	                value = 999;
	        });
	    }
	
	}

工具類代碼

補上工具類代碼,方便大家做伸手黨


	@SuppressWarnings("all")
	public class Threads {
	
	    public static void post(Runnable runnable) {
	        new Thread(runnable).start();
	    }
	
	    public static void sleep(long ms) {
	        try {
	            Thread.sleep(ms);
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void wait(Object lock) {
	        try {
	            lock.wait();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void wait(Object lock, long ms) {
	        try {
	            lock.wait(ms);
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void notify(Object lock) {
	        try {
	            lock.notify();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void notifyAll(Object lock) {
	        try {
	            lock.notifyAll();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void yield() {
	        try {
	            Thread.yield();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	
	    public static void join(Thread thread) {
	        try {
	            thread.join();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void join(Thread thread, long ms) {
	        try {
	            thread.join(ms);
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	
	    public static void interrupt(Thread thread) {
	        try {
	            thread.interrupt();
	        } catch (Exception e) {
	            throw new RuntimeException(e);
	        }
	    }
	}

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