線程同步--synchronized詳解

在Java的多線程中有兩種編程模型:異步編程模型和同步編程模型。
假設t1和t2爲兩個線程,則
異步編程模型: t1線程執行t1的,t2線程執行t2的,兩個線程之間誰也不等誰
同步編程模型:t1線程和t2線程執行,當t1線程必須等t2線程之行結束之後,t1線程才能執行,這是同步編程模型。

爲什麼要引入同步呢?
爲了數據的安全。儘管應用程序的使用率降低,但是爲了保證數據是安全的,必須加入線程同步機制。 線程同步機制使程序變成了(等同於)單線程。

什麼時候要同步呢?
第一、必須是多線程環境
第二、共享同一個數據 [ 注意:該數據必須是類中的變量(也即實例變量)對應的數據]
第三、共享的數據涉及到修改操作
必須同時滿足上面的三個條件才用同步模型。

以下示例演示採用異步編程對共享數據進行修改操作,看會出現什麼問題?

/*
 * 以下程序演示取款例子,在不使用線程同步機制,
 * 多線程同時對同一個賬戶進行操作,會出現什麼問題?
 * 
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        // 創建一個公共賬戶
        Account act = new Account("num-001", 1000.0);
        MyRunnable myRunnable = new MyRunnable(act);
        // 創建兩個線程,對同一個賬戶取款
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);

        t1.start();
        t2.start();

    }
}

class MyRunnable implements Runnable {
    Account act;

    public MyRunnable(Account act) {
        this.act = act;
    }

    @Override
    public void run() {
        act.withdraw(100.0);
        System.out.println("取款100.0成功,餘額爲:" + act.getBalance());

    }

}

class Account {
    private String actnum;
    private Double balance;

    public Account(String actnum, double balance) {
        this.actnum = actnum;
        this.balance = balance;
    }

    public String getActnum() {
        return actnum;
    }

    public void setActnum(String actnum) {
        this.actnum = actnum;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    // 對外提供一個取款的方法
    public void withdraw(double money) {
        // 計算餘額
        double after = balance - money;
        // 延遲
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // 更新
        this.setBalance(after);

    }

}

運行結果如圖 6-1 所示:
這裏寫圖片描述

圖 6-1 不使用同步對共享數據進行修改操作會出現“髒讀”

使用同步可以解決上述問題。同步的方式大概可以分爲兩種:同步方法和同步語句塊。下面分別用這兩種方式解決上述問題。
先用同步語句塊解決,對上面的代碼做如下修改

//把需要同步的代碼放到同步語句塊中
        synchronized (this) {
            // 計算餘額
            double after = balance - money;
            // 延遲
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            // 更新
            this.setBalance(after);
        }

運行結果如圖 6-2 所示
這裏寫圖片描述

圖 6-2 使用同步語句塊解決

然後用同步方法解決,對上面的代碼做如下修改

// 對外提供一個取款的方法,在方法上加上synchronized關鍵詞
    public synchronized void withdraw(double money) {
            // 計算餘額
            double after = balance - money;
            // 延遲
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            // 更新
            this.setBalance(after);



    }

運行結果如圖 6-3 所示
這裏寫圖片描述

圖 6-3 使用同步方法解決

使用synchronized(this)語句塊同步的原理:t1線程和t2線程
t1線程在執行過程中,遇到了synchronized關鍵字,就會去找this的對象鎖, 如果找到this對象鎖,則進入同步語句塊中執行程序。當同步語句塊中的代碼執行結束後, t1線程歸還this的對象鎖。

在t1線程執行同步語句塊的過程中,如果t2線程也過來執行以下代碼,也遇到synchronized 關鍵字,所以也去找this的對象鎖,但是該對象鎖被t1線程持有,只能在這等待this對象的歸還。

使用synchronized方法同步的原理:t1線程和t2線程
每個對象都有一個鎖(也稱監控器monitor),它是對象生來就有的東西(因此你不必爲此寫任何代碼)。
t1線程在執行過程中,遇到了synchronized方法,就會去找this的對象鎖, 如果找到this對象鎖,這個對象就被鎖住了。當同步方法執行結束後,t1線程歸還這個對象的對象鎖,在方法返回並且解鎖之前,誰也不能調用同一個對象的其它synchronized方法。

在t1線程執行同步方法中的代碼過程中,如果t2線程也過來執行以下代碼,也遇到synchronized 關鍵字,所以也去找this的對象鎖,但是該對象鎖被t1線程持有,只能在這等待this對象的歸還。


同步方法又能細分爲:普通同步方法和靜態同步方法。雖然這兩種方法同步效果一樣,但是本質卻是不同的:synchronized關鍵字加到static靜態方法上是給Class類上鎖,而synchronized關鍵字加到普通方法上是給對象上鎖。下面用一個示例驗證不是同一個鎖

/*
 * 驗證靜態同步方法和普通同步方法的本質區別:
 * 靜態同步方法是給Class類上鎖,普通同步方法是給對象上鎖。
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        MyRunnableA myRunnableA = new MyRunnableA(service);
        Thread a = new Thread(myRunnableA);
        a.setName("A");
        a.start();

        MyRunnableB myRunnableB = new MyRunnableB(service);
        Thread b = new Thread(myRunnableB);
        b.setName("B");
        b.start();

        MyRunnableC myRunnableC = new MyRunnableC(service);
        Thread c = new Thread(myRunnableC);
        c.setName("C");
        c.start();

    }
}

class MyRunnableA implements Runnable {
    private Service service;

    public MyRunnableA(Service service) {
        this.service = service;

    }

    @Override
    public void run() {
        service.printA();
    }

}

class MyRunnableB implements Runnable {
    private Service service;

    public MyRunnableB(Service service) {
        this.service = service;

    }

    @Override
    public void run() {
        service.printB();
    }

}

class MyRunnableC implements Runnable {
    private Service service;

    public MyRunnableC(Service service) {
        this.service = service;

    }

    @Override
    public void run() {
        service.printC();
    }

}

/*
 * 提供兩個靜態同步方法和一個普通同步方法,通過輸出結果(異步或同步)來判斷是否是不同的鎖
 */
class Service {
    public synchronized static void printA() {
        try {
            System.out.println(
                    "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA");
            // 設置延遲
            Thread.sleep(3000);
            System.out.println(
                    "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized static void printB() {

        System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB");
        System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB");
    }

    public synchronized void printC() {
        System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printC");
        System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printC");

    }

}

運行結果如圖 6-4 所示:
這裏寫圖片描述

圖 6-4 方法printC()爲異步執行

異步的原因是持有不同的鎖,printC()方法是對象鎖,printA()方法是Class鎖,而Class鎖可以對類的所有對象實例起作用。下面用一個示例進行驗證

/*
 * 驗證Class鎖可以對類的所有對象實例起作用
 */
public class Test {

    public static void main(String[] args) throws InterruptedException {
        // 設置兩個不同的對象實例
        Service service1 = new Service();
        Service service2 = new Service();
        MyRunnableA myRunnableA = new MyRunnableA(service1);
        Thread a = new Thread(myRunnableA);
        a.setName("A");
        a.start();

        MyRunnableB myRunnableB = new MyRunnableB(service2);
        Thread b = new Thread(myRunnableB);
        b.setName("B");
        b.start();
    }
}

class MyRunnableA implements Runnable {
    private Service service;

    public MyRunnableA(Service service) {
        this.service = service;

    }

    @Override
    public void run() {
        service.printA();
    }

}

class MyRunnableB implements Runnable {
    private Service service;

    public MyRunnableB(Service service) {
        this.service = service;

    }

    @Override
    public void run() {
        service.printB();
    }

}

/*
 * 提供兩個靜態同步方法
 */
class Service {
    public synchronized static void printA() {
        try {
            System.out.println(
                    "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA");
            // 設置延遲
            Thread.sleep(3000);
            System.out.println(
                    "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized static void printB() {

        System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB");
        System.out.println("線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB");
    }

}

運行結果如同 6-5 所示
這裏寫圖片描述

圖 6-5 雖然是不同對象,但靜態同步方法還是同步執行


同步語句塊又能細分爲:同步synchronized(class)語句塊、同步synchronized(this)語句塊、同步synchronized(非this對象x)語句塊和同步synchronized(string)語句塊
同步synchronized(this)語句塊在上面已經演示過,下面不再演示。
同步synchronized(class)語句塊的作用其實和synchronized static方法的作用一樣,下面用一個示例進行演示
對圖 6-5 上面的代碼做以下更改

class Service {
    public static void printA() {
        synchronized (Service.class) {
            try {
                System.out.println(
                        "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printA");
                // 設置延遲
                Thread.sleep(3000);
                System.out.println(
                        "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printA");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public synchronized static void printB() {
        synchronized (Service.class) {
            System.out.println(
                    "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "進入printB");
            System.out.println(
                    "線程名稱爲:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "離開printB");
        }
    }

}

運行結果如同 6-6 所示
這裏寫圖片描述

圖 6-6 和靜態同步方法的作用一樣,也是同步執行

同步synchronized(string)語句塊會因爲String常量池的特性而帶來問題,因此同步synchronized 語句塊都不使用String作爲鎖對象,而改用其它,比如new Object()實例化一個Object對象,但它並不放入緩存中。

在JVM中具有String常量池緩存的功能,所以如圖 6-7 所示的結果爲true。
這裏寫圖片描述

圖 6-7 Sting常量池緩存

同步synchronized(非this對象x)語句塊具有一定的優點:如果在一個類中有很多個synchronized 方法,這時雖然能實現同步,但會受到阻塞,所以影響運行效率;但如果使用同步語句塊非this對象,則synchronized (非this對象x)語句塊中的程序與同步方法是異步的,不與其他鎖this同步方法爭搶this鎖,則可以大大提高運行效率。
“synchronized (非this對象x)”格式的寫法是將x對象本身作爲“對象監視器”,這樣就可以得出以下3個結論:

  1. 當多個線程同時執行synchronized (x){}同步語句塊時呈同步效果。
  2. 當其他線程執行x對象中synchronized 同步方法時呈同步效果。
  3. 當其他線程執行x對象方法裏面的synchronized (this)語句塊時也呈同步效果。

注意:如果其他線程調用不加synchronized 關鍵字的方法時,還是異步調用。

驗證第一個結論:當多個線程同時執行synchronized (x){}同步語句塊時呈同步效果,示例如下

/*驗證當多個線程同時執行synchronized (x){}同步語句塊時呈同步效果
*/
public class Test {

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        Object object = new Object();
        ThreadA a = new ThreadA(service, object);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(service, object);
        b.setName("B");
        b.start();
    }
}

class ThreadA extends Thread {
    private Service service;
    private Object object;

    public ThreadA(Service service, Object object) {
        super();
        this.service = service;
        this.object = object;

    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }

}

class ThreadB extends Thread {
    private Service service;
    private Object object;

    public ThreadB(Service service, Object object) {
        super();
        this.service = service;
        this.object = object;

    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }

}

class Service {

    public void testMethod1(Object object) {
        synchronized (object) {
            try {
                System.out.println("testMethod1--getLock     time:" + System.currentTimeMillis() + "   run ThreadName:"
                        + Thread.currentThread().getName());
                Thread.sleep(2000);
                System.out.println("testMethod1--releaseLock time:" + System.currentTimeMillis() + "   run ThreadName:"
                        + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

運行結果如圖 6-8 所示
這裏寫圖片描述

圖 6-8 同步調用

同步的原因是使用了同一個“對象監視器”,如果使用不同的“對象監視器”就會異步執行。
對上面的代碼作以下更改:

public class Test {

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        ThreadA a = new ThreadA(service, new Object());
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(service, new Object());
        b.setName("B");
        b.start();
    }
}

運行結果如圖 6-9 所示
這裏寫圖片描述

圖 6-9 異步調用

驗證第二個結論:當其他線程執行x對象中synchronized 同步方法時呈同步效果,示例如下

public class Test {

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        MyObject object = new MyObject();
        ThreadA a = new ThreadA(service, object);
        a.setName("A");
        a.start();
        ThreadB b = new ThreadB(service, object);
        b.setName("B");
        b.start();
    }
}

class ThreadA extends Thread {
    private Service service;
    private MyObject object;

    public ThreadA(Service service, MyObject object) {
        super();
        this.service = service;
        this.object = object;

    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }

}

class ThreadB extends Thread {
    private Service service;
    private MyObject object;

    public ThreadB(Service service, MyObject object) {
        super();
        this.service = service;
        this.object = object;

    }

    @Override
    public void run() {
        super.run();
        object.speedPrintString();
    }

}

class Service {

    public void testMethod1(MyObject object) {
        synchronized (object) {
            try {
                System.out.println("testMethod1--getLock     time:" + System.currentTimeMillis() + "   run ThreadName:"
                        + Thread.currentThread().getName());
                Thread.sleep(5000);
                System.out.println("testMethod1--releaseLock time:" + System.currentTimeMillis() + "   run ThreadName:"
                        + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

class MyObject {
    public synchronized void speedPrintString() {
        System.out.println("speedPrintString--getLock     time:" + System.currentTimeMillis() + "   run ThreadName:"
                + Thread.currentThread().getName());
        System.out.println("-------------------");
        System.out.println("speedPrintString--releaseLock time:" + System.currentTimeMillis() + "   run ThreadName:"
                + Thread.currentThread().getName());
    }
}

運行結果如圖 6-10 所示
這裏寫圖片描述

圖 6-10 同步效果

驗證第三個結論:當其他線程執行x對象方法裏面的synchronized (this)語句塊時也呈同步效果,示例如下
對上面的代碼作以下更改

class MyObject {
    public  void speedPrintString() {
        synchronized (this) {
            System.out.println("speedPrintString--getLock     time:" + System.currentTimeMillis() + "   run ThreadName:"
                    + Thread.currentThread().getName());
            System.out.println("-------------------");
            System.out.println("speedPrintString--releaseLock time:" + System.currentTimeMillis() + "   run ThreadName:"
                    + Thread.currentThread().getName());
        }

    }
}

運行結果如圖 6-11 所示
這裏寫圖片描述

圖 6-11 也是同步效果

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