Java高併發:多線程概覽

Thread 和 Runnable的區別

  • 可以避免由於Java的單繼承特性而帶來的侷限
  • 增強程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的
  • 適合多個相同程序代碼的線程區處理同一資源的情況
public class RunnableDemo{
    public static void main(String[] args){
        MyThread my = new MyThread();
        new Thread(my).start();
        new Thread(my).start();
        new Thread(my).start();
    }
    class MyThread implements Runnable{
        private int ticket = 5;
        public void run(){
            for (int i=0;i<10;i++)
            {
                if(ticket > 0){
                    System.out.println("ticket = " + ticket--);
                }
            }
        }
    }
}
  • ticket輸出的順序並不是54321,這是因爲線程執行的時機難以預測,ticket–並不是原子操作。
  • 我們new了3個Thread對象,但只有一個Runnable對象,3個Thread對象共享這個Runnable對象中的代碼,因此,便會出現3個線程共同完成賣票任務的結果。如果我們new出3個Runnable對象,作爲參數分別傳入3個Thread對象中,那麼3個線程便會獨立執行各自Runnable對象中的代碼,即3個線程各自賣5張票。
  • 由於3個Thread對象共同執行一個Runnable對象中的代碼,因此可能會造成線程的不安全,比如可能ticket會輸出-1,這種情況的出現是由於,一個線程在判斷ticket爲1>0後,還沒有來得及減1,另一個線程已經將ticket減1,變爲了0,那麼接下來之前的線程再將ticket減1,便得到了-1。這就需要加入同步操作(即互斥鎖),確保同一時刻只有一個線程在執行每次for循環中的操作。

多線程概覽

線程的狀態流轉圖
這裏寫圖片描述

小記:

  • java線程的優先級範圍:0~10,值越大優先級越高,默認5
  • 可運行狀態的線程還需要獲得CPU的時間片後才能運行
  • ThreadLocal:每個線程有一個localValue存儲ThreadLocal=>Object鍵值對,ThreadLocal.put數據的時候把自身作爲key與value保存到localValue,get的時候在從localValue裏取出

多線程和進程

  • 線程依附於進程,每個進程可以有多個線程,每個線程只屬於一個進程
  • 進程擁有獨立內存單元,互不干擾;一個進程裏的多個線程共享內存
  • 進程是資源分配的基本單位,線程是處理調度的基本單位

Thread和Runnable

線程池

  • newCachedThreadPool 無限線程 空閒回收 不夠則創建
  • newFixedThreadPool 控制最大併發數 有隊列
  • newScheduledThreadPool 控制最大併發數 有隊列 有周期性
  • newSingleThreadExecutor 只有一個線程
  • shutdown:線程池置爲關閉狀態並intercept等待的線程;shutdownNow線程池置爲關閉狀態並intercept所有線程

線程常用操作

線程的暫停、恢復和停止的實踐

class TestRunnable implements Runnable
{
    private boolean isPause = false;
    private boolean isStop = false;
    public void run() {
        while(true){
            if(isStop){
                return ;
            }
            while(isPause){
                sleep(200);
            }
            //doSomething
        }
    }
}

經典消費者生產者實例
這裏寫圖片描述

Queue<Integer> buffer = new LinkedList<>();
int maxSize = 10;
Thread producer = new Producer(buffer, maxSize, "PRODUCER");
Thread consumer = new Consumer(buffer, maxSize, "CONSUMER");
producer.start(); consumer.start();

守護線程 和 線程阻塞

Java中有兩類線程:User Thread 用戶線程、Daemon Thread 守護線程,用戶線程即運行在前臺的線程,而守護線程是運行在後臺的線程。只有前臺線程都結束後守護線程纔會退出。

普通線程可在start之前可setDaemon設置爲守護線程,守護線程中產生的線程也是守護線程。

線程可以阻塞於四種狀態:

  • 當線程執行Thread.sleep()時,它一直阻塞到指定的毫秒時間之後,或者阻塞被另一個線程打斷
  • 當線程碰到一條wait()語句時,它會一直阻塞到接到通知(notify())、被中斷或經過了指定毫秒時間爲止(若制定了超時值的話)
  • 線程阻塞與不同I/O的方式有多種。常見的一種方式是InputStream的read()方法,該方法一直阻塞到從流中讀取一個字節的數據爲止,它可以無限阻塞,因此不能指定超時時間
  • 線程也可以阻塞等待獲取某個對象鎖的排他性訪問權限(即等待獲得synchronized語句必須的鎖時阻塞)

    注意,並非所有的阻塞狀態都是可中斷的,以上阻塞狀態的前兩種可以被中斷,後兩種不會對中斷做出反應

https://blog.csdn.net/ns_code/article/details/17099981

同步鎖 volatile synchronized

volatile
Volatile修飾的成員變量在每次被線程訪問時,都強迫從共享內存中重讀該成員變量的值。而且,當成員變量發生變化時,強迫線程將變化值回寫到共享內存。這樣在任何時刻,兩個不同的線程總是看到某個成員變量的同一個值。
volatile是一種稍弱的同步機制,在訪問volatile變量時不會執行加鎖操作,也就不會執行線程阻塞,因此volatilei變量是一種比synchronized關鍵字更輕量級的同步機制

聲明爲volatile的簡單變量如果當前值與該變量以前的值相關,那麼volatile關鍵字不起作用,也就是說如下的表達式都不是原子操作:“count++”、“count = count+1”。

synchronized
每個對象都有一個monitor(鎖標記),當線程擁有這個鎖標記時才能訪問這個資源,沒有鎖標記便進入鎖池。任何一個對象系統都會爲其創建一個互斥鎖,這個鎖是爲了分配給線程的,防止打斷原子操作。每個對象的鎖只能分配給一個線程,因此叫做互斥鎖。

  • 一個方法中synchronized鎖住的部分不會影響其他沒有被synchronized的部分
  • synchronized 修飾方法,鎖住的是類實例對象
    • synchronized關鍵字不能繼承
    • 接口方法和構造函數不能使用synchronized關鍵字
    • 同一個類中多個方法用synchronized修飾,則其中一個方法被調用鎖住時,其他方法也會被鎖住無法被其他線程執行,直到鎖被釋放
  • synchronized(XXX.class) 和 synchronized修飾靜態方法,鎖住的是類對象
    • 同一個類中多個代碼塊被synchronized(XXX.class)修飾則其中一個代碼塊被調用執行,其他幾個同步代碼塊也將被鎖,直到鎖被釋放
    • synchronized修飾靜態方法和synchronized(XXX.class) ,鎖住都是類對象,固二者會互相影響
    • synchronized(XXX.class)和synch(this)互不影響,因爲鎖的對象不同,前者是類對象,後者類實例對象
  • synchronized 修飾方法 和 synchronized(this)代碼塊,鎖住的都是類實例對象,二者互相影響
  • synchronized(obj) 修飾代碼塊,鎖住指定對象
    • 同一個類中如果obj不同,則鎖住的對象也不同,自然不會互相阻塞。
Object obj1 = new Object();
Object obj2 = new Object();
public void m1(){
    synchronized(obj1){
        //todo
    }
}
public void m2(){
    synchronized(obj2){
        //todo
    }
}

m1被A線程執行的時候不會阻塞B線程執行m2

  • synchronized(obj)並不影響其他線程對obj對象的使用,其他線程使用obj對象也不會阻塞

volatile與synchronized:volatile只修飾變量且不保證原子性(取值的時候會從CPU工作內存中取出刷新共享內存得到最新值)。volatile①保證此變量對所有線程的可見性②禁止指令重排序。synchronized 則可以使用在變量、方法、和類級別的,也禁止指令重排序。volatile性能比synchronized好,volatile比較適用於多線程對變量高併發的讀操作

如果同一個方法內同時有兩個或更多線程,則每個線程有自己的局部變量拷貝

重排序 happen-before 內存屏障

重排序是編譯器和處理器爲了優化性能而對指令執行的順序進行重排序。大多數現代處理器都會採用將指令亂序執行的方法,在條件允許的情況下,直接運行當前有能力立即執行的後續指令,避開獲取下一條指令所需數據時造成的等待。通過亂序執行的技術,處理器可以大大提高執行效率。

也就是說程序的執行,並不是嚴格按照程序語句編寫的順序執行,在運行期間可能是被打亂的。

重排序發生的位置:編譯器重排序、指令級並行重排序、內存系統重排序

as-if-serial
重排序下如何保證程序的正常執行呢?
原因就是Java遵循as-if-serial語義。它保證,在單線程執行程序時,即使發生重排序,程序的執行結果不能被改變。

簡單來說,重排序是爲了優化性能,然後通過sa-if-serial語義來保證重排序後程序執行結果不被改變。

happens-before規則

  • 程序次序規則: 線程中每個動作A都happens-before於該線程中的每一個動作B。那麼在程序中,所有的動作B都能出現在A之後。
  • 監視器鎖法則: 對一個監視器的解鎖happens-before於每個後續對同一監視器鎖的加鎖
  • volatile變量法則:對volatile域的寫入操作happens-before於每一個後續對同一個域的讀寫操作
  • 線程啓動法則: 在一個線程中,對於Thread.start的調用會happens-before於每個啓動線程的動作。
  • 線程終結法則: 線程中的任何動作都happens-before於其他線程檢測到這個線程已經終結。
  • 中斷法則: 一個線程調用另一個線程的interrupt happens-before於被中斷的線程發現中斷。
  • 終結法則: 一個對象的構造函數的結束happens-before於這個對象finalizer的開始。
  • 傳遞性: 如果A happens-before於B,且B happens-before於C,則A happens-before於C

PS:volatile和synchronized都可以禁止指令的重排序

內存屏障
內存屏障(Memory Barrier,或有時叫做內存柵欄,Memory Fence)是一種CPU指令,用於控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。

  • LoadLoad屏障:對於這樣的語句Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
  • StoreStore屏障:對於這樣的語句Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
  • LoadStore屏障:對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
  • StoreLoad屏障:對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。

死鎖

當線程需要同時持有多個鎖時,有可能產生死鎖

可重入鎖

A線程進入同步代碼時獲得鎖,且計數值爲1,其他線程無法獲得該鎖,但A線程還可以獲得該鎖(即重入鎖),並且計數值再次+1,數值爲2.退出後計數值-1,直到計數值爲0,其他線程方可獲取
https://blog.csdn.net/ns_code/article/details/17014135

public class Father
{
    public synchronized void doSomething(){
        ......
    }
}

public class Child extends Father
{
    public synchronized void doSomething(){
        ......
        super.doSomething();
    }
}

如果沒有重入鎖機制,這段代碼將產生死鎖。

新特性

Executor框架與線程池
Lock鎖
阻塞隊列和阻塞棧
障礙器CyclicBarrier
信號量Semaphore


【Java併發編程】併發編程大合集

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