Java併發編程以內置鎖(synchronized)

簡介

synchronized在JDK5.0的早期版本中是重量級鎖,效率很低,但從JDK6.0開始,JDK在關鍵字synchronized上作了大量的優化,如偏向鎖、輕量級鎖等,使它的效率有了很大的提高。java

synchronized的做用是實現線程間的同步,當多個線程都須要訪問共享代碼區域時,對共享代碼區域進行加鎖,使得每一次只能有一個線程訪問共享代碼區域,從而保證線程間的安全性安全

由於沒有顯式的加鎖和解鎖過程,因此稱之爲隱式鎖,也叫做內置鎖監視器鎖app

以下實例,在沒有使用synchronized的狀況下,多個線程訪問共享代碼區域時,可能會出現與預想中不一樣的結果。ide

public class Apple implements Runnable {
    private int appleCount = 5;

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

    public void eatApple(){
        appleCount--;
        System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Thread t1 = new Thread(apple, "小強");
        Thread t2 = new Thread(apple, "小明");
        Thread t3 = new Thread(apple, "小花");
        Thread t4 = new Thread(apple, "小紅");
        Thread t5 = new Thread(apple, "小黑");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }
}

可能會輸出以下結果:優化

小強吃了一個蘋果,還剩3個蘋果
小黑吃了一個蘋果,還剩3個蘋果
小明吃了一個蘋果,還剩2個蘋果
小花吃了一個蘋果,還剩1個蘋果
小紅吃了一個蘋果,還剩0個蘋果

輸出結果異常的緣由是eatApple方法裏操做不是原子的,如當A線程完成appleCount的賦值,尚未輸出,B線程獲取到appleCount的最新值,並完成賦值操做,而後A和B同時輸出。(A,B線程分別對應小黑、小強)this

若是改下eatApple方法以下,還會不會有線程安全問題呢?線程

public void eatApple(){
	System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + --appleCount + "個蘋果");
}

仍是會有的,由於--appleCount不是原子操做,--appleCount能夠用另一種寫法表示:appleCount = appleCount - 1,仍是有可能會出現以上的異常輸出結果。code

synchronized的使用

synchronized分爲同步方法和同步代碼塊兩種用法,當每一個線程訪問同步方法或同步代碼塊區域時,首先須要得到對象的鎖,搶到鎖的線程能夠繼續執行,搶不到鎖的線程則阻塞,等待搶到鎖的線程執行完成後釋放鎖。對象

1.同步代碼塊繼承

鎖的對象是object:

public class Apple implements Runnable {
    private int appleCount = 5;
    private Object object = new Object();

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

    public void eatApple(){
	//同步代碼塊,此時鎖的對象是object
        synchronized (object) {
            appleCount--;
            System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
        }
    }

      //...省略main方法
}

2.同步方法,修飾普通方法

鎖的對象是當前類的實例對象:

public class Apple implements Runnable {
    private int appleCount = 5;

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

    public synchronized void eatApple() {
        appleCount--;
        System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
    }

    //...省略main方法
}

等價於如下同步代碼塊的寫法:

public void eatApple() {
	synchronized (this) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
	}
}

3.同步方法,修飾靜態方法

鎖的對象是當前類的class對象:

public class Apple implements Runnable {
    private static int appleCount = 5;

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

    public synchronized static void eatApple() {
        appleCount--;
        System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
    }

    //...省略main方法
}

等價於如下同步代碼塊的寫法:

public static void eatApple() {
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
	}
}

4.同步方法和同步代碼塊的區別

a.同步方法鎖的對象是當前類的實例對象或者當前類的class對象,而同步代碼塊鎖的對象能夠是任意對象。

b.同步方法是使用synchronized修飾方法,而同步代碼塊是使用synchronized修飾共享代碼區域。同步代碼塊相對於同步方法來講粒度更細,鎖的區域更小,通常鎖範圍越小效率就越高。以下狀況顯然同步代碼塊更適用:

public static void eatApple() {
	//不須要同步的耗時操做1
	//...
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
	}
	//不須要同步的耗時操做2
	//...
}

內置鎖的可重入性

內置鎖的可重入性是指當某個線程試圖獲取一個它已經持有的鎖時,它老是能夠獲取成功。以下:

public static void eatApple() {
	synchronized (Apple.class) {
		synchronized (Apple.class) {
			synchronized (Apple.class) {
				appleCount--;
				System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
			}
		}
	}
}

若是鎖不是可重入的,那麼假如某線程持有了該鎖,而後又須要等待持有該鎖的線程釋放鎖,這不就形成死鎖了嗎?

synchronized能夠被繼承嗎?

synchronized不能夠被繼承,若是子類中重寫後的方法須要實現同步,則須要手動添加synchronized關鍵字。

public class AppleParent {
    public synchronized void eatApple(){

    }
}

public class Apple extends AppleParent implements Runnable {
    private int appleCount = 5;

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

    @Override
    public void eatApple() {
        appleCount--;
        System.out.println(Thread.currentThread().getName() + "吃了一個蘋果,還剩" + appleCount + "個蘋果");
    }

    //...省略main方法
}

基於內置鎖的等待和喚醒

基於內置鎖的等待和喚醒是使用Object類中的wait()和notify()或notifyAll()來實現的。這些方法的調用前提是已經持有對應的鎖,因此只能在同步方法或者同步代碼塊裏調用。若是在沒有獲取到對應鎖的狀況下調用則會拋出IllegalMonitorStateException異常。下面介紹下相關的幾個方法:

  1. wait():使當前線程無限期地等待,直到另外一個線程調用notify()或notifyAll()。

  2. wait(long timeout):指定一個超時時間,超時時間事後線程將會被自動喚醒。線程也能夠在超時時間以前被notify()或notifyAll()喚醒。注意,wait(0)等同於調用wait()

  3. wait(long timeout, int nanos):相似於wait(long timeout),主要區別是wait(long timeout, int nanos)提供了更高的精度。

  4. notify():隨機喚醒一個在相同鎖對象上等待的線程。

  5. notifyAll():喚醒全部在相同鎖對象上等待的線程。

一個簡單的等待喚醒實例:

public class Apple {
    //蘋果數量
    private int appleCount = 0;

    /**
     * 買蘋果
     */
    public synchronized void getApple() {
        try {
            while (appleCount != 0) {
                wait();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "買了5個蘋果");
        appleCount = 5;
        notify();
    }

    /**
     * 吃蘋果
     */
    public synchronized void eatApple() {
        try {
            while (appleCount == 0) {
                wait();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "吃了1個蘋果");
        appleCount--;
        notify();
    }
}
/**
 * 生產者,買蘋果
 */
public class Producer extends Thread{
    private Apple apple;

    public Producer(Apple apple, String name){
        super(name);
        this.apple = apple;
    }

    @Override
    public void run(){
        while (true)
        apple.getApple();
    }
}

/**
 * 消費者,吃蘋果
 */
public class Consumer extends Thread{
    private Apple apple;

    public Consumer(Apple apple, String name){
        super(name);
        this.apple = apple;
    }

    @Override
    public void run(){
        while (true)
        apple.eatApple();
    }
}
public class Demo {
    public static void main(String[] args) {
        Apple apple = new Apple();
        Producer producer = new Producer(apple,"小明");
        Consumer consumer = new Consumer(apple, "小紅");
        producer.start();
        consumer.start();
    }
}

輸出結果:

小明買了5個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小紅吃了1個蘋果
小明買了5個蘋果
小紅吃了1個蘋果
    ......
相關文章
相關標籤/搜索