提高Java中锁的性能

两个月前,我们介绍完关于Plumbr的“检测线程锁”之后,我们就已经开始收到了与“hey,大牛,现在我已经知道了是什么影响我的性能问题,但是现在我应该怎么做呢?”这样相似的疑问。

 

我们正在致力于在我们的产品中构建一个解决方案说明,但是在这篇文章中我将分享几个常用的技巧供你应用,您可以应用独立的工具用于检测锁。这些技巧包括锁分裂、并发数据结构、代替代码保护数据和减小锁范围。

 

锁不是恶魔,但是锁竞争是。

 

每当你使用线程代码出现性能问题的时候,你都会怪罪于锁身上。毕竟,大家都普遍认为锁是慢的和可扩展性差。那么如果你是这样想法的话,你就会开始优化代码,只要有可能就会去掉锁,最终,严重的并发BUG将会出现。

 

因此,理解竞争锁与非竞争锁之间的区别是非常重要的。当一个线程正在尝试着进入同步块/方发的时候,另一个线程也被驱动进入就会出现锁竞争的情况。这时第二个线程需要强制等待直到第一个线程完全执行完同步块和释放监控对象。当只有一个线程在尝试进入同步块的时候,锁是没有竞争的。

 

事实上,对于无竞争的情况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 */}
}

 

 

 

最初是一个不起眼的改变,现在将影响到整个类的行为。每当玩家加入游戏的时候,先前的同步方法的锁对象是GamerServer实例,当玩家试图离开桌的同时会引发竞争事件。把锁从方法签名移动到方法体内会延迟和减少锁竞争的可能性。

 

减小锁范围

 

现在,确保我们的数据被保护之后,我们应该确保我们的锁保护的代码是必要的--例如当上面的代码被像下面这样重写之后

 

 

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对于这一问题没有帮助。但是我们可以用createTable()和destroyTable()方法创建和销毁桌子,这些行为对于ConcurrentHashMap来说是并发的,允许增加或减少桌子的数量都是并行的。

 

 

其它的提示和技巧

 

  • 减少锁的可见性。在上面的例子中,锁的声明是public的,因此对所有类都是可见的,因此可能会有一个别的类将通过拿起你的监听对象上锁来破坏你的运转。
  • 查看一下java.util.concurrent.locks 来看看是否有一个锁定战略的实施将改善你的解决方案。
  • 用原子性操作。上面那个例子中简单计数器的递增并不需要一个锁。在计数追踪中用AtomicInteger来替代Integer在这个例子中刚好合适。

 

 

 

无论你是使用Plumbr自动检查锁解决方案或者手动从线程转储过程中提取信息,希望这篇文章可以帮助你解决锁争用问题。

 

原文链接:http://www.javacodegeeks.com/2015/01/improving-lock-performance-in-java.html

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值