摘要:支持多線程,中創建線程的方式有兩種繼承類,重寫方法。多線程編程很常見的情況下是希望多個線程共享資源,通過多個線程同時消費資源來提高效率,但是新手一不小心很容易陷入一個編碼誤區。所以,在進行多線程編程的時候一定要留心多個線程是否共享資源。
文章首發于 http://jaychen.cc
作者 JayChen
最近開始學習 Java,所以記錄一些 Java 的知識點。這篇是一些關于 Java 線程的文章。
Java 支持多線程,Java 中創建線程的方式有兩種:
繼承 Thread 類,重寫 run 方法。
實現 Runnable 接口,實現 run 方法。
// 繼承 Thread 類 class ThreadDemo extends Thread { @Override public void run() { System.out.println("一個簡單的例子就需要這么多代碼..."); } } // 實現 Runnable 接口 class RunnableDemo implements Runnable { public void run() { System.out.println("一個簡單的例子就需要這么多代碼..."); } } public class Main { public static void main(String[] strings) { // 繼承 Thread 類 Thread thread = new ThreadDemo(); thread.start(); // 實現 Runnable 接口 Thread again = new Thread(new RunnableDemo()); again.start(); } }
通過調用 start 函數可以啟動有一個新的線程,并且執行 run 方法中的邏輯。這里可以引出一個很容易被問道的面試題:
Thread 類中 start 函數和 run 函數有什么區別。
最明顯的區別在于,直接調用 run 方法并不會啟動一個新的線程來執行,而是調用 run 方法的線程直接執行。只有調用 start 方法才會啟動一個新的線程來執行。
引入線程的目的是為了使得多個線程可以在多個 CPU 上同時運行,提高多核 CPU 的利用率。
多線程編程很常見的情況下是希望多個線程共享資源,通過多個線程同時消費資源來提高效率,但是新手一不小心很容易陷入一個編碼誤區。
class ThreadDemo extends Thread { private int i = 3; @Override public void run() { i--; System.out.println(i); } } public class Main { public static void main(String[] strings) { Thread thread = new ThreadDemo(); thread.start(); Thread thread1 = new ThreadDemo(); thread1.start(); Thread thread2 = new ThreadDemo(); thread2.start(); } }
上面的實例代碼,希望通過 3 個線程同時執行 i--; 操作,使得最終 i 的值為 0,但是結果不如人意,3 次輸出的結果都為 2。這是因為在 main 方法中創建的三個線程都獨自持有一個 i ,我們的目的一應該是 3 個線程共享一個 i。
public class Main { public static void main(String[] strings) { DemoRunnable demoRunnable = new DemoRunnable(); new Thread(demoRunnable).start(); new Thread(demoRunnable).start(); new Thread(demoRunnable).start(); } } class DemoRunnable implements Runnable { private int i= 3; @Override public void run() { i--; System.out.println(i); } }
使用上面的代碼才有可能使得 i 最終的結果為0。所以,在進行多線程編程的時候一定要留心多個線程是否共享資源。
Volatile如果你運氣好,執行上面的代碼發現,有時候三次 i--; 的結果也不一定是 0。這種怪異的現象需要從 JVM 的內存模型說起。
當 Java 啟動了多個線程分布在不同的 CPU 上執行邏輯,JVM 為了提高性能,會把在內存中的數據拷貝一份到 CPU 的寄存器中,使得 CPU 讀取數據更快。很明顯,這種提高性能的做法會使得 Thread1 中對 i 的修改不能馬上反應到 Thread2 中。
下面例子可以明顯的體現出這個問題。
public class Main { static int NEXT_IN_LINE = 0; public static void main(String[] args) throws Exception { new ThreadA().start(); new ThreadB().start(); } static class ThreadA extends Thread { @Override public void run() { while (true) { if (NEXT_IN_LINE >= 4) { break; } } System.out.println("in CustomerInLine...." + NEXT_IN_LINE); } } static class ThreadB extends Thread { @Override public void run() { while (NEXT_IN_LINE < 10) { System.out.println("in Queue ..." + NEXT_IN_LINE++); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
上面的代碼中,ThreadA 線程進入死循環一直到 NEXT_IN_LINE 的值為 4 才退出,ThreadB 線程不停的對 NEXT_IN_LINE++ 操作。然而執行代碼發現 ThreadA 沒有輸出 in CustomerInLine...." + NEXT_IN_LINE,而是一直處于死循環狀態。這個例子可以很明顯的驗證:"JVM 會把線程共享的變量拷貝到寄存器中以提高效率" 的說法。
那么,怎么才能避免這種優化給編程帶來的困擾?這里要引出一個內存可見性 的概念。
內存可見性指的是一個線程對共享變量值的修改,能夠及時地被其他線程看到。
為了實現內存可見性,Java 引入了 volatile 的關鍵字。這個關鍵字的作用在于,當使用 volatile 修改了某個變量,那么 JVM 就不會對該變量進行優化,即意味著,不會把該變量拷貝到 CPU 寄存器中,每個變量對該變量的修改,都會實時的反應在內存中。
針對上面的例子,把 static int NEXT_IN_LINE = 0; 改成 static volatile int NEXT_IN_LINE = 0; 那么執行的結果就如我們所預料的,在 ThraedB 自增到 NEXT_IN_LINE = 4 的時候 ThreadA 會跳出死循環。
指令重排volatile 還有一個很好玩的特性:防止指令重排。
首先要明白什么是指令重排?
假設在 ThreadA 中有
context = loadContext(); inited = true;
ThreadB 中
while(!inited) { sleep(100); } doSomething(context);
那么,ThreadB 中會在 inited 置位 true 之后執行 doSomething 方法,inited 變量的作用就是用來標志 context 是否被初始化了。但是實際上在執行 ThreadA 代碼的時候 JVM 會根據上下行代碼是否互相關聯而決定是否對代碼執行順序進行重排。這就意味著 CPU 認為 ThreadA 中的兩行代碼沒有順序關聯,于是先執行 inited=true 再執行 context=loadContext()。如此一來,就會導致 ThreadB 中引用了一個值為 null 的 context 對象。
使用 volatile 可以避免指令重排。在定義 inited 變量的時候使用 olatile修飾:volatile boolean inited = false;。 使用 volatile 修飾 inited 之后,JVM 就不會對 inited 相關的變量進行指令重排。
原子性回到最初的例子。在 volatile 部分我們說過最終的結果不是輸出 i = 0 的原因是 JVM 拷貝內存變量到 CPU 寄存器中導致線程之間沒辦法實時更新 i 變量的值導致的,只要使用 volatile 修飾 i 就可以實現內存可見性,可以使得結果輸出 i = 0。但是實際上,即使使用了 volatile 之后,還是有可能的導致 i != 0 的結果。
輸出 i != 0 的結果是由于 i++; 操作并非為原子性操作。
什么是原子性操作?簡單來說就是一個操作不能再分解。i++ 操作實際上分為 3 步:
讀取 i 變量的值。
增加 i 變量的值。
把新的值寫到內存中。
那么,假設 ThraedA 在執行第 2 步之后,ThreadB 讀取了 i 變量的值,這時候還未被 ThreadA 更新,讀取的仍是舊的值,之后 ThreadA 寫入了新的值。這種情況下就會導致 i 在某個時刻被修改多次。
解決這種問題需要用到 synchronized。但是這里不打算對 synchronized 進行討論。這里指出一個很容易被誤解的概念:volatile 能夠實現內存可見性和避免指令重排,但是不能實現原子性。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70308.html
摘要:變量可見性問題的關鍵字保證了多個線程對變量值變化的可見性。只要一個線程需要首先讀取一個變量的值,基于這個值生成一個新值,則一個關鍵字不足以保證正確的可見性。 Java的volatile關鍵字用于標記一個Java變量為在主存中存儲。更確切的說,對volatile變量的讀取會從計算機的主存中讀取,而不是從CPU緩存中讀取,對volatile變量的寫入會寫入到主存中,而不只是寫入到CPU緩存...
摘要:并發編程關鍵字解析解析概覽內存模型的相關概念并發編程中的三個概念內存模型深入剖析關鍵字使用關鍵字的場景內存模型的相關概念緩存一致性問題。事實上,這個規則是用來保證程序在單線程中執行結果的正確性,但無法保證程序在多線程中執行的正確性。 Java并發編程:volatile關鍵字解析 1、解析概覽 內存模型的相關概念 并發編程中的三個概念 Java內存模型 深入剖析volatile關鍵字 ...
摘要:最近在看多線程相關,看到這篇來自大神關于關鍵字的講解感覺非常詳細易懂,特此轉載一下。如果對增加聲明則所有線程對的寫都會立即刷新到主存中,而且所有對的讀也都直接從主存中去讀。 最近在看java多線程相關,看到這篇來自大神Jakob Jenkov關于Volatile關鍵字的講解感覺非常詳細易懂,特此轉載一下。原文鏈接:http://tutorials.jenkov.com/j... 內存可...
摘要:每個會緩存主存的共享變量,從而提高處理效率。為當前緩存行加入緩存一致性協議。任何修改,其他線程是可見的。修飾的變量還是會緩存的,只是通過一系列處理保證了所有線程看到這個變量的值是一致的 java并發編程實戰對volatile的解釋就是:當一個域聲明為valatile類型后,編譯器與運行時會監視這個變量:它是共享的,而且對它的操作不會與其他的內存操作一起被重排序。volatile變量不會...
摘要:今天開始整理學習多線程的知識,談談最重要的兩個關鍵字和。但是這樣一個過程比較慢,在使用多線程的時候就會出現問題。有序性有序性是指多線程執行結果的正確性。這種機制在多線程中會出現問題,因此可以通過來禁止重排。 今天開始整理學習多線程的知識,談談最重要的兩個關鍵字:volatile和synchronized。 一、三個特性 1、原子性 所謂原子性操作就是指這些操作是不可中斷的,要么執行過程...
閱讀 3872·2021-09-27 13:35
閱讀 1069·2021-09-24 09:48
閱讀 2899·2021-09-22 15:42
閱讀 2339·2021-09-22 15:28
閱讀 3145·2019-08-30 15:43
閱讀 2609·2019-08-30 13:52
閱讀 2971·2019-08-29 12:48
閱讀 1451·2019-08-26 13:55