天天看點

java中多線程詳解(一)

搞IT行業,遇到不懂的技術,在學習的時候,一定要先搞明白這是什麼,用在什麼地方,怎麼用

線程這一塊,對我來說,還是比較陌生,借這一次總結,好好地梳理一下這一塊,另外,也希望能幫助到和我有一樣困惑的朋友們,互相學習!

一、簡單介紹一下多線程的概念、使用目的以及使用場景

1、首先講講線程和程序的差別

  程序:每個程序都有獨立的代碼和資料空間(程序上下文),程序間的切換會有較大的開銷,一個程序包含1--n個線程

線程:同一類線程共享代碼和資料空間,每個線程有獨立的運作棧和程式計數器(PC),線程切換開銷小

程序和線程一樣分為五個階段:建立、就緒、運作、阻塞、終止

多程序是指作業系統能同時運作多個任務(程式)

多線程是指在同一個程式中有多個順序流在執行

2、多線程使用目的

吞吐量:JavaWeb開發時,web容器已經做了請求層面的多線程,但僅僅隻限于請求層面,簡單講就是一個請求一個線程,如struts2是多線程的,每個用戶端請求建立一個執行個體,保證線程安全,或者多個請求一個線程,如果是單線程,同一時間隻能處理一個使用者的請求,這樣顯然是不允許的

伸縮性:通過增加CPU核數來提升性能

3、使用場景

1)常見的浏覽器、web服務(現在的web大部分是中間件完成了線程的控制),web處理請求、各種專用伺服器(如遊戲伺服器)

2)servle多線程

3)FTP下載下傳,多線程操作檔案,如百度雲盤下載下傳

4)java多線程操作資料庫,如資料庫百萬條的資料分、資料遷移

5)分布式計算

   6)tomcat内部采用多線程,上百個用戶端通路同一個web應用,tomcat接入後就是把後續的處理扔給一個新的線程來處理,這個新的線程最後調用我們的servlet程式,比如doGet或者doPost方法

7)背景任務,如定時向大量的使用者發送郵件、定期更新配置檔案、任務排程(如quartz),一些用于定時采集的監控等

8)自動作業處理,如定期備份日志、定期備份資料庫

9)異步處理,如發微網誌、記錄日志

10)頁面異步處理,如大批量資料的核對處理(如十萬個使用者名,核對那些是被占用的)

二、java中多線程的實作方法

java中實作多線程的有兩種方法,一、繼承Thread類,二、實作Runnable接口,三、使用ExecutorService、Callable、Future實作有傳回結果的多線程。其中前兩種方式線程執行完後都沒有傳回值,隻有最後一種是帶傳回值的。

1、繼承Thread類

public class ExtendsThread {
	public static void main(String[] args){
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
    }
    
    public static class MyThread extends Thread{
        //車票數量
        private int tickets=100;
        @Override
        public void run() {
            while(tickets>0){
                System.out.println(this.getName()+"賣出第【"+tickets--+"】張火車票");
            }
        }
    }	 	        
}
           

此程式用來模拟4個售票視窗同時售賣100張票,但是資料不是共享的,是以會四個線程分别售出100張票

2、實作Runnable接口

public class ImplRunnable {
	public static class Ticket implements Runnable{
		private int tickets = 100;
		@Override
		public void run() {
			while(tickets>0){
				System.out.println(Thread.currentThread().getName()+"賣出第【"+tickets--+"】張火車票");
			}			
		}		
	}
	public static void main(String[] args) {
		Runnable ticket = new Ticket();
		new Thread(ticket).start();
		new Thread(ticket).start();
		new Thread(ticket).start();
		new Thread(ticket).start();
	}
}
           

此程式用實作Runnable接口來實作四個售票視窗同時售賣100張票,這裡的資料是共享的,四個視窗共同來出售這100張票

在實際工作中,幾乎所有的多線程都是通過實作Runnable接口來實作的

Runnable适合多個相同程式代碼的線程去處理同一資源的情況。把虛拟CPU(線程)同程式的代碼、資料有效的分離,較好的展現了面向對象的設計思想。避免由于Java的單繼承特性帶來的局限性。也就是如果建立的類要繼承其他類的話,因為JAVA中不支援多繼承,就隻能實作java.lang.Runnable接口。有利于程式的健壯性,代碼能夠被多個線程共享,代碼與資料是獨立的。繼承Thread類不能再繼承他類了。編寫簡單,可以直接操縱線程,無需使用Thread.currentThread()

簡單的來說,就是繼承和實作接口的差別。

1)、當使用繼承的時候,主要是為了不必重新開發,并且在不必了解實作細節的情況下擁有了父類我所需要的特征。它也有一個很大的缺點,

那就是如果我們的類已經從一個類繼承(如小程式必須繼承自 Applet 類),則無法再繼承 Thread 類,

2)、java隻能單繼承,是以如果是采用繼承Thread的方法,那麼在以後進行代碼重構的時候可能會遇到問題,因為你無法繼承别的類了,在其他的方面,

兩者之間并沒什麼太大的差別。

3)、implement Runnable是面向接口,擴充性等方面比extends Thread好。

4)、使用 Runnable 接口來實作多線程使得我們能夠在一個類中包容所有的代碼,有利于封裝,它的缺點在于,我們隻能使用一套代碼,若想建立多個

線程并使各個線程執行不同的代碼,則仍必須額外建立類,如果這樣的話,在大多數情況下也許還不如直接用多個類分别繼承 Thread 來得緊湊。

3、使用ExecutorService、Callable、Future實作有傳回結果的多線程

ExecutorService、Callable、Future這個對象實際上都是屬于Executor架構中的功能類。想要詳細了解Executor架構的可以通路http://www.javaeye.com/topic/366591 ,這裡面對該架構做了很詳細的解釋。傳回結果的線程是在JDK1.5中引入的新特征,确實很實用,有了這種特征我就不需要再為了得到傳回值而大費周折了,而且即便實作了也可能漏洞百出。可傳回值的任務必須實作Callable接口,類似的,無傳回值的任務必須Runnable接口。執行Callable任務後,可以擷取一個Future的對象,在該對象上調用get就可以擷取到Callable任務傳回的Object了,再結合線程池接口ExecutorService就可以實作傳說中有傳回結果的多線程了。下面提供了一個完整的有傳回結果的多線程測試例子,在JDK1.5下驗證過沒問題可以直接使用。代碼如下:

package client.demo.bean;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class CallableTest {
	@SuppressWarnings("rawtypes")
	public static void main(String[] args) throws ExecutionException, InterruptedException {  
		System.out.println("----程式開始運作----");  
		Date date1 = new Date();  
  
		int taskSize = 5;  
		// 建立一個線程池  
		ExecutorService pool = Executors.newFixedThreadPool(taskSize);  
		// 建立多個有傳回值的任務  
		List<Future> list = new ArrayList<Future>();  
		for (int i = 0; i < taskSize; i++) {  
			Callable c = new MyCallable(i + " ");  
			// 執行任務并擷取Future對象  
			@SuppressWarnings("unchecked")
			Future f = pool.submit(c);  
			// System.out.println(">>>" + f.get().toString());  
			list.add(f);  
		}  
		// 關閉線程池  
		pool.shutdown();  
  
		// 擷取所有并發任務的運作結果  
		for (Future f : list) {  
			// 從Future對象上擷取任務的傳回值,并輸出到控制台  
			System.out.println(">>>" + f.get().toString());  
		}  
  
		Date date2 = new Date();  
		System.out.println("----程式結束運作----,程式運作時間【"+ (date2.getTime() - date1.getTime()) + "毫秒】");  
	}  
}  
  
	class MyCallable implements Callable<Object> {  
		private String taskNum;  
  
		MyCallable(String taskNum) {  
			this.taskNum = taskNum;  
		}  
  
	public Object call() throws Exception {  
		System.out.println(">>>" + taskNum + "任務啟動");  
		Date dateTmp1 = new Date();  
		Thread.sleep(1000);  
		Date dateTmp2 = new Date();  
		long time = dateTmp2.getTime() - dateTmp1.getTime();  
		System.out.println(">>>" + taskNum + "任務終止");  
		return taskNum + "任務傳回運作結果,目前任務時間【" + time + "毫秒】";  
	}
}
           

代碼說明:

上述代碼中Executors類,提供了一系列工廠方法用于創先線程池,傳回的線程池都實作了ExecutorService接口。

public static ExecutorService newFixedThreadPool(int nThreads) 

建立固定數目線程的線程池。

public static ExecutorService newCachedThreadPool() 

建立一個可緩存的線程池,調用execute 将重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則建立一個新線程并添加到池中。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。

public static ExecutorService newSingleThreadExecutor() 

建立一個單線程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 

建立一個支援定時及周期性的任務執行的線程池,多數情況下可用來替代Timer類。ExecutoreService提供了submit()方法,傳遞一個Callable,或Runnable,傳回Future。如果Executor背景線程池還沒有完成Callable的計算,這調用傳回Future對象的get()方法,會阻塞直到計算完成。

在詳解二中會分享線程狀态切換和線程狀态

繼續閱讀