synchronized的使用(一)

薛8發表於2019-03-21
image

多執行緒簡介

在現代計算機中往往存在多個CPU核心,而1CPU能同時執行一個執行緒,為了充分利用CPU多核心,提高CPU的效率,多執行緒就應時而生了。

那麼多執行緒就一定比單執行緒快嗎?答案是不一定,因為多執行緒存在單執行緒沒有的問題

  • 上下文切換:執行緒從執行狀態切換到阻塞狀態或者等待狀態的時候需要將執行緒的執行狀態儲存,執行緒從阻塞狀態或者等待狀態切換到執行狀態的時候需要載入執行緒上次執行的狀態。執行緒的執行狀態從儲存到再載入就是一次上下文切換,而上下文切換的開銷是非常大的,而我們知道CPU給每個執行緒分配的時間片很短,通常是幾十毫秒(ms),那麼執行緒的切換就會很頻繁。
  • 死鎖:死鎖的一般場景是,執行緒A和執行緒B都在互相等待對方釋放鎖,死鎖會造成系統不可用。
  • 資源限制的挑戰:資源限制指計算機硬體資源或軟體資源限制了多執行緒的執行速度,例如某個資源的下載速度是1Mb/s,資源的伺服器頻寬只有2Mb/s,那麼開10個執行緒下載資源並不會將下載速度提升到10Mb/s

既然多執行緒存在這些問題,那麼我們在開發的過程中有必要使用多執行緒嗎?我們知道任何技術都有它存在的理由,總而言之就是多執行緒利大於弊,只要我們合理使用多執行緒就能達到事半功倍的效果。

多執行緒的意思就是多個執行緒同時工作,那麼多執行緒之間如何協同合作,這也就是我們需要解決的執行緒通訊執行緒同步問題

  • 執行緒通訊:執行緒通訊指執行緒之間以何種機制來交換訊息,執行緒之間的通訊機制有兩種:共享記憶體訊息傳遞。共享記憶體即執行緒通過對共享變數的讀寫而達到隱式通訊,訊息傳遞即執行緒通過傳送訊息給對方顯示的進行通訊。
  • 執行緒同步:執行緒同步指不同執行緒對同一個資源進行操作時候執行緒應該以什麼順序去操作,執行緒同步依賴於執行緒通訊,以共享記憶體方式進行執行緒通訊的執行緒同步是顯式的,以訊息傳遞方式進行執行緒通訊的執行緒同步是隱式的。

synchronized簡介

synchronized是Java的關鍵字,可用於同步例項方法、類方法(靜態方法)、程式碼塊

  • 同步例項方法:當synchronized修飾例項方法的時候,同步的範圍是當前例項的例項方法。
  • 同步類方法:當synchronized修飾類方法的時候,同步的範圍是當前類的方法。
  • 同步程式碼塊:當synchronized修飾程式碼塊的時候,同步的範圍是()中的物件。

"talk is cheap show me the code"讓我們分別執行個例子來看看。

  1. 同步例項方法
synchronized public void synSay() {
    System.out.println("synSay----" + Thread.currentThread().getName());
    while (true) { //保證進入該方法的執行緒 一直佔用著該同步方法

    }
}

public void say() {
    System.out.println("say----" + Thread.currentThread().getName());
}
public static void main(String[] args){
    Test test1 = new Test();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            test1.synSay();
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);  //休眠3秒鐘 保證執行緒t1先執行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test1.say();
            test1.synSay();
        }
    });

    t1.start();
    t2.start();
}
複製程式碼

執行輸出

synSay----Thread-0  //執行緒t1
say----Thread-1  //執行緒t2
複製程式碼

建立t1t2兩個執行緒,分別執行同一個例項test1的方法,執行緒t1先執行加了同步關鍵字的synSay方法,注意方法裡面需要加上個while死迴圈,目的是讓執行緒一直在同步方法裡面,然後然執行緒t1執行之後再讓執行緒t2去執行,此時執行緒t2並不能成功進入到synSay方法裡面,因為此時執行緒t1正在方法裡面,執行緒2只能在synSay方法外面阻塞,但是執行緒t2可以進入到沒有加同步關鍵字的say方法。
也就是說關鍵字synchronized修飾例項方法的時候,鎖住的是該例項的加了同步關鍵字的方法,而沒有加同步關鍵字的方法,執行緒還是可以正常訪問的。但是不同例項之間同步是不會影響的,因為每個例項都有自己的一個鎖,不同例項之間的鎖是不一樣的。

  1. 同步類方法
synchronized static public void synSay() {
    System.out.println("static synSay----" + Thread.currentThread().getName());
    while (true) { //保證進入該方法的執行緒 一直佔用著該同步方法

    }
}

synchronized public void synSay1() {
    System.out.println("synSay1----" + Thread.currentThread().getName());
}

public void say() {
    System.out.println("say----" + Thread.currentThread().getName());
}
public static void main(String[] args){
    Test test1 = new Test();
    Test test2 = new Test();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            test1.synSay();
        }
    });

    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Thread.sleep(3000);  //休眠3秒鐘 保證執行緒t1先執行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            test1.say();
            test2.say();
            test1.synSay();
        }
    });

    t1.start();
    t2.start();
}
複製程式碼

執行輸出

static synSay----Thread-0 //執行緒t1 例項test1
say----Thread-1 //執行緒t2 例項test1
say----Thread-1 //執行緒t2 例項test2

static synSay----Thread-0 //執行緒t1 例項test1
say----Thread-1  //執行緒t2 例項test1
synSay1----Thread-1 //執行緒t2 例項test1
say----Thread-1 //執行緒t2 例項test2
複製程式碼

這裡和上面的同步例項方法的程式碼差不多,就是將synSay方法加上了static修飾符,即把方法從例項方法變成類方法了,然後我們再新建個例項test2,先讓執行緒t1呼叫例項test1的synSay類方法,在讓執行緒t2去呼叫例項test1的say例項方法、synSay類方法和讓執行緒t2去呼叫例項test2的say例項方法,發現線上程t1佔用加了同步關鍵字的synSay類方法的時候,別的執行緒是不能呼叫加了鎖的類方法的,但是可以呼叫沒有加同步關鍵字的方法或者加了同步關鍵字的例項方法,也就是說每個類有且僅有11個鎖,每個例項有且僅有1個鎖,但是每個類可以有一個或者多個例項,類的鎖和例項的鎖不會相互影響,例項之間的鎖也不會相互影響。需要注意的是,一個類和一個例項有且僅有一個鎖,當這個鎖被其他執行緒佔用了,那麼別的執行緒就無法獲得鎖,只有阻塞等待

synchronized的使用(一)

  1. 同步程式碼塊
    public void synSay() {
        String x = "";
        System.out.println("come in synSay----" + Thread.currentThread().getName());
        synchronized (x) {
            System.out.println("come in synchronized----" + Thread.currentThread().getName());
            while (true) { //保證進入該方法的執行緒 一直佔用著該同步方法

            }
        }
    }
public static void main(String[] args){
        Test test1 = new Test();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                test1.synSay();
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);  //休眠3秒鐘 保證執行緒t1先執行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                test1.synSay();
            }
        });

        t1.start();
        t2.start();
}
複製程式碼

執行輸出

come in synSay----Thread-0
come in synchronized----Thread-0
come in synSay----Thread-1
複製程式碼

可以發現同步程式碼塊和同步例項方法、同步類方法其實差不多,但是同步程式碼塊將同步的範圍縮小了,可以同步到指定的物件上,而不像同步例項方法、同步類方法那樣同步的是整個方法,所以同步程式碼塊在效率上比其他兩者都有較大的提升。
需要注意的是,當同步程式碼塊的時候,在類方法中加入同步程式碼塊且同步的物件是xx.class等類的引用的時候,同步的是該類,如果在****例項方法中加入同步程式碼塊且同步的物件是this,那麼同步的是該例項,可以看成前者使用的是類的鎖**,後者使用的是例項的鎖

synchronized的特性

建議把volatile的特性和synchronized的特性進行對比學習,加深理解。《Java volatile關鍵字解析》

synchronized與可見性

JMM關於synchronized的兩條語義規定了:

  • 執行緒加鎖前:需要將工作記憶體清空,從而保證了工作區的變數副本都是從主存中獲取的最新值。
  • 執行緒解鎖前;需要將工作記憶體的變數副本寫回到主存中。

大概流程:清空執行緒的工作記憶體->在主存中拷貝變數副本到工作記憶體->執行完畢->將變數副本寫回到主存中->釋放鎖
所以synchronized能保證共享變數的可見性,而實現這個流程的原理也是通過插入記憶體屏障,和關鍵字volatile相似。

synchronized與有序性

因為synchronized是給共享變數加鎖,即使用阻塞的同步機制,共享變數只能同時被一個執行緒操作,所以JMM不用像volatile那樣考慮加記憶體屏障去保證synchronized多執行緒情況下的有序性,因為CPU在單執行緒情況下是保證了有序性的
所以synchronized修飾的程式碼,是保證了有序性的。

synchronized與原子性

同樣因為synchronized是給共享變數加鎖了,以阻塞的機制去同步,在對共享變數進行讀/寫操作的時候是原子性的。
所以synchronized修飾的程式碼,是能保證原子性的。

參考

Java併發程式設計的藝術
記憶體可見性和原子性:Synchronized和Volatile的比較
java synchronized類鎖,物件鎖詳解(轉載)

原文地址:ddnd.cn/2019/03/21/…

相關文章