七、Java多線程與併發

JAVA平臺爲程序員提供了併發編程所需的功能強大的API,呵呵,就像一塊奶油蛋糕般刺激你的味蕾同時又挑戰你將體重控制在某個水平的信念,該篇及後續文章展示Java 多線程與併發編程的誘人之處,幫助理解Java併發編程的模式,以及如何更精確地使用線程模型。

Java 線程基礎

一、鎖 

       鎖提供兩種主要特性:互斥(mutual exclusion)可見性(visibility)。互斥即一次只允許一個線程持有某個特定的鎖,這樣一次就只有一個線程能夠使用共享數據。可見性必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的 —— 如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。

1.悲觀鎖:指在本系統的其他事務和外部系統的事務   對數據處理過程中,將數據置於鎖定狀態。其實現通常基於數據庫提供的鎖機制,也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據。

2.樂觀鎖:指相對於悲觀鎖的更加寬鬆的加鎖機制。其實現通常是基於數據版本記錄機制。即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫增加一個"version"字段來實現。讀取數據時將此版本號一同讀出,之後更新時講此版本號+1。提交數據庫時將此版本數據與數據庫表中版本數據進行比較,如果提交的數據版本號大於數據庫表中的版本,則更新,否則認爲是過期數據。

悲觀鎖與樂觀鎖的優缺點:悲觀鎖大多情況下是依靠數據庫的鎖機制實現的,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣大的開銷往往無法承受。而樂觀鎖在一定程度上解決了這個問題。

 

二、原子變量

原子變量保證對一個變量的操作是原子的。即在多線程併發處理的時候,使用原子變量可將對一個變量的複合操作(複合操作是指後一個操作依賴前一個操作的結果)合併爲一個原子操作。起到簡化同步處理的作用。

原理:使用同步synchronized的方法實現了對一個Long、Integer、對象的增、減、更新操作。

AtomicLong/AtomicInteger/AtomicReference

注意:a.對array atomic變量來說,一次只能有一個索引變量可以變動,並不是對整個array做原子化的變動。

            b.在對一個atomic變量執行兩個或兩個以上的操作時,或對兩個或兩個以上的atomic變量執行操作時,需要用synchronized來實現將多個操作當做一個原子操作的目的。

方法:

getAndSet() : 設置新值,返回舊值.

compareAndSet(expectedValue,newValue):如果當前值等於expectedValue,則更新指定值爲newValue,如果更新成功返回true。否則返回false,即在當前線程得到這個變量的值之後已經有其他線程修改了這個值,和期望看到的值expectedValue不一樣,那麼更新失敗。

下面是關於AtomicInteger的例子:

package com.taobao.app;

import java.util.concurrent.atomic.AtomicInteger;

public class CounterTest {
 AtomicInteger counter = new AtomicInteger(100);

 public int count() {
  int expectedValue;
  int newValue;
  boolean flag;
  do {
   expectedValue = counter.get();
   System.out.println("when start,"+Thread.currentThread().getName()+"'s expectedValue="+expectedValue);
   try {
    Thread.sleep(1000l);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
// 單線程下, compareAndSet返回永遠爲true
// 多線程下, 在與expectedValue進行比較時, counter可能被其他線程更改了值, 這時需要重新再取一遍再比較,如果還是沒有拿到最新的值則一直循環下去, 直到拿到最新的那個值
   newValue = expectedValue + 1;
   flag = counter.compareAndSet(expectedValue, newValue);
   newValue = counter.get();
   System.out.println("when end  ,"+Thread.currentThread().getName()+"'s newValue="+newValue+" and flag="+flag);
  } while (!flag);

  return expectedValue;
 }

 public static void main(String[] args) {
  final CounterTest c = new CounterTest();
  new Thread() {
   @Override
   public void run() {
    c.count();
   }
  }.start();

  new Thread() {
   @Override
   public void run() {
    c.count();
   }
  }.start();

  new Thread() {
   @Override
   public void run() {
    c.count();
   }
  }.start();
 }
}

輸出結果如下:

when start,Thread-0's expectedValue=100
when start,Thread-1's expectedValue=100
when start,Thread-2's expectedValue=100
when end  ,Thread-0's newValue=101 and flag=true
when end  ,Thread-1's newValue=101 and flag=false
when start,Thread-1's expectedValue=101
when end  ,Thread-2's newValue=101 and flag=false
when start,Thread-2's expectedValue=101
when end  ,Thread-1's newValue=102 and flag=true
when end  ,Thread-2's newValue=102 and flag=false
when start,Thread-2's expectedValue=102
when end  ,Thread-2's newValue=103 and flag=true

 

再來個AtomicReference使用的例子:

public class AtomicTest {     
    private int x, y;     
    
    private enum State {     
        NEW, INITIALIZING, INITIALIZED     
    };     
    
    private final AtomicReference<State> init = new AtomicReference<State>(State.NEW);     
         
    public AtomicTest() {     
    }     
         
    public AtomicTest(int x, int y) {     
        initialize(x, y);     
    }     
    
    private void initialize(int x, int y) {     
        if (!init.compareAndSet(State.NEW, State.INITIALIZING)) {     
            throw new IllegalStateException("initialize is error");     
        }     
        this.x = x;     
        this.y = y;     
        init.set(State.INITIALIZED);     
    }     
    
    public int getX() {     
        checkInit();     
        return x;     
    }     
    
    public int getY() {     
        checkInit();     
        return y;     
    }     
         
    private void checkInit() {     
        if (init.get() == State.INITIALIZED) {     
            throw new IllegalStateException("uninitialized");     
        }     
    }     
         
}

 

三、兩種任務:Runnable和Callable

Runnable接口 ;  Callable接口和Future類、FutureTask類

    1.實現Runable接口或Callable接口的類都是可被其他線程執行的任務.

    2.Callable和Runnable的區別如下:

        1)Callable定義的方法是call,而Runnable定義的方法是run.

        2)Callable的call方法可以有返回值,而Runnable的run方法不能有返回值

        3)Callable的call方法可拋出異常,而Runnable的run方法不能拋出異常

    3.Future表示異步計算的結果,它提供的方法可以用於檢查計算是否完成,以等待計算的完成,並獲取計算的結果.

       cancel方法取消任務的執行,有一個布爾參數,參數爲true表示立即中斷任務的執行,參數爲false表示允許正在運行的任務運行完成.

       get方法等待計算完成,獲取計算結果.

示例代碼如下:

package com.taobao.thread;

import java.util.concurrent.Callable;

public class PrimeCallable implements Callable<String> {
 public String call(){
  try {
   Thread.sleep(3000l);
  } catch (InterruptedException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return "我在執行一個費事的操作,太墨跡了,執行了3秒鐘";
 }
}

import java.util.concurrent.FutureTask;

public class FutureDemo {

 public static void main(String[] args) {
  Callable<String> primeCallable = new PrimeCallable();
  FutureTask<String> primeTask = new FutureTask<String>(primeCallable);
  Thread t = new Thread(primeTask);
  t.start();

  try {
   // 假設現在做其他事情
   Thread.sleep(3001);
 
   // 回來看看primeTask執行好了嗎
   if(primeTask.isDone()) {
    String str = primeTask.get();
    System.out.println(str);
   }
  } catch (InterruptedException e) {
   e.printStackTrace();
  } catch (ExecutionException e) {
   e.printStackTrace();
  }
 }
}

注意上面程序中sleep的時間只有在大於Callable中的執行時間時,纔有可能進入到if(primeTask.isDone()) 中。

上面程序會在控制檯打印出callable返回的結果,即

我在執行一個費事的操作,太墨跡了,執行了3秒鐘

 

注:FutureTask用於要異步獲取執行結果或取消執行任務的場景,通過傳入Runnable或Callable的任務給FutureTask,直接調用其run方法或放入線程池執行,之後可在外部通過FutureTask的get異步獲取執行結果。FutureTask可以確保即使調用了多次run方法,它都只會執行一次Runnable或Callable任務,或者通過cancel取消FutureTask的執行等。

 

再換個調用方式:

package com.taobao.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreadPoolDemo {
 
 public static void main(String[] args) {
  
  PrimeCallable task0 = new PrimeCallable();
  PrimeCallable task1 = new PrimeCallable();
  PrimeCallable task2 = new PrimeCallable();

     //創建一個執行任務的服務
     ExecutorService es = Executors.newFixedThreadPool(3);
 
     try{
 
            //提交併執行任務,任務啓動時返回了一個Future對象,如果想得到任務執行的結果或者是異常可對這個Future對象進行操作
            Future future0 = es.submit(task0);
            //獲得第一個任務的結果,如果調用get方法,當前線程會等待任務執行完畢後才往下執行
            System.out.println(System.currentTimeMillis()+":task0: " + future0.get());
 
           
            //等待5秒後,再停止第二個任務,因爲第二個任務進行的是無限循環
            Future future1 = es.submit(task1);
            Thread.sleep(5000);
            System.out.println(System.currentTimeMillis()+":task1 cancel: " + future1.cancel(true));
 
           
 
            //獲取第三個任務的輸出,因爲執行第三個任務會引起異常,所以下面的語句將引起異常的輸出
            Future future2 = es.submit(task2);
            System.out.println(System.currentTimeMillis()+":task2: " + future2.get());
 
     }catch(Exception e){
 
            System.out.println(e.toString());
 
     }

     es.shutdown();//立即停止任務執行服務
 }

}

 

輸出如下:

1323607709980:task0: 我在執行一個費事的操作,太墨跡了,執行了3秒鐘
1323607717980:task1 cancel: false
1323607717980:task2: 我在執行一個費事的操作,太墨跡了,執行了3秒鐘

 

注:Task Submitter把任務提交給Executor執行,他們之間需要一種通訊手段,這種手段的具體實現,叫做Future。Future通常包括get(阻塞至任務完成),cancel,get(timeout)(等待一段時間)等等。Future也用於異步變同步的場景。

下面是關於FutureTask的例子:

應用場景:有一個帶key的連接池,當key已經存在時,即直接返回key對應的對象;當key不存在是,則直接創建連接。使用一個Map對象來存儲key和連接池對象的對應關係。

典型的實現代碼如下:

Map<String,Connection> connectionPool= new HashMap<String,Connection>();

ReentrantLock reentrantLock = new ReentrantLock ();

public  Connection getConnection(String key){

      try{

            reentrantLock .lock();

            if(connectionPool.containsKey(key)){

                return connectionPool.get(key);

            }else{

                connectionPool.put(key,connection);

                return connection;

           }

     }finally{

          reentrantLock .unlock();

    }

}

改用ConcurrentHashMap的情況下,幾乎可以避免加鎖的操作,但在併發的情況下有可能出現Connection被創建多次的現象。這時最需要的解決方案就是當key不存在時,創建Connection的動作能放在connectionPool之後執行,這正是FutureTask發揮作用的時機,基於ConcurrentHashMap和FutureTask的改造代碼如下:

Map<String,Connection> connectionPool = new ConcurrentHashMap<String,Connection>();

public Connection getConnection(String key){

       FutureTask<Connection> connectionTask = connectionPool.get(key);

       if(connectionTask != null){

             return connectionTask.get();

       }else{

             Callable<Connection> callable = new Callable<Connection>();

             FutureTask<Connection> newTask = new FutureTask<Connection>(callable);

             connectionTask = connectionPool.putIfAbsent(key,newTask);

             if(connectiontask == null){

                   connectionTask = new   Task();

                   connectionTask.run();

            }

            return connectionTask.get();

      }

}

經過這樣的改造,可以避免由於併發帶來的多次創建連接及鎖的出現。

 

四、Synchronized和ReentrantLock

入鎖ReentrantLock是一種遞歸無阻塞的同步機制。ReentrantLock具有與synchronized相同的一些基本行爲和語義,但功能更強大。ReentrantLock將由最近成功獲得鎖定並且還沒有釋放該鎖定的線程所擁有。當鎖定沒有被另一個線程所擁有時,調用 lock 的線程將成功獲取該鎖定並返回。如果當前線程已經擁有該鎖定,此方法將立即返回。

可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查此情況是否發生。

 

示例如下:

package com.taobao.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDemo {
 public static void main(String[] args) throws InterruptedException {

  final ExecutorService exec = Executors.newFixedThreadPool(4);

  final ReentrantLock lock = new ReentrantLock();

  final Condition con = lock.newCondition();

  final int time = 5;

  final Runnable add = new Runnable() {

   public void run() {
 
    System.out.println("Pre " + lock);
  
    lock.lock();
  
    try {
  
     con.await(time, TimeUnit.SECONDS);
  
    } catch (InterruptedException e) {
  
     e.printStackTrace();
  
    } finally {
  
     System.out.println("Post " + lock.toString());
  
     lock.unlock();
  
    }
 
   }

  };

  for(int index = 0; index < 4; index++)

   exec.submit(add);
 
   exec.shutdown();

  }

}

輸出如下:

Pre java.util.concurrent.locks.ReentrantLock@157f0dc[Unlocked]
Pre java.util.concurrent.locks.ReentrantLock@157f0dc[Unlocked]
Pre java.util.concurrent.locks.ReentrantLock@157f0dc[Unlocked]
Pre java.util.concurrent.locks.ReentrantLock@157f0dc[Unlocked]
Post java.util.concurrent.locks.ReentrantLock@157f0dc[Locked by thread pool-1-thread-2]
Post java.util.concurrent.locks.ReentrantLock@157f0dc[Locked by thread pool-1-thread-3]
Post java.util.concurrent.locks.ReentrantLock@157f0dc[Locked by thread pool-1-thread-1]
Post java.util.concurrent.locks.ReentrantLock@157f0dc[Locked by thread pool-1-thread-4]

如將代碼改成:

package com.taobao.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantDemo {
 public static void main(String[] args) throws InterruptedException {

  final ExecutorService exec = Executors.newFixedThreadPool(4);

  final ReentrantLock lock = new ReentrantLock();

  final Condition con = lock.newCondition();

  final int time = 5;

  final Runnable add = new Runnable() {

   public void run() {
 
    System.out.println("Pre " + lock);
  
    lock.lock();
  
    try {
  
//     con.await(time, TimeUnit.SECONDS);
     Thread.sleep(5000);
  
    } catch (InterruptedException e) {
  
     e.printStackTrace();
  
    } finally {
  
     System.out.println("Post " + lock.toString());
  
     lock.unlock();
  
    }
 
   }

  };

  for(int index = 0; index < 4; index++)

   exec.submit(add);
 
   exec.shutdown();

  }

}

則輸出變爲:

Pre java.util.concurrent.locks.ReentrantLock@12ac982[Unlocked]
Pre java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-1]
Pre java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-1]
Pre java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-1]
Post java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-1]
Post java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-3]
Post java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-2]
Post java.util.concurrent.locks.ReentrantLock@12ac982[Locked by thread pool-1-thread-4]

以上的對比說明線程在等待時(con.await),已經不在擁有該鎖了,所以其他線程就可以獲得重入鎖了。

 

 

五、線程池ThreadPoolExecutor、Executor

ThreadPoolExecutor是java.util.concurrent包中提供的一個線程池服務,基於它可以很容易的將一個實現了Runnable接口的任務放入線程池中執行。其方法如下:

執行構造器:ThreadPoolExecutor(int,int,long,TimeUnit,BlockQueue,ThreadFactory,RejectedExecutionHandler)所做的動作僅爲保存了這些入參的值。

execute(Runnable)負責將Runnable任務放入線程池中執行。

 

Executor提供了一些方便創建ThreadPoolExecutor的方法。主要有以下幾個方法:

newFixedThreadPool(int)創建固定大小的線程池。線程keepAliveTime爲0,啓動的corePoolSize數量的線程啓動後就一直運行,並不會由於keepAliveTime時間到達後仍沒有任務需要執行就退出。緩衝任務的隊列爲LinkedBlockingQueue,大小爲整形的最大值。在同時執行的task數量超過傳入的線程池大小值後,將會放入LinkedBlockingQueue,在LinkedBlockingQueue中的task需要等待線程空閒後才執行,當放入LinkedBlockingQueue的task數量超過整形最大值時,拋出RejectedExecutionException。

newSingleThreadExecutor()創建大小爲1的固定線程池。同時執行的task只有1,其他task都在LinkedBlockingQueue中。

newCachedThreadPool()創建corePoolSize爲0,最大線程數爲整形的最大數,線程keepAliveTime爲1分鐘即啓動後的線程存活時間爲1分鐘,緩存任務的隊列爲SynchronousQueue的線程池。

newScheduledThreadPool(int)創建corePoolSize爲傳入參數,最大線程數爲整形的最大值,線程keepAliveTime爲0,緩存任務的隊列爲DelayedWorkQueue的線程池。在遇到需要定時或延遲執行的任務,或在異步操作時需要超時回調的場景,使用ScheduledThreadPool是不錯的選擇。

 

 

六、CountDownLatch和CyclicBarrier

CountDownLatch用於控制多個線程同時開始某動作,採用的方式爲減計數的方式,當計數減至0時,位於await後的代碼纔會被執行。

CountDownLatch(int):將state設置爲入參值。

await():判斷count屬性值是否等於0,如等於0則直接返回,如不等於0則等待直到count等於0爲止或線程被interrupt。

countdown()將state值減。

CyclicBarrier是當await的數量達到了設定的數值後,才繼續往下執行。

CyclicBarrier(int):設置parties、count及barrierCommand屬性。

CyclicBarrier(Runnable):當await的數量到達了設定的數量後,會首先執行此Runnable對象。

await():count屬性值減1後等於0則執行傳入的Runnable對象。

 

 

七、CopyOnWriteArrayList、CopyOnWriteArraySet、ArrayBlockingQueue

CopyOnWriteArrayList是一個線程安全的並且在讀操作時無鎖的ArrayList。通過默認構造子創建的是大小爲0的數組。

CopyOnWriteArraySet基於CopyOnWriteArrayList實現,與CopyOnWriteArrayList不同的是在add時調用的是CopyOnWriteArrayList的addIfAbsent方法,創建一個新的大小+1的Object數組。

ArrayBlockingQueue是一個基於數組、先進先出、線程安全的集合類,特點是可實現指定時間的阻塞讀寫,並且容量是可限制的。

 

 

發佈了42 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章