Java 并发编程系列文章

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 1.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 classConsumerAndProducer {final Lock lock = newReentrantLock();final Condition condition =lock.newCondition();public static voidmain(String[] args) {//TODO Auto-generated method stub

ConsumerAndProducer test = newConsumerAndProducer();

Producer producer= test.new Producer("producer");

Consumer consumer= test.new Consumer("Consumer");

consumer.start();

producer.start();

}class Consumer extendsThread {publicConsumer(String name) {super(name);

}

@Overridepublic voidrun() {

consume();

}private voidconsume() {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 extendsThread {publicProducer(String name) {super(name);

}

@Overridepublic voidrun() {

produce();

}private voidproduce() {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();

}

}

}

}

View Code

输出:

Consumer: run.

Consumer: 我在等一个新信号Consumer

Producer run.

Producer: 我拿到锁producer

Producer: 我发出了一个信号:producer

Consumer: 拿到一个信号Consumer

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

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

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

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

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

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

/*** 普通银行账户,不可透支*/

public classMyCount {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,intcash) {this.oid =oid;this.cash =cash;

}/*** 存款

*@paramx 操作金额

*@paramname 操作人*/

public void saving(intx, String name) {

lock.lock();//获取锁

if (x > 0) {

cash+= x; //存款

System.out.println(name + "存款" + x + ",当前余额为" +cash);

}

_draw.signalAll();//唤醒所有等待线程。

lock.unlock(); //释放锁

}/*** 取款

*@paramx 操作金额

*@paramname 操作人*/

public void drawing(intx, 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 extendsThread {private String name; //操作人

private MyCount myCount; //账户

private int x; //存款金额

SaveThread(String name, MyCount myCount,intx) {this.name =name;this.myCount =myCount;this.x =x;

}public voidrun() {

myCount.saving(x, name);

}

}/*** 取款线程类*/

static class DrawThread extendsThread {private String name; //操作人

private MyCount myCount; //账户

private int x; //存款金额

DrawThread(String name, MyCount myCount,intx) {this.name =name;this.myCount =myCount;this.x =x;

}public voidrun() {

myCount.drawing(x, name);

}

}public static voidmain(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 取款金额为 2300S4存款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

http://286.iteye.com/blog/2296249

java 银行并发_java并发编程——通过ReentrantLock,Condition实现银行存取款相关推荐

  1. java投票锁_Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家去排队本着先来 ...

  2. java线程池并发_Java并发教程–线程池

    java线程池并发 Java 1.5中提供的最通用的并发增强功能之一是引入了可自定义的线程池. 这些线程池使您可以对诸如线程数,线程重用,调度和线程构造之类的东西进行大量控制. 让我们回顾一下. 首先 ...

  3. python银行账户资金交易管理_python 实现网上商城,转账,存取款等功能的信用卡系统...

    一.要求 二.思路 1.购物类buy 接收 信用卡类 的信用卡可用可用余额, 返回消费金额 2.信用卡(ATM)类 接收上次操作后,信用卡可用余额,总欠款,剩余欠款,存款 其中: 1.每种交易类型不单 ...

  4. java lock 对象_Java并发编程锁系列之ReentrantLock对象总结

    Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...

  5. java线程池_Java 并发编程 线程池源码实战

    作者 | 马启航 杏仁后端工程师.「我头发还多,你们呢?」 一.概述 笔者在网上看了好多的关于线程池原理.源码分析相关的文章,但是说实话,没有一篇让我觉得读完之后豁然开朗,完完全全的明白线程池,要么写 ...

  6. java内存 海子_Java并发编程:从根源上解析volatile关键字的实现

    Java并发编程:volatile关键字解析 1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析volatile关键字 使用volatile关键字的场景 2.内存模型的 ...

  7. Java 高并发_JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过!...

    JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过! 1.JPG (37.82 KB, 下载次数: 0) 2018-12-3 09:40 上传 2.JPG (28 ...

  8. java计算时间差_JAVA并发编程三大Bug源头(可见性、原子性、有序性),彻底弄懂...

    原创声明:本文转载自公众号[胖滚猪学编程]​ 某日,胖滚猪写的代码导致了一个生产bug,奋战到凌晨三点依旧没有解决问题.胖滚熊一看,只用了一个volatile就解决了.并告知胖滚猪,这是并发编程导致的 ...

  9. java 延迟初始化_Java并发编程——延迟初始化占位类模式

    --仅作笔记使用,内容多摘自<java并发编程实战> 在并发编程中,如果状态变量仅在单个线程中初始化和使用,自然是线程安全的,但一旦涉及到线程间的数据交互,如何声明一个用于多线程的单例状态 ...

最新文章

  1. .NET : 再谈谈多线程
  2. Win32下内存分配简单示例 - 使用CFree
  3. 数据挖掘 —— 模型评估
  4. JAVA知识学习——类的修饰符
  5. LeetCode547. Friends Circles 利用union find | bfs | dfs三种方法解决
  6. 原生js 样式的操作整理
  7. 有点贵但卖光了!这款旗舰要火了吗...
  8. 移动互联网的发展趋势ios与android哪更有前景,移动互联网是什么?移动互联网现状,发展趋势及前景...
  9. C#语言-NPOI.dll导入Excel功能的实现
  10. 产品经理学习记录(一)
  11. 总结命令----tar
  12. 从CVPR2019看计算机视觉的最新趋势
  13. Wireless Communications - 2.4 Ray Tracing
  14. Linux环境部署:开启电脑虚拟化
  15. 服务器中心地址,互联网时间同步服务器地址(国家授时中心服务器)
  16. 【编译原理】NFA转DFA(子集构造法)
  17. 小米笔记本如何开启VT虚拟化
  18. ubuntu 14.04 搜狗拼音安装
  19. 魅族手机里的便签怎么导出转移到新的手机上?
  20. ec11编码器c语言程序,EC11旋转编码器电路和程序

热门文章

  1. Type-c PD QC AFC取电芯片 快充芯片LDR6328S
  2. 「newbee-mall新蜂商城开源啦」1000 Star Get !仓库Star数破千!记录一下
  3. 周(杰伦)董《不能说的秘密》观后感言
  4. LeetCode -- 765.情侣牵手
  5. Linux常用命令——rev命令
  6. Android Camera 开发你该知道的秘密㊙️-新手入门必备
  7. Python|装饰器|执行时间|递归|动态属性|静态方法和类|继承和多态|isinstance类型判断|溢出|“魔法”方法|语言基础50课:学习记录(6)-函数的高级应用、面向对象编程、进阶及应用
  8. 菜鸟总结之——数据完整性
  9. 用重写HandleMessage
  10. aspose.cell java_aspose-cell 使用