詳解Java中的synchronized

同步博客:My Love

Synchronized

Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object's variables are done through synchronized methods.
------ JDK Document - Synchronized Methods

Synchronized方法支持使用一種簡單的策略來防止線程間的干擾和內(nèi)存一致性錯誤:如果一個對象對多個線程可見看尼,則對該對象變量的所有讀寫都是通過通過Synchronized方法完成。

Synchronized的作用

上面的介紹一句話概括就是:

同一時刻保證只有一個線程執(zhí)行該代碼以達到保證并發(fā)安全的效果。

如果一段代碼被Synchronized修飾了,那么這段代碼就會以原子方式執(zhí)行纽什,多個線程在執(zhí)行這段代碼時不會相互干擾降盹,因為他們不會同時執(zhí)行這段代碼食呻,只要不同時執(zhí)行就不會出現(xiàn)并發(fā)問題。那么線程之間怎么知道不同時執(zhí)行這段代碼呢澎现?這里可以想象有一把鎖仅胞,被Synchronized修飾的這段代碼在被第一段代碼執(zhí)行的時候時會先加把鎖,知道地一段代碼執(zhí)行結束后或者在一定的條件下再打開鎖剑辫,在這把鎖釋放前干旧,其他線程的代碼執(zhí)行到加Synchronized修飾的這段代碼時,會檢測到有鎖的存在妹蔽,所以就先不執(zhí)行加鎖的代碼椎眯,而是等待鎖的打開。等第一段代碼執(zhí)行完胳岂,打開鎖后编整,其他線程的代碼才能執(zhí)行Synchronized修飾的代碼段。

Synchronized是Java的關鍵字乳丰,被Java語言原生支持掌测。是最基本的互斥同步手段,而且是并發(fā)變成的元老級角色产园,是并發(fā)變成必學的內(nèi)容汞斧。

不使用并發(fā)手段會有什么后果

舉個栗子:兩個線程同時對變量a++,最后結果比預期的要小什燕。

public class DisappearedRequest1 implements Runnable{

    static DisappearedRequest1 instance = new DisappearedRequest1();

    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);

        t1.start();
        t2.start();
        //這里加入join的作用是讓t1和t2先執(zhí)行完再執(zhí)行main函數(shù)接下來的代碼
        t1.join();
        t2.join();

        System.out.println(i);
    }

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

最后結果:

103066

Process finished with exit code 0

為什么會這樣呢粘勒?原因就是:i++這個操作看上去是一步完成的,實際上不是的屎即,它包含三個步驟:

  1. 讀取i的值
  2. 將i加一
  3. 將i的值寫入到內(nèi)存中

在這三步執(zhí)行過程中t1庙睡、t2兩個線程的操作可能會有干擾,比如t1剛讀取了i的值并將其加1技俐,t2就執(zhí)行了步驟一的操作讀取i的值乘陪,此時由于t1將i的值加一后沒有寫入到內(nèi)存,所以t2此時讀取的還是原來的i值虽另,接下來也會進行加一操作暂刘,然后t1、t2都會進行寫入內(nèi)存的操作捂刺,這就會造成i少加一的情況谣拣。

這就是線程不安全的情況募寨。

Synchronized兩種用法

對象鎖

對象鎖分兩種:

  • 同步代碼塊鎖:自己指定鎖對象。

  • 方法鎖:默認鎖定對象為this當前實例對象森缠。

同步代碼塊

先上代碼:

package demo;

/**
 * 描述:對象鎖拔鹰,同步代碼塊
 */
public class SynchronizedObjectCodeBlock2 implements Runnable{
    static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
    @Override
    public void run() {
//        synchronized (this) {
            System.out.println("對象鎖的代碼塊形式:" + Thread.currentThread().getName());
            try {
                //休眠3s,使效果更明顯
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運行結束贵涵!");
//        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        //這里不使用join列肢,采用while循環(huán)判斷的方式,功能跟join類似
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }
}

不加synchronized修飾代碼塊的情況下宾茂,運行結果是這樣的:

對象鎖的代碼塊形式:Thread-0
對象鎖的代碼塊形式:Thread-1
Thread-0運行結束瓷马!
Thread-1運行結束!
Finished!

Process finished with exit code 0

修改代碼跨晴,加上synchronized后欧聘,代碼如下:

package demo;

/**
 * 描述:對象鎖,同步代碼塊
 */
public class SynchronizedObjectCodeBlock2 implements Runnable{
    static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
    @Override
    public void run() {
       synchronized (this) {
            System.out.println("對象鎖的代碼塊形式:" + Thread.currentThread().getName());
            try {
                //休眠3s端盆,使效果更明顯
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "運行結束怀骤!");
       }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        //這里不使用join,采用while循環(huán)判斷的方式焕妙,功能跟join類似
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }
}

加入synchronized修飾代碼塊后運行結果如下:

對象鎖的代碼塊形式:Thread-1
Thread-1運行結束蒋伦!
對象鎖的代碼塊形式:Thread-0
Thread-0運行結束!
Finished!

Process finished with exit code 0

這里可以看出使用synchronized和不使用的差別焚鹊,使用了synchronized后線程是一個一個執(zhí)行的痕届,只有在當前線程執(zhí)行完畢后,其他線程才能執(zhí)行寺旺。線程執(zhí)行順序與系統(tǒng)調(diào)度有關爷抓。

這里鎖對象使用的是this势决,如果一個代碼文件中有多個需要同步的線程阻塑,而且他們之間不是完全互斥的,就是說一個線程執(zhí)行的時候也可以有其他線程執(zhí)行果复,也就是有多個線程對(或者叫線程組吧)陈莽,那么此時鎖對象就需要自己指定啦∷涑可以自定義一個:

Object lock = new Object();

這里的lock對象唯一的作用就是充當鎖對象走搁。

使用多把鎖的情況:

package demo;

/**
 * 描述:對象鎖,同步代碼塊
 */
public class SynchronizedObjectCodeBlock2 implements Runnable{
    static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
    Object lock1 = new Object();
    Object lock2 = new Object();
    @Override
    public void run() {
        synchronized (lock1) {
            System.out.println("我是lock1迈窟, " + Thread.currentThread().getName());
            try {
                //休眠3s私植,使效果更明顯
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " lock1運行結束!");
        }
        synchronized (lock2) {
            System.out.println("我是lock2车酣, " + Thread.currentThread().getName());
            try {
                //休眠3s曲稼,使效果更明顯
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " lock2運行結束索绪!");
        }
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }
}

運行結果:

我是lock1, Thread-0


Thread-0 lock1運行結束贫悄!
我是lock2瑞驱, Thread-0
我是lock1, Thread-1


Thread-0 lock2運行結束窄坦!
Thread-1 lock1運行結束唤反!
我是lock2, Thread-1


Thread-1 lock2運行結束鸭津!
Finished!

Process finished with exit code 0

這里我把輸出結果根據(jù)輸出等待時間分成了四部分彤侍。

第一部分是Thread0先拿到lock1這把鎖,那么thread1執(zhí)行run方法時逆趋,首先進入lock1的代碼塊判斷l(xiāng)ock1是否可用拥刻,這里因為thread0占據(jù)了lock1,所以此時thread1進入等待狀態(tài)。

第二部分父泳,3s后般哼,thread0執(zhí)行完釋放了lock1,然后立即往下執(zhí)行,進入lock2的代碼區(qū)域惠窄,拿到lock2這把鎖蒸眠,而thread1在等待thread0釋放lock1后立即獲取到lock1這把鎖。

第三部分杆融,3s后楞卡,thread0運行完畢釋放lock2這把鎖,thread1也運行完畢釋放lock1這把鎖脾歇,然后thread1進入lock2的代碼區(qū)域蒋腮。此時lock2已經(jīng)被thread0釋放了,所以thread1可以順利獲取到lock2,開始執(zhí)行l(wèi)ock2的代碼塊藕各。

第四部分池摧,thread1執(zhí)行完畢lock2的代碼塊,程序執(zhí)行完畢激况,退出髓考。

<span id = "anchor">普通方法鎖</span>

這里將run方法內(nèi)的代碼放到一個方法中徘禁,然后使用synchronized修飾這個方法,功能與使用synchronized代碼塊是一樣的。

package demo;
public class SynchronizedObjectMethod3 implements Runnable{
    static SynchronizedObjectMethod3 instance = new SynchronizedObjectMethod3();
    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        System.out.println("對象鎖的方法修飾形式妹田, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束呕寝!");
    }
}

運行結果:

對象鎖的方法修飾形式达舒, Thread-0

Thread-0 運行結束只嚣!
對象鎖的方法修飾形式, Thread-1

Thread-1 運行結束洛波!
Finished!

Process finished with exit code 0

將結果按輸出延遲分成了三部分胰舆。

第一部分是t1獲取到method的鎖逻杖,開始執(zhí)行method方法。此時t2執(zhí)行到method時由于method的鎖已經(jīng)被占據(jù)思瘟,所有t2進入Blocked狀態(tài)等待鎖解除荸百。

第二部分是t1運行3s后,解除鎖滨攻,然后t2獲取鎖够话,執(zhí)行method內(nèi)的方法。

第三部分光绕,t2執(zhí)行完畢女嘲,程序退出。

類鎖

Java類可能有多個對象诞帐,但是只有一個Class對象欣尼。

所謂的類鎖,就是不同的Class對象的鎖而已停蕉。

類鎖的效果就是在同一時刻愕鼓,只能有一個對象擁有鎖。

類鎖分兩種:

  • Synchronized修飾的靜態(tài)方法

  • 指定鎖為Class對象慧起,synchronized(*.class)

<span id = "anchor1">Synchronized修飾的靜態(tài)方法</span>

在使用synchronized修飾的方法不是靜態(tài)方法時菇晃,不同實例的線程是可以同時訪問該方法的。

package demo;

public class SynchronizedClassStatic4 implements Runnable {
    static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
    static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        System.out.println("類鎖的第一種形式蚓挤,使用synchronized修飾static方法磺送, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束!");
    }
}

運行結果:

類鎖的第一種形式灿意,使用synchronized修飾static方法估灿, Thread-0
類鎖的第一種形式,使用synchronized修飾static方法缤剧, Thread-1

Thread-0 運行結束馅袁!
Thread-1 運行結束!
Finished!

Process finished with exit code 0

可以看到兩個線程同時訪問到了method方法鞭执。

加上static修飾method方法后司顿,代碼如下:

package demo;

public class SynchronizedClassStatic4 implements Runnable {
    static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
    static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        method();
    }

    public static synchronized void method() {
        System.out.println("類鎖的第一種形式,使用synchronized修飾static方法兄纺, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束!");
    }
}

運行結果:

類鎖的第一種形式化漆,使用synchronized修飾static方法估脆, Thread-0

Thread-0 運行結束!
類鎖的第一種形式座云,使用synchronized修飾static方法疙赠, Thread-1

Thread-1 運行結束付材!
Finished!

Process finished with exit code 0

可以看到,當thread0獲取到方法鎖時圃阳,thread1是無法同時執(zhí)行的厌衔,只能等thread0執(zhí)行完畢釋放鎖后thread1才能執(zhí)行。

所以如果遇到一些情況需要在全局同步某個方法捍岳,而不僅僅是一個對象的層面或者是一個小范圍時富寿,就應該使用static修飾和保護被同步的方法。

指定鎖為Class對象

使用synchronized修飾Class锣夹,上代碼吧页徐。

package demo;

public class SynchronizedClassClass5 implements Runnable {
    static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
    static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        method();
    }

    public void method() {
        synchronized (SynchronizedClassClass5.class) {
            System.out.println("類鎖的第二種形式,使用synchronized修飾Class银萍, " + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 運行結束变勇!");
        }
    }
}

運行結果:

類鎖的第二種形式,使用synchronized修飾Class贴唇, Thread-0

Thread-0 運行結束搀绣!
類鎖的第二種形式,使用synchronized修飾Class戳气, Thread-1

Thread-1 運行結束豌熄!
Finished!

Process finished with exit code 0

可以看到,這里是對SynchronizedClassClass5的Class加鎖了物咳,所有SynchronizedClassClass5的實例都會使用這把鎖锣险,在t1獲取到鎖的時候t2進入Blocked狀態(tài),t1執(zhí)行完后t2再執(zhí)行览闰。

如果把SynchronizedClassClass5.class改成this芯肤,那么instance1和instance2就不是共用一把鎖啦,他們可以同時執(zhí)行method方法压鉴。將method改為如下代碼:

public void method() {
    synchronized (this) {
        System.out.println("類鎖的第二種形式崖咨,使用synchronized修飾Class, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束油吭!");
    }
}

最后執(zhí)行結果:

類鎖的第二種形式击蹲,使用synchronized修飾Class, Thread-0
類鎖的第二種形式婉宰,使用synchronized修飾Class歌豺, Thread-1

Thread-0 運行結束!
Thread-1 運行結束心包!
Finished!

Process finished with exit code 0

印證了我們的猜想类咧。

不使用并發(fā)手段那個例子的解決方法

  1. 使用對象鎖中的同步代碼塊鎖,在run方法中加入synchronized同步代碼塊,即:

    @Override
    public void run() {
        synchronized (this) {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }
    
  2. 使用對象鎖中的普通方法鎖痕惋,在run方法前面加上synchronized關鍵字修飾区宇,即:

    @Override
    public synchronized void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
    
  3. 使用類鎖中的修飾靜態(tài)方法,即:

    @Override
    public void run() {
        method();
    }
    
    public synchronized static void method() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
    
  4. 使用類鎖中的修飾Class對象的方法值戳,即:

    @Override
    public void run() {
        synchronized (DisappearedRequest1.class) {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    }
    

常見問題

  1. 兩個線程同時訪問一個對象的同步方法议谷。

使用synchronized修飾普通方法模式修飾的是this對象。

兩個線程訪問同一個對象的同步方法時堕虹,會是互斥的卧晓。因為兩個線程來自同一個實例,一個線程獲取到這個鎖后另一個線程對這個鎖也是可見的鲫凶。最后的結果就是當線程1獲取到鎖后禀崖,線程2進入Blocked狀態(tài),等線程1執(zhí)行完該同步方法釋放鎖后螟炫,線程2才能開始執(zhí)行同步方法波附。參考上面的普通方法鎖相關的內(nèi)容。

  1. 兩個線程訪問的是兩個對象的同步方法昼钻。

兩個線程在訪問同一個同步方法時掸屡,不會存在互斥,也就是兩個方法會同時執(zhí)行然评,互不影響仅财。因為兩個線程來自不同實例,他們采用的鎖對象不是同一個碗淌。一個線程獲取到鎖后跟另一個線程沒有任何關系盏求,所以另一線程也會繼續(xù)執(zhí)行,最后的結果就是兩個線程都同時執(zhí)行亿眠∷榉#可以參考上面的Synchronized修飾的靜態(tài)方法中不加static修飾method的情況。

  1. 兩個線程訪問的是synchronized的靜態(tài)方法纳像。

當兩個線程訪問synchronized修飾的靜態(tài)方法時荆烈,不管這兩個線程是不是同一個實例創(chuàng)建的,只要他們是同一個類的實例竟趾,他們就會反問到同一個鎖憔购,所以是互斥的〔砻保可以參考上面的Synchronized修飾的靜態(tài)方法中加上static修飾method的情況玫鸟。

  1. 同時訪問同步方法和非同步方法

先上代碼:

package demo;

public class SynchronizedYesAndNo6 implements Runnable{

    static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            method1();
        } else {
            method2();
        }
    }

    public synchronized void method1() {
        System.out.println("這是加鎖的方法, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束山卦!");
    }

    public void method2() {
        System.out.println("這是沒有加鎖的方法鞋邑, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束诵次!");
    }
}

該實例中兩個線程皆由同一個實例創(chuàng)建账蓉,run方法中枚碗,讓thread0調(diào)用method1,讓thread1調(diào)用method2铸本,運行結果如下:

這是沒有加鎖的方法肮雨, Thread-1
這是加鎖的方法, Thread-0

Thread-1 運行結束箱玷!
Thread-0 運行結束怨规!
Finished!

Process finished with exit code 0

由于method1是加鎖的,所以thread0運行到method1時锡足,獲取到鎖波丰,然后進入休眠狀態(tài)。method2沒有鎖舶得,thread1在執(zhí)行method2時沒有任何阻礙掰烟,直接執(zhí)行,所以看起來的效果就是thread1和thread2同時執(zhí)行了沐批,因為二者一個有鎖一個沒鎖纫骑,互不影響。

  1. 訪問同一個對象的不同的普通同步方法

將第四問代碼中的method2改成同步方法就是該問題的測試代碼九孩,我比較懶先馆,就不重新創(chuàng)建代碼了,直接將method2改為 , 算了躺彬,還是貼全一點吧:

package demo;

public class SynchronizedDifferentMthod7 implements Runnable{

    static SynchronizedDifferentMthod7 instance = new SynchronizedDifferentMthod7();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            method1();
        } else {
            method2();
        }
    }

    public synchronized void method1() {
        System.out.println("這是第一個加鎖的方法煤墙, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束!");
    }

    public synchronized void method2() {
        System.out.println("這是第二個加鎖的方法宪拥, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束仿野!");
    }
}

運行代碼,結果是:

這是第一個加鎖的方法江解, Thread-0

Thread-0 運行結束设预!
這是第二個加鎖的方法, Thread-1

Thread-1 運行結束犁河!
Finished!

Process finished with exit code 0

由于兩個線程是同一個實例創(chuàng)建鳖枕,而且synchronized修飾的是普通方法,也就相當與修飾的是普通的this對象桨螺,所以這兩個線程訪問到兩個方法時碰到的鎖是同一個鎖宾符,最終執(zhí)行時就是誰先獲取到鎖誰先執(zhí)行。

  1. 同時訪問靜態(tài)synchronized和非靜態(tài)synchronized方法

貼代碼:

package demo;

public class SynchronizedStaticAndNormal8 implements Runnable{
    static SynchronizedStaticAndNormal8 instance = new SynchronizedStaticAndNormal8();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            method1();
        } else {
            method2();
        }
    }

    public synchronized static void method1() {
        System.out.println("這是第一個靜態(tài)加鎖的方法灭翔, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束魏烫!");
    }

    public synchronized void method2() {
        System.out.println("這是第二個非靜態(tài)加鎖的方法, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束!");
    }
}

運行結果:

這是第一個靜態(tài)加鎖的方法哄褒, Thread-0
這是第二個非靜態(tài)加鎖的方法稀蟋, Thread-1

Thread-0 運行結束!
Thread-1 運行結束呐赡!
Finished!

Process finished with exit code 0

這是因為使用synchronized修飾靜態(tài)方法method1時退客,它的鎖對象是該實例類的Class對象。而使用synchronized修飾的非靜態(tài)方法method2時链嘀,它的鎖對象是this對象也就是該實例對象本身萌狂,method1和method2兩個觸發(fā)的鎖是不同的鎖,所以二者互補影響怀泊,同時執(zhí)行茫藏。

  1. 方法拋出異常后,會釋放鎖嗎霹琼?

會的务傲,設置一個這樣的場景驗證:兩個線程同時運行,第一個線程拋出異常后碧囊,看第二個線程是否會立即進入同步方法树灶,如果能成功進入,那就意味著鎖已經(jīng)釋放糯而。

上代碼:

package demo;

public class SynchronizedException9 implements Runnable {

    static SynchronizedException9 instance = new SynchronizedException9();

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while(t1.isAlive() || t2.isAlive()) {
        }
        System.out.println("Finished!");
    }

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")) {
            method1();
        } else {
            method2();
        }
    }

    public synchronized void method1() {
        System.out.println("這是第一個方法天通, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
            throw new Exception();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束!");
    }

    public synchronized void method2() {
        System.out.println("這是第二個方法熄驼, " + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 運行結束像寒!");
    }
}

運行結果:

這是第一個方法, Thread-0

java.lang.Exception
    at demo.SynchronizedException9.method1(SynchronizedException9.java:30)
    at demo.SynchronizedException9.run(SynchronizedException9.java:20)
    at java.lang.Thread.run(Thread.java:748)
Thread-0 運行結束瓜贾!
這是第二個方法诺祸, Thread-1

Thread-1 運行結束!
Finished!

Process finished with exit code 0

可以看到祭芦,第一個方法拋出異常后筷笨,第二個方法能夠正常獲取鎖然后執(zhí)行method2,說明拋出異常后會釋放try代碼塊中獲取的鎖。這里釋放鎖的操作是jvm自動完成龟劲,后面會有介紹里面的詳細原理胃夏。

7中常見問題總結(3個核心思想)

  1. 一把鎖同時只能被一個線程獲取,沒拿到鎖的線程必須等待其他線程釋放鎖才能獲取到鎖昌跌,對應問題1,5

  2. 每個實例都對應有自己的一把鎖仰禀,不同實例之間互不影響,但是有例外情況:鎖的對象是Class以及synchronized修飾的是static方法的時候蚕愤,所有對象共用一把類鎖答恶,對應2,3,4,6情況饺蚊。

  3. 無論方法是正常執(zhí)行完畢還是拋出異常而結束,都會釋放鎖悬嗓,對應問題7

終極一問: 如果在synchronized修飾的方法中調(diào)用了普通方法(沒有synchronized修飾)污呼,那么這個被synchronized修飾的方法還是線程安全的嗎?

答案是:NO烫扼,因為一旦調(diào)用了不加鎖的方法(不用synchronized修飾)曙求,那么其他線程也可能會同時調(diào)用那個不加鎖的方法碍庵,導致當前被synchronized修飾的方法變成線程不安全方法映企。

Synchronized性質(zhì)

可重入

可重入指的是同一線程的外層函數(shù)獲得鎖后,內(nèi)層函數(shù)可以直接再次獲取該鎖静浴。

好處是:避免死鎖堰氓,提升封裝性。

假設有兩個方法fun1,fun2苹享,fun1中調(diào)用fun2,他們都被synchronized修飾双絮,如果線程1執(zhí)行到了fun1,并且獲得了fun1的鎖得问。假設synchronized不具有可重入性囤攀,由于fun2的鎖跟fun1的鎖是同一把鎖,所以線程訪問到fun2時會等待鎖的解除宫纬,而fun1卻一直在等待fun2的結束焚挠,最后就是fun2一直等待fun1釋放鎖,而fun1卻一直在等待fun2執(zhí)行完成漓骚,所就會導致死鎖的發(fā)生蝌衔。

另外可重入性避免了一次次解鎖加鎖操作,提升了封裝性蝌蹂。

粒度:線程而非調(diào)用(用3種情況說明和pthread的區(qū)別)噩斟。只要在當前線程中獲取到了一個鎖那么這個鎖可以被當前線程的其他部分使用,不需要重新獲取和釋放鎖孤个。

情況1:證明同一個方法是可重入的

先從一個synchronized方法調(diào)用自身說起剃允,如果能夠調(diào)用成功則說明synchronized修飾的方法具有可重入性,如果進入死鎖狀態(tài)齐鲤,則說明synchronized修飾的方法不具有可重入性斥废。代碼如下:

package demo;

public class SynchronizedRecursion10 {

    int a = 0;

    public static void main(String[] args) {
        SynchronizedRecursion10 synchronizedRecursion10 = new SynchronizedRecursion10();
        synchronizedRecursion10.method1();
    }

    private synchronized void method1() {
        System.out.println("method1, a = " + a);
        if (a ==0) {
            a++;
            method1();
        }
    }
}

運行結果:

method1, a = 0
method1, a = 1

Process finished with exit code 0

程序正確調(diào)用了自身,說明synchronized修飾的方法是具有可重入性的佳遂。

情況2:證明可重入不要求是同一個方法营袜。

建立兩個synchronized修飾的方法method1和method2,使用method1調(diào)用method2,如果能夠調(diào)用成功,那么可以證明可重入性要求調(diào)用的是同一個方法丑罪,如果不能調(diào)用成功而進入死鎖狀態(tài)荚板,則說明可重入不要求同一個方法凤壁。代碼如下:

package demo;

public class SynchronizedOtherMethod11 {
    public synchronized void method1() {
        System.out.println("mtheod1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        SynchronizedOtherMethod11 synchronizedOtherMethod11 = new SynchronizedOtherMethod11();
        synchronizedOtherMethod11.method1();
    }
}

運行結果:

mtheod1
method2

Process finished with exit code 0

結果表明可重入不要求是同一個方法。

情況3:證明可重入不要求是同一個類中的跪另。

使用一個子類繼承父類的方法拧抖,該方法在子類和父類中都用synchronized修飾,然后在子類中調(diào)用父類的方法免绿,看是否能調(diào)用成功唧席,如果能調(diào)用成功,則說明可重入不要求是同一個類中嘲驾,代碼如下:

package demo;

public class SynchronizedSuperClass12 {

    public synchronized void doSomthing() {
        System.out.println("我是父類方法");
    }
}

class TestClass extends SynchronizedSuperClass12 {
    public synchronized void doSomthing() {
        System.out.println("我是子類方法");
        super.doSomthing();
    }

    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        testClass.doSomthing();
    }
}

運行結果:

我是子類方法
我是父類方法

Process finished with exit code 0

不可中斷

一旦一個鎖被其他線程獲取了淌哟,如果還想獲得,只能選擇等待其他線程釋放鎖或者進入阻塞狀態(tài)辽故,直到其他線程釋放鎖徒仓。如果其他線程永遠不釋放鎖,那么就會永遠等待下去誊垢。

相對來說掉弛,Lock類具有中斷能力。首先喂走,如果覺得等待時間太長殃饿,程序有權中斷現(xiàn)在已經(jīng)獲取到鎖的線程;其次芋肠,如果覺得等待時間太久不想等了乎芳,也可以選擇退出等待。

底層原理

加鎖和釋放鎖的原理

現(xiàn)象: 每個類的實例對應一把鎖业栅,每個synchronized修飾的方法都必須首先獲得調(diào)用該方法的類的實例的鎖才能執(zhí)行秒咐,否則線程就會阻塞。方法一旦執(zhí)行碘裕,就會獨占這把鎖携取,直到該方法返回或者拋出異常才釋放鎖,然后其他線程才能獲取這把鎖帮孔,進入可執(zhí)行狀態(tài)雷滋。

這意味著如果一個對象中有synchronized修飾的方法或者代碼塊,要想執(zhí)行這段代碼就必須先獲得對象的實例對象鎖文兢。如果此對象的鎖已經(jīng)被其他調(diào)用著占用晤斩,就必須等待鎖被釋放。所有的java對象都有一個互斥鎖姆坚,由jvm自動獲取和釋放澳泵,我們只需要指定這個對象即可,至于鎖的獲取和釋放不用關系兼呵,jvm會幫你做兔辅。

獲取和釋放鎖的時機:內(nèi)置鎖

每一個java對象都可以作為實現(xiàn)同步的鎖腊敲,這個鎖就稱為內(nèi)置鎖,或者叫監(jiān)視器鎖(Monitor Lock)维苔,線程在進入和退出同步代碼塊前后會自動獲取和釋放這個鎖碰辅,無論是正常途徑推出還是拋出異常推出,都會釋放鎖介时。獲取內(nèi)置鎖的唯一途徑就是進入內(nèi)部同步代碼塊方法中没宾。

等價代碼

以一段代碼為例,創(chuàng)建兩個等價的方法method1和method2,method1使用synchronized修飾沸柔,method2使用Lock鎖實現(xiàn)類似與method1的鎖形式循衰,代碼如下:

package demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SynchronizedToLock13 {

    Lock lock = new ReentrantLock();

    public synchronized void method1() {
        System.out.println("這是synchronized鎖!");
    }
    //method2與method1等價
    public void method2() {
        lock.lock();
        try {
            System.out.println("這是Lock鎖!");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SynchronizedToLock13 synchronizedToLock13 = new SynchronizedToLock13();
        synchronizedToLock13.method1();
        synchronizedToLock13.method2();
    }
}

運行結果如下:

這是synchronized鎖勉失!
這是Lock鎖!

Process finished with exit code 0

深入JVM看字節(jié)碼:反編譯羹蚣、monitor指令

java對象的頭字段中有一個表示鎖狀態(tài)的標記,在獲取和釋放鎖的時候是基于monitor對象實現(xiàn)的乱凿,monitor主要有兩個指令monitor enter(插入到方法開始的位置)和monitor exit(插入到方法結束的位置),JVM規(guī)定每一個monitorenter都會有monigtorexit與之對應咽弦,但是一個monitorenter可能會有多個monitorexit徒蟆,因為退出的時機可能是多種多樣的。

每個對象都有一個monitor與之關聯(lián)型型,一旦持有了一個monitor段审,該對象就會處于鎖定狀態(tài),當線程執(zhí)行到monitor enter這個指令時闹蒜,將會嘗試獲取這個對象所持有的monitor的所有權寺枉,也就是嘗試獲取對象的鎖。

反編譯

創(chuàng)建一個實例代碼命名為Decompilation14.java:

package demo;

public class Decompilation14 {
    private Object object = new Object();

    public void insert(Thread thread) {
        synchronized (object) {

        }
    }
}

在命令行進行編譯:

javac Decompilation14.java 

可以看到反編譯的信息:

Code:
    stack=2, locals=4, args_size=2
        0: aload_0
        1: getfield      #3                  // Field object:Ljava/lang/Object;
        4: dup
        5: astore_2
        6: monitorenter
        7: aload_2
        8: monitorexit
        9: goto          17
    12: astore_3
    13: aload_2
    14: monitorexit
    15: aload_3
    16: athrow
    17: return

第6行的monitorenter表示進入同步代碼塊绷落,獲取到monitor對象也就是獲取到對象的鎖姥闪。第8和14行表示離開同步代碼塊,釋放鎖砌烁。

其實monitorenter和monitorexit指令在執(zhí)行的時候會將對象的鎖計數(shù)加1或者減1,monitor同一時刻只能被一個對象獲得筐喳,線程在嘗試獲得與當前對象關聯(lián)的monitor的所有權的時候,monitor只會發(fā)生下面三種情況:

  1. monitor計數(shù)器為0,表示目前該monitor還沒有被其他進程占據(jù)函喉,當前線程會立即獲得monitor對象避归,并把計數(shù)器加1,此后其他線程想要獲取monitor的時候看到計數(shù)器為1,就知道當前對象的monitor已經(jīng)被其他線程占有了管呵。

  2. 當前線程已經(jīng)拿到了monitor的所有權梳毙,而且已經(jīng)重入,這回導致monitor的計數(shù)器累加捐下,計數(shù)器變?yōu)?或者3等.

  3. monitor已經(jīng)被其他線程持有账锹,當前線程想要獲取的時候無法獲取堂氯,進入阻塞狀態(tài),直到monitor計數(shù)器變?yōu)?后再嘗試獲取monitor牌废。

monitorexit的操作很簡單咽白,就是將monitor計數(shù)器減1,一旦monitor的計數(shù)器減為0,就表示當前線程不再具備monitor的所有權,也就是完全釋放了鎖鸟缕。如果減完后晶框,monitor計數(shù)器不是0,就表示剛才是重入進來的,當前線程還是持有這把鎖懂从。

可重入原理

可重入是利用了加鎖次數(shù)計數(shù)器實現(xiàn)的授段,JVM負責跟蹤對象被加鎖的次數(shù),也就是monitor中計數(shù)器番甩。

線程第一次對象加鎖的時候侵贵,計數(shù)器變?yōu)?,每當這個線程在次對象上再次獲得鎖是,計數(shù)器就加1缘薛,只有首先在這個對象上獲取到鎖的線程可以再次獲取這把鎖窍育。

當任務離開時,計數(shù)器減1,當計數(shù)器為0的時候宴胧,鎖被完全釋放漱抓。

保證可見性的原理

Java內(nèi)存模型:

Java內(nèi)存模型

各個線程會將主內(nèi)存中的共享變量復制一份放到本地內(nèi)存,這樣做的原因是加速程序運行恕齐。但是這樣做會帶來一些風險乞娄,線程A要跟線程B通信需要現(xiàn)將數(shù)據(jù)寫入主內(nèi)存,然后線程B再去主內(nèi)存中讀取數(shù)據(jù)到本地內(nèi)存B,這部分是JVM進行控制的显歧。

線程通信

使用synchronized修飾的代碼在釋放鎖之前都要將線程內(nèi)存中的數(shù)據(jù)寫入到主內(nèi)存中仪或,在進入代碼塊得到鎖后,被鎖定的對象的數(shù)據(jù)會立即從主內(nèi)存中讀取到本地線程內(nèi)存中士骤。

Synchronized的缺陷

  1. 效率低范删。鎖的釋放情況少,試圖獲得鎖的時候不能設定超時時間敦间,不能中斷一個正在試圖獲得鎖的線程瓶逃。

鎖的釋放情況少是因為只有在以下兩種情況下才會釋放鎖:線程執(zhí)行完代碼后釋放鎖和代碼執(zhí)行過程中出現(xiàn)異常釋放鎖。

如果遇到IO阻塞廓块,而鎖又不能釋放厢绝,這就會出現(xiàn)效率問題。

  1. 不夠靈活(讀寫鎖更靈活):加鎖和釋放的時機單一带猴,每個鎖僅有單一的條件(某個對象)昔汉,不夠靈活。比如讀寫鎖,它的加鎖和解鎖的時機是非常靈活的靶病,在讀操作的時候不加鎖会通,只在寫的時候才加鎖,大大提高程序執(zhí)行效率娄周。

  2. 無法知道是否成功獲取到鎖涕侈。

常見面試問題

  1. 使用synchronized時需要注意些什么?

使用時需要注意鎖對象不能為空煤辨,作用域不宜過大裳涛,要避免死鎖。

鎖對象為空是众辨,鎖的信息無法保存到對象頭中端三,所以鎖是不起任何作用的。

作用域過大時鹃彻,就變成串行變成了郊闯,失去了多線程的意義。

  1. 如何選擇Lock和synchronized關鍵字蛛株?

如果可以团赁,即不要使用Lock也不要使用synchronized,而是使用java.util.concurrent包中的各種類泳挥,更方便也更不容易出錯然痊。

如果synchronized在程序中適用,優(yōu)先使用synchronized屉符,因為這樣可以減少代碼的編寫,也就減少了代碼出錯的幾率锹引。

如果特別需要用到Lock獨有的特性時才考慮使用Lock矗钟。

  1. 多線程訪問同步方法的各種具體情況?

參考上面的各種問題嫌变。

思考題

  1. 多個線程等待同一個synchronized鎖的時候吨艇,JVM是如何選擇下一個獲取鎖的是哪個線程?

這涉及到JVM內(nèi)部對于鎖的調(diào)度機制腾啥,持有鎖的進程釋放鎖后东涡,競爭鎖的進程包括之前就在等待的,還包括現(xiàn)在剛剛到來的線程倘待,誰能獲取到這把鎖是不公平的狀態(tài)疮跑,誰都有可能獲取,根據(jù)JVM版本和算法的不同而不同凸舵。

  1. synchronized使得同時只有一個線程執(zhí)行祖娘,性能較差,有什么辦法可以提升性能啊奄?

優(yōu)化使用范圍桩蓉,防止誤操作挽霉,充分發(fā)揮多線程的功能浪汪。

使用其他類型的鎖比如讀寫鎖Lock。

  1. 我想更靈活地控制鎖的獲取和釋放(現(xiàn)在釋放鎖的時機都被規(guī)定死了)仪吧,怎么辦?

可以自己實現(xiàn)一個Lock接口鞠眉,這樣鎖的控制和釋放就完全由自己控制了薯鼠,可以參考一些已經(jīng)實現(xiàn)的已有的鎖。

  1. 什么是鎖的升級凡蚜、降級人断?什么是JVM里的偏斜鎖、輕量級鎖朝蜘、重量級鎖恶迈?

balabala

總結

一句話介紹synchronized:

JVM會自動通過使用monitor來加鎖和解鎖,保證了同時只有一個線程可以執(zhí)行指定代碼谱醇,從而保證了線程安全暇仲,同時具有可重入和不可中斷的性質(zhì)。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末副渴,一起剝皮案震驚了整個濱河市奈附,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌煮剧,老刑警劉巖斥滤,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異勉盅,居然都是意外死亡佑颇,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門草娜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挑胸,“玉大人,你說我怎么就攤上這事宰闰〔绻螅” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵移袍,是天一觀的道長解藻。 經(jīng)常有香客問我,道長咐容,這世上最難降的妖魔是什么舆逃? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上路狮,老公的妹妹穿的比我還像新娘虫啥。我一直安慰自己,他們只是感情好奄妨,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布涂籽。 她就那樣靜靜地躺著,像睡著了一般砸抛。 火紅的嫁衣襯著肌膚如雪评雌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天直焙,我揣著相機與錄音景东,去河邊找鬼。 笑死奔誓,一個胖子當著我的面吹牛斤吐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播厨喂,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼和措,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蜕煌?” 一聲冷哼從身側響起派阱,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎斜纪,沒想到半個月后贫母,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡盒刚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年颁独,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伪冰。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖樟蠕,靈堂內(nèi)的尸體忽然破棺而出贮聂,到底是詐尸還是另有隱情,我是刑警寧澤寨辩,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布吓懈,位于F島的核電站,受9級特大地震影響靡狞,放射性物質(zhì)發(fā)生泄漏耻警。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望甘穿。 院中可真熱鬧腮恩,春花似錦、人聲如沸温兼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽募判。三九已至荡含,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間届垫,已是汗流浹背释液。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留装处,地道東北人误债。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像符衔,于是被迫代替她去往敵國和親找前。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容