Java多線程拾遺(四)——簡單聊聊synchronized

前言

關於synchronized的作用,以及爲什麼要有synchronized,這裏就不再總結,這裏只是總結一下synchronized中容易忽略的幾個點

synchronized的具體表現

1、synchronized提供了一種鎖的機制,能夠確保共享變量的互斥訪問,從而防止數據出現不一致的情況。

2、從JVM字節碼指令來看,synchronized關鍵字包括monitor enter和monitor exit兩個JVM指令,它能夠在任何時候任何線程執行到monitor enter成功之前都必須從主內存中讀取數據,而不是從緩存中讀取數據,在monitor exit運行成功之後,共享變量被更新後的值必須刷入到主內存(這個在後面會再拾遺一遍)。monitor exit指令之前必定要有一個monitor enter指令的存在。

synchronized的用法

synchronized可用於修飾代碼塊和方法(包括對象方法和靜態方法),但是不能修飾變量和class

與monitor關聯的對象不能爲空,比如,如下的代碼就是不正確的。

private final Object mutex = null;

public void syncMethod(){
	synchronized(mutex){//會拋出異常,這個mutex不能爲null
		
	}
}

This Monitor

其實針對這種操作,不同的書籍有不同的名稱,我個人願意簡單稱之爲對象監視器。查看如下代碼

/**
 * autor:liman
 * createtime:2020/6/11
 * comment:this monitor的示例
 */
@Slf4j
public class ObjectMonitor {

    public synchronized void method01() {
        log.info(Thread.currentThread().getName() + " enter to method01");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

//    public synchronized void method02() {
//        log.info(Thread.currentThread().getName()+" enter to method02");
//        try {
//            TimeUnit.SECONDS.sleep(10);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
//    }
    
    //也可以採用這種方式
    public void method02(){
        synchronized (this){
        log.info(Thread.currentThread().getName() + " enter to method02");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    	}
	}

    public static void main(String[] args) {
        ObjectMonitor thisMonitor = new ObjectMonitor();
        new Thread(thisMonitor::method01,"T1").start();
        new Thread(thisMonitor::method02,"T2").start();
    }
}

上述代碼中,線程T1和線程T2並不能做到異步執行,在JDK文檔中有如下一段話

When a thread invokes a synchronized method, it automatically acquires the
intrinsic lock for that method's object and releases it when the method returns. The
lock release occurs even if the return was caused by an uncaught exception.

當一個線程調用synchronized修飾的方法,會自動去獲取該方法所在對象的對象鎖,當方法正常執行完成,或遇到異常返回纔會釋放該對象鎖

Class Monitor

差不多的代碼,看看靜態方法的synchronized對其的影響

/**
 * autor:liman
 * createtime:2020/6/11
 * comment:class monitor實例
 */
@Slf4j
public class ClassMonitor {

    public static synchronized void method01(){
        log.info(Thread.currentThread().getName()+" enter to method01");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

//    public static synchronized void method02(){
//            log.info(Thread.currentThread().getName()+ " enter to method02");
//        try {
//            TimeUnit.SECONDS.sleep(10);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }

//    }

    public static synchronized void method02(){
        synchronized (ClassMonitor.class) {
            log.info(Thread.currentThread().getName() + " enter to method02");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(ClassMonitor::method01,"T1").start();
        new Thread(ClassMonitor::method02,"T2").start();
    }

}

上述代碼線程T1和T2依舊無法做到並行運行,JDK文檔中對於這種情況,有如下描述

since a static method is associated with a class, not an object. In this case,
the thread acquires the intrinsic lock for the Class object associated with the class.
Thus access to class's static fields is controlled by a lock that's distinct
from the lock for any instance of the class.

一個靜態方法與類關聯,而不是一個對象,在這種情況下一個線程獲取的是內部相關的類鎖,這樣一來,通過這個類的任何對象,訪問這個類中的靜態屬性和靜態代碼快都會受到內部類鎖的控制。

再來看一個實例

@Slf4j
public class ClassMonitorUpdate {

    static {
        synchronized (ClassMonitorUpdate.class){
            //try {
            //    log.info("static block thread name : "+Thread.currentThread().getName());
            //    Thread.sleep(1000L);
            //} catch (InterruptedException e) {
            //    e.printStackTrace();
            //}
        }
    }

    public static synchronized void method01(){
        log.info(Thread.currentThread().getName()+" enter to method01");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void method02(){
        synchronized (ClassMonitorUpdate.class) {
            log.info(Thread.currentThread().getName() + " enter to method02");
            try {
                TimeUnit.SECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void method03(){
        log.info(Thread.currentThread().getName() + " enter to method03");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//另外一個Main類
public class ClassMonitorMain {

    public static void main(String[] args) {
        new Thread(ClassMonitorUpdate::method01,"T1").start();
        new Thread(ClassMonitorUpdate::method02,"T2").start();
        new Thread(ClassMonitorUpdate::method03,"T3").start();
    }

}

如果將註釋的代碼打開,則上述三個線程會出現串行化運行的情況。這個也不難理解,我們在調用method03的時候會初始化靜態代碼塊,然後在靜態代碼塊的時候,需要去獲取類鎖。

總結

其實這個之前總結過,這裏只是在梳理一下。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章