JDK1.5引入的concurrent併發包

  併發是伴隨着多核處理器的誕生而產生的,爲了充分利用硬件資源,誕生了多線程技術。但是多線程又存在資源競爭的問題,引發了同步和互斥,並帶來線程安全的問題。於是,從jdk1.5開始,引入了concurrent包來解決這些問題。

  java.util.concurrent 包是專爲 Java併發編程而設計的包。

在Java中,當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替進行,在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是線程安全的。

    一般來說,concurrent包基本上由有3個package組成 : 

   java.util.concurrent:提供大部分關於併發的接口和類,如BlockingQueue,Callable,ConcurrentHashMap,ExecutorService, Semaphore等 ;
  java.util.concurrent.atomic:提供所有原子操作的類, 如AtomicInteger, AtomicLong等;
  java.util.concurrent.locks:提供鎖相關的類, 如Lock, ReentrantLock, ReadWriteLock, Condition等。 

   concurrent包下的所有類可以分爲如下幾大類:

    locks部分:顯式鎖(互斥鎖和速寫鎖)相關,如ReentrantLock,ReentrantReadWriteLock等;
    atomic部分:原子變量類相關,是構建非阻塞算法的基礎,如AtomicInteger,AtomicBoolean,AtomicLong,AtomicReference等;
    executor部分:線程池相關,如ExecutorService,Callable,Future等;
    collections部分:併發容器相關,如BlockingQueue,Deque,ConcurrentMap等;
    tools部分:同步工具相關,如CountDownLatch,CyclicBarrier,Semaphore,Executors,Exchanger等。

  JUC的類圖結構如下所示:

  concurrent包的優點有:
  ①功能豐富,諸如線程池(ThreadPoolExecutor),CountDownLatch等併發編程中需要的類已經有現成的實現,不需要自己去實現一套; 相比較而言,jdk1.4對多線程編程的主要支持幾乎只有Thread, Runnable,synchronized等。synchronized和JDK5之後的Lock均是悲觀鎖(悲觀鎖一般是一個人在使用的時候,另一個人不能用,所以性能極低,所能支持的併發量就不高)。
  ②concurrent包裏面的一些操作是基於硬件級別的CAS(compare and swap,比較再賦值),就是在cpu級別提供了原子操作,簡單的說就是可以提供無阻塞、無鎖定的算法; 而現代cpu大部分都是支持這種算法的。JUC(java.util.concurrent)是基於樂觀鎖的,既能保證數據不混亂,又能保證性能。

  version-(版本管理)就是基於樂觀鎖機制-->拿着我們期望的結果,和現有結果進行比對,如果是相同的,就賦值,如果不是相同的,就重試。
  CAS:有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做。  
  CAS算法內部是通過JNI--native方法來實現: 由java底層的C語言或者C++實現。 

  一般情況下:同步容器是有使用價值的。有時候,我們的異步容器,比如ArrayList,在併發環境下,會有這些問題:①數據紊亂;②java.util.ConcurrentModificationException。這都是對於集合的讀寫狀態不一致造成的問題。
   我們可以這樣構建同步容器:   

     Collections.synchronizedList(new ArrayList<>());
     Collections.synchronizedMap()      
     Collections.synchronizedSet()

   上面的方法可以實現同步容器,但是使用了悲觀鎖,因而效率不高。
  使用JUC體系中提供的容器,如:ConcurrentHashMap,則有這樣的優勢:①不會出現同步問題,數據是正常的;②速度相對較快  ,就算沒有hashmap快,也比hashtable快的多。其實,ConcurrentHashMap內部也是有同步的,在這方面面和hashtable沒有區別。那麼,ConcurrentHashMap快在哪裏?主要是因爲,其內部劃分了很多segment區域,當不同的線程操作不同的segment的時候,其實還是一個異步操作;只有當不同線程操作同一個segment的時候,纔會發生同步操作,所以速度很快。一個ConcurrentHashMap內部最多能有16個segment。

  我們接下來看一個非常有用的類CountDownLatch, 它是一個可以用來在一個進程中等待多個線程完成任務的類。在此給出一個應用場景:某個主線程接到一個任務,起了n個子線程去完成,但是主線程需要等待這n個子線程都完成任務以後纔開始執行某個操作。詳見代碼:

package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
/**
 * 主線程會在啓動的子線程完全結束後再繼續執行
 */
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.demoCountDown();
    }

    public void demoCountDown() {
        int count = 10;
        final CountDownLatch l = new CountDownLatch(count);
        for (int i = 0; i < count; ++i) {
            final int index = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.currentThread().sleep(2 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread -" + index + "- has finished...");
                    l.countDown();
                }
            }).start();
        }
        try {
            l.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("now all threads have finished");
    }
}

   執行結果如下所示:

thread -9- has finished...
thread -8- has finished...
thread -0- has finished...
thread -4- has finished...
thread -1- has finished...
thread -2- has finished...
thread -5- has finished...
thread -6- has finished...
thread -3- has finished...
thread -7- has finished...
now all threads have finished

   接下來,我們再看下Atomic相關的類, 比如AtomicLong, AtomicInteger等。簡單來說,這些類都是線程安全的,支持無阻塞無鎖定的。

package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
/**
 * 測試AtomicLong與long
 */
public class AtomicTest {
    public static void main(String[] args) {
        AtomicTest test = new AtomicTest();
        test.testAtomic();
    }

    public void testAtomic() {
        final int loopcount = 10000;
        int threadcount = 10;
        final NonSafeSeq seq1 = new NonSafeSeq();
        final SafeSeq seq2 = new SafeSeq();
        final CountDownLatch l = new CountDownLatch(threadcount);
        for (int i = 0; i < threadcount; ++i) {
            final int index = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < loopcount; ++j) {
                        seq1.inc();
                        seq2.inc();
                    }
                    System.out.println("finished : " + index);
                    l.countDown();
                }
            }).start();
        }
        try {
            l.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("both have finished....");
        System.out.println("NonSafeSeq:" + seq1.get());
        System.out.println("SafeSeq with atomic: " + seq2.get());
    }
    class NonSafeSeq {
        private long count = 0;
        public void inc() {
            count++;
        }
        public long get() {
            return count;
        }
    }

    class SafeSeq {
        private AtomicLong count = new AtomicLong(0);
        public void inc() {
            count.incrementAndGet();
        }
        public long get() {
            return count.longValue();
        }
    }
}

  上述代碼執行如下:

finished : 0
finished : 3
finished : 2
finished : 6
finished : 9
finished : 5
finished : 8
finished : 1
finished : 4
finished : 7
both have finished....
NonSafeSeq:98454
SafeSeq with atomic: 100000

   其中,NonSafeSeq是作爲對比的類,直接放一個private long count不是線程安全的,而SafeSeq裏面放了一個AtomicLong,是線程安全的;可以直接調用incrementAndGet來增加。通過上述執行結果可以看到,10個線程,每個線程運行了10,000次,理論上應該有100,000次增加,使用了普通的long是非線程安全的,而使用了AtomicLong是線程安全的。需要注意的是,這個例子也說明,雖然long本身的單個設置是原子的,要麼成功要麼不成功,但是諸如count++這樣的操作就不是線程安全的,因爲這包括了讀取和寫入兩步操作。

   在jdk 1.4時代,線程間的同步主要依賴於synchronized關鍵字,本質上該關鍵字是一個對象鎖,可以加在不同的instance上或者class上。
  concurrent包提供了一個可以替代synchronized關鍵字的ReentrantLock,簡單的說,你可以new一個ReentrantLock, 然後通過lock.lock和lock.unlock來獲取鎖和釋放鎖;需要注意的是,必須將unlock放在finally塊裏面。reentrantlock的好處有 :
  ①更好的性能;
  ②提供同一個lock對象上不同condition的信號通知;
  ③還提供lockInterruptibly這樣支持響應中斷的加鎖過程,意思是說你試圖去加鎖,但是當前鎖被其他線程hold住,然後你這個線程可以被中斷。  

package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
 * 測試ReentrantLock
 */
public class ReentrantLockTest {
    public static void main(String[] args) {
        ReentrantLockTest lockTest = new ReentrantLockTest();
        lockTest.demoLock();
    }

    public void demoLock() {
        final int loopcount = 10000;
        int threadcount = 10;
        final SafeSeqWithLock seq = new SafeSeqWithLock();
        final CountDownLatch l = new CountDownLatch(threadcount);
        for (int i = 0; i < threadcount; ++i) {
            final int index = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < loopcount; ++j) {
                        seq.inc();
                    }
                    System.out.println("finished : " + index);
                    l.countDown();
                }
            }).start();
        }
        try {
            l.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("both have finished....");
        System.out.println("SafeSeqWithLock:" + seq.get());
    }

    class SafeSeqWithLock {
        private long count = 0;
        private ReentrantLock lock = new ReentrantLock();

        public void inc() {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }

        public long get() {
            return count;
        }
    }
}

   上述代碼執行如下:

finished : 5
finished : 3
finished : 1
finished : 8
finished : 0
finished : 6
finished : 4
finished : 2
finished : 7
finished : 9
both have finished....
SafeSeqWithLock:100000

   上述代碼操作中,通過對inc操作加鎖,保證了線程安全。

  concurrent包裏面還提供了一個非常有用的鎖,讀寫鎖ReadWriteLock。  

A ReadWriteLock maintains a pair of associated locks, one for read-only operations and one for writing. 
The read lock may be held simultaneously by multiple reader threads, so long as there are no writers. 
The write lock is exclusive. 

   上述英文意思是說:讀鎖可以有很多個鎖同時上鎖,只要當前沒有寫鎖; 寫鎖是排他的,上了寫鎖,其他線程既不能上讀鎖,也不能上寫鎖;同樣,需要上寫鎖的前提是既沒有讀鎖,也沒有寫鎖。  

package com.itszt.test1;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * 測試讀寫鎖
 */
public class RWLockTest {
    public static void main(String[] args) {
        RWLockTest lockTest = new RWLockTest();
        lockTest.testRWLock_getw_onr();
    }

    public void testRWLock_getw_onr() {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        final Lock rlock = lock.readLock();
        final Lock wlock = lock.writeLock();
        final CountDownLatch l = new CountDownLatch(2);
        // start r thread,開啓讀鎖
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " now to get rlock--獲取讀鎖");
                rlock.lock();
                try {
                    Thread.currentThread().sleep(2 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " now to unlock rlock--釋放讀鎖");
                rlock.unlock();
                l.countDown();
            }
        }).start();
        // start w thread,開啓寫鎖
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + " now to get wlock--獲取寫鎖");
                wlock.lock();
                System.out.println(Thread.currentThread().getName() + " now to unlock wlock--釋放寫鎖");
                wlock.unlock();
                l.countDown();
            }
        }).start();
        try {
            l.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " finished");
    }
}

   上述代碼執行如下:

Thread-0 now to get rlock--獲取讀鎖
Thread-1 now to get wlock--獲取寫鎖
Thread-0 now to unlock rlock--釋放讀鎖
Thread-1 now to unlock wlock--釋放寫鎖
main finished

   ReadWriteLock的實現是ReentrantReadWriteLock,有趣的是,在一個線程中,讀鎖不能直接升級爲寫鎖,但是寫鎖可以降級爲讀鎖;這意思是說,如果你已經有了讀鎖,再去試圖獲得寫鎖,將會無法獲得, 一直堵住了;但是如果你有了寫鎖,再去試圖獲得讀鎖,就沒問題。

  下面是一段寫鎖降級的代碼:

public void testRWLock_downgrade() {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        Lock rlock = lock.readLock();
        Lock wlock = lock.writeLock();
        System.out.println("now to get wlock");
        wlock.lock();
        System.out.println("now to get rlock");
        rlock.lock();
        System.out.println("now to unlock wlock");
        wlock.unlock();
        System.out.println("now to unlock rlock");
        rlock.unlock();
        System.out.println("finished");
    }

   上述代碼在main函數中執行後,結果如下:

now to get wlock
now to get rlock
now to unlock wlock
now to unlock rlock
finished

   我們再看一段讀鎖升級的代碼:

public void testRWLock_upgrade() {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        Lock rlock = lock.readLock();
        Lock wlock = lock.writeLock();
        System.out.println("now to get rlock");
        rlock.lock();
        System.out.println("now to get wlock");
        wlock.lock();
        System.out.println("now to unlock wlock");
        wlock.unlock();
        System.out.println("now to unlock rlock");
        rlock.unlock();
        System.out.println("finished");
    }

   上述代碼執行中,已經有了讀鎖,再去試圖獲得寫鎖,將會無法獲得, 程序一直堵塞,進入死鎖狀態,顯示如下:

now to get rlock
now to get wlock

   另外,CountDownLatch是一個同步的輔助類,允許一個或多個線程,等待其他一組線程完成操作,再繼續執行。
  CyclicBarrier也是一個同步的輔助類,允許一組線程相互之間等待,達到一個共同點,再繼續執行。
  CountDownLatch和CyclicBarrier都是Synchronization  aid,即“同步輔助器”,既然都是輔助工具,在使用中有什麼區別,各自的使用場景如何?  

CountDownLatch場景舉例:一年級期末考試要開始了,監考老師發下去試卷,然後坐在講臺旁邊玩着手機等待着學生答題,有的學生提前交了試卷,並約起打球了,等到最後一個學生交卷了,老師開始整理試卷,貼封條,下班,陪老婆孩子去了。
啓發:CountDownLatch很像一個倒計時鎖,倒計時結束,另一個線程纔開始執行。就如監考老師要結束監考工作,必須等待所有學生都交了試卷,監考工作才能進入結束環節。

CyclicBarrier場景舉例:公司組織戶外拓展活動,幫助團隊建設,其中最重要的一個項目就是要求全體員工(包括女同事,BOSS,一個都不能少)都能翻越一個高達四米,而且沒有任何抓點的高牆,才能繼續進行其他項目。
啓發:CyclicBarrier可以看成是個障礙,所有的線程必須到齊後才能一起通過這個障礙。   

  另外,concurrent包中線程池部分,請參考我的另一篇博文《ScheduledThreadExecutor定時任務線程池》

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