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

74次阅读
没有评论

共计 6593 个字符,预计需要花费 17 分钟才能阅读完成。

行业资讯    
服务器    
云计算    
java 并发编程中如何通过 ReentrantLock 和 Condition 实现银行存取款

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

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

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

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

  以下是 locks 包的相关类图:

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

        在之前我们同步一段代码或者对象时都是使用 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

执行步骤如下:

初始化账户,有余额 100。

S1,S3 完成存款。

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

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

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

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

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

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

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

 1 存款 100,当前余额为 1100
S3 存款 600,当前余额为 1700
D2 阻塞中
S2 存款 1000,当前余额为 2700
D3 取款 1800,当前余额为 900
D2  余额不足, 当前余额为  900  取款金额为  2300
S4 存款 200,当前余额为 1100
D1 阻塞中
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 是一个典型的生产者消费者模型,通过一个数组保存元素。为了保证添加和删除元素的线程安全性,增加了可重入锁和条件变量。

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

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

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

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

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

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-25发表,共计6593字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)