论坛首页 Java企业应用论坛

构建高性能服务(二)减小锁粒度 提高Java并发吞吐实例

浏览 7613 次
精华帖 (1) :: 良好帖 (1) :: 新手帖 (1) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-06-18  

提高系统并发吞吐能力是构建高性能服务的重点和难点。通常review代码时看到synchronized是我都会想一想,这个地方可不可以优化。使用synchronized使得并发的线程变成顺序执行,对系统并发吞吐能力有极大影响,我的博文 http://maoyidao.iteye.com/blog/1149015 介绍了可以从理论上估算系统并发处理能力的方法。

 

那么对于必须使用synchronized的业务场景,这里提供几个小技巧,帮助大家减小锁粒度,提高系统并发能力。

 

初级技巧 - 乐观锁

乐观锁适合这样的场景:读不会冲突,写会冲突。同时读的频率远大于写。

 

以下面的代码为例,悲观锁的实现:

public Object get(Object key) {
   synchronized(map) {
      if(map.get(key) == null) {
         // set some values
      }

       return map.get(key);
   }
}
 

 乐观锁的实现:

public Object get(Object key) {
   Object val = null;
   if((val = map.get(key) == null) {
       // 当map取值为null时再加锁判断
       synchronized(map) {
           if(val = map.get(key) == null) {
               // set some value to map...
           }
        }
   }

    return map.get(key);
}

 

中级技巧 - String.intern()

乐观锁不能很好解决大量写冲突问题,但是如果很多场景下,锁实际上不是针对某个用户或者某个订单。比如一个用户必须先创建session,才能进行后面的操作。但是由于网络原因,创建用户session的请求和后续请求几乎同时达到,而并行线程可能会先处理后续请求。一般情况,需要对用户sessionMap加锁,比如上面的乐观锁。在这种场景下,使用String.inter()是一种更高效的办法。类 String 维护一个字符串池。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。可见,当String相同时,String.intern()总是返回同一个对象,因此就实现了对同一用户加锁。由于锁的粒度局限于具体用户,使系统获得了最大程度的并发。

 

public void doSomeThing(String uid) {
   synchronized(uid.intern()) {
       // ...
   }
}

 高级技巧 - 类ConcurrentHashMap

String.inter()的缺陷是类 String 维护一个字符串池是放在JVM perm区的,如果用户数特别多,导致放入字符串池的String不可控,有可能导致OOM错误或者过多的Full GC。怎么样能控制锁的个数,同时减小粒度锁呢?Java ConcurrentHashMap提供了一种很好的借鉴方式,将需要加锁的对象分为多个bucket,每个bucket加一个锁,伪代码如下:

Map locks = new Map();
List lockKeys = new List();
for(int number : 1 - 10000) {
   Object lockKey = new Object();
   lockKeys.add(lockKey);
	locks.put(lockKey, new Object());
}

public void doSomeThing(String uid) {
   Object lockKey = lockKeys.get(uid.hash() % lockKeys.size());
   Object lock = locks.get(lockKey);
   
   synchronized(lock) {
      // do something
   }
}
 

 

 

 

   发表时间:2012-06-19  
有一些小技巧。但是不经常使用
0 请登录后投票
   发表时间:2012-06-19   最后修改:2012-06-19
我的理解和楼主有比较大的误差

1:并行计算的吞吐辆并不是由锁影响的,锁只是其中一个占有30%比例的因素,锁影响的是性能,而非吞吐。

并行计算的吞吐量是受到唯一个因素制约的: 加速比。

加速比又受到

锁(锁因子,锁力度,锁策略,锁模式,锁数量),
并行调度方案(负载均衡),
CPU核数,
CPU cache命中率来影响的。

其中锁虽然是影响并行执行效率的一个编程因素,但是在并行计算中不是所有的并行操作都会遇见锁,这与CPU的cache命中率有关,所以对于一般的业务锁的需求,并不是盲目的加锁,有时候虽然他们看上去会有并发问题,但是实际上不会命中cache也就完全不必要加锁,并且锁的数量只要符合CPU核数和线程数的公式(有兴趣可以去intel的官方网站查询)就完全产生不了负作用,而如果锁的数量超过了公式,那么是设计问题,而不是编程问题。

所以就涉及不到各种锁的模式了比如随机锁,集中锁,分布锁等,这主要因为锁的策略选择空间不大

而并行计算的吞吐瓶颈就在于复杂平衡和cache的命中。复载平衡可以看成是一个设计问题,但是也可以通过调度算法来转化为了技术问题,


2:对于锁来说,乐观锁与悲观锁是对于并发问题的类型区分的,对于互斥类型的,大部分情况是悲观锁,而对于同步类型的则大多为乐观锁,这个选择的空间不是很大,不能通过转化锁的策略来优化什么。而对于同步问题,其实还有一种解决方案是线程局部变量(ThreadLocal)的解决方案。
0 请登录后投票
   发表时间:2012-06-19  
我想知道,怎么去决定锁哪些东西。

例如一个方法:methodA 里面 会去操作 4个共享变量。
为了让这个方法methodA  达到线程安全,
是锁方法
synchronized methodA(){

}

还是锁某个共享变量
synchronized(table){
//udpate size
//update list
//update user
}


还是锁 方法块synchronized(this){
//udpate size
//update list
//update user
}


还有,是否每个操作 size、list、user的方法 也需要加锁。
0 请登录后投票
   发表时间:2012-06-19  
diz 写道
我的理解和楼主有比较大的误差

1:并行计算的吞吐辆并不是由锁影响的,锁只是其中一个占有30%比例的因素,锁影响的是性能,而非吞吐。

并行计算的吞吐量是受到唯一个因素制约的: 加速比。

加速比又受到

锁(锁因子,锁力度,锁策略,锁模式,锁数量),
并行调度方案(负载均衡),
CPU核数,
CPU cache命中率来影响的。

其中锁虽然是影响并行执行效率的一个编程因素,但是在并行计算中不是所有的并行操作都会遇见锁,这与CPU的cache命中率有关,所以对于一般的业务锁的需求,并不是盲目的加锁,有时候虽然他们看上去会有并发问题,但是实际上不会命中cache也就完全不必要加锁,并且锁的数量只要符合CPU核数和线程数的公式(有兴趣可以去intel的官方网站查询)就完全产生不了负作用,而如果锁的数量超过了公式,那么是设计问题,而不是编程问题。

所以就涉及不到各种锁的模式了比如随机锁,集中锁,分布锁等,这主要因为锁的策略选择空间不大

而并行计算的吞吐瓶颈就在于复杂平衡和cache的命中。复载平衡可以看成是一个设计问题,但是也可以通过调度算法来转化为了技术问题,


2:对于锁来说,乐观锁与悲观锁是对于并发问题的类型区分的,对于互斥类型的,大部分情况是悲观锁,而对于同步类型的则大多为乐观锁,这个选择的空间不是很大,不能通过转化锁的策略来优化什么。而对于同步问题,其实还有一种解决方案是线程局部变量(ThreadLocal)的解决方案。


你说的吞吐量其实说的是伸缩性,但是楼主明显不想说这方面的问题吧,而是一定资源条件下的吞吐量,使用减少锁粒度的办法;再者,如果固定资源已经是多核环境了,那么在这上面不能满足吞吐量的话,负载均衡就是一个伪命题,所谓的伸缩性也不存在。

对于第二点锁的描述倒是很赞同,在选择同步策略的时候,要做到的其实是让 程序语言和逻辑上的同步情况一致,不盲目地简单使用某种策略,你说的ThreadLocal其实也要符合实际的case。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics