当前位置: 首页 > news >正文

《深入浅出 Java Concurrency》—锁机制(十) 锁的一些其它问题

转自:http://www.blogjava.net/xylz/archive/2010/07/16/326246.html

主要谈谈锁的性能以及其它一些理论知识,内容主要的出处是《Java Concurrency in Practice 》,结合自己的理解和实际应用对锁机制进行一个小小的总结。

 

首先需要强调的一点是:所有锁(包括内置锁和高级锁)都是有性能消耗的,也就是说在高并发的情况下,由于锁机制带来的上下文切换、资源同步等消耗是非常可观的。在某些极端情况下,线程在锁上的消耗可能比线程本身的消耗还要多。所以如果可能的话,在任何情况下都尽量少用锁,如果不可避免那么采用非阻塞算法是一个不错的解决方案,但是却也不是绝对的。

 

内部锁

Java语言通过synchronized关键字来保证原子性。这是因为每一个Object都有一个隐含的锁,这个也称作监视器对象。在进入synchronized之前自动获取此内部锁,而一旦离开此方式(不管通过和中方式离开此方法)都会自动释放锁。显然这是一个独占锁,每个锁请求之间是互斥的。相对于前面介绍的众多高级锁(Lock/ReadWriteLock等),synchronized的代价都比后者要高。但是synchronized的语法比较简单,而且也比较容易使用和理解,不容易写法上的错误。而我们知道Lock一旦调用了lock()方法获取到锁而未正确释放的话很有可能就死锁了。所以Lock的释放操作总是跟在finally代码块里面,这在代码结构上也是一次调整和冗余。另外前面介绍中说过Lock的实现已经将硬件资源用到了极致,所以未来可优化的空间不大,除非硬件有了更高的性能。但是synchronized只是规范的一种实现,这在不同的平台不同的硬件还有很高的提升空间,未来Java在锁上的优化也会主要在这上面。

 

性能

由于锁总是带了性能影响,所以是否使用锁和使用锁的场合就变得尤为重要。如果在一个高并发的Web请求中使用了强制的独占锁,那么就可以发现Web的吞吐量将急剧下降。

为了利用并发来提高性能,出发点就是:更有效的利用现有的资源,同时让程序尽可能的开拓更多可用的资源。这意味着机器尽可能的处于忙碌的状态,通常意义是说CPU忙于计算,而不是等待。当然CPU要做有用的事情,而不是进行无谓的循环。当然在实践中通常会预留一些资源出来以便应急特殊情况,这在以后的线程池并发中可以看到很多例子。

 

线程阻塞

锁机制的实现通常需要操作系统提供支持,显然这会增加开销。当锁竞争的时候,失败的线程必然会发生阻塞。JVM既能自旋等待(不断尝试,知道成功,很多CAS就是这样实现的),也能够在操作系统中挂起阻塞的线程,直到超时或者被唤醒。通常情况下这取决于上下文切换的开销以及与获取锁需要等待的时间二者之间的关系。自旋等待适合于比较短的等待,而挂起线程比较适合那些比较耗时的等待。

挂起一个线程可能是因为无法获取到锁,或者需要某个特定的条件,或者耗时的I/O操作。挂起一个线程需要两次额外的上下文切换以及操作系统、缓存等多资源的配合:如果线程被提前换出,那么一旦拿到锁或者条件满足,那么又需要将线程换回执行队列,这对线程而言,两次上下文切换可能比较耗时。

 

锁竞争

影响锁竞争性的条件有两个:锁被请求的频率和每次持有锁的时间。显然当而这二者都很小的时候,锁竞争不会成为主要的瓶颈。但是如果锁使用不当,导致二者都比较大,那么很有可能CPU不能有效的处理任务,任务被大量堆积。

所以减少锁竞争的方式有下面三种:

  1. 减少锁持有的时间
  2. 减少锁请求的频率
  3. 采用共享锁取代独占锁

 

死锁

如果一个线程永远不释放另外一个线程需要的资源那么就会导致死锁。这有两种情况:一种情况是线程A永远不释放锁,结果B一直拿不到锁,所以线程B就“死掉”了;第二种情况下,线程A拥有线程B需要的锁Y,同时线程B拥有线程A需要的锁X,那么这时候线程A/B互相依赖对方释放锁,于是二者都“死掉”了。

还有一种情况为发生死锁,如果一个线程总是不能被调度,那么等待此线程结果的线程可能就死锁了。这种情况叫做线程饥饿死锁。比如说在前面介绍的非公平锁中,如果某些线程非常活跃,在高并发情况下这类线程可能总是拿到锁,那么那些活跃度低的线程可能就一直拿不到锁,这样就发生了“饥饿死”。

避免死锁的解决方案是:尽可能的按照锁的使用规范请求锁,另外锁的请求粒度要小(不要在不需要锁的地方占用锁,锁不用了尽快释放);在高级锁里面总是使用tryLock或者定时机制(这个以后会讲,就是指定获取锁超时的时间,如果时间到了还没有获取到锁那么就放弃)。高级锁(Lock)里面的这两种方式可以有效的避免死锁。

 

活锁

活锁描述的是线程总是尝试某项操作却总是失败的情况。这种情况下尽管线程没有被阻塞,但是人物却总是不能被执行。比如在一个死循环里面总是尝试做某件事,结果却总是失败,现在线程将永远不能跳出这个循环。另外一种情况是在一个队列中每次从队列头取出一个任务来执行,每次都失败,然后将任务放入队列头,接下来再一次从队列头取出任务执行,仍然失败。

还有一种活锁方式发生在“碰撞协让”情况下:两个人过独木桥,如果在半路相撞,双方礼貌退出去然后再试一次。如果总是失败,那么这两个任务将一直无法得到执行。

 

总之解决锁问题的关键就是:从简单的开始,先保证正确,然后再开始优化。

相关文章:

  • Unix高级编程之文件IO
  • java集合框架学习—ArrayList的实现原理
  • 不等式证明
  • java集合框架学习—HashMap的实现原理
  • PHP 错误 系列:编码格式错误解决
  • java集合框架学习—HashSet的实现原理
  • 【腾讯优测干货分享】Android内存泄漏的简单检查与分析方法
  • java集合框架学习—LinkedHashMap的实现原理
  • C#基础-MD5验证
  • java中Hashtable与HashMap的区别
  • html5使用FileReader上传图片
  • java中hashcode()和equals()的详解
  • ADO.NET完整增删改
  • Java精华积累:初学者都应该搞懂的问题
  • EasyUI——常见用法总结
  • hexo+github搭建个人博客
  • ComponentOne 2017 V2版本正式发布
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • ES学习笔记(12)--Symbol
  • EventListener原理
  • Fabric架构演变之路
  • js递归,无限分级树形折叠菜单
  • Koa2 之文件上传下载
  • Linux后台研发超实用命令总结
  • MQ框架的比较
  • spring cloud gateway 源码解析(4)跨域问题处理
  • Vue学习第二天
  • 从0到1:PostCSS 插件开发最佳实践
  • 如何编写一个可升级的智能合约
  • 世界上最简单的无等待算法(getAndIncrement)
  • 再谈express与koa的对比
  • Unity3D - 异步加载游戏场景与异步加载游戏资源进度条 ...
  • 翻译 | The Principles of OOD 面向对象设计原则
  • ​决定德拉瓦州地区版图的关键历史事件
  • #基础#使用Jupyter进行Notebook的转换 .ipynb文件导出为.md文件
  • (ibm)Java 语言的 XPath API
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (ZT)一个美国文科博士的YardLife
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (分类)KNN算法- 参数调优
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133
  • (篇九)MySQL常用内置函数
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试
  • (算法)Travel Information Center
  • (源码版)2024美国大学生数学建模E题财产保险的可持续模型详解思路+具体代码季节性时序预测SARIMA天气预测建模
  • (转) ns2/nam与nam实现相关的文件
  • (转载)CentOS查看系统信息|CentOS查看命令
  • *p=a是把a的值赋给p,p=a是把a的地址赋给p。
  • .MSSQLSERVER 导入导出 命令集--堪称经典,值得借鉴!
  • .NET 2.0中新增的一些TryGet,TryParse等方法
  • .NET C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .Net各种迷惑命名解释