首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

在 Java 中高效率使用锁的技巧

2012-12-25 
在 Java 中高效使用锁的技巧简介锁(lock)作为用于保护临界区(critical section)的一种机制,被广泛应用在多

在 Java 中高效使用锁的技巧
简介

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


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

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

    * 如果一个锁对象只能由当前线程访问,那么其他线程无法获得该锁并发生同步 , 因此 JVM 可以去除对这个锁的请求。
    * 逸出分析 (escape analysis) 可以识别本地对象的引用是否在堆中被暴露。如果没有,就可以将本地对象的引用变为线程本地的 (thread local) 。
    * 编译器还可以进行锁的粗化 (lock coarsening) 。把邻近的 synchronized 块用相同的锁合并起来,以减少不必要的锁的获取和释放。

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


降低锁竞争的方法

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

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

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

通常使代码变成线程安全的技术是给整个函数加上一把“大锁”。例如在 Java 中,将整个方法声明为 synchronized 。但是,我们需要保护的仅仅是对象的共享状态,而不是代码。
import java.util.concurrent.atomic.AtomicLongArray; public class AtomicLock implements Runnable{ private final long d[]; private final AtomicLongArray a; private int a_size; public AtomicLock(int size) { a_size = size; d = new long[size]; a = new AtomicLongArray(size); } public synchronized void set1(int idx, long val) { d[idx] = val; } public synchronized long get1(int idx) { long ret = d[idx]; return ret; } public void set2(int idx, long val) { a.addAndGet(idx, val); } public long get2(int idx) { long ret = a.get(idx); return ret; } public void run() { for (int i=0; i<a_size; i++) { //The slower operations //set1(i, i); //get1(i); //The quicker operations set2(i, i); get2(i); } } } set1 和 get1 的结果 Execution Time: 23550 milliseconds set2 和 get2 的结果 Execution Time: 842 milliseconds



使用并发容器

从 Java1.5 开始,java.util.concurrent 包提供了高效地线程安全的并发容器。并发容器自身保证线程安全性,同时为常用操作在大量线程访问的情况下做了优化。这些容器适合在多核平台上运行的多线程应用中使用,具有高性能和高可扩展性。 Amino 项目提供的更多的高效的并发容器和算法。



使用 Immutable 数据和 Thread Local 的数据

Immutable 数据在其生命周期中始终保持不变,所以可以安全地在每个线程中复制一份以便快速读取。

ThreadLocal 的数据,只被线程本身锁使用,因此不存在不同线程之间的共享数据的问题。 ThrealLocal 可以用来改善许多现有的共享数据。例如所有线程共享的对象池、等待队列等,可以变成每个 Thread 独享的对象池和等待队列。采用 Work-stealing scheduler 代替传统的 FIFO-Queue scheduler 也是使用 Thread Local 数据的例子。


结论

锁是开发多线程应用不可或缺的工具。随着在多核平台成为主流的今天,正确使用锁将成为开发人员的一项基本技能。尽管无锁编程和 Transactional Memory 已经出现在软件开发人员的视野中,在可见的将来,使用锁的编程方式仍然是最为重要的并行编程技能。我们希望本文中提出的方法能帮助大家正确的使用锁这个工具。

热点排行