Java锁与性能相关知识复习与整理

来源:互联网 发布:图库软件下载 编辑:程序博客网 时间:2024/06/06 00:21

Java锁相关技巧:

===================================================================================================================================

锁无罪,竞争其罪

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

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

事实上,JVM中的同步已经针对这种无竞争的情况进行了优化,对于绝大多数应用而言,无竞争的锁几乎是没有任何额外的开销的。因此,出了性能问题不能光怪锁,你得怪竞争锁。在明确了这点以后 ,我们来看下如何能减少锁的竞争或者竞争的时间。

保护数据而非代码

实现线程安全最快的方法就是直接将整个方法上锁。比如说下面的这个例子,这是在线扑克游戏服务端的一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classGameServer {
  publicMap<<String, List<Player>> tables = newHashMap<String, List<Player>>();
 
  publicsynchronizedvoidjoin(Player player, Table table) {
    if(player.getAccountBalance() > table.getLimit()) {
      List<Player> tablePlayers = tables.get(table.getId());
      if(tablePlayers.size() < 9) {
        tablePlayers.add(player);
      }
    }
  }
  publicsynchronizedvoidleave(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()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
classGameServer {
  publicMap<String, List<Player>> tables = newHashMap<String, List<Player>>();
 
  publicvoidjoin(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);
        }
      }
    }
  }
  publicvoidleave(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)的玩家产生竞争行为。而将锁从方法签名移到方法内部以后,则将上锁的时机往后推迟了,一定程度上减小了竞争的可能性。

缩小锁的作用域

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicclassGameServer {
  publicMap<String, List<Player>> tables = newHashMap<String, List<Player>>();
 
  publicvoidjoin(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
}

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

分拆锁

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicclassGameServer {
  publicMap<String, List<Player>> tables = newHashMap<String, List<Player>>();
 
  publicvoidjoin(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来存储所有的扑克桌,这样代码就会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
publicclassGameServer {
  publicMap<String, List<Player>> tables = newConcurrentHashMap<String, List<Player>>();
 
  publicsynchronizedvoidjoin(Player player, Table table) {/*Method body skipped for brevity*/}
  public synchronized void leave(Player player, Table table) {/*Method body skipped for brevity*/}
 
  publicsynchronizedvoidcreateTable() {
    Table table = newTable();
    tables.put(table.getId(), table);
  }
 
  publicsynchronizedvoiddestroyTable(Table table) {
    tables.remove(table.getId());
  }
}

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

其它的技巧及方法

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

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

=======================================================================================

Java中不同压缩算法的性能比较

  • JDK GZIP ——这是一个压缩比高的慢速算法,压缩后的数据适合长期使用。JDK中的java.util.zip.GZIPInputStream / GZIPOutputStream便是这个算法的实现。
  • JDK deflate ——这是JDK中的又一个算法(zip文件用的就是这一算法)。它与gzip的不同之处在于,你可以指定算法的压缩级别,这样你可以在压缩时间和输出文件大小上进行平衡。可选的级别有0(不压缩),以及1(快速压缩)到9(慢速压缩)。它的实现是java.util.zip.DeflaterOutputStream / InflaterInputStream。
  • LZ4压缩算法的Java实现——这是本文介绍的算法中压缩速度最快的一个,与最快速的deflate相比,它的压缩的结果要略微差一点。如果想搞清楚它的工作原理,我建议你读一下这篇文章。它是基于友好的Apache 2.0许可证发布的。
  • Snappy——这是Google开发的一个非常流行的压缩算法,它旨在提供速度与压缩比都相对较优的压缩算法。我用来测试的是这个实现。它也是遵循Apache 2.0许可证发布的
  • 如果你认为数据压缩非常慢的话,可以考虑下LZ4(快速)实现,它进行文本压缩能达到大约320Mb/秒的速度——这样的压缩速度对大多数应用而言应该都感知不到。
  • 如果你受限于无法使用第三方库或者只希望有一个稍微好一点的压缩方案的话,可以考虑下使用JDK deflate(lvl=1)进行编解码——同样的文件它的压缩速度能达到75Mb/秒。
  • ====================================================================================
  • Java字符串性能优化
Java字符串性能优化

========================================================================================

  • 当转化成字符串的时候,应当避免使用”"串进行转化。使用合适的String.valueOf方法或者包装类的toString(value)方法。
  • 尽量使用StringBuilder进行字符串拼接。检查下老旧码,把那些能替换掉的StringBuffer也替换成它。
  • 使用Java 6 update 20引入的-XX:+OptimizeStringConcat选项来提高字符串拼接的性能。在最近的Java7的版本中已经默认打开了,不过在Java 6_41还是关闭的。

String,StringBuffer,StringBuilder的区别

=======================================================================================

String对象是不可变的,在说原因的时候没说清,其实看看String源码就知道了
在new String的时候,String 中的3个成员变量value,count,offset都是final的,当然String类也是final的,所以一旦初始化后不能修改的。

StringBuffer,与StringBuilder都实现了相同的接口,而且都继承相同的父类,不同的是,StringBuffer的大部分方法都是同步的,所以是线程安全,StringBuilder没有同步,所以通常情况下效率上StringBuilder是优于StringBuffer的。

StringBuffer与StringBuilder随着append会扩大value[]的容量,这里具体做法是使用System类中的arraycopy方法拷贝,这个方法是调用底层本地方法来处理的,类似于直接使用C的指针操作,比较快。

Java死锁的故障排查和解决

==================================================================================================================================

本文将重温这个经典的线程问题,并总结提出了关键的故障排除和解决方法。我也将基于我自己的多线程的故障排除经验的主题来扩展。


Java deadlock: what is it?

 

     一个真正的Java死锁基本上可以被描述为一种情况,两个或多个线程被永久阻塞,彼此等待。这种情况和其他更多常见的“day-to-day”线程问题的模式如锁争用和线程比赛,等待阻塞的IO调用等是不同的。这样的锁顺序死锁情况可以如下表示:

Java_Thread_deadlock_example.png

线程A & 线程B尝试以不同的顺序获得2个锁是致命的。一旦线程达到死锁的状态,它们永远无法恢复,迫使你重启受影响的JVM进程。

     Heinz也描述了另外类型的死锁:资源死锁。这是我迄今经历过的Java EE企业级系统故障排查中最常见的线程问题模式。一个资源死锁本质上是一个场景,一个或多个线程正在等待获取的资源绝不会被提供,如JDBC池枯竭。

 

Lock-ordering deadlocks


     你或许知道我对JVM thread dump analysis相当感兴趣,这是参与Java/Java EE开发或生产支持相当重要的技能。好消息是Java级别的死锁,可以很容易地被大多数JVM线程转储格式识别出(HotSpot,IBM VM...),因为它们包含一个本地死锁检测机制,这实际上通过执行堆栈跟踪将为你展示参与线程真正的Java级别的死锁情况。JVM线程dump可以由你选择的工具所获得,如VisualVM,jstack或基于Unix OS的kill -3 <PID>。当运行lab 1之后找出下面JVM Java级别的死锁检测部分:

HotSpot_Java_level_deadlock_detection2.png

 

现在,这是比较容易的部分......分析工作的核心是要理解为什么这样的参与线程死锁的情况在首位。锁顺序死锁可以从应用程序代码被触发,但除非你是从事高并发编程,罪魁祸首很可能是代码来自第三方API或框架(您正在使用或实际的Java EE容器本身)。

 

现在,让我们来回顾下Heinz提出的锁顺序死锁解决策略:

 

# Deadlock resolution by global ordering (see lab1 solution)

  • Essentially involves the definition of a global ordering for the locks that would always prevent deadlock (please see lab1 solution)

# Deadlock resolution by TryLock (see lab2 solution)

  • Lock the first lock
  • Then try to lock the second lock
  • If you can lock it, you are good to go
  • If you cannot, wait and try again

 

上面的策略可以通过Java Lock & ReantrantLock来实现,这可以令你灵活地设置一个等待超时,以防止在锁定时间过长的情况下导致线程饥饿。

 

01public interface Lock {02 03void lock();04 05void lockInterruptibly() throws InterruptedException;06 07boolean tryLock();08 09boolean tryLock(long timeout, TimeUnit unit)10 11throws InterruptedException;12 13void unlock();14 15Condition newCondition();16 17}

 

如果你看过JBoss AS7 的实现,你会发现它的核心实现层使用了Lock & ReantrantLock,例如:

 

  • 部署服务
  • EJB3 实现
  • 集群和会话管理
  • 内部缓存 & 数据结构 (LRU, ConcurrentReferenceHashMap…)

 

现在按照Heinz的观点,死锁的解决策略#2可以是相当有效的,但也需要合适的对待,如通过finally{}块释放所有持锁,否则你会将死锁情况变成活锁。

 

Resource deadlocks


现在,让我们继续探讨资源死锁情况。我很高兴,Heinz的实验室#3覆盖这个,因为从我的经验,这是目前最常见的“死锁”的情景,你会看到,特别是如果你正在开发和支持大型分布式Java EE的生产系统。

 

概括如下:

 

  • 资源锁是真正的Java级别的死锁
  • JVM的线程转储会神奇地要你这些类型的死锁。这意味着更多的工作,为您分析和理解这个问题为出发点。
  • 当你刚开始学习如何读取线程dump,线程dump分析可能是特别混乱的,因为线程常常会显示为运行状态与阻塞状态的Java级别的死锁。现在,重要的是要记住,线程状态不是这种问题的类型,比如这一重要运行状态!=健康的状态。
  • 该分析方法与Java级别的死锁有很大的不同。你必须采取多线程dump快照,并确定每个快照之间的线程问题/等待模式。您将可以看到线程不动,如等待线程从池中或已获得这样资源的线程中获取资源...
  • 线程dump分析是不是唯一重要的数据点/因素。你需要收集其他线程正在等待的资源、整个中间件或环境健康的统计数据。这些因素的组合可以令你得出解决策略根本原因的结论,可能或可能不涉及代码的改动。

 

我会尽快给你更多的线程dump问题的模式,但首先,请确保您熟悉的JVM线程dump为起点的基本原则。

 

总结


0 0