1. 程式人生 > >Java程式設計思想——第21章 併發(一)

Java程式設計思想——第21章 併發(一)

前言

  對於某些問題,如果能夠並行的執行程式中的多個部分,則回變得非常方便甚至必要,這些部分要麼看起來是併發執行,要麼是在多處理環境下同時執行。並行編輯可以使程式執行速度得到極大提高,或者為設計某些型別的程式提供更易用的模型。當並行執行的任務彼此開始產生互相干涉時,實際的併發問題就發生了。

一、併發的多面性

  併發解決的問題答題上可以分為“速度”和“設計可管理新”兩種。

1.更快的執行

  想要更快的執行,需要多處理器,併發是用於多處理器程式設計的基本工具。這是使用強有力的多處理器Web伺服器的常見情況,在為每個請求分配一個執行緒的程式中,它可以將大量的使用者請求分佈到多個CPU上。

  當併發執行在單處理器時,開銷可能要比順序執行開銷大,因為增加了上下文切換的代價。但是阻塞使得問題變得不同:如果程式中的某個任務因為該程式控制範圍之外的某些條件(如:I/O)而導致不能繼續執行,那麼這個任務執行緒阻塞了。如果沒有併發,則整個程式都將停止下來。因此,如果沒有任務會阻塞,在單執行緒處理器機器上使用併發就沒有任何意義。單執行緒併發一般使用在視窗操作。

  Java所使用的這種併發系統會共享諸如記憶體和I/O這樣的資源,因此編寫多執行緒程式最基本的困難在於協調不同執行緒驅動的任務之間對這些資源的使用,以使得這些資源不會同時被多個任務訪問。

2.改進程式碼設計

  簡單舉個例子吧,遊戲裡面多個npc,各自走各自的。

二、基本的執行緒機制

  併發程式設計是我們可以將程式劃分為多個分離的、獨立執行的任務。通過多執行緒機制,這些獨立任務中每一個都將由執行執行緒來驅動。一個執行緒就是在程序中的一個單一的順序控制流,因此,單程序可以擁有多個併發執行的任務,但是程式使得每個人物都想有自己的CPU。其底層機制是切分CPU時間。

1.定義任務

  執行緒可以驅動任務,因此你需要一種描述任務的方式,這可以由Runnable介面來提供。要想定義任務,只需實現Runnable介面並編寫run()方法,使得該任務可以執行你的命令。

public class RunnableDemo implements Runnable {
    int i =100;
    @Override
    public void run() {
        while (i-->0){
            Thread.yield();
        }
    }
}

  任務的run()方法總會以迴圈的形式使任務一直進行下去,在run()中對靜態方法Thread.yield()的呼叫是對執行緒排程器(Java執行緒機制的一部分,可以將CPU從一個執行緒轉移給另一個執行緒)的一種建議,它宣告:“我已經完成生命週期中最重要的部分,此刻是切換給其他任務執行一段時間的大好時機。

  當Runnable匯出一個類時,它必須具有run()方法,但是這個方法並無特殊之處——它不會產生任何內在的執行緒能力。要實現縣城行為,你必須顯式地將一個任務附著到執行緒了。

2.Thread類

  將Runnable物件轉變為工作任務的傳統方式是把它提交給一個Thread構造器:

    public static void main(String[] args) {
        Thread t = new Thread(new RunnableDemo());
        t.start();
     //其他方法 }

  Thread構造器只需要一個Runnable物件。呼叫Thread物件的start()方法為該執行緒執行必須的初始化操作,然後呼叫Runnable的run()方法,以便在這個新執行緒中啟動該任務。start()方法實際上,產生的是對Runnable.run()的呼叫。程式會同時執行兩個方法,main()裡面的其他方法和Runnable.run()是程式中與其他執行緒“同時”執行程式碼。

3.使用Executor  

  執行器(Excutor)將為你管理Thread物件,簡化了併發程式設計。相當於中介。但是由於一下原因不是很推薦

推薦:ThreadPoolExecutor使用 。

4.從任務中產生返回值

  Runnable是執行工作的獨立任務,但是它不返回任何值。如果希望任務中返回值那麼應當實現Callable介面。Callable具有泛型,它的型別引數標識從call()方法中返回的值,並且必須使用ExectorService.submit()方法呼叫:

public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(5, 10, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1));
        List<Future<String>> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(executorService.submit(new TaskWithResult(i)));
        }
        for (Future<String> fs : results) {
            try {
                //得到返回值
                System.out.println(fs.get());
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                executorService.shutdown();
            }
        }
    }
}

class TaskWithResult implements Callable<String> {
    private int id;

    TaskWithResult(int id) {
        this.id = id;
    }

    @Override
    public String call() {
        return "result of TaskWithResult" + id;
    }
}

5.休眠

  影響任務行為的一種簡單方法是呼叫sleep(),這將使任務中止執行對應的時間。

  @Override
    public void run() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

6.優先順序

  執行緒的優先順序將該執行緒的重要性傳遞給排程器,排程器傾向於讓優先權最高的執行緒先執行。但這並不意味著優先順序低的執行緒得不到執行(優先權高的等待不會導致死鎖),優先權低的執行緒僅僅是執行頻率較低。在絕大多數時間裡,所有程式都應該是預設優先順序,試圖操作執行緒優先順序通常是一種錯誤。

  @Override
    public void run() {
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY );
        Thread.currentThread().getPriority();
    }

最好在run方法裡面設定優先順序,而且最好就用那三種常用的級別 :

Thread.MAX_PRIORITY
Thread.NORM_PRIORITY

Thread.MIN_PRIORITY

7.讓步

  當工作做了一段時間可以讓別的執行緒使用cpu了。此時可以使用Thread.yield()給執行緒排程一個暗示(只是一個暗示,不一定被採納)。

8.後臺執行緒

  所謂後臺執行緒,是指在程式執行時,在後臺提供一種通用服務的執行緒,並且這種執行緒並不屬於程式中不可或缺的部分。當所有非後臺執行緒結束時,程式也就終止了,同時會殺死程序中所有的後臺執行緒。

設定後臺執行緒:

  public static void main(String[] args) {
        Thread t = new Thread(new RunnableDemo());
        //這句設定執行緒為後臺執行緒
        t.setDaemon(true);
        t.start();
    }

9.編碼的變體

  在非常簡單的情況下,你可能會希望使用直接哦那個Thread繼承這種可替換的方式:

public class SimpleThrad extends Thread {
    private int countDown = 5;

    /**
     * 依然需要實現run方法
     */
    @Override
    public void run() {
        while (true) {
            System.out.println(this);
            if (--countDown == 0) {
                return;
            }
        }
    }
}

但是不提倡還是提倡使用ThreadPoolExecutor實現執行緒管理。

10.術語

  從上面的各種情況中你可以看到實際你沒有對Thread的控制權。你建立任務,並通過某種方式將一個執行緒附著到任務上,以使得這個執行緒可以驅動任務。在Java中Thread類自身不執行任何操作,它只是驅動賦予給他的任務,將任務和執行緒區分開能讓你更好的理解執行緒。

11.加入一個執行緒

  一個執行緒可以在其他執行緒上呼叫join()方法,其效果是等待一段時間直到第二執行緒結束才繼續執行。如果某個執行緒在另一個執行緒t上呼叫t.join(),此執行緒將被掛起,知道目標執行緒t結束才恢復。

也可也在join()加上超時引數(毫秒),使得目標函式在引數時間外還未結束,join()方法依舊能返回。對join()方法的呼叫可以被中斷,做法是在呼叫執行緒上呼叫interrppt()方法,並加try-catch。這裡不舉例子了因為在使用過程中CycliBarrier要比join更