多線程核心技術(1)-線程的基本方法

多線程核心技術(1)-線程的基本方法

進程和線程

瞭解多線程首先要了解進程和線程的概念,在操做系統裏,進程是資源分配最小單位,通常狀況下一個應用就會在計算機系統內開啓一個進程,線程能夠理解爲進程中多個獨立運行的子任務,是操做系統可以進行調度運算的最小單位,可是線程不擁有資源,只能共享進程中的數據,因此多個線程對進程中某個數據同時進行修改時,就會產生線程安全問題。因爲一個進程中容許存在多個線程,因此在多線程中,如何處理線程併發和線程之間通訊的問題,是學習多線程編程的重點。
複製代碼

多線程的使用

​ 在java中,建立一個線程通常有兩種方式,繼承Thread類或者實現Runable接口,重寫run方法便可,而後調用start()方法便可以開啓一個線程並執行。若是想要獲取當前線程執行返回值,在jdk1.5之後,能夠經過實現Callable接口,而後藉助FutureTask或者線程池獲得返回值。因爲線程的執行具備隨機性,因此線程的開啓順序並不意味線程的執行順序。java

一、繼承Thread建立一個線程
/** * 繼承Thread 重寫Run方法 * 類是單繼承的,生產環境中若是此類無需實現其餘接口 可以使用這種方法建立線程 * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        log.info("Hi,I am a thread extends Thread,My name is:{}", this.getName());
    }
}

複製代碼
二、實現Runable接口建立一個線程
/** * 實現Runnable接口 * 類容許有多個接口實現 生產中通常使用這種方式建立線程 * 線程的開啓還須要藉助於Thread實現 * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
@Getter
public class ThreadRunable implements Runnable {

    private String name;

    public ThreadRunable(String name) {
        this.name = name;
    }

    public void run() {
        log.info("Hi,I am a thread implements Runnable,My name is:{}", this.getName());
    }
}
複製代碼
三、實現Callable 接口 獲取線程執行結果
/** * 實現Callable接口建立獲取具備返回值的線程 * 線程使用須要藉助FutureTask和Thread,或者使用線程池 * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
public class CallableThread implements Callable<Integer> {

    private AtomicInteger seed;
    @Getter
    private String name;

    public CallableThread(String name, AtomicInteger seed) {
        this.name = name;
        this.seed = seed;
    }

    public Integer call() throws Exception {
        //使用併發安全的原子類生成一個整數
        Integer value = seed.getAndIncrement();
        log.info("I am thread implements Callable,my name is:{} my value is:{}", this.name, value);
        return value;
    }
}
複製代碼
四、驗證三種線程的啓動
/** * User: lijinpeng * Created by Shanghai on 2019/4/13. */
@Slf4j
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     threadTest();
     runableTest();
     callableTest();
    }

    public static void threadTest() {
        MyThread threadA = new MyThread("threadA");
        threadA.start();
    }

    public static void runableTest() {
        ThreadRunable runable = new ThreadRunable("threadB");
        //須要藉助Thread來開啓一個新的線程
        Thread threadB = new Thread(runable);
        threadB.start();
    }

    public static void callableTest() throws ExecutionException, InterruptedException {
        AtomicInteger atomic = new AtomicInteger();
        CallableThread threadC1 = new CallableThread("threadC1", atomic);
        CallableThread threadC2 = new CallableThread("threadC2", atomic);
        CallableThread threadC3 = new CallableThread("threadC3", atomic);
        FutureTask<Integer> task1 = new FutureTask<Integer>(threadC1);
        FutureTask<Integer> task2 = new FutureTask<Integer>(threadC2);
        FutureTask<Integer> task3 = new FutureTask<Integer>(threadC3);
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3);
        thread1.start();
        thread2.start();
        thread3.start();
        while (task1.isDone()&&task2.isDone()&&task3.isDone())
        {
        }
        log.info(threadC1.getName()+"執行結果:"+String.valueOf(task1.get()));
        log.info(threadC2.getName()+"執行結果:"+String.valueOf(task2.get()));
        log.info(threadC2.getName()+"執行結果:"+String.valueOf(task3.get()));
    }
}
複製代碼

如下是程序執行結果:編程

結論:安全

  1. 這三種方式均可以開啓一個線程,實現具體開啓線程的任務仍是交給Thread類實現,所以對於Runable和Callable來講,最後都是要藉助於Thread開啓線程,類是單繼承的,接口是多實現的,因爲生產環境業務複雜性,一個類可能會有其餘功能,所以通常使用接口實現的方式。
  2. 從上面的線程聲明順序和執行順序結果來看,線程的執行是無序的,CPU執行任務是採用輪詢機制來提升CPU使用率,在線程獲取執行資源進行就緒隊列後 纔會再次被CPU調用,而這個過程跟程序無關。

線程的生命週期

​ 一個線程的運行一般伴隨着線程的啓動、阻塞、中止等過程,線程啓動能夠經過Thread類的start()方法執行,因爲多線程可能會共享進程數據,阻塞通常發生在等待其餘線程釋放進程某塊資源的過程,當線程執行完畢,能夠自動中止,也能夠經過調用stop()強制終止線程,或者在線程執行過程當中因爲異常致使線程終止,瞭解線程的生命週期是學習多線程最重要的理論基礎。bash

​ 下圖爲線程的生命週期以及狀態轉換過程markdown

新建狀態

當經過Thread thead=new Thread()建立一個線程的時候,該線程就處於 new 狀態,也叫新建狀態。多線程

就緒狀態

當調用thread.start()時,線程就進入了就緒狀態,在該狀態下線程並不會運行,只是表示線程進入可供CPU調用的就緒隊列,具有運行條件。併發

運行狀態

當線程得到了JVM中線程調度器的調度時候,線程就進入運行狀態,會執行重寫的 run方法。ide

阻塞狀態

此時的線程仍處於活動狀態,可是因爲某種緣由失去了CPU對其調度權利,具體緣由可分爲如下幾種函數

  1. 同步阻塞學習

    此時因爲線程A須要獲取進程的資源1,可是資源1被線程B所持有,必須等待線程B釋放資源1以後,該線程纔會進入資源1的就緒線程池裏,獲取到資源1後,等待被CPU調度器調度再次運行。同步阻塞通常出如今線程等待某項資源的使用權利,在程序中使用鎖機制會產生同步阻塞。
    複製代碼
  2. 等待阻塞

    當執行Thread類的wait() 和join()方法時,會形成當前線程的同步阻塞,wait()會使當前線程暫停運行,而且釋放所擁有的鎖,能夠通該線程要等待的某個類(Object)的notify()或者notifyall()方法喚醒當前線程。join()方法會阻塞當前線程,直到線程執行完畢,能夠經過join(time)指定等待的時間,而後喚醒線程。
    複製代碼
  3. 其餘阻塞

    調用sleep()方法主動放棄所佔用的CPU資源,這種方式不會釋放該線程所擁有的鎖,或者調用一個阻塞式IO方法、發出了I/O請求,進入這種阻塞狀態。被阻塞的線程會在合適的時候(阻塞解除後)從新進入就緒狀態,從新等待線程調度器再次調度它。
    複製代碼
死亡狀態

​ 當線程執行完run方法時,就會自動終止或者處於死亡狀態,這是線程的正常死亡過程。或者經過顯示調用stop()終止線程,但不安全。還能夠經過拋異常法終止線程。

實例變量與線程安全

多線程訪問進程資源

​ 在多線程任務應用中若是多個線程執行之間使用了進程的不一樣資源,即運行中不共享任何進程資源,各線程運行不受影響,且不會產生數據安全問題。若是多個線程共享了進程的某塊資源,會同時修改該塊資源數據,產生最終結果與預期結果不一致的狀況,致使線程安全問題。如圖:

主內存與工做內存

​ Java內存模型分爲主內存,和工做內存。主內存是全部的線程所共享的,工做內存是每一個線程本身有一個,不是共享的。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝。線程對變量的全部操做(讀取、賦值),都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成,線程、主內存、工做內存三者之間的交互關係以下圖:

線程對主存的操做指令:lock,unlock,read,load,use,assign,store,write操做

  • read-load階段從主內存複製變量到當前工做內存

  • use和assign階段執行代碼改變共享變量值

  • store和write階段用工做內存數據刷新主存對應變量的值。

  • store and write執行時機

    一、java內存模型規定不容許read和load、store和write操做之一單獨出現,以上兩個操做必須按順序執行,不必    連續執行,也就是說read與load之間、store與write之間是可插入其餘指令的。
    二、不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。變量在當前線程中改變一次其實就是一次assign,並且不容許丟棄最近的assign,因此一定有一次store and write,又根據第一條read and load 和store and write 不能單一出現,因此有一次store and write 一定有一次 read and load,所以推斷出,變量在當前線程中每一次變化都會執行 read 、 load 、use 、assign、store、write
    三、volatile修飾變量,是在use和assign階段保證獲取到的變量永遠是跟主內存變量保持同步
    複製代碼
    非線程安全問題

    ​ 在多線程環境下use和assign是屢次出現的,但此操做並非原子性的,也就是說在線程A執行了read和load從主內存 加載過變量C後,此時若是B線程修改了主內存中變量C的值,因爲線程A已經加載過變量C,沒法感知數據已經發生變化,即從線程A的角度來看,工做內存和主內存的變量A已經再也不同步,當線程A使用use和assign時,就會出現非線程安全的問題。解決此問題能夠經過使用volatile關鍵字修飾,volatile能夠保證線程每次使用use和assign時,都從主內存中拿到最新的數據,並且能夠防止指令重排,但volatile僅僅是保證變量的可見性,沒法使數據加載的幾個步驟是原子操做,因此volatile並不能保證線程安全。

    以下代碼所示:

    多個業務線程訪問用戶餘額balance,最終致使扣款總金額超過了用戶餘額,由線程不安全致使的資損情景.並且每一個業務線程都扣款了兩次,也說明了線程啓動時須要將balance加載到工做內存中,以後該線程基於加載到的balance操做,其餘線程如何改變balance值,對當前業務線程來講都是不可見的。

    /** * 業務訂單代扣線程 持續扣費 * User: lijinpeng * Created by Shanghai on 2019/4/13. */
    @Slf4j
    public class WithHoldPayThread extends Thread {
        //繳費金額
        private Integer amt;
        //業務類型
        private String busiType;
    
        public WithHoldPayThread(Integer amt, String busiType) {
            this.amt = amt;
            this.busiType = busiType;
        }
    
        @Override
        public void run() {
            int payTime = 0;
            while (WithHodeTest.balance > 0) {
                synchronized (WithHodeTest.balance) {
                    boolean result = false;
                    if (WithHodeTest.balance >= amt) {
                        WithHodeTest.balance -= amt;
                        result = true;
                        payTime++;
                    }
                    log.info("業務:{} 扣款金額:{} 扣款狀態:{}", busiType, amt,result);
                }
            }
            log.info("業務:{} 共繳費:{} 次", busiType, payTime);
        }
    }
    複製代碼

    測試函數

    /** * User: lijinpeng * Created by Shanghai on 2019/4/13. */
    public class WithHodeTest {
        //用戶餘額 單位 分
        public static volatile Integer balance=100;
    
        public static void main(String[] args) {
            WithHoldPayThread phoneFare = new WithHoldPayThread(50, "繳存話費");
            WithHoldPayThread waterFare = new WithHoldPayThread(50, "繳存水費");
            WithHoldPayThread electricFare = new WithHoldPayThread(50, "繳存電費");
            phoneFare.start();
            waterFare.start();
            electricFare.start();
        }
    }
    複製代碼

    執行結果:

實驗結果證實,每一個線程的扣款都成功了,這就致使了線程安全問題,解決這個問題最簡單的作法是在run方法裏面加synchronized修飾,而且對balance使用volatile修飾就能夠了。

//用戶餘額 單位 分
    public static  volatile Integer balance=100;
複製代碼
@Override
    public void run() {
        int payTime = 0;
        while (WithHodeTest.balance > 0) {
            synchronized (WithHodeTest.balance) {
                boolean result = false;
                if (WithHodeTest.balance >= amt) {
                    WithHodeTest.balance -= amt;
                    result = true;
                    payTime++;
                }
                log.info("業務:{} 扣款金額:{} 扣款狀態:{}", busiType, amt,result);
            }
        }
        log.info("業務:{} 共繳費:{} 次", busiType, payTime);
    }
複製代碼

執行結果:

線程的基本API

線程的中止

相關文章
相關標籤/搜索