synchronized用法簡單梳理

線程安全是併發編程中的重要關注點,應該注意到的是,形成線程安全問題的主要誘因有兩點java

  • 一是存在共享數據(也稱臨界資源)
  • 二是存在多條線程共同操做共享數據

所以爲了解決這個問題,咱們可能須要這樣一個方案,當存在多個線程操做共享數據時,須要保證同一時刻有且只有一個線程在操做共享數據,其餘線程必須等到該線程處理完數據後再進行,這種方式有個高尚的名稱叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享數據被當前正在訪問的線程加上互斥鎖後,在同一個時刻,其餘線程只能處於等待的狀態,直到當前線程處理完畢釋放該鎖。
在 Java 中,關鍵字 synchronized 能夠保證在同一個時刻,只有一個線程能夠執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操做),同時咱們還應該注意到synchronized另一個重要的做用,synchronized可保證一個線程的變化(主要是共享數據的變化)被其餘線程所看到(保證可見性,徹底能夠替代Volatile功能),這點確實也是很重要的。編程

synchronized的三種應用方式

synchronized關鍵字最主要有如下3種應用方式,下面分別介紹安全

  • 修飾實例方法,做用於當前實例加鎖,進入同步代碼前要得到 當前實例 的鎖
  • 修飾靜態方法,做用於當前類對象加鎖,進入同步代碼前要得到 當前類對象 的鎖
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要得到 給定對象 的鎖。

synchronized做用於實例方法

所謂的實例對象鎖就是用synchronized修飾實例對象中的實例方法,注意是實例方法不包括靜態方法併發

package sychronized;

import static net.mindview.util.Print.*;

import java.util.concurrent.*;

public class AccountingSync2 implements Runnable {
    //共享資源(臨界資源)
    static int i = 0;

    /**
     * synchronized 修飾實例方法
     */
    synchronized void getI() {
        if (i % 1000000 == 0) {
            print(i);
        }
    }

    public synchronized void increase() {
        i++;
        getI();
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            increase();
        }
        print(i);
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        AccountingSync2 accountingSync2 = new AccountingSync2();
        exec.execute(accountingSync2);
        exec.execute(accountingSync2);
        exec.shutdown();
    }
}

最後的結果爲:
1000000
1519541
2000000
2000000
  • 上述代碼中,咱們開啓兩個線程操做同一個共享資源即變量i,因爲i++操做並不具有原子性,該操做是先讀取值,而後寫回一個新值,至關於原來的值加上1,分兩步完成,若是第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那麼第二個線程就會與第一個線程一塊兒看到同一個值,並執行相同值的加1操做,這也就形成了線程安全失敗,所以對於increase方法必須使用synchronized修飾,以便保證線程安全。
  • 此時注意到synchronized修飾的是實例方法increase,在這樣的狀況下,當前線程的鎖即是實例對象instance,注意Java中的線程同步鎖能夠是任意對象。
  • 當一個線程正在訪問一個對象的 synchronized 實例方法,那麼其餘線程不能訪問該對象的其餘 synchronized 方法,畢竟一個對象只有一把鎖,當一個線程獲取了該對象的鎖以後,其餘線程沒法獲取該對象的鎖,因此沒法訪問該對象的其餘synchronized實例方法,可是其餘線程仍是能夠訪問該實例對象的其餘非synchronized方法,可是一個 synchronized 方法能夠調用另外一個須要得到一樣鎖的synchronized方法,由於已經獲取了鎖。
  • 若是是一個線程 A 須要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另外一個線程 B 須要訪問實例對象 obj2 的 synchronized 方法f2(當前對象鎖是obj2),這樣是容許的,由於兩個實例對象鎖並不一樣相同,此時若是兩個線程操做數據並不是共享的,線程安全是有保障的,遺憾的是若是兩個線程操做的是共享數據,那麼線程安全就有可能沒法保證了。
package sychronized;

import static net.mindview.util.Print.*;
import java.util.concurrent.*;

public class AccountingSync2 implements Runnable {
    //共享資源(臨界資源)
    static int i = 0;

    /**
     * synchronized 修飾實例方法
     */
    synchronized void getI() {
        if (i % 1000000 == 0) {
            print(i);
        }
    }

    public synchronized void increase() {
        i++;
        getI();
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            increase();
        }
        print(i);
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        AccountingSync2 accountingSync2 = new AccountingSync2();
        exec.execute(accountingSync2);
        exec.execute(new AccountingSync2());
        exec.shutdown();
    }
}

#輸出結果:
1000000
1249050
1329218
  • 上述代碼與前面不一樣的是咱們同時建立了兩個新實例AccountingSync2,而後啓動兩個不一樣的線程對共享變量i進行操做,但很遺憾操做結果是1329218而不是指望結果2000000。
  • 雖然咱們使用synchronized修飾了increase方法,但卻new了兩個不一樣的實例對象,這也就意味着存在着兩個不一樣的實例對象鎖,所以兩個進程都會進入各自持有對象的對象鎖,也就是說兩個線程使用的是不一樣的鎖,所以線程安全是沒法保證的。
  • 解決這種困境的的方式是將synchronized做用於靜態的increase方法,這樣的話,對象鎖就當前類對象,因爲不管建立多少個實例對象,但對於的類對象擁有隻有一個,全部在這樣的狀況下對象鎖就是惟一的。下面咱們看看如何使用將synchronized做用於靜態的increase方法。

synchronized做用於靜態方法

當synchronized做用於靜態方法時,其鎖就是當前類的class對象鎖。因爲靜態成員不專屬於任何一個實例對象,是類成員,所以經過class對象鎖能夠控制靜態成員的併發操做。須要注意的是若是一個線程A調用一個實例對象的非static synchronized 方法,而線程B須要調用這個實例對象所屬類的靜態 synchronized方法,是容許的,不會發生互斥現象,由於訪問靜態 synchronized 方法佔用的鎖是當前類的 class 對象,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。ide

package sychronized;

import static net.mindview.util.Print.*;
import java.util.concurrent.*;

class OtherTask implements Runnable{
    AccountingSyncClass accounting = new AccountingSyncClass();
    @Override
    public void run(){
        for (int j = 0; j < 1000000; j++) {
            accounting.increaseForObject();
        }
        print(accounting.getI());
    }
}

public class AccountingSyncClass implements Runnable {
    //共享資源(臨界資源)
    private static int i = 0;

    /**
     * synchronized 修飾實例方法
     */
    public synchronized void increaseForObject() {
        i++;
    }

    public synchronized static void increase() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            increase();
        }
        print(i);
    }
    
    public int getI(){
        return i;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new AccountingSyncClass());
        exec.execute(new AccountingSyncClass());
        exec.execute(new OtherTask()); // 1
        exec.shutdown();
    }
}

輸出結果爲:
1459696
2692181
2754098

註釋掉代碼中的 1 那一行代碼的輸出結果爲:
1468495
2000000
  • 因爲synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不一樣的是,其鎖對象是當前類的class對象。
  • 注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,若是別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不一樣,但咱們應該意識到這種狀況下可能會發現線程安全問題(操做了共享靜態變量i)。
  • 所以在設計同步代碼的時候必定要仔細思考到底該用 多大的同步粒度 和 該對什麼對象 使用同步操做。

synchronized同步代碼塊

除了使用關鍵字修飾實例方法和靜態方法外,還可使用同步代碼塊,在某些狀況下,咱們編寫的方法體可能比較大,同時存在一些比較耗時的操做,而須要同步的代碼又只有一小部分,若是直接對整個方法進行同步操做,可能會得不償失,此時咱們可使用同步代碼塊的方式對須要同步的代碼進行包裹,這樣就無需對整個方法進行同步操做了。this

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其餘耗時操做....
        //使用同步代碼塊對變量i進行同步操做,鎖對象爲instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}
  • 從代碼看出,將synchronized做用於一個給定的實例對象instance,即當前實例對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖。
  • 若是當前有其餘線程正持有該對象鎖,那麼新到的線程就必須等待,這樣也就保證了每次只有一個線程執行i++操做。
  • 固然除了instance做爲對象外,咱們還可使用this對象(表明當前實例)或者當前類的class對象做爲鎖。
//this,當前實例對象鎖
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class對象鎖
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

同步方法最好運用在 共享資源 內部而不是使用它的外部

package sychronized;

import static net.mindview.util.Print.*;
import java.util.concurrent.*;

class OtherTask implements Runnable{
    AccountingSyncClass accounting = new AccountingSyncClass();
    @Override
    public void run(){
        for (int j = 0; j < 1000000; j++) {
            accounting.increaseForObject();
        }
        print(accounting.getAdapterInteger());
    }
}

class AdapterInteger {
    private int i = 0;
    public synchronized void increase(){
        ++i;
    }
    public synchronized int getI(){
        return i;
    }
}

public class AccountingSyncClass implements Runnable {
    //共享資源(臨界資源)
    private static AdapterInteger adapterInteger = new AdapterInteger();

    /**
     * synchronized 修飾實例方法
     */
    public synchronized void increaseForObject() {
        adapterInteger.increase();
    }

    public synchronized static void increase() {
        adapterInteger.increase();
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            increase();
        }
        print(getAdapterInteger());
    }

    public static int getAdapterInteger() {
        return adapterInteger.getI();
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new AccountingSyncClass());
        exec.execute(new AccountingSyncClass());
        exec.execute(new OtherTask());
        exec.shutdown();
    }
}

#輸出結果爲
1183139
2688189
3000000

這樣三個線程中的任務的鎖都是咱們的共享變量 adapterInteger 對象的鎖,這樣就能夠完成真正的同步,無論哪一個線程都是得到了 adapterInteger 對象的鎖才能運行相應的代碼。線程

相關文章
相關標籤/搜索