Java編程思想——第21章 併發

前言

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

1、併發的多面性

  併發解決的問題答題上能夠分爲「速度」和「設計可管理新」兩種。編程

1.更快的執行

  想要更快的執行,須要多處理器,併發是用於多處理器編程的基本工具。這是使用強有力的多處理器Web服務器的常見狀況,在爲每一個請求分配一個線程的程序中,它能夠將大量的用戶請求分佈到多個CPU上。服務器

  當併發運行在單處理器時,開銷可能要比順序執行開銷大,由於增長了上下文切換的代價。可是阻塞使得問題變得不一樣:若是程序中的某個任務由於該程序控制範圍以外的某些條件(如:I/O)而致使不能繼續執行,那麼這個任務線程阻塞了。若是沒有併發,則整個程序都將中止下來。所以,若是沒有任務會阻塞,在單線程處理器機器上使用併發就沒有任何意義。單線程併發通常使用在窗口操做。多線程

  Java所使用的這種併發系統會共享諸如內存和I/O這樣的資源,所以編寫多線程程序最基本的困難在於協調不一樣線程驅動的任務之間對這些資源的使用,以使得這些資源不會同時被多個任務訪問。併發

2.改進代碼設計

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

2、基本的線程機制

  併發編程是咱們能夠將程序劃分爲多個分離的、獨立運行的任務。經過多線程機制,這些獨立任務中每個都將由執行線程來驅動。一個線程就是在進程中的一個單一的順序控制流,所以,單進程能夠擁有多個併發執行的任務,可是程序使得每一個人物都想有本身的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從一個線程轉移給另外一個線程)的一種建議,它聲明:「我已經完成生命週期中最重要的部分,此刻是切換給其餘任務執行一段時間的大好時機。this

  當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更好。

3、共享受限資源

  對於併發任務,你須要某種方式來防止兩個任務訪問相同的資源,至少在關鍵階段不能出現這種狀況。

1.解決共享資源競爭

  防止這種衝突的方法就是當資源被一個任務使用時,在其上加鎖。基本上全部的併發模式在解決線程衝突問題的時候,都是採用序列化訪問共享資源的方案。一般這是經過在代碼前面加上以挑鎖語句來實現的,這使得在一段時間內只有一個任務能夠運行這段代碼。由於鎖語句產生了一種互相排斥的效果,因此這種機制撐場成爲互斥量(mutex)。

  synchronized,當代碼要執行被synchronized保護的代碼塊時,先檢查鎖是否可用,再得到鎖,執行代碼塊,釋放鎖。共享資源通常是以對象形式存在的內存片斷,也能夠是文件,I/O,打印機等。要控制對共享資源的訪問,須要先把它包裝進一個對象。而後把全部調用這個資源的方法標記爲synchronized。若是某個任務在調用標記爲synchronized的方法,那麼那麼在這個線程從該方法返回前,其餘全部要調用類中任何標記爲synchronized方法的線程都會被阻塞。

  對全部對象,自動含有單一鎖,當在對象上調用其任意synchronized方法的時候,此對象都被加鎖,這時該對象上的其餘synchronized方法只有等到前一個方法調用完畢並釋放鎖後才能被調用。注意,在使用併發時,將域設置爲private是很是重要的,不然,synchronized關鍵字就不能防止其餘任務直接訪問域。

  對每一個類,也有一個鎖。因此 synchronized static 方法能夠在類的範圍內防止對static數據的併發訪問。

相關文章
相關標籤/搜索