Java並發編程深入學習

來源:互聯網
上載者:User

標籤:tac   引用   .com   java7   區別   執行個體   code   override   map   

基本概念

在實踐中,為了更好的利用資源提高系統整體的輸送量,會選擇並發編程。但由於環境切換和死結等問題,並發編程不一定能提高效能,因此如何合理的進行並發編程時本文的重點,接下來介紹關於鎖最基本的一些知識(選學)。

  • volatile:輕量,保證共用變數的可見度,使得多個線程對共用變數的變更都能及時擷取到。其包括兩個子過程,將當前處理器緩衝行的資料寫回到系統記憶體,之後會使其他CPU裡緩衝了該記憶體位址的資料無效。
  • synchronized:相對重量,其包含3種形式,針對普通同步方法,鎖是當前執行個體對象;針對靜態同步方法,鎖是當前類的Class對象;對於同步代碼塊,鎖是Synchonize括弧內配置的對象。此外,synchronize用的鎖存在ava對象頭中,編譯後會插入類似monitorenter, monitorexit的代碼。
  • 鎖狀態:包括無鎖狀態,偏向鎖狀態,輕量級鎖狀態,重量級鎖狀態。Tip,鎖可以升級但不能降級。
  • Java實現原子操作:可以通過鎖和迴圈CAS來實現原子操作,不過其也存在3個問題,包括ABA問題,通過版本號碼解決;迴圈時間長開銷大,通過pause指令減少自旋帶來的開銷;只能保證一個共用變數的原子操作,通過AtomicRefence保證引用對象間的原子性,接下來看一個最簡單的CAS操作樣本。

    protected void safeCount() {    for (;;) {        int i = atomicI.get();        if (atomicI.compareAndSet(i, ++i))            break;    }}
線程

這部分和之後的鎖是基礎部分的核心內容,需要好好理解。一般來說,線程都是作業系統最小的調度單元,一個進程中可以包含多個線程,每個線程都擁有自己的計數器、堆棧和局部變數。系統會採用分時的形式調度啟動並執行線程,OS會分出一個個的時間片到線程,此外還可以給線程設定優先權,來保證優先順序高的線程獲得更多的CPU時間。通過下面的範例程式碼可以發現,java程式的運行不僅就是main線程,還有清楚Reference的線程、調用對象finalize方法的線程、分發處理髮送給JVM資訊的線程、Attach Listener線程等。

// 擷取管理線程的MXbeanThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true);// 列印線程資訊for (ThreadInfo threadInfo : threadInfos) {    System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());}
  • 線程的狀態:Java線程的整個生命週期包括6種不同狀態,分別是NEW初始狀態,線程被構建但未start;RUNNABLE運行狀態,Java線程將OS中的就緒和運行兩種狀態都稱作“運行中”;BLOCKED阻塞狀態,表示線程阻塞於鎖;WAITING等待狀態,表示線程進入等待狀態,進入該狀態表示當前線程需要等待其他線程做出特定動作(通知或中斷);TIME_WAITING逾時等待狀態,該狀態不同於WAITING,其會在指定的時候後返回;TERMINATED終止狀態,可以使用interrupt()合理的終止線程,表示當前線程已經執行完畢,之後通過一張Java線程狀態圖來做個形象的瞭解。

    Daemon守護線程概念非常簡單,java的虛擬機器只有在不存在Daemon線程時才會退出。

  • 線程間通訊有一個的經典範式,等待/通知機制。一個線程修改了一個對象的值,而另一個線程感知到了變化,然後進行相應的操作,整個過程開始於一個線程,而最終執行的是另一個線程。

    //等待方:1.擷取對象的鎖 2.如果條件不滿足,那麼調用對象的wait方法,被通知後要檢查條件//3.條件滿足則執行對應的邏輯synchronized(lock){while(!flag){lock.wait();}}//通知方:1.擷取對象的鎖 2.改變條件 3.通知所有等待在對象上線程synchronized(lock){flag = true;lock.notifyAll();}


    如果線程A執行了Thread.join(),表示當線程A等待的線程終止之後才從thread.join()返回,其還提供了join(long millis)和join(int millis, int nanos)方法,當給點時間內前驅線程未結束則強制返回。
    ThreadLocal線程變數是以ThreadLocal對象為鍵,任意對象為值的儲存結構。此外,這部分常見的應用執行個體包括等待逾時模式,資料庫線程池,基於線程池的簡單Web伺服器等。

鎖是用來控制多個線程訪問共用資源的方式,在Lock介面出現前都是通過synchronized來處理線程間同步問題。鎖的主要方法包括locktryLockunlocknewCondition擷取等待通知群組件等方法。其相關的實現包括隊列同步器AbstractQueuedSynchronizer、重入鎖ReentrantLock、讀寫鎖ReentrantReadWriteLock、LockSupport和Condition介面,這部分的重點講是可重新進入鎖ReenterLock

  • 重入鎖ReentrantLock表示該鎖可以支援一個線程對資源的重複加鎖,並支援擷取瑣時的公平性的選擇。預設是非公平鎖,其特點是效能要遠高於公平鎖(嚴格按照請求時間順序擷取所,FIFO)。

    ReentrantLock lock = new ReentrantLock(true);lock.lock();try {    // TODO} finally {    lock.unlock();}
  • 讀寫鎖ReentrantReadWriteLock同時維護一個讀鎖和一個寫鎖,允許多個讀線程同時訪問共用資料,只會在寫線程訪問時阻塞,和資料庫的鎖機制很類似,該方式使得並發性等到很大提升。其除了公平性選擇、可重新進入等特性外,還支援鎖降級,遵循擷取寫鎖、擷取讀鎖再釋放寫鎖的次序,寫鎖能降級為讀鎖。
  • LockSupport提供park阻塞,unpark喚醒的靜態方法。
  • Condition介面:任意的Java對象,都擁有一組監視器方法,包括wait()notify()等,這些方法與synchronized關鍵字配合可以實現等待/通知模式,Condition介面也提供了類似的監視器方法,但功能更加強大。

進階概念並發容器和架構
  • ConcurrentHashMap VS HashTable:之所以決定好好學學Java並發編程,可以說就是面試時被面試官懟住這個問題。過去只知道ConcurrentHashMap是HashMap的安全執行緒版本,但其與HashTable的區別卻從來沒關心過。簡答來說,前者通過SegmentHashEntry進行封裝,達到了記錄層級的鎖粒度,和資料庫相關知識類似。HashTable由於只支援[表]級鎖,因此效能比較低下。ConcurrentLinkedQueue則是隊列的安全執行緒版本,沒有什麼特別要說的。
  • BlockingQueue阻塞隊列是一種支援兩個附加操作的隊列,一個是支援阻塞插入,即當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿,另一個支援阻塞的移除方法,意思是隊列為空白時,擷取元素的線程會等待隊列變為非空。其處理方式包括拋出異常、返回特殊值、一直阻塞和逾時退出。Java7提供的阻塞隊列包括ArrayBlockingQueue,LinkedBlockingQueue,DelayQueue等,不是重點。
  • Fork/Join架構:Java7中提供的類似Map/Reduce的並行開發架構,Fork可以將任務分解為子任務,而Join則負責匯總結果。其中涉及一個工作竊取work-stealing演算法,可以使得線程可以從其他隊列裡竊取任務來執行,優點是充分利用線程進行並行計算,減少了線程間的競爭;缺點是在某些情況下存在競爭,比如雙端隊列裡只有一個任務時,該演算法會消耗更多的系統資源。
並發工具類

這部分的內容非常重要,之後介紹的一些常見模式可以很好的應用在日常的開發情境中,一定要掌握牢靠。

  • 13個原子操作類:比較常見的有AtomicBooleanAtomicInteger,AtomicIntegerArray,AutomicReference等,接下來選擇一個比較複雜的作為樣本。

    User user = new User("xionger", 30);atomicUserRef.set(user);User updateUser = new User("xiongerda", 32);atomicUserRef.compareAndSet(user, updateUser);System.out.println(atomicUserRef.get().getName());System.out.println(atomicUserRef.get().getOld());
  • CountDownLatch:類似一個計數器,允許一個或多個線程等待其他線程完成操作,比如主線程需要等待2個子線程完成任務後返回。常見情境,比如我們解析Excel多個Sheet的資料,那麼可以由每個線程處理一個,再都完成後再通知系統解析完成。

    static CountDownLatch latch = new CountDownLatch(3);public static void main(String[] args) throws InterruptedException {    new Thread(new Runnable() {        @Override        public void run() {            latch.countDown();        }    }).start();    new Thread(new Runnable() {        @Override        public void run() {            latch.countDown();        }    }).start();    latch.await();}
  • CyclicBarrier:其讓一組線程到達一個屏障,類似跑步的起跑線,直到最後一個線程到達屏障,屏障才會開門,所有被阻塞的線程才能繼續執行。以可用於多線程計算資料,最後合并計算資料的情境,例如用一個Excel儲存使用者所有銀行流水,每個Sheet儲存一個賬戶近一年的流水,現在要統計日均流水,那麼可以先計算每個Sheet的日均流水,最後匯總。使用上和CountDownLatch有些相似,不過其特點是可以使用reset方法重設,並通過isBroken()判斷線程是否中斷。
  • Semaphore訊號量用於控制同時訪問特定資源的線程數量,常用與流量控制,比如資料庫連接的控制,有50個線程需要使用15資料庫連接。

    private static ExecutorService executorService = Executors.newFixedThreadPool(50);    private static Semaphore sema = new Semaphore(15);    public static void main(String[] args) {        for (int i = 0; i < 50; i++) {            executorService.execute(new Runnable() {                @Override                public void run() {                    try {                        sema.acquire();                        System.out.println("save data");                        sema.release();                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                }            });        }        executorService.shutdown();    }
  • Exchanger交換者可用於線程間資料交換,它提供一個同步點,在這個同步點,兩個線程可以交換彼此的資料。Exchange可以用於遺傳演算法和校對工作等情境,比如需要將紙質流水錄入到系統,為了避免錯位,使用AB崗兩人進行錄入,錄入到Excel後,系統需要載入這兩個Excel並進行校對。

    private static final Exchanger<String> exchanger = new Exchanger<>();private static ExecutorService threadPool = Executors.newFixedThreadPool(2);public static void main(String[] args) {    threadPool.execute(new Runnable() {        @Override        public void run() {            String a = "銀行流水A";            try {                exchanger.exchange(a);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    });    threadPool.execute(new Runnable() {        @Override        public void run() {            String b = "銀行流水B";            try {                String a = exchanger.exchange(b);                System.out.println("a和b是否資料一致:" + a.equals(b) + ",a錄入的是: " + a + ",b錄入的是" + b);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    });}
Executor架構
    • 線程池
      在介紹Executor架構前,先介紹線程池相關的原理,其是並發編程中最為重要的部分,合理的使用線程池可以降低系統消耗、提高響應速度、提高線程的可管理性,接下來介紹線程池的基礎處理流程。

      1.如果當前啟動並執行線程少於corePoolSize直接建立新線程來執行任務,需要擷取全域鎖。
      2.如果啟動並執行線程等於或多餘corePoolSize則將任務加入BlockingQueue。
      3.如果由於隊列已滿,無法將任務加到BlockingQueue,則建立新的線程來處任務,需要擷取全域鎖。
      4.如果建立新線程將操作maximumPoolSize,任務將被拒絕,並調用RejectedExecutionHandler.rejectedExecution()方法。
      ThreadPoolExecutor採用上述步驟,保證了執行execute()時,儘可能的避免了擷取全域鎖,大部分的可能都會執行步驟2,而無需擷取全域鎖。
      在引入Executor架構前,Java線程既是工作單元,也是執行機制。而在Executor架構中,工作單元和執行機制被分離開來,前者包括RunnableCallable,而執行機制由Executor架構提供。該架構是一個兩級的調度模型,在上層,通過調度器Executor將多個任務映射到固定數量的線程;在底層,作業系統核心將這些線程再映射到處理器上。而我們的應用程式只需通過E該架構控制上層的調度即可。
      Tip:
      合理配置線程池時,需要根據具體情境給出對應的解決方案,總體來說,推薦使用有界隊列,便於控制。
      CPU密集型:配置儘可能少的線程,如cpu數量+1,可以通過Runtime.getRuntime().availableProcessors()擷取CPU個數
      IO密集型:配置儘可能多的線程,如2*cpu數量,常見情境,等待資料庫或服務介面的返回。
      優先順序:可以通過PriorityBlockingQueue來處理
      監控:可以通過taskCount,completedTaskCount,getActiveSize等函數來監控線程池的運行。

    • Executor架構結構主要由三部分組成
      a.任務,包括任務實現的介面RunnableCallable
      b.任務的執行,包括任務執行機制的核心介面Executor和其子類ExecutorService,相關的實作類別包括ThreadPoolExecutorScheduledThreadPoolExecutor
      c.非同步計算的結果,包括Future和其實現FutureTask
    • ThreadPoolExecutor:架構的核心類,由corePoolmaximumPoolBlockingQueueRejectedExecutionHandler4部分組成,可以由工具類Executors建立。具體老說,工具類可以建立FixedThreadPool固定線程數(最推薦)、SingleThreadExecutorCachedThreadPool三種類型的ThreadPoolExecutor
    • ScheduledThreadPoolExecutor:比基礎的Timer對象更加全面,其通過DelayQueue來執行循環性或定時的任務。
    • FutureTask基於AbstractQueuedSynchronizer(AQS),之前介紹的ReentrantLockCountDownLatch等其實都是基於AQS來實現的。AQS是一個同步架構,提供通用機制來原子性的管理同步狀態、阻塞&喚醒線程、維護被阻塞的線程隊列。每個基於AQS的實現都會包含兩類操作,acquire用於阻塞調用線程,對應futureTask.get(),知道AQS狀態允許這個線程才能繼續執行;另一個為release,對應futureTask.cancel()&run(),該操作改變AQS狀態,改變後的狀態允許一個或多個阻塞線程解除阻塞。

      public static void main(String[] args) throws InterruptedException, ExecutionException {    ExecutorService executor = Executors.newSingleThreadExecutor();     Future<BigDecimal> result = executor.submit(new Callable<BigDecimal>() {        @Override        public BigDecimal call() throws Exception {            return getSalaryByService();        }    });    System.out.println(result.get());}
    • 生產者消費者模式:該模式可以解決大部分的並發問題,其通過阻塞隊列,平衡生產線程和消費線程的工作能力來提高程式整體處理資料的速度。比如經常會郵件來分享技術知識,就可以通過通過Job到郵箱中擷取到文章並放入阻塞隊列,之後消費者去擷取資料並插入到類似confluence的文件管理工具中,接下來展示一個單個生產者,多個消費者的應用情境實現。
      Tip:
      線上問題定位:Linux中可以通過top命令查看進程的情況,之後可以使用互動命令1查看CPU效能,H查看每個線程的效能資訊。
      效能測試:比如使用Jmeter來做壓測,可以通過netstat -nat | grep 3306 -c來查看資料的壓力情況。

Java並發編程深入學習

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.