1. 程式人生 > 其它 >synchronized有幾種用法?

synchronized有幾種用法?

在 Java 語言中,保證執行緒安全性的主要手段是加鎖,而 Java 中的鎖主要有兩種:synchronized 和 Lock,我們今天重點來看一下 synchronized 的幾種用法。

用法簡介

使用 synchronized 無需手動執行加鎖和釋放鎖的操作,我們只需要宣告 synchronized 關鍵字就可以了,JVM 層面會幫我們自動的進行加鎖和釋放鎖的操作。
synchronized 可用於修飾普通方法、靜態方法和程式碼塊,接下來我們分別來看。

1、修飾普通方法

synchronized 修飾普通方法的用法如下:

/**
 * synchronized 修飾普通方法
 */
public synchronized void method() {
    // ....
}

當 synchronized 修飾普通方法時,被修飾的方法被稱為同步方法,其作用範圍是整個方法,作用的物件是呼叫這個方法的物件。

2、修飾靜態方法

synchronized 修飾靜態方法和修飾普通方法類似,它的用法如下:

/**
 * synchronized 修飾靜態方法
 */
public static synchronized void staticMethod() {
    // .......
}

當 synchronized 修飾靜態方法時,其作用範圍是整個程式,這個鎖對於所有呼叫這個鎖的物件都是互斥的。

所謂的互斥,指的是同一時間只能有一個執行緒能使用,其他執行緒只能排隊等待。

修飾普通方法 VS 修飾靜態方法

synchronized 修飾普通方法和靜態方法看似相同,但二者完全不同,對於靜態方法來說 synchronized 加鎖是全域性的,也就是整個程式執行期間,所有呼叫這個靜態方法的物件都是互斥的,而普通方法是針對物件級別的,不同的物件對應著不同的鎖,比如以下程式碼,同樣是呼叫兩次方法,但鎖的獲取完全不同,實現程式碼如下:

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SynchronizedUsage {
    public static void main(String[] args) throws InterruptedException {
        // 建立執行緒池同時執行任務
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        // 執行兩次靜態方法
        threadPool.execute(() -> {
            staticMethod();
        });
        threadPool.execute(() -> {
            staticMethod();
        });
        
        // 執行兩次普通方法
        threadPool.execute(() -> {
            SynchronizedUsage usage = new SynchronizedUsage();
            usage.method();
        });
        threadPool.execute(() -> {
            SynchronizedUsage usage2 = new SynchronizedUsage();
            usage2.method();
        });
    }

    /**
     * synchronized 修飾普通方法
     * 本方法的執行需要 3s(因為有 3s 的休眠時間)
     */
    public synchronized void method() {
        System.out.println("普通方法執行時間:" + LocalDateTime.now());
        try {
            // 休眠 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * synchronized 修飾靜態方法
     * 本方法的執行需要 3s(因為有 3s 的休眠時間)
     */
    public static synchronized void staticMethod() {
        System.out.println("靜態方法執行時間:" + LocalDateTime.now());
        try {
            // 休眠 3s
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

以上程式的執行結果如下:

從上述結果可以看出,靜態方法加鎖是全域性的,針對的是所有呼叫者;而普通方法加鎖是物件級別的,不同的物件擁有的鎖也不同。

3、修飾程式碼塊

我們在日常開發中,最常用的是給程式碼塊加鎖,而不是給方法加鎖,因為給方法加鎖,相當於給整個方法全部加鎖,這樣的話鎖的粒度就太大了,程式的執行效能就會受到影響,所以通常情況下,我們會使用 synchronized 給程式碼塊加鎖,它的實現語法如下:

public void classMethod() throws InterruptedException {
    // 前置程式碼...
    
    // 加鎖程式碼
    synchronized (SynchronizedUsage.class) {
        // ......
    }
    
    // 後置程式碼...
}

從上述程式碼我們可以看出,相比於修飾方法,修飾程式碼塊需要自己手動指定加鎖物件,加鎖的物件通常使用 this 或 xxx.class 這樣的形式來表示,比如以下程式碼:

// 加鎖某個類
synchronized (SynchronizedUsage.class) {
    // ......
}

// 加鎖當前類物件
synchronized (this) {
    // ......
}

this VS class

使用 synchronized 加鎖 this 和 xxx.class 是完全不同的,當加鎖 this 時,表示用當前的物件進行加鎖,每個物件都對應了一把鎖;而當使用 xxx.class 加鎖時,表示使用某個類(而非類例項)來加鎖,它是應用程式級別的,是全域性生效的,如以下程式碼所示:

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SynchronizedUsageBlock {
    public static void main(String[] args) throws InterruptedException {
        // 建立執行緒池同時執行任務
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        // 執行兩次 synchronized(this)
        threadPool.execute(() -> {
            SynchronizedUsageBlock usage = new SynchronizedUsageBlock();
            usage.thisMethod();
        });
        threadPool.execute(() -> {
            SynchronizedUsageBlock usage2 = new SynchronizedUsageBlock();
            usage2.thisMethod();
        });

        // 執行兩次 synchronized(xxx.class)
        threadPool.execute(() -> {
            SynchronizedUsageBlock usage3 = new SynchronizedUsageBlock();
            usage3.classMethod();
        });
        threadPool.execute(() -> {
            SynchronizedUsageBlock usage4 = new SynchronizedUsageBlock();
            usage4.classMethod();
        });
    }

    /**
     * synchronized(this) 加鎖
     * 本方法的執行需要 3s(因為有 3s 的休眠時間)
     */
    public void thisMethod() {
        synchronized (this) {
            System.out.println("synchronized(this) 加鎖:" + LocalDateTime.now());
            try {
                // 休眠 3s
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * synchronized(xxx.class) 加鎖
     * 本方法的執行需要 3s(因為有 3s 的休眠時間)
     */
    public void classMethod() {
        synchronized (SynchronizedUsageBlock.class) {
            System.out.println("synchronized(xxx.class) 加鎖:" + LocalDateTime.now());
            try {
                // 休眠 3s
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

以上程式的執行結果如下:

總結

synchronized 用 3 種用法,用它可以來修飾普通方法、靜態方法和程式碼塊,其中最常用的是修飾程式碼塊,而修飾程式碼塊時需要指定一個加鎖物件,這個加鎖物件通常使用 this 或 xxx.class 來表示,當使用 this 時,表示使用當前物件來加鎖,而使用 class 時,表示表示使用某個類(非類物件例項)來加鎖,它是全域性生效的。

是非審之於己,譭譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

面試合集:https://gitee.com/mydb/interview