Java Thread 的一些認識:
- Java是搶占式線程,一個線程就是進程中單一的順序控制流,單個進程可以擁有多個並發任務,其底層是切分CPU時間,多線程和多任務往往是使用多處理器系統的最合理方式
- 進程可以看作一個程序或者一個應用;線程是進程中執行的一個任務,多個線程可以共享資源
- 一個Java 應用從main 方法開始運行,main 運行在一個線程內,也被稱為 “主線程”,Runnable也可以理解為Task (任務)
- JVM啟動后,會創建一些守護線程來進行自身的常規管理(垃圾回收,終結處理),以及一個運行main函數的主線程
- 隨着硬件水平的提高,多線程能使系統的運行效率得到大幅度的提高,同時異步操作也增加復雜度和各種並發問題
■ 線程 VS 進程
- 在一個已有進程中創建一個新線程比創建一個新進程快的多
- 終止一個線程比終止一個進程快的多
- 同一個進程內線程間切換比進程間切換更快
- 線程提供了不同的執行程序間通信的效率,同一個進程中的線程共享同一進程內存和文件,無序調用內核就可以互相通信,而進程間通信必須通過內核
■ 同步和異步
- 同步方法一旦開始,調用者必須等到方法調用返回之后,才能繼續后續行為
- 無先后順序,一旦開始,方法調用便立即返回,調用者就可以繼續后續行為,一般為另一個線程執行
■ 阻塞和非阻塞
- 當一個線程占用臨界區資源,其他線程也想要使用該資源就必須等待,等待會導致線程的掛起,也就是阻塞(線程變成阻塞狀態)。
此時若占用資源的線程一直不願意釋放資源,那么其他所有阻塞在該臨界區的線程都會被掛起,變成阻塞狀態,不能正常工作,直到占用線程釋放資源 - 非阻塞強調沒有一個線程可以妨礙其他線程執行,所有線程都會嘗試去做下一步工作
■ 臨界資源與臨界區
- 一般指的是公共共享資源,即可以被多個線程共享使用。但同一時間只能由一個線程去訪問和操作臨界區的資源,一旦臨界區資源被一個線程占用,其他線程也想要使用該資源就必須等待,
就好比好多人想上大號,但只有一個坑,一個人占了坑,其他人就得排隊等待嘍 - 臨界區可以認為是一段代碼,線程會在該端代碼中訪問共享資源,因此臨界區的界定標准就是是否訪問共享(臨界)資源(有點類似形成閉包的概念);一次只允許有一個程序(進程/線程)在該臨界區中
■ 上下文切換
- CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片后會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時可以重新加載這個任務的狀態。所有任務從保存到再加載的過程就是一次上下文切換
- 多線程性能問題:由於線程有創建和上下文切換的開銷,在多線程環境下,這種開銷對時間和資源的利用都是一個極大的負擔,很可能導致並發任務執行速度還不如串行快
-
減少上下文切換: 無鎖並發編程、CAS算法、減少並發、使用最少線程、協程
無鎖並發編程:避免使用鎖,比如數據分段執行(MapReduce)、盡可能使用無狀態對象、避免競爭情況等
CAS算法:java.util.concurrent包中大量使用CAS算法,比如Atomic、AQS等
減少並發:JAVA8中新引入的LongAdder、DoubleAdder等新類,將CAS算法替換成value分擔原則
使用最少線程:避免創建不必要的線程,當任務很少但線程很多時,會導致大量線程為等待狀態
協程:在單線程里實現多任務的調度,並在單線程里維持多個任務間的切換 - 補充:需要注意的是,Java的線程是映射到操作系統的原生線程上,因此若要阻塞或喚醒一個線程都需要操作系統的協助,這就意味着要從用戶態轉換到核心態,因此狀態轉換是非常耗費處理器時間的
■ 競合條件
- 競爭條件指多個線程並發訪問和操作同一數據且執行結果與線程訪問的特定順序有關
- 競爭條件發生在當多個線程在讀寫數據時,其最終的的結果依賴於多個線程指令執行順序
- 由於競爭條件的指令順序操作的不確定性甚至是錯誤的,可能會造成結果的混亂,例如臭名昭著的i++的原子性問題
- 值得注意的是即使在單處理器環境下,也可能因為中斷的可以在任何地方停止指令的執行的特性,導致類似並發情況下的數據不一致性,解決方案同競爭條件一致:保證指令的執行順序
■ 並發級別
A. 阻塞(Blocking)
- 線程若是阻塞的,那么在其他線程釋放資源之前,當前線程會被掛起,無法繼續執行
- 在Java中,若使用 synchronized 關鍵字或者重入鎖時,得到的就是阻塞的線程
-
無論是 synchronized 還是重入鎖,都會試圖在執行后續代碼之前,競爭臨界區的鎖:
如果競爭成功,當前線程會獲得鎖並占用資源,從而繼續往后執行
如果競爭失敗,繼續掛起阻塞,等待下次資源被釋放后的再次競爭
B. 無飢餓(Starvation-Free)
- 若線程間區分優先級,那么線程調度通常會優先滿足高優先級的線程(非公平原則),就可能產生飢餓
- 對於非公平的鎖來說,系統允許高優先級的線程插隊,這樣就可能導致低優先級線程產生飢餓 ,如 ReentrantLock 非公平構造 sync = new NonfairSync()
- 對於公平的鎖來說,不管新到的線程優先級多高都需要乖乖排隊,所有線程都有機會執行,飢餓很難產生,ReentrantLock 公平構造 sync = boolean fair ? new FairSync() : new NonfairSync()
- 當一個任務非常耗時導致某線程一直占據關鍵資源不放,其他線程難以獲取,此時其他線程也可以說是飢餓的
■ 線程狀態流程圖:
Java 線程轉換狀態 (重要)
- 新建(new):新創建一個線程對象:在JAVA中的表現就是Thread thread = new Thread();
- 就緒(runnable):線程創建后,其他線程調用該對象的start方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU時間分片使用權:在JAVA中的表現就是 thread.start()
- 運行(running):就緒態線程獲取到CPU時間分片之后,就可以執行任務:在JAVA中的表現就是thread.run(),但需要注意的是,此時線程不一定是立即執行,這跟系統調度有關
- 阻塞(block):阻塞狀態是指線程因為某種原因放棄CPU使用權,讓出CPU時間片,暫時停止運行(注意此時線程在內存中是存在的,並沒有被GC),直到線程進入就緒態,才有機會再次獲取時間片轉成運行態。阻塞分三種情況:
- 常規阻塞:運行態線程在發出I/O請求、執行Thread.sleep方法、t.join方法時,JVM會將該線程設置為阻塞狀態;當I/O處理完畢並發出響應、sleep方法超時、join等待線程終止或超時,線程重新轉入就緒態
- 同步阻塞:運行態線程在獲取對象的同步鎖時,當該同步鎖已被其他線程占用,JVM會將該線程暫時放入同步隊列中,當其他線程放棄同步鎖同時該線程競爭到該同步鎖時,該線程轉為就緒態
- 等待阻塞:運行態線程執行wait方法,JVM會將該線程放入等待隊列中,直到被其他線程喚醒或其他線程中斷或超時,再次獲取同步鎖標識,重新進入就緒態`
- 死亡(dead):線程main方法、run方法執行完畢或出現異常,則該線程生命周期結束。處於死亡或終結狀態的線程不可再次被調度,不可被分配到時間片;已死亡線程不可復生,當然可以使用線程池機制來提高線程的復用性,避免線程被直接殺死;此時其寄存器上下文和棧都將被釋放
Java 線程枚舉狀態
/** * JAVA對於線程狀態的枚舉類,使用jstack查看dump文件可以看到相對應的線程狀態 * 注意:狀態的轉換要以狀態圖為參照標准,枚舉類只是用來統一記錄某種狀態以方便JAVA代碼編寫! * 對應JVM中對線程的監控的4種核心抽象狀態: * 運行(running),休眠(Sleeping),等待(Wait),監視(Monitor) */ public enum State { /** * Thread state for a thread which has not yet started. * 新建:線程創建但還不是就緒態(Thread還沒有執行start方法) */ NEW, /** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may be waiting * for other resources from the operating system such as processor. * 運行狀態:Java將就緒態和運行態統一設置為RUNNABLE * 筆者認為這可能與Thread執行start方法之后會立即執行run方法有關 */ RUNNABLE, /** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock to enter * a synchronized block/method or reenter a synchronized block/method * after calling {@link Object#wait() Object.wait}. * 阻塞:線程正等待獲取監視鎖(同步鎖),調用wait方法就會阻塞當前線程 * 只有獲取到鎖的線程才能進入或重入同步方法或同步代碼 */ BLOCKED, /** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * 調用以上方法會使線程進入等待狀態 * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * 進入等待狀態的線程,需要等待其他線程的喚醒才能繼續運行 * For example, a thread that has called <tt>Object.wait()</tt> * on an object is waiting for another thread to call * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on * that object. A thread that has called <tt>Thread.join()</tt> * is waiting for a specified thread to terminate. * 比如線程被wait方法執行等待,需要被notify或notifyAll喚醒 * 再比如join方法會等待一個指定線程結束之后才會繼續運行 */ WAITING, /** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> * 調用以上方法會使線程進入等待狀態,但會超時返回 */ TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. * 終止:當線程任務完成之后,就終止了 */ TERMINATED; }
■ Thread 類定義
public class Thread implements Runnable { /* Make sure registerNatives is the first thing <clinit> does.
初始化時調用 Java 本地方法,實現了Runnable接口
*/ private static native void registerNatives(); static { registerNatives(); }
■ 構造器
/** * 默認構造器 * 其中name規則為 "Thread-" + nextThreadNum() */ public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } /** * 創建一個指定Runnable的線程 * 其中name規則為 "Thread-" + nextThreadNum() */ public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } /** * 創建一個指定所屬線程組和Runnable的線程 * 其中name規則為 "Thread-" + nextThreadNum() */ public Thread(ThreadGroup group, Runnable target) { init(group, target, "Thread-" + nextThreadNum(), 0); } /** * 創建一個指定name線程 */ public Thread(String name) { init(null, null, name, 0); } /** * 創建一個指定所屬線程組和name的線程 */ public Thread(ThreadGroup group, String name) { init(group, null, name, 0); } /** * 創建一個指定Runnable和name的線程 */ public Thread(Runnable target, String name) { init(null, target, name, 0); } /** * Allocates a new {@code Thread} object so that it has {@code target} * as its run object, has the specified {@code name} as its name, * and belongs to the thread group referred to by {@code group}. * 創建一個新的Thread對象,同時滿足以下條件: * 1.該線程擁有一個指定的Runnable對象用於方法執行 * 2.該線程具有一個指定的名稱 * 3.該線程屬於一個指定的線程組ThreadGroup * <p>If there is a security manager, its * {@link SecurityManager#checkAccess(ThreadGroup) checkAccess} * method is invoked with the ThreadGroup as its argument. * 若這里有個安全管理器,則ThreadGroup將調用checkAccess方法進而觸發SecurityManager的checkAccess方法 * <p>In addition, its {@code checkPermission} method is invoked with * the {@code RuntimePermission("enableContextClassLoaderOverride")} * permission when invoked directly or indirectly by the constructor of a subclass which * overrides the {@code getContextClassLoader} or {@code setContextClassLoader} methods. * 當enableContextClassLoaderOverride被開啟時,checkPermission將被重寫子類直接或間接地調用 * <p>The priority of the newly created thread is set equal to the * priority of the thread creating it, that is, the currently running * thread. The method {@linkplain #setPriority setPriority} may be * used to change the priority to a new value. * 新創建的Thread的優先級等同於創建它的線程的優先級,調用setPriority會變更其優先級 * <p>The newly created thread is initially marked as being a daemon * thread if and only if the thread creating it is currently marked * as a daemon thread. The method {@linkplain #setDaemon setDaemon} * may be used to change whether or not a thread is a daemon. * 當且僅當線程創建時被顯示地標記為守護線程,新創建的線程才會被初始化為一個守護線程 * setDaemon方法可以設置當前線程是否為守護線程 */ public Thread(ThreadGroup group, Runnable target, String name) { init(group, target, name, 0); } /** * Allocates a new {@code Thread} object so that it has {@code target} as its run object, * has the specified {@code name} as its name, and belongs to the thread group referred to * by {@code group}, and has the specified <i>stack size</i>. * 創建一個新的Thread對象,同時滿足以下條件: * 1.該線程擁有一個指定的Runnable對象用於方法執行 * 2.該線程具有一個指定的名稱 * 3.該線程屬於一個指定的線程組ThreadGroup * 4.該線程擁有一個指定的棧容量 * <p>This constructor is identical to {@link #Thread(ThreadGroup,Runnable,String)} * with the exception of the fact that it allows the thread stack size to be specified. * The stack size is the approximate number of bytes of address space that the virtual machine * is to allocate for this thread's stack. <b>The effect of the {@code stackSize} parameter, * if any, is highly platform dependent.</b> * 棧容量指的是JVM分配給該線程的棧的地址(內存)空間大小,這個參數的效果高度依賴於JVM運行平台 * <p>On some platforms, specifying a higher value for the {@code stackSize} parameter may allow * a thread to achieve greater recursion depth before throwing a {@link StackOverflowError}. * Similarly, specifying a lower value may allow a greater number of threads to exist * concurrently without throwing an {@link OutOfMemoryError} (or other internal error). * The details of the relationship between the value of the <tt>stackSize</tt> parameter * and the maximum recursion depth and concurrency level are platform-dependent. * <b>On some platforms, the value of the {@code stackSize} parameter * may have no effect whatsoever.</b> * 在一些平台上,棧容量越高,(會在棧溢出之前)允許線程完成更深的遞歸(換句話說就是棧空間更深) * 同理,若棧容量越小,(在拋出內存溢出之前)允許同時存在更多的線程數 * 對於棧容量、最大遞歸深度和並發水平之間的關系依賴於平台 * <p>The virtual machine is free to treat the {@code stackSize} parameter as a suggestion. * If the specified value is unreasonably low for the platform,the virtual machine may instead * use some platform-specific minimum value; if the specified value is unreasonably high, * the virtual machine may instead use some platform-specific maximum. * Likewise, the virtual machine is free to round the specified value up or down as it sees fit * (or to ignore it completely). * JVM會將指定的棧容量作為一個參考依據,但當小於平台最小值時會直接使用最小值,最大值同理 * 同樣,JVM會動態調整棧空間的大小以適應程序的運行或者甚至直接就忽視該值的設置 * <p><i>Due to the platform-dependent nature of the behavior of this constructor, extreme care * should be exercised in its use.The thread stack size necessary to perform a given computation * will likely vary from one JRE implementation to another. In light of this variation, * careful tuning of the stack size parameter may be required,and the tuning may need to * be repeated for each JRE implementation on which an application is to run.</i> * 簡單總結一下就是:這個值嚴重依賴平台,所以要謹慎使用,多做測試驗證 * @param group * the thread group. If {@code null} and there is a security * manager, the group is determined by {@linkplain * SecurityManager#getThreadGroup SecurityManager.getThreadGroup()}. * If there is not a security manager or {@code * SecurityManager.getThreadGroup()} returns {@code null}, the group * is set to the current thread's thread group. * 當線程組為null同時有個安全管理器,該線程組由SecurityManager.getThreadGroup()決定 * 當沒有安全管理器或getThreadGroup為空,該線程組即為創建該線程的線程所屬的線程組 * @param target * the object whose {@code run} method is invoked when this thread * is started. If {@code null}, this thread's run method is invoked. * 若該值為null,將直接調用該線程的run方法(等同於一個空方法) * @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. * 當棧容量被設置為0時,JVM就會忽略該值的設置 * @throws SecurityException * if the current thread cannot create a thread in the specified thread group * 如果當前線程在一個指定的線程組中不能創建一個新的線程時將拋出 安全異常 * @since 1.4 */ public Thread(ThreadGroup group, Runnable target, String name,long stackSize) { init(group, target, name, stackSize); }
■ JVM棧異常分類
根據棧異常的不同,主要有兩種分類:
1) 棧溢出:若線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常
2) 內存溢出: 若虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常
■ 重要變量
//線程名,用char來保存(String底層實現就是char) private char name[]; //線程優先級 private int priority; //不明覺厲 private Thread threadQ; //不明覺厲 private long eetop; /* Whether or not to single_step this thread. 不明覺厲*/ private boolean single_step; /* Whether or not the thread is a daemon thread. 是否是守護線程,默認非守護線程*/ private boolean daemon = false; /* JVM state. 是否一出生就領便當,默認false*/ private boolean stillborn = false; /* What will be run. Thread的run方法最終會調用target的run方法*/ private Runnable target; /* The group of this thread. 當前線程的所屬線程組*/ private ThreadGroup group; /* The context ClassLoader for this thread 當前線程的ClassLoader*/ private ClassLoader contextClassLoader; /* The inherited AccessControlContext of this thread 當前線程繼承的AccessControlContext*/ private AccessControlContext inheritedAccessControlContext; /* For autonumbering anonymous threads. 給匿名線程自動編號,並按編號起名字*/ private static int threadInitNumber; /* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. * 當前線程附屬的ThreadLocal,而ThreadLocalMap會被ThreadLocal維護(ThreadLocal會專門分析) */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. * 主要作用:為子線程提供從父線程那里繼承的值 * 在創建子線程時,子線程會接收所有可繼承的線程局部變量的初始值,以獲得父線程所具有的值 * 創建一個線程時如果保存了所有 InheritableThreadLocal 對象的值,那么這些值也將自動傳遞給子線程 * 如果一個子線程調用 InheritableThreadLocal 的 get() ,那么它將與它的父線程看到同一個對象 */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; /* * The requested stack size for this thread, or 0 if the creator did not specify a stack size. * It is up to the VM to do whatever it likes with this number; some VMs will ignore it. * 棧容量:當設置為0時,JVM會忽略該值;該值嚴重依賴於JVM平台,有些VM甚至會直接忽視該值 * 該值越大,線程棧空間變大,允許的並發線程數就越少;該值越小,線程棧空間變小,允許的並發線程數就越多 */ private long stackSize; /* JVM-private state that persists after native thread termination.*/ private long nativeParkEventPointer; /* Thread ID. 每個線程都有專屬ID,但名字可能重復*/ private long tid; /* For generating thread ID 用於ID生成,每次+1*/ private static long threadSeqNumber; /* * Java thread status for tools,initialized to indicate thread 'not yet started' * 線程狀態 0僅表示已創建 */ private volatile int threadStatus = 0; /** * The argument supplied to the current call to java.util.concurrent.locks.LockSupport.park. * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker * Accessed using java.util.concurrent.locks.LockSupport.getBlocker * 主要是提供給 java.util.concurrent.locks.LockSupport該類使用 */ volatile Object parkBlocker; /* The object in which this thread is blocked in an interruptible I/O operation, if any. * The blocker's interrupt method should be invoked after setting this thread's interrupt status. * 中斷阻塞器:當線程發生IO中斷時,需要在線程被設置為中斷狀態后調用該對象的interrupt方法 */ private volatile Interruptible blocker; //阻塞器鎖,主要用於處理阻塞情況 private final Object blockerLock = new Object(); /* The minimum priority that a thread can have. 最小優先級*/ public final static int MIN_PRIORITY = 1; /* The default priority that is assigned to a thread. 默認優先級*/ public final static int NORM_PRIORITY = 5; /* For generating thread ID 最大優先級*/ public final static int MAX_PRIORITY = 10; /* 用於存儲堆棧信息 默認是個空的數組*/ private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0]; private static final RuntimePermission SUBCLASS_IMPLEMENTATION_PERMISSION = new RuntimePermission("enableContextClassLoaderOverride"); // null unless explicitly set 線程異常處理器,只對當前線程有效 private volatile UncaughtExceptionHandler uncaughtExceptionHandler; // null unless explicitly set 默認線程異常處理器,對所有線程有效 private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
■ 本地方法
/* * Make sure registerNatives is the first thing <clinit> does. * 確保clinit最先調用該方法:所有該方法是類中的最靠前的一個靜態方法 * clinit:在JVM第一次加載class文件時調用,用於靜態變量初始化語句和靜態塊的執行 * 所有的類變量初始化語句和類型的靜態初始化語句都被Java編譯器收集到該方法中 * * registerNatives方法被native修飾,即是本地方法,將由C/C++去完成,並被編譯成了.dll,供JAVA調用 * 其主要作用是將C/C++中的方法映射到Java中的native方法,實現方法命名的解耦 */ private static native void registerNatives(); static { registerNatives(); } /** 主動讓出CPU資源,當時可能又立即搶到資源 **/ public static native void yield(); /** 休眠一段時間,讓出資源但是並不會釋放對象鎖 **/ public static native void sleep(long millis) throws InterruptedException; /** 檢查 線程是否存活 **/ public final native boolean isAlive(); /** 檢查線程是否中斷 isInterrupted() 內部使用 **/ private native boolean isInterrupted(boolean ClearInterrupted); /** 返回當前執行線程 **/ public static native Thread currentThread(); public static native boolean holdsLock(Object obj); private native void start0(); private native void setPriority0(int newPriority); private native void stop0(Object o); private native void suspend0(); private native void resume0(); private native void interrupt0(); private native void setNativeName(String name);
■ 線程初始化
/** * 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. */ private void init(ThreadGroup g, Runnable target, String name,long stackSize) { if (name == null) { throw new NullPointerException("name cannot be null"); } //返回當前線程,即創建該hread的線程 currentThread是個本地方法 Thread parent = currentThread(); //安全管理器根據Java安全策略文件決定將哪組權限授予類 //如果想讓應用使用安全管理器和安全策略,可在啟動JVM時設定-Djava.security.manager選項 //還可以同時指定安全策略文件 //如果在應用中啟用了Java安全管理器,卻沒有指定安全策略文件,那么Java安全管理器將使用默認的安全策略 //它們是由位於目錄$JAVA_HOME/jre/lib/security中的java.policy定義的 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(); } } /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */ //判斷當前運行線程是否有變更其線程組的權限 g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } //新建線程數量計數+1 或者說未就緒線程數+1==> nUnstartedThreads++; g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon();//若當前運行線程是守護線程,新建線程也是守護線程 this.priority = parent.getPriority();//默認使用當前運行線程的優先級 this.name = name.toCharArray(); //設置contextClassLoader if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = AccessController.getContext(); this.target = target; setPriority(priority);//若有指定的優先級,使用指定的優先級 if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); /* Stash the specified stack size in case the VM cares */ this.stackSize = stackSize; /* Set thread ID 線程安全,序列號每次同步+1*/ tid = nextThreadID(); }
private static synchronized long nextThreadID() { return ++threadSeqNumber; }
■ start 方法
/** * Causes this thread to begin execution; the Java Virtual Machine * calls the <code>run</code> method of this thread. * 線程進入就緒態,隨后JVM將會調用這個線程run方法 * 當獲取到CPU時間片時,會立即執行run方法,此時線程會直接變成運行態 * <p> * The result is that two threads are running concurrently: the * current thread (which returns from the call to the * <code>start</code> method) and the other thread (which executes its * <code>run</code> method). * <p> * It is never legal to start a thread more than once. * In particular, a thread may not be restarted once it has completed execution. * 一個線程只能被start一次,特別是線程不會在執行完畢后重新start * 當線程已經start了,再次執行會拋出IllegalThreadStateException異常 * @exception IllegalThreadStateException if the thread was already started. * @see #run() * @see #stop() */ 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. * 該方法不會被主線程或系統線程組調用,若未來有新增功能,也會被添加到VM中 * A zero status value corresponds to state "NEW". * 0對應"已創建"狀態 -> 用常量或枚舉標識多好 */ 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. */ //通知所屬線程組該線程已經是就緒狀態,因而可以被添加到該線程組中 //同時線程組的未就緒線程數需要-1,對應init中的+1 group.add(this); boolean started = false; try { //調用本地方法,將內存中的線程狀態變更為就緒態 //同時JVM會立即調用run方法,獲取到CPU之后,線程變成運行態並立即執行run方法 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出錯,會被調用棧直接通過 } } } ------------- //start之后會立即調用run方法 Thread t = new Thread( new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } },"roman"); t.start(); //roman
■ run 方法
/** * If this thread was constructed using a separate <code>Runnable</code> run object, * then that <code>Runnable</code> object's <code>run</code> method is called; * otherwise, this method does nothing and returns. * Subclasses of <code>Thread</code> should override this method. * 若Thread初始化時有指定Runnable就執行其的run方法,否則doNothing * 該方法必須被子類實現 */ @Override public void run() { if (target != null) { target.run(); } }
■ isAlive 方法
/** * Tests if this thread is alive. A thread is alive if it has * been started and has not yet died. * 測試線程是否處於活動狀態 * 活動狀態:線程處於正在運行或者准備開始運行狀態 * @return <code>true</code> if this thread is alive; * <code>false</code> otherwise. */ public final native boolean isAlive();
✺ 線程運行 : 模擬電梯運行類
public class LiftOffTask implements Runnable { //Runnable 可看作任務(Task) private int countDown = 10; //電梯階層 public LiftOffTask(){ } // syn countDown private synchronized int getCountDown(){ --countDown; // ++ -- 是非線程安全 return countDown; } // getStatus public String status(){ return Thread.currentThread().toString()+ "("+ (countDown > 0? countDown: "Liftoff!") + "),"; } @Override public void run(){ while (getCountDown() >0){ System.out.println(status()); Thread.yield(); } } public static void main(String[] args){ Thread thread = new Thread(new LiftOffTask()); thread.start(); // 調用 run() System.out.println("================ Waiting for LiftOff... ==========================="); } }
■ sleep 方法
/** * Causes the currently executing thread to sleep (temporarily cease execution) * for the specified number of milliseconds plus the specified number of nanoseconds, * subject to the precision and accuracy of system timers and schedulers. * The thread does not lose ownership of any monitors. * 使線程睡眠一段毫秒時間,但線程並不會丟失已有的任何監視器 */ public static void sleep(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException("nanosecond timeout value out of range"); } //換算用 if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } sleep(millis); } /** 我們一般會直接調用native方法,這或許是我們主動使用的最多次的native方法了 **/ public static native void sleep(long millis) throws InterruptedException;
■ yield 方法
/** * A hint to the scheduler that the current thread is willing to yield * its current use of a processor. The scheduler is free to ignore this hint. * 暗示線程調度器當前線程將釋放自己當前占用的CPU資源 * 線程調度器會自由選擇是否忽視此暗示 * <p> Yield is a heuristic attempt to improve relative progression * between threads that would otherwise over-utilise a CPU. Its use * should be combined with detailed profiling and benchmarking to * ensure that it actually has the desired effect. * 該方法會放棄當前的CPU資源,將它讓給其他的任務去占用CPU執行時間 * 但放棄的時間不確定,可能剛剛放棄又獲得CPU時間片 * <p> It is rarely appropriate to use this method. It may be useful * for debugging or testing purposes, where it may help to reproduce * bugs due to race conditions. It may also be useful when designing * concurrency control constructs such as the ones in the * {@link java.util.concurrent.locks} package. * 該方法的適合使用場景比較少,主要用於Debug,比如Lock包設計 */ public static native void yield();
✺ 線程讓步實例
public class ThreadYieldTest { public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { long begin = System.currentTimeMillis(); int count = 0; for (int i=1; i<1000000; i++) { // Thread.yield(); count += i; } long end = System.currentTimeMillis(); System.out.println("耗時:" + (end - begin) + "毫秒!"); } }, "yieldTask"); t1.start(); } }
■ 中斷機制
1. Java 終止線程主要有三個手段
- 自動終止: 使用退出標識,使線程正常退出,即當run()方法完成之后線程自動終止
- 強行終止: 使用stop()方法強行終止,但該方法已被棄用,使用它們可能產生不可預料的后果:該線程會立即停止,並拋出特殊的ThreadDeath()異常,若此時任務仍未執行完畢,可能產生臟數據
- 手動終止: 使用interrupt()方法中斷線程,並在獲取到線程中斷時結束(退出)任務,當然 interrupt 並非真正中斷線程,可由程序員自行處理
2. 中斷機制
由於Java中無法立即停止一個線程,而停止操作很重要,因此Java提供了一種用於停止線程的機制,即中斷機制:
- 中斷狀態:在Java中每個線程會維護一個Boolean中斷狀態位,用來表明當前線程是否被中斷,默認非中斷為 false
- 中斷方法:中斷僅僅只是一種協作方式(可以直接理解為開關標志),JDK僅提供設置中斷狀態和判斷是否中斷的方法,如 interrupted() 和i isInterrupted()
- 中斷過程:由於JDK只負責檢測和更新中斷狀態,因此中斷過程必須由程序猿自己實現,包括中斷捕獲、中斷處理,因此如何優雅的處理中斷變得尤為重要
3. 中斷方法
- Thread.currentThread().isInterrupted(): 判斷是否中斷,對象方法,不會清除中斷狀態
- Thread.interrupted(): 判斷是否中斷,靜態方法,會清除中斷狀態(設置為false)
- Thread.currentThread().interrupt(): 中斷操作,對象方法,會將線程的中斷狀態設置為true,僅此而已(不會真正中斷線程),捕獲和處理中斷由程序猿自行實現
中斷后:線程中斷后的結果是死亡、等待新的任務或是繼續運行至下一步,取決於程序本身
/** * 中斷一個線程(實質是設置中斷標志位,標記中斷狀態) * - 線程只能被自己中斷,否則拋出SecurityException異常 * - 特殊中斷處理如下: * 1.若中斷線程被如下方法阻塞,會拋出InterruptedException同時清除中斷狀態: * Object.wait()、Thread.join() or Thread.sleep() * * 2.若線程在InterruptibleChannel上發生IO阻塞,該通道要被關閉並將設置中斷狀態同時拋出ClosedByInterruptException異常 * * 3.若線程被NIO多路復用器Selector阻塞,會設置中斷狀態且從select方法中立即返回一個非0值(當wakeup方法正好被調用時) * * - 非上述情況都會將線程狀態設置為中斷 * - 中斷一個非活線程不會有啥影響 */ public void interrupt() { if (this != Thread.currentThread()) checkAccess(); synchronized (blockerLock) { Interruptible b = blocker; if (b != null) { // Just to set the interrupt flag // 調用interrupt方法僅僅是在當前線程中打了一個停止的標記,並不是真的停止線程! interrupt0(); b.interrupt(this); return; } } interrupt0(); }
使用 isInterrupt() 方法可以測試一下:
/** * Tests whether this thread has been interrupted. * The <i>interruptedstatus< /i> of the thread is unaffected by this method. * 測試線程Thread對象是否已經是中斷狀態,但不清除狀態標志 * @return <code>true</code> if this thread has been interrupted; * <code>false</code> otherwise. * @see #interrupted() * @revised 6.0 */ public boolean isInterrupted() { //會調用本地isInterrupted方法,同時不清除狀態標志 return isInterrupted(false); } /** * @param ClearInterrupted 是否清除狀態標注,false不清除,true清除 */ private native boolean isInterrupted(boolean ClearInterrupted); ------------- Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0 ;i < 10000;i++){} System.out.println(Thread.currentThread().getName()); } },"kira"); t2.start(); t2.interrupt(); System.out.println("是否停止 1 ?= " + t2.isInterrupted());//是否停止 1 ?=true
// 正常輸出了10000次 線程名
設置中斷監聽:
Thread thread = new Thread(() -> { //循環監聽中斷狀態 while(!Thread.currentThread().isInterrupted()) { //正常執行任務 } //處理中斷 }).start();
4. 中斷情況
1. 中斷非阻塞線程: volatile共享變量或使用interrupt(),前者需要自己實現,后者是JDK提供的
2. 中斷阻塞線程: 當處於阻塞狀態的線程調用interrupt()時會拋出中斷異常,並且會清除線程中斷標志(設置為false);由於中斷標志被清除,若想繼續中斷,需在捕獲中斷異常后需重新調用interrupt()重置中斷標志位(true)
3. 不可中斷線程: synchronized 和 aquire()不可被中斷,但AQS提供了acquireInterruptibly()方法相應中斷
- 中斷阻塞線程測試用例 - 有可能拋出 InterruptedException
Thread thread = new Thread(() -> { while(!Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + " while run "); try { System.out.println(Thread.currentThread().getName() + " sleep begin"); Thread.sleep(500); System.out.println(Thread.currentThread().getName() + " sleep end"); } catch (InterruptedException e) { //sleep方法會清空中斷標志,若不重新中斷,線程會繼續執行 Thread.currentThread().interrupt(); e.printStackTrace(); } } if (Thread.currentThread().isInterrupted()) { System.out.println(Thread.currentThread().getName() + "is interrupted"); } }); thread.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } thread.interrupt();
■ Daemon
- 分類:在JAVA中分成兩種線程:用戶線程和守護線程
- 特性:當進程中不存在非守護線程時,則全部的守護線程會自動化銷毀
- 應用: JVM在啟動后會生成一系列守護線程,最有名的當屬GC(垃圾回收器)
1 Thread t2 = new Thread(new Runnable() { 2 @Override 3 public void run() { 4 System.out.println("守護線程運行了"); 5 for (int i = 0; i < 500000;i++){ 6 System.out.println("守護線程計數:" + i); 7 } 8 } 9 }, "kira"); 10 t2.setDaemon(true); 11 t2.start(); 12 Thread.sleep(500); 13 ------------- 14 //輸出: 15 ...... 16 守護線程計數:113755 17 守護線程計數:113756 18 守護線程計數:113757 19 守護線程計數:113758 20 //結束打印:會發現守護線程並沒有打印500000次,因為主線程已經結束運行了
■ 線程間的通信(重要)
線程與線程之間不是獨立的個體,彼此之間可以互相通信和協作:
Java提供多種線程間通信方案:輪詢機制、等待/通知機制、join()、ThreadLocal、Synchronized、Volatile等
Volitile: Java內存模型 &Volatile
Synchronized: JAVA 鎖之 Synchronied
ThreadLocal: ThreadLocal 線程本地變量及源碼分析
- sleep + while(true) 輪詢
public class SleepWhileThread { public static void main(String[] args) { final List<Integer> list = new ArrayList<Integer>(); Thread t1 = new Thread(new Runnable() { @Override public void run(){ try { for (int i=0; i<6; i++){ list.add(i); System.out.println("添加了" + (i+1) + "個元素"); } Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }, "listAdd"); Thread t2 = new Thread(new Runnable() { @Override public void run() { try { while (true) { if(list.size() == 3) { System.out.println("已添加3個元素, sleepWhile線程需要退出"); throw new InterruptedException(); } } } catch (InterruptedException e) { e.printStackTrace(); //這里只是打印堆棧信息,不是真的停止執行 } } }, "sleepWhile"); t1.start(); t2.start(); } }
- 難以保證及時性:睡眠時,基本不消耗資源,但睡眠時間過長(輪詢間隔時間較大),就不能及時發現條件變更
- 難以降低開銷:減少睡眠時間(輪訓間隔時間較小),能更迅速發現變化,但會消耗更多資源,造成無端浪費
- Java引入等待/通知 (wait/notify) 機制來減少CPU的資源浪費,同時還可以時間在多個線程間的通信
- wait 和 notify 機制
■ Object.wait()
- wait方法使當前線程進行等待,該方法是Object類的方法,用來將當前線程放入"等待隊列"中,並在wait所在的代碼處停止執行,直到收到通知被喚醒或被中斷或超時
- 調用wait方法之前,線程必須獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait方法
- 在執行wait方法后,當前線程釋放鎖,在從wait方法返回前,線程與其他線程競爭重新獲得鎖
- 如果調用wait方法時沒有持有適當的鎖,則拋出運行期異常類IllegalMonitorStateException
■ Object.notify()
- notify方法使線程被喚醒,該方法是Object類的方法,用來將當前線程從"等待隊列中"移出到"同步隊列中",線程狀態重新變成阻塞狀態,notify方法所在同步塊釋放鎖后,從wait方法返回繼續執行
- 調用notify方法之前,線程必須獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用notify方法
- 該方法用來通知那么可能等待該對象的對象鎖的其他線程,如果有多個線程等待,則由線程規划器從等待隊列中隨機選擇一個WAITING狀態線程,對其發出通知轉入同步隊列並使它等待獲取該對象的對象鎖
- 在執行notify方法之后,當前線程不會馬上釋放對象鎖,等待線程也並不能馬上獲取該對象鎖,需要等到執行notify方法的線程將程序執行完,即退出同步代碼塊之后當前線程才能釋放鎖,而等待線程才可以有機會獲取該對象鎖
- 如果調用notify方法時沒有持有適當的鎖,則拋出運行期異常類IllegalMonitorStateException
■ wait/notify 機制
wait()使線程停止運行,notify()使停止的線程繼續運行
1). 使用wait()、notify()、notifyAll()需要先對調用對象加鎖,即只能在同步方法或同步塊中調用這些方法
2). 調用wait()方法后,線程狀態由RUNNING變成WAITING,並將當前線程放入對象的等待隊列中
3). 調用notify()或notifyAll()方法之后,等待線程不會從wait()返回,需要notify()方法所在同步塊代碼執行完畢而釋放鎖之后,等待線程才可以獲取到該對象鎖並從wait()返回
4). notify()方法將隨機選擇一個等待線程從等待隊列中移到同步隊列中;notifyAll()方法會將等待隊列中的所有等待線線程全部移到同步隊列中,被移動線程狀態由WAITING變成BLOCKED
// wait/notify 實現線程調度
public class NumberPrint implements Runnable { private int number; public byte[] res; public static int count = 5; public NumberPrint(int number, byte[] a ) { this.number = number; this.res = a; } @Override public void run() { synchronized (res) { while (count-- > 0){ try { res.notify(); System.out.println(" " + number); res.wait(); System.out.println("----------線程"+Thread.currentThread().getName() + "獲得鎖,wait()后的代碼繼續運行:"+ number); System.out.println("count:" + count); //count 第一次循環為3,因為兩個線程都執行了 count-- } catch (InterruptedException e){ e.printStackTrace(); } } return; } //syn end } public static void main(String[] args) { final byte[] a = {0}; new Thread(new NumberPrint(1,a),"1").start(); new Thread(new NumberPrint(2,a),"2").start(); } }
- join() : 等待線程對象銷毀,可以使得一個線程在另一個線程結束后再執行,底層使用wait() 實現
/** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * 后續線程需要等待當前線程至多運行millis毫秒(超過millis當前線程會自動死亡,結束等待) * 若millis表示0,表示后續線程需要永遠等待(直到當前線程運行完畢) * <p> This implementation uses a loop of {@code this.wait} calls conditioned on * {@code this.isAlive}. As a thread terminates the {@code this.notifyAll} method is invoked. * It is recommended that applications not use {@code wait}, {@code notify}, or * {@code notifyAll} on {@code Thread} instances. * 該方法的原理是循環調用wait方法阻塞后續線程直到當前線程已經不是存活狀態了 * @param millis * the time to wait in milliseconds * @throws IllegalArgumentException * if the value of {@code millis} is negative * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ //注意 join方法被synchronized修改,即是個同步方法,也是此處獲取到同步鎖,為wait做好前提准備 //同時lock指的就是調用join方法的對象 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } //當millis為0時,說明后續線程需要被無限循環等待,直到當前線程結束運行 if (millis == 0) { while (isAlive()) { wait(0);//wait的超時時間為0 } } else { //當millis>0時,在millis毫秒內后續線程需要循環等待,直到超時當前線程自動死亡 while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay);//wait的超時時間為delay now = System.currentTimeMillis() - base; } } } /** * Thread類還提供一個等待時間為0的join方法 * 用於將后續線程無限循環等待,直到當前線程結束運行 */ public final void join() throws InterruptedException { join(0); }
join 使用
Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0;i<100;i++){ System.out.println(Thread.currentThread().getName()+"線程值為:sally" + i); } } },"sally"); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0;i<2;i++){ System.out.println(Thread.currentThread().getName()+"線程值為:kira" + i); } } },"kira"); t1.start(); t1.join();//讓t2線程和后續線程無限等待直到sally線程執行完畢 t2.start(); ------------- //輸出: ...... sally線程值為:sally97 sally線程值為:sally98 sally線程值為:sally99 kira線程值為:kira0 //可以發現直到sally線程執行完畢,kira線程才開始執行 kira線程值為:kira1
■ join VS sleep
- join(long)方法 在內部使用 wait(long)方法 進行等待,因此 join(long)方法 能夠釋放鎖
- Thread.sleep(long)方法 卻不會釋放鎖
■ join VS synchronized
- join()方法 在內部使用 wait()方法 進行等待
- `synchronized 使用的是對象監視器原理作為同步
-------------------------------------------------------------------------------------------------------------------------------
PS 致謝:
***** 各位觀眾,由於對線程的調度機制還理解比較淺,所以本文會持續迭代更新
***** 特別感謝我的好友kira 對本人的日常指導和源碼分析提供,一萬個Thank you! Kira *******
更新版本:
2018/2/8 : 添加線程狀態、線程中斷等論述
2018/4/14: 修改了線程中斷的一些說明;join() 說明
2018/9/27: 修改了一些版式,添加了 join() 的例子