1 進程和線程的由來
1 起初,爲了提高一個時間段內CPU的利用率,允許多個任務程序進行切換,人們發明了進程,用進程來對應一個程序,每個進程對應一定的內存地址空間,並且只能使用它自己的內存空間,各個進程間互不干擾。並且進程保存了程序每個時刻的運行狀態,這樣就爲進程切換提供了可能。當進程暫停時,它會保存當前進程的狀態(比如進程標識、進程的使用的資源等),在下一次重新切換回來時,便根據之前保存的狀態進行恢復,然後繼續執行。
這就是併發:能夠讓操作系統從宏觀上看起來同一個時間段有多個任務在執行;但在微觀上,任一個具體的時刻,只有一個任務在佔用CPU資源(針對單核CPU)。
換句話說,進程讓操作系統的併發成爲了可能。
2 後來,當一個進程有多個任務的時候,人們爲了將一個進程下的多個子任務分開執行。發明了線程,讓一個線程去執行一個子任務,這樣一個進程就包括了多個線程,每個線程負責一個獨立的子任務。
但是要注意,一個進程雖然包括多個線程,但是這些線程是共同享有進程佔有的資源和地址空間的。進程是操作系統進行資源分配的基本單位,而線程是操作系統進行調度的基本單位。
換句話說,進程讓操作系統的併發性成爲可能,而線程讓進程的內部併發成爲可能。
2 創建線程的四種方法
瞭解JVM的都知道,一個應用程序對應一個JVM實例或JVM進程(一般來說名字默認爲java.exe或者javaw.exe)。Java採用單線程編程模型,如果不自己創建線程,一個程序默認創建一個線程,也是主線程;但是執行的時候,或許有其他線程一起併發執行,比如垃圾收集器線程。
創建一個線程,一般有四種創建方法:(源碼:https://github.com/TerenceJing/training)
1)繼承Thread類;
2)實現Runnable接口;
3)使用Callable和Future創建線程
4)使用線程池框架創建線程來執行任務
2.1 方式一:繼承Thread類
此時必須重寫run方法,在run方法中定義需要執行的任務;
class MyThread extends Thread{
private static int num = 0;
public MyThread(){
num++;
}
@Override
public void run() {
System.out.println("主動創建的第"+num+"個線程");
}
}
利用start()方法啓動線程(同時創建了一個新的線程):
public class Test {
public static void main(String[]args) {
MyThread thread = new MyThread();
thread.start();
}
}
注意:
1)啓動創建一個線程是通過start()方法,而不是run()方法;
調用run方法只是相當於在主線程中執行run方法中定義的任務,類似於調用普通方法,不會創建一個新的線程來執行定義的任務。
2)新線程創建的時候,不會阻塞主線程的後續執行。例如創建線程A的時候,線程B可能正在執行,兩者執行結果順序和表面的線程啓動順序無關。
2.2 方式二:實現Runnable接口
實現Runnable接口也必須重寫其run方法。
class MyRunnable implements Runnable{
public MyRunnable() {}
@Override
public void run() {
System.out.println("子線程ID:"+Thread.currentThread().getId());
}
}
public class Test {
public static void main(String[]args) {
System.out.println("主線程ID:"+Thread.currentThread().getId());
MyRunnable runnable = new MyRunnable();//定義一個子任務
Thread thread = new Thread(runnable);//將子任務作爲參數交給Thread去執行。
thread.start();//創建一個新的線程來執行子任務。
}
}
MyRunnable實現了Runnable接口,也就是定義了一個子任務,然後將子任務MyRunnable作爲參數交由Thread去執行,通過Thread.Start()方法來創建一個新線程來執行子任務。
同樣,調用Runnable的run方法不會創建新線程,類似於普通的方法調用。
綜合兩種方式:都可以用來創建線程去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現Runnable接口看起來更加簡潔,但是由於Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現Runnable接口。
2.3 方式三:使用Callable和Future創建線程
類似於Runnable接口一樣,Callable接口(也只有一個方法)也提供了一個任務執行方法call()
@FunctionalInterface
public interface Callable<V> {
T call() throws Exception;
}
由上面聲明的接口可以知道,使用Callable方式創建線程可以返回一個指定類型的結果,但需要FutureTask類的支持,用於接收運算結果,可以使用泛型指定返回值的類型。
//匿名類方式
FutureTask<String> task = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
String res = "通過實現Callable接口的call方法";
System.out.println(res);
return res;
}
});
new Thread(task, "有返回值的線程name").start();
String result = task.get();
System.out.println(result);
注意:get方法是阻塞的,即:線程無返回結果,get方法會一直等待。
2.4 方式四:使用線程池框架創建線程
第四種方式,就是可以通過線程池啓動線程,當有一個Runnable任務或Callable任務時,可以使用線程池提交任務,線程池會啓動空閒線程來執行任務,此處可以使用Excutor框架作爲線程池,也可以自定義線程池
2.4.1 Excutor框架作爲線程池創建線程
引入的Executor框架的最大優點是把任務的提交和執行解耦。要執行任務的人只需把Task描述清楚,然後提交即可。
//第四種方式:使用線程池框架創建線程
//4.1
ExecutorService executorService=Executors.newCachedThreadPool();
//當提交任務速度高於線程池中任務處理速度時,緩存線程池會不斷的創建線程。
// 適用於提交短期的異步小程序,以及負載較輕的服務器
ExecutorService executorService1=Executors.newFixedThreadPool(5);
//固定大小的線程池,爲了滿足資源管理需求而需要限制當前線程數量
ExecutorService executorService2=Executors.newSingleThreadExecutor();
// 單線程池,需要保證順序執行各個任務的場景
List<Future<String>> resultList = new ArrayList<Future<String>>();
for (int i = 0; i < 6; i++){
System.out.println("************* a" + i + " *************");
//execute()執行Runnable接口任務
executorService.execute(()-> {
System.out.println("線程池Runnable任務:"+Thread.currentThread().getName());
});
executorService.execute(()-> {
System.out.println("線程池Runnable任務:"+Thread.currentThread().getName());
});
//執行Callable接口任務
Future<String> ft=executorService.submit(()-> {
System.out.println("線程池Callable任務:"+Thread.currentThread().getName());
return "hello";
});
resultList.add(ft);
}
//遍歷任務的結果
for (Future<String> fs : resultList){
try{
while(!fs.isDone()); //Future返回如果沒有完成,則一直循環等待,直到Future返回完成
System.out.println(fs.get()); //打印各個線程(任務)執行的結果
}catch(InterruptedException e){
e.printStackTrace();
}catch(ExecutionException e){
e.printStackTrace();
}finally{
//啓動一次順序關閉,執行以前提交的任務,但不接受新任務
executorService.shutdown();
}
}
ExecutorService的生命週期包括三種狀態:運行、關閉、終止。創建後便進入運行狀態,當調用了shutdown()方法時,便進入關閉狀態,此時意味着ExecutorService不再接受新的任務,但它還在執行已經提交了的任務,當素有已經提交了的任務執行完後,便到達終止狀態。如果不調用shutdown()方法,ExecutorService會一直處在運行狀態,不斷接收新的任務,執行新的任務,服務器端一般不需要關閉它,保持一直運行即可。
2.4.2 自定義線程池創建線程
自定義線程池,可以用ThreadPoolExecutor類創建(線程池詳細解析參考:Java併發編程:線程池解析),它有多個構造方法來創建線程池,用該類很容易實現自定義的線程池:
@Test
public void threadPoolTest() {
//自定義一個線程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3,5,5000,
TimeUnit.MICROSECONDS,new ArrayBlockingQueue<>(10) );
for(int i=0;i<10;i++){
poolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("自定義線程池執行線程:"+Thread.currentThread().getName());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
poolExecutor.shutdown();
poolExecutor.shutdown();
System.out.println(poolExecutor.getPoolSize());
System.out.println(poolExecutor.getQueue().size());
}
向線程池提交了10個任務,其中三個在線程池中創建了線程執行,7個進入阻塞隊列等待。
四種線程創建方法對比
實現Runnable和實現Callable接口的方式基本相同,不過是後者執行call()方法有返回值,前者執行run()方法無返回值,因此可以把這兩種方式歸爲一種,這種實現接口的方式與繼承Thread類的方法之間的差別如下:
1、線程只是實現Runnable或實現Callable接口,還可以繼承其他類。
2、這種方式下,多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。
3、但是編程稍微複雜,如果需要訪問當前線程,必須調用Thread.currentThread()方法。
4、繼承Thread類的線程類不能再繼承其他父類(Java單繼承決定)。
綜合比較:前三種的線程如果創建關閉頻繁會消耗系統資源影響性能(前三種推薦實現接口的方式),而使用線程池可以不用線程的時候放回線程池,用的時候再從線程池取,項目開發中主要使用線程池
創建進程
兩種方式創建進程:ProcessBuilder的start方法和Runtime.exec()方法
總共涉及到5個主要的類:
首先說一個前提進程類:java.lang.Process
public abstract class Process
{
abstract public OutputStream getOutputStream(); //獲取進程的輸出流
abstract public InputStream getInputStream(); //獲取進程的輸入流
abstract public InputStream getErrorStream(); //獲取進程的錯誤流
abstract public int waitFor() throws InterruptedException; //讓進程等待
abstract public int exitValue(); //獲取進程的退出標誌
abstract public void destroy(); //摧毀進程
}
方式一:ProcessBuilder的start方法
Final類:ProcessBuilder
public final class ProcessBuilder
{
private List<String> command;
private File directory;
private Map<String,String> environment;
private boolean redirectErrorStream;
//第一種構造器,參數作爲List傳入
public ProcessBuilder(List<String> command) {
if (command == null)
throw new NullPointerException();
this.command= command;
}
//第二種構造器,不定長參數,以字符串形式傳入
public ProcessBuilder(String... command) {
this.command= new ArrayList<String>(command.length);
for (String arg : command)
this.command.add(arg);
}
....
}
start方法中具體做了哪些事情:
上述根據命令參數以及設置的工作目錄進行一些參數設定,並返回一個ProcessImpl類型的Process對象。
Final類java.lang.ProcessImpl:
final class ProcessImpl extends Process {
static Process start(String cmdarray[],
java.util.Map<String,String>environment,
Stringdir,
boolean redirectErrorStream)
throws IOException
{
String envblock =ProcessEnvironment.toEnvironmentBlock(environment);
return new ProcessImpl(cmdarray, envblock, dir,redirectErrorStream);// 創建一個ProcessImpl對象
}
....
}
那麼,如何使用ProcessBuilder創建進程:打開cmd,獲取ip地址信息:
public class Test {
public static void main(String[]args) throws IOException {
//關鍵步驟:將命令字符串傳給ProcessBuilder的構造器
ProcessBuilderpb = new ProcessBuilder("cmd","/c","ipconfig/all");
Processprocess = pb.start();
Scannerscanner = new Scanner(process.getInputStream());
while(scanner.hasNextLine()){
System.out.println(scanner.nextLine());
}
scanner.close();
}
}
方式二:Runtime的exec方法
Runtime,顧名思義,即運行時,表示運行時採用單利模式(進程只會運行於一個虛擬機實例當中)產生了當前進程所在的虛擬機實例。
Exec方法實現:事實上通過Runtime類的exec創建進程的話,最終還是通過ProcessBuilder類的start方法來創建的
public Process exec(String[] cmdarray, String[] envp,File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
創建進程:
因爲exec方法不支持不定長參數,必須先把命令參數拼接好再傳進去
public class Test {
public static void main(String[]args) throws IOException {
Stringcmd = "cmd "+"/c "+"ipconfig/all";
Processprocess = Runtime.getRuntime().exec(cmd);
Scannerscanner = new Scanner(process.getInputStream());
while(scanner.hasNextLine()){
System.out.println(scanner.nextLine());
}
scanner.close();
}
}