併發三大特性:
- 原子性:CAS和Automic類可以實現簡單的原子性,對於複雜的操作可以使用synchronized和lock來實現
- 可見性:當多個線程訪問同一個變量時,其中一個線程修改了變量的值,其他的線程可以立即看到修改的值。即立刻刷新修改的值到主存而不是工作內存。
- 有序性:程序執行的順序按照代碼的先後順序執行,禁止進行指令衝排序。指令重排序是jvm爲了優化指令,提高程序運行效率,在不影響單線程程序執行的情況下提高程序的並行度。但在多線程的情況下,有些代碼順序的改變有可能造成程序的不正確。
代碼實例:
- (1) 加上volatile修飾,並不能保證原子性
運行結果不等於300000
// volatile 只具有可見性和有序性,但不保證原子性
public static volatile int num = 0;
//使用CountDownLatch來等待計算線程執行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//開啓30個線程進行累加操作
for(int i=0;i<30;i++){
new Thread(){
@Override
public void run(){
for(int j=0;j<10000;j++){
num++;//自加操作
}
countDownLatch.countDown();
}
}.start();
}
countDownLatch.await();
System.out.println(num);
}
- (2)使用AtomicInteger來計數,可以保證原子性,
- 並配合CountDownLatch來確保所有線程執行完畢,可以看到執行結果等於300000
//使用原子操作類
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch來等待計算線程執行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
//開啓30個線程進行累加操作
for(int i=0;i<30;i++){
new Thread(){
@Override
public void run(){
for(int j=0;j<10000;j++){
num.incrementAndGet();//原子性的num++,通過循環CAS方式
}
countDownLatch.countDown();
}
}.start();
}
//等待計算線程執行完
countDownLatch.await();
System.out.println(num);
}
(3)volatile爲什麼沒有原子性
volatile保證了讀寫一致性。但是當線程2已經使用舊值完成了運算指令,且將要回寫到內存時,是不能保證原子性的。
具體化:使用git開發項目時存在主幹和分支,有一個全項目都使用的枚舉類,
所以甲修改了該類立即提交主幹,並通知組內成員:“你們使用這個類時需要在主幹
上拉取一下”,但是此時乙在舊版本開發完畢並且正在提交這個類,導致了衝突。
(4)volatile防止指令重排
普通變量僅僅會保證在該方法執行過程中所有依賴賦值結果的地方都能得到正確的結果,
而不能保證變量賦值操作的順序域代碼中的執行順序一致。
被volatile修飾的變量,會加一個lock前綴的彙編指令。若變量被修改後,
會立刻將變量由工作內存回寫到主存中。那麼意味了之前的操作已經執行完畢。這就是內存屏障。
(5)內存屏障
內存屏障分爲兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。
內存屏障的兩個作用:
1.阻止屏障兩側的指令重排序;
2.強制把寫緩衝區/高速緩存中的髒數據等寫回主內存,讓緩存中相應的數據失效。
- 對於Load Barrier來說,在指令前插入Load Barrier,可以讓高速緩存中的數據失效,強制從新的主內存加載數據;
- 對於Store Barrier來說,在指令後插入Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其他線程可見。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因爲它需要在本地代碼中
插入許多內存屏障指令來保證處理器不發生亂序執行。