java并发编程——通过ReentrantLock,Condition实现银行存取款
Java 并发编程系列文章
Java 并发编程——Callable+Future+FutureTask
java并发编程——通过ReentrantLock,Condition实现银行存取款
java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件,但以更难用的语法为代价。
Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
以下是locks包的相关类图:
在之前我们同步一段代码或者对象时都是使用 synchronized关键字,使用的是Java语言的内置特性,然而 synchronized的特性也导致了很多场景下出现问题,比如:
在一段同步资源上,首先线程A获得了该资源的锁,并开始执行,此时其他想要操作此资源的线程就必须等待。如果线程A因为某些原因而处于长时间操作的状态,比如等待网络,反复重试等等。那么其他线程就没有办法及时的处理它们的任务,只能无限制的等待下去。如果线程A的锁在持有一段时间后可自动被释放,那么其他线程不就可以使用该资源了吗?再有就是类似于数据库中的共享锁与排它锁,是否也可以应用到应用程序中?所以引入Lock机制就可以很好的解决这些问题。
Lock提供了比 synchronized更多的功能。但是要注意以下几点:
1. Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2. Lock和synchronized有一点非常大的不同,采用 synchronized不需要用户去手动释放锁,当synchronized方法或者 synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
3. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
4. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
总结: 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 实例实质上被绑定到一个锁上。
Condition是在java .5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),
使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。 Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();
Condition中的signal()对应Object的notify();
Condition中的signalAll()对应Object的notifyAll()。
通过condition进程线程通信的例子如下:
public class ConsumerAndProducer {
final Lock lock = new ReentrantLock();
final Condition condition = lock.newCondition(); public static void main(String[] args) {
// TODO Auto-generated method stub
ConsumerAndProducer test = new ConsumerAndProducer();
Producer producer = test.new Producer("producer");
Consumer consumer = test.new Consumer("Consumer"); consumer.start();
producer.start();
} class Consumer extends Thread { public Consumer(String name) {
super(name);
} @Override
public void run() {
consume();
} private void consume() { try {
System.out.println("Consumer: run.");
lock.lock();
System.out.println("Consumer: 我在等一个新信号" + this.currentThread().getName());
Thread.sleep(5000);
condition.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
System.out.println("Consumer: 拿到一个信号" + this.currentThread().getName());
lock.unlock();
} }
} class Producer extends Thread { public Producer(String name) {
super(name);
} @Override
public void run() {
produce();
} private void produce() {
try {
System.out.println("Producer run.");
lock.lock();
System.out.println("Producer: 我拿到锁" + this.currentThread().getName());
condition.signalAll();
System.out.println("Producer: 我发出了一个信号:" + this.currentThread().getName());
} finally {
lock.unlock();
}
}
}
}
输出:
Consumer: run.
Consumer: 我在等一个新信号Consumer
Producer run.
Producer: 我拿到锁producer
Producer: 我发出了一个信号:producer
Consumer: 拿到一个信号Consumer
三、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 是一个典型的生产者消费者模型,通过一个数组保存元素。为了保证添加和删除元素的线程安全性,增加了可重入锁和条件变量。
可重入锁主要保证多线程对阻塞队列的操作是线程安全的,同时为了让被阻塞的消费者或者生产者能够被自动唤醒,这里引入了条件变量。
当队列已满时,Producer会被阻塞,此时如果Customer消费一个元素时,被阻塞的Producer就会被自动唤醒并往队列中添加元素。
上面的两个例子可见java.util.concurrent.locks包下的ReentrantLock和Condition配合起来的灵活性及实用性。
参考:
可重入锁介绍:https://blog.csdn.net/yanyan19880509/article/details/52345422
https://www.cnblogs.com/nullllun/p/9004309.html
IBM关于Lock介绍:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
http://286.iteye.com/blog/2296249
java并发编程——通过ReentrantLock,Condition实现银行存取款的更多相关文章
- 【java并发编程】Lock & Condition 协调同步生产消费
一.协调生产/消费的需求 本文内容主要想向大家介绍一下Lock结合Condition的使用方法,为了更好的理解Lock锁与Condition锁信号,我们来手写一个ArrayBlockingQueue. ...
- Java并发编程基础-ReentrantLock的机制
同步锁: 我们知道,锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源,在Lock接口出现之前,Java应用程序只能依靠synchronized关键字来实现同步锁 ...
- java并发编程基础-ReentrantLock及LinkedBlockingQueue源码分析
ReentrantLock是一个较为常用的锁对象.在上次分析的uil开源项目中也多次被用到,下面谈谈其概念和基本使用. 概念 一个可重入的互斥锁定 Lock,它具有与使用 synchronized 相 ...
- 【java并发编程】ReentrantLock 可重入读写锁
目录 一.ReentrantLock可重入锁 二.ReentrantReadWriteLock读写锁 三.读锁之间不互斥 欢迎关注我的博客,更多精品知识合集 一.ReentrantLock可重入锁 可 ...
- Java 并发编程:Callable和Future
项目中经常有些任务需要异步(提交到线程池中)去执行,而主线程往往需要知道异步执行产生的结果,这时我们要怎么做呢?用runnable是无法实现的,我们需要用callable实现. import java ...
- Java 并发编程——Executor框架和线程池原理
Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...
- Java并发编程——BlockingQueue
简介 BlockingQueue很好的解决了多线程中,如何高效安全"传输"数据的问题.通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利. 阻塞队列是 ...
- Java 并发编程——Callable+Future+FutureTask
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
- java 并发编程——Thread 源码重新学习
Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...
随机推荐
- iOS9 中关闭ATS的方法
大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) iOS9中增加了系统的安全性,你会发现默认情况下打开非http ...
- Coroutine协同程序介绍(Unity3D开发之三)
猴子原创,欢迎转载.转载请注明: 转载自Cocos2D开发网–Cocos2Dev.com,谢谢! 原文地址: http://www.cocos2dev.com/?p=496 Coroutine在Uni ...
- 【Unity Shaders】Mobile Shader Adjustment—— 什么是高效的Shader
本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...
- 5.创建表,使用alter进行表信息的增删改,Oracle回收站,集合运算
1 Oracle基于用户的管理方案 2 DDL语句可以管理数据库的对象有:视图 索引 序列 同义词 约束 3 创建一个表,有2个条件(1 有权限:2有表空间) Oracle给你提 ...
- SpriteBuilder实现2D精灵光影明暗反射效果(二)
使用SpriteBuilder新建一个项目,将默认MainScene.ccb中的内容统统删掉,此时场景应该是一片漆黑. 将官网中的2张图片以及我自己做的2张图片全部拖拽到其文件视图中去: 其中加_n后 ...
- 【一天一道LeetCode】#125. Valid Palindrome
一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 Given a ...
- 流密码和RC4
定义:流密码是对称密码算法,从明文输入流逐位或逐字节产生密文输出. 实例:使用最广泛的流密码是RC4. 流密码结构: 流密码类似于”一次一密”,不同的是”一次一密”使用的是真正的随机数流,而流密码使用 ...
- Leetcode_7_Reverse Integer
本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/41578077 Reverse Integer Revers ...
- Dynamics CRM ADFS及IFD部署后延长系统注销时间
Dynamics CRM 部署IFD后,一段时间后登陆状态会失效,系统会提示让你重新登陆,可以通过延长失效时间来规避 在 powershell中执行如下指令 Set-ADFSRelyingPartyT ...
- WebStorm开发工具设置React Native智能提示
最近在做React Native开发的时候,相信大家一般会使用WebStorm,Sublime,Atom等等开发工具.二之前搞前端的对WebStorm会很熟悉,WebStorm最新版是WebStorm ...