如何改善Java中锁的性能
我们努力为自己的产品所遇到的问题思考解决办法,但在这篇文章中我将给大家分享几种常用的技术,包括分离锁、并行数据结构、保护数据而非代码、缩小锁的作用范围,这几种技术可以使我们不使用任何工具来检测死锁。
锁不是问题的根源,锁之间的竞争才是
通常在多线程的代码中遇到性能方面的问题时,一般都会抱怨是锁的问题。毕竟锁会降低程序的运行速度和其较低的扩展性是众所周知的。因此,如果带着这种“常识”开始优化代码,其结果很有可能是在之后会出现讨人厌的并发问题。
因此,明白竞争锁和非竞争锁的不同是非常重要的。当一个线程试图进入 另一个线程正在执行的同步块或方法时会触发锁竞争。该线程会被强制进入等待状态,直到第一个线程执行完同步块并且已经释放了监视器。当同一时间只有一个线 程尝试执行同步的代码区域时,锁会保持非竞争的状态。
事实上,在非竞争的情况下和大多数的应用中,JVM已经对同步进行了优化。非竞争锁在执行过程中不会带来任何额外的开销。因此,你不应该因为性能问题抱怨锁,应该抱怨的是锁的竞争。当有了这个认识之后,让我们来看下能做些什么,以降低竞争的可能性或减少竞争的持续时间。
保护数据而非代码
解决线程安全问题的一个快速的方法就是对整个方法的可访问性加锁。例如下面这个例子,试图通过这种方法来建立一个在线扑克游戏服务器:
public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/} public synchronized void createTable() {/*body skipped for brevity*/} public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}
作者的意图是好的——当一个新的玩家加入牌桌 时,必须确保牌桌上的玩家个数不会超过牌桌可以容纳的玩家总个数9。
但是这种解决办法事实上无论何时都要对玩家进入牌桌进行控制——即使是在服务器的访问量较小的时候也是这样,那些等 待锁释放的线程注定会频繁的触发系统的竞争事件。包含对账户余额和牌桌限制检查的锁定块很可能大幅提高调用操作的开销,而这无疑会增加竞争的可能性和持续 时间。
解决的第一步就是确保我们保护的是数据,而不是从方法声明移到方法体中的那段同步声明。对于上面那个简单的例子来说,可能改变不大。但是我们要站在整个游戏服务的接口之上来考虑,而不是单单的一个join()方法。
原本可能只是一个小小的改变,影响的可是整个类的行为方式。玩家无论何时加入牌桌,先前的同步方法都会对整个GameServer实例加锁,进而会与那些同时试图离开牌桌的玩家产生竞争。将锁从方法声明移到方法体中会延迟锁的加载,进而降低了锁竞争的可能性。
缩小锁的作用范围
现在,当确信了需要保护的是数据而非程序后,我们应该确保我们只在必要的地方加锁——例如当上面的代码被重构之后:
这样那段包含对玩家账号余额检测(可能引发IO操作)的可能引起费时操作的代码,被移到了锁控制的范围之外。注意,现在锁仅仅被用来防止玩家人数超过桌子可容纳的人数,对账户余额的检查不再是该保护措施的一部分了。
分离锁
你可以从上面例子最后一行代码清楚的看到:整个数据结构是由相同的锁保护着。考虑到在这一种数据结构中可能会有数以千计的牌桌,而我们必须保护任何一张牌桌的人数不超过容量,在这样的情况下仍然会有很高的风险出现竞争事件。
关于这个有一个简单的办法,就是对每一张牌桌引入分离锁,如下面这个例子所示:
现在,我们只对单一牌桌的可访问性进行同步而不是所有的牌桌,这样就显著降低了出现锁竞争的可能性。举一个具体的例子,现在在我们的数据结构中有100个牌桌的实例,那么现在发生竞争的可能性就会比之前小100倍。
使用线程安全的数据结构
另一个可以改善的地方就是抛弃传统的单线程数据结构,改用被明确设计为线程安全的数据结构。例如,当采用ConcurrentHashMap来储存你的牌桌实例时,代码可能像下面这样:
public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/} public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/} public synchronized void createTable() { Table table = new Table(); tables.put(table.getId(), table); public synchronized void destroyTable(Table table) { tables.remove(table.getId());
在join()和leave()方法内部的同步块仍然和先前的例子一样,因为我们要保证单个牌桌数据的完整性。ConcurrentHashMap 在这点上并没有任何帮助。但我们仍然会在increateTable()和destoryTable()方法中使用ConcurrentHashMap创建和销毁新的牌桌,所有这些操作对于ConcurrentHashMap来说是完全同步的,其允许我们以并行的方式添加或减少牌桌的数量。
其他一些建议和技巧
降低锁的可见度。在上面的例子中,锁被声明为public(对外可见),这可能会使得一些别有用心的人通过在你精心设计的监视器上加锁来破坏你的工作。
通过查看java.util.concurrent.locks 的API来看一下 有没有其它已经实现的锁策略,使用其改进上面的解决方案。
使用原子操作。在上面正在使用的简单递增计数器实际上并不要求加锁。上面的例子中更适合使用 AtomicInteger代替Integer作为计数器。
最后一点,无论你是否正在使用Plumber的自动死锁检测解决方案,还是手动从线程转储获得解决办法的信息,都希望这篇文章可以为你解决锁竞争的问题带来帮助。
作者:飘扬叶
来源:51CTO
大数据开发基础的编程语言的Java的并发/多线程编程的锁 在Java并发编程中,锁是一种非常重要的机制,用于保证多个线程之间访问共享资源的安全性。本文将介绍Java的锁机制以及如何使用锁进行多线程编程。
java中的锁类型 java中的锁不同的分类,只是从不同的角度标准去分类的。一把锁也有可能同时占有多个标准,符合多种分类,比如ReentrantLock既是可中断锁,又是可重入锁。根据分类标准,我们把锁分为以下七个大类别。 一、偏向锁、轻量级锁、重量级锁 二、可重入锁、非可重入锁 三、共享锁、独占锁
JAVA中锁的解决方案 在上一节中,我们给大家介绍了什么是锁,以及锁的使用场景,我相信大家对锁的定义,以及锁的重要性都有了比较清晰的认识。在这一节中,我们会给大家继续做深入的介绍,介绍JAVA为我们提供的不同种类的锁。
包名全小写,类名首字母大写,常量全部大写并用下划线分割,变量采用驼峰命名的方法。注意对于O这个容易混淆要加注释 包名如 com.
相关文章
- java虚拟机学习-JVM调优总结-分代垃圾回收详述(9)
- JAVA遍历机制的性能的比较
- 【JAVA】MyEclipse 各菜单项:功能、说明(转自CSDN - lgx06)
- Java当中什么场景使用递归,如何使用
- JVM性能优化, Part 5:Java的伸缩性
- Java 下的 JSON库性能比较:JSON.simple vs. GSON vs. Jackson vs. JSONP
- java 大厂面试指南:性能优化 + 微服务 + 并发编程 + 开源框架 + 分布式
- Java常见bean 工具类性能对比
- Hbase框架原理及相关的知识点理解、Hbase访问MapReduce、Hbase访问Java API、Hbase shell及Hbase性能优化总结
- java多线程面试题整理及回答
- 第69节:Java中数据库的多表操作
- java 线程间的通信 (wait / notify / notifyAll)
- java性能优化
- Java性能最后一个领域:去除垃圾回收器
- java ASM
- Java学习-028-JSON 之二 -- 数据读取
- Jmeter性能测试实战之java.net.BindException: Address already in use报错解决方案
- 诊断Java代码中常见的数据库性能热点问题应该这么做!
- java io性能分析
- Java程序性能优化
- Java Map各遍历方式的性能比较
- Java Map遍历方式的选择
- 使用java自带工具监控jvm运行状态
- 华为OD机试 - 求最多可以派出多少支团队(Java) | 机试题+算法思路+考点+代码解析 【2023】
- jvm 命令使用调优 通过jstat、jmap对java程序进行性能调优
- 关于 Java Web 项目性能提升的一些思路
- How to improve Java's I/O performance( 提升 java i/o 性能)
- JAVA:从public static void main(String args[])開始
- 【Java】Synchronized锁范围