摘要:目的是解決由于多線程通過共享內存進行通信時,存在的原子性可見性緩存一致性以及有序性問題。最多只有一個線程能持有鎖。線程加入規則對象的結束先行發生于方法返回。
前言
學習情況記錄
時間:week 1
SMART子目標 :Java 多線程
學習Java多線程,要了解多線程可能出現的并發現象,了解Java內存模型的知識是必不可少的。
對學習到的重要知識點進行的記錄。
注:這里提到的是Java內存模型,是和并發編程相關的,不是JVM內存結構(堆、方法棧這些概念),這兩個不是一回事,別弄混了。
Java 內存模型Java內存模型(Java Memory Model ,JMM)就是一種符合內存模型規范的,屏蔽了各種硬件和操作系統的訪問差異的,保證了Java程序在各種平臺下對內存的訪問都能得到一致效果的機制及規范。目的是解決由于多線程通過共享內存進行通信時,存在的原子性、可見性(緩存一致性)以及有序性問題。主內存與工作內存
先看計算機硬件的緩存訪問操作:
? 處理器上的寄存器的讀寫的速度比內存快幾個數量級,為了解決這種速度矛盾,在它們之間加入了高速緩存。
? 加入高速緩存帶來了一個新的問題:緩存一致性。如果多個緩存共享同一塊主內存區域,那么多個緩存的數據可能會不一致,需要一些協議來解決這個問題。
Java的內存訪問操作與上述的硬件緩存具有很高的可比性:
? Java內存模型中,規定了所有的變量都存儲在主內存中,每個線程還有自己的工作內存,工作內存存儲在高速緩存或者寄存器中,保存了該線程使用的變量的主內存副本拷貝。線程只能直接操作工作內存中的變量,不同線程之間的變量值傳遞需要通過主內存來完成。
內存間交互操作Java 內存模型定義了 8 個操作來完成主內存和工作內存的交互操作
read:把一個變量的值從主內存傳輸到線程的工作內存中
load:在 read 之后執行,把 read 得到的值放入線程的工作內存的變量副本中
use:把線程的工作內存中一個變量的值傳遞給執行引擎
assign:把一個從執行引擎接收到的值賦給工作內存的變量
store:把工作內存的一個變量的值傳送到主內存中
write:在 store 之后執行,把 store 得到的值放入主內存的變量中
lock:作用于主內存的變量,把一個變量標識成一條線程獨占的狀態
unlock: 作用于主內存的變量,把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
內存模型三大特性 原子性Java 內存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 類型的變量執行 assign 賦值操作,這個操作就是原子性的。但是 Java 內存模型允許虛擬機將沒有被 volatile 修飾的 64 位數據(long,double)的讀寫操作劃分為兩次 32 位的操作來進行,也就是說基本數據類型的訪問讀寫是原子性的,除了long和double是非原子性的,即 load、store、read 和 write 操作可以不具備原子性。書上提醒我們只需要知道有這么一回事,因為這個是幾乎不可能存在的例外情況。
雖然上面說對基本數據類型的訪問讀寫是原子性的,但是不代表在多線程環境中,如int類型的變量不會出現線程安全問題。詳細的例子可以參考范例一。
想要保證原子性,可以嘗試以下幾種方式:
如果是基礎類型的變量的話,使用Atomic類(例如AtomicInteger)
其他情況下,可以使用synchronized互斥鎖來保證 限定臨界區 內操作的原子性。它對應的內存間交互操作為:lock 和 unlock,在虛擬機實現上對應的字節碼指令為 monitorenter 和 monitorexit。
可見性可見性指的是,當一個線程修改了共享變量中的值,其他線程能夠立即得知這個修改。Java 內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值來實現可見性的。
可見性的錯誤問題范例比較難以模擬,有興趣的可以借助此篇文章更好的理解。
想要保證可見性,主要有三種實現方式:
volatile
Java的內存分主內存和線程工作內存,volatile保證修改立即由當前線程工作內存同步到主內存,但其他線程仍需要從主內存取才能保證線程同步。
synchronized
當線程獲取鎖時會從主內存中獲取共享變量的最新值,釋放鎖的時候會將共享變量同步到主內存中。最多只有一個線程能持有鎖。
final
被 final 關鍵字修飾的字段在構造器中一旦初始化完成,并且沒有發生 this 逃逸(其它線程通過 this 引用訪問到初始化了一半的對象),那么其它線程就能看見 final 字段的值。
范例一中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,因為 volatile 并不能保證操作的原子性。
有序性有序性是指:在本線程內觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因為發生了指令重排序。在 Java 內存模型中,允許編譯器和處理器對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會影響到多線程并發執行的正確性。
想要保證可見性,主要以下實現方式:
volatile
volatile的真正意義在于產生內存屏障,禁止指令重排序。即重排序時不能把后面的指令放到內存屏障之前。
synchronized
它保證每個時刻只有一個線程執行同步代碼,相當于是讓線程順序執行同步代碼。
有序性這塊比較難比較深的內容實際上是指令重排序這塊的知識。我這就借花獻佛,引一篇我認為講的比較清楚的文章。內存模型之重排序
先行發生原則JVM 內存模型下,規定了先行發生原則,讓一個操作無需任何同步器協助就能先于另一個操作完成。如果兩個操作之間的關系不在此列,并且無法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對他們隨意的進行重排序。
單一線程規則 - Single Thread Rule
在其他書上又叫 Program Order Rule - 程序次序規則
在一個線程中, 在線程前面的操作先行發生于后面的操作。(準確的來說,是控制流順序,而不是代碼順序,因為或有邏輯判斷分支)
管道鎖定規則 - Monitor Lock Rule
一個 unlock 操作先行發生于后面對同一個鎖的 lock 操作。
volatile 變量規則 - Volatile Variable Rule
對一個volatile 變量的寫操作先行發生于 后面對這個變量的讀操作
線程啟動規則 - Thread Start Rule
Thread 對象的 start() 方法調用先行發生于此線程的每一個動作。
線程加入規則 - Thread Join Rule
Thread 對象的結束先行發生于 join() 方法返回。
線程中斷規則 - Thread Interruption Rule
對線程 interrupt() 方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過 interrupted() 方法檢測到是否有中斷發生。
對象終結規則- Finalizer Rule
一個對象的初始化完成(構造函數執行結束)先行發生于它的 finalize() 方法的開始。
傳遞性 - Transitivity
如果操作 A 先行發生于操作 B,操作 B 先行發生于操作 C,那么操作 A 先行發生于操作 C。
在多線程情況下,時間先后順序和先行發生原則之間基本沒有太大的關系,我們衡量并發安全問題的時候不要受到時間順序的告饒,一切必須以先行發生原則為準。
插入案例幫助理解 案例一 代碼/** * 內存模型三大特性 - 原子性驗證對比 * * @author Richard_yyf * @version 1.0 2019/7/2 */ public class AtomicExample { private static AtomicInteger atomicCount = new AtomicInteger(); private static int count = 0; private static void add() { atomicCount.incrementAndGet(); count++; } public static void main(String[] args) { final int threadSize = 1000; final CountDownLatch countDownLatch = new CountDownLatch(threadSize); ExecutorService executor = Executors.newCachedThreadPool(); for (int i = 0; i < threadSize; i++) { executor.execute(() -> { add(); countDownLatch.countDown(); }); } System.out.println("atomicCount: " + atomicCount); System.out.println("count: " + count); ThreadPoolUtil.tryReleasePool(executor); } }Outout
atomicCount: 1000 count: 997分析
可以借助下圖幫助理解。
count++這個簡單的操作根據上面的原理分析,可以知道內存操作實際分為讀寫存三步;因為讀寫存這個整體的操作,不具備原子性,count被兩個或多個線程讀入了同樣的舊值,讀到線程內存當中,再進行寫操作,再存回去,那么就可能出現主內存被重復set同一個值的情況,如上圖所示,兩個線程進行了count++,實際上只進行了一次有效操作。
案例二 代碼class Foo { private int x = 100; public int getX() { return x; } public int fix(int y) { x = x - y; return x; } } public class MyRunnable implements Runnable { private Foo foo =new Foo(); public static void main(String[] args) { MyRunnable r = new MyRunnable(); Thread ta = new Thread(r,"Thread-A"); Thread tb = new Thread(r,"Thread-B"); ta.start(); tb.start(); } public void run() { for (int i = 0; i < 3; i++) { this.fix(30); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " :當前foo對象的x值= " + foo.getX()); } } public int fix(int y) { return foo.fix(y); } }Output
Thread-A:當前foo對象的的x值= 70 Thread-B:當前foo對象的的x值= 70 Thread-A:當前foo對象的的x值= 10 Thread-B:當前foo對象的的x值= 10 Thread-A:當前foo對象的的x值= -50 Thread-B:當前foo對象的的x值= -50分析
這個案例是案例一的變體,只是代碼有點復雜有點繞而已,實際上就是存在兩個線程,對一個實例的共享變量進行-30的操作。
read 的操作發生在x-y的x處,相當于兩個線程第一次fix(30)的時候,對x變量做了兩次100-30的賦值操作。
案例三public class Test { // 是否是原子性? int i = 1; public static void main(String[] args) { Test test = new Test(); } }
請問上述 int i = 1是否是原子性的呢?
實際上很微妙。
本案例中的int a = 1在java中叫顯式初始化,它實際上包含兩次賦值,第一次java自動將a初始化為0,第二次再賦值為1。從這個角度看,這條語句包含了兩步操作,并不是原子的。
但是由于這句代碼是在構造方法中,而從類的實例化角度看,一般認為構造方法中對當前實例的初始化過程是原子的。這是因為在實例化完成之前,一般是無法從別的代碼中訪問到當前實例的。所以從這個角度看,int a = 1實際上是原子的。
參考《深入理解Java虛擬機》
https://juejin.im/post/5bd971...
http://ifeve.com/concurrency-...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/75208.html
摘要:前言學習情況記錄時間子目標多線程記錄在學習線程安全知識點中,關于的有關知識點。對于資源競爭嚴重線程沖突嚴重的情況,自旋的概率會比較大,從而浪費更多的資源,效率低于。 前言 學習情況記錄 時間:week 1 SMART子目標 :Java 多線程 記錄在學習線程安全知識點中,關于CAS的有關知識點。 線程安全是指:多個線程不管以何種方式訪問某個類,并且在主調代碼中不需要進行同步,都能表...
摘要:底層使用的是雙向鏈表數據結構之前為循環鏈表,取消了循環??焖匐S機訪問就是通過元素的序號快速獲取元素對象對應于方法。而接口就是用來標識該類支持快速隨機訪問。僅僅是起標識作用。,中文名為雙端隊列。不同的是,是線程安全的,內部使用了進行同步。 前言 學習情況記錄 時間:week 2 SMART子目標 :Java 容器 記錄在學習Java容器 知識點中,關于List的需要重點記錄的知識點。...
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
摘要:因為管理人員是了解手下的人員以及自己負責的事情的。處理器優化和指令重排上面提到在在和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。有沒有發現,緩存一致性問題其實就是可見性問題。 網上有很多關于Java內存模型的文章,在《深入理解Java虛擬機》和《Java并發編程的藝術》等書中也都有關于這個知識點的介紹。但是,很多人讀完之后還是搞不清楚,甚至有的人說自己更懵了。本文,就來整體的...
閱讀 2260·2023-04-25 14:50
閱讀 1234·2021-10-13 09:50
閱讀 1866·2019-08-30 15:56
閱讀 1839·2019-08-29 15:29
閱讀 2886·2019-08-29 15:27
閱讀 3548·2019-08-29 15:14
閱讀 1192·2019-08-29 13:01
閱讀 3299·2019-08-26 14:06