天天看點

Java多線程(四) 解決多線程安全——synchronizedJava多線程(四) 解決多線程安全——synchronized

Java多線程(四) 解決多線程安全——synchronized

  • Java多線程(四) 解決多線程安全——synchronized
    • synchronized的使用
    • synchronized 是重量型鎖
    • synchronized 原理和例子
      • synchronized 作用于執行個體方法
      • synchronized 作用于代碼塊
      • synchronized作用于靜态方法
    • synchronized 可重入鎖

在上一篇文章 《Java多線程(三) 多線程不安全的典型例子》中說到了多線程不安全的問題以及三個典型例子,在這一篇中講解一中保證多線程安全的一種方式——synchronized關鍵字。

synchronized的使用

synchronized相當于給對象上鎖或者給類上鎖,這樣防止其他線程通路共享資源,進而保護多線程的安全。synchronized的原理是它使用了flag标記ACC_SYN-CHRONIZED,執行線程先持有同步鎖,然後執行方法,最後在方法完成時才釋放鎖。

synchronized主要有三種用法:

  1. 修飾執行個體方法:作用于目前對象執行個體加鎖,進入同步代碼前要獲得目前對象執行個體的鎖。
synchronized void method() {
  //業務代碼
}
           
  1. 修飾靜态方法:也就是給目前類加鎖,會作用于類的所有對象執行個體 ,進入同步代碼前要獲得目前 class 的鎖。因為靜态成員不屬于任何一個執行個體對象,是類成員( static 表明這是該類的一個靜态資源,不管 new 了多少個對象,隻有一份),是以,如果一個線程 A 調用一個執行個體對象的非靜态 synchronized 方法,而線程 B 需要調用這個執行個體對象所屬類的靜态 synchronized 方法,是允許的,不會發生互斥現象,因為通路靜态 synchronized 方法占用的鎖是目前類的鎖,而通路非靜态 synchronized 方法占用的鎖是目前執行個體對象鎖。
synchronized void staic method() {
  //業務代碼
}
           
  1. 修飾代碼塊:指定加鎖對象,對給定對象/類加鎖。synchronized(this / object) 表示進入同步代碼庫前要獲得給定對象的鎖。synchronized(類.class) 表示進入同步代碼前要獲得目前 class 的鎖
synchronized(this) {
  //業務代碼
}
           

總的來說:

synchronized 關鍵字加到 static 靜态方法和 synchronized(class) 代碼塊上都是是給 Class 類上鎖。

synchronized 關鍵字加到執行個體方法上是給對象執行個體上鎖。
           

synchronized 是重量型鎖

Synchronized是通過對象内部的一個叫做螢幕鎖(monitor)來實作的。但是螢幕鎖本質又是依賴于底層的作業系統的Mutex Lock來實作的。而作業系統實作線程之間的切換這就需要從使用者态轉換到核心态,這個成本非常高,狀态之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因。是以,這種依賴于作業系統Mutex Lock所實作的鎖我們稱之為“重量級鎖”。

synchronized 原理和例子

在 Java 中,synchronized可以保證在同一個時刻,隻有一個線程可以執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享資料的操作),此外synchronized的另外一個重要的作用是synchronized可保證一個線程的變化(主要是共享資料的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點确實也是很重要的。

synchronized 作用于執行個體方法

第一個例子舉一個很簡單的對一個類的共享資源加一的操作,為了保證多線程安全,使用synchronized修飾increase()。對于增加i值來說,他并不是一個原子操作,因為第一步要讀取i值,第二步要對他進行加一操作,是以需要進行互斥操作。synchronized修飾的是執行個體方法increase,在這樣的情況下,目前線程的鎖便是執行個體對象instance。當一個線程正在通路一個對象的 synchronized 執行個體方法,那麼其他線程不能通路該對象的所有 synchronized 方法,因為一個對象隻有一把鎖,當一個線程擷取了該對象的鎖之後,其他線程無法擷取該對象的鎖,是以無法通路該對象的synchronized執行個體方法,這樣的方式也就保護了多線程的安全,不過其他線程還是可以通路該執行個體對象的其他非synchronized方法。

public class syntest {

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

class AccountingSync implements Runnable{
    static int i=0;

    public synchronized void increase() throws InterruptedException {
        i++;
        Thread.sleep(300);
        System.out.println(Thread.currentThread().getName()+"增加了i值,它的值為 "+i);
    }
    @Override
    public void run() {
        for(int j=0;j<100;j++){
            try {
                increase();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
           
Java多線程(四) 解決多線程安全——synchronizedJava多線程(四) 解決多線程安全——synchronized

(省略中間的)

Java多線程(四) 解決多線程安全——synchronizedJava多線程(四) 解決多線程安全——synchronized

synchronized 作用于代碼塊

這裡舉的例子是上一篇《Java多線程(三) 多線程不安全的典型例子》的第一個不安全的買票例子,這裡依舊是三個人買票,一共三十張票,不同的是這次使用synchronized對買票的代碼塊進行上鎖。當編寫的方法體比較大時,同時存在一些比較耗時的操作,而需要同步的代碼又隻有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹,這樣就無需對整個方法進行同步操作了。将synchronized作用于一個給定的執行個體對象t,即目前執行個體對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求目前線程持有instance執行個體對象鎖,如果目前有其他線程正持有該對象鎖,那麼新到的線程就必須等待。當然除了t作為對象外,我們還可以使用this對象(代表目前執行個體)或者目前類的class對象作為鎖。

class Ticket implements Runnable{

    private int alltickets = 30;
    private boolean flag = true;

    @Override
    public void run() {
        while(alltickets>0) {
                synchronized (this) {
                    try {
                        Thread.sleep(300);
                        buy();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

        }
    }

    public void buy() throws InterruptedException {

        if(this.alltickets<=0)
        {
            System.out.println("沒票可買了"+Thread.currentThread().getName());
            this.flag = false;
            return;
        }

        else
        {
            Thread.sleep(100);
            System.out.println(Thread.currentThread().getName()+"買了第"+this.alltickets--+"張票   ");

        }
    }

}

public class testThread {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        Ticket t = new Ticket();
        new Thread(t, "小華同學").start();
        new Thread(t, "小明同學").start();
        new Thread(t, "黃牛").start();
        }

           
Java多線程(四) 解決多線程安全——synchronizedJava多線程(四) 解決多線程安全——synchronized

可以看到這次買票過程是有序的,并且沒有出現買到重複票的問題

synchronized作用于靜态方法

當synchronized作用于靜态方法時,其鎖就是目前類的class對象鎖。由于靜态成員不專屬于任何一個執行個體對象,是類成員,是以通過class對象鎖可以控制靜态成員的并發操作。需要注意的是如果一個線程A調用一個執行個體對象的非static的synchronized方法,而線程B需要調用這個執行個體對象所屬類的靜态 synchronized方法,是允許的,不會發生互斥現象,因為通路靜态 synchronized 方法占用的鎖是目前類的class對象,而通路非靜态 synchronized 方法占用的鎖是目前執行個體對象鎖。synchronized關鍵字修飾的是靜态方法,其鎖對象是目前類的class對象。注意代碼中的increase2方法是執行個體方法,其對象鎖是目前執行個體對象,如果别的線程調用該方法,将不會産生互斥現象,畢竟鎖對象不同,但我們應該意識到這種情況下可能會發現線程安全問題(操作了共享靜态變量i)。

public class syntest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(new StaticTest());
        Thread t2=new Thread(new StaticTest());
        t1.start();t2.start();
        
    }
}

class StaticTest implements Runnable{
    static int i=0;

    /**
     * 作用于靜态方法,鎖是目前class對象,也就是StaticTest類對應的class對象
     */
    public static synchronized void increase() throws InterruptedException {
        i++;
        Thread.sleep(300);
        System.out.println(Thread.currentThread().getName()+"增加了i值,它的值為 "+i);
    }

    /**
     * 非靜态,通路時鎖不一樣不會發生互斥
     */
    public synchronized void increase2(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<100;j++){
            try {
                increase();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}
           

synchronized 可重入鎖

關鍵字synchronized擁有重入鎖的功能,即在使用synchronized時,當一個線程得到了一個對象鎖後,再次請求此對象鎖時是可以得到該對象鎖的,意思是在一個synchronized方法或者代碼塊中,調用這個類的其他synchronized方法或者代碼塊時是可以做到的。

public class syntest {
    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(new StaticTest());

        t1.start();

    }
}

class StaticTest implements Runnable{
    public synchronized void method1()
    {
        System.out.println("method1");
        method2();
    }

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

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


    @Override
    public void run() {
        method1();
    }
}
           
Java多線程(四) 解決多線程安全——synchronizedJava多線程(四) 解決多線程安全——synchronized