阻塞
非竞争的同步可以完全在JVM中处理,竞争的同步则可能需要操作系统的介入,从而增加开销。当在锁上发生竞争时,竞争失败的线程肯定会阻塞。JVM在实现阻塞行为时,可以采用自旋等待,就是通过循环不断的尝试获取锁。直到成功。或者通过操作系统挂起被阻塞的线程。这两种方式的效率高低取决于上下文切换的开销以及在成功获取锁之前需要等待的时间。如果等待时间较短,采用自旋等待的方式,如果时间较长,则采用操作系统挂起的方式。有些JVM将根据历史等待时间的分析数据在这两者之间进行选择,但是大多数JVM在等待锁时都只是将线程挂起。
减少锁的竞争
在并发程序中,对可伸缩性的最主要威胁就是独占方式的资源锁
有两个因素将影响在锁上发生竞争的可能性:锁的请求频率,持有锁的时间 。如果二者乘积很小,那么大多数获取锁的操作都不会发生竞争。
所以,降低锁的竞争程序有三个方法:
- 减少锁的持有时间。
- 降低锁的请求频率。
- 使用带有协调机制的独占锁,这些机制允许更高的并发性。
缩小锁的范围
public class AttributeStore{
private final Map<String,String> attributes=new HashMap<String,String>();
public synchronized boolean userLocationMatches(String name,String regexp){
String key="user."+name+".location";
String location=attributes.get(key);
if (location==null)
return false;
else
return Pattern.matches(regexp, location);
}
}
如上面这个类里面的userLocation方法,整个方法的执行过程都持有了锁,而真正需要保证同步性的操作其实只有String location=attributes.get(key);这一行代码。所以大部分的持有时间是浪费的。
修改后如下。只对需要持有锁的操作加上内置锁。大大减少锁的持有时间,可以提高可伸缩性。而且完全可以用ConcurrentHashMap进一步提升它的可伸缩性。
public class AttributeStore{
private final Map<String,String> attributes=new HashMap<String,String>();
public boolean userLocationMatches(String name,String regexp){
String key="user."+name+".location";
synchronized (this) {
String location=attributes.get(key);
}
if (location==null)
return false;
else
return Pattern.matches(regexp, location);
}
}
减小锁的粒度
刚才说减小锁的请求频率,这可以通过锁分解和锁分段技术实现。在这些技术中将采用多个相互独立的锁来保护独立的状态变量。比如原来我们对每一个方法都加上synchronized关键字。但是它们访问的域不都是一个。所以就没必要以保护一个域的时候把其实的状态变量也被限制住。
import java.util.Set;
public class ServerStatus {
private final Set<String> users;
private final Set<String> queries;
public ServerStatus(Set<String> users, Set<String> queries) {
super();
this.users = users;
this.queries = queries;
}
public synchronized void addUser(String u) {
users.add(u);
}
public synchronized void addQuery(String u) {
queries.add(u);
}
public synchronized void removeUser(String u) {
users.remove(u);
}
public synchronized void removeQuery(String q) {
queries.remove(q);
}
}
用锁分解技术改进后
import java.util.Set;
public class ServerStatus {
private final Set<String> users;
private final Set<String> queries;
public ServerStatus(Set<String> users, Set<String> queries) {
super();
this.users = users;
this.queries = queries;
}
public void addUser(String u) {
synchronized (users) {
users.add(u);
}
}
public void addQuery(String u) {
synchronized (queries) {
queries.add(u);
}
}
public void removeUser(String u) {
synchronized (users) {
users.remove(u);
}
}
public void removeQuery(String q) {
synchronized (queries) {
queries.remove(q);
}
}
}
锁分段
在某些情况下,可以将锁分解技术进一步扩展 对一组独立对象上的锁进行分解。这种情况叫做锁分段。
例如:在ConcurrentHashMap的实现中使用了一个包含16个锁的数组。每个锁保护所有散列桶的16分之1。从而实现更好的并发性。
锁分段的劣势在于:与采用单个锁来实现独占访问相比,要获取多个锁来实现独占访问将更加困难并且开销更高。比如当ConcurrentHashMap需要扩展映射范围,以及重新计算键值的散列值要分布到更大的桶集合中,就需要获取分段所集合中的所有锁。
public class StripedMap {
private static final int N_LOCKS = 16;
private final Node[] buckets;
private final Object[] lockes;
public StripedMap(int num) {
super();
this.buckets = new Node[num];
this.lockes = new Object[N_LOCKS];
for (Object lock : lockes) {
lock = new Object();
}
}
private final int hash(Object key) {
return Math.abs(key.hashCode() % buckets.length);
}
public Object get(Object key) {
int hash = hash(key);
synchronized (lockes[hash % N_LOCKS]) {
for (Node m = buckets[hash]; m != null; m = m.next) {
if (m.key.equals(key)) {
return m.Value;
}
}
}
return null;
}
public void clear() {
for (int i = 0; i < buckets.length; i++) {
synchronized (lockes[i % N_LOCKS]) {
buckets[i] = null;
}
}
}
}