java并发编程实战第十一章笔记

第十一章 性能与可伸缩性

一 线程引入的开销:

使用多个线程会引发额外的开销,这些开销包括:线程之间的协调(加锁,内存同步),上下文切换,线程创建和销毁和线程的调度等。如果过度使用线程,这些开销甚至大于由于提高吞吐量,响应性或者计算能力所带来的性能提升。

1. 上下文切换 
如果可运行的线程数大于CPU数量,那么操作系统会将某个正在运行的线程调度出来,保存当前线程执行的上下文(以便下次再次调度能保存进度),调度其他线程使用CPU,并将新调度的线程的上下文设置为当前的上下文。

2. 内存同步 
在synchronized和volatile提供的可见性保证中,可能会使用一些特殊指令——内存栅栏(Barrier)。内存栅栏可以刷新缓存,使缓存无效,这可能会对性能带来影响,因为它将抑制一些编译器优化。在内存栅栏中,大多数操作是不能重排序的。 

3.阻塞  

当在锁发生竞争时,失败的线程肯定会被阻塞。当线程无法获取某个锁或在某个条件等待或在IO操作阻塞时,需要被挂起,这个过程将包括两次额外的上下文切换,以及必要的操作系统和缓存操作。

二 减少锁的竞争

1.缩小锁的范围以缩短锁的持有时间

/**
 * 减少锁的持有时间
 * @author cream
 *
 */
public class Better {  
    private final Map<String,String> attr = new HashMap<String,String>();
    
    public boolean method(String name,String reg){
    	String key = name;
    	String location;
    	synchronized (this) {
			location = attr.get(key);
		}
    	if(location==null) return false;
    	else return true;
    }
} 

例如上面的代码,只有get方法需要被同步,因此只需要为该方法加同步代码块,而不要给整个method都进行同步,或者也可以采用类似于ConcurrentHashMap等线程安全的类来委托。

2.减少锁的粒度

private final Set<String> users = new HashSet<String>();
    private final Set<String> querys = new HashSet<String>();

    public synchronized void addUser(String user){
        users.add(user);
    }

    public synchronized void addQuery(String query){
        querys.add(query);
    }

对于上面这个代码,addUser和addQuery是对两个不同对象的操作,如果为两个操作都加上同一个锁,addUser和addQuery不能同时并发,但更好的方式是应该让addUser和addQuery可以同时执行,因为它们不影响彼此。

private final Set<String> users = new HashSet<String>();
    private final Set<String> querys = new HashSet<String>();

    public void addUser(String user){
        synchronized(users){
            users.add(user);
        }
    }

    public void addQuery(String query){
        synchronized(querys){
            querys.add(query);
        }
    }

3.锁分段

在某些情况下,可以将锁分解技术进一步扩展为对一组独立对象上的锁进行分解,这种情况被称为锁分段,例如,在ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的1/16,其中第N个散列桶由第(N mod 16 )个锁来保护。具体可以参看ConcurrentHashMap的源代码。

4.避免热点域

5.采用一些替代独占锁的方法:如使用并发容器,读写锁,不可变对象以及原子变量等。

猜你喜欢

转载自blog.csdn.net/llcream/article/details/79990001