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

三、共享受限資源

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

1.解決共享資源競爭

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

  synchronized,當代碼要執行被synchronized保護的代碼塊時,先檢查鎖是否可用,再獲得鎖,執行代碼塊,釋放鎖。共享資源一般是以對象形式存在的內存片段,也可以是文件,I/O,打印機等。要控制對共享資源的訪問,需要先把它包裝進一個對象。然后把所有調用這個資源的方法標記為synchronized。如果某個任務在調用標記為synchronized的方法,那么那么在這個線程從該方法返回前,其他所有要調用類中任何標記為synchronized方法的線程都會被阻塞。

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

  對每個類,也有一個鎖。所以 synchronized static 方法可以在類的范圍內防止對static數據的並發訪問。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM