Volatile和Synchronized

線程安全能夠歸納爲三個方面:原子性可見性有序性java

原子性:對於涉及共享變量的操做看作一個總體,在同一時間內,只能由一個線程執行,在其它線程看來,這部分操做要麼還沒有開始,要麼已經完成。Java中,基本類型除了long和double,其它類型變量的寫操做都是原子性的。安全

可見性:一個線程修改了共享變量後,其它線程可以當即看見改變後的值。多線程

有序性:即程序按照代碼的前後順序執行。咱們寫好的代碼在執行的時候不必定是按照順序的,由於虛擬機編譯的時候,在保證輸出結果不變的狀況下可能對代碼進行優化,也就是常說的指令重排。在單線程的狀況下不會有什麼影響,可是多線程的環境下則會有隱患。ide

Volatile

先來看Java內存模型性能

 線程先從主內存讀取到變量,對變量進行修改以後再刷新回主內存。當使用了volatile以後:線程直接讀寫主內存。測試

這就保證了共享變量在線程間的可見性。一個常見的例子就是使用volatile修飾的變量,線程A經過改變變量的值去中止另外一個正在運行的線程B。優化

class MyTask implements Runnable{

    private volatile boolean flag = true;

    public void stop(){
        flag = false;
    }

    @Override
    public void run() {
        System.out.println("====進入循環====" + flag);
        while (flag){
        }
        System.out.println("====中止循環====" + flag);
    }
}

測試this

import java.util.concurrent.*;

public class Main {
    
    public static void main(String[] args) throws Exception {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5));

        MyTask task = new MyTask();
        executor.execute(task);
        Thread.sleep(2000);
        System.out.println("外部調用stop");
        task.stop();
    }

}

若是不加volatile關鍵字,則不能中止線程。由於其它線程不能當即看見改變。另外volatile也保證了有序性spa

對於volatile變量的寫操做,JVM會在該操做以前加入一個釋放屏障,操做以後加入一個存儲屏障操作系統

 

 釋放屏障禁止了volatile寫操做和該操做以前的任何讀寫操做進行重排序。這就保障了實際執行順序和源代碼順序同樣,即保障了有序性。

volatile雖然可以保障有序性,可是不具備鎖那樣的排他性,只可以保證所修飾變量寫操做的原子性,不能保證其餘操做的原子性。

對於volatile的讀操做,JVM會在該操做以前加入一個加載屏障,操做以後加入一個獲取屏障

 

 volatile總結:

volatile變量的寫操做與該操做之的任何讀寫操做不會被重排序。

volatile變量的讀操做與該操做之的任何讀寫操做不會被重排序。

volatile只能保證可見性和有序性,對於包含多個操做的共享區域,不能保證線程安全。

Synchronized

先來個簡單代碼

package com.demo.tools;


public class Demo {

    public void hello(){
        synchronized(this) {
            System.out.println("Hello");
        }
    }

}

編譯以後進入classes目錄,查看編譯後的結構。

能夠看到在代碼中用synchronized包起來的代碼塊先後有兩個東西:monitorentermonitorexit,這就涉及了一個叫作Monitor的東西:咱們知道對象在內存中分爲三個區域(對象頭、實例變量,填充數據),而這個Monitor則存儲在對象頭裏。

當執行到monitorenter,線程嘗試獲取鎖(也就是搶佔Monitor);也由於Monitor存在對象頭裏,因此解釋了爲何Java中任意對象均可以做爲鎖。其中還有個計數器,當爲0的時候表明能夠獲取,當線程獲取到了,計數器+1,當執行到monitorexit的時候,線程不佔有這個鎖,計數器-1。因爲Synchronized是可重入鎖,也就是在持有當前鎖的基礎上繼續獲取當前鎖,是能夠的,這個時候,計數器繼續+1,退出同步區域則-1,直到爲0。

同時Synchronized還維護了一個入口集(Entry Set),這個集合存放等待的線程。

引伸

JDK1.6的時候,JVM團隊作了一系列的鎖優化,因此如今的synchronized在一些狀況中性能不比ReentrantLock差:

咱們都知道線程的開銷主要是上下文切換,掛起線程和恢復線程的操做都須要轉入內核態中完成。而不少狀況是:共享數據的鎖定狀況只會持續很短的一段時間,根本不值得掛起恢復。自旋鎖由此而來:

1. 自旋鎖:線程不放棄處理器的執行時間,爲了讓線程等待,而是去執行一個忙循環(自旋)。

固然,雖然避免了上下文切換的開銷,可是它也是佔用處理器時間的:若是鎖佔用時間很短,那麼就達到了自旋的目的;反之佔用時間很長,那麼自旋線程只會白白浪費處理器資源。因此自旋一般都有個限制,自旋次數默認是10次。

2. 在JDK1.6中引入了適應性自旋:

自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間,好比100個循環。
若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能省略掉自旋過程,以免浪費處理器資源。

說白了就是前事不忘後事之師,看到前面那個自旋成功,則本次也認爲可以自旋成功;若是前面自旋失敗,則本次也八九不離十失敗,乾脆省略自旋,直接掛起。

3. 鎖消除:指虛擬機即時編譯器在運行時,對一些代碼上被檢測到不可能存在共享數據競爭的鎖進行消除。

4. 偏向鎖:這個鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。當有另一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束,進而轉變爲輕量級鎖。偏向鎖能夠提升帶有同步但無競爭的程序性能。

5. 輕量級鎖:「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的,所以傳統的鎖機制就稱爲「重量級」鎖。

對於synchronized的升級過程:

①:第一次執行到synchronized代碼塊的時候,鎖對象是偏向鎖。當線程執行完同步代碼塊,不釋放鎖,若是下一次又執行到了同步代碼塊,先判斷持有鎖的線程是否是本身(當前持有鎖的線程ID存儲在對象頭裏),若是是本身就不用從新加鎖;不然就是有人來搶了,此時偏向鎖升級爲輕量級鎖。

②:在輕量級鎖上繼續競爭,沒有搶到鎖的線程自旋,搶到鎖的線程把鎖對象的對象頭裏的線程ID更改成本身。自旋的線程在白白的消耗CPU,這種狀態叫busy-waiting,超過了最大自旋次數的限制後,會將輕量級鎖升級爲重量級鎖。後面再來線程嘗試獲取鎖,發現是個重量級鎖,就把本身掛起,等待被喚醒恢復。

總結:之因此說synchronized很差,由於在1.6以前的synchronized直接是重量級鎖,而後鎖的是整個對象,不如ReentrantLock零活粒度細。而如今的synchronized通過優化以後性能以及很好了,ConcurrentHashMap都在用。還有就是synchronized只能按照偏向鎖->輕量級鎖->重量級鎖的順序逐漸升級(鎖膨脹),不容許降級。

另外,synchronized和ReentrantLock都是可重入鎖(容許同一個線程屢次獲取同一把鎖);synchronized是不可中斷鎖,Lock接口的實現類好比ReentrantLock都是可中斷鎖。

Volatile和Synchronized區別

volatile只能修飾變量,synchronized能夠修飾方法和代碼塊。多線程訪問volatile不會阻塞,synchronized會阻塞。volatile只保證了變量在多個線程之間的可見性,但不能保證原子性;而synchronized能夠保證原子性,也能夠間接保證可見性,由於它會將工做內存和主內存中的數據作同步處理。

相關文章
相關標籤/搜索