天天看點

Java多線程筆記(二)

文章目錄

  • ​​1. 并發,并行概念​​
  • ​​2. 程序,線程概念​​
  • ​​3. 類變量, 成員變量, 局部變量​​
  • ​​4. 線程安全​​
  • ​​5. 并發程式設計中,最重要的三個特性​​
  • ​​6. 線程狀态的轉換​​
  • ​​7. Java同步塊​​
  • ​​8. 線程通信​​
  • ​​9. Thread.join()​​
  • ​​[參考文獻]​​

1. 并發,并行概念

  • 并發(concurrent) : 一個時間段有多個程式都處于已啟動運作的運作完畢之間,且這幾個程式都是在同一個處理機上運作.
  • 并行(parallel) : 當一個系統有一個以上的CPU時,當一個CPU執行一個程序時, 另一個CPU可以執行另一個程序, 兩個程序互不搶占CPU資源, 可以同時進行.
通俗了解, 排隊取咖啡, 并發就是兩個隊伍一個咖啡機,交替取; 并行就是兩個隊伍兩個咖啡機, 同時取.(Joe Armstrong圖中意思)

2. 程序,線程概念

  • 程序 : 一個獨立的程式就是一個程序
  • 線程 : 對于音樂播放器, 其中播放聲音, 進度條, 歌詞的滾動… 就是多個線程
其中多個程序直接切換需要進行​

​上下文切換​

​需要消耗資源, 是以考慮一個程序中同時運作多個"子任務"(線程)

​​CPU上下文切換詳解​​

​​一文讓你明白CPU上下文切換​​

對于Java語言, Java語言程式是運作在JVM上面的, 每一個JVM就是一個程序. 其中JVM是一個完整的計算機的一個模型.

3. 類變量, 成員變量, 局部變量

  • 共享變量 : 類變量 , 成員變量
  • 非共享變量 : 局部變量

4. 線程安全

多個線程同時通路共享變量的時候, 得到的結果和我們預期的一樣, 就是線程安全.

允許被多個線程同時執行的代碼稱作線程安全的代碼.

5. 并發程式設計中,最重要的三個特性

  • 原子性

    一個操作是不可中斷的, 要麼全部執行成功要麼全部執行失敗. 即使在多個線程一起執行的時候, 一段代碼或者一個變量的操作, 再沒有執行完之前, 就不會被其他線程所幹擾.

    ​​

    ​synchronized滿足原子性, 而volatile并不能保證原子性.​

  • 有序性

    有序性即程式執行的順序按照代碼的先後順序執行

    如果不能滿足有序性, 則可能得到的結果和預期的不同, 如編譯器指令的重排序. 而​

    ​volatile包含禁止指令重排序的語義, 其具有有序性.​

    ​synchronized語義表示鎖在同一時刻隻能由一個線程進行擷取, 當鎖被占用後, 其他線程隻能等待. 是以, synchronized就要求線程通路讀寫共享變量時隻能"串行"執行, 是以​

    ​synchronized具有有序性​

    ​這篇部落格解釋了有序性在多線程中出現的重排序将導緻程式出錯 : 并發程式設計——原子性,可見性和有序性(有序性的介紹)

    對于指令重排序設計到JMM(Java Memory Model), 請看這篇部落格 : ​​Java記憶體模型(JMM)總結​​

  • 可見性

    多個線程共享同一個變量, 當變量被其中一個線程修改後, 其他線程均可感覺到, 該變量已被修改.

    ​​

    ​synchronized和volatile具有可見性​

總結

synchronized : 具有原子性, 有序性和可見性

volatile : 具有有序性和可見性

參考部落格 :

​​三大性質總結:原子性,有序性,可見性​​

6. 線程狀态的轉換

Java多線程筆記(二)

這張圖詳細介紹了線程轉換的六種狀态 :

  1. 初始(NEW) : 新建立一個線程對象, 但還沒調用start()方法
  2. 運作(RUNNABLE) : 就緒(READY) 與 運作中(RUNNING) 兩種狀态成為"運作"

    2.1 就緒(READY) : 線程建立後, 其他線程(比如main線程)調用了該對象的start()方法. 該狀态的線程位于可運作線程池中, 等待被線程排程選中并配置設定CPU使用權

    2.2 運作中(RUNNING) : 就緒狀态的線程獲得了CPU時間片, 開始執行程式代碼

  3. 阻塞(BLOCKED) : 表示線程阻塞于鎖
  4. 等待(WAITING) : 進入該狀态的線程需要等待其他線程做出一些特定的動作(通知或中斷)
  5. 逾時等待(TIMED_WAITING) : 該狀态不同于WAITING, 它可以在指定的時間後自行傳回
  6. 終止(TERMINATED) : 表示該線程以及執行完畢

7. Java同步塊

Java同步塊用來标記方法或者代碼塊是同步的.

通過​

​synchronized​

​關鍵字. 同步塊在Java中是同步在某個對象上. 所有同步在一個對象上的同步塊同僚隻能被一個線程進入并執行操作, 所有其他等待進入該同步塊的線程将被阻塞, 直到執行該同步塊的線程退出.

執行個體方法同步 : add方法

執行個體方法中的同步塊 : delete方法

public class Test {
    int count = 0;
    
    public synchronized void add(int value) {
        this.count += value;
    }
    
    public void delte(int value) {
        synchronized(this) {
            this.count -= value;
        }
    }
    
}      

在上例中, 若他們所調用的是同一個執行個體對象, 同步的執行效果是等效的, 每次隻有一個線程能夠在兩個同步塊中任意一個方法内執行.

如果第兩個同步塊不是同步在​

​this​

​對象上, 而是其他對象上, 那麼兩個方法可以被線程同時執行.

靜态方法同步 : add方法

靜态方法中的同步塊 : delete方法

public class Test {
    int count = 0;
    
    public static synchronized void add(int value) {
        this.count += value;
    }
    
    public void delete(int value) {
        synchronized(Test.class) {
            this.count -= value;
        }
    }
}      

這兩個方法不允許同時被線程通路.

如果第二個同步塊不是同步在​

​Test.class​

​這個對象上. 那麼這兩個方法可以同時被線程通路.

推薦閱讀

​​​[譯]Java虛拟機是如何執行線程同步的​​

8. 線程通信

線程通信的目标是使線程間能夠互相發送信号. 另一方面, 線程通信使線程能夠等待其他線程的信号.
  • 通過共享對象通信

    兩個或多個線程必須獲得指向一個共享執行個體的引用, 以便進行通信.

    在wait()/notify()機制中, 不要使用全局對象, 字元串常量等. 應該使用對應唯一的對象

  • wait(), notify()和notifyAll()

    這三個方法必須在同步代碼塊中調用. 如果一個線程沒有持有對象鎖, 調用這幾個方法将抛出​

    ​java.lang.IllegalMonitorStateException​

    ​wait() : 該線程将變為非運作狀态, 并且會釋放所持有的螢幕對象上的鎖.

    notify() : 正在等待該對象的所有線程中将有一個線程被喚醒并允許執行(被喚醒的線程是随機的, 不可以指定喚醒哪個線程). 喚醒後的wait()方法必須等待notify()的線程退出了它自己的同步塊才可以獲得螢幕對象的鎖才可以繼續執行(因為wait方法調用運作在同步塊裡面)

    notifyAll() : 在同一時刻将隻有一個線程可以退出wait()方法, 因為每個線程在退出wait()錢必須獲得螢幕對象的鎖

圖檔來源 : ​​Java線程的6種狀态及切換(透徹講解)​​
Java多線程筆記(二)

關于線程間通信, 有一個執行個體: 兩個方法add和size; 啟動兩個線程, t1添加10個元素; t2對t1進行監控, 當size大小為5時, t2線程提示并退出, t1線程繼續執行.

第一種方法 : wait/notify進行線程間通信

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class MyContainer {
    // volatile關鍵字 : list進行修改時, 将會通知其他線程
    volatile List list = new ArrayList();
    
    public void add(Object obj) {
        list.add(obj);
    }
    
    public int size() {
        return list.size();
    }
    
    public static void main(String[] args) {
        MyContainer mc = new MyContainer();
        
        final Object lock = new Object();
        
        new Thread(() -> {
            // 首先啟動t2線程若size不為5, 則等待t1線程
            System.out.println("t2啟動");
            synchronized(lock) {
                if (mc.size() != 5) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("t2結束");
                // 因為隻有兩個線程, 是以無需使用notifyAll()喚醒所有線程
                lock.notify();
            }
        }, "t2").start();
        
        new Thread(() -> {
            System.out.println("t1啟動");
            synchronized {
                     for (int i = 0; i < 10; i++) {
                        mc.add(new Object());
                        System.out.println("add" + i);

                        if (mc.size() == 5) {
                            lock.notify();
                            try {
                                // 需要釋放鎖來讓t2線程執行, 否則t2将會等t1執行結束後, 才會執行t2
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }

                        try {
                        TimeUnit.SECONDS.sleep(1);                    
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
            }
        }, "t1").start();
    }
    
}      

線程間通信太繁瑣, 可以使用jdk已經提供好的Latch來替代wait/notify.

方法二 : 使用CountDownLatch :

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class MyContainer3 {
    // 添加volatile, 使t2能夠得到通知
    volatile List list = new ArrayList();

    public void add(Object object) {
        list.add(object);
    }

    public int size() {
        return list.size();
    }

    public static void main(String[] args) {
        MyContainer3 mc = new MyContainer3();

        CountDownLatch latch = new CountDownLatch(1);

        new Thread(() -> {
            System.out.println("t2啟動");
            if (mc.size() != 5) {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t2結束");
        }, "t2").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> {
            System.out.println("t1啟動");
            for (int i = 0; i < 10; i++) {
                mc.add(new Object());
                System.out.println("add" + i);

                if (mc.size() == 5) {
                    latch.countDown();
                }

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("t1結束");
        }, "t1").start();
    }
}      

9. Thread.join()

該方法把指定的線程加入到目前線程, 可以将兩個交替執行的線程合并為順序執行的線程.

比如主線程調用正在執行的線程A的join()方法, 那麼主線程将會等待線程A執行完畢後, 才會繼續執行.

Thread.join()的源碼調用join(0)即下面代碼 :

/**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     */
    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }      
public class JoinTest implements Runnable {
    public 
}      

[參考文獻]

  1. ​​Java并發性和多線程 v1.2​​
  2. ​​Hollis部落格​​