Java多線程併發編程學習

Java多線程併發編程學習筆記
關鍵字:java.util.concurrent;Executors;Executor;ExecutorService;ScheduledExecutorService;
ThreadFactory;Callable;AtomicLong;CountDownLatch;Lock;

1 線程與任務
1.1 理解
  線程是比進程更小的實體,可以獨立運行,能共享訪問整個進程的所有資源。任務是線
程實際工作的內容,一個線程需要指定他的任務,纔會工作,所以Thread必須指定Runnable。
  通過線程的start()啓動的任務才被認爲是一個線程,如果直接調用runnable.run(),這隻
能是看做一次函數調用,不具備任何線程的特性,比如生命週期管理等。
  一旦一個thread調用run方法後,就會在gc中註冊,在run放回前,不可能被gc回收。
1.2 線程池
  線程池,顧名思義,就是存放線程的池子,裏面有正在運行的線程和閒置的線程,線程
池會根據當前池子裏的狀態,決定是否需要創建線程。Java.util.concurrent包中提供了線程池
相關的類。
  線程池分爲4種,分別是newCachedThreadPool、newFixedThreadPool、
newSingleThreadExecutor和newScheduledThreadPool,他們有一個共同點,就是能夠複用現
有的空閒線程,這也是任務XX池設計的初衷,比如數據庫連接池。
1. newCachedThreadPool
容量自動擴展的線程池,示例代碼:
ExecutorService exec = Executors.newCachedThreadPool();
for(int j = 0; j < i; j++)
{
 Thread t = new MultiThread();
 exec.submit(t);
}

2. newFixedThreadPool
能指定容量的線程池,示例代碼:
ExecutorService exec = Executors.newFixedThreadPool(10);
for(int j = 0; j < i; j++)
{
 Thread t = new MultiThread();
 exec.submit(t);
}
3. newSingleThreadExecutor
容量=1的線程池,其實就是單個線程。實例代碼略
4. newScheduledThreadPool
能指定容量,且池中的線程具備定時執行的能力。實例代碼:
ScheduledExecutorService exec = Executors.newScheduledThreadPool(10);
for(int j = 0; j < i; j++)
{
Thread t = new MultiThread();
exec.scheduleAtFixedRate(t, 1, 3, TimeUnit.SECONDS);
exec.scheduleWithFixedDelay(t, 1, 3, TimeUnit.SECONDS);
}//從現在起的下一秒開始執行t,每隔3秒執行一次。
ScheduleAtFixedRate 每次執行時間爲上一次任務開始起向後推一個時間間隔,即
每次執行時間爲 :initialDelay, initialDelay+period, initialDelay+2*period, ...;
ScheduleWithFixedDelay 每次執行時間爲上一次任務結束起向後推一個時間間隔,
即每次執行時間爲:initialDelay, initialDelay+executeTime+delay, 
initialDelay+2*executeTime+2*delay。由此可見,ScheduleAtFixedRate 是基於固定
時間間隔進行任務調度,ScheduleWithFixedDelay 取決於每次任務執行的時間長短,
是基於不固定時間間隔進行任務調度

1.3 從線程中獲取返回值
  普通的線程都是實現Runnable接口,並且允許通過start()啓動,但這種方式下,
子線程無法返回結果,如果需要獲取子線程執行的結果,那麼需要實現Callable接
口(call方法),並且指明泛型爲返回值類型,通過submit提交,示例代碼:
static class MultiThreadWithReturn extends Thread implements Callable<String>{
  @Override
  public String call() throws Exception {
   // TODO Auto-generated method stub
   return ""+Thread.currentThread().getId();
  }
 }

 public static void main(String[] args) {
  List<Future<String>> fslist = new ArrayList<Future<String>>();
  ExecutorService execc = Executors.newCachedThreadPool();
  for(int j = 0; j < 5; j++)
  {
   Thread t = new MultiThreadWithReturn();
   Future<String> fs = (Future<String>) execc.submit(t);
   fslist.add(fs);
  }
  for(Iterator<Future<String>> it = fslist.iterator(); it.hasNext();)
  {
   Future<String> fs = it.next();
   String result = "";
   try {
    result = fs.get();
    //此處是阻塞的,fs只是類似子線程的句柄,並不是執行結果。fs.isDone()可以
判斷當前是否已經執行結束,避免阻塞
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } catch (ExecutionException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } finally {
    System.out.println(result);
   }
  }//當for循環執行完成後,表示所有的子線程都執行完成了


1.4 線程狀態及阻塞場景
1.4.1 線程的狀態
線程有4個狀態,新建NEW、就緒Runnable、阻塞Blocked、死亡Dead。

1.4.2 何時進入阻塞
a: 被sleep:這種阻塞,可以被中斷
 b:被wait:不需要考慮是否可以被中斷
 c:I/O輸入阻塞,等待輸入:不能被中斷
 d:同步鎖阻塞,等待獲得鎖:不能被中斷
此處討論是否可以被中斷,並不是關心能否被中斷本身,而是關心意外中斷是否會帶來
死鎖,比如sleep時會佔用cpu,如果它會被中斷,需要考慮是否能釋放鎖。
I/O和同步鎖是不能在代碼層面被中斷的,只能被外部中斷,比如I/O操作的文件被刪除
了,socket斷開了之類的場景,此時無法捕獲到中斷,故只能捕獲到關閉異常並做處理
2 併發
2.1 併發的實際意義和使用場景
  在多處理器上,併發可以讓每個處理器處理各自的任務,從而實現多個處理器併發工作。
那麼,在單處理器上,併發有什麼意義呢?如果任務可以順利的執行,那麼單處理器就沒有
使用併發的必要,因爲這個處理器一直在忙於正常的工作,且沒有其他處理器了。但是,如
果任務不能順利執行呢?比如阻塞?當一個任務被阻塞時,處理器就閒置了,此時如果有多
個任務,那是不是可以讓處理器去處理另一個任務呢?這就是單處理器下實現併發的意義,
常見的場景是事件機制。
  總而言之,使用併發有2種情況,第一種,利用多處理器的特性,提高整個系統的計算
能力,第二種,利用任務可能掛起的特性,提高單個處理器的利用率,不讓其空等。


2.2.2 父線程join()所有子線程,只要代碼跑完所有join就表示子都運行結束
示例代碼:
List<Thread> ts = new ArrayList<Thread>();
for(int j = 0; j < i; j++)
{
 Thread t = new MultiThread();
 ts.add(t);
 exec.submit(t);
}
for(Iterator<Thread> it = ts.iterator(); it.hasNext();)
{
 Thread t = it.next();
 try {
  t.join();//此處會阻塞,等待t執行完成後繼續
 } catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
 }
}

2.2.3 通過倒計時鎖CountDownLatch,一旦await結束,表示子都運行結束
示例代碼:
static class MultiThreadCountDownLatch extends Thread implements Runnable{
  private CountDownLatch cd;
  MultiThreadCountDownLatch(CountDownLatch cd)
  {
   this.cd = cd;
  }
  @Override
  public void run() {
   cd.countDown();
  }
 }
 public static void main(String[] args) {
  ExecutorService execd = Executors.newCachedThreadPool();
  CountDownLatch cd = new CountDownLatch(10);
  for(int j = 0; j < 10; j++)
  {
   Thread t = new MultiThreadCountDownLatch(cd);
   execd.submit(t);
  }
  try {
   cd.await();//此處阻塞,當子線程執行cd.countDown()時會使計數--,當到達0
時,此處喚醒
  } catch (InterruptedException e1) {
   // TODO Auto-generated catch block
   e1.printStackTrace();
  } finally {
   System.out.println("10個子都執行結束");
  }

2.2.4 通過柵欄動作CyclicBarrier,一旦匿名runnable執行run,就表示子都運行
結束
示例代碼:
static class MultiThreadCyclicBarrier extends Thread implements Runnable{
  private CyclicBarrier cb;
  MultiThreadCyclicBarrier(CyclicBarrier cb)
  {
   this.cb = cb;
  }
  
  @Override
  public void run() {
   try {
    cb.await();
   } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } catch (BrokenBarrierException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
   } finally {
    System.out.println("Thread "+this.getId()+" over !!");
   }
  }
 }
 public static void main(String[] args) {
  ExecutorService execb = Executors.newCachedThreadPool();
  CyclicBarrier cb = new CyclicBarrier(10, new Runnable(){
   @Override
   public void run() {//cb被調用10次await後,會觸發run的執行
    // TODO Auto-generated method stub
    System.out.println("main over!! ");
   }
   
  });//創建一個值爲10的柵欄
  for(int j = 0; j < 10; j++)
  {
   Thread t = new MultiThreadCyclicBarrier(cb);
   execb.submit(t);
  }

2.2.5 最原始的方法,通過wait和notify配合完成
示例代碼如下,仔細揣摩synchronized加鎖的位置,提示:無法確保wait一次只接收一個
notify,也就無法通過wait執行次數爛判斷,故只能通過notify的次數來判斷;必須確保子線
程發起notify時,父線程已經在wait了,這樣才能保證i從10變爲0,從而證明所有子都跑完了:
static class MultiThread extends Thread implements Runnable{
  @Override
  public void run() {
   synchronized (MultiThreadTest.list) 
   {
    System.out.println("Thread "+this.getId()+": get list lock and 
start notify list");
    i--;
    list.notify();
    //通知發出後,未必能立即被喚醒,此時可能會同時有多個通知發給同一個掛起對象
    System.out.println("Thread "+this.getId()+": notify list over, i 
= "+i);
   }
  }
 }
 
 public static void main(String[] args) {
// while(i > 0)//如果while放到外層,則先判斷i的值,再獲取鎖。有可能導致notify先搶到鎖,
//並在掛起前就發生了所有的notify,此時再執行list.wait就會導致掛死,不會再有notify信號。//
參見think in java p1203
// {
  synchronized (MultiThreadTest.list) 
  {
   ExecutorService exec = Executors.newCachedThreadPool();
   for(int j = 0; j < i; j++)
   {
    Thread t = new MultiThread();
    exec.submit(t);
   }
   while(i > 0)//while在內層,表示應該先搶到鎖再判斷i的值,此時由於鎖被搶到了,
則表示notify不會執行,i--也不會執行,此時判斷i值纔是有意義的
   {
    try {
     System.out.println("Main thread start waiting");
     list.wait();//掛起,釋放鎖,只有一個子會搶到鎖
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
   }
  }
// }

這裏有一個弊端,i是一個全局變量,在鎖範圍內維護,而wait其實只有最後一次是有
效的,但是如果父子線程不再一個類中,在2個不同的地方執行,那i就無法被共享。
也就無法實現:
static class MultiThread extends Thread implements Runnable{

  public void run() {
   synchronized (MultiThreadTest.list) 
   {
    try {
     sleep(10);
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
    System.out.println("Thread "+this.getId()+": get list lock and 
start notify list");
    i--;
    list.notify();
    //通知發出後,未必能立即被喚醒,此時可能會同時有多個通知發給同一個掛起對象
    System.out.println("Thread "+this.getId()+": notify list over, i 
= "+i);
   }
  }
 }

public static void main(String[] args) {
// while(i > 0)//如果while放到外層,則先判斷i的值,在獲取鎖。
//有可能導致notify先搶到鎖,並在掛起前就發生了所有的notify,此時再執行list.wait就會導致掛
死,不會再有notify信號。參見think in java p1203
// {
  synchronized (MultiThreadTest.list) 
  {
   //主線程,負責啓動所有子線程,並等所有子線程返回後,在繼續執行,創建10個子線程,
並逐個啓動
   ExecutorService exec = Executors.newCachedThreadPool();
   for(int j = 0; j < i; j++)
   {
    Thread t = new MultiThread();
    exec.submit(t);
   }
   int k = i;
   int maxK = k;
   while(k > 0)//while在內層,表示應該先搶到鎖再判斷i的值,此時由於鎖被搶到了,
則表示notify不會執行,i--也不會執行,此時判斷i值纔是有意義的
   {
    try {
     list.wait();
     k--;
     System.out.println("主線程喚醒,第"+(maxK-k)+"次");
     TimeUnit.MILLISECONDS.sleep(50);
    } catch (InterruptedException e) {
     // TODO Auto-generated catch block
     e.printStackTrace();
    }
   }
  }
// }

執行結果:
Thread 204: get list lock and start notify list
Thread 204: notify list over, i = 99
Thread 202: get list lock and start notify list
Thread 202: notify list over, i = 98
...省略,都是一樣的
Thread 10: get list lock and start notify list
Thread 10: notify list over, i = 2
Thread 8: get list lock and start notify list
Thread 8: notify list over, i = 1
主線程喚醒,第1次              ---此處,是第一次被喚醒,這裏至少有98次notify消息
Thread 206: get list lock and start notify list
Thread 206: notify list over, i = 0
主線程喚醒,第2次              ---此處,是第二次被喚醒,這裏至少有2次notify消息

由此可見,當wait收到多個notify時,只會響應一次,一旦喚醒,鎖就佔用了,子線程也就無法
再notify了,等再次掛起時,會清楚所有前一次的notify。所以,這裏wait喚醒的此處遠遠小於
100次。
3 線程間協作
3.1 Sleep、wait、notify、notifyAll的關係
3.1.1 Sleep和wait的比較
a Sleep:睡眠,不釋放任何資源(包括CPU、鎖),類似死等,待睡眠時間超時後,立
即喚醒繼續向下執行,由於不釋放CPU,所以喚醒後不需要等待操作系統調度,本身就
有CPU資源
b Wait:掛起,釋放所有資源(包括CPU、鎖),直接進入等待區,待掛起超時後,不立
即向下執行,而進入就緒隊列,等待調度獲取CPU。等待期間可以被notify喚醒。通常
配合同步鎖一起使用。掛起是針對某個對象而言(如a.wait()),執行掛起的代碼會被
阻塞在這一句。
3.1.2 Notify和notifyAll的比較
a Notify:通知獲得該對象的單個線程
b NotifyAll:通知獲得該對象的所有線程,競爭後,只有一個線程獲得notify消息


4 性能比較分析
4.1 Synchronized、Lock、Atomic性能比較
> Synchronized:在使用頻率較低時,相對高效,且代碼易讀
> Lock:開銷小,性能好,且穩定,代碼可讀性差,要配合try-finally
> Atomic:性能好,但多個atomic配合使用時,性能明顯下降,故使用場景單一


4.2 Collections.synchronizedHashMap和ConcurrentHashMap性能比較
> Collections.synchronizedHashMap:性能差
> ConcurrentHashMap:性能優異,做過特殊處理,優化措施包括分段鎖+JMM,具體參見
http://guibin.iteye.com/blog/1172231



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章