Java併發編程:(1)進程和線程的由來、進程的創建、線程的創建

 

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();
    }
}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章