Java編程思想——第21章 並發(一)
- 2019 年 10 月 30 日
- 筆記
前言
對於某些問題,如果能夠並行的執行程式中的多個部分,則回變得非常方便甚至必要,這些部分要麼看起來是並發執行,要麼是在多處理環境下同時執行。並行編輯可以使程式執行速度得到極大提高,或者為設計某些類型的程式提供更易用的模型。當並行執行的任務彼此開始產生互相干涉時,實際的並發問題就發生了。
一、並發的多面性
並發解決的問題答題上可以分為“速度”和“設計可管理新”兩種。
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更好。