synchronized的使用(一)

image

多線程簡介

在現代計算機中每每存在多個CPU核心,而1CPU能同時運行一個線程,爲了充分利用CPU多核心,提升CPU的效率,多線程就應時而生了。java

那麼多線程就必定比單線程快嗎?答案是不必定,由於多線程存在單線程沒有的問題編程

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

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

多線程的意思就是多個線程同時工做,那麼多線程之間如何協同合做,這也就是咱們須要解決的線程通訊線程同步問題服務器

  • 線程通訊:線程通訊指線程之間以何種機制來交換消息,線程之間的通訊機制有兩種:共享內存消息傳遞。共享內存即線程經過對共享變量的讀寫而達到隱式通訊,消息傳遞即線程經過發送消息給對方顯示的進行通訊。
  • 線程同步:線程同步指不一樣線程對同一個資源進行操做時候線程應該以什麼順序去操做,線程同步依賴於線程通訊,以共享內存方式進行線程通訊的線程同步是顯式的,以消息傳遞方式進行線程通訊的線程同步是隱式的。

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();
}
複製代碼

運行輸出ide

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();
}
複製代碼

運行輸出ui

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個鎖,可是每一個類能夠有一個或者多個實例,類的鎖和實例的鎖不會相互影響,實例之間的鎖也不會相互影響。須要注意的是,一個類和一個實例有且僅有一個鎖,當這個鎖被其餘線程佔用了,那麼別的線程就沒法得到鎖,只有阻塞等待this

  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/…

相關文章
相關標籤/搜索