提升Java的锁性能

标签: 提升 java 性能 | 发表时间:2015-01-23 20:01 | 作者:
出处:http://it.deepinmind.com

几个月前我们介绍了如何通过 Plumbr进行线程锁检测,随后便收到了很多类似的问题,“Hi,文章写得不错,现在我终于知道是什么引发的性能问题了,但是现在我该怎么做?”

为了在我们的产品中集成这个解决方案,我们付出了许多努力,不过在本文中,我想给大家分享几个常用的优化技巧,而不一定非要使用我们这款锁检测的工具。包括分拆锁,并发数据结构,保护数据而非代码,以及缩小锁的作用域。

锁无罪,竞争其罪

如果你在多线程代码中碰到了性能问题,你肯定会先抱怨锁。毕竟,从“常识”来讲,锁的性能是很差的,并且还限制了程序的可伸缩性。如果你怀揣着这样的想法去优化代码并删除锁的话,最后你肯定会引入一些难缠的并发BUG。

因此分清楚竞争锁与无竞争锁的区别是很有必要的。如果一个线程尝试进入另一个线程正在执行的同步块或者方法时,便会出现锁竞争。第二个线程就必须等待前一个线程执行完这个同步块并释放掉监视器(monitor)。如果只有一个线程在执行这段同步的代码,这个锁就是无竞争的。

事实上,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个。

不过这个上锁的方案更适合加到牌桌上,而不是玩家进入的时候——即便是在一个流量一般的扑克网站上,这样的系统也肯定会由于线程等待锁释放而频繁地触发竞争事件。被锁住的代码块包含了帐户余额以及牌桌上限的检查,这里面很可能会包括一些很昂贵的操作,这样不仅会容易触发竞争并且使得竞争的时间变长。

解决问题的第一步就是要确保你保护的是数据,而不是代码,先将同步从方法声明移到方法体里。在上面这个简短的例子中,刚开始好像能修改的地方并不多。不过我们考虑的是整个GameServer类,而不只限于这个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的this实例上,并与同时想离开牌桌(leave)的玩家产生竞争行为。而将锁从方法签名移到方法内部以后,则将上锁的时机往后推迟了,一定程度上减小了竞争的可能性。

缩小锁的作用域

现在我们已经确保保护的是数据而不是代码了,我们得再确认锁住的部分都是必要的——比如说,代码可以重写成这样 :

   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
}

现在检查玩家余额的这个耗时操作就在锁作用域外边了。注意到了吧,锁的引入其实只是为了保护玩家数量不超过桌子的容量而已,检查帐户余额这个事情并不在要保护的范围之内。

分拆锁

再看下上面这段代码,你会注意到整个数据结构都被同一个锁保护起来了。考虑到这个数据结构中可能会存有上千张牌桌,出现竞争的概率还是非常高的,因此保护每张牌桌不超出容量的工作最好能分别来进行。

对于这个例子而言,为每张桌子分配一个独立的锁并非难事,代码如下:

   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()方法的同步操作变得更简单了,因为我们现在不用再对tables进行加锁了,这都多亏了ConcurrentHashMap。然而,我们还是要保证每个tablePlayers的一致性。因此这个地方ConcurrentHashMap帮不上什么忙。同时我们还得在createTable()与destroyTable()方法中创建新的桌子以及销毁桌子,这对ConcurrentHashMap而言本身就是并发的,因此你可以并行地增加或者减少桌子的数量。

其它的技巧及方法

  • 降低锁的可见性。在上述例子中,锁是声明为public的,因此可以被别人所访问到,你所精心设计的监视器可能会被别人锁住,从而功亏一篑。
  • 看一下 java.util.concurrent.locks包下面有哪些锁策略对你是有帮助的。
  • 使用原子操作。上面这个例子中的简单的计数器其实并不需要进行加锁。将计数的Integer换成 AtomicInteger对这个场景来说就绰绰有余了。

希望本文对你解决锁竞争的问题能有所帮助,不管你有没有使用我们的Plumbr所提供的 自动化锁检测方案,还是自己手动从线程dump信息中提取信息。

原创文章转载请注明出处: 提升Java的锁性能

英文原文链接

相关 [提升 java 性能] 推荐:

提升Java的锁性能

- - Java译站
几个月前我们介绍了如何通过 Plumbr来 进行线程锁检测,随后便收到了很多类似的问题,“Hi,文章写得不错,现在我终于知道是什么引发的性能问题了,但是现在我该怎么做. 为了在我们的产品中集成这个解决方案,我们付出了许多努力,不过在本文中,我想给大家分享几个常用的优化技巧,而不一定非要使用我们这款锁检测的工具.

Java 8:HashMap的性能提升

- - Java译站
HashMap是一个高效通用的数据结构,它在每一个Java程序中都随处可见. 你可能也知道,HashMap使用key的hashCode()和equals()方法来将值划分到不同的桶里. 桶的数量通常要比map中的记录的数量要稍大,这样每个桶包括的值会比较少(最好是一个). 当通过key进行查找时,我们可以在常数时间内迅速定位到某个桶(使用hashCode()对桶的数量进行取模)以及要找的对象.

关于 Java Web 项目性能提升的一些思路

- - BlogJava-qileilove
  使用 Nginx 作为前端接入.   用 Nginx 进行动静分离. 这个不用多讲,新浪、网易、淘宝、. 腾讯等巨头的使用已经说明了一切.   遵守 KISS 原则(Keep it simple and stupid). 过多的考虑项目外的重用,必然会增加项目的复杂度. 避免过度集成,让每个模块只做自己的事,这对于日后的维护和模块复用都有好处.

Java Jdbc减少与Oracle之间交互提升批量处理性能,到底该如何优化才好?

- - CSDN博客推荐文章
不拾掇Java有好几年了(N>3吧),之所以写这篇文章其实是纯粹是为了给开发人员一些好的使用jdbc真正去减少交互和提升批量处理batch update性能的例子;  如果你是DBA,那么工作之余你可以把这篇文章推荐给开发看一下, 也许这些例子他已经知道了, 倘若他不知道,那么也算一种福利了. 能考虑到在应用程序client和 数据库服务器DB server间减少交互时间,批量更新处理的绝对是有助于重构和优化代码的好同志;  但这种优化一定要注意方法,如果是自行去重新发明一种轮子的话, 效果往往是不如人意的.

Java 性能调优

- - 编程语言 - ITeye博客
1.用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用. 但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法. clone()方法不会调用任何类构造函数. 在使用设计模式(Design Pattern)的场合,如果用Factory模式创建对象,则改用clone()方法创建新的对象实例非常简单.

[原]Java性能调优

- - 傲慢的上校的专栏
写Java也有n年了,现在还是有不少的坏的代码习惯,也通过学习别人的代码学到了不少好的习惯. 留给自己做个警戒,提示以后写代码的时候注意. 在文章的后面,会提供整理的原材料下载. 1、尽量少用new生成新对象.         用new创建类的实例时,构造雨数链中所有构造函数都会被自动调用,操作速度较慢.

Java性能调优续

- - 编程语言 - ITeye博客
       下面列出一些常见的关于字符串优化的策略,简单的我就不多作解释了. 1) 使用规则表达式处理字符串匹配代替复杂的字符串查找和复制操作;. 2) 使用不拷贝字符串中字符的高效方法,例如String.subString()方法;. 3) 尽可能不要使用需要拷贝字符串中字符的低效方法,例如String.toUpperCase()和String.toLowercase();.

java 8 JVM性能优化

- - Java - 编程语言 - ITeye博客
转自:http://qindongliang.iteye.com/blog/2199633. jvm java 垃圾回收 . JVM是JAVA世界的核心,了解它有助于我们更好调试,调优和开发程序,最近散仙在看JAVA特种兵一书,看完觉得,作者写的内容还是挺不错,大家感兴趣的,也可以购买本温故而知新下.

Java 代码性能优化

- - IT瘾-geek
代码 优化,一个很重要的课题. 可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢. 这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小虾米有用吗. 没用,但是,吃的小虾米一多之后,鲸鱼就被喂饱了. 代码优化也是一样,如果项目着眼于尽快无BUG上线,那么此时可以抓大放小,代码的细节可以不精打细磨;但是如果有足够的时间开发、维护代码,这时候就必须考虑每个可以优化的细节了,一个一个细小的优化点累积起来,对于代码的运行效率绝对是有提升的.

Java 性能调优指南 – 高性能Java

- - 码蜂笔记
本文主要基于 Java performance tuning tips or everything you want to know about Java performance in 15 minutes的翻译. 这篇指南主要调优 java 代码而不是 JVM 设置. Java 1.7.0_06 String 内部表示的改变.