`

如何聪明地使用锁

阅读更多

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

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

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

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

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





降低锁竞争的方法

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

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

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

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

JLM 报告注释
%MISS : 申请锁失败的百分比
GETS : 申请锁的总次数 =FAST + SLOW + REC
NONREC : 非递归申请锁的总次数
SLOW : 非递归没有获得锁的次数 ( 线程被阻塞 )
FAST: 非递归获得锁的次数 =NOREC - SLOW
REC : 递归获得锁的次数
TIER2 : 在支持 3 层自旋锁的平台上,为获得锁而在内层循环的次数。
TIER3 : 在支持 3 层自旋锁的平台上,为获得锁而在外层循环的次数。
%UTIL : 锁利用时间 = 持有锁的总时间 / 采样时间
AVER-HTM : 平均持有锁时间 = 持有锁的总时间 / 非递归获得锁的总次数

过长时间的持有锁会限制应用程序的可扩展性。 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<String, String> maph = new HashMap<String, String>(); 

    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<opNum; 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<String> users = new HashSet<String>(); 
    private final Set<String> queries = new HashSet<String>(); 
        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<opNum; 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
			


例 3. 分离锁
				
 public class StrippingLock implements Runnable{ 
    private final Object[] locks; 
    private static final int N_LOCKS = 4; 
    private final String [] share ; 
    private int opNum; 
    private int N_ANUM; 

    public StrippingLock(int on, int anum) { 
        opNum = on; 
        N_ANUM = anum; 
        share = new String[N_ANUM]; 
        locks = new Object[N_LOCKS]; 
        for (int i = 0; i<N_LOCKS; i++) 
        locks[i] = new Object(); 
    } 

    public synchronized void put1(int indx, String k) { 
        share[indx] = k;    //acquire the object lock         
    } 

      
    public void put2(int indx, String k) { 
        synchronized (locks[indx%N_LOCKS]) { 
            share[indx] = k;    // acquire the corresponding lock 
        } 
    } 
    
    public void run() 
    { 
        //The expensive put 
        /*for (int i=0; i<opNum; i++) 
        { 
            put1(i%N_ANUM, Integer.toString(i+1)); 
        }*/ 
        //The cheap put 
        for (int i=0; i<opNum; i++) 
        { 
            put2(i%N_ANUM, Integer.toString(i+1)); 
        } 
    }    
 }
 
 results from JLM report 
 
 使用 put1 的结果
 
 MON-NAME [08121228] StrippingLock@D79962D8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM 
     0  4830690  4830690      460        0 229538313  5010789    18     2552  

 Execution Time: 5536 milliseconds 

 使用 put2 的结果
  
 MON-NAME [08121388] java/lang/Object@D79964B8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  4591046  4591046     1517        0 151042525  3016162    13     1925  
     
 MON-NAME [08121330] java/lang/Object@D79964C8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  1717579  1717579      523        0 50596994   958796     5     1901  
     
 MON-NAME [081213E0] java/lang/Object@D79964D8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  1814296  1814296      536        0 58043786  1113454     5     1799  
     
 MON-NAME [08121438] java/lang/Object@D79964E8 (Object) 
 %MISS     GETS   NONREC     SLOW      REC    TIER2    TIER3 %UTIL AVER_HTM  
     0  3126427  3126427      901        0 96627408  1857005     9     1979  
     
 Execution Time: 1857  milliseconds
			

避免热点域

在某些应用中,我们会使用一个共享变量缓存常用的计算结果。每次更新操作都需要修改该共享变量以保证其有效性。例如,队列的 size,counter,链表的头节点引用等。在多线程应用中,该共享变量需要用锁保护起来。这种在单线程应用中常用的优化方法会成为多线程应用中的“热点域 (hot field) ”,从而限制可伸缩性。如果一个队列被设计成为在多线程访问时保持高吞吐量,那么可以考虑在每个入队和出队操作时不更新队列 size 。 ConcurrentHashMap 中为了避免这个问题,在每个分片的数组中维护一个独立的计数器,使用分离的锁保护,而不是维护一个全局计数。

独占锁的替代方法

用于减轻竞争锁带来的性能影响的第三种技术是放弃使用独占锁,而使用更高效的并发方式管理共享状态。例如并发容器,读 - 写锁,不可变对象,以及原子变量。

java.util.concurrent.locks.ReadWriteLock 实现了一个多读者 - 单写者锁:多个读者可以并发访问共享资源,但是写者必须独占获得锁。对于多数操作都为读操作的数据结构,ReadWriteLock 比独占锁提供更好的并发性。

原子变量提供了避免“热点域”更新导致锁竞争的方法,如计数器、序列发生器、或者对链表数据结构头节点引用的更新。

在例 4 中,我们使用原子操作更新数组的每个元素,避免使用独占锁。程序的执行时间由 23550 毫秒下降到 842 毫秒。


例 4. 使用原子操作的数组
				
 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 已经出现在软件开发人员的视野中,在可见的将来,使用锁的编程方式仍然是最为重要的并行编程技能。我们希望本文中提出的方法能帮助大家正确的使用锁这个工具。

分享到:
评论

相关推荐

    (同步备份恢复)SyncBackPro V6.0.12.0 多国语言注册版

    SyncBackSE 是一个档案备份及同步备份程序,它能够与硬盘,...新特性包含,可复制已锁住的档案,更快的备份,聪明的同步,快速的压缩,更容易的档案夾及档案选择,及一个加 强的FTP 引擎,可支援 SSL 及 Z 模式压缩。

    文档备份同步软件 SyncBackPro 9.3.40.0 + x64.rar

    2BrightSparks SyncBackPro是一款备份同步软件,是一个先进的...新特性包含,可复制已锁住的档案,更快的备份,聪明的同步,快速的压缩,更容易的档案夹及档案选择,及一个加强的FTP 引擎,可支援 SSL 及 Z 模式压缩。

    Reveal_1.6.3破解版OSX10.11.6亲测可用

    Reveal_1.6.3破解版,OSX10.11.6上亲测可用,请勿升级新版,否则将无法使用。聪明的人自己会找到密码,实在找不到可以留言哦~~

    神笔马良强制码字软件2.0版-神笔马良软件唯一正版-进小黑屋写文字

    只要您在神笔马良强制码字软件官网下载的尽可放心使用,现在我们在努力地争取各种杀软的认证。尽最大努力,为神笔马良强制码字软件用户避免困扰。 有报毒的情况,请不要随便使用,通过联系方式联系我们,告诉我们是...

    GodMode9:GodMode9 Explorer-用于Nintendo 3DS控制台的完全访问文件浏览器

    没有这样的解锁序列,就不可能覆盖或修改任何重要的东西,也不可能意外地解锁某些东西。 一如既往,要聪明,保持备份,只是为了安全。快速入门指南与GodMode9一起使用的推荐引导程序是 。 使用基于和的标准设置时...

    实用多功能网吧公告RoolM1.2

    5.禁止虚拟桌面(专防“超级防锁专家”,就是那个“一点既破”,百度“一点既破”就知道是啥了!) 6.防修改IE主页(IE主页始终为百度) 7.可限制客人U盘使用容量(限制客户机使用大磁盘拉你辛苦下载的游戏电影什么的), ...

    java汽车租赁源码-JUC:java.util.concurrentJUC多线程及高并发Demo

    不要故作聪明地推断出不需要使用同步。 在设计过程中考虑线程安全,或者在文档中明确指出它不是线程安全的。 将同步策略文档化。 Executor 执行策略。在执行策略中定义了任务执行的“What、Where、When、How”等方面...

    windows系统漏洞加固

    1.4 检查是否按照权限、责任创建、使用用户账号(低危) 1.5 检查是否已正确配置“复位帐户锁定计数器”时间(低危) 1.6 检查是否已正确配置帐户锁定阈值(低危) 1.7 检查是否已删除或禁用高危帐户(低危) 1.8 ...

    HikariCP-benchmark:JDBC连接池的JHM基准

    我们已经了解到,使用Dead Code Elimination(DCE),锁合并,内联,循环展开,栈上替换(OSR)和无数其他技巧的JVM基准测试使大多数尝试完全进行基准测试无效-包括我们自己的原始基准。 阅读[即使是聪明的]在JVM上...

    go-advice:Go的建议和技巧列表

    互斥锁序列化。 接口越大,抽象性越弱。 使零值有用。 interface{}什么也没说。 Gofmt的风格不是每个人的最爱,但gofmt是每个人的最爱。 稍微复制胜于一点依赖。 Syscall必须始终使用构建标记来保护。 Cgo...

    网络安全技术与方案.doc

    保密和完整性通过使用注册过的邮件和文件锁来实现。 一、计算机网络的安全技术 计算机网络安全是指通过采用各种安全技术和管理上的安全措施,确保网络数据的可 用性、完整性和保密性,其目的是确保经过网络传输和...

    网络安全设计方案.doc

    保密和完整性通过使用注册过的邮件和文件锁来 2、设计的安全性 通过对网络系统的风险分析及需要解决的安全问题,我们需要制定合理的安全策略及 安全方案来确保网络系统的机密性、完整性、可用性、可控性与可审查性。...

    网络安全设计方案(2).doc

    保密和完整性通过使用注册过的邮件和文件锁来 2、设计的安全性 通过对网络系统的风险分析及需要解决的安全问题,我们需要制定合理的安全策略及 安全方案来确保网络系统的机密性、完整性、可用性、可控性与可审查性。...

    智能建筑内设备的无线连接及照明解决方案

    智能建筑革命正在进行中,为...所有者可以优先考虑加热或照明系统,作为智能技术的步,然后可以使用其他智能功能扩展系统,例如设备控制器,百叶窗或门锁。所有类型的智能建筑设备似乎都有光明的未来,从简单的灯开关,

    网络安全设计方案(4).doc

    保密和完整性通过使用注册过的邮件和文件锁来 设备选型 传统的组网已经不能满足现在网络应用的变化了,在组网的初期必须考虑到安全和网络 的问题,考虑到这个问题我们就不能不考虑免疫网络的作用以及前景如何。...

    [机器人怎么画]画机器人.docx

    因为现在我们用的是洗衣机,这个机器人可比洗衣机聪明管用多了,所我给它起了一个这么动听的名字。 篇二:未来的机器人 在很多年以后,地球上的人越来越多,人们产生的垃圾也越来越多,许多地方都变成了垃圾场、垃圾...

    LinkedIn Dux-Soup。「Dux-Soup for LinkedIn」-crx插件

    聪明的浏览器插件为LinkedIn简化了生成和业务开发。 关于Dux-Soup:https://www.dux-soup.com Dux-Soup使您可以轻松地在LinkedIn上找到,吸引和吸引潜在客户。 它会自动查看潜在客户简介,认可技能,跟踪活动并代表...

Global site tag (gtag.js) - Google Analytics