java 對執行緒安全支援有哪些?

爬蜥發表於2018-09-16
  1. 同步容器。它的原理是將狀態封裝起來,並對每個公有方法都實行同步,使得每次只有1個執行緒能夠訪問容器的狀態。

    • Vector和HashTable
    • Collections.synchronizedXXX方法

    同步容器的問題

    1. 這種方式使得對容器的訪問都序列化,嚴重降低了併發性,如果多個執行緒來競爭容器的鎖時,吞吐量嚴重降低
    2. 對容器的多個方法的複合操作,是執行緒不安全的,比如一個執行緒負責刪除,另一個執行緒負責查詢,有可能出現越界的異常
  2. 併發容器。java.util.concurrent包裡面的一系列實現

    • Concurrent開頭系列。以ConcurrentHashMap為例,它的實現原理為分段鎖。預設情況下有16個,每個鎖守護1/16的雜湊資料,這樣保證了併發量能達到16

    分段鎖缺陷在於雖然一般情況下只要一個鎖,但是遇到需要擴容等類似的事情,只能去獲取所有的鎖

    ConcurrentHashMap一些問題

    1. 需要對整個容器中的內容進行計算的方法,比如size、isEmpty、contains等等。由於併發的存在,在計算的過程中可能已進過期了,它實際上就是個估計值,但是在併發的場景下,需要使用的場景是很少的。
      以ConcurrentHashMap的size方法為例:
    /**
        * Returns the number of key-value mappings in this map.  If the
        * map contains more than <tt>Integer.MAX_VALUE</tt> elements, returns
        * <tt>Integer.MAX_VALUE</tt>.
        *
        * @return the number of key-value mappings in this map
        */
       public int size() {
           //為了能夠算準數量,會算2次,如果兩次算的不準,就鎖住再算
           final Segment<K,V>[] segments = this.segments;
           int size;
           boolean overflow; // true if size overflows 32 bits
           long sum;         // sum of modCounts
           long last = 0L;   // previous sum
           int retries = -1; // 第一輪的計算總數不重試
           try {
               for (;;) {
                   if (retries++ == RETRIES_BEFORE_LOCK) {
                   //RETRIES_BEFORE_LOCK 預設是2
                       for (int j = 0; j < segments.length; ++j)
                           ensureSegment(j).lock(); // force creation
                   }
                   sum = 0L;
                   size = 0;
                   overflow = false;
                   for (int j = 0; j < segments.length; ++j) {
                       Segment<K,V> seg = segmentAt(segments, j);
                       if (seg != null) {
                           sum += seg.modCount;
                           int c = seg.count;
                           if (c < 0 || (size += c) < 0)
                               overflow = true;
                       }
                   }
                   //第一次計算的時候
                   if (sum == last)
                       break; //如果前後兩次數數一致,就認為已經算好了
                   last = sum;
               }
           } finally {
               if (retries > RETRIES_BEFORE_LOCK) {
                   for (int j = 0; j < segments.length; ++j)
                       segmentAt(segments, j).unlock();
               }
           }
           return overflow ? Integer.MAX_VALUE : size;
       }
    複製程式碼
    1. 不能提供執行緒獨佔的功能
    • CopyOnWrite系列。以CopyOnWriteArrayList為例,只在每次修改的時候,進行加鎖控制,修改會建立並重新釋出一個新的容器副本,其它時候由於都是事實上不可變的,也就不會出現執行緒安全問題

    CopyOnWrite的問題

    每次修改都複製底層陣列,存在開銷,因此使用場景一般是迭代操作遠多於修改操作

    CopyOnWriteArrayList的讀寫示例

    /**
       * Appends the specified element to the end of this list.
        *
       * @param e element to be appended to this list
      * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
           final ReentrantLock lock = this.lock;
          lock.lock();
         try {
            Object[] elements = getArray();
           int len = elements.length;
          Object[] newElements = Arrays.copyOf(elements, len + 1);
         newElements[len] = e;
        setArray(newElements);
       return true;
    } finally {
      lock.unlock();
    }
    }
           /**
          * {@inheritDoc}
         *
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        public E get(int index) {
            return get(getArray(), index);
        }
        /**
       * Gets the array.  Non-private so as to also be accessible
       * from CopyOnWriteArraySet class.
       */
        final Object[] getArray() {
           return array;
        }
        private E get(Object[] a, int index) {
            return (E) a[index];
         }
    複製程式碼

java中的同步工具類

  1. 阻塞佇列,BlockingQueue。它提供了put和take方法,在佇列不滿足各自條件時將產生阻塞

    BlockingQueue使用示例,生產者-消費者

    public static void main(String[] args) throws Exception {
           BlockingQueue queue = new ArrayBlockingQueue(1024);
           Producer producer = new Producer(queue);
           Consumer consumer = new Consumer(queue);
           new Thread(producer).start();
           new Thread(consumer).start();
       }
    }
    public class Producer implements Runnable{
       protected BlockingQueue queue = null;
    
       public Producer(BlockingQueue queue) {
           this.queue = queue;
       }
       
       public void run() {
           try {
               queue.put("1");
               Thread.sleep(1000);
               queue.put("2");
               Thread.sleep(2000);
               queue.put("3");
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
    public class Consumer implements Runnable{
       
       protected BlockingQueue queue = null;
       
       public Consumer(BlockingQueue queue) {
           this.queue = queue;
       }
       
       public void run() {
           try {
               System.out.println(queue.take());
               System.out.println("Wait 1 sec");
               System.out.println(queue.take());
               System.out.println("Wait 2 sec");
               System.out.println(queue.take());
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
    }
    複製程式碼

    輸出為

    1
    Wait 1 sec
    2
    Wait 2 sec
    3
    複製程式碼
  2. 閉鎖

    • CountDownLatch。使多個執行緒等待一組事件發生,它包含一個計數器,表示需要等待的事件的數量,每發生一個事,就遞減一次,當減為0時,所有事情發生,允許“通行”

    CountDownLatch示例:

    public class TestHarness{
       public long timeTasks(int nThreads,final Runnable task) throws InterruptedException {
       final CountDownLatch startGate = new CountDownLatch(1);
       final CountDownLatch endGate = new CountDownLatch(nThreads);
       for (int i=0;i<nThreads;i++){
           Thread t = new Thread(){
               public void run(){
                   try {
                       startGate.await();
                       try {
                           task.run();
                       }finally {
                           endGate.countDown();
                       }
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           };
           t.start();
       }
       long start = System.nanoTime();
       startGate.countDown();
       endGate.await();
       long end=System.nanoTime();
       return end-start;
       }
    }
    複製程式碼

    啟動門使主執行緒能夠同時釋放所有的工作執行緒,結束門使得主執行緒能夠等待最後一個執行緒執行完

    • FutureTask。Future.get的如果任務執行完成,則立即返回,否則將阻塞直到任務完結,再返回結果或者是丟擲異常
  3. 訊號量,Semaphore 。它管理著一組虛擬的許可,許可的數量可通過建構函式指定,在執行操作時首先獲得許可,並在使用後釋放許可,如果沒有,那麼accquire將阻塞直到有許可。

    Semaphore示例

    public class BoundedHashSet<T>{
       private final Set<T> set;
       private final Semaphore sem;
    
       public BoundedHashSet(int bound) {
           this.set = Collections.synchronizedSet(new HashSet<T>());
           this.sem = new Semaphore(bound);
       }
       public boolean add(T o) throws InterruptedException {
           sem.acquire();
           boolean wasAdded = false;
           try {
               wasAdded = set.add(o);
              return wasAdded;
           }finally {
               if (!wasAdded){
                   sem.release();
               }
           }
       }
       public boolean remove(Object o){
           boolean wasRemoved = set.remove(o);
           if(wasRemoved){
              sem.release();
           }
           return wasRemoved;
               
       }
    }
    複製程式碼
  4. 柵欄。它能阻塞一組執行緒直到某個事件發生。 與閉鎖的區別:

    • 所有執行緒必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其它執行緒。
    • 閉鎖一旦進入終止狀態,就不能被重置,它是一次性物件,而柵欄可以重置
    • CyclicBarrier。可以使一定數量的參與方反覆地在柵欄位置彙集

    CyclicBarrier使用示例

    public static void main(String[] args) {
    //第k步執行完才能執行第k+1步
           CyclicBarrier barrier = new CyclicBarrier(3,new StageKPlusOne());
           StageK[] stageKs = new StageK[3];
           for (int i=0;i<3;i++){
               stageKs[i] = new StageK(barrier,"k part "+(i+1));
           }
           for (int i=0;i<3;i++){
               new Thread(stageKs[i]).start();
           }
    }    
    class StageKPlusOne implements Runnable{
       @Override
       public void run() {
           System.out.println("stage k over");
           System.out.println("stage k+1 start counting");
       }
    }
    class StageK implements Runnable{
       private CyclicBarrier barrier;
       private String stage;
       
       public StageK(CyclicBarrier barrier, String stage) {
           this.barrier = barrier;
           this.stage = stage;
       }
       
       @Override
       public void run() {
           System.out.println("stage "+stage+" counting...");
           try {
               TimeUnit.MILLISECONDS.sleep(500);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println("stage "+stage+" count over");
           try {
               barrier.await();
           } catch (InterruptedException e) {
               e.printStackTrace();
           } catch (BrokenBarrierException e) {
               e.printStackTrace();
           }
       }
    }
    複製程式碼

    輸出為

    stage k part 1 counting...
    stage k part 3 counting...
    stage k part 2 counting...
    stage k part 2 count over
    stage k part 3 count over
    stage k part 1 count over
    stage k over
    stage k+1 start counting
    複製程式碼
    • Exchanger。它是一種兩方柵欄,各方在柵欄位置交換資料
      Exchanger 使用示例:
    public static void main(String[] args) {
           Exchanger exchanger = new Exchanger();
            ExchangerRunnable er1 = new ExchangerRunnable(exchanger,"1");
            ExchangerRunnable er2 = new ExchangerRunnable(exchanger,"2");
            new Thread(er1).start();
            new Thread(er2).start();
        
        }
        class ExchangerRunnable implements Runnable{
        
        private Exchanger e;
        private Object o;
    
        public ExchangerRunnable(Exchanger e, Object o) {
           this.e = e;
            this.o = o;
    }
       
        @Override
        public void run() {
           Object pre=o;
            try {
                o=e.exchange(o);
                System.out.println("pre:"+pre+" now:"+o);
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        }
    }
    複製程式碼

    輸出如下

    pre:1 now:2
    pre:2 now:1
    複製程式碼

附錄

案例

相關文章