線程間協作和通信

線程操作的定義

1、如果一個程序沒有數據競爭,那麼程序的所有執行看起來都是順序一致的,各自按照線程內語義執行即可,JMM對此不需要有額外的描述了。
2、線程間操作指:一個程序執行的操作可被其他線程感知或被其他線程直接影響。

線程間操作:

1、write 要寫的變量以及要寫的值。
2、read 要讀的變量以及可見的寫入值(由此,我們可以確定可見的值)。
3、lock 要鎖定的管程(監視器monitor)。
4、unlock 要解鎖的管程。
5、外部操作(socket等等…)
6、啓動和終止

注意: 所有線程間操作,都存在可見性問題,JMM需要對其進行規範。

對於同步的規則定義

  1. 對於監視器 m 的解鎖與所有後續操作對於 m 的加鎖同步
  2. 對 volatile 變量 v 的寫入,與所有其他線程後續對 v 的讀同步
  3. 對於每個屬性寫入默認值(0, false,null)與每個線程對其進行的操作同步
  4. 啓動線程的操作與線程中的第一個操作同步
  5. 線程 T12的最後操作與線程 T1 發現線程 T2 已經結束同步。( isAlive ,join可以判斷線程是否終結)
  6. 如果線程 T1 中斷了 T2,那麼線程 T1 的中斷操作與其他所有線程發現 T2 被中斷了同步,通過拋出 InterruptedException 異常,或者調用 Thread.interrupted 或 Thread.isInterrupted

Happens-before先行發生原則

happens-before 關係用於描述兩個有衝突的動作之間的順序,如果一個action happends before 另一個action,則第一個操作被第二個操作可見 。

具體的虛擬機實現,有必要確保以下原則的成立:

  1. 某個線程中的每個動作都 happens-before 該線程中該動作後面的動作。
  2. 某個管程上的 unlock 動作 happens-before 同一個管程上後續的 lock 動作。
  3. 對某個 volatile 字段的寫操作 happens-before 每個後續對該 volatile 字段的讀操作。
  4. 在某個線程對象上調用 start()方法 happens-before 被啓動線程中的任意動作。
  5. 如果在線程t1中成功執行了t2.join(),則t2中的所有操作對t1可見。
  6. 如果某個動作 a happens-before 動作 b,且 b happens-before 動作 c,則有 a happens-before c.

補充: 當程序包含兩個沒有被 happens-before 關係排序的衝突訪問時,就稱存在數據競爭。遵守了這個原則,也就意味着有些代碼不能進行重排序,有些數據不能緩存!

final在JMM中的處理

1、final在該對象的構造函數中設置對象的字段,當線程看到該對象時,將始終看到該對象的final字段的正確構造版本。僞代碼示例:f = new finalDemo(); 讀取到的 f.x 一定最新,x爲final字段。

public class Demo2Final {

    final int x;
    int y;

    static Demo2Final f;

    public Demo2Final(){
        x = 3;
        y = 4;
    }

    static void writer(){
        f = new Demo2Final();
    }

    static void reader(){
        if (f!=null){
            int i = f.x;        //一定讀到正確構造版本
            int j = f.y;        //可能會讀到 默認值0
            System.out.println("i=" + i + ", j=" +j);
        }
    }

}

在多線程中,調用reader方法,f.x一定能讀到在構造函數中的正確賦值,但是f.y卻不一定。

2、如果在構造函數中設置字段後發生讀取,則會看到該final字段分配的值,否則它將看到默認值;僞代碼示例:public finalDemo(){ x = 1; y = x; }; y會等於1;

public class Demo3Final {
    final int x;
    int y;

    static Demo2Final f;

    public Demo3Final(){
        x = 3;
        //#### 重點 語句 #####
        y = x;      //因爲x被final修飾了,所以可讀到y的正確構造版本
    }

    static void writer(){
        f = new Demo2Final();
    }

    static void reader(){
        if (f!=null){
            int i = f.x;        //一定讀到正確構造版本
            int j = f.y;        //也能讀到正確構造版本
            System.out.println("i=" + i + ", j=" +j);
        }
    }
}

在多線程中,reader方法都能讀到x和y的正確值,因爲x被final修飾了,所以可讀到y的正確構造版本。

3、讀取該共享對象的final成員變量之前,先要讀取共享對象。僞代碼示例: r = new ReferenceObj(); k = r.f ; 這兩個操作不能重排序。

4、通常static final是不可以修改的字段 。然而System.in,System.out和System.err是static final字段,遺留原因,必須允許通過set方法改變,我們將這些字段稱爲寫保護,以區別於普通final字段;

Word Tearing字節處理

Java虛擬機實現的一個考慮因素是,每個字段和數組元素被認爲是不同的;對一個字段或元素的更新,不得與任何其他字段或元素的讀取或更新交互。特別是,分別更新字段數組的相鄰元素的兩個線程不得干擾或交互。

有些處理器(尤其是早期的 Alphas 處理器)沒有提供寫單個字節的功能。在這樣的處理器上更新 byte 數組,若只是簡單地讀取整個內容,更新對應的字節,然後將整個內容再寫回內存,將是不合法的。

這個問題有時候被稱爲“字分裂(word tearing)”,更新單個字節有難度的處理器,就需要尋求其它方式來解決問題。

因此,編程人員需要注意,儘量不要對byte[]中的元素進行重新賦值,更不要在多線程程序中這樣做。

volatile關鍵字

多個線程同時訪問一個共享的變量的時候,每個線程的工作內存有這個變量的一個拷貝,變量本身還是保存在共享內存中。

Violate修飾的字段,對這個變量的訪問必須要從共享內存刷新一次。最新的修改寫回共享內存。可以保證字段的可見性。
注意:
Violate修飾的字段絕對不是線程安全的,沒有操作的原子性。
適用場景:

  1. 一個線程寫,多個線程讀;
  2. volatile變量的變化很固定

示例代碼:

public class VolatileThread implements Runnable {

    private volatile  int a= 0;

    @Override
    public void run() {
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
    }
}
public class VolatileTest {
    public static void main(String[] args) {
        VolatileThread volatileThread = new VolatileThread();

        Thread t1 = new Thread(volatileThread);
        Thread t2 = new Thread(volatileThread);
        Thread t3 = new Thread(volatileThread);
        Thread t4 = new Thread(volatileThread);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

輸出結果:

Thread-2----3
Thread-3----4
Thread-1----3
Thread-0----3
Thread-2----5
Thread-3----8
Thread-0----8
Thread-1----8

可以看到變量a雖然用volatile修飾,是線程共享的變量,但是並不是線程安全的,輸出結果是不可預測的。

理解volatile特性的一個好方法是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。
在這裏插入圖片描述
假設有多個線程分別調用上面程序的3個方法,這個程序在語義上和下面程序等價。
在這裏插入圖片描述
volatile寫的內存語義如下:
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
volatile讀的內存語義如下:
當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

synchronized關鍵字

可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性,又稱爲內置鎖機制。
例如在上面的VolatileThread示例代碼中,在run方法裏面加上synchronized鎖,那麼就可以保證運行結果,修改後的代碼爲:

public class VolatileThread implements Runnable {

    private volatile  int a= 0;

    @Override
    public void run() {
        synchronized (this){
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);

        }
    }
}

VolatileTest運行結果:

Thread-0----1
Thread-0----2
Thread-3----3
Thread-3----4
Thread-2----5
Thread-2----6
Thread-1----7
Thread-1----8

所以可以知道synchronized鎖住的代碼,同一時間只能有一個線程運行。

Synchronized的類鎖和對象鎖,本質上是兩把鎖,Synchronized加在靜態方法上就是類鎖,加在非靜態的上面就是對象鎖,類鎖實際鎖的是每一個類的class對象。對象鎖鎖的是當前對象實例。

示例代碼:

package com.dongnaoedu.syn;

import com.dongnaoedu.threadstate.SleepUtils;

/**
 * 動腦學院-Mark老師
 * 創建日期:2017/11/28
 * 創建時間: 20:45
 * 類鎖和實例鎖
 */
public class InstanceAndClass {

    //測試類鎖
    private static class TestClassSyn extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass is going...");
            synClass();
        }
    }

    //測試類鎖
    private static class TestClassSyn2 extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass2 is going...");
            synClass2();
        }
    }

    //測試對象鎖
    private static class TestInstanceSyn extends Thread{
        private InstanceAndClass instanceAndClass;

        public TestInstanceSyn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }

        @Override
        public void run() {
            System.out.println("TestInstance is going..."+instanceAndClass);
            instanceAndClass.synInstance();
        }

    }

    //測試對象鎖
    private static class TestInstance2Syn implements Runnable{
        private InstanceAndClass instanceAndClass;

        public TestInstance2Syn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is going..."+instanceAndClass);
            instanceAndClass.synInstance2();
        }
    }

    //鎖對象的方法
    private synchronized void synInstance(){
        SleepUtils.second(3);
        System.out.println("synInstance is going...");
        SleepUtils.second(3);
        System.out.println("synInstance ended");
    }

    //鎖對象的方法
    private synchronized void synInstance2(){
        SleepUtils.second(3);
        System.out.println("synInstance2 going...");
        SleepUtils.second(3);
        System.out.println("synInstance2 ended");
    }

    //鎖類的方法
    private static synchronized void synClass(){
        SleepUtils.second(5);
        System.out.println("synClass going...");
        SleepUtils.second(5);
    }

    //鎖類的方法
    private static synchronized void synClass2(){
        SleepUtils.second(1);
        System.out.println("synClass2 going...");
        SleepUtils.second(1);
    }

    public static void main(String[] args) {
        InstanceAndClass instanceAndClass = new InstanceAndClass();
        Thread t1 = new TestClassSyn();
        Thread t4 = new TestClassSyn2();
        Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass));
        Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass));
        t2.start();
        t3.start();
        SleepUtils.second(1);
        t1.start();
        t4.start();
    }

}

輸出結果:

TestInstance is going...com.dongnaoedu.syn.InstanceAndClass@698b3761
TestInstance2 is going...com.dongnaoedu.syn.InstanceAndClass@698b3761
TestClass is going...
TestClass2 is going...
synInstance is going...
synInstance ended
synClass going...
synInstance2 going...
synInstance2 ended
synClass2 going...

從輸出結果可以看出,類鎖和對象鎖是兩個不同的鎖,並且當一個對象在運行一個加了該對象鎖的方法時,其他線程不能用該對象運行加了該對象鎖的其他方法。如上例代碼中,一個線程運行着synInstance方法,在synInstance運行結束之前,那麼其他線程就不能通過instanceAndClass對象調用synInstance2或者synInstance方法。

補充: 即使是同一個方法,加了對象鎖,如果不是同一個對象去調用,那麼是不會互斥的,因爲synchronized鎖的不是同一個對象,如下面,把main方法改成如下,那麼t2和t3是互不干擾的,不會互斥。
把TestInstance2Syn 在run方法中調用的方法改成synInstance,讓兩個線程都去調用synInstance方法。

private static class TestInstance2Syn implements Runnable{
        private InstanceAndClass instanceAndClass;

        public TestInstance2Syn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is going..."+instanceAndClass);
            instanceAndClass.synInstance();
        }
    }
    public static void main(String[] args) {
        InstanceAndClass instanceAndClass1 = new InstanceAndClass();
        InstanceAndClass instanceAndClass2 = new InstanceAndClass();
        Thread t1 = new TestClassSyn();
        Thread t4 = new TestClassSyn2();
        Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass1));
        Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass2));
        t2.start();
        t3.start();
        SleepUtils.second(1);
        t1.start();
        t4.start();
    }

運行結果:

TestInstance is going...com.dongnaoedu.syn.InstanceAndClass@64669643
TestInstance2 is going...com.dongnaoedu.syn.InstanceAndClass@7e437185
TestClass is going...
TestClass2 is going...
synInstance is going...
synInstance is going...
synClass going...
synInstance ended
synInstance ended
synClass2 going...

從結果上看,t2和t3是可以併發執行的。所以即使加了synchronized的對象方法,不同對象去調用是不會互斥的,是可以併發執行的。

等待和通知機制

等待方原則:
1、獲取對象鎖
2、如果條件不滿足,調用對象的wait方法,被通知後依然要檢查條件是否滿足
3、條件滿足以後,才能執行相關的業務邏輯
wait方法導致當前線程等待,加入該對象的等待集合中,並且放棄當前持有的對象鎖。
格式:

Synchronized(對象){
	While(條件不滿足){
		對象.wait()
	}
	業務邏輯處理
}

通知方原則:
1、獲得對象的鎖;
2、改變條件;
3、通知所有等待在對象的線程
notify/notifyAll方法喚醒一個或所有正在等待這個對象鎖的線程。
格式:

Synchronized(對象){
	業務邏輯處理,改變條件
	對象.notify/notifyAll
}

示例代碼:

public class BlockingQueueWN<T> {

    private List queue = new LinkedList<>();
    private final int limit;

    public BlockingQueueWN(int limit) {
        this.limit = limit;
    }

    //入隊
    public synchronized void enqueue(T item) throws InterruptedException {
        while(this.queue.size()==this.limit){
            wait();
        }
        //將數據入隊,可以肯定有出隊的線程正在等待
        if (this.queue.size()==0){
            notifyAll();
        }
        this.queue.add(item);
    }

    //出隊
    public synchronized T dequeue() throws InterruptedException {
        while(this.queue.size()==0){
            wait();
        }
        if (this.queue.size()==this.limit){
            notifyAll();
        }
        return (T)this.queue.remove(0);
    }
}

public class BqTest {
    public static void main(String[] args) {
        BlockingQueueWN bq = new BlockingQueueWN(10);
        Thread threadA = new ThreadPush(bq);
        threadA.setName("Push");
        Thread threadB = new ThreadPop(bq);
        threadB.setName("Pop");
        threadB.start();
        threadA.start();
    }

    //推數據入隊列
    private static class ThreadPush extends Thread{
        BlockingQueueWN<Integer> bq;

        public ThreadPush(BlockingQueueWN<Integer> bq) {
            this.bq = bq;
        }

        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            int i = 20;
            while(i>0){
                try {
                    Thread.sleep(1000);
                    System.out.println(" i="+i+" will push");
                    bq.enqueue(i--);
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }

            }
        }
    }

    //取數據出隊列
    private static class ThreadPop extends Thread{
        BlockingQueueWN<Integer> bq;

        public ThreadPop(BlockingQueueWN<Integer> bq) {
            this.bq = bq;
        }
        @Override
        public void run() {
            while(true){
                try {
                    System.out.println(Thread.currentThread().getName()
                            +" will pop.....");
                    Integer i = bq.dequeue();
                    System.out.println(" i="+i.intValue()+" alread pop");
                } catch (InterruptedException e) {
                    //e.printStackTrace();
                }
            }

        }
    }
}

注意:
1、雖然會wait自動解鎖,但是對順序有要求, 如果在notify被調用之後,纔開始wait方法的調用,線程會永遠處於WAITING狀態。
2、這些方法只能由同一對象鎖的持有者線程調用,也就是寫在同步塊裏面,否則會拋出IllegalMonitorStateException異常。

join方法

線程A,執行了thread.join(),線程A等待thread線程終止了以後,A在join後面的語句纔會繼續執行.
示例代碼:

public class JoinTest {

    static class CutInLine implements Runnable{

        private Thread thread;

        public CutInLine(Thread thread) {
            this.thread = thread;
        }

        @Override
        public void run() {
            try {
                //在被插隊的線程裏,調用一下插隊線程的join方法
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" will work");
        }
    }

    public static void main(String[] args) {
        Thread previous = Thread.currentThread();
        for(int i=0;i<10;i++){
            Thread thread =
                    new Thread(new CutInLine(previous),String.valueOf(i));
            System.out.println(previous.getId()+" cut in the thread:"+thread.getName());
            thread.start();
            previous = thread;
        }

    }

}

運行結果:

1 cut in the thread:0
12 cut in the thread:1
13 cut in the thread:2
14 cut in the thread:3
15 cut in the thread:4
16 cut in the thread:5
17 cut in the thread:6
18 cut in the thread:7
19 cut in the thread:8
20 cut in the thread:9
0 will work
1 will work
2 will work
3 will work
4 will work
5 will work
6 will work
7 will work
8 will work
9 will work

這裏在main方法啓動的10個線程中,每個線程都是得在上一個線程執行完成(終止)之後,纔會執行自己的輸出。

park/unpark機制

線程調用park則等待“許可”,處於等待狀態,unpark方法爲指定線程提供“許可(permit)”,線程繼續運行。
補充:調用unpark之後,再調用park,線程會直接運行。
提前調用的unpark不疊加,連續多次調用unpark後,第一次調用park後會拿到“許可”直接運行,後續調用會進入等待。
代碼示例:

package com.dongnao.concurrent.period2;

import java.util.concurrent.locks.LockSupport;

public class Demo9_ParkUnpark {

    public static void main(String args[]) throws Exception {
        Demo9_ParkUnpark demo = new Demo9_ParkUnpark();
        demo.test1_normal();
        //demo.test2_DeadLock();
    }

    public static Object iceCream = null;

    /** 正常的park/unpark */
    public void test1_normal() throws Exception {
        //開啓一個線程,代表小朋友
        Thread consumerThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (iceCream == null) {     // 若沒有冰激凌
                    System.out.println("沒有冰激凌,小朋友不開心,等待...");
                    LockSupport.park();
                }
                System.out.println("小朋友買到冰激凌,開心回家");
            }
        });
        consumerThread.start();

        Thread.sleep(3000L);    // 3秒之後
        iceCream = new Object();    //店員做了一個冰激凌

        LockSupport.unpark(consumerThread);     //通知小朋友

        System.out.println("通知小朋友");
    }

    /** 死鎖的park/unpark */
    public void test2_DeadLock() throws Exception {
        //開啓一個線程,代表小朋友
        Thread consumerThread = new Thread(new Runnable() {
            @Override
            public void run() {
                if (iceCream == null) {     // 若沒有冰激凌
                    System.out.println("沒有冰激凌,小朋友不開心,等待...");

                    synchronized (this) {   // 若拿到鎖
                        LockSupport.park();     //執行park
                    }
                }
                System.out.println("小朋友買到冰激凌,開心回家");
            }
        });
        consumerThread.start();

        Thread.sleep(3000L);    // 3秒之後
        iceCream = new Object();    //店員做了一個冰激凌

        synchronized (this) {   // 爭取到鎖以後,才能恢復consumerThread
            LockSupport.unpark(consumerThread);
        }
        System.out.println("通知小朋友");
    }
}

僞喚醒

注意:
之前代碼中用 if 語句來判斷,是否進入等待狀態,這樣的做法是錯誤的!

官方建議應該在循環中檢查等待條件,原因是處於等待狀態的線程可能會收到錯誤警報和僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。

僞喚醒是指線程並非因爲notify、notifyall、unpark等api調用而意外喚醒,是更底層原因導致的。

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