工做中許多地方須要涉及到多線程的設計與開發,java多線程開發當中咱們爲了線程安全所作的任何操做其實都是圍繞多線程的三個特性:原子性、可見性、有序性展開的。針對這三個特性的資料網上已經不少了,在這裏我但願在站在便於理解的角度,用相對直觀的方式闡述這三大特性,以及爲何要實現和知足三大特性。java
1、原子性數組
原子性是指一個操做或者一系列操做要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。其實這句話就是在告訴你,若是有多個線程執行相同一段代碼時,而你又可以預見到這多個線程相互之間會影響對方的執行結果,那麼這段代碼是不知足原子性的。結合到實際開發當中,若是代碼中出現這種狀況,大機率是你操做了共享變量。安全
針對這個狀況網上有個很經典的例子,銀行轉帳問題:多線程
好比A和B同時向C轉帳10萬元。若是轉帳操做不具備原子性,A在向C轉帳時,讀取了C的餘額爲20萬,而後加上轉帳的10萬,計算出此時應該有30萬,但還將來及將30萬寫回C的帳戶,此時B的轉帳請求過來了,B發現C的餘額爲20萬,而後將其加10萬並寫回。而後A的轉帳操做繼續——將30萬寫回C的餘額。這種狀況下C的最終餘額爲30萬,而非預期的40萬。 若是A和B兩個轉帳操做是在不一樣的線程中執行,而C的帳戶就是你要操做的共享變量,那麼不保證執行操做原子性的後果是十分嚴重的。併發
OK,上面的情況咱們理清楚了,由此能夠引伸出下列三個問題app
一、哪些是共享變量ide
從JVM內存模型的角度上講,存儲在堆內存上數據都是線程共享的,如實例化的對象、全局變量、數組等。存儲在線程棧上的數據是線程獨享的,如局部變量、操做棧、動態連接、方法出口等信息。工具
舉個通俗的例子,若是你的執行方法至關於作菜,你能夠認爲每一個線程都是一名廚師,方法執行時會在虛擬機棧中建立棧幀,至關於給每一個廚師分配一個單獨的廚房,作菜也就是執行方法的過程當中須要不少資源,裏面的鍋碗瓢盆各類工具,就諸如你在方法內的局部變量是每一個廚師獨享的;但若是須要使用水電煤氣等公共資源,就諸如全局變量通常是共享的,使用時須要保證線程安全。優化
二、哪些是原子操做spa
既然是要保證操做的原子性,如何判斷個人操做是否符合原子性呢,一段代碼確定是不符合原子性的,由於它包含不少步操做。但若是隻是一行代碼呢,好比上面的銀行轉帳的例子若是沒有這麼複雜,共享變量「C的帳戶」只是一個簡單的count++操做呢?針對這個問題,首先咱們要明確,看起來十分簡單的一句代碼,在JMM(java線程內存模型)中多是須要多步操做的。
先來看一個經典的例子:使用程序實現一個計數器,指望獲得的結果是1000,代碼以下:
public class threadCount { public volatile static int count = 0; public static void main( String[] args ) throws InterruptedException { ExecutorService threadpool = Executors.newFixedThreadPool(1000); for (int i = 0; i < 1000; i++) { threadpool.execute(new Runnable() { @Override public void run() { count++; } }); } threadpool.shutdown(); //保證提交的任務所有執行完畢 threadpool.awaitTermination(10000, TimeUnit.SECONDS); System.out.println(count); } }
運行程序你能夠看到,輸出的結果並不每次都是指望的1000,這正是由於count++不是原子操做,線程不安全致使的錯誤結果。
實際上count++包含2個操做,首先它先要去讀取count的值,再將count的值寫入工做內存,雖然讀取count的值以及將count的值寫入工做內存 這2個操做都是原子性操做,但合起來就不是原子性操做了。
在JMM中定義了8中原子操做,以下圖所示,原子性變量操做包括read、load、assign、use、store、write,其實你能夠理解爲只有JMM定義的一些最基本的操做是符合原子性的,若是須要對代碼塊實行原子性操做,則須要JMM提供的lock、unlock、synchronized等來保證。
三、如何保證操做的原子性
使用較多的三種方式:
內置鎖(同步關鍵字):synchronized;
顯示鎖:Lock;
自旋鎖:CAS;
固然這三種實現方式和保證同步的機制上都有所不一樣,在這裏咱們不作深刻的說明。
2、可見性
可見性是一種複雜的屬性,由於可見性的錯誤一般比較隱蔽而且違反咱們的直覺。
咱們看下面這段代碼
public class VolatileApp { //volatile private static boolean isOver = false; private static int number = 0; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { while (!isOver) { //Thread.yield(); } System.out.println(number); } }); thread.start(); Thread.sleep(1000); number = 50; isOver = true; } }
若是你直接運行上面的代碼,那麼你永遠也看不到number的輸出的,線程將會無限的循環下去。你可能會有疑問代碼當中明明已經把isOver設置爲了false,爲何循環還不會中止呢?這正是由於多線程之間可見性的問題。在單線程環境中,若是向某個變量寫入某個值,在沒有其餘寫入操做的影響下,那麼你總能取到你寫入的那個值。然而在多線程環境中,當你的讀操做和寫操做在不一樣的線程中執行時,狀況就並不是你想象的理所固然,也就是說不知足多線程之間的可見性,因此爲了確保多個線程之間對內存寫入操做的可見性,必須使用同步機制。
咱們來看下JMM(java線程內存模型):
volatile
保證線程之間可見性的手段有多種,在上面的代碼中,咱們就能夠經過volatile修飾靜態變量來保證線程的可見性。
你能夠把volatile變量看做一種削弱的同步機制,它能夠確保將變量的更新操做通知到其餘線程;使用volatile保證可見性相比通常的同步機制更加輕量級,開銷也相對更低。
其實這裏還有另一種狀況,若是上面的代碼中你撤銷對Thread.yield()的註釋,你會發現即使沒有volatile的修飾兩個靜態變量 ,number也會正常打印輸出了,乍一看你會覺得可見性是沒有問題的,其實否則,這是由於Thread.yield()的加入,使JVM幫助你完成了線程的可見性。
下面這段段話闡述的比較明確:
3、有序性
理解多線程的有序性實際上是比較困難的,由於你很難直觀的去觀察到它。
有序性的本義是指程序在執行的時候,程序的代碼執行順序和語句的順序是一致的。可是在Java內存模型中,是容許編譯器和處理器對指令進行重排序的,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。也就是說在多線程中代碼的執行順序,不必定會與你直觀上看到的代碼編寫的邏輯順序一致。
下面咱們舉個簡單的例子:
線程A:
context = loadContext(); //語句1 inited = true; //語句2
線程B:
while(!inited ){ sleep } doSomethingwithconfig(context);
線程A中的代碼中語句1與語句2之間沒有必然的聯繫,因此線程A是會發生重排序問題的,也就是說語句2會在語句1以前執行,這必然會影響到線程B的執行(context沒有實例化)。
其實指令的重排序之因此抽象難懂,由於它是一種較爲底層的行爲,是基於編譯器對你代碼進行深層優化的一種結果,結合上面的例子若是loadContext()中存在阻塞的話,優先執行語句2能夠說是一種合理的行爲。
4、happen-before規則
上面咱們也提到了,多線程的可見性與有序性之間實際上是有聯繫的,若是程序沒有按你但願的順序執行,那麼可見性也就無從談起。JMM(Java 線程內存模型) 中的 happen-before規則,該規則定義了 Java 多線程操做的有序性和可見性,防止了編譯器重排序對程序結果的影響。
按照官方的說法:
當一個變量被多個線程讀取而且至少被一個線程寫入時,若是讀操做和寫操做沒有happen-before關係,則會產生數據競爭問題。 要想保證操做 B
的線程看到操做 A
的結果(不管 A
和 B
是否在一個線程),那麼在 A
和 B
之間必須知足 HB 原則,若是沒有,將有可能致使重排序。 當缺乏 happen-before關係時,就可能出現重排序問題。
1.程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做; 2.鎖定規則:一個unLock操做先行發生於後面對同一個鎖額lock操做; 3.volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做; 4.傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C; 5.線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做; 6.線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生; 7.線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行; 8.對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
從上面的規則中咱們能夠看到,使用synchronized、volatile,加鎖lock等方式通常及能夠保證線程的可見性與有序性。
經過以上對多線程三大特性的總結,能夠看出多線程開發中線程安全問題主要是基於原子性、可見性、有序性實現的,在這裏我根據本身的理解進行了一下簡單整理和闡述,自我感受仍是比較淺顯的,若有不足之處還望指出與海涵。