前面介绍了如何通过线程同步来避免多线程并发的资源冲突问题,然而添加synchronized的方式只在简单场合够用,在一些高级场合就暴露出它的局限性,包括但不限于下列几点:
1、synchronized必须用于修饰方法或者代码块,也就是一定会有花括号把需要同步的代码给包裹起来。这样的话,花括号内外的变量交互比较麻烦,特别是同步代码块,多出来的花括号硬生生把原来的代码隔离开,只好通过局部变量来传递数值。
2、synchronized的同步方式很傻,一旦同步方法/代码块被某个线程执行,其它线程到了这里就必须等待前个线程的处理,要是前个线程迟迟不退出同步方法/代码块,那么其它线程只能傻傻的一直等下去。
3、synchronized无法判断当前线程处于等待队列中的哪个位置,等待队列要是很长的话,也许走另外一条分支更合适,但synchronized是个死脑筋,它不知道等待队列的详细情况,也就无从选择更优的代码路径。
为此Java又设计了一套锁机制,通过锁的对象把加锁和解锁操作分离开,从而解决同步方式的弊端。锁机制提供了好几把锁,最常见的名叫可重入锁ReentrantLock,所谓可重入,字面意思指的是支持重新进入,凡是遇到被当前线程自身锁住的代码,则仍然允许进入这块代码;但要是遇到被其它线程锁住的代码,则不允许进入那块代码。换句话说,加锁不是为了锁自己,加锁是为了锁别人,故而可重入锁又称作自旋锁,之前介绍的synchronized也属于可重入机制。下面是ReentrantLock相关的锁方法说明:
lock:对可重入锁加锁。
unlock:对可重入锁解锁。
tryLock:尝试加锁。加锁成功返回true,加锁失败返回false。该方法与lock的区别在于:lock方法会一直等待加锁,而tryLock要求立刻加锁,要是加锁失败(表示之前已经被其它线程加了锁),就马上返回false,一会都等不了。
isLocked:判断该锁是否被锁住了。
getQueueLength:获取有多少个线程正在等待该锁的释放。
回到售票线程的例子,现在把同步方式改为加锁解锁的实现,修改后的售票代码示例如下:

    // 创建一个可重入锁private final static ReentrantLock reentrantLock = new ReentrantLock();// 测试通过可重入锁避免资源冲突private static void testReentrantLock() {Runnable seller = new Runnable() {private Integer ticketCount = 100; // 可出售的车票数量@Overridepublic void run() {while (ticketCount > 0) { // 还有余票可供出售reentrantLock.lock(); // 对可重入锁加锁int count = --ticketCount; // 余票数量减一reentrantLock.unlock(); // 对可重入锁解锁// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息String left = String.format("当前余票为%d张", count);PrintUtils.print(Thread.currentThread().getName(), left);}}};new Thread(seller, "售票线程A").start(); // 启动售票线程Anew Thread(seller, "售票线程B").start(); // 启动售票线程Bnew Thread(seller, "售票线程C").start(); // 启动售票线程C}

以上采用锁机制的代码,运行起来没什么问题。可是实际业务往往不会这么简单,比如售票员在售票前还要帮旅客挑选合适的行程,这样又会消耗一定时间。通过编码演示的话,可在售票之前打开某个磁盘文件,模拟售票前的准备工作。于是添加模拟代码后的run方法变成了下面这副模样:

           public void run() {while (ticketCount > 0) { // 还有余票可供出售int count = 0;// 根据指定路径构建文件输出流对象try (FileOutputStream fos = new FileOutputStream(mFileName)) {reentrantLock.lock(); // 对可重入锁加锁count = --ticketCount; // 余票数量减一reentrantLock.unlock(); // 对可重入锁解锁fos.write(new String(""+count).getBytes()); // 把字节数组写入文件输出流} catch (Exception e) {e.printStackTrace();}// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息String left = String.format("当前余票为%d张", count);PrintUtils.print(Thread.currentThread().getName(), left);}}

接着运行上述的模拟代码,在售票日志中经常发现以下的负数余票:

………………………这里省略前面的日志……………………
17:12:06.568 售票线程C 当前余票为3张
17:12:06.569 售票线程B 当前余票为2张
17:12:06.569 售票线程A 当前余票为1张
17:12:06.570 售票线程B 当前余票为0张
17:12:06.570 售票线程A 当前余票为-1张
17:12:06.570 售票线程C 当前余票为-2张

明明每次循环之前都有判断余票数量要大于零,为啥还会出现车票被卖到负数的情况?真是咄咄怪事。原来在循环开始之后到对余票减一之间,多了一个打开文件的步骤,正是因为文件的打开操作耗费了一点点时间,导致其它线程在这一瞬间卖掉车票,而当前线程以为还有余票可卖,其结果必然导致卖出了早就卖光的车票。譬如当前线程在循环开始前检查余票数量为1,认为有票可卖,于是开始给旅客选择车票,谁知别的线程刚好在这空挡卖掉最后一张票,那么实时的余票数量减少到0,可是当前线程浑然不知,继续后面的选票与售票操作,最终又卖掉了一张票,此时余票数量刷新为-1。显然在每次循环开头检查余票不够保险,还得在选票之后售票之前再检查一次,务必确保还有余票才能进行售票操作。
鉴于检查余票和售出车票的性质有所不同,检查余票不会更改余票变量,所以它属于读操作;而售出车票会更改余票变量,所以它属于写操作。理论上可以同时进行读操作,但不能同时进行写操作。更具体地说,A线程在读的时候,B线程允许读但不允许写;A线程在写的时候,B线程既不允许读也不允许写。据此可将锁再细分为读锁和写锁两类,读锁与读锁不是互斥关系,而读锁与写锁是互斥关系,且写锁与写锁也是互斥关系。总而言之,检查余票这项操作适用于读锁,售出车票这项操作适用于写锁。
Java提供的读写锁工具名叫ReentrantReadWriteLock,意即可重入的读写锁,调用读写锁对象的readLock方法可获得读锁对象,调用读写锁对象的writeLock方法可获得写锁对象,之后再根据实际情况分别对读锁或者写锁进行加锁和解锁操作。利用读写锁优化之前的售票逻辑,主要开展以下两点修改:
1、在售票(余票数量减一)这一步骤的前面加上写锁,该步骤后面解除写锁。
2、售票之前补充检查余票的判断语句,并在检查步骤的前面加上读锁,该步骤后面解除读锁。
通过读写锁优化修改后的完整售票代码如下所示:

  // 创建一个可重入的读写锁private final static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 获取读写锁中的写锁private final static WriteLock writeLock = readWriteLock.writeLock();// 获取读写锁中的读锁private final static ReadLock readLock = readWriteLock.readLock();// 测试通过读写锁避免资源冲突private static void testReadWriteLock() {Runnable seller = new Runnable() {private Integer ticketCount = 100; // 可出售的车票数量@Overridepublic void run() {while (ticketCount > 0) { // 还有余票可供出售int count = 0;// 根据指定路径构建文件输出流对象try (FileOutputStream fos = new FileOutputStream(mFileName)) {readLock.lock(); // 对读锁加锁。加了读锁之后,其它线程可以继续加读锁,但不能加写锁if (ticketCount <= 0) { // 余票数量为0,表示已经卖光了,只好关门歇业fos.close(); // 关闭文件break; // 跳出售票的循环}readLock.unlock(); // 对读锁解锁writeLock.lock(); // 对写锁加锁。一旦加了写锁,则其它线程在此既不能读也不能写count = --ticketCount; // 余票数量减一writeLock.unlock(); // 对写锁解锁fos.write(new String(""+count).getBytes()); // 把字节数组写入文件输出流} catch (Exception e) {e.printStackTrace();}// 以下打印售票日志,包括售票时间、售票线程、当前余票等信息String left = String.format("当前余票为%d张", count);PrintUtils.print(Thread.currentThread().getName(), left);}}};new Thread(seller, "售票线程A").start(); // 启动售票线程Anew Thread(seller, "售票线程B").start(); // 启动售票线程Bnew Thread(seller, "售票线程C").start(); // 启动售票线程C}

运行上面的读写锁售票代码,从打印的售票日志中再也找不到余票为负数的情况了,可见读写锁很好地解决了盲目售票的问题。

………………………这里省略前面的日志……………………
16:29:44.899 售票线程C 当前余票为3张
16:29:44.899 售票线程B 当前余票为2张
16:29:44.899 售票线程A 当前余票为1张
16:29:44.900 售票线程C 当前余票为0张

  

更多Java技术文章参见《Java开发笔记(序)章节目录》

转载于:https://www.cnblogs.com/pinlantu/p/10907989.html

Java开发笔记(一百零一)通过加解锁避免资源冲突相关推荐

  1. Java开发笔记(一百零三)线程间的通信方式

    前面介绍了多线程并发之时的资源抢占情况,以及利用同步.加锁.信号量等机制解决资源冲突问题,不过这些机制只适合同一资源的共享分配,并未涉及到某件事由的前因后果.日常生活中,经常存在两个前后关联的事务,像 ...

  2. Java开发笔记XML报文的解析

    Java开发笔记XML报文的解析 前言 正文 代码示例 结语 前言 项目任务里需要解析xml报文. 于是开始着手学习相关知识,在查看了多篇博文后找到了一篇不错的,讲的很实用. 转载来源:Java开发笔 ...

  3. Java开发笔记(三十三)字符包装类型

    正如整型int有对应的包装整型Integer那样,字符型char也有对应的包装字符型Character.初始化字符包装变量也有三种方式,分别是:直接用等号赋值.调用包装类型的valueOf方法.使用关 ...

  4. Java开发笔记(八十六)通过缓冲区读写文件

    前面介绍了利用文件写入器和文件读取器来读写文件,因为FileWriter与FileReader读写的数据以字符为单位,所以这种读写文件的方式被称作"字符流I/O",其中字母I代表输 ...

  5. Java开发笔记(五十)几种开放性修饰符

    前面介绍子类继承父类的时候,提到了public(公共)和private(私有)两个修饰符,其中public表示它所修饰的实体是允许外部访问的:而private表示它所修饰的实体不允许外部访问,只能在当 ...

  6. (硅谷课堂项目)Java开发笔记4:前端基础知识(二)

    文章目录 (硅谷课堂项目)Java开发笔记4:前端基础知识(二) 一.NPM 1.NPM简介 1.1.什么是NPM 1.2.NPM工具的安装位置 2.使用npm管理项目 2.1.创建文件夹npm 2. ...

  7. (硅谷课堂项目)Java开发笔记2:项目概述,搭建项目环境和开发讲师管理接口

    文章目录 (硅谷课堂项目)Java开发笔记2:项目概述,搭建项目环境和开发讲师管理接口 1.项目概述 1.1 项目介绍 1.2 硅谷课程流程图 1.3 硅谷课堂功能架构 1.4 硅谷课堂技术架构 1. ...

  8. 微信公众号Java开发-笔记02【开发接入准备、开发接入】

    学习视频网址:哔哩哔哩网站 微信公众号开发-Java版 [P01-P02]微信公众号Java开发-笔记01[微信公众号介绍.开发环境搭建] [P03-P04]微信公众号Java开发-笔记02[开发接入 ...

  9. 微信公众号Java开发-笔记01【微信公众号介绍、开发环境搭建】

    学习网址:哔哩哔哩网站 微信公众号开发-Java版 微信公众号Java开发-笔记01[微信公众号介绍.开发环境搭建] 微信公众号Java开发-笔记02[] 微信公众号Java开发-笔记03[] 微信公 ...

最新文章

  1. vim括号匹配跳转操作
  2. 学习C语言的理由-别问我为什么,会用C语言,就是NB
  3. 台湾国立大学郭彦甫Matlab教程笔记(14)polynomial differentiation多项式微分
  4. Ansible Playbook企业案例:利用 playbook 安装 nginx、安装和卸载 httpd、安装mysql
  5. 直接插入排序,折半插入排序,希尔排序,简单选择排序,冒泡排序,快速排序模板以及比较次数与移动次数的分析,折半搜索算法模板
  6. 速战速决?你不会是不行吧......
  7. 复制java文件 案例
  8. 计算机基础八进制和十六进制试题,计算机基础知识考试试题
  9. 1.通俗解释分布式系统
  10. 由浅入深逐步了解 Synchronized
  11. 标签打印软件中标签间距以及边距如何设置
  12. ps新手秒变大师必备的Ps插件全在这!(mac版本)
  13. 黑色的cms商城网站后台管理模板——后台
  14. 软件工程--团队作业2
  15. 简单的超市会员管理系统
  16. raid配置ssd为缓存_固态硬盘做缓存如何设置
  17. 【51单片机实验笔记】3. LED点阵的基本控制
  18. 【java面对对象】分数类型加减乘除运算的实现
  19. 为什么说用PHP开发大型系统令人不爽
  20. 阿里云网站备案申请被驳回的问题解答汇总

热门文章

  1. 主控全志R16-魅族Gravity悬浮音响拆解
  2. 博士3年前被判定学术不端、失去工作!如今发Nature子刊证明自己是对的!
  3. 发现可以在线编辑转换下载glb模型,gltf格式模型
  4. 流放之路一直显示与服务器连接,流放之路公开测试在线太多而引起服务器崩溃...
  5. 基于视频的行人流量密度检测
  6. 中金面经及我的找工作经历的一些总结
  7. Windows下多线程编程
  8. 23岁产妇坐月子双腿险被截肢,产后绝对不能做这八件事!
  9. 雨痕大神的《学习笔记系列》
  10. “三国时代”:公募销售保有规模前100排名出炉