併發:多個線程訪問同一份資源。java
並行:一邊聽歌一邊寫論文就是並行,同時作事。數組
volatile是java虛擬機提供的輕量級的同步機制。安全
voliatile有三大特性:多線程
1.保證可見性。併發
2.不保證原子性。性能
3.禁止指令重排。優化
JMM(Java內存模型 Java Memory Model)自己是一種抽象的概念並不真實存在,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段,靜態字段和構成數組對象的元素)的訪問方式。this
JMM關於同步的規定:atom
1.線程解鎖前,必須把共享變量的值刷新回主內存spa
2.線程加鎖前,必須讀取主內存的最新值到本身的工做內存。
3.加鎖解鎖是同一把鎖。
因爲JVM運行程序的實體是線程,而每一個線程建立時JVM都會爲其建立一個工做內存(有些地方稱爲棧空間),工做內存是每一個線程的私有數據區域,而Java內存模型中規定全部變量都存儲在主內存,主內存是共享內存區域,全部線程均可以訪問,但線程對變量的操做(讀取賦值等)必須在工做內存中進行,首先要將變量從主內存拷貝到本身的工做內存空間,而後對變量進行操做,操做完成後再將變量寫回主內存,不能直接操做主內存中的變量,各個線程中的工做內存中存儲主內存的變量副本拷貝,所以不一樣的線程間沒法訪問對方的工做內存,線程間的通訊(傳值)必須經過主內存來完成。
JMM的三大特性:
1.可見性。
2.原子性。
3.有序性。
volatile可見性代碼例子:
class MyData {
volatile int number = 0;
public void add() {
this.number =60;
}
}
/**
* 驗證volatile的可見性,若是number不用volatile修飾,那麼main線程將死循環,由於volatile具備可見性,能夠及時通知其餘線程主內存的值已經修改
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
new Thread(() ->{
System.out.println(Thread.currentThread().getName()+"\t come in number = " + myData.number);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.add();
System.out.println(Thread.currentThread().getName()+"\t update number = " + myData.number);
},"t1").start();
while (myData.number == 0) {
}
System.out.println(Thread.currentThread().getName()+"\t 執行完,當前number = " +myData.number);
}
}
運行結果見下圖:
驗證volatile不保證原子性
原子性指的是什麼?
不可分割,完整性,也即某個線程正在作某個具體業務時,中間不能夠被加塞或者被分割,須要總體完整要麼同時成功,要麼同時失敗。
例子以下:
class MyData1 {
volatile int number = 0;
public void addPlus() {
/** number++ 底層其實被拆分紅了3個指令:
執行getfield拿到元原始number;
執行iadd進行加1操做;
執行putfield寫把累加後的值寫回
*/
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement(); //CAS,底層是unsafe類和自旋,保證原子性
}
}
/**
* 驗證volatile的原子性
*/
public class VolatileDemo1 {
public static void main(String[] args) {
MyData1 myData = new MyData1();
for(int i = 1; i<=20; i++) {
new Thread(() ->{
for(int j = 1; j<= 1000; j++) {
myData.addPlus();
myData.addAtomic();
}
},String.valueOf(i)).start();
}
//須要等待上面20個線程都所有計算完成後,再用main線程取得最終的結果看是多少
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int number 的最終結果是" +myData.number);
System.out.println(Thread.currentThread().getName()+"\t atomic number 的最終結果是" +myData.atomicInteger);
}
}
運行結果見下圖:
緣由分析:例若有3個線程同時寫操做,主內存值是0,3個線程分別拷貝本身工做內存後值0都進行了加1,有可能線程1剛把值1寫入了主內存,線程2或線程3因爲某些緣由掛起如今忽然被喚醒,因爲時間太快還沒收到最新值的通知又去寫成1,等於寫覆蓋丟失了數據,正常主內存的值是3,結果3個線程操做寫覆蓋結果變成1。可使用JUC下面的AtomicInteger解決原子性i++問題。
volatile指令重排
計算機再執行程序時,爲了提升性能,編譯器和處理器的經常會對指令作重排,通常分爲如下3種
源代碼→編譯器優化的重排→指令並行的重排→內存系統的重排→最終執行的指令
單線程環境裏面確保程序最終執行結果和代碼順序執行的結果一致。
處理器在進行重排序時必需要考慮指令之間的數據依賴性。
多線程環境中線程交替執行,因爲編譯器優化重排的存在,兩個線程中使用的變量可否保證一致性是沒法肯定的,結果沒法預測。
例子:
public class ReSortDemo {
int a = 0;
boolean flag = false;
public void method1() {
a = 1;
flag = true;
}
/**
* 多線程環境中線程交替執行,因爲編譯器優化重排的存在
* 兩個線程中使用的變量可否保證一致性沒法肯定, 變量a 和變量 flag 因爲不存在依賴性,多線程下指令重排可能先執行flag,
* 而變量a 仍是舊值0,打印結果就多是5而不是6
*/
public void method2() {
if(flag) {
a = a+5;
System.out.println(a);
}
}
}
單例模式volatile例子:
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName()+"我是構造方法");
}
/** DCL(雙層檢鎖機制)不必定線程安全,緣由是有指令重排序的存在,加入volatile能夠禁止指令重排
* 緣由在於某一個線程執行到第一個檢測,讀取到的instance不爲null時,instance的引用對象可能沒有完成初始化
* instance = new SingletonDemo();能夠分爲如下3步完成(僞代碼)
* memory = allocate();//1. 分配對象內存空間
* instance(memory);//2.初始化對象
* instance = memory;//3.設置instance指向剛分配的內存地址,此時instance != null
*步驟2和步驟3不存在數據依賴關係,並且不管重排前仍是重排後程序的執行結果在單線程中並無改變,所以這種重排序
* 是容許的
* 多線程下可能致使的順序就是132
* @return
*/
public static SingletonDemo getInstance() {
if(instance == null) {
synchronized (SingletonDemo.class) {
if(instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
//模擬10個線程
for (int i = 1; i<= 10; i++) {
new Thread(() ->{
SingletonDemo.getInstance();
},String.valueOf(i)).start();
}
}
}
運行結果以下: