@(架構之路之Java多線程編程實戰)
歡迎關注作者博客
簡書傳送門
文章目錄
- 前言
- 第一章
- 1、 jconsole監控工具
- 2、策略模式在Thread和Runnable中的應用分析
- 3、Thread與ThreadGroup
- 3.1、Thread和ThredGroup的關係
- 3.2、Thread API
- 3.2.1、基本屬性
- 3.2.2、字段摘要
- 3.2.3、構造方法
- 3.2.4、方法摘要
- 3.2.5、setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
- 3.2.6、interrupt() 、interrupted() 、isInterrupted()作用
- 3.2.7、yield()和sleep(0)
- 3.2.8、stop()、suspend()、resume()爲什麼不建議使用
- 3.3、ThreadGroup API
- 4、Thread API 綜合實戰
前言
最近打算就多線程編程做個筆記整理與學習記錄,以此打卡,激勵自我驅動,fighting!
第一章
1、 jconsole監控工具
- 想驗證你對 jvm 配的一些調優參數(比如 Xms、Xmx 等)有沒有起作用嗎?
- 想不想實時監控你自定義的線程池的在實際運行時的線程個數、有沒有死鎖?
- 應用出現 java.lang.OutOfMemoryError: Java heap space,你知道需要去調整 Xms、Xmx。想不想實時監控你的 Java 應用的堆內存使用情況,並根據峯值等數據設置最適合你的 Xms、Xmx 等參數?
- 應用出現 java.lang.OutOfMemoryError: PermGen space,你知道需要去調整 XX:PermSize、XX:MaxPermSize。想不想找到你的應用的永久區 PermGen 的使用峯值,並根據其去設置合理的 XX:PermSize、XX:MaxPermSize 等參數?
- 我們都知道,JVM 堆內存劃分爲年輕代和年老代。JVM 默認下的年老代與年輕代的比例(即 XX:NewRatio,這個名字容易讓人產生混淆,即認爲是年輕代比年老代)爲 2(即把 JVM 堆內存平均分成了三份,年老大佔用了兩份,而年輕代佔用一份。參考資料 Sun Java System Application Server Enterprise Edition 8.2 Performance Tuning Guide),這個比例並不適合所有情況,特別是當你的應用裏局部變量遠遠大於全局變量,而且大量局部變量生命週期都很短的時候。如何根據應用實時的運行運行情況合理配置年輕代(Young Generation,即 Eden 區和兩個 Survivor 區之和)和年老代(Old Generation,即 Tenured 區)的比例 XX:NewRatio 值?
Java 自帶性能監控工具:監視和管理控制檯 jconsole,它可以提供 Java 某個進程的內存、線程、類加載、jvm 概要以及 MBean 等的實時信息,也許能夠對以上問題提供參考。
1.1、JVM一些參數
在啓動 jconsole 之前我們先來回顧一下 JVM 的一些主要參數:
- -Xms 初始/最小堆內存大小
- -Xmx 最大堆內存大小
- -Xmn 年輕代大小
- -XX:NewSize 年輕代大小
- -XX:MaxNewSize 年輕代最大值
- -XX:NewRatio 年老代與年輕代比值
- -XX:MaxPermSize 持久代最大值
- -XX:PermSize 持久代初始值
有些資料說,Xms、Xmx 設置的是 JVM 內存大小,是不對的,JVM 除了留給開發人員使用的堆內存之外還有非堆內存。
讀者可能發現,有三種方式可以劃分年輕代大小:-Xmn 方式、-XX:NewSize + -XX:MaxNewSize 方式、-XX:NewRatio 方式。三種都可以,優先級從高到低依次是 -XX:NewSize + -XX:MaxNewSize 方式、-Xmn 方式、-XX:NewRatio 方式,也就是說配置了前面優先級高的後面的優先級低的就被覆蓋掉了。
1.2、啓用 jconsole 以監控 Java 進程
CMD 切換到 %JAVA_HOME%/bin 目錄,直接執行 jconsole
即可打開 Java 監視和管理控制檯:
本地進程列表裏顯示了所有本地執行中的 Java 進程,雙擊你感興趣的那個進程(比如 PID 爲 8504 那個),即可對該進程進行監控了:
1.3、遠程監控 Java 進程
要對 Java 進程進行遠程監控,在啓動它的時候需要啓用 JMX。
以遠程主機上的 tomcat 爲例,先爲 jmx 找一個可用的遠程端口,比如 9999:
No news is good news~在 %TOMCAT_HOME%/bin/catalina.sh 文件的前面加上以下配置:
JAVA_OPTS="-Xms1024m -Xmx2048m -XX:MaxPermSize=128m -Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
如圖
這樣寫在 tomcat 關閉的時候(執行 %tomcat%/bin/shutdown.sh)會報端口已使用異常:
錯誤: 代理拋出異常錯誤: java.rmi.server.ExportException: Port already in use: 9999; nested exception is:
java.net.BindException: 地址已在使用
這是因爲 tomcat 在啓動、停止的時候都會執行 JAVA_OPTS 配置。這樣就只能使用 kill -9 來關閉 tomcat 了…
解決辦法是把監控配置寫在 CATALINA_OPTS 裏:
JAVA_OPTS="-Xms1024m -Xmx2048m -XX:MaxPermSize=128m"
CATALINA_OPTS="-Dcom.sun.management.jmxremote.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
然後重啓 tomcat,在本機打開 Java 監視和管理控制檯,“遠程進程” 輸入遠程主機名和 jmx 端口號:
點擊 “連接” 按鈕,即可對遠程主機上的 tomcat 進行實時監控了:
1.4、jconsole 提供的一些有用信息
1.4.1、JVM 設定信息是否起作用檢查
點擊 “VM 概要” 可以查看到剛纔我們設定的 JAVA_OPTS 的一些參數已經奏效了:
1.4.2、tomcat 線程池、自定義線程池數量情況實時監控
還在爲 tomcat 線程池的神祕面紗而頭疼?還在爲自己定義的線程池 “黑盒” 一般而苦惱?看看下圖:
我們的 tomcat 剛啓動,從上圖可以看出只有一個 http-8080-Acceptor-0 線程,我們去訪問一下我們的項目,然後再回來看看:
http-8080 線程一下子增長到了 8 個。是不是一切一目瞭然,盡在掌握之中?
1.4.3、內存使用實際消耗
點擊 Java 監視和管理控制檯 “內存” 葉項,可以看到 tomcat 堆內存的使用情況:
圖表裏有很多選項:
我們看一下 Eden 區:
Eden 區基本和整個堆內存的走勢差不多。再看 Survivor 區:
Survivor 區在較短時間內的走勢相對平穩。再看 Old Gen 區:
這個走勢更加平穩,而且對比 Survivor 區、Old Gen 區兩張圖,可以很明顯地看出,在大約 19:58 那個時刻有將一批對象從 Survivor 區移到 Old Gen 區。最後看 Perm Gen 區。
這個走勢最平穩了。可以明顯看出,在大約 19:58,在我們訪問一下我們的項目的時候,一些新的 class 等靜態資源加載到了 JVM 中。1.4.4 的加載類數的圖也證實了這一點。
1.4.4、tomcat 加載類的情況
1.5、配合 jmap 的使用
先找到我們 tomcat 進程的 PID 是 13863,然後執行 jmap -heap 13863:
Heap Configuration 裏列的基本就是我們剛纔配的那些,比如 MaxHeapSize 是 2048 MB,MaxPermSize 是 128 MB。這個和 5.1 裏的是一樣的。
2、策略模式在Thread和Runnable中的應用分析
2.1、策略模式
2.2、源代碼
抽象策略(Strategy)角色類
package com.scmd.concurrency.chap2;
@FunctionalInterface
public interface CalculatorStrategy {
double calculate(double salary, double bouns);
}
具體策略(Strategy)角色類
package com.scmd.concurrency.chap2;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-16 18:18
*/
public class SimpleCalculatorStrategy implements CalculatorStrategy {
private static final Double SALARY_RATE = 0.1;
private static final Double BOUNS_RATE = 0.2;
@Override
public double calculate(double salary, double bouns) {
return SALARY_RATE * salary + BOUNS_RATE * bouns;
}
}
環境(Context)角色類
package com.scmd.concurrency.chap2;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-16 18:09
*/
public class TaxCalculator {
private final double salary;
private final double bonus;
private final CalculatorStrategy strategy;
public TaxCalculator(double salary, double bonus, CalculatorStrategy strategy) {
this.salary = salary;
this.bonus = bonus;
this.strategy = strategy;
}
public double calTax() {
return strategy.calculate(salary, bonus);
}
protected double calculate() {
return this.calTax();
}
public double getSalary() {
return salary;
}
public double getBonus() {
return bonus;
}
}
客戶端
package com.scmd.concurrency.chap2;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-16 18:12
*/
public class TaxCalculatorMain {
public static void main(String[] args) {
// 策略模式
// 非lambd
// TaxCalculator t = new TaxCalculator(1000d, 2000d, new SimpleCalculatorStrategy());
// lambd表達式 + 函數式接口
TaxCalculator t = new TaxCalculator(1000d, 2000d, (s, b) -> s * 0.3 + b * 0.5);
double tax = t.calculate();
System.out.println(tax);
}
}
3、Thread與ThreadGroup
3.1、Thread和ThredGroup的關係
因爲Thread的構造函數中有關於ThradGroup的,所以瞭解它們之間的關係是有必要的。ThradGroup之間的關係是樹的關係,而Thread與ThradGroup的關係就像元素與集合的關係。關係圖簡單如下:
其中有一點要明確一下:main方法執行後,將自動創建system線程組合main線程組,main方法所在線程存放在main線程組中。
3.2、Thread API
3.2.1、基本屬性
首先應該瞭解線程的基本屬性:
- name:線程名稱,可以重複,若沒有指定會自動生成。
- id:線程ID,一個正long值,創建線程時指定,終生不變,線程終結時ID可以複用。
- priority:線程優先級,取值爲1到10,線程優先級越高,執行的可能越大,若運行環境不支持優先級分10級,如只支持5級,那麼設置5和設置6有可能是一樣的。
- state:線程狀態,Thread.State枚舉類型,有NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED 5種。
- ThreadGroup:所屬線程組,一個線程必然有所屬線程組。
- UncaughtExceptionHandler:未捕獲異常時的處理器,默認沒有,線程出現錯誤後會立即終止當前線程運行,並打印錯誤。
3.2.2、字段摘要
Thread類有三個字段,設置線程優先級時可使用:
- MIN_PRIORITY:1,最低優先級
- NORM_PRIORITY:5,普通優先級
- MAX_PRIORITY:10,最高優先級
3.2.3、構造方法
現只介紹參數最多的一個:
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
- group:指定當前線程的線程組,未指定時線程組爲創建該線程所屬的線程組。
- target:指定運行其中的Runnable,一般都需要指定,不指定的線程沒有意義,或者可以通過創建Thread的子類並重新run方法。
- name:線程的名稱,不指定自動生成。
- stackSize:預期堆棧大小,不指定默認爲0,0代表忽略這個屬性。與平臺相關,不建議使用該屬性。
3.2.4、方法摘要
靜態方法
- Thread Thread.currentThread() :獲得當前線程的引用。獲得當前線程後對其進行操作。
- Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() :返回線程由於未捕獲到異常而突然終止時調用的默認處理程序。
- int Thread.activeCount():當前線程所在線程組中活動線程的數目。
- void dumpStack() :將當前線程的堆棧跟蹤打印至標準錯誤流。
- int enumerate(Thread[] tarray) :將當前線程的線程組及其子組中的每一個活動線程複製到指定的數組中。
- Map<Thread,StackTraceElement[]> getAllStackTraces() :返回所有活動線程的堆棧跟蹤的一個映射。
- boolean holdsLock(Object obj) :當且僅當當前線程在指定的對象上保持監視器鎖時,才返回 true。
- boolean interrupted() :測試當前線程是否已經中斷。
- void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) :設置當線程由於未捕獲到異常而突然終止,並且沒有爲該線程定義其他處理程序時所調用的默認處理程序。
- void sleep(long millis) :休眠指定時間
- void sleep(long millis, int nanos) :休眠指定時間
- void yield() :暫停當前正在執行的線程對象,並執行其他線程。意義不太大
普通方法
- void checkAccess() :判定當前運行的線程是否有權修改該線程。
- ClassLoader getContextClassLoader() :返回該線程的上下文 ClassLoader。
- long getId() :返回該線程的標識符。
- String getName() :返回該線程的名稱。
- int getPriority() :返回線程的優先級。
- StackTraceElement[] getStackTrace() :返回一個表示該線程堆棧轉儲的堆棧跟蹤元素數組。
- Thread.State getState() :返回該線程的狀態。
- ThreadGroup getThreadGroup() :返回該線程所屬的線程組。
- Thread.UncaughtExceptionHandler getUncaughtExceptionHandler() :返回該線程由於未捕獲到異常而突然終止時調用的處理程序。
- void interrupt() :中斷線程。
- boolean isAlive() :測試線程是否處於活動狀態。
- boolean isDaemon() :測試該線程是否爲守護線程。
- boolean isInterrupted():測試線程是否已經中斷。
- void join() :等待該線程終止。
- void join(long millis) :等待該線程終止的時間最長爲 millis 毫秒。
- void join(long millis, int nanos) :等待該線程終止的時間最長爲 millis 毫秒 + nanos 納秒。
- void run() :線程啓動後執行的方法。
- void setContextClassLoader(ClassLoader cl) :設置該線程的上下文 ClassLoader。
- void setDaemon(boolean on) :將該線程標記爲守護線程或用戶線程。
- void start():使該線程開始執行;Java 虛擬機調用該線程的 run 方法。
- String toString():返回該線程的字符串表示形式,包括線程名稱、優先級和線程組。
作廢方法
- int countStackFrames() :沒有意義不做解釋。
- void destroy() :破壞線程,不釋放鎖,已經不能再使用,使用會拋出NoSuchMethodError。
- void suspend() :掛起線程,不要使用。
- void resume() :恢復線程,不要使用。
- void stop() :停止線程釋放鎖,不要使用。
- void stop(Throwable obj) :同上。
3.2.5、setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
首先要了解什麼是Thread.UncaughtExceptionHandler,默認來說當線程出現未捕獲的異常時,會中斷並拋出異常,拋出後的動作只有簡單的堆棧輸出。如:
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1=new Thread(new Runnable(){
public void run(){
int a=1/0;
}
});
t1.start();
}
}
那麼代碼運行到int a=1/0;就會報錯:
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
at yiwangzhibujian.ThreadTest$1.run(ThreadTest.java:11)
at java.lang.Thread.run(Thread.java:662)
這時候如果設置了Thread.UncaughtExceptionHandler,那麼處理器會將異常進行捕獲,捕獲後就可以對其進行處理:
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1=new Thread(new Runnable(){
public void run(){
int a=1/0;
}
});
t1.setUncaughtExceptionHandler(new UncaughtExceptionHandler(){
@Override
public void uncaughtException(Thread t,Throwable e){
System.out.println("線程:"+t.getName()+"出現異常,異常信息:"+e);
}
});
t1.start();
}
}
那麼當線程拋出異常後就可以對其抓取並進行處理,最終結果如下:
線程:Thread-0出現異常,異常信息:java.lang.ArithmeticException: / by zero
如果自己寫線程,那麼完全可以在run方法內,將所有代碼進行try catch,在catch裏做相同的操作。UncaughtExceptionHandler的意義在於不對(或者不能對)原有線程進行修改的情況下,爲其增加一個錯誤處理器。
3.2.6、interrupt() 、interrupted() 、isInterrupted()作用
因爲stop()方法已經不建議使用了,下面的3.5.4進行詳解,所以如何中斷一個線程就成了一個問題,一種簡單的辦法是設置一個全局變量needStop,如下:
@Override
public void run(){
while(!needStop){
//執行某些任務
}
}
或者需要操作耗時較長的方法內,每一步執行之前進行判斷:
@Override
public void run(){
//耗時較長步驟1
if(needStop) return;
//耗時較長步驟2
if(needStop) return;
//耗時較長步驟3
}
這樣在其他的地方將此線程停止掉,因爲停止是在自己的預料下,所以不會有死鎖或者數據異常問題(當然你的程序編寫的時候要注意)。
其實Thread類早就有類似的功能,那就是Thread具有中斷屬性。可以通過調用interrupt()方法對線程中斷屬性設置爲true,這將導致如下兩種情況:
- 當線程正常運行時,中斷屬性設置爲true,調用其isInterrupted()方法會返回true。
- 當線程阻塞時(wait,join,sleep方法),會立即拋出InterruptedException異常,並將中斷屬性設置爲false。此時再調用isInterrupted()會返回false。
這樣就由程序來決定當檢測到中斷屬性爲true時,怎麼對線程中斷進行處理。因此,上面的代碼可以改成:
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
//執行某些任務
}
}
---------------------------------------------------------
@Override
public void run(){
//耗時較長步驟1
if(Thread.currentThread().isInterrupted()) return;
//耗時較長步驟2
if(Thread.currentThread().isInterrupted()) return;
//耗時較長步驟3
}
interrupted()的方法名容易給人一種誤解,看似和interrupt()方法一樣,但是其實際含義是,返回當前中斷狀態,並將其設置爲false。
3.2.7、yield()和sleep(0)
yield()方法的API容易給人一種誤解,它的實際含義是停止執行當前線程(立即),讓CPU重新選擇需要執行的線程,因爲具有隨機性,所以也有可能重新執行該線程,通過下面例子瞭解:
public class ThreadTest{
public static void main(String[] args) throws Exception{
Thread t1=new Thread(new Runnable(){
@Override
public void run(){
while(true){
System.out.println(1);
Thread.yield();
}
}
});
Thread t2=new Thread(new Runnable(){
public void run(){
while(true){
System.out.println(2);
Thread.yield();
}
}
});
t1.start();
t2.start();
}
}
程序執行結果並不是121212而是有,有連續的1和連續的2。
經過測試yield()和sleep(0)的效果是一樣的,sleep(0)底層要麼是和yield()一樣,要麼被過濾掉了(純靠猜測),不過sleep(0)沒有任何意義。要是真打算讓當前線程暫停還是應該使用sleep(long millis,int nanos)這個方法,設置幾納秒錶示下誠意,或者找到想要讓步的線程,調用它的join方法更實際一些。
3.2.8、stop()、suspend()、resume()爲什麼不建議使用
stop方法會立即中斷線程,雖然會釋放持有的鎖,但是線程的運行到哪是未知的,假如在具有上下文語義的位置中斷了,那麼將會導致信息出現錯誤,比如:
@Override
public void run(){
try{
//處理資源並插入數據庫
}catch(Exception e){
//出現異常回滾
}
}
如果在調用stop時,代碼運行到捕獲異常需要回滾的地方,那麼將會因爲沒有回滾,保存了錯誤的信息。
而suspend會將當前線程掛起,但是並不會釋放所持有的資源,如果恢復線程在調用resume也需要那個資源,那麼就會形成死鎖。當然可以通過你精湛的編程來避免死鎖,但是這個方法具有固有的死鎖傾向。所以不建議使用。其他暫停方法爲什麼可用:
- wait方法會釋放鎖,所以不會有死鎖問題
- sleep方法雖然不釋放鎖,但是它不需要喚醒,在使用的時候已經指定想要的睡眠時間了。
jdk的文章詳細介紹了方法禁用的原因:文章地址,有空可以看一看,如果你足夠大膽,也是可以使用的。
3.3、ThreadGroup API
3.3.1、基本屬性
name:當前線程的名稱。
parent:當前線程組的父線程組。
MaxPriority:當前線程組的最高優先級,其中的線程優先級不能高於此。
3.3.2、構造方法
只介紹一個構造方法:
ThreadGroup(ThreadGroup parent, String name) :
- parent:父線程組,若爲指定則是創建該線程組的線程所需的線程組。
- name:線程組的名稱,可重複。
3.3.3、常用方法摘要
API詳解(中文,英文)。
- int activeCount():返回此線程組中活動線程的估計數。
- void interrupt():中斷此線程組中的所有線程。
- void uncaughtException(Thread t, Throwable e) :設置當前線程組的異常處理器(只對沒有異常處理器的線程有效)。
3.3.4、ThreadGroup作用
這個線程組可以用來管理一組線程,通過activeCount() 來查看活動線程的數量。其他沒有什麼大的用處。
4、Thread API 綜合實戰
4.1、優雅結束線程
- 全局變量
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-20 20:02
*/
public class ThreadCloseGraceful {
static class Worker extends Thread {
private volatile boolean open = true;
@Override
public void run() {
while (open) {
// code ......
try {
Thread.sleep(1000);
System.out.println(".........");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void shutdown (boolean open) {
this.open = open;
}
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.start();
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
worker.shutdown(false);
}
}
- interrupt()
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description:
* @author: zhouzhixiang
* @create: 2019-03-20 20:02
*/
public class ThreadCloseGraceful2 {
static class Worker extends Thread {
@Override
public void run() {
while (!Thread.interrupted()) {
// code ......
try {
Thread.sleep(1000);
System.out.println(".........");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
Worker worker = new Worker();
worker.start();
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
worker.interrupt();
}
}
4.2、暴力結束線程
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description: 暴力停止
* @author: zhouzhixiang
* @create: 2019-03-20 20:40
*/
public class ThreadService {
private Thread excuteThread;
private volatile boolean finished = false;
public void excute(Runnable task) {
excuteThread = new Thread() {
@Override
public void run() {
Thread daemon = new Thread(task);
daemon.setDaemon(true);
daemon.start();
try {
daemon.join();
finished = true;
} catch (InterruptedException e) {
// e.printStackTrace();
}
}
};
excuteThread.start();
}
public void shutdown(long millis) {
long currentTime = System.currentTimeMillis();
while (!finished) {
if ((System.currentTimeMillis() - currentTime) > millis) {
System.out.println("任務超時,需要結束!");
excuteThread.interrupt();
break;
}
try {
excuteThread.sleep(1);
} catch (InterruptedException e) {
System.out.println("執行線程被打斷!");
break;
}
}
finished = false;
}
}
package com.scmd.concurrency.chap6;
/**
* @program: scmd-knb-common
* @description: 暴力
* @author: zhouzhixiang
* @create: 2019-03-20 20:38
*/
public class ThreadCloseForce {
public static void main(String[] args) {
ThreadService service = new ThreadService();
long startTime = System.currentTimeMillis();
service.excute(() -> {
// load a very heavy resouce
// while (true) {
//
// }
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
service.shutdown(10000);
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}