JMM的關鍵技術點都是圍繞着多線程的原子性、可見性和有序性來創建的。所以,咱們首先必須瞭解這些概念java
1,原子性編程
原子性是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程干擾,好比,對於一個靜態全局變量int i,兩個線程同時對它賦值,線程A給他賦值1,線程B給它賦值爲-1。那麼無論這2個線程以何種方式、何種步調工做,i的值要麼是1,要麼是-1。線程A和線程B之間是沒有干擾的。這就是原子性的一個特色,不可被中斷數組
2,可見性(Visibility)緩存
可見性是指當一個線程修改了某一個共享變量的值,其餘線程是否可以當即知道這個修改。顯然,對於串行程序來講,可見性問題是不存在的。由於你在任何一個操做步驟中修改了某個變量,那麼在後續的步驟中,讀取這個變量的值,必定是修改後的新值。安全
3 有序性(Ordering)多線程
有序性問題多是三個問題中最難理解的了。對於一個線程的執行代碼而言,咱們老是習慣地認爲代碼的執行是從先日後,依次執行的。這麼理解也不能說徹底錯誤,由於就一個線程內而言,確實會表現成這樣。可是,在併發時,程序的執行可能就會出現亂序。給人直觀的感受就是:寫在前面的代碼,會在後面執行。聽起來有些難以想象,是嗎?有序性問題的緣由是由於程序在執行時,可能會進行指令重排,重排後的指令與原指令的順序未必一致併發
以上概念均摘自《實戰java高併發程序設計》ide
Java內存模型規定全部的變量都是存在主存當中,每一個線程都有本身的工做內存。線程對變量的全部操做都必須在工做內存中進行,而不能直接對主存進行操做。而且每一個線程不能訪問其餘線程的工做內存。高併發
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量與Java編程時所說的變量不同,指包括了實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量與方法參數,後者是線程私有的,不會被共享。性能
Java內存模型中規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存(能夠與前面講的處理器的高速緩存類比),線程的工做內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成,線程、主內存和工做內存的交互關係以下圖所示,和上圖很相似。
注意:這裏的主內存、工做內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分,這二者基本上沒有關係。
由上面的交互關係可知,關於主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步到主內存之間的實現細節,Java內存模型定義瞭如下八種操做來完成:
若是要把一個變量從主內存中複製到工做內存,就須要按順尋地執行read和load操做,若是把變量從工做內存中同步回主內存中,就要按順序地執行store和write操做。Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是能夠插入其餘指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。Java內存模型還規定了在執行上述八種基本操做時,必須知足以下規則:
Java 使用了一些特殊的操做或者關鍵字來申明、告訴虛擬機,在這個地方,要尤爲注意,不能隨意變更優化目標指令。關鍵字volatile 就是其中之一。當你用volatile 去申明一個變量時,就等於告訴了虛擬機,這個變量極有可能會被某些程序或者線程修改。爲了確保這個變量被修改後,應用程序範圍內的全部線程都可以「看到」這個改動,虛擬機就必須採用一些特殊的手段,保證這個變量的可見性等特色。
先來看一個使用場景,以下面一段代碼在32位java虛擬機上的運行
public class Test { public static long t = 0; public static class ChangeT implements Runnable { private long to; public ChangeT(long to) { this.to = to; } @Override public void run() { while (true) { Test.t = to; Thread.yield(); } } } public static class ReadT implements Runnable { @Override public void run() { while (true) { long temp = Test.t; if (temp != 111L && temp != -999L && temp != 333L && temp != -444L) { System.out.println(temp); } Thread.yield(); } } } public static void main(String args[]) { new Thread(new ChangeT(111L)).start(); new Thread(new ChangeT(-999L)).start(); new Thread(new ChangeT(333L)).start(); new Thread(new ChangeT(-444L)).start(); new Thread(new ReadT()).start(); } }
能夠看出因爲long是64位在32位的虛擬機上經過多線程賦值,致使數據的讀和寫都不是原子性的,於是形成,打印出的數字部分是亂的
-4294966963
4294966297
-4294966963
-4294966963
4294966297
-4294967185
-4294966963
4294966297
...
但當使用 volatile修飾後就沒有問題了
public volatile static long t = 0;
同時也須要注意volatile是沒法徹底保證一些複合操做的原子性,它並不可以代替鎖。
package com.ishop.controller; public class Test { public volatile static int i = 0; public static void main(String[] args) throws InterruptedException { AddThread t1 = new AddThread(); AddThread t2 = new AddThread(); AddThread t3 = new AddThread(); AddThread t4 = new AddThread(); t1.start(); t2.start(); t3.start(); t4.start(); System.out.println("i="+i); } } class AddThread extends Thread { @Override public void run() { for (int a = 0; a < 100000; a++){ Test.i++; } } }
如:這一段代碼,輸出i老是一個小於100000的值。
對於volatile的特色以及使用場景總結以下:
volatile變量具備2種特性:
volatile語義並不能保證變量的原子性。對任意單個volatile變量的讀/寫具備原子性,但相似於i++、i–這種複合操做不具備原子性,由於自增運算包括讀取i的值、i值增長一、從新賦值3步操做,並不具有原子性。
因爲volatile只能保證變量的可見性和屏蔽指令重排序,只有知足下面2條規則時,才能使用volatile來保證併發安全,不然就須要加鎖(使用synchronized、lock或者java.util.concurrent中的Atomic原子類)來保證併發中的原子性。
由於須要在本地代碼中插入許多內存屏蔽指令在屏蔽特定條件下的重排序,volatile變量的寫操做與讀操做相比慢一些,可是其性能開銷比鎖低不少。
參考:《實戰java高併發程序設計》
文章部份內容引自:http://blog.csdn.net/u011080472/article/details/51337422