Java線程同步實現二:Lock鎖和Condition

一.同步鎖

我們還是用同步鎖來實現存取款的例子:

package com.chanshuyi.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ThreadDemo93 {

    public static void main(String[] args) {
        Account account = new Account(2300);
        new DrawMoneyThread(account).start();
        new DepositeThread(account).start();
    }
}

class DepositeThread extends Thread{

    private Account account;
    
    public DepositeThread(Account account){
        this.account = account;
    }
    
    @Override
    public void run() {
        //每次存200,10次共存2000
        for(int i = 0; i < 10; i++){
            account.deposit(200, i + 1);
            //模擬存錢的時間間隔
            try {
                Thread.sleep((long)Math.random()*5);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

class DrawMoneyThread extends Thread{
    
    private Account account;
    
    public DrawMoneyThread(Account account){
        this.account = account;
    }
    
    @Override
    public void run() {
        //每次取100,10次共取1000
        for(int i = 0; i < 10; i++){
            account.withdraw(100, i + 1);
            //模擬取錢的時間間隔
            try {
                Thread.sleep((long)Math.random()*5);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

class Account{
    
    private Lock lock = new ReentrantLock();
    
    //存錢
    public void deposit(double amount, int i){
        lock.lock();
        try {
            Thread.sleep((long)Math.random()*10000);  //模擬存錢的延遲
            this.balance = this.balance + amount;
            System.out.println("***第" + i + "次,存入錢:" + amount);
            System.out.println("***第" + i + "次,存錢後賬戶餘額:" + this.balance);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    //取錢
    public void withdraw(double amount, int i){
        lock.lock();
        try {
            Thread.sleep((long)Math.random()*10000);  //模擬取錢的延遲
            if(this.balance >= amount){
                this.balance = this.balance - amount;
                System.out.println("第" + i + "次,取出錢:" + amount);
                System.out.println("第" + i + "次,取錢後賬戶餘額:" + this.balance);
            }else{
                System.out.println("第" + i + "次,餘額不足");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    public Account(){
    }
    
    public Account(double balance){
        this.balance = balance;
    }
    
    private double balance;
}

當我們進入需要同步的代碼時,我們調用lock.lock()方法獲取鎖對象。當退出同步代碼塊時,使用lock.unlock()釋放鎖對象。下面是其中的一個輸出:

第1次,取出錢:100.0
第1次,取錢後賬戶餘額:2200.0
***第1次,存入錢:200.0
***第1次,存錢後賬戶餘額:2400.0
第2次,取出錢:100.0
第2次,取錢後賬戶餘額:2300.0
***第2次,存入錢:200.0
***第2次,存錢後賬戶餘額:2500.0
第3次,取出錢:100.0
第3次,取錢後賬戶餘額:2400.0
***第3次,存入錢:200.0
***第3次,存錢後賬戶餘額:2600.0
第4次,取出錢:100.0
第4次,取錢後賬戶餘額:2500.0
***第4次,存入錢:200.0
***第4次,存錢後賬戶餘額:2700.0
第5次,取出錢:100.0
第5次,取錢後賬戶餘額:2600.0
***第5次,存入錢:200.0
***第5次,存錢後賬戶餘額:2800.0
第6次,取出錢:100.0
第6次,取錢後賬戶餘額:2700.0
第7次,取出錢:100.0
第7次,取錢後賬戶餘額:2600.0
第8次,取出錢:100.0
第8次,取錢後賬戶餘額:2500.0
第9次,取出錢:100.0
第9次,取錢後賬戶餘額:2400.0
***第6次,存入錢:200.0
***第6次,存錢後賬戶餘額:2600.0
***第7次,存入錢:200.0
***第7次,存錢後賬戶餘額:2800.0
第10次,取出錢:100.0
第10次,取錢後賬戶餘額:2700.0
***第8次,存入錢:200.0
***第8次,存錢後賬戶餘額:2900.0
***第9次,存入錢:200.0
***第9次,存錢後賬戶餘額:3100.0
***第10次,存入錢:200.0
***第10次,存錢後賬戶餘額:3300.0

上面這個例子只是實現了存款和取款的隔離,使其不能同時進行存取款操作。但是沒有考慮到餘額不足的情況,所以當我們將初始賬戶的餘額改爲0時,其最後的餘額就不是準確的數據了。那ReentrantLock鎖能使用wait()/notify()進行線程通信麼?答案是可以,但是不是使用wait()/notify(),而是使用Condition對象的await()和signal()方法。

二.用Condition進行線程間通信

package com.chanshuyi.thread.part3.part36;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 銀行存取款 - 使用Lock鎖進行線程同步,用Condition進行線程通信
 * 實現效果:不會出現餘額不足的情況
 * @author yurongchan
 *
 */
public class ThreadDemo1 {

    public static void main(String[] args) {
        Account account = new Account(0);
        new DrawMoneyThread(account).start();
        new DepositeThread(account).start();
    }

}

class DepositeThread extends Thread{

    private Account account;
    
    public DepositeThread(Account account){
        this.account = account;
    }
    
    @Override
    public void run() {
        //每次存200,10次共存2000
        for(int i = 0; i < 10; i++){
            account.deposit(200, i + 1);
            //模擬存錢的時間間隔
            try {
                Thread.sleep((long)Math.random()*5);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
}

class DrawMoneyThread extends Thread{
    
    private Account account;
    
    public DrawMoneyThread(Account account){
        this.account = account;
    }
    
    @Override
    public void run() {
        //每次取100,10次共取1000
        for(int i = 0; i < 10; i++){
            account.withdraw(100, i + 1);
            //模擬取錢的時間間隔
            try {
                Thread.sleep((long)Math.random()*5);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }
    
}

class Account{
    
    private Lock lock = new ReentrantLock();
    private Condition sufficientFunds = lock.newCondition();
    
    //存錢
    public void deposit(double amount, int i){
        System.out.println("***取款進程" + i + "準備存款.");
        lock.lock();
        try {
            Thread.sleep((long)Math.random()*10000);  //模擬存錢的延遲
            this.balance = this.balance + amount;
            System.out.println("***存款進程" + i + "存款" + amount);
            System.out.println("***存款進程" + i + "存款後賬戶餘額:" + this.balance);
            sufficientFunds.signalAll();  //每次存入錢就喚醒其他存錢進程,通知其餘額可能足夠取款了
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    //取錢
    public void withdraw(double amount, int i){
        System.out.println("---取款進程" + i + "準備取款.");
        lock.lock();
        while (this.balance < amount) {
            try {
                System.out.println("---取款進程" + i + "準備取款時發現餘額不足.放棄對象鎖,進入阻塞狀態.");
                sufficientFunds.await(); // 餘額不足,等待
                System.out.println("---取款進程" + i + "被喚醒,重新嘗試取款.");
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println("---賬戶餘額充足.取款進程" + i + "開始取款.");
        try {
            Thread.sleep((long) Math.random() * 10000); // 模擬取錢的延遲
            this.balance = this.balance - amount;
            System.out.println("---取款進程" + i + "成功取款:" + amount);
            System.out.println("---取款進程" + i + "成功取款後餘額:" + this.balance);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    
    public Account(){
        
    }
    
    public Account(double balance){
        this.balance = balance;
    }
    
    private double balance;
}

上面的代碼我們故意將賬戶餘額設置爲0,虛擬了賬戶餘額不足的情況。之後我們通過lock.newCondition方法創建了一個sufficientFunds的條件,當我們判斷得出餘額不足的時候,我們調用sufficientFunds.await()方法讓該線程放棄條件(Condition)對應的鎖對象,讓其他存款進程可以往賬戶中取款。當存款進程成功存款之後,存款進程調用sufficientFunds.signal()方法通知取款進程說:我存錢進去了,你再看看錢夠不夠。這時候取款進程繼續運行,判斷餘額是否足夠,如果不足則繼續放棄鎖對象並等待,否則就進行取款操作。下面是其中一次運行結果:

---取款進程1準備取款.
---取款進程1準備取款時發現餘額不足.放棄對象鎖,進入阻塞狀態.
***取款進程1準備存款.
***存款進程1存款200.0
***存款進程1存款後賬戶餘額:200.0
---取款進程1被喚醒,重新嘗試取款.
---賬戶餘額充足.取款進程1開始取款.
***取款進程2準備存款.
---取款進程1成功取款:100.0
---取款進程1成功取款後餘額:100.0
---取款進程2準備取款.
***存款進程2存款200.0
***存款進程2存款後賬戶餘額:300.0
---賬戶餘額充足.取款進程2開始取款.
***取款進程3準備存款.
---取款進程2成功取款:100.0
---取款進程2成功取款後餘額:200.0
---取款進程3準備取款.
***存款進程3存款200.0
***存款進程3存款後賬戶餘額:400.0
---賬戶餘額充足.取款進程3開始取款.
***取款進程4準備存款.
---取款進程3成功取款:100.0
---取款進程3成功取款後餘額:300.0
---取款進程4準備取款.
***存款進程4存款200.0
***存款進程4存款後賬戶餘額:500.0
---賬戶餘額充足.取款進程4開始取款.
***取款進程5準備存款.
---取款進程4成功取款:100.0
---取款進程4成功取款後餘額:400.0
---取款進程5準備取款.
***存款進程5存款200.0
***存款進程5存款後賬戶餘額:600.0
---賬戶餘額充足.取款進程5開始取款.
***取款進程6準備存款.
---取款進程5成功取款:100.0
---取款進程5成功取款後餘額:500.0
---取款進程6準備取款.
***存款進程6存款200.0
***存款進程6存款後賬戶餘額:700.0
---賬戶餘額充足.取款進程6開始取款.
***取款進程7準備存款.
---取款進程6成功取款:100.0
---取款進程6成功取款後餘額:600.0
---取款進程7準備取款.
***存款進程7存款200.0
***存款進程7存款後賬戶餘額:800.0
---賬戶餘額充足.取款進程7開始取款.
***取款進程8準備存款.
---取款進程7成功取款:100.0
---取款進程7成功取款後餘額:700.0
---取款進程8準備取款.
***存款進程8存款200.0
***存款進程8存款後賬戶餘額:900.0
---賬戶餘額充足.取款進程8開始取款.
***取款進程9準備存款.
---取款進程8成功取款:100.0
---取款進程8成功取款後餘額:800.0
---取款進程9準備取款.
***存款進程9存款200.0
***存款進程9存款後賬戶餘額:1000.0
---賬戶餘額充足.取款進程9開始取款.
***取款進程10準備存款.
---取款進程9成功取款:100.0
---取款進程9成功取款後餘額:900.0
---取款進程10準備取款.
***存款進程10存款200.0
***存款進程10存款後賬戶餘額:1100.0
---賬戶餘額充足.取款進程10開始取款.
---取款進程10成功取款:100.0
---取款進程10成功取款後餘額:1000.0

從上面的運行結果我們可以看出,一開始取款線程1準備取款,但是此時賬戶餘額爲0,無法取款,於是取款線程1讓出對象鎖,並等待。存款線程1因此獲得了對象鎖,併成功存進200,並喚醒了所有取款線程。此時取款線程1甦醒過來並判斷賬戶餘額滿足了取款需求,於是取款線程1進行取款操作。

到這裏我們學會了如何用synchronized和lock進行線程同步,並且利用其相對應的wait()/notify和Condition進行線程間的通信,以實現更高級的功能。

那既然synchronized能實現的功能,爲什麼還要有lock鎖呢?他們之間究竟有什麼異同呢?

三.同步線程的實現原理(共同點)

其實無論通過synchronized方法、synchronized代碼塊、還是Lock鎖,他們的共同點都是傳入一個唯一的對象,並以這個唯一的對象作爲鎖來實現線程同步的。雖然使用synchronized方法進行線程同步時並沒有顯示地傳入一個鎖對象,但是實際上它默認鎖對象的就是synchronized方法所在類的對象(即例子中的Account對象)
三.使用synchronized的wait()/notify() 和 Lock的Condition進行線程通信有什麼區別?(不同點)

使用synchronized和lock都能實現線程的通信,但是synchronized和wait()/notify()方法只能實現兩個線程之間的通信,當有更多的線程需要互相通信時,wait()/notify()就無法做到了。而Lock對象能通過newCondition()方法創建出無數的"條件",通過這些條件,我們就能夠成功地實現多線程(N>3)之間的數據通信,對它們進行控制。

比如我需要用線程實現這樣的功能:有老大、老二、老三三個人,我們要這3個人進行報數,報數的順序是這樣的,首先是老大報數,報5次;然後是老二報數,報5次;之後是老三報數,報5次;就這樣一直報兩輪。

我們可以用三個方法分別代表三個人的報數,outputOne()方法代表老大報數,outputTwo()方法代表老二報數,outputThree()方法代表老三報數。接下來就是如果確保他們是按我們需要的順序進行報數的。這時候如果我們用wait()和notify()方法的話,當老大報完數使用notifyAll()方法喚醒線程,這時候老二和老三都會搶着去報數,這時候我們是無法對其進行準確的順序控制的。

但Lock鎖的newCondition()方法允許有多個條件(Condition),我們可以創建三個條件,分別代表3個通知,比如:調用condition1.signal()時代表讓老大報數,調用condition2.signal()時讓老二報數,調用condition3.signal()時讓老三報數,這樣子就可以精準的控制他們的執行順序了。實現後的代碼如下:

package com.chanshuyi.class13;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 用Condition實現多個操作的通信協作
 * Condition不同於Object.wait()和Object.notify()的一個方面就是Condition能實現多個操作之間的相互協調通信,而Object.wait()和Object.notify()只能是兩個操作之間的協調通信
 * 這個實例實現3個線程輪流輸出
 * @author chenyr
 * @time 2014-12-24 下午06:40:30
 * All Rights Reserved.
 */
public class Condition3 {

    public static void main(String[] args)throws Exception{
        final Outputer outputer = new Outputer();
        new Thread(new Runnable(){
            public void run(){
                for(int i = 1; i <= 2; i++){
                    try {
                        outputer.outputOne(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        
        new Thread(new Runnable(){
            public void run(){
                for(int i = 1; i <= 2; i++){
                    try {
                        outputer.outputTwo(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        
        new Thread(new Runnable(){
            public void run(){
                for(int i = 1; i <= 2; i++){
                    try {
                        outputer.outputThree(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

class Outputer{
    private int index = 1;  //1表示該老大輸出,2表示該老二輸出,3表示該老三輸出
    
    final Lock lock = new ReentrantLock();
    final Condition condition1 = lock.newCondition();
    final Condition condition2 = lock.newCondition();
    final Condition condition3 = lock.newCondition();
    
    //老大的輸出
    public void outputOne(int i) throws InterruptedException{
        lock.lock();
        System.out.println("outputOne-" + i + " has lock.");
        try{
            while(index != 1){
                System.out.println("outputOne-" + i + " has release the lock and wait.");
                condition1.await();
            }
            for(int j = 1; j <= 5; j++){
                System.out.println("老大的第" + i + "次,第" + j + "小次");
            }
            index = 2;
            condition2.signal();
        }finally{
            lock.unlock();
        }
    }
    
    //老二的輸出
    public void outputTwo(int i)throws InterruptedException{
        lock.lock();
        System.out.println("outputTwo-" + i + " has lock.");
        try{
            while(index != 2){
                System.out.println("outputTwo-" + i + " has release the lock and wait.");
                condition2.await();
            }
            for(int j = 1; j <= 5; j++){
                System.out.println("老二的第" + i + "次,第" + j + "小次");
            }
            index = 3;
            condition3.signal();
        }finally{
            lock.unlock();
        }
    }
    
    //老三的輸出
    public void outputThree(int i)throws InterruptedException{
        lock.lock();
        System.out.println("outputThree-" + i + " has lock.");
        try{
            while(index != 3){
                System.out.println("outputThree-" + i + " has release the lock and Wait.");
                condition3.await();
            }
            for(int j = 1; j <= 5; j++){
                System.out.println("老三的第" + i + "次,第" + j + "小次");
            }
            index = 1;
            condition1.signal();
        }finally{
            lock.unlock();
        }
    }
}

下面是輸出的結果:

outputOne-1 has lock.
老大的第1次,第1小次
老大的第1次,第2小次
老大的第1次,第3小次
老大的第1次,第4小次
老大的第1次,第5小次
outputThree-1 has lock.
outputThree-1 has release the lock and Wait.
outputTwo-1 has lock.
老二的第1次,第1小次
老二的第1次,第2小次
老二的第1次,第3小次
老二的第1次,第4小次
老二的第1次,第5小次
outputTwo-2 has lock.
outputTwo-2 has release the lock and wait.
outputOne-2 has lock.
outputOne-2 has release the lock and wait.
老三的第1次,第1小次
老三的第1次,第2小次
老三的第1次,第3小次
老三的第1次,第4小次
老三的第1次,第5小次
outputThree-2 has lock.
outputThree-2 has release the lock and Wait.
老大的第2次,第1小次
老大的第2次,第2小次
老大的第2次,第3小次
老大的第2次,第4小次
老大的第2次,第5小次
老二的第2次,第1小次
老二的第2次,第2小次
老二的第2次,第3小次
老二的第2次,第4小次
老二的第2次,第5小次
老三的第2次,第1小次
老三的第2次,第2小次
老三的第2次,第3小次
老三的第2次,第4小次
老三的第2次,第5小次

一開始老大報了5次數,之後老大還準備繼續報數(第7行),但是index此時表明應該是老二報數了,所以老大隻能時調用condition.await()方法暫時泛起鎖(第8行)。之後老二獲得了對象鎖(第9行)進行了報數……

從線程同步上來看,無論synchronized還是Lock,他們的底層都是通過傳入唯一的鎖對象來實現線程同步的。
從線程通信來看,synchronized的線程只能實現兩個線程之間的通信,但是Condition卻可以實現更多線程之間的通信。

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