千家信息网

java并发编程中如何通过ReentrantLock和Condition实现银行存取款

发表于:2025-02-04 作者:千家信息网编辑
千家信息网最后更新 2025年02月04日,本篇文章为大家展示了java并发编程中如何通过ReentrantLock和Condition实现银行存取款,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。jav
千家信息网最后更新 2025年02月04日java并发编程中如何通过ReentrantLock和Condition实现银行存取款

本篇文章为大家展示了java并发编程中如何通过ReentrantLock和Condition实现银行存取款,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件,但以更难用的语法为代价。

Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。

ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。

  以下是locks包的相关类图:

在之前我们同步一段代码或者对象时都是使用 synchronized关键字,使用的是Java语言的内置特性,然而 synchronized的特性也导致了很多场景下出现问题,比如:

在一段同步资源上,首先线程A获得了该资源的锁,并开始执行,此时其他想要操作此资源的线程就必须等待。如果线程A因为某些原因而处于长时间操作的状态,比如等待网络,反复重试等等。那么其他线程就没有办法及时的处理它们的任务,只能无限制的等待下去。如果线程A的锁在持有一段时间后可自动被释放,那么其他线程不就可以使用该资源了吗?再有就是类似于数据库中的共享锁与排它锁,是否也可以应用到应用程序中?所以引入Lock机制就可以很好的解决这些问题。

  Lock提供了比 synchronized更多的功能。但是要注意以下几点:

   Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

   Lock和synchronized有一点非常大的不同,采用 synchronized不需要用户去手动释放锁,当synchronized方法或者 synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

一、可重入锁 ReentrantLock

  想到锁我们一般想到的是同步锁即 Synchronized,这里介绍的可重入锁ReentrantLock的效率更高。IBM对于可重入锁进行了一个介绍:JDK 5.0 中更灵活、更具可伸缩性的锁定机制

  这里简单介绍下可重入锁的分类:(假设线程A获取了锁,现在A执行完成了,释放了锁同时唤醒了正在等待被唤醒的线程B。但是,A执行唤醒操作到B真正获取锁的时间里可能存在线程C已经获取了锁,造成正在排队等待的B无法获得锁)

  1) 公平锁:

     由于B先在等待被唤醒,为了保证公平性原则,公平锁会先让B获得锁。

  2) 非公平锁

     不保证B先获取到锁对象。

  这两种锁只要在构造ReentrantLock对象时加以区分就可以了,当参数设置为true时为公平锁,false时为非公平锁,同时默认构造函数也是创建了一个非公平锁。

    private Lock lock = new ReentrantLock(true); ReentrantLock的公平锁在性能和实效性上作了很大的牺牲,可以参考IBM上发的那篇文章中的说明。

二、条件变量 Condition

  Condition是java.util.concurrent.locks包下的一个接口, Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。

Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。

  Condition(也称为条件队列 或条件变量)为线程提供了一种手段,在某个状态条件下直到接到另一个线程的通知,一直处于挂起状态(即"等待")。因为访问此共享状态信息发生在不同的线程中,所以它必须受到保护,因此要将某种形式的锁与 Condition相关联。

Condition 实例实质上被绑定到一个锁上。

  这里不再对Locks包下的源码进行分析。

三、ReentrantLock和Condition设计多线程存取款

1. 存款的时候,不能有线程在取款 。取款的时候,不能有线程在存款。

2. 取款时,余额大于取款金额才能进行取款操作,否则提示余额不足。

3. 当取款时,如果金额不足,则阻塞当前线程,并等待2s(可能有其他线程将钱存入)。

如果2s之内没有其它线程完成存款,或者还是金额不足则打印金额不足。

如果其它存入足够金额则通知该阻塞线程,并完成取款操作。

/** * 普通银行账户,不可透支 */public class MyCount {    private String oid; // 账号    private int cash;   // 账户余额    //账户锁,这里采用公平锁,挂起的取款线程优先获得锁,而不是让其它存取款线程获得锁    private Lock lock = new ReentrantLock(true);    private Condition _save = lock.newCondition(); // 存款条件    private Condition _draw = lock.newCondition(); // 取款条件    MyCount(String oid, int cash) {        this.oid = oid;        this.cash = cash;    }    /**     * 存款     * @param x 操作金额     * @param name 操作人     */    public void saving(int x, String name) {        lock.lock(); // 获取锁        if (x > 0) {            cash += x; // 存款            System.out.println(name + "存款" + x + ",当前余额为" + cash);        }        _draw.signalAll(); // 唤醒所有等待线程。        lock.unlock(); // 释放锁    }    /**     * 取款     * @param x  操作金额     * @param name 操作人     */    public void drawing(int x, String name) {        lock.lock(); // 获取锁        try {            if (cash - x < 0) {                System.out.println(name + "阻塞中");                _draw.await(2000,TimeUnit.MILLISECONDS); // 阻塞取款操作, await之后就隐示自动释放了lock,直到被唤醒自动获取            }            if(cash-x>=0){                cash -= x; // 取款                System.out.println(name + "取款" + x + ",当前余额为" + cash);            }else{                System.out.println(name+" 余额不足,当前余额为 "+cash+"   取款金额为 "+x);            }            // 唤醒所有存款操作,这里并没有什么实际作用,因为存款代码中没有阻塞的操作            _save.signalAll();        } catch (InterruptedException e) {            e.printStackTrace();        } finally {            lock.unlock(); // 释放锁        }    }}

这里的可重入锁也可以设置成非公平锁,这样阻塞取款线程可能后与其它存取款操作。

/**     * 存款线程类     */    static class SaveThread extends Thread {        private String name; // 操作人        private MyCount myCount; // 账户        private int x; // 存款金额        SaveThread(String name, MyCount myCount, int x) {            this.name = name;            this.myCount = myCount;            this.x = x;        }        public void run() {            myCount.saving(x, name);        }    }    /**     * 取款线程类     */    static class DrawThread extends Thread {        private String name; // 操作人        private MyCount myCount; // 账户        private int x; // 存款金额        DrawThread(String name, MyCount myCount, int x) {            this.name = name;            this.myCount = myCount;            this.x = x;        }        public void run() {            myCount.drawing(x, name);        }    }    public static void main(String[] args) {        // 创建并发访问的账户        MyCount myCount = new MyCount("95599200901215522", 1000);        // 创建一个线程池        ExecutorService pool = Executors.newFixedThreadPool(3);        Thread t1 = new SaveThread("S1", myCount, 100);        Thread t2 = new SaveThread("S2", myCount, 1000);        Thread t3 = new DrawThread("D1", myCount, 12600);        Thread t4 = new SaveThread("S3", myCount, 600);        Thread t5 = new DrawThread("D2", myCount, 2300);        Thread t6 = new DrawThread("D3", myCount, 1800);        Thread t7 = new SaveThread("S4", myCount, 200);        // 执行各个线程        pool.execute(t1);        pool.execute(t2);        pool.execute(t3);        pool.execute(t4);        pool.execute(t5);        pool.execute(t6);        pool.execute(t7);        try {            Thread.sleep(3000);        } catch (InterruptedException e) {            e.printStackTrace();        }        // 关闭线程池        pool.shutdown();    }}

上述类中定义了多个存取款的线程,执行结果如下:

S1存款100,当前余额为1100
S3存款600,当前余额为1700
D2阻塞中
S2存款1000,当前余额为2700
D2取款2300,当前余额为400
D3阻塞中
S4存款200,当前余额为600
D3 余额不足,当前余额为 600 取款金额为 1800
D1阻塞中
D1 余额不足,当前余额为 600 取款金额为 12600

执行步骤如下:

  1. 初始化账户,有余额100。

  2. S1,S3完成存款。

  3. D2取款,余额不足,释放锁并阻塞线程,进入等待队列中。

  4. S2完成存款操作后,会唤醒挂起的线程,这时D2完成了取款。

  5. D3取款,余额不足,释放锁并阻塞线程,进入等待队列中。

  6. S4完成存款操作后,唤醒D3,但是依然余额不足,D3 取款失败。

  7. D1 进行取款,等待2s钟,无任何线程将其唤醒,取款失败。

这里需要注意的是,当Condition调用await()方法时,当前线程会释放锁(否则就和Sychnize就没有区别了)

将银行账户中的 锁改成非公平锁时,执行的结果如下:

1存款100,当前余额为1100S3存款600,当前余额为1700D2阻塞中S2存款1000,当前余额为2700D3取款1800,当前余额为900D2 余额不足,当前余额为 900   取款金额为 2300S4存款200,当前余额为1100D1阻塞中D1 余额不足,当前余额为 1100   取款金额为 12600

D2 取款出现余额不足后释放锁,进入等待状态。但是当S2线程完成存款后并没有立刻执行D2线程,而是被D3插队了。

通过执行结果可以看出 公平锁和非公平锁的区别,公平锁能保证等待线程优先执行,但是非公平锁可能会被其它线程插队。

四、ArrayBlockingQueue中关于ReentrantLock和Condition的应用

JDK源码中关于可重入锁的非常典型的应用是 BlockingQueue,从它的源码中的成员变量大概就能知道了(ArrayBlockingQueue为例):

/** The queued items */    final Object[] items;    /** items index for next take, poll, peek or remove */    int takeIndex;    /** items index for next put, offer, or add */    int putIndex;    /** Number of elements in the queue */    int count;    /*     * Concurrency control uses the classic two-condition algorithm     * found in any textbook.     */    /** Main lock guarding all access */
// 主要解决多线程访问的线程安全性问题    final ReentrantLock lock;    /** Condition for waiting takes */
 // 添加元素时,通过notEmpty 唤醒消费线程(在等待该条件)    private final Condition notEmpty;    /** Condition for waiting puts */
 // 删除元素时,通过 notFull 唤醒生成线程(在等待该条件)    private final Condition notFull;

ArrayBlockingQueue 是一个典型的生产者消费者模型,通过一个数组保存元素。为了保证添加和删除元素的线程安全性,增加了可重入锁和条件变量。

可重入锁主要保证多线程对阻塞队列的操作是线程安全的,同时为了让被阻塞的消费者或者生产者能够被自动唤醒,这里引入了条件变量。

当队列已满时,Producer会被阻塞,此时如果Customer消费一个元素时,被阻塞的Producer就会被自动唤醒并往队列中添加元素。

上面的两个例子可见java.util.concurrent.locks包下的ReentrantLock和Condition配合起来的灵活性及实用性。

上述内容就是java并发编程中如何通过ReentrantLock和Condition实现银行存取款,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注行业资讯频道。

0