並發是一種能並行運行多個程式或並行運行一個程式中多個部分的能力。如果程式中一個耗時的任務能以非同步或並行的方式運行,那麼整個程式的吞吐量和可 互動性將大大改善。現代的PC都有多個CPU或一個CPU中有多個核,是否能合理運用多核心的能力將成為一個大規模應用程式的關鍵。
執行緒基本上使用
寫執行緒執行緒執行的程式碼有兩種方式:一種是建立Thread子類別的一個實例並重寫run方法,第二種是建立類別的時候實作Runnable介面。當然,實作 Callable也算是一種方式,Callable和Future結合實作可以實現在執行完任務後取得回傳值,而Runnable和Thread方式是無法取得任務執行後的結果的。
public class ThreadMain { public static void main(String[] args) { MyThread myThread = new MyThread(); new Thread(myThread).start(); new MyThreas2().start(); } } // 第一种方式,实现Runable接口 class MyThread implements Runnable { @Override public void run() { System.out.println("MyThread run..."); } } // 第二种方式,继承Thread类,重写run()方法 class MyThreas2 extends Thread { @Override public void run() { System.out.println("MyThread2 run..."); } }
一旦執行緒啟動後start()方法會立即返回,而不會等待run()方法執行完畢後返回,就好像run方法是在另一個cpu上執行一樣。
注意:建立並執行一個執行緒所犯的常見錯誤是呼叫執行緒的run()方法而非start()方法,如下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run (); //should be start();
起初你並不會感覺到有什麼不妥,因為run()方法的確如你所願的被調用了。但是,事實上,run()方法並非是由剛建立的新執行緒所執行的,而是當前執行緒所執行了。也就是被執行上面兩行程式碼的執行緒所執行的。想要讓建立的新執行緒執行run()方法,必須呼叫新執行緒的start方法。
Callable和Future結合實作實現在執行完任務後取得回傳值:
public static void main(String[] args) { ExecutorService exec = Executors.newSingleThreadExecutor(); Future<String> future = exec.submit(new CallTask()); System.out.println(future.get()); } class CallTask implements Callable { public String call() { return "hello"; } }
給執行緒設定執行緒名稱:
MyTask myTask = new MyTask(); Thread thread = new Thread(myTask, "myTask thread"); thread.start(); System.out.println(thread.getName());
當建立一個執行緒的時候,可以給執行緒取一個名字。它有助於我們區分不同的線程。
volatile
在多線程並發編程中synchronized和Volatile都扮演著重要的角色,Volatile是輕量級的synchronized,它在多處理器開發中都扮演著重要的角色,Volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個執行緒修改一個共享變數時,另一個執行緒能讀到這個修改的值。它在某些情況下比synchronized的開銷更小,但是volatile不能保證變數的原子性。
volatile變數進行寫入操作時(彙編下有lock指令),該lock指令在多核心系統下有2個作用:
將目前CPU快取行寫回系統記憶體。
這個寫回操作會造成其他CPU快取了改位址的資料失效。
多CPU下遵循快取一致性原則,每個CPU透過嗅探在總線上傳播的資料來檢查自己的快取值是否過期了,當發現快取對應的記憶體位址被修改,將對應快取行設定為無效狀態,下次對資料操作會從系統記憶體重新讀取。更多volatile知識請點選深入分析Volatile的實現原理。
synchronized
在多線程並發編程中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,但是隨著Java SE1.6對Synchronized進行了各種優化它並不那麼重了。
Java中每個物件都可以作為鎖,當一個執行緒試圖存取同步程式碼區塊時,它首先必須得到鎖,退出或拋出例外時必須釋放鎖定。
對於同步方法,鎖定是目前實例物件。
對於靜態同步方法,鎖定是目前物件的Class物件。
對於同步方法區塊,鎖定是Synchonized括號裡配置的物件。
synchronized關鍵字是不能繼承的,也就是說基底類別中的synchronized方法在子類別中預設並不是synchronized的。當執行緒試圖存取同步程式碼區塊時,必須先取得鎖,退出或拋出例外時釋放鎖。 Java中每個物件都可以當作鎖,那麼鎖存在哪裡呢?鎖存在Java物件頭中,如果物件是陣列類型,則虛擬機器用3個word(字寬) 儲存物件頭,如果物件是非陣列類型,則用2字寬儲存物件頭。更多synchronized知識請點選Java SE1.6中的Synchronized。
線程池
線程池負責管理工作線程,包含一個等待執行的任務佇列。執行緒池的任務佇列是一個Runnable集合,工作執行緒負責從任務佇列中取出並執行Runnable物件。
ExecutorService executor = Executors.newCachedThreadPool();for (int i = 0; i < 5; i++) { executor.execute(new MyThread2()); } executor.shutdown();
Java透過Executors提供了4種線程池:
newCachedThreadPool:建立一個可緩存線程池,對於新任務如果沒有空閒線程就新建立一個線程,如果空閒線程超過一定時間就會回收。
newFixedThreadPool:创建一个固定数量线程的线程池。
newSingleThreadExecutor:创建一个单线程的线程池,该线程池只用一个线程来执行任务,保证所有任务都按照FIFO顺序执行。
newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
以上几种线程池底层都是调用ThreadPoolExecutor来创建线程池的。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。,可以选择的阻塞队列有以下几种:
workQueue(任务队列):用于保存等待执行的任务的阻塞队列。
ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:一个具有优先级得无限阻塞队列。
当提交新任务到线程池时,其处理流程如下:
先判断基本线程池是否已满?没满则创建一个工作线程来执行任务,满了则进入下个流程。
其次判断工作队列是否已满?没满则提交新任务到工作队列中,满了则进入下个流程。
最后判断整个线程池是否已满?没满则创建一个新的工作线程来执行任务,满了则交给饱和策略来处理这个任务。