java synchronized 關鍵字 使用方法 學習筆記

java synchronized 關鍵字 使用方法 學習筆記

1. 綜述

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

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

因此爲了解決這個問題,我們可能需要這樣一個方案,當存在多個線程操作共享數據時,需要保證同一時刻有且只有一個線程在操作共享數據,其他線程必須等到該線程處理完數據後再進行,這種方式有個高尚的名稱叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享數據被當前正在訪問的線程加上互斥鎖後,在同一個時刻,其他線程只能處於等待的狀態,直到當前線程處理完畢釋放該鎖。

在 Java 中,關鍵字 synchronized 可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數據的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。

它修飾的對象有以下幾種:

  1. 修飾一個方法,被修飾的方法稱爲同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;
  2. 修飾一個靜態的方法,其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象;
  3. 修飾一個代碼塊,被修飾的代碼塊稱爲同步語句塊,其作用的範圍是大括號{}括起來的代碼,作用的對象是調用這個代碼塊的對象;
  4. 修飾一個類,其作用的範圍是synchronized後面括號括起來的部分,作用主的對象是這個類的所有對象。

2. 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方法。

3. synchronized作用於靜態方法

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

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)。
  • 因此在設計同步代碼的時候一定要仔細思考到底該用 多大的同步粒度 和 該對什麼對象 使用同步操作。

4. synchronized同步代碼塊

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

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++操作。

5. synchronized作用於類

當然除了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 對象的鎖才能運行相應的代碼。

總結

  1. 無論synchronized關鍵字加在方法上還是對象上,如果它作用的對象是非靜態的,則它取得的鎖是對象;如果synchronized作用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的對象同一把鎖。
  2. 每個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以運行它所控制的那段代碼。
  3. 實現同步是要很大的系統開銷作爲代價的,甚至可能造成死鎖,所以儘量避免無謂的同步控制。

Ref

  1. https://blog.csdn.net/zhangqilugrubby/article/details/80500505
  2. https://segmentfault.com/a/1190000012203437#articleHeader2
  3. https://www.jianshu.com/p/ffb8e441b55b
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章