Java多線程面試基礎篇

​ 本篇主要是詳細的總結了一下Java中的多線程基礎知識點,其中包括Java線程狀態、線程建立、中止線程、守護線程、Thread類的方法原理區別、synchronized關鍵字原理與使用、線程協做方式、自定義線程池實現、鎖優化、生產者消費者模型、緩存一致性和Java內存模型之間的關係、volatile原理與使用場景,單例模式實現與細節總結。(總結篇幅比較長)下一篇會主要總結juc包下同步組件原理和使用。html

Java線程基礎

Java中的Thread建立和狀態

新建(New)

​ 當程序使用 new 關鍵字建立了一個線程以後,該線程就處於新建狀態,此時僅由 JVM 爲其分配 內存,並初始化其成員變量的值;java

可運行(Runnable)

​ 當線程對象調用了 start()方法以後,該線程處於就緒狀態。Java 虛擬機會爲其建立方法調用棧和程序計數器,等待調度運行。若是處於就緒狀態的線程被調度得到CPU執行權,就會執行run方法的邏輯,此時處於運行狀態。(總之就是可能正在運行,也可能正在等待 CPU 時間片。包含了操做系統線程狀態中的 Running 和 Ready)程序員

阻塞(Blocked)

​ 阻塞狀態是指線程由於某種緣由放棄CPU使用權,暫時中止運行。這個時候須要等到線程進入可運行狀態(Runnable)纔有機會得到cpu時間片從而執行(running)。這個狀態分下面三種狀況:編程

  • 等待阻塞(obj.wait()->進入wait set):即運行中的線程執行wait方法(前提是須要得到對應的某個monitor,若是沒有會拋出異常),JVM會將該線程放入等待隊列中
  • 同步阻塞(lock.lock()/synchronized->鎖池):即運行中的線程獲取對象的同步鎖(指jvm提供的內置所synchronized)或者顯示鎖lock失敗,會將線程阻塞掛起/
  • 其餘方式阻塞(sleep/join):運行中的線程執行Thread.sleep()或者thread.join()方法,或者發出I/O請求待處理的時候,jvm會將線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程運行結束或者超時、或者處理I/O完畢,會從新進入runnable狀態

無限期等待(Waiting)

等待其它線程顯式地喚醒,不然不會被分配 CPU 時間片。設計模式

限期等待(Timed Waiting)

無需等待其它線程顯式地喚醒,在必定時間以後會被系統自動喚醒。數組

調用 Thread.sleep() 方法使線程進入限期等待狀態時,經常用「使一個線程睡眠」進行描述。緩存

調用 Object.wait() 方法使線程進入限期等待或者無限期等待時,經常用「掛起一個線程」進行描述。安全

睡眠和掛起是用來描述行爲,而阻塞和等待用來描述狀態。多線程

阻塞和等待的區別在於,阻塞是被動的,它是在等待獲取一個排它鎖。而等待是主動的,經過調用 Thread.sleep() 和 Object.wait() 等方法進入。架構

死亡(Terminated)

能夠是線程結束任務以後本身結束,或者產生了異常而結束。

建立線程的方式

繼承Thread類

​ 查看Thread類的源碼能發現,其實Thread類本質上就是Runnable接口的實現類,表明一個線程實例。使用Thread類建立線程以後,啓動線程的方式就是調用start實例方法。start方法是一個native方法,使用start0()方法,並在其中調用run方法執行核心邏輯。下面是Thread類的start方法源碼(模板設計模式的應用)

public synchronized void start() {
    /** * threadStatus==0表示線程尚未start,因此不能兩次啓動thread,不然拋出異常IllegalThreadStateException */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* 將線程添加到所屬的線程組中,並減小線程組中unstart的線程數 */
    group.add(this);

    boolean started = false;
    try {
        start0();//調用本地方法,本地方法中調用run方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* */
        }
    }
}
複製代碼

實現runnable接口

重寫Thread類的run方法和實現Runnable接口的run方法的不一樣:

​ ①繼承Thread類以後就不能繼承別的類,而實現Runnable接口更靈活

​ ②Thread類的run方法是不能共享的,線程A不能把線程B的run方法當作本身的執行單元,而使用Runnable接口可以實現這一點,即便用同一個Runnable的實例構造不一樣的Thread實例

Callable接口

​ 執行Callable 任務後,能夠獲取一個 Future 的對象,在該對象上調用 get 就能夠獲取到 Callable 任務 返回的 Object 了,再結合線程池接口 ExecutorService 就能夠實現傳說中有返回結果的多線程了。

守護線程

​ 守護線程是程序運行時在後臺提供服務的線程,不屬於程序中不可或缺的部分。當程序中沒有一個非守護線程的時候,JVM也會退出,即當全部非守護線程結束時,程序也就終止,同時會殺死全部守護線程。main() 屬於非守護線程。

​ 在線程啓動以前使用 setDaemon() 方法能夠將一個線程設置爲守護線程。若是一個線程已經start,再調用setDaemon方法就會拋出異常IllegalThreadStateException

public class TestThread4 {

    public static void main(String[] args) {

        //thread t做爲當前處理業務的線程,其中建立子線程用於檢測某些事項
        Thread t = new Thread() {
            @Override
            public void run() {
                Thread innerThread  = new Thread() {
                    @Override
                    public void run() {
                        try {
                            while (true) {
                                System.out.println("我是檢測XX的線程");
                                TimeUnit.SECONDS.sleep(1);//假設每隔 1s check
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                };
                innerThread.setDaemon(true);//設置爲守護線程
                innerThread.start();

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t.start();
        //父線程t 執行完退出以後,他的子線程innerThread也會退出,
        //若是innerThread沒有設置爲守護線程,那麼這個Application就會一直運行不會退出
        //設置爲守護線程,那麼這個Application完成任務或者被異常打斷執行,這個子線程也會退出而不會繼續執行check
        //t.setDaemon(true);//Exception in thread "main" java.lang.IllegalThreadStateException
    }
}
複製代碼

Join方法

join方法使用

​ 在線程中調用另外一個線程的 join() 方法,會將當前線程掛起,而不是忙等待,直到調用join方法的線程結束。對於如下代碼,Work實現Runnable接口,並在構造方法傳遞調用join的線程,因此儘管main中start方法的調用是t三、t二、t1可是實際上的執行順序仍是t1先執行,而後t2,最後t3(t2會等待t1執行結束,t3會等待t2執行結束)

package demo1;

public class TestThread5 {

    public static void main(String[] args) {

        Thread t1 = new Thread(new Work(null));
        Thread t2 = new Thread(new Work(t1));
        Thread t3 = new Thread(new Work(t2));
        t3.start();
        t2.start();
        t1.start();
    }
}

class Work implements Runnable {

    private Thread prevThread;

    public Work(Thread prevThread) {
        this.prevThread = prevThread;
    }

    public void run() {
        if(null != prevThread) {
            try {
                prevThread.join();
                for (int i = 0; i < 50; i++) {
                    System.out.println(Thread.currentThread().getName() + "-" + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println(Thread.currentThread().getName());
        }
    }
}
複製代碼

join方法原理

下面是join方法的源碼

//默認的join方法
public final void join() throws InterruptedException {
    join(0);
}

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) { //默認直接使用join方法的時候,會進入這個分支
        while (isAlive()) {
            wait(0); //注意這裏調用的是wait方法,咱們就須要知道wait方法的做用(調用此方法的當前線程須要釋放鎖,並等待喚醒)
        }
    } else { //設置join方法的參數(即調用者線程等待多久,目標線程尚未結束的話就不等待了)
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break; //結束等待
            }
            wait(delay); //設置的時間未到以前,繼續等待
            now = System.currentTimeMillis() - base;
        }
    }
}
複製代碼

​ 在上面的方法中,咱們看到public final synchronized void join(long millis){}這是一個synchronized方法,瞭解過synchronized的知道,修飾方法的表明鎖住對象實例(this),那麼這個this是什麼呢,(調用者線程CallThread調用thread.join()方法,this就是當前的線程對象)。

​ CallThread線程調用join方法的時候,先持有當前的鎖(線程對象thread),而後在這裏這裏wait住了(釋放鎖,並進入阻塞等待狀態),那麼CallThread何時被notify呢,下面是jvm的部分源碼,能夠看到notify的位置。當thread執行完畢,jvm就是喚醒阻塞在thread對象上的線程(即剛剛說的CallThread調用者線程),那麼這個調用者線程就能繼續執行下去了。

​ 總結就是,假設當前調用者線程是main線程,那麼:

​ ①main線程在調用方法以後,會首先獲取鎖——thread對象實例;②進入其中一個分支以後,會調用wait方法,釋放鎖,並阻塞等待thread執行完以後被喚醒;③當thread執行完畢,會調用線程的exit方法,在下面的代碼中咱們能看出其中調用ensure_join方法,而在ensure_join中就會喚醒阻塞等待的thread對象上的線程;④main線程被notify,而後繼續執行

// 位於/hotspot/src/share/vm/runtime/thread.cpp中
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
    // ...

    // Notify waiters on thread object. This has to be done after exit() is called
    // on the thread (if the thread is the last thread in a daemon ThreadGroup the
    // group should have the destroyed bit set before waiters are notified).
    // join(ensure_join)
    ensure_join(this);

    // ...
}


static void ensure_join(JavaThread* thread) {
    // We do not need to grap the Threads_lock, since we are operating on ourself.
    Handle threadObj(thread, thread->threadObj());
    assert(threadObj.not_null(), "java thread object must exist");
    ObjectLocker lock(threadObj, thread);
    // Ignore pending exception (ThreadDeath), since we are exiting anyway
    thread->clear_pending_exception();
    // Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
    java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
    // Clear the native thread instance - this makes isAlive return false and allows the join()
    // to complete once we've done the notify_all below
    java_lang_Thread::set_thread(threadObj(), NULL);

    // thread就是當前線程,就是剛纔例子中說的t線程
    lock.notify_all(thread);

    // Ignore pending exception (ThreadDeath), since we are exiting anyway
    thread->clear_pending_exception();
}
複製代碼

終止線程的方法

stop弊端

​ 破壞原子邏輯,以下面的例子中,MultiThread實現了Runnable接口,run方法中加入了synchronized同步代碼塊,表示內部是原子邏輯,a的值會先增長後減小,按照synchronized的規則,不管啓動多少個線程,打印出來的結果都應該是a=0。可是若是有一個正在執行的線程被stop,就會破壞這種原子邏輯.(下面面main方法中代碼)

public class TestStop4 {

    public static void main(String[] args) {
        MultiThread t = new MultiThread();
        Thread testThread = new Thread(t,"testThread");
        // 啓動t1線程
        testThread.start();
        //(1)這裏是保證線程t1先線得到鎖,線程t1啓動,並執行run方法,因爲沒有其餘線程同步代碼塊的鎖,
        //因此t1線程執行自加後執行到sleep方法開始休眠,此時a=1.
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //(2)JVM又啓動了5個線程,也同時運行run方法,因爲synchronized關鍵字的阻塞做用,這5個線程不能執行自增和自減操做,等待t1線程釋放線程鎖.
        for (int i = 0; i < 5; i++) {
            new Thread(t).start();
        }
        //(3)主線程執行了t1.stop方法,終止了t1線程,可是因爲a變量是線程共享的,因此其餘5個線程得到的a變量也是1.(synchronized的可見性保證)
        testThread.stop();
        //(4)其餘5個線程得到CPU的執行機會,打印出a的值.
    }
}
class MultiThread implements Runnable {
    int a = 0;
    private Object object = new Object();
    public void run() {
        // 同步代碼塊,保證原子操做
        synchronized (object) {
            // 自增
            a++;
            try {
                // 線程休眠0.1秒
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 自減
            a--;
            String tn = Thread.currentThread().getName();
            System.out.println(tn + ":a =" + a);
        }
    }
}
複製代碼

實現stop線程

下面是兩種優雅的關閉線程的方式

①使用自定義標誌位配合volatile實現

public class ThreadStop1 {
    private static class Work extends Thread {
        private volatile boolean flag = true;
        public void run() {
            while (flag) {
                //do something
            }
        }
        public void shutdown() {
            this.flag = false;
        }
    }
    public static void main(String[] args) {
        Work w = new Work();
        w.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        w.shutdown();
    }

}
複製代碼

②使用中斷

public class ThreadStop2 {
    private static class Work extends Thread{
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    break; //別的線程中斷該線程
                }
            }
        }
    }
    public static void main(String[] args) {
        Work w = new Work();
        w.start();
        try {
            TimeUnit.SECONDS.sleep(1);
            w.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

複製代碼

start 與 與 run 區別

  1. start()方法來啓動線程,真正實現了多線程運行。這時無需等待 run 方法體代碼執行完畢,能夠繼續執行start後面的代碼。
  2. 經過調用 Thread 類的 start()方法來啓動一個線程, 這時此線程是處於就緒狀態, 並無運行。
  3. 方法 run()稱爲線程體,它包含了要執行的這個線程的內容,線程就進入了運行狀態,開始運行 run 函數當中的代碼。 Run 方法運行結束, 此線程終止。而後 CPU 再調度其它線程。直接調用run方法並非開啓一個線程(普通方法調用),開啓一個線程必須調用start方法

synchronized

能夠參考我總結的Java中Lock和synchronized。這裏在總結一下synchronized的相關知識

synchronized用法

①當synchronized做用在實例方法時,監視器鎖(monitor)即是對象實例(this)

②當synchronized做用在靜態方法時,監視器鎖(monitor)即是對象的Class實例,由於Class數據存在於永久代,所以靜態方法鎖至關於該類的一個全局鎖

③當synchronized做用在某一個對象實例時,監視器鎖(monitor)即是括號括起來的對象實例

public class TestSynchronized1 {
	//靜態代碼塊
    static {
        synchronized (TestSynchronized1.class) {
            System.out.println(Thread.currentThread().getName() + "==>static code");
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //static
    private synchronized static void m1() {
        System.out.println(Thread.currentThread().getName() + "==>method:m1");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //this
    private synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + "==>method:m2");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
	//普通方法
    private void m3() {
        System.out.println(Thread.currentThread().getName() + "==>method:m3");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    //static
    private synchronized static void m4() {
        System.out.println(Thread.currentThread().getName() + "==>method:m4");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        final TestSynchronized1 test = new TestSynchronized1();
        new Thread() {
            @Override
            public void run() {
                test.m2();
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                TestSynchronized1.m1();
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                test.m3();}
        }.start();

        new Thread() {
            @Override
            public void run() {
                TestSynchronized1.m4();
            }
        }.start();
    }
}


複製代碼

synchronized的原理

(關於鎖與原子交換指令硬件原語能夠參考從同步原語看非阻塞同步 的介紹)

①Java 虛擬機中的同步(Synchronization)基於進入和退出管程(Monitor)對象實現。同步代碼塊是使用monitorenter和monitorexit來實現的,同步方法 並非由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標誌來隱式實現的(可是實際上差很少)。monitorenter指令是在編譯後插入同步代碼塊的起始位置,而monitorexit指令是在方法結束處和異常處,每一個對象都有一個monitor與之關聯,當一個monitor被持有後它就會處於鎖定狀態。

②synchronized用的鎖是存在Java對象頭(非數組類型包括Mark Word、類型指針,數組類型多了數組長度)裏面的,對象頭中的Mark Word存儲對象的hashCode,分代年齡和鎖標記位,類型指針指向對象的元數據信息,JVM經過這個指針肯定該對象是那個類的實例等信息。

③當在對象上加鎖的時候,數據是記錄在對象頭中,對象頭中的Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化(無鎖、輕量級鎖00、重量級鎖十、偏向鎖01)。當執行synchronized的同步方法或者同步代碼塊時候會在對象頭中記錄鎖標記,鎖標記指向的是monitor對象(也稱爲管程或者監視器鎖)的起始地址。因爲每一個對象都有一個monitor與之關聯,monitor和與關聯的對象一塊兒建立(當線程試圖獲取鎖的時候)或銷燬,當monitor被某個線程持有以後,就處於鎖定狀態。

④Hotspot虛擬機中的實現,經過ObjectMonitor來實現的。

關於synchronized的說明

①synchronized可以保證原子性、可見性、有序性

②在定義同步代碼塊的時候,不要將常量對象做爲鎖對象

③synchronized鎖的是對象,而不是引用

④當同步方法出現異常的時候會自動釋放鎖,不會影響其餘線程的執行

⑤synchronized是可重入的

實現一個會發生死鎖的程序

public class TestDeadLock {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    private void m1() {
        synchronized (lock1) {
            System.out.println(Thread.currentThread().getName() + " get lock:lock1");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " get lock:lock2");
            }
        }
    }

    private void m2() {
        synchronized (lock2) {
            System.out.println(Thread.currentThread().getName() + " get lock:lock2");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " get lock:lock1");
            }
        }
    }

    public static void main(String[] args) {
        final TestDeadLock test = new TestDeadLock();
        new Thread(){
            @Override
            public void run() {
                test.m1();
            }
        }.start();
        new Thread(){
            @Override
            public void run() {
                test.m2();
            }
        }.start();
    }
}


複製代碼

Java中的鎖概念

樂觀鎖

樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。java 中的樂觀鎖基本都是經過 CAS 操做實現的,CAS 是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。

悲觀鎖

​ 悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如 RetreenLock。

鎖優化

​ 從JDK6開始,對synchronized的實現機制進行了較大調整,好比自適應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖這些優化策略等等,是爲了解決線程之間更加高效的共享數據,解決競爭問題。

減小鎖持有時間 只用在有線程安全要求的程序上加鎖 減少鎖粒度 將大對象(這個對象可能會被不少線程訪問),拆成小對象,大大增長並行度,下降鎖競爭。下降了鎖的競爭,偏向鎖,輕量級鎖成功率纔會提升。最最典型的減少鎖粒度的案例就是ConcurrentHashMap。 鎖分離 最多見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了線程安全,又提升了性能。讀寫分離思想能夠延伸,只要操做互不影響,鎖就能夠分離。好比LinkedBlockingQueue 從頭部取出,從尾部放數據 鎖粗化 一般狀況下,爲了保證多線程間的有效併發,會要求每一個線程持有鎖的時間儘可能短,即在使用完公共資源後,應該當即釋放鎖。可是,凡事都有一個度,若是對同一個鎖不停的進行請求、同步和釋放,其自己也會消耗系統寶貴的資源,反而不利於性能的優化 。 鎖消除 鎖消除是在編譯器級別的事情。在即時編譯器時,若是發現不可能被共享的對象,則能夠消除這些對象的鎖操做,多數是由於程序員編碼不規範引發。

線程間通訊與生產者消費者模型

wait-notify的消息通知機制

​ 結合上面說到的synchronized,在上圖中,ObjectMonitor中有兩個隊列(EntryList、WaitSet(Wait Set是一個虛擬的概念))以及鎖持有者Owner標記,其中WaitSet是哪些調用wait方法以後被阻塞等待的線程隊列,EntryList是ContentionList中能有資格獲取鎖的線程隊列。當多個線程併發訪問同一個同步代碼時候,首先會進入EntryList,當線程得到鎖以後monitor中的Owner標記會記錄此線程,並在該monitor中的計數器執行遞增計算表明當前鎖被持有鎖定,而沒有獲取到的線程繼續在EntryList中阻塞等待。若是線程調用了wait方法,則monitor中的計數器執行賦0運算,而且將Owner標記賦值爲null,表明當前沒有線程持有鎖,同時調用wait方法的線程進入WaitSet隊列中阻塞等待,直到持有鎖的執行線程調用notify/notifyAll方法喚醒WaitSet中的線程,喚醒的線程進入EntryList中等待鎖的獲取。除了使用wait方法能夠將修改monitor的狀態以外,顯然持有鎖的線程的同步代碼塊執行結束也會釋放鎖標記,monitor中的Owner會被賦值爲null,計數器賦值爲0。以下圖所示是一個線程狀態轉換圖。

①wait

​ 該方法用來將當前線程置入BLOCKED狀態(或者進入WAITSET中),直到接到通知或被中斷爲止。在調用 wait()以前,線程必需要得到該對象的MONITOR,即只能在同步方法或同步塊中調用 wait()方法。調用wait()方法以後,當前線程會釋放鎖。若是調用wait()方法時,線程並未獲取到鎖的話,則會拋出IllegalMonitorStateException異常,這是個RuntimeException。被喚醒以後,若是再次獲取到鎖的話,當前線程才能從wait()方法處成功返回。

②notify

​ 該方法也要在同步方法或同步塊中調用,即在調用前,線程也必需要得到該對象的MONITOR,若是調用 notify()時沒有持有適當的鎖,也會拋出 IllegalMonitorStateException。 該方法任意從在該MONITOR上的WAITTING狀態的線程中挑選一個進行通知,使得調用wait()方法的線程從等待隊列移入到同步隊列中,等待有機會再一次獲取到鎖,從而使得調用wait()方法的線程可以從wait()方法處退出。調用notify後,當前線程不會立刻釋放該對象鎖,要等到程序退出同步塊後,當前線程纔會釋放鎖。

③notifyAll

​ 該方法與 notify ()方法的工做方式相同,重要的一點差別是: notifyAll 使全部原來在該對象的MONITOR上 wait 的線程通通退出WAITTING狀態,使得他們所有從等待隊列中移入到同步隊列中去,等待下一次可以有機會獲取到對象監視器鎖。

notify早期通知

​ 假設如今有兩個線程,threadWait、threadNotify,在測試程序中讓使得threadWait還沒開始 wait 的時候,threadNotify已經 notify 了。再這樣的前提下,儘管threadNotify進行了notify操做,可是這個通知是沒有任何響應的(threadWait尚未進行wait),當 threadNotify退出 synchronized 代碼塊後,threadWait再開始 wait,便會一直阻塞等待,直到被別的線程打斷。好比在下面的示例代碼中,就模擬出notify早期通知帶來的問題:

public class EarlyNotify {

    private static final Object object = new Object();

    public static void main(String[] args) {
        Thread threadWait = new Thread(new ThreadWait(object),"threadWait");
        Thread threadNotify = new Thread(new ThreadNotify(object), "threadNotify");
        threadNotify.start(); //喚醒線程線啓動
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //wait線程後啓動
        threadWait.start();
    }

    private static class ThreadWait implements Runnable {
        private Object lock;
        public ThreadWait(Object lock) {
            this.lock = lock;
        }
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "進入代碼塊");
                System.out.println(Thread.currentThread().getName() + "進入wait set");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "退出wait set");
            }
        }
    }

    private static class ThreadNotify implements Runnable {
        private Object lock;
        public ThreadNotify(Object lock) {
            this.lock = lock;
        }
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + "進入代碼塊");
                System.out.println(Thread.currentThread().getName() + "開始notify");
                lock.notify();
                System.out.println(Thread.currentThread().getName() + "notify結束");
            }
        }
    }
}


複製代碼

​ 運行以後使用jconsole查看以下

wait的條件發生變化

​ 等待線程wait的條件發生變化,若是沒有再次檢查等待的條件就可能發生錯誤。看一下下面的程序實例以及運行結果。

//運行出現下面的結果
/* * consumerThread1 list is empty * producerThread begin produce * consumerThread2 取出元素element0 * consumerThread1 end wait, begin consume * Exception in thread "consumerThread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 */
public class WaitThreadUncheckCondition {
    private static List<String> lock = new ArrayList<String>();
    public static void main(String[] args) {
        Thread producerThread = new Thread(new ProducerThread(lock),"producerThread");
        Thread consumerThread1 = new Thread(new ConsumerThread(lock), "consumerThread1");
        Thread consumerThread2 = new Thread(new ConsumerThread(lock), "consumerThread2");
        consumerThread1.start();
        consumerThread2.start();
        producerThread.start();
    }
    private static class ProducerThread implements Runnable {
        private List<String> lock;
        private int i = 0;
        public ProducerThread(List<String> lock) {
            this.lock = lock;
        }
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + " begin produce");
                lock.add("element" + i++);
                lock.notifyAll();
            }
        }
    }
    private static class ConsumerThread implements Runnable {
        private List<String> list;
        public ConsumerThread(List<String> list) {
            this.list = list;
        }
        public void run() {
            synchronized (lock) {
                try {
                    if (lock.isEmpty()) {
                        System.out.println(Thread.currentThread().getName() + " list is empty");
                        lock.wait();
                        System.out.println(Thread.currentThread().getName() + " end wait, begin consume");
                    }
                    System.out.println(Thread.currentThread().getName() + " 取出元素" + lock.remove(0));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

複製代碼

​ 在這個例子中一共開啓了3個線程,consumerThread1,consumerThread2以及producerThread。首先consumerThread1在獲取到lock以後判斷isEmpty爲true,而後調用了wait方法進入阻塞等待狀態,而且將對象鎖釋放出來。而後,consumerThread2可以獲取對象鎖,從而進入到同步代塊中,當執行到wait方法時判斷isEmpty也爲true,一樣的也會釋放對象鎖。所以,producerThread可以獲取到對象鎖,進入到同步代碼塊中,向list中插入數據後,經過notifyAll方法通知處於WAITING狀態的consumerThread1和consumerThread2線程。consumerThread2獲得對象鎖後,從wait方法出退出而後繼續向下執行,刪除了一個元素讓List爲空,方法執行結束,退出同步塊,釋放掉對象鎖。這個時候consumerThread1獲取到對象鎖後,從wait方法退出,繼續往下執行,這個時候consumerThread1再執行lock.remove(0);就會出錯,由於List因爲Consumer1刪除一個元素以後已經爲空了。

修改的生產者消費者模型

//多生產者消費者的狀況,須要注意若是使用notify方法會致使程序「假死」,即全部的線程都wait住了,沒有可以喚醒的線
//程運行:假設當前多個生產者線程會調用wait方法阻塞等待,當其中的生產者線程獲取到對象鎖以後使用notify通知處於
//WAITTING狀態的線程,若是喚醒的仍然是生產者線程,就會形成全部的生產者線程都處於等待狀態。這樣消費者得不到消費
//的信號,就也是一直處於wait狀態
public class ProducersAndConsumersPattern {

    private int sth = 0;
    private volatile boolean isProduced = false;
    private final Object object = new Object();

    private void producer() {
        synchronized (object) {
            //若是不須要生產者生產
            while (isProduced) {
                //生產者進入等待狀態
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //生產者進行生產
            sth++;
            System.out.println(Thread.currentThread().getName() + " producer: " + sth);
            //喚醒消費者
            object.notifyAll();
            isProduced = true; //消費者能夠進行消費
        }
    }

    private void consumer() {
        synchronized (object) {
            //消費者不能夠消費,進入等待阻塞狀態
            while (!isProduced) {
                try {
                    object.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + " consumer: " + sth);
            object.notifyAll();
            isProduced = false;//生產者生產
        }
    }

    public static void main(String[] args) {
        final ProducersAndConsumersPattern producerAndConsumerPattern = new ProducersAndConsumersPattern();
        for (int i = 0; i < 3; i++) {
            new Thread("consumer thread-" + i) {
                @Override
                public void run() {
                    while (true) {
                        producerAndConsumerPattern.consumer();
                    }
                }
            }.start();
        }
        for (int i = 0; i < 3; i++) {
            new Thread("producer thread-" + i) {
                @Override
                public void run() {
                    while (true) {
                        producerAndConsumerPattern.producer();
                    }
                }
            }.start();
        }
    }
}


複製代碼

wait和sleep的區別

(1)wait是Object的方法,而sleep是Thread的方法

(2)wait會釋放monitor,並將線程加入wait-set中,可是sleep不會釋放monitor

(3)使用wait必需要首先獲取monitor,可是sleep不須要

(4)調用sleep不須要被喚醒,可是wait調用者須要被喚醒

自定義顯示鎖

使用synchronized、wait、notify/notifyAll

自定義Lock接口

/** * 自定義顯示鎖接口 */
public interface Lock {
    class TimeOutException extends Exception {
        public TimeOutException(String msg) {
            super(msg);
        }
    }
    /** * 獲取lock * @throws InterruptedException */
    void lock() throws InterruptedException;

    /** * 指定時間的lock * @param millis * @throws InterruptedException * @throws TimeOutException */
    void lock(long millis) throws InterruptedException,TimeOutException;

    /** * 釋放鎖 */
    void unlock();

    /** * 獲取阻塞在當前monitor上的線程 * @return */
    Collection<Thread> getBlockedThreads();

    /** * 獲取當前阻塞隊列的大小 * @return */
    int getBlockedSize();
}


複製代碼

BooleanLock實現類

/** * 實現Lock接口:經過一個boolean類型的變量表示同步狀態的獲取 */
public class BooleanLock implements Lock {

    //獲取鎖的標誌
    //true:表示當前lock被獲取
    //false:表示當前lock沒有被獲取
    private boolean initValue;

    //阻塞在lock monitor上的線程集合
    private Collection<Thread> blockedThreads = new ArrayList<Thread>();

    //當前獲取到鎖的線程:用於在迪調用unlock的時候進行check(只有是得到lock monitor的線程才能unlock monitor)
    private Thread currentGetLockThread;

    public BooleanLock() {
        this.initValue = false;
    }

    /** * 原子性的獲取鎖 * * @throws InterruptedException */
    public synchronized void lock() throws InterruptedException {
        //當initValue爲true的時候,表示獲取失敗,就加入阻塞隊列中
        while (initValue) {
            blockedThreads.add(Thread.currentThread());
            this.wait(); //加入阻塞隊列中,而後調用wait進入(這裏的monitor是當前類的instance)
        }
        //這裏表示能夠獲取鎖,就置標誌位位true,並將本身移除阻塞隊列
        blockedThreads.remove(Thread.currentThread());
        this.initValue = true;
        //設置當前得到到lock monitor的線程
        this.currentGetLockThread = Thread.currentThread();
    }
    
    /** * 設置帶有超時的lock方法 * @param millis * @throws InterruptedException * @throws TimeOutException */
    public synchronized void lock(long millis) throws InterruptedException, TimeOutException {
        if (millis <= 0) {
            lock();
        }
        long holdTime = millis;
        long endTime = System.currentTimeMillis() + millis;
        while (initValue) {
            //若是已經超時
            if (holdTime <= 0) {
                throw new TimeOutException("TimeOut");
            }
            //已經被別的線程獲取到lock monitor,就將當前線程加入blockedThreads中
            blockedThreads.add(Thread.currentThread());
            this.wait(millis); //等待指定時間,若是尚未獲取鎖,下一次循環判斷的時候就會拋出TimeOutException
            holdTime  =endTime - System.currentTimeMillis();
            System.out.println(holdTime);
        }
        this.initValue = true;
        currentGetLockThread = Thread.currentThread();
    }

    /** * 原子性的釋放鎖 */
    public synchronized void unlock() {
        //檢查是不是當前獲取到lock monitor的線程釋放
        if (Thread.currentThread() == currentGetLockThread) {
            this.initValue = false;
            System.out.println(Thread.currentThread().getName() + " release the lock monitor.");
            this.notifyAll(); //當前線程釋放掉鎖以後,喚醒別的阻塞在這個monitor上的線程
        }
    }

    public Collection<Thread> getBlockedThreads() {
        //這裏的blockedThreads是read only的
        return Collections.unmodifiableCollection(blockedThreads); //設置不容許修改
    }

    public int getBlockedSize() {
        return blockedThreads.size();
    }
}


複製代碼

自定義線程池實現

​ 線程池的使用主要是解決兩個問題:①當執行大量異步任務的時候線程池可以提供更好的性能,在不使用線程池時候,每當須要執行異步任務的時候直接new一個線程來運行的話,線程的建立和銷燬都是須要開銷的。而線程池中的線程是可複用的,不須要每次執行異步任務的時候從新建立和銷燬線程;②線程池提供一種資源限制和管理的手段,好比能夠限制線程的個數,動態的新增線程等等。

​ 下面是Java中的線程池的處理流程,參考我總結的Java併發包中線程池ThreadPoolExecutor原理探究

​ ①corePoolSize:線程池核心線程個數;

​ ②workQueue:用於保存等待任務執行的任務的阻塞隊列(好比基於數組的有界阻塞隊列ArrayBlockingQueue、基於鏈表的無界阻塞隊列LinkedBlockingQueue等等);

​ ③maximumPoolSize:線程池最大線程數量;

​ ④ThreadFactory:建立線程的工廠;

​ ⑤RejectedExecutionHandler:拒絕策略,表示當隊列已滿而且線程數量達到線程池最大線程數量的時候對新提交的任務所採起的策略,主要有四種策略:AbortPolicy(拋出異常)、CallerRunsPolicy(只用調用者所在線程來運行該任務)、DiscardOldestPolicy(丟掉阻塞隊列中最近的一個任務來處理當前提交的任務)、DiscardPolicy(不作處理,直接丟棄掉);

​ ⑥keepAliveTime:存活時間,若是當前線程池中的數量比核心線程數量多,而且當前線程是閒置狀態,該變量就是這些線程的最大生存時間;

​ ⑦TimeUnit:存活時間的時間單位。

​ 下面是自定義一個簡單的線程池實現。

/** * 自定義線程池實現 */
public class SimpleThreadPool extends Thread {

    //線程池線程數量
    private int size;
    //任務隊列大小
    private int queueSize;
    //任務隊列最大大小
    private static final int MAX_TASK_QUEUE_SIZE = 200;
    //線程池中線程name前綴
    private static final String THREAD_PREFIX = "SIMPLE-THREAD-POLL-WORKER";
    //線程池中的線程池所在的THREADGROUP
    private static final ThreadGroup THREAD_GROUP = new ThreadGroup("SIMPLE-THREAD-POLL-GROUP");
    //任務隊列
    private static final LinkedList<Runnable> TASK_QUEUE = new LinkedList<>();
    //線程池中的工做線程集合
    private static final List<Worker> WORKER_LIST = new ArrayList<>();
    //線程池中的線程編號
    private AtomicInteger threadSeq = new AtomicInteger();
    //拒絕策略
    private RejectionStrategy rejectionStrategy;
    //默認的拒絕策略:拋出異常
    public final static RejectionStrategy DEFAULT_REJECTION_STRATEGY = () -> {
        throw new RejectionException("Discard the task.");
    };
    //線程池當前狀態是否爲shutdown
    private boolean destroy = false;
    //增長線程池自動維護功能的參數
    //默認初始化的時候的線程池中的線程數量
    private int minSize;
    //默認初始化時候線程數量不夠時候,擴充到activeSize大小
    private int activeSize;
    //線程池中線程最大數量:當任務隊列中的數量大於activeSize的時候,會再進行擴充
    //當執行完畢,會回收空閒線程,數量變爲activeSize
    private int maxSize;

    /** * 默認構造 */
    public SimpleThreadPool() {
        this(2, 4, 8, MAX_TASK_QUEUE_SIZE, DEFAULT_REJECTION_STRATEGY);
    }

    /** * 指定線程池初始化時候的線程個數 * @param minSize * @param activeSize * @param maxSize * @param queueSize * @param rejectionStrategy */
    public SimpleThreadPool(int minSize, int activeSize, int maxSize, int queueSize, RejectionStrategy rejectionStrategy) {
        if (queueSize <= MAX_TASK_QUEUE_SIZE) {
            this.minSize = minSize;
            this.activeSize = activeSize;
            this.maxSize = maxSize;
            this.queueSize = queueSize;
            this.rejectionStrategy = rejectionStrategy;
            init();
        }
    }

    /** * 線程池自己做爲一個線程,run方法中維護線程中的線程個數 */    
    @Override
    public void run() {
        while (!destroy) {
            System.out.printf("Pool===#Min:%d; #Active:%d; #Max:%d; #Current:%d; QueueSize:%d\n",
                    this.minSize, this.activeSize, this.maxSize, this.size, this.TASK_QUEUE.size());
            try {
                TimeUnit.SECONDS.sleep(5);
                //下面是擴充邏輯
                //當前任務隊列中的任務數多於activeSize(擴充到activeSize個線程)
                if (TASK_QUEUE.size() > activeSize && size < activeSize) {
                    for (int i = size; i < activeSize; i++) {
                        createWorkerTask();
                    }
                    size = activeSize;
                    System.out.println("Thread pool's capacity has increased to activeSize.");
                }
                if (TASK_QUEUE.size() > maxSize && size < maxSize) { //擴充到maxSize
                    for (int i = size; i < maxSize; i++) {
                        createWorkerTask();
                    }
                    size = maxSize;
                    System.out.println("Thread pool's capacity has increased to maxSize.");
                }
                //當任務隊列爲空時候,須要將擴充的線程回收
                //以activeSize爲界限,大於activeSize而且線程池空閒的時候回收空閒的線程
                //先獲取WORKER_LIST的monitor
                //這樣就會在回收空閒線程的時候,防止提交任務
                synchronized (WORKER_LIST) {
                    if (TASK_QUEUE.isEmpty() && size > activeSize) {
                        int releaseNum = size - activeSize;
                        for (Iterator<Worker> iterator = WORKER_LIST.iterator(); iterator.hasNext(); ) {
                            if (releaseNum <= 0) {
                                break;
                            }
                            Worker work = (Worker) iterator.next();
                            work.close();
                            work.interrupt();
                            iterator.remove();
                            System.out.println(Thread.currentThread().getName() + "===============Closed " + work.getName());
                            releaseNum--;
                        }
                        size = activeSize;
                        System.out.println("Thread pool's capacity has reduced to activeSize.");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /** * 初始化線程池 */
    private void init() {
        for (int i = 0; i < minSize; i++) {
            createWorkerTask();
            size++;
        }
    }

    /** * 線程池/線程狀態 */
    private enum TaskState {
        FREE, RUNNING, BLOCKED, TERMINATED
    }

    /** * 提交任務到線程池 * @param runnable */
    public void execute(Runnable runnable) {
        if (destroy == false) {
            if (null != runnable) {
                synchronized (TASK_QUEUE) {
                    if (TASK_QUEUE.size() >= queueSize) {
                        rejectionStrategy.throwExceptionDirectly();
                    }
                    TASK_QUEUE.addLast(runnable);
                    TASK_QUEUE.notify();
                    //這裏使用notify方法而不是使用notifyAll方法,是由於可以肯定必然有工做者線程Worker被喚
                    //醒,因此比notifyAll會有更小的開銷(避免了將等待隊列中的工做線程所有移動到同步隊列中競爭任務)
                }
            }
        } else {
            throw new IllegalStateException("線程池已經關閉");
        }
    }

    /** * 向線程池中添加工做線程 */
    private void createWorkerTask() {
        Worker worker = new Worker(THREAD_GROUP, THREAD_PREFIX + threadSeq.getAndIncrement()); //建立一個工做線程
        worker.start(); //啓動工做者線程
        WORKER_LIST.add(worker); //向線程池的工做線程集合中加入該線程
    }

    public void shutdown() {
        //任務隊列中還有任務
        while (!TASK_QUEUE.isEmpty()) {
            try {
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (WORKER_LIST) {
            int curNum = WORKER_LIST.size(); //獲取工做線程集合中的線程個數
            while (curNum > 0) {
                for (Worker work : WORKER_LIST) {
                    if (work.getTaskState() == TaskState.BLOCKED) {
                        work.interrupt();
                        work.close();
                        System.out.println(Thread.currentThread().getName() + "=============Closed.");
                        curNum--;
                    } else {
                        try {
                            TimeUnit.SECONDS.sleep(4);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        this.destroy = true;
        System.out.println("Thread-Pool shutdown.");
    }

    /** * 線程池的工做線程定義 */
    private static class Worker extends Thread {

        //volatile類型的taskState,用於在關閉線程的時候調用
        private volatile TaskState taskState = TaskState.FREE;

        public Worker(ThreadGroup group, String name) {
            super(group, name);
        }

        /** * 返回線程狀態 * * @return */
        public TaskState getTaskState() {
            return this.taskState;
        }

        @Override
        public void run() {
            BEGIN:
            while (this.taskState != TaskState.TERMINATED) {
                Runnable runnable = null;
                //從任務隊列中取任務(首先獲取TASK_QUEUE的monitor)
                synchronized (TASK_QUEUE) {
                    //判斷任務隊列是否爲空
                    while (TASK_QUEUE.isEmpty()) {
                        try {
                            this.taskState = TaskState.BLOCKED;
                            TASK_QUEUE.wait(); //任務隊列爲空,當前線程就進入等待狀態
                        } catch (InterruptedException e) {
                            System.out.println("Interrupted Close.");
                            break BEGIN; //被打斷以後,從新執行本身的run方法
                        }
                    }
                    //被喚醒以後,且判斷任務隊列不爲空,就從任務隊列中獲取一個Runnable
                    runnable = TASK_QUEUE.removeFirst();
                }
                if (null != runnable) {
                    this.taskState = TaskState.RUNNING;//當前執行任務的線程爲RUNNING
                    runnable.run();//執行run
                    this.taskState = TaskState.FREE; //任務執行完畢,狀態變爲FREE
                }
            }
        }

        public void close() {
            this.taskState = TaskState.TERMINATED;
        }
    }

    public interface RejectionStrategy {
        //直接拋出異常
        void throwExceptionDirectly() throws RejectionException;
    }

    //自定義異常類:拒絕策略——拋出異常
    public static class RejectionException extends RuntimeException {
        public RejectionException(String msg) {
            super(msg);
        }
    }
    //測試編寫的SimpleThreadPool
    public static void main(String[] args) {
        SimpleThreadPool simpleThreadPool = new SimpleThreadPool();
        for (int index = 0; index < 50; index++) {
            simpleThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " start the task");
                    //do sth
                    int i = 0;
                    while (i++ < 50) { }
                    System.out.println(Thread.currentThread().getName() + " finish the task");
                }
            });
        }
        simpleThreadPool.start(); //線程池維護參數
        simpleThreadPool.shutdown();
    }    
}

複製代碼

java內存模型

CPU-cache模型

​ 在計算機中,運算操做都是由CPU的寄存器完成的,而指令的執行過程涉及數據的讀取和寫入操做,CPU能訪問的數據一般只能是計算機的主存(沒有引入cache以前),雖然CPU的處理速度不斷提升,可是訪存速度卻成爲整個處理系統性能提高的瓶頸。因此爲了解決這種速度矛盾,在它們出如今CPU和內存之間增長緩存的設計。如圖所示。

​ 由於程序指令和程序數據的行爲和熱點(使用狀況)分佈差別很大,因此L1-Cache被劃分爲指令cache(L1i(instruction))和數據cache(L1d(data))這兩種將數據和指令用於各自功能的緩存。CPU Cache是由不少個cache line(緩存行)構成。

CPU緩存一致性問題

​ 加入高速緩存帶來了一個新的問題:緩存一致性。若是多個緩存共享同一塊主內存區域,那麼多個緩存的數據可能會不一致。好比:對於i++操做,須要:

​ ①將主內存中cache調入本身的CPU cache的緩存行中中;

​ ②對i進行+1操做;

​ ③將結果寫到CPU Cache中;

​ ④將數據刷新到主內存中。

​ 這種操做在單線程下沒有問題,可是多線程情況下,因爲本地內存(cache)的存在,假設兩個程都將初始值爲0的i調入本身的本地內存中,而後計算,以後刷新到主內存。這樣雖然計算兩次結果計算是1(一次的結果),這就是典型的緩存不一致性問題。

​ 因此須要解決緩存不一致的問題,好比經過對總線加鎖(悲觀/串行化的方式,須要對整個總線加鎖),或者經過緩存一致性協議實現。

​ 其中緩存一致性協議的核心思想是:①當寫入數據的時候,若是發現該數據是被共享的(其餘CPU的cache中有該數據的變量),會發出一個信號,通知其餘的CPU對該共享數據的緩存無效;②當其餘CPU對該數據進行讀取的時候,會從新從主內存中讀取。

(關於更多詳細內容能夠參考學過的組原和系統結構)

Java內存模型

​ 線程通訊的機制主要有兩種:共享內存和消息傳遞。①共享內存:線程之間共享程序的公共狀態,經過寫-讀共享內存中的公共狀態來進行隱式通訊;②消息傳遞:線程之間沒有公共狀態,線程之間 必須經過發送消息來顯式通訊。Java併發採用的是共享內存模型。那什麼是Java內存模型呢,大概的抽象關係是這些:

​ ①規定了全部的共享變量都存儲在主內存中,每一個線程夠能夠訪問(線程之間存在共享狀態)

​ ②每一個線程中還有本身的工做內存

​ ③線程的工做內存中保存了被該線程所使用到的變量(這些變量是從主內存中拷貝而來的副本)

​ ④工做內存和java內存模型同樣是一個抽象的概念,它涵蓋了緩存、寄存器、編譯器優化等,能夠由虛擬機實現着充分的自由空間利用機器的硬件實現。

​ 根據java內存模型的特色能夠看出,線程對變量的全部操做(讀取,賦值)都必須在工做內存中進行。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。以下圖所示。

​ java內存模型是一個抽象的概念,與計算機的硬件架構並不徹底同樣,好比計算機物理內存中不爲有棧內存和堆內存的概念,可是無論是棧內存仍是堆內存都會對應到具體的物理內存上,固然CPU調度線程運行的時候也有一部分堆棧內存的數據會調入cache寄存器中。咱們來看一下java內存模型和CPU-cache硬件架構的交互圖。

原子性

Ⅰ:原子性概念

一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。

Ⅱ:JMM中的原子性

​ 在java中對基本數據類型的b變量的讀取賦值操做都是原子性的,這類操做是不可被中斷的,要麼所有執行要麼不執行。下面是集中狀況

x=10;//賦值操做

複製代碼

​ 這個操做是原子性的,執行線程先將x=10寫入工做內存中,而後同步到主內存中(有可能在刷新到主內存的過程當中別的線程也在進行同步刷新操做,假設另外一個線程刷新x=11,那麼最終主內存的結果要麼是10,要麼是11,不會出現別的狀況。)因此單單就這種賦值來看是原子性的。

y=x;//賦值操做

複製代碼

​ 這個操做是非原子性的。他包含2個步驟:①執行線程先從主內存中獲取x的值,而後存入本身的工做線程,若是x在工做內存中就直接獲取;②執行線程將工做內存中的值修改成x的值,而後刷新到主內存中。其中第一個操做是對基本類型x的簡單讀取操做,第二個操做是基本類型y的寫操做,可是兩個操做在一塊兒就不是原子操做了。

y++;//自增操做

複製代碼

​ 這個操做是非原子的。包含這幾個步驟:①執行線程從主內存中如y的值,而後存入本身的工做內存中,若是工做內存中由就直接獲取;②執行+1操做;③執行線程將+1的結果寫入工做內存;④刷新到主內存中。這幾個步驟在一塊兒就不是原子性的了。

y=y+1;//加1操做,同y++。

複製代碼

Ⅲ:總結

​ ①多個原子性操做在一塊兒若是不加額外措施,就不是原子性操做了;②java中只提供簡單類型的讀取和賦值操做是原子性的,可是想y=x這樣將另外一個變量的值賦值給一個變量就不是原子性的了;③使用synchronized或者juc中的Lock可使得某個代碼片斷操做具有原子性。

可見性

Ⅰ:可見性概念

當一個線程修改了共享變量的值,其餘線程可以當即獲得這個修改的情況

Ⅱ:JMM與可見性

java中提供volatile來提供可見性保證,除了volatile,Java中synchronized和juc下面的Lock也能保證可見性。

有序性

Ⅰ:有序性概念

即程序執行的順序按照代碼的前後順序執行。(單線程中指令重排序的結果和按照代碼順序執行的最終結果一致)

Ⅱ:JMM與有序性

​ Java提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一時刻只容許一條線程對其進行lock操做」這條規則得到的,這個規則決定了持有同一個鎖的兩個同步塊只能串行地進入,JUC下的Lock也具有這樣的效果。

happens-before原則

​ Java內存模型具有一些先天的「有序性」,即不須要經過任何手段就可以獲得保證的有序性,這個一般也稱爲 happens-before 原則。若是兩個操做的執行次序沒法從happens-before原則推導出來,那麼它們就不能保證它們的有序性,虛擬機能夠隨意地對它們進行重排序

①程序次序規則:在一個線程內,按照程序控制流(包括分支、循環)順序執行。

②鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做。

③volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做。

④線程啓動規則:Thread對象的start方法先行發生於此線程的每一個動做。

⑤線程終止規則:線程中的全部操做都先行發生於此線程的終止檢測,咱們能夠經過Thread.join()方法結束/Thread.isAlive()的返回值等手段檢測到線程已經終止執行。

⑥線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。

⑦對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於他的finalize方法的開始、

⑧傳遞性:若是操做A先行發生於操做B,操做B先行發生於操做C,那麼操做A先行發生於操做C。

volatile關鍵字

詳細介紹參考總結之Java併發編程基礎之volatile

volatile做用與原理

Ⅰ:可見性保證

​ 保證共享變量的可見性,即被volatile修飾的變量在線程之間的可見性,保證volatile變量可以被一致性的更新。在編寫多線程程序中,使用volatile修飾的共享變量在進行寫操做的時候,編譯器生成的彙編代碼中會多出一條lock#指令,這條lock指令的做用是將這個變量所在緩存行的數據寫會到系統內存(確保了若是有其餘線程對聲明瞭volatile變量進行使用,主內存中數據是更新的)。而後經過緩存一致性協議,即每一個處理器檢查本身緩存的數據是否是過時,若是發現本身緩存的數據被修改就會將緩存行的數據置爲無效狀態,當對這個數據再次使用的時候須要從主內存中從新讀入到本身的緩存中。

​ volatile的實現原則(可見性保證的實現):

​ ①將當前處理器緩存行中的數據寫回到系統內存 ​ ②這個寫回內存的操做會使得其餘CPU裏緩存了該內存地址的數據無效,當須要使用的時候再也不從緩存中獲取,而是直接從系統內存中獲取。

Ⅱ:禁止指令重排序

​ lock前綴指令實際上至關於一個內存屏障(也成內存柵欄),它確保**指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;**確保了在執行到這個內存屏障時前面的指令所有執行完。

volatile的一個簡單例子

public class TestVolatile1 {

    private volatile static int INIT_VALUE = 0;
    private final static int MAX_VALUE = 5;

    public static void main(String[] args) {

        //讀取volatile類型的變量INIT_VALUE
        new Thread("READER-THREAD ") {
            @Override
            public void run() {
                int localValue = INIT_VALUE;
                while (localValue < MAX_VALUE) {
                    if (localValue != INIT_VALUE) {
                        System.out.printf("[%s]: The INIT_VALUE update to [%d]\n", Thread.currentThread().getName(), INIT_VALUE);
                        localValue = INIT_VALUE;
                    }
                }
            }
        }.start();

        //更新volatile類型的變量MAX_VALUE
        new Thread("UPDATER-THREAD") {
            @Override
            public void run() {
                int localValue = INIT_VALUE;
                while (INIT_VALUE < MAX_VALUE) {
                    System.out.printf("[%s]: Update the value to [%d]\n", Thread.currentThread().getName(), ++localValue);
                    INIT_VALUE = localValue;
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}
//輸出結果
/* [UPDATER-THREAD]: Update the value to [1] [READER-THREAD ]: The INIT_VALUE update to [1] [UPDATER-THREAD]: Update the value to [2] [READER-THREAD ]: The INIT_VALUE update to [2] [UPDATER-THREAD]: Update the value to [3] [READER-THREAD ]: The INIT_VALUE update to [3] [UPDATER-THREAD]: Update the value to [4] [READER-THREAD ]: The INIT_VALUE update to [4] [UPDATER-THREAD]: Update the value to [5] [READER-THREAD ]: The INIT_VALUE update to [5] */

複製代碼

volatile的典型應用場景

①開關控制

public class TestVolatile3 extends Thread {

    //(1)利用volatile的可見性保證,當其餘線程調用close方法的時候,TestVolatile3線程可以馬上感知到shutdown
    //的變化(別的線程更改了volatile類型的shutdown的值,由緩存一致性協議,TestVolatile3線程將本地內存中的
    //shutdown置爲無效,並沖洗從主內存中獲取)
    //(2)若是沒有使用volatile修飾,那麼可能當別的線程修改了shutdown的值以後,沒有及時的刷新到主內存中,或者
    //說TestVolatile3線程一直使用的是本地線程的變量值(由於對shutdown一直是讀的操做,因此可能不會訪問主存獲取
    //刷新後的值)
    private volatile boolean shutdown = false;

    @Override
    public void run() {
        while(shutdown) {
            //do work
        }
    }

    public void close() {
        this.shutdown = true;
    }
}

複製代碼

②狀態標記

//(1)這是一段僞代碼,其中使用不帶volatile的boolean類型的變量表示context是否被加載過,在單線程下無論怎樣進行
//指令重排序確定是沒有問題的,都會返回context。
//(2)可是若是在多線程下,若是存在指令重排序,比入在threadA將initialized = true排序到了context = 
//loadContext()前面,而且initialized也被刷新到了主內存,threadB剛恰好從新從主內存加載到initialized的值爲
//true,就在if判斷的時候直接返回context,可是問題來了,在threadA中context是未被初始化完畢的,那麼threadB使
//用context不就會出現問題了嗎
//(3)可是若是使用了volatile修飾以後,就會禁止指令重排序,從上面的原理能夠獲得,context = loadContext()不能
//被排序到initialized = true以前,即確保了執行到initialized = true的時候,context已經初始化完畢。
public class TestVolatile4 {
    private volatile boolean initialized = false;

    private Context context;

    public Context load() {
        if (!initialized) {
            context = loadContext();
            initialized = true;
        }
        return context;
    }

    private Context loadContext() {
        return new Context();//這裏僅僅是一個示例,實際上loadContext可能會作不少事情
    }
    
    class Context {
        //好比讀取一些配置文件操做
        //do sth
    }
}

複製代碼

③double-check結合volatile實現線程安全的單例模式(後面的單例模式部分會介紹這種實現)

synchronized和volatile的區別

使用上的區別

①volatile只能用於修飾實例變量或者類變量,不能修飾方法以及方法參數和局部變量、常量等;

②synchronized關鍵字不能用於對變量的修飾,只能用於修飾方法或者代碼塊;

③volatile修飾的變量能夠爲null,可是synchronized關鍵字同步塊的monitor不能爲null。

對原子性的保證

①volatile不能保證原子性;

②synchronized是一種排他的機制,因此是能夠保證原子性的。

對可見性的保證

①兩者均可以保證共享資源在多線程間的可見性,可是實現機制不一樣;

②synchronized使用JVM提供的指令monitor enter和monitor exit經過排他的方式實現同步代碼塊的串行執行。在monitor exit以後,會將共享資源刷新到主內存中;

③volatile使用機器指令(帶有lock#前綴的指令)迫使其餘線程工做內存中的數據失效,須要到主內存中從新加載.

對有序性的保證

①volatile禁止編譯器以及處理器的指令重排序,可以保證有序性;

②synchronized能保證有序性,可是這種有序性至關於屏蔽掉外部線程的感知。具體而言就是:多線程之間串行化的執行,獲取到monitor的線程。在執行synchronized同步塊的時候,同步塊中的指令仍是能夠進行重排序的,可是由於串行化,因此別的線程執行不會由於這種重排序出現問題。

阻塞與非阻塞

①volatile不會使線程阻塞;

②synchronized會使線程阻塞(獲取monitor失敗的時候)。

單例模式的幾種實現

當即加載/"餓漢模式"

//餓漢式是線程安全的,由於虛擬機保證只會裝載一次,使用的時候是已經加載初始化好的instance。
public class SingletonPattern1 {

    private static final SingletonPattern1 instance = new SingletonPattern1();

    private SingletonPattern1() {
        //do sth about init
    }

    public static SingletonPattern1 getInstance() {
        return instance;
    }
}
複製代碼

延遲加載/"懶漢模式"

①線程不安全的懶漢模式

//線程不安全的懶漢式
public class SingletonPattern2 {
    private static SingletonPattern2 instance;

    private SingletonPattern2() {
        //do sth about init
    }

    public static SingletonPattern2 getInstance() {
        //假設兩個線程執行thread一、thread2;
        //(1)其中thread1執行到if判斷的時候instance爲null,這時候剛好由於線程調度thread1失去執行權; 
        //(2)thread2也執行到此處,並進入if代碼塊new一個instance,而後返回;
        //(3)thread1再次今後處執行,由於判斷到的instance爲null,也一樣new一個instance而後返回;
        //(4)這樣thread1和thread2的instance實際上並非相同的了
        if(null == instance) {
            instance = new SingletonPattern2();
        }
        return SingletonPattern2.instance;
    }
}
複製代碼

②使用synchronized解決

//使用synchronized的線程安全的懶漢式
public class SingletonPattern2 {
    private static SingletonPattern2 instance;

    private SingletonPattern2() {
        //do sth about init
    }
	//缺點:實際上只有一個線程是寫的操做(得到monitor以後new一個instance),後面的線程由於由於已經建立了instance,就是至關於讀的操做,可是read的操做仍是加鎖同步了(串行化了),效率較低
    public synchronized static SingletonPattern2 getInstance() {
        if(null == instance) {
            instance = new SingletonPattern2();
        }
        return SingletonPattern2.instance;
    }
}
複製代碼

③使用DCL機制(無volatile)

//double check的方式
public class SingletonPattern3 {
    private static SingletonPattern3 instance;

    private SingletonPattern3() {
        //do sth about init
    }

    public static SingletonPattern3 getInstance() {
        //假設兩個線程執行thread一、thread2,都判斷instance爲null
        if (null == instance) {
            synchronized (SingletonPattern3.class) {
                //(1)其中thread1進入同步代碼塊以後,new了一個instance
                //(2)在thread1退出同步代碼塊以後,thread2進入同步代碼塊,由於instance不爲null,因此直接退出同步塊,返回建立好的instance
                if(null == instance) {
                    instance = new SingletonPattern3();
                }
            }
        }
        //(3)如今已經建立好了instace,後面的線程在調用getInstance()方法的時候就會直接到此處,返回instance
        return SingletonPattern3.instance;
    }
}
複製代碼

④使用volatile的DCL

//double check + volatile的方式
public class SingletonPattern3 {
    
    //volatile禁止指令重排序(happens-before中有一條volatile變量規則:對一個volatile變量的寫的操做先行發生與對這個變量的讀操做)
    private volatile static SingletonPattern3 instance;

    //SingletonPattern3其餘的一些引用類型的屬性PropertyXXX propertyxxx;
    
    private SingletonPattern3() {
        //do sth about init
    }

    public static SingletonPattern3 getInstance() {
        if (null == instance) {
            synchronized (SingletonPattern3.class) {
                if(null == instance) {
                    //instance = new SingletonPattern3();這句,這裏看起來是一句話,但實際上它並非一個原
                    //子操做,在被編譯後在JVM執行的對應彙編代碼作了大體3件事情: 
                    //(1)分配內存
                    //(2)調用構造器
                    //(3)將instance對象指向分配的內存空間首地址(這時候的instance不爲null)
                    //但因爲指令重排序(Java編譯器容許處理器亂序執行)的存在,上面三個步驟多是1-2-3也多是
                    //1-3-2,注意,若是是1-3-2的狀況就可能出現問題,咱們來分析一下可能出現的問題:
                    //I:假設如今兩個線程thread一、thread2,如今threa1獲取到monitor,而後按照上面的1-3-2執行,
                    //II:假設在3執行完畢、2未執行以前(或者說2只執行一部分,SingletonPattern3中的引用類型
                    //的屬性一部分仍是null),這個時候切換到thread2上,
                    //III:這時候instance由於已經在thread1內執行過了(3),instance已是非空了,因此
                    //thread2直接拿走instance,而後使用,可是實際上instance指向的內存地址並無調用構造器
                    //初始化的,這就可能會出現問題了。
                    instance = new SingletonPattern3();
                }
            }
        }
        return SingletonPattern3.instance;
    }
}
複製代碼

Holer方式

參照下面的介紹Holder方式實現單例的原理

枚舉方式

//枚舉方式實現
public class SingletonPattern5 {

    //枚舉型不容許被繼承、是線程安全的,只被實例化一次(一樣下面使用相似Holder的方式)
    private enum EnumHolderSingleton {
        INSTANCE;

        private final SingletonPattern5 instance;

        EnumHolderSingleton() {
            this.instance = new SingletonPattern5();
        }

        private SingletonPattern5 getInstance() {
            return instance;
        }
    }

    public SingletonPattern5 getInstance() {
        return EnumHolderSingleton.INSTANCE.getInstance();
    }
}
複製代碼

Holder方式實現單例的原理

​ 其中關於主動引用參考總結的JVM之虛擬機類加載機制中講到的主動引用和被動引用。咱們下面會專門詳解這種方式的原理。首先咱們看看這種方式的實現

//Holder方式:延遲加載、不加鎖、線程安全
public class SingletonPattern4 {

    private SingletonPattern4() {
        //do sth about init
    }

    //在靜態內部類中,有SingletonPattern4的實例,而且直接被初始化
    private static class Holder {
        private static SingletonPattern4 instance = new SingletonPattern4();
    }

    //返回的是Holer的靜態成員instance
    public static SingletonPattern4 getInstance() {
        //在SingletonPattern4中沒有instance的靜態成員,而是將其放到了靜態內部類Holder之中,所以在
        //SingletonPattern4類的初始化中並不會建立Singleton(延遲加載)的實例,Holder中定義了
        //SingletonPattern4的靜態變量,而且直接進行了初始化。當Holder被主動引用的時候會建立
        //SingletonPattern4的實例,SingletonPattern4實例的建立過程在Java程序編譯時候由同步方法<clinit>()
        //方法執行,保證內存的可見性、JVM指令的順序性和原子性。
        return Holder.instance;
    }
}
複製代碼

​ 在《深刻理解Java虛擬機》的虛擬機類加載機制哪一章中咱們知道類加載大體分爲加載、連接(驗證、準備、解析)、初始化這幾個階段,而上面這種方式的實現主要就是利用初始化階段的一些規則來保證的。

​ JVM 在類的初始化階段(即在 Class 被加載後,且被線程使用以前),會執行類的初始化(初始化階段執行了類構造器<clinit()>方法)。關於<clinit()>方法的其餘特色和細節咱們暫不作討論,這裏咱們關注其中一點:即JVM保證一個類的<clinit()>方法在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只有一個類可以執行這個類的<clinit()>方法,其餘的線程都須要阻塞等待。好比下面的例子

public class TestClinitThreadSafety {

    public static void main(String[] args) {

        new Thread() {
            @Override
            public void run() {
                new SimpleObject();
            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                new SimpleObject();
            }
        }.start();
    }

    private static class SimpleObject {
        private static AtomicBoolean flag = new AtomicBoolean(true);
        static {
            System.out.println(Thread.currentThread().getName() + " init this static code.");
            while(flag.get()) {
                //do nothing
                //在多線程環境中若是同時去對這個類進行初始化,只有一個線程可以執行到這裏。
            }
            System.out.println(Thread.currentThread().getName() + " finish this static code.");
        }
    }
}
複製代碼

​ 咱們經過下面的圖來講明。Java 語言規範規定,對於每個類或接口 C,都有一個惟一的初始化鎖 LC 與之對應。從 C 到 LC 的映射,由 JVM 的具體實現去自由實現。JVM 在類初始化期間會獲取這個初始化鎖,而且每一個線程至少獲取一次鎖來確保這個類已經被初始化過了(即每一個線程若是要使用這個類,去初始化的時候都會得到這把鎖確認初始化過了)。大概的處理過程以下:

​ ①經過在 Class 對象上同步(即獲取 Class 對象的初始化鎖),來控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程可以獲取到這個初始化鎖。假設 Class 對象當前尚未被初始化(初始化狀態 state 此時被標記爲 state = false),且有兩個線程 A 和 B 試圖同時初始化這個 Class 對象。

​ ②線程 A 獲取到初始化鎖以後,執行類的初始化,同時線程 B 在初始化鎖對應的初始化鎖上等待。

​ ③線程 A 執行完畢,將初始化狀態設置爲 state = true,而後喚醒在初始化鎖上等待的全部線程。

​ ④線程B(這裏統一表示其餘競爭這個初始化鎖的線程)讀取state=true,結束初始化退出(同一個類加載器下,保證一個類型只被初始化一次)。

相關文章
相關標籤/搜索