并发程序的可伸缩性主要是在保证程序安全同步,操作准确的前提下,保障程序在大量线程并发的情况下访问共享同步资源的效率.锁住某个共享的资源以获得独占式的访问这种做法会形成可伸缩性瓶颈――它使其他线程不能访问那个资源,即使有空闲的处理器可以调用那些线程也无济于事。为了取得可伸缩性,我们必须消除或者减少对独占式资源锁的依赖。
基于这种原理有如下几种解决方案,部分内容整理于ibm developerworks.
1 减小锁粒度
public class ShareHashMap {
private static final Map<Integer,Map<String ,Object>> shareMap =
new HashMap<Integer,Map<String ,Object>>();
public void addValue(Integer index,String key,Object value){
checkIndex(index);
shareMap.get(index).put(key, value);
}
public void addValue2(Integer index,String key,Object value){
checkIndex(index);
synchronized(shareMap.get(index)){
shareMap.get(index).put(key, value);
}
}
public Object getValue(Integer index,String key){
checkIndex(index);
return shareMap.get(index).get(key);
}
public Object getValue2(Integer index,String key){
checkIndex(index);
synchronized(shareMap.get(index)){
return shareMap.get(index).get(key);
}
}
private void checkIndex(Integer index){
if(null==index){
return;
}
synchronized(shareMap){
if(null==shareMap.get(index)){
shareMap.put(index, new HashMap<String ,Object>());
}
}
}
}
提高 HashMap
的并发性同时还提供线程安全性的一种方法是废除对整个表使用一个锁的方式,而采用对hash表的每个bucket都使用一个锁的方式(或者,更常见的是,使用一个锁池,每个锁负责保护几个bucket) 。这意味着多个线程可以同时地访问一个 Map
的不同部分,而不必争用单个的集合范围的锁。这种方法能够直接提高插入、检索以及移除操作的可伸缩性。不幸的是,这种并发性是以一定的代价换来的――这使得对整个 集合进行操作的一些方法(例如 size()
或 isEmpty()
)的实现更加困难,因为这些方法要求一次获得许多的锁,并且还存在返回不正确的结果的风险。然而,对于某些情况,例如实现cache,这样做是一个很好的折衷――因为检索和插入操作比较频繁,而 size()
和 isEmpty()
操作则少得多。
实现1,为每个线程分配一个Integer类型的值,每个线程独享唯一一个键值对.这个原理和threadLocal的原理差不多,为每个线程分配独享的数据区域.下面是直接以当前线程为索引值,创建空间的一种实现.
public class ThreadHashMap {
private static final Map<Thread,Map<String ,Object>> shareMap =
new HashMap<Thread,Map<String ,Object>>();
public void addObject(String key,Object value){
checkThread();
shareMap.get(Thread.currentThread()).put(key, value);
}
public Object getObject(String key){
checkThread();
return shareMap.get(Thread.currentThread()).get(key);
}
private void checkThread(){
Thread t = Thread.currentThread();
synchronized(shareMap){
if(null==shareMap.get(t)){
shareMap.put(t, new HashMap<String ,Object>());
}
}
}
}
实例2,运行多个线程读写同一个键值对,虽然也存在锁定,但是效率要比整个锁住整个hashmap要好的多.
2. 使用非堵塞的编程方式
使用ConcurrentHashMap,NIO等非堵塞资源提供方式.
如果拥有一个互斥锁,但是却堵塞导致其他线程等待甚至饥饿,是极其应该避免的.
具体内容可以见我在javaeye发布的文章线程,非堵塞队列和线程池相关小贴士
3. 根据访问操作的特征选择合适的数据结构.
如果读操作远高出写操作,建议使用类似org.apache.commons.collections的FastHashMap等快速集合,但是要注意
FastHashMap的写操作效率是很低的,写线程比较多的情况下很容易堵塞,另外FastHashMap是 以slow的方式运行的,要通过设置才能以fast模式运行,关于更多的FastHashMap内容可以看我在javaeye的另外一篇文章:关于FastHashMap
4. 合理使用线程池技术
线程池的使用一方面是为了节省线程创建的资源,或者例如网络连接,数据库连接只有有限的资源为了最大限度的利用这些资源,另一方面也是为了避免同时运行大量的线程,避免cpu在线程切换间浪费大量的资源.
5.CopyOnWrite
Vector<String> v = new Vector<String>();
for(int i=0;i<v.size();i++){
v.get(i);
}
vector是线程安全的,但是通过索引遍历vector并不是安全的,虽然size()和get()方法每次调用都是线程安全的,但是遍历操作本身应该视为一个事务来处理,事务是具有原子性的.如果临时插入或者删除一个元素,导致index发生变更就有可能读取重复或者少读取元素.可以拷贝一个vecotr的副本,对副本进行遍历操作.
6. 锁崩溃
与其在获取锁和释放锁之间切换,不如一直占有锁,把当前的工作完成.获取您已占用的锁比获取无人占用的锁要快得多。
7.减小锁占用的时间
不要把和同步无关的代码,预处理代码和后期处理代码都放在同步块中.