如何优化性能:java
由于锁是串行的这会引发大量的阻塞:因此咱们在使用锁的时候要尽可能的作到如下几点:算法
使用分离锁能够增长并发访问容器的量.这可使容器并发的get等相同的操做:编程
竞争锁是形成多线程应用程序性能瓶颈的主要缘由:数组
区分竞争锁和非竞争锁对性能的影响很是重要。若是一个锁自始至终只被一个线程使用,那么 JVM 有能力优化它带来的绝大部分损耗。若是一个锁被多个线程使用过,可是在任意时刻,都只有一个线程尝试获取锁,那么它的开销要大一些。咱们将以上两种锁称为非竞争锁。而对性能影响最严重的状况出如今多个线程同时尝试获取锁时。这种状况是 JVM 没法优化的,并且一般会发生从用户态到内核态的切换。现代 JVM 已对非竞争锁作了不少优化,使它几乎不会对性能形成影响。常见的优化有如下几种。缓存
所以,不要过度担忧非竞争锁带来的开销,要关注那些真正发生了锁竞争的临界区中性能的优化。安全
下降锁竞争的方法:数据结构
不少开发人员由于担忧同步带来的性能损失,而尽可能减小锁的使用,甚至对某些看似发生错误几率极低的临界区不使用锁保护。这样作每每不会带来性能提升,还会引入难以调试的错误。由于这些错误一般发生的几率极低,并且难以重现。多线程
所以,在保证程序正确性的前提下,解决同步带来的性能损失的第一步不是去除锁,而是下降锁的竞争。一般,有如下三类方法能够下降锁的竞争:减小持有锁的时间,下降请求锁的频率,或者用其余协调机制取代独占锁。这三类方法中包含许多最佳实践,在下文中将一一介绍。并发
避免在临界区中进行耗时计算:app
一般使代码变成线程安全的技术是给整个函数加上一把“大锁”。例如在 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. 避免在临界区中进行耗时计算
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 } } }
使用 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. 分拆锁
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); } } }
使用 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)); } } }
使用 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. 使用原子操做的数组
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 数据的例子。
公平锁和非公平锁
jdk1.5并发包中ReentrantLock的建立能够指定构造函数的boolean类型来获得公平锁或非公平锁,关于二者区别,java并发编程实践里面有解释
公平锁,就是很公平,在并发环境中,每一个线程在获取锁时会先查看此锁维护的等待队列,若是为空,或者当前线程线程是等待队列的第一个,就占有锁,不然就会加入到等待队列中,之后会按照FIFO的规则从队列中取到本身。
非公平锁比较粗鲁,上来就直接尝试占有锁,若是尝试失败,就再采用相似公平锁那种方式。
//公平获取锁 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //状态为0,说明当前没有线程占有锁 if (c == 0) { //若是当前线程是等待队列的第一个或者等待队列为空,则经过cas指令设置state为1,当前线程得到锁 if (isFirst(current) && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //若是当前线程自己就持有锁,那么叠加状态值,持续得到锁 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded"); } setState(nextc); return true; } //以上条件都不知足,那么线程进入等待队列。 return false; }
//非公平获取锁 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //若是当前没有线程占有锁,当前线程直接经过cas指令占有锁,管他等待队列,就算本身排在队尾也是这样 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow { throw new Error("Maximum lock count exceeded"); } setState(nextc); return true; } return false; }