相信大多數同學在開始接觸併發程式設計的時候,首先了解的就是synchronized關鍵字的修飾,被synchronized修飾的方法或程式碼塊都可以解決多執行緒安全問題。在Java SE1.6版本之前,我們稱之為重量級鎖。因為它在獲取共享鎖的時候是對CPU的獨佔鎖,以至於在當前執行緒釋放鎖之前,其他執行緒均處於阻塞狀態。這樣雖然保證了多執行緒安全的問題,卻反而影響了多執行緒任務的執行效率。在Java SE1.6版本之後,針對Synchronized已經做了很多的優化來減少獲得鎖和釋放鎖所帶來的效能消耗,其中最重要的就是偏向所和輕量級鎖的引入,以及鎖的儲存結構和升級過程。但我們仍然認為synchronized是一個比較消耗效能的併發操作,對此我們會在後面其他併發鎖實現中做比較得出結論。

        當然首先我們還是得了解synchronized的應用,然後再理解它的實現原理。

synchronized應用

        synchronized實現同步的基礎:Java中的每一個物件都可以作為鎖。具體表現形式有三種:

        1、對於普通同步方法,鎖是當前例項物件;

        2、對於靜態同步方法,鎖是當前類的Class物件;

        3、對於同步方法塊,鎖是Synchronized括號裡配置的物件。

        怎麼理解這三種形式,讓我通過具體程式碼看下。

.普通同步方法

public class SynchronizedTest {
    
    public synchronized void doSomething(){
        try {
            System.out.println(Thread.currentThread().getName() + " start");
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + " end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SynchronizedTest s = new SynchronizedTest();
        for(int i = 0; i<5; i++){
            Thread t= new MyThread();
            t.start();
        }
        
    }
    
    
}

class MyThread extends Thread{

    SynchronizedTest s = new SynchronizedTest();
    
    @Override
    public void run(){
        s.doSomething();
    }

}

看一下執行結果:

Thread-1 start
Thread-0 start
Thread-2 start
Thread-3 start
Thread-4 start
Thread-1 end
Thread-0 end
Thread-2 end
Thread-3 end
Thread-4 end

多執行緒同步好像沒有起作用,各自執行緒執行各自的,彼此沒有影響,為什麼?因為普通同步方法鎖物件是當前方法的例項物件本身,即例項化物件Synchronized。我們發現每個多執行緒內部使用的例項化物件Synchronized都是執行緒自己建立的,並沒有鎖住同一個物件。

我們嘗試修改上面這段程式碼:

public class SynchronizedTest {
    
    public synchronized void doSomething(){
        try {
            System.out.println(Thread.currentThread().getName() + " start");
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName() + " end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SynchronizedTest s = new SynchronizedTest();
        for(int i = 0; i<5; i++){
            Thread t= new MyThread(s);
            t.start();
        }
        
    }
    
    
}

class MyThread extends Thread{
    
    private SynchronizedTest s;
    
    public MyThread(SynchronizedTest s){
        this.s = s;
    }
    
    @Override
    public void run(){
        s.doSomething();
    }

}

執行結果:

Thread-0 start
Thread-0 end
Thread-3 start
Thread-3 end
Thread-4 start
Thread-4 end
Thread-2 start
Thread-2 end
Thread-1 start
Thread-1 end

當我們對呼叫該同步方法的同一個例項物件傳入執行緒內部的時候,即達到了多執行緒同步效果,因為此時我們鎖住的是同一個例項物件。故普通同步方法有一個明顯的缺點就是,鎖住的物件為呼叫該方法的例項物件,太重量級,一般我們只要鎖住方法內部需要同步的物件即可,所以我們更建議使用同步方法塊。

.同步方法塊

public class SynchronizedBlock {
    
    public void doSomething(){
        Student student = new Student();
        student.setGrade(1);
        for (int i = 0; i < 5; i++) {
            new MyThread1(student).start();
        }
    }

    public static void main(String[] args) {
        SynchronizedBlock s = new SynchronizedBlock();
        s.doSomething();
    }
    
}

class MyThread1 extends Thread{
    
    private Student student;
    
    public MyThread1(Student student){
        this.student = student;
    }
    
    @Override
    public void run(){
        synchronized (student) {
            try {
                Thread.sleep(100);
                student.setGrade(student.getGrade()+1);
                System.out.println(student.getGrade());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class Student{
    
    private Integer grade;

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }
}

可以嘗試去掉run方法內部的同步程式碼,你會發現得到的是一堆無邏輯的數字。同步方法塊,即傳入我們需要同步的物件即可,這個物件可以是你需要同步例項下的某個物件(操作變數),也可以是當前的例項物件(即this),也可以是當前類的例項物件(class物件),已符合我們實際應用中大部分需求。

.靜態同步方法

Synchronized修飾靜態方法,實際上是對該類物件加鎖,俗稱“類鎖”。注意這裡的類物件不是指當前呼叫此方法的某個具體類物件,而是指當前的Class類,更深入一點理解是類載入器載入編譯後位元組碼檔案所儲存的空間物件。

為了驗證加鎖物件的範圍,我們可以在一個類中定義2個不同的靜態同步方法。

public class SynchronizedStaticTest {
        
    public synchronized static void test1(){
        try {
            for( int i = 0; i < 3 ; i++) {
                System.out.println("test1 ->"+ i+ " start....");
                Thread.sleep(100);
                System.out.println("test1 ->"+ i+ " end....");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized static void test2(){
        try {
            for( int i = 0; i < 3 ; i++) {
                System.out.println("test2 ->"+ i+ " start....");
                Thread.sleep(100);
                System.out.println("test2 ->"+ i+ " end....");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new MyThreadDemo1().start();
        new MyThreadDemo2().start();
    }
    
    
}

class MyThreadDemo1 extends Thread{

    SynchronizedStaticTest t = new SynchronizedStaticTest();
    
    @Override
    public void run(){
        t.test1();
    }
    
}

class MyThreadDemo2 extends Thread{

    SynchronizedStaticTest t = new SynchronizedStaticTest();
    
    @Override
    public void run(){
        t.test2();
    }

}

class StudentDemo{

    private Integer grade;

    public Integer getGrade() {
        return grade;
    }

    public void setGrade(Integer grade) {
        this.grade = grade;
    }
    
}

執行結果:

test1 ->0 start....
test1 ->0 end....
test1 ->1 start....
test1 ->1 end....
test1 ->2 start....
test1 ->2 end....
test2 ->0 start....
test2 ->0 end....
test2 ->1 start....
test2 ->1 end....
test2 ->2 start....
test2 ->2 end....

我們可以看到雖然2個方法內呼叫的執行緒都建立了新的例項物件SynchronizedStaticTest,但是2個不同的方法共同結合成一個多執行緒同步過程。即:多個不同的靜態同步方法,均對同一個CLASS類物件加了鎖。