`

在 Java 中高效使用锁的技巧

    博客分类:
  • JAVA
 
阅读更多
在 Java 中高效使用锁的技巧(一)
锁(lock)作为用于保护临界区(critical section)的一种机制,被广泛应用在多线程程序中。无论是 Java 语言中的 synchronized 关键字,还是 java.util.concurrent 包中的 ReentrantLock,都是多线程应用开发人员手中强有力的工具。但是强大的工具通常是把双刃剑,过多或不正确的使用锁,会导致多线程应用的性能下降。这种问题在多核平台成为主流的今天越发明显。

  竞争锁是造成多线程应用程序性能瓶颈的主要原因

  区分竞争锁和非竞争锁对性能的影响非常重要。如果一个锁自始至终只被一个线程使用,那么 JVM 有能力优化它带来的绝大部分损耗。如果一个锁被多个线程使用过,但是在任意时刻,都只有一个线程尝试获取锁,那么它的开销要大一些。我们将以上两种锁称为非竞争锁。而对性能影响最严重的情况出现在多个线程同时尝试获取锁时。这种情况是 JVM 无法优化的,而且通常会发生从用户态到内核态的切换。现代 JVM 已对非竞争锁做了很多优化,使它几乎不会对性能造成影响。常见的优化有以下几种。

  * 如果一个锁对象只能由当前线程访问,那么其他线程无法获得该锁并发生同步 , 因此 JVM 可以去除对这个锁的请求。

  * 逸出分析 (escape analysis) 可以识别本地对象的引用是否在堆中被暴露。如果没有,就可以将本地对象的引用变为线程本地的 (thread local) 。

  * 编译器还可以进行锁的粗化 (lock coarsening) 。把邻近的 synchronized 块用相同的锁合并起来,以减少不必要的锁的获取和释放。

  因此,不要过分担心非竞争锁带来的开销,要关注那些真正发生了锁竞争的临界区中性能的优化。

  降低锁竞争的方法

  很多开发人员因为担心同步带来的性能损失,而尽量减少锁的使用,甚至对某些看似发生错误概率极低的临界区不使用锁保护。这样做往往不会带来性能提高,还会引入难以调试的错误。因为这些错误通常发生的概率极低,而且难以重现。

  因此,在保证程序正确性的前提下,解决同步带来的性能损失的第一步不是去除锁,而是降低锁的竞争。通常,有以下三类方法可以降低锁的竞争:减少持有锁的时间,降低请求锁的频率,或者用其他协调机制取代独占锁。这三类方法中包含许多最佳实践,在下文中将一一介绍。

  避免在临界区中进行耗时计算

  通常使代码变成线程安全的技术是给整个函数加上一把“大锁”。例如在 Java 中,将整个方法声明为 synchronized 。但是,我们需要保护的仅仅是对象的共享状态,而不是代码。

  过长时间的持有锁会限制应用程序的可扩展性。 Brian Goetz 在《 Java Concurrency in Practice 》一书中提到,如果一个操作持有锁的时间超过 2 毫秒,并且每一个操作都需要这个锁,那么无论有多少个空闲处理器,应用程序的吞吐量都不会超过每秒 500 个操作。如果能够减少持有这个锁的时间到 1 毫秒,就能将这个与锁相关的吞吐量提高到每秒 1000 个操作。事实上,这里保守地估计了过长时间持有锁的开销,因为它并没有计算锁的竞争带来的开销。例如,因为获取锁失败带来的忙等和线程切换,都会浪费 CPU 时间。减小锁竞争发生可能性的最有效方式是尽可能缩短持有锁的时间。这可以通过把不需要用锁保护的代码移出同步块来实现, 尤其是那些花费“昂贵”的操作,以及那些潜在的阻塞操作,比如 I/O 操作。
在例 1 中,我们使用 JLM(Java Lock Monitor) 查看 Java 中锁使用的情况。 foo1 使用 synchronized 保护整个函数,foo2 仅保护变量 maph 。 AVER_HTM 显示了每个锁的持有时间。可以看到将无关语句移出同步块后,锁的持有时间降低了,并且程序执行时间也缩短了。

  例 1. 避免在临界区中进行耗时计算


    import java.util.Map;
  import java.util.HashMap;
  public class TimeConsumingLock implements Runnable {
  private final Map maph = new HashMap();
  private int opNum;
  public TimeConsumingLock(int on)
  {
  opNum = on;
  }
  public synchronized void foo1(int k)
  {
  String key = Integer.toString(k);
  String value = key+"value";
  if (null == key)
  {
  return ;
  }else {
  maph.put(key, value);
  }
  }
  public void foo2(int k)
  {
  String key = Integer.toString(k);
  String value = key+"value";
  if (null == key)
  {
  return ;
  }else {
  synchronized(this){
  maph.put(key, value);
  }
  }
  }
  public void run()
  {
  for (int i=0; i 
  {
  //foo1(i); //Time consuming
  foo2(i); //This will be better
  }
  }
  }


  results from JLM report

  使用 foo1 的结果

  MON-NAME [08121048] TimeConsumingLock@D7968DB8 (Object)

  %MISS GETS NONREC SLOW REC TIER2 TIER3 %UTIL AVER_HTM

  0 5318465 5318465 35 0 349190349 8419428 38 5032

  Execution Time: 16106 milliseconds

  使用 foo2 的结果

  MON-NAME [D594C53C] TimeConsumingLock@D6DD67B0 (Object)

  %MISS GETS NONREC SLOW REC TIER2 TIER3 %UTIL AVER_HTM

  0 5635938 5635938 71 0 373087821 8968423 27 3322

  Execution Time: 12157 milliseconds
分拆锁和分离锁

  降低锁竞争的另一种方法是降低线程请求锁的频率。分拆锁 (lock splitting) 和分离锁 (lock striping) 是达到此目的两种方式。相互独立的状态变量,应该使用独立的锁进行保护。有时开发人员会错误地使用一个锁保护所有的状态变量。这些技术减小了锁的粒度,实现了更好的可伸缩性。但是,这些锁需要仔细地分配,以降低发生死锁的危险。

  如果一个锁守护多个相互独立的状态变量,你可能能够通过分拆锁,使每一个锁守护不同的变量,从而改进可伸缩性。通过这样的改变,使每一个锁被请求的频率都变小了。分拆锁对于中等竞争强度的锁,能够有效地把它们大部分转化为非竞争的锁,使性能和可伸缩性都得到提高。

  在例 2 中,我们将原先用于保护两个独立的对象变量的锁分拆成为单独保护每个对象变量的两个锁。在 JLM 结果中,可以看到原先的一个锁 SplittingLock@D6DD3078 变成了两个锁 java/util/HashSet@D6DD7BE0 和 java/util/HashSet@D6DD7BE0 。并且申请锁的次数 (GETS) 和锁的竞争程度 (SLOW, TIER2, TIER3) 都大大降低了。最后,程序的执行时间由 12981 毫秒下降到 4797 毫秒。

  当一个锁竞争激烈时,将其分拆成两个,很可能得到两个竞争激烈的锁。尽管这可以使两个线程并发执行,从而对可伸缩性有一些小的改进。但仍然不能大幅地提高多个处理器在同一个系统中的并发性。

  分拆锁有时候可以被扩展,分成若干加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁。例如,ConcurrentHashMap 的实现使用了一个包含 16 个锁的数组,每一个锁都守护 HashMap 的 1/16 。假设 Hash 值均匀分布,这将会把对于锁的请求减少到约为原来的 1/16 。这项技术使得 ConcurrentHashMap 能够支持 16 个的并发 Writer 。当多处理器系统的大负荷访问需要更好的并发性时,锁的数量还可以增加。

  在例 3 中,我们模拟了 ConcurrentHashMap 中使用分离锁的情况。使用 4 个锁保护数组的不同部分。在 JLM 结果中,可以看到原先的一个锁 StrippingLock@D79962D8 变成了四个锁 java/lang/Object@D79964B8 等。并且锁的竞争程度 (TIER2, TIER3) 都大大降低了。最后,程序的执行时间由 5536 毫秒下降到 1857 毫秒。

  例 2. 分拆锁


    import java.util.HashSet;
  import java.util.Set;
  public class SplittingLock implements Runnable{
  private final Set users = new HashSet();
  private final Set queries = new HashSet();
  private int opNum;
  public SplittingLock(int on) {
  opNum = on;
  }
  public synchronized void addUser1(String u) {
  users.add(u);
  }
  public synchronized void addQuery1(String q) {
  queries.add(q);
  }
  public void addUser2(String u) {
  synchronized(users){
  users.add(u);
  }
  }
  public void addQuery2(String q) {
  synchronized(queries){
  queries.add(q);
  }
  }
  public void run() {
  for (int i=0; i 
  String user = new String("user");
  user+=i;
  addUser1(user);
  String query = new String("query");
  query+=i;
  addQuery1(query);
  }
  }
  }


  results from JLM report

  使用 addUser1 和 addQuery1 的结果

  MON-NAME [D5848CB0] SplittingLock@D6DD3078 (Object)

  %MISS GETS NONREC SLOW REC TIER2 TIER3 %UTIL AVER_HTM

  0 9004711 9004711 101 0 482982391 10996987 44 3393

  Execution Time: 12981 milliseconds

  使用 addUser2 和 addQuery2 的结果

  MON-NAME [D5928C98] java/util/HashSet@D6DD7BE0 (Object)

  %MISS GETS NONREC SLOW REC TIER2 TIER3 %UTIL AVER_HTM

  0 1875510 1875510 38 0 108706364 2546875 14 5173

  MON-NAME [D5928C98] java/util/HashSet@D6DD7BE0 (Object)

  %MISS GETS NONREC SLOW REC TIER2 TIER3 %UTIL AVER_HTM

  0 272365 272365 0 0 15154239 352397 1 3042

  Execution Time: 4797 milliseconds

分享到:
评论

相关推荐

    提升Java的锁性能Java开发Java经验技巧共5页.p

    在Java开发过程中,锁性能是决定程序并发效率的关键因素之一。高效的锁机制能够确保多线程环境中的数据一致性,减少资源争抢,从而提高应用程序的性能。本篇内容将深入探讨如何提升Java的锁性能,结合Java开发的经验...

    Java高效编程指南

    - 了解类型擦除,避免在泛型方法中使用instanceof或Class.isInstance()。 - 避免使用通配符?的边界类型,除非必要。 4. 方法设计 - 方法应该做一件事,做好一件事,保持函数单一职责。 - 遵循Java命名规范,如...

    支持10000同步锁,Spring Boot,Java

    在Java编程语言中,同步锁(Synchronized)是多线程环境下确保数据一致性的重要机制。在Spring Boot框架中,我们同样可以有效地利用同步锁来处理并发问题。标题和描述提到的"支持10000同步锁"可能是指在特定场景下,...

    在Java里处理文件的技巧Java开发Java经验技巧共5

    在Java编程语言中,处理文件是一项基础且重要的任务。无论是读取、写入、移动、复制还是删除文件,Java都提供了丰富的API来支持这些操作。本篇内容将深入探讨Java中处理文件的一些关键技巧和最佳实践,以帮助开发者...

    《码出高效:Java开发手册》

    通过学习锁、同步块、原子变量等工具的使用,开发者可以写出高效、安全的多线程程序,避免数据竞争和死锁等问题。 此外,书中还涵盖了异常处理的最佳实践,教导开发者如何正确地使用try-catch-finally结构,以及...

    java数据库开发技巧

    在Java数据库开发中,掌握一些高效且实用的技巧至关重要,这不仅能提高开发效率,还能确保应用程序的稳定性和性能。以下是一些关键知识点的详细说明: 1. **JDBC(Java Database Connectivity)**: JDBC是Java中...

    JAVA程序设计技巧1001例

    在《JAVA程序设计技巧1001例》中,我们深入探讨了Java编程的各种实用技巧和最佳实践,这些实例旨在帮助开发者提升技能,优化代码,提高程序效率。本书覆盖了从基础到高级的广泛主题,适合各个层次的Java开发者学习。...

    java多线程经典案例

    这些方法必须在同步环境中使用,否则会抛出异常。此外,Java 5引入了BlockingQueue阻塞队列,它是一种线程安全的数据结构,线程可以等待队列中有数据可取或等待队列有空位可存,常用于生产者-消费者模型。 线程阻塞...

    MySQL与Java锁的学习

    在实际开发中,Java程序员需要根据业务需求和并发场景选择合适的锁策略,并合理设置事务隔离级别,以确保数据一致性并避免死锁。 总结来说,“MySQL与Java锁的学习”涵盖了数据库锁机制和Java中与数据库交互的并发...

    Java程序设计技巧1001例

    通过深入学习和实践这些技巧,开发者不仅能掌握Java编程的核心技能,还能培养解决问题的思维,提升编程效率,从而在实际项目中发挥更大的作用。无论是初学者还是经验丰富的开发者,都能从中受益匪浅。

    java并非编程,合集(各种锁、并发集合、障碍器、线程同步).zip

    本合集深入探讨了这些主题,帮助开发者理解和掌握在Java环境中高效处理并发问题的技巧。 首先,我们要理解“锁”在并发编程中的作用。Java提供了多种锁机制,包括内置锁(也称为监视器锁)和显式锁。内置锁是通过...

    利用redis生成注解实现进程锁

    在Java开发中,进程锁是一种常见的并发控制机制,用于确保多线程或分布式系统中的数据一致性。本篇文章将深入探讨如何利用Redis这一高效、内存型的数据存储系统来生成注解,实现进程锁的功能。 首先,我们需要理解...

    提高Java开发数据库效率的技巧.pdf

    - **代码层面的优化**:在Java代码中,使用单例模式或静态数据管理数据库连接,以及采用合适的数据结构处理查询结果等,都可以提升效率。 4. JDBC最佳实践 - **使用最新的JDBC驱动**:确保使用与数据库兼容的最新...

    JAVA技巧(Java多线程运行时,减少内存占用量).pdf

    - **使用静态方法减少对象创建**:在`OpenidServices`类中使用静态方法`getOpenidAccount`可以减少不必要的对象实例化,因为静态方法不需要创建类的实例。 - **合理设计对象的生命周期**:对于临时需要的大型对象,...

    Java_并发核心编程-中文翻译_英文原版开源项目JNA-中文翻译版

    最后,我们需要关注Java并发的性能优化技巧,如避免过多的线程上下文切换,使用并发集合类(如ConcurrentHashMap、CopyOnWriteArrayList等)来减少同步开销,以及使用Lock接口提供的更细粒度的锁控制来提高并发性能...

    《java 并发编程实战高清PDF版》

    这本书旨在帮助开发者理解和掌握在Java环境中创建高效、可扩展且可靠的多线程应用程序的关键技术和实践。它涵盖了从基本概念到高级主题的广泛内容,是Java开发者的必备参考资料。 在Java并发编程中,多线程是核心...

    java并发实战中文文档

    通过阅读《Java并发实战》,开发者不仅可以掌握Java并发编程的基础,还能学习到高级技巧和最佳实践,从而在实际项目中更好地利用并发提高程序性能。这个高清且带有目录的文档,无疑将为学习和工作带来极大的便利。

    Java高手关于java的文章合集

    在这个“Java高手关于Java的文章合集”中,我们可以期待深入探讨Java和J2EE的相关技术、最佳实践以及实用技巧。 1. **Java基础知识**:文章可能涵盖Java语法基础,包括变量、数据类型、控制流、类与对象、继承、...

    Java性能优化技巧集锦

    这些技巧旨在帮助开发者写出更高效、更稳定的Java程序。通过深入理解和实践,我们可以提升代码的运行效率,减少系统资源消耗,为用户提供更好的体验。"Java性能优化技巧集锦"的PDF文档应该包含了这些知识点的详细...

Global site tag (gtag.js) - Google Analytics