併發編程—Java多線程總結

目錄

先了解幾個概念

  • 多線程:進程和線程是一對多的關係,一個進程(一個程序),由不一樣的線程來運行。有共享的空間也有獨立的空間。
  • 並行: 同時進行,拿兩個cpu來跑一樣的程序一樣的代碼片斷,那就並行了。
  • 併發:不一樣時進行,只有一個cpu,而多個線程都在爭取這個cpu資源。即是併發。用TPS和QPS去衡量併發程度。
  • TPS:Transactions Per Second(每秒傳輸的事物處理個數),簡單說就是服務器每秒處理事務的個數。 
    完整的包括: 請求+數據庫訪問+響應
  • QPS:Queries Per Second(每秒查詢率),簡單說就是服務器每秒處理完請求的個數。

一、線程的生命週期

先了解線程的生命週期,上圖。線程的生命週期從一個新的線程產生到結束中間會經歷很是多的狀況,大致上以下圖,多線程環境下咱們主要是再running的時候採起線程的保護措施,從而使多線程環境下,讓線程進入阻塞的狀態。這種保護思想其實就是排他了,到最後都得一個個來,不管式任務仍是內存互不干擾,便達到線程安全了。java

線程的生命週期數據庫

二、jvm內存模型

到了jdk8,內存模型已經有了至關的改變了,下圖是小編學習了幾篇優秀的博文學習,根據本身的理解繪製出來的,請多指教。編程

jdk8內存模型緩存

獨立內存空間安全

從圖中能夠看出線程安全的區域是在棧空間,每一個線程會有獨立的棧空間,從而也解釋了爲何方法內是線程安全的,而全局變量這些是線程不安全的,由於這些都在堆區。服務器

共享內存空間多線程

堆空間,和MateSpace是被全部線程共享的,所以在處理多線程問題的時候,其實主要是處理這兩個空間的內容。共享區域在不加任何保護的狀況下對其操做,會有異常結果。併發

怎麼作到線程安全?

  • 只使用線程安全的內存空間,不使用共享的空間
  • 對共享的內存空間採起保護措施,好比:加Lock,volatile修飾等

三、線程的實現方式

  • 繼承Thread
package com.example.demo;

import org.junit.Test;

/**
 * Project <demo-project>
 * Created by jorgezhong on 2018/8/31 16:01.
 */
public class ThreadDemo {

    @Test
    public void extendThreadTest() {
        ExtendThread extendThread = new ExtendThread();
        extendThread.start();
    }

    
    class ExtendThread extends Thread {

        @Override
        public void run() {
            // TODO: 2018/8/31
        }
    }

}
  • 實現Runnable接口
@Test
    public void runnableThreadTest(){

        RunnableThread runnableThread = new RunnableThread();
        Thread thread = new Thread(runnableThread);
        thread.start();
        
    }

    class RunnableThread implements Runnable{

        @Override
        public void run() {
            // TODO: 2018/8/31
        }
    }
  • Callable和Future
@Test
    public void callableThreadTest(){

        CallableThread callableThread = new CallableThread();
        FutureTask<String> stringFutureTask = new FutureTask<>(callableThread);
        Thread thread = new Thread(stringFutureTask);
        thread.start();


    }

    /**
     * 這種實現是由返回值的
     */
    class CallableThread implements Callable<String>{

        @Override
        public String call() {
            // TODO: 2018/8/31
            return "";
        }
    }

補充:Fulture和Callable(Future模式)框架

首先,這兩東西都在java.util.concurrent下,java自己就未多線程環境考慮了不少。看看下面的UML圖,RunnableFuturej繼承了Future和Runnable接口,將Future引入Runnable中,而且提供了默認實現FutureTask。RunnbleCallable和Future補充解決了兩個問題,一個是多線程阻塞解決方案,另外一個則是返回值問題。咱們知道Runnable和Thread定義的run()是沒有返回值的。並且當線程遇到IO阻塞的時候,只能等待,該線程沒法作任何事情。Callable和Fulture分別解決了這兩個問題。Callable提供了返回值的調用,而Fulture提供了多線程異步的機制。異步

Callable沒什麼好說的,例子如上面代碼,就是多了個泛型的返回值,方法變成了call而已。Future就比較複雜了。FultureTask的構造方法接受Runnable或者Callable,也就是說Runnable和Callable的實例均可以使用Fulture來完成異步獲取阻塞返回值的操做。

uml java fulture m

Future只有5個方法

  • cancel:取消任務的執行。參數表示是否當即中斷任務
  • isCancelled:判斷任務是否已經取消
  • isDone:判斷任務是否已經完成
  • get():阻塞到任務接受獲取返回值
  • get(long,TimeUnit):指定超時時間,獲取返回值

Future模式缺陷

Fulture比較簡單,基本上只經過兩種方式:查看狀態和等待完成。要麼去查看一下是否是完成了,要麼就等待完成,而線程和線程之間的通訊只有經過等待喚醒機制來完成。原來的Fulture功能太弱,以致於google的Guava和Netty這些牛逼的框架都是從新去實現以拓展功能。而java8引入了實現了CompletionStage接口的CompletableFuture。能夠說是極大的擴展了Future的功能。吸取了Guava的長處。

  • CompletableFuture介紹

關於CompletableFuture和的具體內容,後續再寫一篇詳細介紹。結合java8的Stream API CompletionStage接口定義不少流式編程的方法,咱們能夠進行流式編程,這很是適用於多線程編程。CompletableFuture實現了該接口,並拓展了本身的方法。對比Fulture多了幾十個方法。大體能夠分爲同步的和異步的兩種類型。而做業的時候,能夠切入任務某一時刻,好比說完成後作什麼。還能夠組合CompletionStage,也就是進行線程之間的協調做業。

  • 使用線程池提交線程的實現(見下文)

四、線程池

咱們能夠看到java線程池相關的包,他們之間的關係以下圖。

java uml thread

java uml thread m

從uml類圖能夠看出(圖片有點大,放大一下把),整個線程池構成實際上是這樣的:

  • 一、 Executor 封裝了線程的實現
  • 二、 Executor 的子接口 ExecutorService 定義了管理 Executor 的一系列方法。 
    ThreadPoolExecutor 實現了 ExecutorService ,定義了一系列處理多線程的內容,好比線程工程和保存線程任務的隊列
  • 三、 ScheduledExecutorService 擴展了 ExecutorService ,增長了定時任務調度的功能。 
    ScheduledThreadPoolExecutor 實現了 ScheduledExecutorService ,同時繼承 ThreadPoolExecutor 的功能
  • 四、 Executors 靜態類,包含了生成各類ExecutorService的方法。

從接口的組成能夠看出,Executor、ExecutorService和ScheduledThreadPoolExecutor三個接口定義了線程池的基礎功能。能夠理解爲他們三個就是線程池。

那麼整個線程池是圍繞兩個默認實現ThreadPoolExecutor和ScheduledThreadPoolExecutor類來操做的。

至於操做,我發現java還蠻貼心的,默認實現的線程池只區分了可定時調度和不可定時調度的。實在是太過於靈活了,本身使用的話要配置一大堆參數,我想個線程池而已,給我搞這麼多配置表示很麻煩,只須要關心是否是定時的,只考慮我分配多少線程給線程池就行了。所以有了Executors

Executors操做兩個默認的實現類,封裝了了大量線程池的默認配置,並提供瞭如下幾種線程池給咱們,咱們只須要管線少部分必要的配置便可。

  • Single Thread Executor:只有一個線程的線程池,順序執行
ExecutorService pool = Executors.newSingleThreadExecutor();
//提交實現到線程池
pool.submit(() -> {
    // TODO: 2018/8/31 do something
});
  • Cached Thread Pool:緩存線程池,超過60s池內線程沒有被使用,則刪掉。就是一個動態的線程池,咱們不須要關心線程數
ExecutorService pool = Executors.newCachedThreadPool();
//提交實現到線程池
pool.submit(() -> {
    // TODO: 2018/8/31 do something
});
  • Fixed Thread Pool:固定數量的線程池
//參數爲線程數
ExecutorService pool = Executors.newFixedThreadPool(8);
//提交實現到線程池
pool.submit(() -> {
    // TODO: 2018/8/31 do something
});
  • Scheduled Thread Pool:用於調度指定時間執行任務的線程池
//參數爲線程數
ScheduledExecutorService pool = Executors.newScheduledThreadPool(8);

/*
* 提交到線程池
* 參數1:Runnable
* 參數2:初始延遲時間
* 參數3:間隔時間
* 參數4:時間單位
*/
pool.scheduleAtFixedRate(() -> {
    // TODO: 2018/8/31 do something 
}, 1000, 2000, TimeUnit.MILLISECONDS);
  • Single Thread Scheduled Pool:調度指定時間執行任務的線程池,只有一個線程
ScheduledExecutorService pool = Executors.newSingleThreadScheduledExecutor();

//參數少了初始延遲時間
pool.schedule(() -> {
    // TODO: 2018/8/31 do something 
}, 1000, TimeUnit.MILLISECONDS);
  • 線程池的配置策略

一、 考慮業務類型

除了考慮計算機性能外,更多的仍是考慮業務邏輯,若是業務是運算密集型的,不適合開太多的線程,由於運算通常是cpu在算,cpu自己就是用於計算,極快,所以一個線程很快就能計算完畢。線程多了反而增長了資源的消耗。另外一種是IO密集型業務,這種業務就比較是適合開多一點線程,由於IO、通訊這些業務自己就是很是慢的,大部分的系統的瓶頸都集中這兩方面。所以這些業務適合開多個線程。

二、配合cpu的核心和線程數

在咱們配置線程的時候,能夠參考cpu的總線程,儘可能不超出總線程數。通常使用核心數。

五、保護措施

5.一、 synchronized

這實際上是一個監視器。能夠監視類和對象。

原理:能夠這麼理解,每一個實例化的對象都有一個公共的鎖,該鎖被該實例共享。所以對於該對象的全部被synchronized修飾的實例方法,是共享的同一個對象鎖。同理,類鎖也是同樣的,伴隨Class對象的生成,也會有一個類監視器,也就有一個默認的類鎖了,被synchronized修飾的全部靜態方法都共享一個類鎖。

缺陷:同步鎖關鍵子雖然方便,可是畢竟是被限制了修飾方式,所以不夠靈活,另外修飾在方法上是修飾了整個方法,所以性能在併發量大且頻繁的時候就顯得不那麼好了。

  • 修飾實例方法:
public synchronized void synchronizedMethod(){
    // TODO: 2018/8/29 do something 
}
  • 修飾靜態方法:
public static synchronized void synchronizedMethod(){
    // TODO: 2018/8/29 do something
}
  • 修飾代碼快:
public void synchronizedMethod(){
    //Object.class爲鎖對象,其實就是鎖的鑰匙,使用同一把鑰匙的鎖是同步的
    synchronized (Object.class){
        // TODO: 2018/8/29 do something
    }
}

5.二、Lock&&ReadWriteLock

因爲synchronized的缺陷不夠靈活,對應的天然有靈活的解決方案。Lock即是解決方案。Lock是java.util.concurrent.locks包下的一個接口。可是Lock是靈活了,可是既然都多線程了,咱們固然是最求性能啦。因爲不少數據是對查看沒有線程安全要求的,只須要對寫入修改要求線程安全便可,因而有了ReadWriteLock,讀寫鎖能夠只對某一方加鎖,把鎖住的內容範圍更加縮小了,提高了性能。從下圖能夠看到,ReentrantLock實現了Lock而ReentrantReadWriteLock實現了ReadWiteLock。咱們能夠直接使用它們的實現類實現鎖功能。

uml_java_lock

5.2.一、Lock

獲取鎖:lock()、tryLock()、lockInterruptibly()

釋放鎖:unLock()

直接上代碼來學習效果是最快的

  • DEMO:兩個線程爭取同一把鎖
package com.example.demo;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.locks.ReentrantLock;

/**
 * Project <demo-project>
 * Created by jorgezhong on 2018/8/30 15:48.
 */
public class LockDemo {

    private static final Logger LOGGER = LoggerFactory.getLogger(LockDemo.class);

    /**
     * 兩個線程爭取同一把鎖
     */
    @Test
    public void lockTest() throws InterruptedException {
        //造一把鎖先
        ReentrantLock reentrantLock = new ReentrantLock();

        Thread thread0 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                lockTestHandle(reentrantLock);
            }
        });

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                lockTestHandle(reentrantLock);
            }
        });

        thread0.start();
        thread1.start();

        while (thread0.isAlive() || thread1.isAlive()) {}
    }

    private void lockTestHandle(ReentrantLock reentrantLock) {
        try {

            //  加鎖
            reentrantLock.lock();
            LOGGER.info("拿到鎖了,持有鎖5s");
            Thread.sleep(5000);

        } catch (Exception e) {
            // TODO: 2018/8/30 do something
        } finally {
            // 記得本身釋放鎖,否則形成死鎖了
            reentrantLock.unlock();
            LOGGER.info("釋放鎖了");
        }
    }



}

運行結果:咱們能夠看到,循環的代碼是連續的,沒有被其餘線程干擾。確實是鎖上了,使用同一個鎖,必須等一個釋放了另外一個才能持有。一個線程持有鎖,其餘使用同一把鎖的線程就會同步阻塞,從新持有鎖以後纔會結束阻塞的狀態,才能往下執行代碼。

16:36:05.740 [Thread-0] INFO com.example.demo.LockDemo - 拿到鎖了
16:36:05.744 [Thread-0] INFO com.example.demo.LockDemo - 循環:0 持有鎖
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循環:1 持有鎖
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循環:2 持有鎖
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循環:3 持有鎖
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 循環:4 持有鎖
16:36:05.746 [Thread-0] INFO com.example.demo.LockDemo - 釋放鎖了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到鎖了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循環:0 持有鎖
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循環:1 持有鎖
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循環:2 持有鎖
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循環:3 持有鎖
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循環:4 持有鎖
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 拿到鎖了
16:36:05.746 [Thread-1] INFO com.example.demo.LockDemo - 循環:0 持有鎖
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循環:1 持有鎖
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循環:2 持有鎖
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循環:3 持有鎖
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 循環:4 持有鎖
16:36:05.747 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了
16:36:05.747 [Thread-0] INFO com.example.demo.LockDemo - 拿到鎖了

......
16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 循環:4 持有鎖
16:36:05.748 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 拿到鎖了
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循環:0 持有鎖
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循環:1 持有鎖
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循環:2 持有鎖
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循環:3 持有鎖
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 循環:4 持有鎖
16:36:05.748 [Thread-0] INFO com.example.demo.LockDemo - 釋放鎖了
  • DEMO:可被中斷鎖
/**
     * lockInterruptibly:加了可中斷鎖的線程,若是在獲取不到鎖,可被中斷。
     * <p>
     * 中斷實際上是使用了異常機制,當調用中斷方法,會拋出InterruptedException異常,捕獲它可處理中斷邏輯
     */
    @Test
    public void lockInterruptiblyTest() throws InterruptedException {

        ReentrantLock reentrantLock = new ReentrantLock();

        Thread thread0 = new Thread(() -> {

            try {
                lockInterruptiblyTestHandle(reentrantLock);
            } catch (InterruptedException e) {
                LOGGER.info("被中斷了");
            }

        });

        Thread thread1 = new Thread(() -> {

            try {
                lockInterruptiblyTestHandle(reentrantLock);
            } catch (InterruptedException e) {
                LOGGER.info("被中斷了");
            }

        });
        thread1.setPriority(10);

        thread1.start();
        thread0.start();

        Thread.sleep(500);
        thread0.interrupt();

        while (thread0.isAlive() || thread1.isAlive()) {}
    }

    private void lockInterruptiblyTestHandle(ReentrantLock reentrantLock) throws InterruptedException {
        /*
         * 加鎖不能放在try...finally塊裏面,會出現IllegalMonitorStateException,意思是當lockInterruptibly()異常的時候,執行了unlock()方法
         * 其實就是加鎖都拋出異常失敗了,你還去解鎖時不行的。放外面拋出異常的時候就不會去解鎖了
         */
        reentrantLock.lockInterruptibly();
        try {
            LOGGER.info("拿到鎖了,持有鎖5秒");
            Thread.sleep(5000);
        } finally {
            // 釋放鎖
            reentrantLock.unlock();
            LOGGER.info("釋放鎖了");
        }
    }

從結果能夠看到,thread-0被中斷了以後再也不繼續執行

20:11:22.227 [Thread-1] INFO com.example.demo.LockDemo - 拿到鎖了,持有鎖5秒
20:11:22.742 [Thread-0] INFO com.example.demo.LockDemo - 被中斷了
20:11:27.231 [Thread-1] INFO com.example.demo.LockDemo - 釋放鎖了

Process finished with exit code 0

5.2.2 ReadWriteLock

ReadWriteLock只是定義了讀鎖和寫鎖兩個方法,其具體實現和拓展再默認實現ReentrantReadWriteLock中。簡單來講讀寫鎖呢,提供讀鎖和寫鎖,將讀和寫要獲取的鎖類型分開,用一個對列來管理,全部的鎖都會通過隊列。當須要獲取寫鎖的時候,後買的讀寫鎖獲取都須要等待,知道該寫鎖被釋放才能進行。

@Test
    public void readWriteLockTest(){

        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        try {
            readEvent();
        } catch (Exception e) {
            LOGGER.error(e.getMessage(),e);
        }finally {
            readLock.unlock();
        }

        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        try {
            writeEvent();
        } catch (Exception e) {
            LOGGER.error(e.getMessage(),e);
        }finally {
            writeLock.unlock();
        }


    }

    private void writeEvent() {
        // TODO: 2018/9/3 done write event
    }

    private void readEvent() {

        // TODO: 2018/9/3 done read event

    }

總的來講:凡是遇到寫,阻塞後面的線程隊列,讀與讀是不阻塞的。

5.三、 volatile

volatile可修飾成員變量,能保證變量的可見性,可是不能保證原子性,也就是說併發的時候多個線程對變量進行計算的話,結果是會出錯的,保證可見性只是能保證每一個線程拿到的東西是最新的。

對於volatile來講,保證線程共享區域內容的可見性能夠這麼來理解,堆內存的數據原來是須要拷貝到棧內存的,至關於複製一份過去,可是呢。再不加volatile的時候,棧區計算完以後在賦值給堆區,問題就產生了。加了volatile以後,線程訪問堆區的數據以後,堆區必須等待,知道棧區計算完畢將結果返回給堆區以後,其餘線程才能繼續訪問堆區數據。

public volatile String name = "Jorgezhong";
相關文章
相關標籤/搜索