如何提高 Java 中锁的性能
锁不是问题的根源,锁之间的竞争才是
通常在多线程的代码中遇到性能方面的问题时,一般都会抱怨是锁的问题。毕竟锁会降低程序的运行速度和其较低的扩展性是众所周知的。因此,如果带着这种“常识”开始优化代码,其结果很有可能是在之后会出现讨人厌的并发问题。
因此,明白竞争锁和非竞争锁的不同是非常重要的。当一个线程试图进入 另一个线程正在执行的同步块或方法时会触发锁竞争。该线程会被强制进入等待状态,直到第一个线程执行完同步块并且已经释放了监视器。当同一时间只有一个线 程尝试执行同步的代码区域时,锁会保持非竞争的状态。
事实上,在非竞争的情况下和大多数的应用中,JVM已经对同步进行了优化。非竞争锁在执行过程中不会带来任何额外的开销。因此,你不应该因为性能问题抱怨锁,应该抱怨的是锁的竞争。当有了这个认识之后,让我们来看下能做些什么,以降低竞争的可能性或减少竞争的持续时间。
保护数据而非代码
解决线程安全问题的一个快速的方法就是对整个方法的可访问性加锁。例如下面这个例子,试图通过这种方法来建立一个在线扑克游戏服务器:
class GameServer {public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();public synchronized void join(Player player, Table table) {if (player.getAccountBalance() > table.getLimit()) {List<Player> tablePlayers = tables.get(table.getId());if (tablePlayers.size() < 9) {tablePlayers.add(player);}}}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()方法。
class GameServer {public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();public void join(Player player, Table table) {synchronized (tables) {if (player.getAccountBalance() > table.getLimit()) {List<Player> tablePlayers = tables.get(table.getId());if (tablePlayers.size() < 9) {tablePlayers.add(player);}}}}public void leave(Player player, Table table) {/* body skipped for brevity */}public void createTable() {/* body skipped for brevity */}public void destroyTable(Table table) {/* body skipped for brevity */}
}
原本可能只是一个小小的改变,影响的可是整个类的行为方式。玩家无论何时加入牌桌,先前的同步方法都会对整个GameServer实例加锁,进而会与那些同时试图离开牌桌的玩家产生竞争。将锁从方法声明移到方法体中会延迟锁的加载,进而降低了锁竞争的可能性。
缩小锁的作用范围
现在,当确信了需要保护的是数据而非程序后,我们应该确保我们只在必要的地方加锁——例如当上面的代码被重构之后:
public class GameServer {public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();public void join(Player player, Table table) {if (player.getAccountBalance() > table.getLimit()) {synchronized (tables) {List<Player> tablePlayers = tables.get(table.getId());if (tablePlayers.size() < 9) {tablePlayers.add(player);}}}}//other methods skipped for brevity
}
这样那段包含对玩家账号余额检测(可能引发IO操作)的可能引起费时操作的代码,被移到了锁控制的范围之外。注意,现在锁仅仅被用来防止玩家人数超过桌子可容纳的人数,对账户余额的检查不再是该保护措施的一部分了。
分离锁
你可以从上面例子最后一行代码清楚的看到:整个数据结构是由相同的锁保护着。考虑到在这一种数据结构中可能会有数以千计的牌桌,而我们必须保护任何一张牌桌的人数不超过容量,在这样的情况下仍然会有很高的风险出现竞争事件。
关于这个有一个简单的办法,就是对每一张牌桌引入分离锁,如下面这个例子所示:
public class GameServer {public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();public void join(Player player, Table table) {if (player.getAccountBalance() > table.getLimit()) {List<Player> tablePlayers = tables.get(table.getId());synchronized (tablePlayers) {if (tablePlayers.size() < 9) {tablePlayers.add(player);}}}}//other methods skipped for brevity
}
现在,我们只对单一牌桌的可访问性进行同步而不是所有的牌桌,这样就显著降低了出现锁竞争的可能性。举一个具体的例子,现在在我们的数据结构中有100个牌桌的实例,那么现在发生竞争的可能性就会比之前小100倍。
使用线程安全的数据结构
另一个可以改善的地方就是抛弃传统的单线程数据结构,改用被明确设计为线程安全的数据结构。例如,当采用ConcurrentHashMap来储存你的牌桌实例时,代码可能像下面这样:
public class GameServer {public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();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的自动死锁检测解决方案,还是手动从线程转储获得解决办法的信息,都希望这篇文章可以为你解决锁竞争的问题带来帮助。
如何提高 Java 中锁的性能相关推荐
- Java中多线程的性能比较
Java中有多种用于多线程的技术. 可以通过同步关键字,锁或原子变量来并行化Java中的一段代码. 这篇文章将比较使用synced关键字ReentrantLock,getAndIncrement()以 ...
- 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础
欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...
- 铂金1:探本溯源-为何说Lock接口是Java中锁的基础
欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...
- java中锁的介绍及运用
个人免费资源分享网站:http://xiaocaoshare.com/ 1.悲观锁和乐观锁 对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加 ...
- Java如何避免重量级锁,Java 中锁是如何一步步膨胀的(偏向锁、轻量级锁、重量级锁)...
文章目录 重量级锁(Mutex Lock) 偏向锁(比较 ThreadID) 偏向锁获取过程 偏向锁的释放 轻量级锁(自旋) 轻量级锁的加锁过程 轻量级锁的释放 总结 重量级锁(Mutex Lock) ...
- Java中锁的使用和实现
首先,我们要了解一个概念,JAVA中的锁到底是什么呢? 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源. Lock接口 在Java SE 5之后,并发包中新增 ...
- Java中String连接性能的分析
总结:如果String的数量小于4(不含4),使用String.concat()来连接String,否则首先计算最终结果的长度,再用该长度来创建一个StringBuilder,最后使用这个String ...
- java中锁的基本原理和升级:偏向锁、轻量级锁、重量级锁
目录 由一个问题引发的思考 多线程对于共享变量访问带来的安全性问题 线程安全性 思考如何保证线程并行的数据安全性 synchronized 的基本认识 synchronized 的基本语法 synch ...
- java正则表达式性能_译:Java 中的正则表达式性能概述
译者:Darren Luo 1. 概述 在本快速教程中,我们将展示模式匹配引擎是如何工作的.我们还将介绍在 Java 中优化正则表达式的不同方式. 有关正则表达式的的使用介绍,请参阅此文. 2. 模式 ...
最新文章
- 关于loadtxt编码问题的解决方法
- 算法工程师和算法框架开发,谁会代表未来?
- [转载] 大型网站的 HTTPS 实践(一)—— HTTPS 协议和原理
- 远程视频监控之驱动篇(LED)
- 域名转让代码_互联网域名转让协议(中英)
- ironpython2.7_IronPython
- iTextSharp 添加文字内容
- 面试总结:测试常见面试题汇总
- 阿里巴巴使命、愿景、价值观、绩效管理中的六大价值观、TAE 与 TOP、聚石塔、阿里云的差异化服务
- 天翼云携手华为,强强联合,共创数据存储新生态
- 用一生的漫长,等待最初的绿色
- 链式法则(chain rule)
- js-如何获取class对象
- hp mfp m281fdw 彩色激光打印机不通电
- 给iOS开发者的Sketch入门教程
- 球差产生的原因、定量分析和校正方法(zemax)
- 你怀疑过“温水煮青蛙”的故事吗
- STM32 keil如何下载STM32芯片F1/F4固件库 .pack文件
- 4g+uim卡是什么卡_从 2G 到 4G,中国电信天翼 UIM 手机卡图赏
- 计算机耦合分类,耦合性 (计算机科学)