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却可以实现更多线程之间的通信。

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