- 浏览: 341541 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (198)
- HIBERNATE (8)
- JAVA (13)
- 数据库 (24)
- SPRING (1)
- LINUX (0)
- 需求管理 (3)
- 职业提升 (6)
- 团队建设 (1)
- 日常用到语句 (1)
- FLEX (6)
- 用户体验 (3)
- 设计模式 (6)
- weblogic (2)
- PowerDesigner (3)
- HTML (7)
- ANT (7)
- 工具. (1)
- bat (5)
- 存储过程 (1)
- strus2 (1)
- DWR (2)
- jfreechart (4)
- 上线测试优化 (17)
- JVM (9)
- 工具使用 (2)
- 算法 (3)
- 私事 (0)
- 数据库-Oracle session (1)
- 软件开发 (5)
- 产品 (2)
- 项目管理 (4)
- oracle语句 (1)
- IntelliJ IDEA (4)
- GRAILS (10)
- Groovy (1)
- JS (1)
- DUBBO (1)
- JAVA EXCEL (3)
- netty websocket (1)
- kafka (1)
- 秘钥体系 (2)
- golang (6)
- gradle (1)
- spring cloud (0)
最新评论
-
wujt:
...
Grails_数据库逆向工程插件 db-reverse-engineer -
yy8093:
我也遇到这个问题,不过并不觉得是个好的方法。。。。不过最后也确 ...
关于dubbo服务产生异常之:Caused by: com.alibaba.dubbo.remoting.TimeoutException: Waiting s -
wujt:
# ----- Execute The Requested C ...
jconsole基础配置(原创) -
gaowei52306:
你好,请问remotedir="/home/dmwe ...
Ant FTP -
抢街饭:
ant生成日志 在命令行也能看见 怎么去做啊
ant生成日志
如何聪明地使用锁--java
锁(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 已经出现在软件开发人员的视野中,在可见的将来,使用锁的编程方式仍然是最为重要的并行编程技能。我们希望本文中提出的方法能帮助大家正确的使用锁这个工具。
锁(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 已经出现在软件开发人员的视野中,在可见的将来,使用锁的编程方式仍然是最为重要的并行编程技能。我们希望本文中提出的方法能帮助大家正确的使用锁这个工具。
发表评论
-
HashMap的实现原理 .
2014-10-31 13:11 619HashMap的实现原理 . 1. ... -
不健壮代码的特征及解决办法
2013-01-27 11:33 733不健壮代码的特征及解决办法 1、尽早释放无用对象的引用。好 ... -
Java线程:新特征-锁
2012-12-26 11:29 976Java线程:新特征-锁 在Java5中,专门提供了锁对象,利 ... -
在 Java 中高效使用锁的技巧
2012-12-26 11:26 1072在 Java 中高效使用锁的技巧(一) 锁(lock)作为用于 ... -
利用读写锁写一个缓存系统
2012-12-26 11:24 937利用读写锁写一个缓存系统 01.import java.u ... -
myEclipse 导入注释模板
2011-12-14 11:25 3664myEclipse 导入注释模板 ... -
javadoc文档标签
2011-12-14 10:14 4545.javadoc可以根据项目代 ... -
转载:Java程序的编码规范
2011-12-01 13:04 768Java程序的编码规范 所有的程序开 ... -
转载:整理出来的一份java编码规则
2011-12-01 13:00 826转载: ... -
通过SPRING配置多个实现类,循环执行中的方法
2011-11-29 03:17 1814自动处理后续代码方法 在接口 定义完成方法 pub ... -
<session-config> 0
2010-08-13 10:36 3484现象: 在访问页面时,网络有点延时的情况下(网络 ... -
JAVA编程小经验
2010-06-05 16:53 974JAVA编程小经 ...
相关推荐
在IT行业中,尤其是在多线程编程领域,"如何聪明地使用锁"是一个至关重要的主题。锁是一种同步机制,用于在并发环境中控制对共享资源的访问,以防止数据不一致性和竞态条件。以下是对这个话题的详细阐述: 1. **锁...
在这篇文章中,我们将探讨Java双重检查加锁单例模式的详解,包括它的优点和缺点,以及如何正确地使用它。 Java双重检查加锁单例模式的优点是延迟初始化和避免竞态条件。延迟初始化可以让程序启动更快,而避免竞态...
Java是一种广泛使用的面向对象的编程语言,其设计目标是具有高度的可移植性、健壮性和安全性。在2021年的Java面试中,开发者需要掌握一系列核心概念和技术,以应对日益激烈的竞争。本压缩包文件"2021 Java面试题.rar...
- **并发编程**:随着多核处理器的普及,Java也提供了强大的并发编程支持,包括线程、同步、锁等机制。 ##### 3. 核心库与框架 - **集合框架**:Java中的集合框架提供了一组标准的数据结构和算法,如List、Set、Map...
《Java开发手册(嵩山版)》是一份深入探讨Java编程规范的重要文档,它为Java开发者提供了详尽的指导原则和最佳实践。这份手册旨在提高代码质量,提升开发效率,确保项目的可维护性和团队协作的一致性。以下是该手册...
我们可以使用java.nio的ByteBuffer来创建堆外内存,并通过调用allocateDirect方法申请内存。同时,我们也可以通过设置-XX:MaxDirectMemorySize=10M控制堆外内存的大小。 结论 本文详细介绍了Java堆外内存的概念,...
- **单元测试**:内置JUnit单元测试工具,能够方便地测试Java代码的功能性和准确性。测试结果会以绿色或红色条形图的形式显示,绿色表示测试成功,红色则表示失败。 #### MyEclipse的增强功能 - **Java EE支持**:...
不要故作聪明地推断出不需要使用同步。 在设计过程中考虑线程安全,或者在文档中明确指出它不是线程安全的。 将同步策略文档化。 Executor 执行策略。在执行策略中定义了任务执行的“What、Where、When、How”等方面...
这包括线程池、锁机制、同步工具类如`java.util.concurrent`包中的工具。 5. **框架应用**:项目可能采用了Spring Boot等Java Web框架,简化了开发流程并提供了依赖注入、AOP(面向切面编程)等功能。熟悉这些框架...
我们已经了解到,使用Dead Code Elimination(DCE),锁合并,内联,循环展开,栈上替换(OSR)和无数其他技巧的JVM基准测试使大多数尝试完全进行基准测试无效-包括我们自己的原始基准。 阅读[即使是聪明的]在JVM上...
解决方案可能包括引入分布式锁机制、使用缓存技术如Redis、以及进行数据库查询优化等。 总的来说,基于Struts+Hibernate的采购管理系统是一个集信息化、自动化于一体的工具,它能够有效提升企业的供应链管理能力,...