簡介雖然相對其他語言來說,Java將線程抽象化爲Thread類來控制,做到了一定程度的簡化。但是由於多線程自身受到JDK版本、硬件、操作系統等影響帶來的不確定性,尤其是當多線程訪問臨界資源時,我們必須在多線程編程時保持一個謹慎的心態。下面對多線程知識做一個簡單的總結,由於個人水平有限有不到之處請指正。
什麼時候應該使用多線程
多線程,聽起來很高端的樣子,彷彿使用了多線程我們就是大牛了。但是真的是這樣麼?其實不是。我們首先應該瞭解多線程可以給我們帶來什麼好處,根據這些好處我們才能確定什麼情景下使用多線程。
多線程的好處其實只有兩點:1,並行處理(性能需求);2,仿真(功能需求)。
並行處理很簡單,由於現在的操作系統普遍支持多核多線程,所以當我們使用多線程的時候就可以更好的利用系統資源,加快應用處理、響應速度,避免醜陋的用戶體驗。
仿真,仿真可能在遊戲編程中會比較多的用到,就是利用多線程每個線程模擬一個或一類資源。例如遊戲中打怪獸,怪獸是一個線程,我們控制的任務是一個線程,這就是仿真。當然,在業務應用系統中也會用到,例如常見的生產者和消費者模型中,一個線程作爲生產者的仿真,另一個線程作爲消費者的仿真。
好了,多線程機制的基本好處已經說明,接下來就靠我們自己來確定自己應用的情景是否適合使用多線程。
JDK線程
除了上文提到的Thread類來作爲線程的抽象,Java還有Runnable接口作爲線程任務的抽象。所以,我們在實際使用時即可以通過繼承Thread類重寫run方法來實現線程,也可以通過實現Runnable接口實現run方法來實現線程。但是,都必須通過Thread的start方法啓動線程。這裏要說明的是,大家可能碰到過是run方法還是start方法來運行/啓動/開始線程(一般是運行多)的面試題,其實就是這個運行干擾了我們,記住,線程只會被啓動(start),運行(run)指的是線程的內部業務邏輯。
一般情況下無論是Thraed的派生類還是Runnable的實現類,它們的run方法體內都會有根據某個條件運行的while循環保證線程不斷執行,直到達到某個條件後循環結束、線程終止。
concurrent類庫
java.util.concurrent類庫雖然包含在JDK中,但是其來源於Doug Lea的util.concurrent庫。這個庫包含許多線程安全、測試良好、高性能的併發構建塊。主要是爲了對Collection數據結構執行併發操作。推薦使用concurrent類庫來實現、管理多線程,這樣可以避免我們在使用多線程的過程中犯一些不必要的錯誤。concurrent類庫主要包含以下的接口和接口實現類。
ExecutorService接口
ExecutorService常用方法:
- executor(Runnable),啓動Runnable任務,無返回值。
- submit(Callable),啓動Callable任務,返回Future接口實例。
- shutdown(),該執行器就不再接受新的線程任務,只會保證當前的多線程任務順利執行完。
/** * * @ClassName: CachedThreadPool * @Description: 用來測試concurrent線程工具類庫的功能 * @author lixiaodai * @date 2014-3-11 上午9:18:54 * */ public class CachedThreadPool { public static void main(String[] args) { //JDK1.5 以後引入了concurrent類庫包類輔助我們的線程操作,也可以理解爲一個多線程的框架 //常用的方式是通過ExecutorService創建一個爲一個的執行器來統一管理多個線程 ExecutorService executorService = Executors.newCachedThreadPool(); for(int i=0;i<5;i++){ executorService.execute(new BasicRunnable()); } //executor的shutdown()方法調用後,該執行器就不會再接受新的線程任務,只會保證當前的多線程任務順利執行完 executorService.shutdown(); } }
/**
*
* @ClassName: FixedThreadPool
* @Description: FixedThreadPool的使用
* @author lixiaodai
* @date 2014-3-12 上午11:15:30
*
*/
public class FixedThreadPool {
public static void main(String[] args) {
//創建FiexedThreadPool作爲線程管理器,FixedThreadPool與CachedThreadPool
//不同之處在於FixedThreadPool在創建的時候就已經確定了管理線程的上限,並不是說只能有指定個線程而是說同時只能有指定個線程存在
//也就是在使用前已經完成了高昂的線程分配
//SingleThreadExecutor是線程數量爲1的FiexedThreadProol
//SingleThreadExecutor實際上是爲了保證線程的按序執行,執行完一個之後再執行另一個,並且不需要處理對共享資源的訪問
ExecutorService service = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
service.execute(new BasicRunnable());
}
service.shutdown();
}
}
ThreadFactory接口
ThreadFactory常用方法:
- newThread(Runnable r),傳入的是executor中啓動的runnable,返回的是Thread對象。在方法啓動創建Thread(r),設置Thread屬性後返回該Thread對象。
線程機制
Java中將線程抽象化爲Thread類,線程執行的任務抽象化爲Runnable接口的實現類。爲了很好的使用多線程,我們先來學習一下Java中線程的基本概念以及對應的操作。
線程
多次說過,Java中的線程就是Thread類及其子類,Thread類抽象的只是線程,雖然可以通過繼承重寫run方法來描述具體的線程任務,但是由於Java不支持多繼承,所以並不推薦使用直接繼承Thread的方式來實現線程。
public class MyExecutorThread extends Thread{
/**
* 線程要執行的任務,對於並行任務,那麼直接寫入任務邏輯
* 對於類似於監聽/定時等任務一般通過while(true)死循環的方式
*/
@Override
public void run() {
while(true){
System.out.println("一個簡單的線程,就是不結束,氣死你!");
}
}
}
線程任務
線程任務,即線程要執行的任務邏輯,分爲無返回值的任務和有返回值的任務。
無返回值任務
無返回值任務實現Runnable方法即可,在Runnable實現類中的run方法實現任務邏輯即可。通過concurrent類庫中的Executor的executor方法啓動Runnable實例即可。
/**
*
* @ClassName: BasicRunnable
* @Description: 第一個簡單的Runnable實現類,Runnable的實現類本身並不能說明啓動線程
* 只是在run()方法中描述了線程的邏輯,必須附着到一個Thread類上纔是真正的線程
* @author lixiaodai
* @date 2014-3-10 下午6:56:37
*
*/
public class BasicRunnable implements Runnable {
private int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public BasicRunnable() {
}
public BasicRunnable(int countDown){
this.countDown = countDown;
}
public String status(){
return "#"+id+"("+(countDown>0?countDown:"Liftoff!")+").";
}
/**
* run方法體內一般都會包含一個根據某條件的不斷循環,知道滿足條件後循環退出
* 循環退出也就是線程退出
*/
public void run() {
while(countDown-- >0){
System.out.print(status());
//Java線程機制中的yield方法,只是聲明該線程以準備好退出資源佔用,但是並不是可靠的立刻退出
//資源,而是選擇性的不可預測的,所以在使用時一定要小心
Thread.yield();
}
}
public static void main(String[] args) {
Thread thread = new Thread(new BasicRunnable());
//啓動線程是使用start()方法,和中文意思一樣啓動=start
thread.start();
//這裏的輸出並沒有等到thread線程執行完畢之後才執行,而是啓動線程後就直接執行了
//這一點正是我們使用線程的理由之一:不阻塞的快速執行某些邏輯,即加快運行速度
System.out.println("Wait For Lift Off");
}
}
有返回值任務
有返回值任務時concurrent類庫中提供的,實現Callable接口,Callable接口是一個泛型接口,實現call方法實現業務邏輯,call方法返回值是泛型類型對象。使用Executor的submit(Callable)方法啓動,submit方法的返回值是Future類型。
/**
*
* @ClassName: BasicCallable
* @Description: 演示了Callable接口的使用,Callable與Runnable都是用來
* 描述業務邏輯,但是不同之處在於Callable啓動的線程可以有返回值
* 而Runnable啓動的線程不會有返回值
* Callable和Runnable的區別類似於Python中的eval和exec的區別
* @author lixiaodai
* @date 2014-3-12 下午2:07:10
*
*/
public class BasicCallable implements Callable<String>{
private int id;
public BasicCallable(int id){
this.id = id;
}
/**
* 注意,這裏是call而不是run
*/
public String call() throws Exception {
return "result of Cannable : "+id;
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<String>> results = new ArrayList<Future<String>>();
for(int i=0;i<10;i++){
/**
* 在執行callable實現類的時候,不是使用execute
* 而是使用submit來啓動線程,同時Callable實現類的返回值是Future接口的實現類
* 通過使用Future的get()方法得到Callable實現類的call方法的返回值
* 可調用Future的isDone()判斷線程是否完成了
* 可調用Future的isCancel()判斷線程在爲完成前是否被撤銷了
*/
results.add(executorService.submit(new BasicCallable(i)));
}
for(Future<String> fs:results){
if(fs.isDone()){
System.out.println("完成了"+fs.isCancelled());
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
System.out.println(e);
return;
} catch (ExecutionException e) {
System.out.println(e);
return;
}finally{
executorService.shutdown();
}
}
}
}
}
線程休眠
線程休眠可以使用Thread.sleep(休眠時間,毫秒單位)靜態方法,來設定當前線程的休眠時間。concurrent類庫中提供了TimeUnit枚舉類來實現多個時間單位的休眠設定。
/**
*
* @ClassName: SleepThread
* @Description: 線程的休眠,即強制性的將線程退出CPU佔用,JDK1.5之前是使用Thread.sleep(毫秒數)來設定線程線程休眠時間
* 而JDK1.5之後,推薦使用concurrent包中的TimeUnit類來設定休眠時間
* @author lixiaodai
* @date 2014-3-12 下午2:35:41
*
*/
public class SleepThread implements Runnable{
private int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
public SleepThread() {
}
public SleepThread(int countDown){
this.countDown = countDown;
}
public String status(){
return "#"+id+"("+(countDown>0?countDown:"Liftoff!")+").";
}
/**
* run方法體內一般都會包含一個根據某條件的不斷循環,知道滿足條件後循環退出
* 循環退出也就是線程退出
*/
public void run() {
try {
while(countDown-- >0){
System.out.print(status());
/**
* 原來是使用sleep(毫秒數)的方法來設定線程休眠時間
* concurrent類庫中提供了更爲強大的枚舉類來設定線程按不同時間單位的休眠時間
*/
//Thread.sleep(1000)
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new SleepThread());
}
executorService.shutdown();
}
}
線程優先級
線程的優先級指線程被執行的概率,並不是線程的執行順序。不要試圖通過設定線程的優先級來控制線程執行順序。使用Thread.currentThread().setPriority(優先級值)。JDK中提供了10個級別的優先級,但是底層的OS提供的優先級級別並不一定和JDK提供的一樣,所以在使用的時候最好使用Thread提供的.MAX_PRIORITY或MIN_PRIORITY或NORM_PRIORITY三個級別來設定優先級。可以通過concurrent中的ThreadFactory來設置線程的優先級。
/**
*
* @ClassName: BasicPriorities
* @Description: 這個類來描述線程優先級的概念,但是要注意的是線程的優先級並不是可靠的
* 優先級只是用來控制線程的執行概率,既然是概率那麼就充滿了不確定性,不要試圖通過
* 控制優先級來控制線程的執行,只是非常錯誤的
* 使用Thread.currentThread().setPriority(優先級值)
* 雖然JDK爲我們提供了10個級別的優先級,但是底層的OS不一定也是10個,所以我們在使用的時候應該使用
* 使用Thread.MAX_PRIORITY或MIN_PRIORITY或NORM_PRIORITY
* @author lixiaodai
* @date 2014-3-12 下午3:04:04
*
*/
public class BasicPriorities implements Runnable {
private int countDown = 5;
private volatile double d;
private int priority;
public BasicPriorities() {
}
public BasicPriorities(int priority){
this.priority = priority;
}
public void run() {
Thread.currentThread().setPriority(priority);
while(true){
for(int i = 1;i<100000;i++){
d+=(Math.PI+Math.E)/(double)i;
if(i%1000 == 0){
Thread.yield();
}
}
System.out.println(this);
if(--countDown == 0){
return;
}
}
}
@Override
public String toString() {
return Thread.currentThread() + " : "+countDown;
}
/**
*
* @Title: main
* @Description:執行結果說明,並不是優先級高的線程就能有限執行,只是執行概率高
* @param @param args 設定文件
* @return void 返回類型
* @throws
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new BasicPriorities(Thread.MAX_PRIORITY));
executorService.execute(new BasicPriorities(Thread.MIN_PRIORITY));
}
executorService.shutdown();
}
}
線程讓步
線程讓步,也就是當前線程試圖主動讓出CPU執行時間。使用Thread.yield()方法來實現。但是和所有的線程一般設定一樣,都是不靠譜的,儘量少使用。
後臺線程/守護線程
後臺線程就是我們通常所說的守護線程,通過使用Thread.setDeamon(true)來設置當前線程爲後臺線程。後臺/守護線程在使用的是由要注意,後臺線程在進程中所有的非後臺線程結束後會自動結束,同時後臺線程派生出的線程自動爲後臺線程。可以通過concurrent中的ThreadFactory來設置線程的是否爲後臺線程。
/**
*
* @ClassName: DaemonThread
* @Description: 後臺/守護線程的例子,後臺/守護線程是作爲其他線程的守護來使用,當非後臺線程終止時,後臺/守護
* 線程自動終止.
* 必須在線程啓動(start)前使用對象的setDeamon(true)方法將線程設置爲後臺線程.
* 由於使用concurrent的executor.executor()方法直接啓動的是runnable實現類,如果我們要對
* Thread相關設置就需要用到ThreadFactory
* 後臺線程創建中啓動的線程自動是後臺線程
* @author lixiaodai
* @date 2014-3-12 下午3:31:18
*
*/
public class DaemonThread implements Runnable{
public void run() {
while(true){
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+ " "+this);
}
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
Thread thread = new Thread(new DaemonThread());
/**
* 設置線程爲後臺線程,必須在start啓動線程前設置
*/
thread.setDaemon(true);
thread.start();
}
System.out.println("All deamons started");
TimeUnit.MILLISECONDS.sleep(1000);
}
}
線程合併
線程合併就是將制定線程合併到當前線程,使得隨機執行的線程變爲順序執行。即A線程在run方法內調用B線程.join(),那麼A線程就會掛起,直到B線程結束後纔會繼續執行線程A中join()後的任務。
/**
*
* @ClassName: Sleeper
* @Description: 這幾個類爲了掩飾join()方法如何使用
* @author lixiaodai
* @date 2014-3-13 下午2:54:52
*
*/
public class Sleeper extends Thread{
private int duration;
/**
* <p>Title: </p>
* <p>Description: </p>
*/
public Sleeper() {
}
/**
*
* <p>Title: </p>
* <p>Description: </p>
* @param name
* @param sleepTime
*/
public Sleeper(String name,int sleepTime){
super(name);
duration = sleepTime;
start();
}
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
//線程休眠一段時間
TimeUnit.MILLISECONDS.sleep(duration);
} catch (Exception e) {
//如果線程被中斷,就捕獲中斷異常,但是異常被捕獲時,將清理線程中斷標識
//所以這個的isInterreupt()返回的總是false(默認值)
System.out.println(getName() + " is interrupted." + "isInterrupted(): "+isInterrupted());
return;
}
System.out.println(getName()+" has awakened");
}
public static void main(String[] args) {
Sleeper sleeper = new Sleeper("Sleepy", 1500);
Sleeper grumpy = new Sleeper("Grumpy", 1500);
Joiner dopey = new Joiner("Depey", sleeper);
Joiner cod = new Joiner("Doc", grumpy);
//中斷grumpy線程
grumpy.interrupt();
}
}
class Joiner extends Thread{
private Sleeper sleeper;
/**
* <p>Title: </p>
* <p>Description: </p>
*/
public Joiner() {
}
public Joiner(String name,Sleeper sleeper) {
super(name);
this.sleeper = sleeper;
start();
}
/* (non-Javadoc)
* @see java.lang.Thread#run()
*/
@Override
public void run() {
try {
/**
* sleeper線程執行執行,執行完sleeper線程後再執行本線程
*/
sleeper.join();
} catch (InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println(getName() + " join completed");
}
}
線程銷燬
線程的銷燬,很多朋友在問如果線程執行完畢了,那麼線程會自動銷燬麼,當然可以,只不過會有一點點延遲,也就是幾毫秒的事情。請看demo:
public class CachedThreadPool {
public static void main(String[] args) throws InterruptedException {
//JDK1.5 以後引入了concurrent類庫包類輔助我們的線程操作,也可以理解爲一個多線程的框架
//常用的方式是通過ExecutorService創建一個爲一個的執行器來統一管理多個線程
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i=0;i<5;i++){
executorService.execute(new BasicRunnable());
System.out.println("當前線程數:"+Thread.activeCount());
}
//executor的shutdown()方法調用後,該執行器就不會再接受新的線程任務,只會保證當前的多線程任務順利執行完
executorService.shutdown();
Thread.sleep(1);
System.out.println("當前線程數:"+Thread.activeCount());
}
}
以上需要其他的類,自己腦補即可。
多線程中的異常
由於多線程的特性,主線程中並不能捕捉到所啓動子線程的異常,子線程異常會直接拋出到控制檯。那麼我們怎麼能夠捕獲子線程的異常呢,Java爲我們提供了UncaughtExceptionHandler,只要實現這個接口的uncaughtException()方法來處理異常,然後將實現類通過thread對象的setUncaughtExceptionHandler(UncaughtExceptionHandler實現類)方法將異常處理和子線程綁定在一起即可。
不使用UncaughtExceptionHandler:
/**
* @ClassName: ThreadExceptionDemo
* @Description: 這個類主要來說明在子線程中拋出的異常是無法再主線程中catch到的
* 即try{childrenThread.start()}catch{得不到childrenThread拋出的異常}
* @author lijie
* @date 2014-3-13 下午12:46:03
*
*/
public class SimpleExceptionDemo implements Runnable{
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
try {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new SimpleExceptionDemo());
} catch (Exception e) {
/**
* 這裏的catch並不能捕捉到子線程的異常
*/
System.out.println("捕捉到了子線程的異常");
}
}
}
異常會直接拋出到控制檯,所以我們只能通過使用UncaughtExceptionHandler:/**
*
* @ClassName: ExceptionThread
* @Description: 這個文件演示了利用UncaughtExceptionHandler接口,在Runnable或concurrent類庫的ThreadFactory
* 中捕獲子線程異常的方式.無論是Runnable還是ThreadFactory中,都是通過threand對象的setUncaughtExceptionHandler、
* 方法來設定子線程異常的捕獲/處理
* @author lixiaodai
* @date 2014-3-13 下午12:57:12
* @version V1.0
*
*/
public class ExceptionThread implements Runnable{
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
public void run() {
Thread t = Thread.currentThread();
/**
* 下邊的輸出只是爲了起到跟蹤的作用
*/
System.out.println("run() by "+t);
System.out.println("eh = "+t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
public static void main(String[] args) {
/**
* 利用ThreadFactory接口的實現類將異常處理類與Runnable綁定到一起
*/
ExecutorService executorService = Executors.newCachedThreadPool(new HandlerThreadFactory());
executorService.execute(new ExceptionThread());
}
}
/**
*
* @ClassName: MyUncaughtExceptionHandler
* @Description: UncaughtExceptionHandler類的實例類,用於異常處理邏輯
* @author lijie
* @date 2014-3-13 下午2:59:28
*
*/
class MyUncaughtExceptionHandler implements UncaughtExceptionHandler{
/* (non-Javadoc)
* @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable)
*/
public void uncaughtException(Thread t, Throwable e) {
System.out.println("catch thread: "+t.getName()+" exception: "+e.getMessage());
}
}
/**
*
* @ClassName: HandlerThreadFactory
* @Description: ThreadFactory實現類,綁定異常處理類和Runnable實例
* @author lijie
* @date 2014-3-13 下午2:59:59
*
*/
class HandlerThreadFactory implements ThreadFactory{
/* (non-Javadoc)
* @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable)
*/
public Thread newThread(Runnable r) {
System.out.println(this + "creating new Thread");
Thread t = new Thread(r);
System.out.println("created "+t.getName());
/**
* 綁定
*/
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
System.out.println("eh = "+t.getUncaughtExceptionHandler());
return t;
}
}
多線程中的事務
對於Web程序中的事務,無論是Spring還是什麼的,由於他們的事務實現一般都是通過ThreadLocal來保證或實現的,所以如果在事務處理中新啓動一個線程處理,那麼新線程是不會和原線程共享一個事務的。所以如果需要在事務中實現多線程那麼就需要人爲的操作線程來保證事務。以後會介紹到