第十一章 性能与可伸缩性
一 线程引入的开销:
使用多个线程会引发额外的开销,这些开销包括:线程之间的协调(加锁,内存同步),上下文切换,线程创建和销毁和线程的调度等。如果过度使用线程,这些开销甚至大于由于提高吞吐量,响应性或者计算能力所带来的性能提升。
1. 上下文切换
如果可运行的线程数大于CPU数量,那么操作系统会将某个正在运行的线程调度出来,保存当前线程执行的上下文(以便下次再次调度能保存进度),调度其他线程使用CPU,并将新调度的线程的上下文设置为当前的上下文。
2. 内存同步
在synchronized和volatile提供的可见性保证中,可能会使用一些特殊指令——内存栅栏(Barrier)。内存栅栏可以刷新缓存,使缓存无效,这可能会对性能带来影响,因为它将抑制一些编译器优化。在内存栅栏中,大多数操作是不能重排序的。
当在锁发生竞争时,失败的线程肯定会被阻塞。当线程无法获取某个锁或在某个条件等待或在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.采用一些替代独占锁的方法:如使用并发容器,读写锁,不可变对象以及原子变量等。