樂觀鎖與悲觀鎖總結
805
2025-03-31
簡介:
線程是操作系統(tǒng)調度的最小單元,在多核環(huán)境中,多個線程能同時執(zhí)行,如果運用得當,能顯著的提升程序的性能。
一、線程初步認識
1、什么是線程
操作系統(tǒng)運行一個程序會為其啟動一個進程。例如,啟動一個Java程序會創(chuàng)建一個Java進程。現(xiàn)代操作系統(tǒng)調度的最小單元是線程,線程也稱為輕量級進程(Light Weight Process),一個進程中可以創(chuàng)建一個到多個線程,線程擁有自己的計數(shù)器、堆棧和局部變量等屬性,并且能訪問共享的內存變量。處理器會通過快速切換這些線程,來執(zhí)行程序。
2、Java本身就是多線程
示例代碼:
package com.lizba.p2; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.util.Arrays; /** *
* *
* * @Author: Liziba * @Date: 2021/6/13 23:03 */ public class MultiThread { public static void main(String[] args) { // 獲取Java線程管理MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 獲取線程和線程堆棧信息; // boolean lockedMonitors = false 不需要獲取同步的monitor信息; // boolean lockedSynchronizers = false 不需要獲取同步的synchronizer信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 打印線程ID和線程name Arrays.stream(threadInfos).forEach(threadInfo -> { System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName()); }); } }輸出結果(不一定一致):
[6]Monitor Ctrl-Break // idea中特有的線程(不用管)
[5]Attach Listener // JVM進程間的通信線程
[4]Signal Dispatcher // 分發(fā)處理發(fā)送給JVM信號的線程
[3]Finalizer // 調用對象的finalizer線程
[2]Reference Handler // 清楚Reference的線程
[1]main // main線程,用戶程序入口
總結:
從輸出結果不難看出,Java程序本身就是多線程的。它不僅僅只有一個main線程在運行,而是main線程和其他多個線程在同時運行。
3、為什么要使用多線程
使用多線程的好處如下:
更多處理器核心
計算機處理器核心數(shù)增多,由以前的高主頻向多核心技術發(fā)展,現(xiàn)在的計算機更擅長于并行計算,因此如何充分利用多核心處理器是現(xiàn)在的主要問題。線程是操作系統(tǒng)調度的最小單元,一個程序作為一個進程來運行,它會創(chuàng)建多個線程,而一個線程在同一時刻只能運行在一個處理器上。因此一個進程如果能使用多線程計算,將其計算邏輯分配到多個處理器核心上,那么相比單線程運行將會有更顯著的性能提升。
更快響應時間
在復雜業(yè)務場景中,我們可以將非強一致性關聯(lián)的業(yè)務派發(fā)給其他線程處理(或者使用消息隊列)。這樣可以減少應用響應用戶請求的時間
更好的編程模型
合理使用Java的提供的多線程編程模型,能使得程序員更好的解決問題,而不需要過于復雜的考慮如何將其多線程化。
4、線程的優(yōu)先級
現(xiàn)代操作系統(tǒng)基本采用的是時間片分配的方式來調度線程,也就是操作系統(tǒng)將CPU的運行分為一個個時間片,線程會分配的若干時間片,當線程時間片用完了,就會發(fā)生線程調度等待下次時間片的分配。線程在一次CPU調度中能執(zhí)行多久,取決于所分時間片的多少,而線程優(yōu)先級就是決定線程需要多或者少分配一些處理器資源的線程屬性。
在Java線程中,線程的優(yōu)先級的可設置范圍是1-10,默認優(yōu)先級是5,理論上優(yōu)先級高的線程分配時間片數(shù)量要優(yōu)先于低的線程(部分操作系統(tǒng)這個設置是不生效的);
示例代碼:
package com.lizba.p2; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** *
* 線程優(yōu)先級設置 *
* * @Author: Liziba * @Date: 2021/6/14 12:03 */ public class Priority { /** 線程執(zhí)行流程控制開關 */ private static volatile boolean notStart = true; /** 線程執(zhí)行流程控制開關 */ private static volatile boolean notEnd = true; public static void main(String[] args) throws InterruptedException { List執(zhí)行結果:
從輸出結果上來看,優(yōu)先級為1的線程和優(yōu)先級為10的線程執(zhí)行的次數(shù)非常相近,因此這表明程序正確性是不能依賴線程的優(yōu)先級高低的。
5、線程的狀態(tài)
線程的生命周期如下:
狀態(tài)名稱
說明
NEW
初始狀態(tài),線程被構建,并未調用start()方法
RUNNABLE
運行狀態(tài),Java線程將操作系統(tǒng)中的就緒和運行兩種狀態(tài)統(tǒng)稱為“運行中”
BLOCKED
阻塞狀態(tài),線程阻塞于鎖
WAITING
等待狀態(tài),線程進入等待狀態(tài),進入該狀態(tài)表示當前線程需要等待其他線程作出一些特定動作(通知或中斷)
TIME_WAITING
超時等待,先比WAITING可以在指定的時間內自行返回
TERMINATED
終止狀態(tài),表示當前線程已經執(zhí)行完畢
通過代碼來查看Java線程的狀態(tài)
代碼示例:
package com.lizba.p2; import java.util.concurrent.TimeUnit; /** *
* 睡眠指定時間工工具類 *
* * @Author: Liziba * @Date: 2021/6/14 13:27 */ public class SleepUtil { public static final void sleepSecond(long seconds) { try { TimeUnit.SECONDS.sleep(seconds); } catch (InterruptedException e) { e.printStackTrace(); } } }package com.lizba.p2; /** *
* 線程狀態(tài)示例代碼 *
* * @Author: Liziba * @Date: 2021/6/14 13:25 */ public class ThreadStateDemo { public static void main(String[] args) { // TimeWaiting new Thread(new TimeWaiting(), "TimeWaitingThread").start(); // Waiting new Thread(new Waiting(), "WaitingThread").start(); // Blocked1和Blocked2一個獲取鎖成功,一個獲取失敗 new Thread(new Blocked(), "Blocked1Thread").start(); new Thread(new Blocked(), "Blocked2Thread").start(); } // 線程不斷的進行睡眠 static class TimeWaiting implements Runnable { @Override public void run() { while (true) { SleepUtil.sleepSecond(100); } } } // 線程等待在Waiting.class實例上 static class Waiting implements Runnable { @Override public void run() { while (true) { synchronized (Waiting.class) { try { Waiting.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } // 該線程Blocked.class實例上加鎖,不會釋放該鎖 static class Blocked implements Runnable { @Override public void run() { synchronized (Blocked.class) { while (true) { SleepUtil.sleepSecond(100); } } } } }使用JPS查看Java進程:
查看示例代碼ThreadStateDemo進程ID是2576,鍵入jstack 2576查看輸出:
整理輸出結果:
線程名稱
線程狀態(tài)
Blocked2Thread
BLOCKED (on object monitor),阻塞在獲取Blocked.class的鎖上
Blocked1Thread
TIMED_WAITING (sleeping)
WaitingThread
WAITING (on object monitor)
TimeWaitingThread
TIMED_WAITING (sleeping)
總結:
線程在自身生命周期中不是規(guī)定處于某一個狀態(tài),而是隨著代碼的執(zhí)行在不同的狀態(tài)之間進行切換。
Java線程的狀態(tài)變化圖如下:
Java線程狀態(tài)變遷圖
總結:
線程創(chuàng)建后,調用start()方法開始運行
線程執(zhí)行wait()方法后,線程進入等待狀態(tài),進入等待的線程需要依靠其他線程才能夠返回到運行狀態(tài)
超時等待相當于在等待朱姑娘太的基礎上增加了超時限制,達到設置的超時時間后返回到運行狀態(tài)
線程執(zhí)行同步方法或代碼塊時,未獲取到鎖的線程,將會進入到阻塞狀態(tài)。
線程執(zhí)行完Runnable的run()方法之后進入到終止狀態(tài)
阻塞在Java的concurrent包中Lock接口的線程是等待狀態(tài),因為Lock接口阻塞的實現(xiàn)使用的是Daemon線程
6、Daemon線程
簡介:
Daemon線程是一種支持型線程,它的主要作用是程序中后臺調度和支持性工作。當一個Java虛擬機中不存在非Daemon線程的時候,Java虛擬機將會退出。Daemon線程需要在啟動之前設置,不能在啟動之后設置。
設置方式:
Thread.setDaemon(true)
需要特別注意的點:
Daemon線程被用作支持性工作的完成,但是在Java虛擬機退出時Daemon線程的finally代碼塊不一定執(zhí)行。
示例代碼:
package com.lizba.p2; /** *
* DaemonRunner線程 *
* * @Author: Liziba * @Date: 2021/6/14 19:50 */ public class DaemonRunner implements Runnable{ @Override public void run() { try { SleepUtil.sleepSecond(100); } finally { System.out.println("DaemonRunner finally run ..."); } } }測試:
package com.lizba.p2; /** *
* *
* * @Author: Liziba * @Date: 2021/6/14 19:59 */ public class DaemonTest { public static void main(String[] args) { Thread t = new Thread(new DaemonRunner(), "DaemonRunner"); t.setDaemon(true); t.start(); } }輸出結果:
總結:
不難發(fā)現(xiàn),DaemonRunner的run方法的finally代碼塊并沒有執(zhí)行,這是因為,當Java虛擬機種已經沒有非Daemon線程時,虛擬機會立即退出,虛擬機中的所以daemon線程需要立即終止,所以線程DaemonRunner會被立即終止,finally并未執(zhí)行。
二、線程啟動和終止
1、構造線程
運行線程之前需要構造一個線程對象,線程對象在構造的時候需要設置一些線程的屬性,這些屬性包括線程組、線程的優(yōu)先級、是否時daemon線程、線程名稱等信息。
代碼示例:
來自java.lang.Thread
/** * Initializes a Thread. * * @param g the Thread group * @param target the object whose run() method gets called * @param name the name of the new Thread * @param stackSize the desired stack size for the new thread, or * zero to indicate that this parameter is to be ignored. * @param acc the AccessControlContext to inherit, or * AccessController.getContext() if null * @param inheritThreadLocals if {@code true}, inherit initial values for * inheritable thread-locals from the constructing thread */ 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(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); // 設置線程組 this.group = g; // 將daemon屬性設置為父線程的對應的屬性 this.daemon = parent.isDaemon(); // 將prority屬性設置為父線程的對應的屬性 this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); this.target = target; setPriority(priority); // 復制父線程的InheritableThreadLocals屬性 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; // 設置一個線程id tid = nextThreadID(); }
總結:
在上述代碼中,一個新構建的線程對象時由其parent線程來分配空間的,而child繼承了parent是否為Daemon、優(yōu)先級和加載資源的contextClassLoader以及可繼承的ThreadLocal,同時會分配一個唯一的ID來標志線程。此時一個完整的能夠運行的線程對象就初始化好了,在堆內存中等待運行。
2、什么是線程中斷
中斷可以理解為線程的一個標識位屬性,它表示一個運行中的線程是否被其他線程進行了中斷操作。線程通過檢查自身是否被中斷來進行響應,線程通過方法isInterrupted()來進行判斷是否被中斷,也可以通過調用靜態(tài)方法Thread.interrupted()對當前線程的中斷標志位進行復位。
如下情況不能準確判斷線程是否被中斷過:
線程已經終止運行,即使被中斷過,isInterrupted()方法也會返回false
方法拋出InterruptedException異常,即使被中斷過,調用isInterrupted()方法將會返回false,這是因為拋出InterruptedException之前會清除中斷標志。
示例代碼:
package com.lizba.p2; /** *
* 線程中斷示例代碼 *
* * @Author: Liziba * @Date: 2021/6/14 20:36 */ public class Interrupted { public static void main(String[] args) { // sleepThread不停的嘗試睡眠 Thread sleepThread = new Thread(new SleepRunner(), "sleepThread"); sleepThread.setDaemon(true); // busyThread Thread busyThread = new Thread(new BusyRunner(), "busyThread"); busyThread.setDaemon(true); // 啟動兩個線程 sleepThread.start(); busyThread.start(); // 休眠5秒,讓sleepThread和busyThread運行充分 SleepUtil.sleepSecond(5); // 中斷兩個線程 sleepThread.interrupt(); busyThread.interrupt(); System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted()); System.out.println("BusyThread interrupted is " + busyThread.isInterrupted()); // 睡眠主線程,防止daemon線程退出 SleepUtil.sleepSecond(2); } static class SleepRunner implements Runnable { @Override public void run() { while (true) { SleepUtil.sleepSecond(10); } } } static class BusyRunner implements Runnable { @Override public void run() { while (true) {} } } }查看運行結果:
總結:
拋出InterruptedException的是sleepThread線程,雖然兩者都被中斷過,但是sleepThread線程的中斷標志返回的是false,這是因為TimeUnit.SECONDS.sleep(seconds)會拋出InterruptedException異常,拋出異常之前,sleepThread線程的中斷標志被清除了。但是,busyThread一直在運行沒有拋出異常,中斷位沒有被清除。
3、suspend()、resume()和stop()
舉例:
線程這三個方法,相當于QQ音樂播放音樂時的暫停、恢復和停止操作。(注意這些方法已經過期了,不建議使用。)
示例代碼:
package com.lizba.p2; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit; /** *
* 線程過期方法示例 *
* * @Author: Liziba * @Date: 2021/6/14 20:57 */ public class Deprecated { static DateFormat format = new SimpleDateFormat("HH:mm:ss"); public static void main(String[] args) { Thread printThread = new Thread(new PrintThread(), "PrintThread"); printThread.start(); SleepUtil.sleepSecond(3); // 暫停printThread輸出 printThread.suspend(); System.out.println("main suspend PrintThread at " + format.format(new Date())); SleepUtil.sleepSecond(3); // 恢復printThread輸出 printThread.resume(); System.out.println("main resume PrintThread at " + format.format(new Date())); SleepUtil.sleepSecond(3); // 終止printThread輸出 printThread.stop(); System.out.println("main stop PrintThread at " + format.format(new Date())); SleepUtil.sleepSecond(3); } static class PrintThread implements Runnable { @Override public void run() { while (true) { System.out.println(Thread.currentThread().getName() + "Run at " + format.format(new Date())); SleepUtil.sleepSecond(1); } } } }輸出結果:
總結:
上述代碼執(zhí)行輸出的結果,與API說明和我們的預期完成一致,但是看似正確的代碼卻隱藏這很多問題。
存在問題:
suspend()方法調用后不會釋放已占有的資源(比如鎖),可能會導致死鎖
stop()方法在終結一個線程時不能保證資源的正常釋放,可能會導致程序處于不確定的工作狀態(tài)
4、正確的終止線程
調用線程的interrupt()方法
使用一個Boolean類型的變量來控制是否停止任務并終止線程
示例代碼:
package com.lizba.p2; /** *
* 標志位終止線程示例代碼 *
* * @Author: Liziba * @Date: 2021/6/14 21:17 */ public class ShutDown { public static void main(String[] args) { Runner one = new Runner(); Thread t = new Thread(one, "CountThread"); t.start(); SleepUtil.sleepSecond(1); t.interrupt(); Runner two = new Runner(); t = new Thread(two, "CountThread"); t.start(); SleepUtil.sleepSecond(1); two.cancel(); } private static class Runner implements Runnable { private long i; private volatile boolean on = true; @Override public void run() { while (on && !Thread.currentThread().isInterrupted()) { i++; } System.out.println("Count i = " +i); } /** * 關閉 */ public void cancel() { on = false; } } }輸出結果:
總結:
main線程通過中斷操作和cancel()方法均可使CountThread得以終止。這兩種方法終止線程的好處是能讓線程在終止時有機會去清理資源。做法更加安全和優(yōu)雅。
文章總結至《java并發(fā)編程的藝術》,下文總結-線程間通信。
Java
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。