Java對象鎖和類鎖全面解析(多線程synchronized關鍵字)

簡述

最近工做有用到一些多線程的東西,以前吧,有用到synchronized同步塊,不過是別人怎麼用就跟着用,並無搞清楚鎖的概念。最近也是遇到一些問題,不搞清楚鎖的概念,很容易碰壁,甚至有些時候本身連用沒用對都不知道。java

今天把一些疑惑都解開了,寫篇文章分享給你們,文章還算比較全面。固然可能有小寶鴿理解得不夠深刻透徹的地方,若是說得不正確還望指出。編程

看以前有必要跟某些猿友說一下,若是看一遍沒有看明白呢,也不要緊,當是瞭解一下,等真正使用到了,再回頭看。安全

本文主要是將synchronized關鍵字用法做爲例子來去解釋Java中的對象鎖和類鎖。特別的是但願能幫你們理清一些概念。多線程

1、synchronized關鍵字

synchronized關鍵字有以下兩種用法:異步

一、 在須要同步的方法的方法簽名中加入synchronized關鍵字。

synchronized public void getValue() {
    System.out.println("getValue method thread name="
            + Thread.currentThread().getName() + " username=" + username
            + " password=" + password);
}

上面的代碼修飾的synchronized是非靜態方法,若是修飾的是靜態方法(static)含義是徹底不同的。具體不同在哪裏,後面會詳細說清楚。ide

synchronized static public void getValue() {
    System.out.println("getValue method thread name="
            + Thread.currentThread().getName() + " username=" + username
            + " password=" + password);
}

二、使用synchronized塊對須要進行同步的代碼段進行同步。

public void serviceMethod() {
    try {
        synchronized (this) {
            System.out.println("begin time=" + System.currentTimeMillis());
            Thread.sleep(2000);
            System.out.println("end    end=" + System.currentTimeMillis());
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

上面的代碼塊是synchronized (this)用法,還有synchronized (非this對象)以及synchronized (類.class)這兩種用法,這些使用方式的含義也是有根本的區別的。咱們先帶着這些問題繼續往下看。性能

2、Java中的對象鎖和類鎖

小寶鴿彷佛並無辦法用清晰簡短的語言來描述對象鎖和類鎖的概念。即使能用簡單的語句概況,也會顯得抽象。猿友們耐心看完天然會明白。this

以前網上有找一些相關資料,有篇博客是這樣描述的(看的是轉載的,原創鏈接我也不知道):線程

一段synchronized的代碼被一個線程執行以前,他要先拿到執行這段代碼的權限, 在Java裏邊就是拿到某個同步對象的鎖(一個對象只有一把鎖); 若是這個時候同步對象的鎖被其餘線程拿走了,他(這個線程)就只能等了(線程阻塞在鎖池等待隊列中)。 取到鎖後,他就開始執行同步代碼(被synchronized修飾的代碼); 線程執行完同步代碼後立刻就把鎖還給同步對象,其餘在鎖池中等待的某個線程就能夠拿到鎖執行同步代碼了。 這樣就保證了同步代碼在統一時刻只有一個線程在執行。code

上面提到鎖,這裏先引出鎖的概念。先來看看下面這些囉嗦而必不可少的文字。

多線程的線程同步機制其實是靠鎖的概念來控制的。

在Java程序運行時環境中,JVM須要對兩類線程共享的數據進行協調:

  • 1)保存在堆中的實例變量
  • 2)保存在方法區中的類變量

這兩類數據是被全部線程共享的。 (程序不須要協調保存在Java 棧當中的數據。由於這些數據是屬於擁有該棧的線程所私有的。)

Java的普通對象存活在堆中。與棧不一樣,堆的空間不會隨着方法調用結束而清空。所以,在某個方法中建立的對象,能夠在方法調用結束以後,繼續存在於堆中。這帶來的一個問題是,若是咱們不斷的建立新的對象,內存空間將最終消耗殆盡。

在java虛擬機中,每一個對象和類在邏輯上都是和一個監視器相關聯的。 對於對象來講,相關聯的監視器保護對象的實例變量。

對於類來講,監視器保護類的類變量。

(若是一個對象沒有實例變量,或者一個類沒有變量,相關聯的監視器就什麼也不監視。) 爲了實現監視器的排他性監視能力,java虛擬機爲每個對象和類都關聯一個鎖。表明任什麼時候候只容許一個線程擁有的特權。線程訪問實例變量或者類變量不需鎖。

可是若是線程獲取了鎖,那麼在它釋放這個鎖以前,就沒有其餘線程能夠獲取一樣數據的鎖了。(鎖住一個對象就是獲取對象相關聯的監視器)

類鎖實際上用對象鎖來實現。當虛擬機裝載一個class文件的時候,它就會建立一個java.lang.Class類的實例。當鎖住一個對象的時候,實際上鎖住的是那個類的Class對象。

一個線程能夠屢次對同一個對象上鎖。對於每個對象,java虛擬機維護一個加鎖計數器,線程每得到一次該對象,計數器就加1,每釋放一次,計數器就減 1,當計數器值爲0時,鎖就被徹底釋放了。

java編程人員不須要本身動手加鎖,對象鎖是java虛擬機內部使用的。

在java程序中,只須要使用synchronized塊或者synchronized方法就能夠標誌一個監視區域。當每次進入一個監視區域時,java 虛擬機都會自動鎖上對象或者類。

3、synchronized關鍵字各類用法與實例

看完了」2、Java中的對象鎖和類鎖」,咱們再來結合」1、synchronized關鍵字」裏面提到的synchronized用法。

事實上,synchronized修飾非靜態方法、同步代碼塊的synchronized (this)用法和synchronized (非this對象)的用法鎖的是對象,線程想要執行對應同步代碼,須要得到對象鎖。

synchronized修飾靜態方法以及同步代碼塊的synchronized (類.class)用法鎖的是類,線程想要執行對應同步代碼,須要得到類鎖。

所以,事實上synchronized關鍵字能夠細分爲上面描述的五種用法。

本文的實例均來自於《Java多線程編程核心技術》這本書裏面的例子。

一、咱們先看看非線程安全實例(Run.java):

public class Run {

    public static void main(String[] args) {

        HasSelfPrivateNum numRef = new HasSelfPrivateNum();

        ThreadA athread = new ThreadA(numRef);
        athread.start();

        ThreadB bthread = new ThreadB(numRef);
        bthread.start();

    }

}

class HasSelfPrivateNum {

    private int num = 0;

    public void addI(String username) {
        try {
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}


class ThreadA extends Thread {

    private HasSelfPrivateNum numRef;

    public ThreadA(HasSelfPrivateNum numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("a");
    }

}



 class ThreadB extends Thread {

    private HasSelfPrivateNum numRef;

    public ThreadB(HasSelfPrivateNum numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("b");
    }

}

運行結果爲:

a set over!
b set over!
b num=200
a num=200

修改HasSelfPrivateNum以下,方法用synchronized修飾以下:

class HasSelfPrivateNum {

    private int num = 0;

    synchronized public void addI(String username) {
        try {
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

運行結果是線程安全的:

b set over!
b num=200
a set over!
a num=100

實驗結論:兩個線程訪問同一個對象中的同步方法是必定是線程安全的。本實現因爲是同步訪問,因此先打印出a,而後打印出b

這裏線程獲取的是HasSelfPrivateNum的對象實例的鎖——對象鎖。

二、多個對象多個鎖

就上面的實例,咱們將Run改爲以下:

public class Run {

    public static void main(String[] args) {

        HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
        HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();

        ThreadA athread = new ThreadA(numRef1);
        athread.start();

        ThreadB bthread = new ThreadB(numRef2);
        bthread.start();

    }

}

運行結果爲:

a set over!
b set over!
b num=200
a num=200

這裏是非同步的,由於線程athread得到是numRef1的對象鎖,而bthread線程獲取的是numRef2的對象鎖,他們並無在獲取鎖上有競爭關係,所以,出現非同步的結果

這裏插播一下:同步不具備繼承性

三、同步塊synchronized (this)

咱們先看看代碼實例(Run.java)

public class Run {

    public static void main(String[] args) {
        ObjectService service = new ObjectService();

        ThreadA a = new ThreadA(service);
        a.setName("a");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("b");
        b.start();
    }

}

class ObjectService {

    public void serviceMethod() {
        try {
            synchronized (this) {
                System.out.println("begin time=" + System.currentTimeMillis());
                Thread.sleep(2000);
                System.out.println("end    end=" + System.currentTimeMillis());
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


class ThreadA extends Thread {

    private ObjectService service;

    public ThreadA(ObjectService service) {
        super();
        this.service = service;
    }

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

}


class ThreadB extends Thread {
    private ObjectService service;

    public ThreadB(ObjectService service) {
        super();
        this.service = service;
    }

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

運行結果:

begin time=1466148260341
end    end=1466148262342
begin time=1466148262342
end    end=1466148264378

這樣也是同步的,線程獲取的是同步塊synchronized (this)括號()裏面的對象實例的對象鎖,這裏就是ObjectService實例對象的對象鎖了。

須要注意的是synchronized (){}的{}先後的代碼依舊是異步的。

四、synchronized (非this對象)

咱們先看看代碼實例(Run.java)

public class Run {

    public static void main(String[] args) {

        Service service = new Service("xiaobaoge");

        ThreadA a = new ThreadA(service);
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB(service);
        b.setName("B");
        b.start();

    }

}

class Service {

    String anyString = new String();

    public Service(String anyString){
        this.anyString = anyString;
    }

    public void setUsernamePassword(String username, String password) {
        try {
            synchronized (anyString) {
                System.out.println("線程名稱爲:" + Thread.currentThread().getName()
                        + "在" + System.currentTimeMillis() + "進入同步塊");
                Thread.sleep(3000);
                System.out.println("線程名稱爲:" + Thread.currentThread().getName()
                        + "在" + System.currentTimeMillis() + "離開同步塊");
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

class ThreadA extends Thread {
    private Service service;

    public ThreadA(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.setUsernamePassword("a", "aa");

    }

}


class ThreadB extends Thread {

    private Service service;

    public ThreadB(Service service) {
        super();
        this.service = service;
    }

    @Override
    public void run() {
        service.setUsernamePassword("b", "bb");

    }

}

不難看出,這裏線程爭奪的是anyString的對象鎖,兩個線程有競爭同一對象鎖的關係,出現同步

如今有一個問題:一個類裏面有兩個非靜態同步方法,會有影響麼?

答案是:若是對象實例A,線程1得到了對象A的對象鎖,那麼其餘線程就不能進入須要得到對象實例A的對象鎖才能訪問的同步代碼(包括同步方法和同步塊)。不理解能夠細細品味一下!

五、靜態synchronized同步方法

咱們直接看代碼實例:

public class Run {

    public static void main(String[] args) {

        ThreadA a = new ThreadA();
        a.setName("A");
        a.start();

        ThreadB b = new ThreadB();
        b.setName("B");
        b.start();

    }

}

class Service {

    synchronized public 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();
        }
    }

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

}


class ThreadA extends Thread {
    @Override
    public void run() {
        Service.printA();
    }

}


class ThreadB extends Thread {
    @Override
    public void run() {
        Service.printB();
    }
}

運行結果:

線程名稱爲:A在1466149372909進入printA
線程名稱爲:A在1466149375920離開printA
線程名稱爲:B在1466149375920進入printB
線程名稱爲:B在1466149375920離開printB

兩個線程在爭奪同一個類鎖,所以同步

六、synchronized (class)

對上面Service類代碼修改爲以下:

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 static void printB() {
        synchronized (Service.class) {
            System.out.println("線程名稱爲:" + Thread.currentThread().getName()
                    + "在" + System.currentTimeMillis() + "進入printB");
            System.out.println("線程名稱爲:" + Thread.currentThread().getName()
                    + "在" + System.currentTimeMillis() + "離開printB");
        }
    }
}

運行結果:

線程名稱爲:A在1466149372909進入printA
線程名稱爲:A在1466149375920離開printA
線程名稱爲:B在1466149375920進入printB
線程名稱爲:B在1466149375920離開printB

兩個線程依舊在爭奪同一個類鎖,所以同步

須要特別說明:對於同一個類A,線程1爭奪A對象實例的對象鎖,線程2爭奪類A的類鎖,這二者不存在競爭關係。也就說對象鎖和類鎖互補干預內政

靜態方法則必定會同步,非靜態方法需在單例模式才生效,可是也不能都用靜態同步方法,總之用得很差可能會給性能帶來極大的影響。另外,有必要說一下的是Spring的bean默認是單例的。

相關文章
相關標籤/搜索