After we introduced locked thread detection to Plumbr couple of months ago, we have started to receive queries similar to “hey, great, now I understand what is causing my performance issues, but what I am supposed to do now?”

We are working hard to build the solution instructions into our own product, but in this post I am going to share several common techniques you can apply independent of the tool used for detecting the lock. The methods include lock splitting, concurrent data structures, protecting the data instead of the code and lock scope reduction.

Locking is not evil, lock contention is

Whenever you face a performance  problem with the threaded code there is a chance that you will start blaming locks. After all, common “knowledge” is that locks are slow and limit scalability. So if you are equipped with this “knowledge” and start to optimize the code and getting rid of locks there is a chance that you end up introducing nasty concurrency bugs that will surface later on.

So it is important to understand the difference between contended and uncontended locks. Lock contention occurs when a thread is trying to enter the synchronized block/method currently executed by another thread. This second thread is now forced to wait until the first thread has completed executing the synchronized block and releases the monitor. When only one thread at a time is trying to execute the synchronized code, the lock stays uncontended.

As a matter of fact, synchronization in JVM is optimized for the uncontended case and for the vast majority of the applications, uncontended locks pose next to no overhead during execution. So, it is not locks you should blame for performance, but contended locks. Equipped with this knowledge, lets see what we can do to reduce either the likelihood of contention or the length of the contention.

Protect the data not the code

A quick way to achieve thread-safety is to lock access to the whole method. For example, take look at the following example, illustrating a naive attempt to build an online poker server:

01.class GameServer {
02.public Map<<String, List<Player>> tables = new HashMap<String, List<Player>>();
03. 
04. 
05. 
06. 
07.public synchronized void join(Player player, Table table) {
08.if (player.getAccountBalance() > table.getLimit()) {
09.List<Player> tablePlayers = tables.get(table.getId());
10.if (tablePlayers.size() < 9) {
11.tablePlayers.add(player);
12.}
13.}
14.}
15.public synchronized void leave(Player player, Table table) {/*body skipped for brevity*/}
16.public synchronized void createTable() {/*body skipped for brevity*/}
17.public synchronized void destroyTable(Table table) {/*body skipped for brevity*/}
18.}

The intentions of the author have been good - when new players join() the table, there must be a guarantee that the number of players seated at the table would not exceed the table capacity of nine.

But whenever such a solution would actually be responsible for seating players to tables - even on a poker site with moderate traffic, the system would be doomed to constantly trigger contention events by threads waiting for the lock to be released. Locked block contains account balance and table limit checks which potentially can involve expensive operations both increasing the likelihood and length of the contention.

First step towards solution would be making sure we are protecting the data, not the code by moving the synchronization from the method declaration to the method body. In the minimalistic example above, it might not change much at the first place. But lets consider the whole GameServerinterface, not just the single join() method:

01.class GameServer {
02.public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
03. 
04. 
05. 
06. 
07.public void join(Player player, Table table) {
08.synchronized (tables) {
09.if (player.getAccountBalance() > table.getLimit()) {
10.List<Player> tablePlayers = tables.get(table.getId());
11.if (tablePlayers.size() < 9) {
12.tablePlayers.add(player);
13.}
14.}
15.}
16.}
17.public void leave(Player player, Table table) {/* body skipped for brevity */}
18.public void createTable() {/* body skipped for brevity */}
19.public void destroyTable(Table table) {/* body skipped for brevity */}
20.}

What originally seemed to be a minor change, now affects the behaviour of the whole class. Whenever players were joining tables, the previously synchronized methods locked on theGameServer instance (this) and introduced contention events to players trying to simultaneouslyleave() tables. Moving the lock from the method signature to the method body postpones the locking and reduces the contention likelihood.

Reduce the lock scope

Now, after making sure it is the data we actually protect, not the code, we should make sure our solution is locking only what is necessary - for example when the code above is rewritten as follows:

01.public class GameServer {
02.public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
03. 
04. 
05. 
06. 
07.public void join(Player player, Table table) {
08.if (player.getAccountBalance() > table.getLimit()) {
09.synchronized (tables) {
10.List<Player> tablePlayers = tables.get(table.getId());
11.if (tablePlayers.size() < 9) {
12.tablePlayers.add(player);
13.}
14.}
15.}
16.}
17.//other methods skipped for brevity
18.}

then the potentially time-consuming operation of checking player account balance (which potentially can involve IO operations) is now outside the lock scope. Notice that the lock was introduced only to protect against exceeding the table capacity and the  account balance check is not anyhow part of this protective measure.

Split your locks

When we look at the last code example, you can clearly notice that the whole data structure is protected by the same lock. Considering that we might hold thousands of poker tables in this structure, it still poses a high risk for contention events  as we have to protect each table separately from overflowing in capacity.

For this there is an easy way to introduce individual locks per table, such as in the following example:

01.public class GameServer {
02.public Map<String, List<Player>> tables = new HashMap<String, List<Player>>();
03. 
04. 
05. 
06. 
07.public void join(Player player, Table table) {
08.if (player.getAccountBalance() > table.getLimit()) {
09.List<Player> tablePlayers = tables.get(table.getId());
10.synchronized (tablePlayers) {
11.if (tablePlayers.size() < 9) {
12.tablePlayers.add(player);
13.}
14.}
15.}
16.}
17.//other methods skipped for brevity
18.}

Now, if we synchronize the access only to the same table instead of all the tables, we have significantly reduced the likelihood of locks becoming contended. Having for example 100 tables in our data structure, the likelihood of the contention is now 100x smaller than before.

Use concurrent data structures

Another improvement is to drop the traditional single-threaded data structures and use data structures designed explicitly for concurrent usage. For example, when picking ConcurrentHashMapto store all your poker tables would result in code similar to following:

01.public class GameServer {
02.public Map<String, List<Player>> tables = new ConcurrentHashMap<String, List<Player>>();
03. 
04. 
05. 
06. 
07.public synchronized void join(Player player, Table table) {/*Method body skipped for brevity*/}
08.public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}
09. 
10. 
11. 
12. 
13.public synchronized void createTable() {
14.Table table = new Table();
15.tables.put(table.getId(), table);
16.}
17. 
18. 
19. 
20. 
21.public synchronized void destroyTable(Table table) {
22.tables.remove(table.getId());
23.}
24.}

The synchronization in join() and leave() methods is still behaving as in our previous example, as we need to protect the integrity of individual tables. So no help from ConcurrentHashMap in this regards. But as we are also creating new tables and destroying tables in createTable() and destroyTable()methods, all these operations to the ConcurrentHashMap are fully concurrent, permitting to increase or reduce the number of tables in parallel.

Other tips and tricks

  • Reduce the visibility of the lock. In the example above, the locks are declared public and are thus visible to the world, so there is there is a chance that someone else will ruin your work by also locking on your carefully picked monitors.
  • Check out java.util.concurrent.locks to see whether any of the locking strategies implemented there will improve the solution.
  • Use atomic operations. The simple counter increase we are actually conducting in example above does not actually require a lock. Replacing the Integer in count tracking withAtomicInteger would most suit this example just fine.

Hope the article helped you to solve the lock contention issues, independent of whether you are using Plumbr automatic lock detection solutionor manually extracting the information from thread dumps.

reference from:

http://java.dzone.com/articles/improving-lock-performance

Improving Lock Performance in Java--reference的更多相关文章

  1. What Influences Method Call Performance in Java?--reference

    reference from:https://www.voxxed.com/blog/2015/02/too-fast-too-megamorphic-what-influences-method-c ...

  2. Java Reference 源码分析

    @(Java)[Reference] Java Reference 源码分析 Reference对象封装了其它对象的引用,可以和普通的对象一样操作,在一定的限制条件下,支持和垃圾收集器的交互.即可以使 ...

  3. java Reference

    相关讲解,参考: Java Reference 源码分析 Java Reference详解 Reference: // 名称说明下:Reference指代引用对象本身,Referent指代被引用对象 ...

  4. Java Reference & ReferenceQueue一览

    Overview The java.lang.ref package provides more flexible types of references than are otherwise ava ...

  5. Java Reference核心原理分析

    本文转载自Java Reference核心原理分析 导语 带着问题,看源码针对性会更强一点.印象会更深刻.并且效果也会更好.所以我先卖个关子,提两个问题(没准下次跳槽时就被问到). 我们可以用Byte ...

  6. Java Reference简要概述

    @(Java)[Reference] Java Reference简要概述 Reference对象封装了其它对象的引用,可以和普通的对象一样操作. Java提供了四种不同类型的引用,引用级别从高到低分 ...

  7. Monitor and diagnose performance in Java SE 6--转载

    Java SE 6 provides an in-depth focus on performance, offering expanded tools for managing and monito ...

  8. CLH lock 原理及JAVA实现

    --喜欢记得关注我哟[shoshana]--​ 前记 JUC中的Lock中最核心的类AQS,其中AQS使用到了CLH队列的变种,故来研究一下CLH队列的原理及JAVA实现 一. CLH背景知识 SMP ...

  9. Implementing the skip list data structure in java --reference

    reference:http://www.mathcs.emory.edu/~cheung/Courses/323/Syllabus/Map/skip-list-impl.html The link ...

随机推荐

  1. Android TextView自动实现省略号

    TextView自带的可以通过 android:ellipsize="end" android:singleLine="true"实现单行省略,  但是当我们需 ...

  2. Import Items – Validation Multiple Languages Description

            ð  提交标准请求创建和更新物料,因语言环境与处理次序方式等因素,造成物料中英(更多语言)描述和长描述混乱刷新. 症状: >>> Submit Standard Op ...

  3. hdu4722Good Numbers(dp)

    链接 这题规律其实挺明显的 打表找规律估计都可以 正规点就是DP 算出第N位所包含的good number的数量 如果给出的数是N+1位 就枚举各位上比原来小的数 加上下一位的dp值 一个i写成g了 ...

  4. 获取Mac、CPUID、硬盘序列号、本地IP地址、外网IP地址OCX控件

    提供获取Mac.CPUID.硬盘序列号.本地IP地址.外网IP地址OCX控件 开发语言:vc++ 可应用与WEB程序开发应用 <HTML><HEAD><TITLE> ...

  5. C# 获取word批注信息

    今天在Silverlight 应用程序中实现了 获取word文档批注信息 的功能. 在wcf服务继承接口类中编写的函数如下 /// <summary> /// 获取word批注信息 /// ...

  6. 从零开始学习jQuery (十一) 实战表单验证与自动完成提示插件

    一.摘要 本系列文章将带您进入jQuery的精彩世界, 其中有很多作者具体的使用经验和解决方案,  即使你会使用jQuery也能在阅读中发现些许秘籍. 本文是介绍两个最常用的jQuery插件. 分别用 ...

  7. CLR via C# 读书笔记 6-2 不同AppDomain之间的通信 z

    跨AppDomain通信有两种方式 1.Marshal By reference : 传递引用 2.Marshal By Value : 把需要传递的对象 通过序列化反序列化的方式传递过去(值拷贝) ...

  8. HDU 3416 Marriage Match IV dij+dinic

    题意:给你n个点,m条边的图(有向图,记住一定是有向图),给定起点和终点,问你从起点到终点有几条不同的最短路 分析:不同的最短路,即一条边也不能相同,然后刚开始我的想法是找到一条删一条,然后光荣TLE ...

  9. centos解决ping unknown host的问题

    当ping www.baidu.com 的时候如果出现 unknown host的提示 再ping一下IP, ping 8.8.8.8 如果此时能ping通,那么就是DNS服务器没有设置,不能解析域名 ...

  10. Esper系列(七)数据缓存、外部事件应用(静态方法)

    LRU Cache 功能:最近最少使用策略. 数据库查询缓存应用配置: 1  ); 12          } 13          return bean; 14      }    15  } ...