Java多線程之Synchronized

做者 某人Valar
如需轉載請保留原文連接html

相關推薦:java

Java多線程之Synchronized編程

Java多線程之volatile安全

目錄:bash

  • 什麼是synchronized?
  • synchronized和原子性、可見性和有序性之間的關係
  • synchronized的幾種用法
  • synchronized與lock的區別

1. 什麼是synchronized

synchronized中文意爲:同步的,同步化的。是Java中的一個關鍵字。多線程

經常使用做給方法或者代碼塊加鎖。加鎖後,同一時刻只能有一個線程執行這段代碼。以此來保證線程安全。併發

2. synchronized和原子性、可見性和有序性之間的關係

先簡單理解下3個概念,jvm

  • 原子性(Atomic):原子(atom)本意指化學反應不可再分的基本微粒。在編程中原子性指一個操做不可再被分隔成多步。一個操做或者多個操做 要麼所有執行且執行的過程不會被任何因素打斷,要麼就都不執行。
  • 可見性:當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
  • 有序性:即程序的執行順序按照代碼的前後順序執行。

爲何程序的執行順序有時會不按照代碼的前後順序執行呢?ide

這裏面涉及到指令重排序(Instruction Reorder)的概念。在Java內存模型中,容許編譯器和處理器對指令進行重排序,重排序的結果不會影響到單線程的執行,但不能保證多線程併發執行時不受影響。post

例如如下代碼在未發生指令重排序時,其執行順序爲1->2->3->4。但在真正執行時,將可能變爲1->2->4->3或者2->1->3->4或者其餘。但其會保證1處於3以前,2處於4以前。全部最終結果都是a=10; b=20

int a = 0;//語句1
int b = 1;//語句2
a = 10; //語句3
b = 20; //語句4
複製代碼

但若是是多線程狀況下,另外一個線程中有如下程序。當上述的執行順序被重排序爲1->2->4->3,當線程1執行到第3步b=20時,切換到線程2執行,其會輸出a此時已是10了,而此時a的值其實仍是爲0。

if(b == 20){
  System.out.print("a此時已是10了");
}

複製代碼

2.1 synchronized與原子性的關係?

被synchronized關鍵字包裹起來的方法或者代碼塊能夠認爲是原子的。由於在鎖未釋放以前,這段代碼沒法被其餘線程訪問到,因此從一個線程觀察另一個線程的時候,看到的都是一個個原子性的操做。

在Java中,synchronized對應着兩個字節碼指令monitorentermonitorexit。經過monitorentermonitorexit指令,能夠保證被synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放以前,沒法被其餘線程訪問到。

2.2 synchronized是如何保證可見性的?

根據JMM(Java Memory Model,Java內存模型)機制,內存主要分爲主內存和工做內存兩種,線程工做時會從主內存中拷貝一份變量到工做內存中。

JMM對synchronized作了2條規定:

  1. 線程解鎖前,必須把變量的最新值刷新到主內存中。
  2. 線程加鎖時,先清空工做內存中的變量值,從主內存中從新獲取最新值到工做內存中。

2.3 synchronized能夠保證有序性嗎?

synchronized能夠保證必定程度的有序性,但其是不能禁止指令重排序的,synchronized 代碼塊裏的非原子操做依舊可能發生指令重排。

具體怎麼理解呢?

  • 這裏要先說一個概念,as-if-serial語義,其是指無論怎麼重排序(編譯器和處理器爲了提升並行度),單線程程序的執行結果都不能被改變。編譯器和處理器不管如何優化,都必須遵照as-if-serial語義
  • as-if-serial語義保證了單線程中指令重排序是有一些限制的,即不管怎麼重排序,都不能影響到單線程執行的結果。而synchronized保證了這一塊程序在同一時間內只能被同一線程訪問,因此其也算是保證了有序性。

3. synchronized的幾種用法

synchronized的用法大概能夠分爲3種,

  1. 用來修飾普通方法(實例方法),鎖是當前實例對象,進入同步代碼前要得到當前實例的鎖
  2. 用來修飾靜態方法,鎖是當前類的class對象,進入同步代碼前要得到當前類對象的鎖
  3. 做用於代碼塊,鎖是括號裏面的對象,對給定對象加鎖,進入同步代碼庫前要得到給定對象的鎖。

其做用在方法上的寫法以下圖,synchronized只要放在返回類型前面就行。

下面將舉一些具體的實例,來看下使用synchronized後的結果:

3.1 synchronized做用於實例方法

public class TestBean {
    //TestBean中有兩個實例方法,method1和method2
    public synchronized void method1(){
        System.out.println("method1 start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method1 end");
    }

    public synchronized void method2(){
        System.out.println("method2 start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method2 end");
    }
}
複製代碼
public class MainTest {

    public static void main(String[] args){
        TestBean testBean = new TestBean();
        //第一個線程,執行method1()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean.method1();
            }
        }).start();
        /馬上開啓第二個線程,執行method2()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean.method2();
            }
        }).start();
    }
}
複製代碼

控制檯結果:

method1 start
(...3秒後輸出)
method1 end
method2 start
(...3秒後輸出)
method2 end
複製代碼

能夠看出鎖做用於testBean對象上,其餘線程來訪問synchronized修飾的其餘方法時須要等待線程1先把鎖釋放。

  • 那若是method2()synchronized修飾符去掉呢。那天然是不用等鎖釋放,就會馬上執行method2()。控制檯輸出如下結果:
method1 start
method2 start
(...3秒後輸出)
method1 end
method2 end
複製代碼
  • 又或者TestBean.java類保持不變,兩個方法均由synchronized修飾,但有兩個不一樣的實例對象。
public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();
        TestBean testBean2 = new TestBean();
        
        //第一個線程,執行method1()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean1.method1();
            }
        }).start();
        /馬上開啓第二個線程,執行method2()
        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean2.method2();
            }
        }).start();
    }
}
複製代碼

此時由於兩個線程做用於不一樣的對象,得到的是不一樣的鎖,因此互相併不影響,控制檯的結果以下:

method1 start
method2 start
(...3秒後輸出)
method1 end
method2 end
複製代碼

3.2 synchronized修飾靜態方法

將上文中的TestBean.java改成:

public class TestBean {
    //TestBean中有一個靜態方法,method
    synchronized public static void method(String threadName){
        System.out.println("method start by " + threadName);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("method end by " + threadName);
    }
}
複製代碼

Main.java以下:

public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();
        TestBean testBean2 = new TestBean();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean1.method("thread1");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean2.method("thread2");
            }
        }).start();
    }
}
複製代碼

控制檯結果:

method start by thread1
(...3秒後輸出)
method end by thread1
method start by thread2
(...3秒後輸出)
method end by thread2
複製代碼

分析:由例子可知,兩個線程雖然使用的是兩個不一樣的對象,可是訪問的方法是靜態的,兩個線程最終仍是發生了互斥(即一個線程訪問,另外一個線程只能等着),由於靜態方法是依附於類而不是對象的,當synchronized修飾靜態方法時,鎖是class對象。

3.3 synchronized修飾代碼塊

爲何要做用於代碼塊呢?

在某些狀況下,咱們編寫的方法體可能比較大,同時存在一些比較耗時的操做,而須要同步的代碼又只有一小部分,此時咱們可使用同步代碼塊的方式對須要同步的代碼進行包裹,畢竟長鎖不如短鎖,儘量只鎖必要的部分。

public class TestBean {

    private final static Object objectLock = new Object();

    void method1(){
        System.out.println("not synchronized method1");
        synchronized (objectLock){
            System.out.println("synchronized method1 start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("synchronized method1 end ");
        }

    }

    void method2(){
        System.out.println("not synchronized method2" );
        synchronized (objectLock){
            System.out.println("synchronized method2 start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("synchronized method2 end");
        }
    }
}
複製代碼
public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();
        TestBean testBean2 = new TestBean();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean1.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                testBean2.method2();
            }
        }).start();
    }
}
複製代碼

控制檯輸出:

not synchronized method1
synchronized method1 start
not synchronized method2
(...3秒後輸出)
synchronized method1 end  
synchronized method2 start
(...3秒後輸出)
synchronized method2 end
複製代碼

能夠看到未被synchronized包裹的代碼時不存在互斥的,System.out.println("not synchronized method2" ); 無需等到method1執行完成,而被synchronized包裹的代碼塊,且使用了同一個對象做爲鎖的話,那就互斥了。

結論:

  • synchronized做用於一個給定的實例對象objectLock,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有objectLock實例對象鎖,若是當前有其餘線程正持有該對象鎖,那麼新到的線程就必須等待。
  • 固然除了objectLock做爲對象外,咱們還可使用this對象(表明當前實例)或者當前類的class對象做爲鎖,以下代碼:
synchronized (this){
        System.out.println("synchronized method1 start");
        System.out.println("synchronized method1 end");
    }

    synchronized (TestBean.class){
        System.out.println("synchronized method2 start");
        System.out.println("synchronized method2 end");
    }
複製代碼

4. synchronized和lock之間的區別

Lock是Java語言中的一個接口類,對應的實現類爲ReentrantLock,它們都位於java.util.concurrent.locks包下,熟悉Java的同窗確定都知道concurrent包下都是用於處理Java多線程問題的類。

4.1 lock的使用

lock的幾個經常使用方法:

lock():獲取鎖,若是鎖被暫用則一直等待

unlock():釋放鎖

tryLock(): 注意返回類型是boolean,若是獲取鎖的時候鎖被佔用就返回false,不然返回true

tryLock(long time, TimeUnit unit):比起tryLock()就是給了一個時間期限,保證等待參數時間
ck
lockInterruptibly():用該鎖的得到方式,若是線程在獲取鎖的階段進入了等待,那麼能夠中斷此線程,先去作別的事
複製代碼

lock的使用與synchronized做用於代碼塊時相似:

public class TestBean {
    private Lock lock = new ReentrantLock();
    void method1(String threadName) {
        lock.lock();
        try{
            System.out.println("method1 start " + threadName);
            //耗時操做
            ...
        } finally {
            System.out.println("method1 end " + threadName); 
            lock.unlock();//釋放鎖
        }
    }

}
複製代碼
public class MainTest {

    public static void main(String[] args){
        TestBean testBean1 = new TestBean();

        new Thread("thread1") {
            @Override
            public void run() {
                testBean1.method1(Thread.currentThread().getName());
            }
        }.start();

        new Thread("thread2"){
            @Override
            public void run() {
                testBean1.method1(Thread.currentThread().getName());
            }
        }.start();
    }
}
複製代碼

執行結果以下:

method1 start thread1
(...3秒後輸出)
method1 end thread1
method1 start thread2
(...3秒後輸出)
method1 end thread2
複製代碼

注意:使用lock時,須要在finally中釋放鎖lock.unlock();,否則可能會形成死鎖。

  • 將TestBean.java稍微改動,來看下tryLock()方法的使用:
public class TestBean {
    private Lock lock = new ReentrantLock();
    void method1(String threadName) {
         if (lock.tryLock()){
            try{
                System.out.println("method1 start " + threadName);
                //耗時操做
                ...
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("我是"+threadName+",有人佔着鎖,我放棄了");
        }
    }

}
複製代碼

控制檯輸出:

method1 start thread1
我是thread2,有人佔着鎖,我放棄了
(...3秒後輸出)
method1 end thread1
複製代碼

4.2 lock與synchronized的區別

類別 synchorinzed lock
存在層次 Java的關鍵字 接口類,由ReentrantLock實現
鎖的釋放 一、以獲取鎖的線程執行完同步代碼,釋放鎖 二、線程執行發生異常,jvm會讓線程釋放鎖 在finally中必須釋放鎖,否則容易形成線程死鎖
鎖的獲取 假設A線程得到鎖,B線程等待。若是A線程阻塞,B線程會一直等待 分狀況而定,Lock有多個鎖獲取的方式,可嘗試得到鎖tryLock(),線程能夠不用一直等待
鎖的狀態 沒法判斷 可判斷
性能 資源競爭不是很激烈的狀況下,比較合適的;當同步很是激烈的時候,synchronized的性能會一會兒能降低得很快 在資源競爭不激烈的情形下,性能稍微比synchronized差點;但其在資源競爭激烈時,可維持常態

二者具體的性能測試:www.cnblogs.com/nsw2018/p/5…

參考:blog.csdn.net/u012403290/…

結語

以後會寫一篇文章描述下synchronizeds的底層原理: 涉及到的概念會有Monitor、monitorenter和monitorexit指令、synchronized的可重入性、synchronized與中斷。

另外會寫一篇關於volatile的文章:主要涉及Volatile的特性、其實現原理、指令重排序與內存屏障

相關文章
相關標籤/搜索