读书人

多线程摘记 006

发布时间: 2012-12-20 09:53:21 作者: rapoo

多线程摘录 006
生存性的问题

* 死锁
最常见的情况, 就是一组线程处理任务的时候, 需要共享使用多个资源. 例如线程A和B需要访问资源X和Y, 实际情况是, A先锁住了X, B锁住了Y, 在B没有释放Y的时候A同时尝试去锁住Y, 而B也同时在尝试锁住X, 这样A和B就相互等待对方释放资源, 造成死锁.

如果是一个数据库系统, 就会设计一些机制来避免这种问题. 当数据库发现这种死锁的时候, 会选择放弃其中一个事务.

死锁的代码可能类似这样
public class LeftRightDeadlock {
??? private final Object left = new Object();
??? private final Object right = new Object();

??? public void leftRight() {
??????? synchronized (left) {
??????????? synchronized (right) {
??????????????? doSomething();
??????????? }
??????? }
??? }

??? public void rightLeft() {
??????? synchronized (right) {
??????????? synchronized (left) {
??????????????? doSomethingElse();
??????????? }
??????? }
??? }
}

一种很狼狈的方式, 是按照锁的hash值来确定锁定资源的顺序. 比如转账:
private static final commonLock = new Object();
public void doTransaction(fromAct, toAct, amount) {
??? int fromHash = System.identityHashCode(fromAct);
??? int toHash = System.identityHashCode(toAct);
??? Object min = fromHash>=toHash ? toAcct : fromAcct;
??? Object max = fromHash>=toHash ? fromHash : toAcct;

??? if (fromHash==toHash) {
??????? synchronized(commonLock) {
??????????? // do transfer...
??????? }
??? }else {
??????? synchronized(min) {
??????????? synchronized(max) {
??????????????? // do transfer...
??????????? }
??????? }
??? }
}


* 两个有相互依赖关系的对象之间的操作也可能出现死锁, 这是滥用synchronized的后果
// Warning: deadlock-prone!
class Taxi {
??? @GuardedBy("this") private Point location, destination;
??? private final Dispatcher dispatcher;

??? public Taxi(Dispatcher dispatcher) {
??????? this.dispatcher = dispatcher;
??? }

??? public synchronized Point getLocation() {
??????? return location;
??? }

??? public synchronized void setLocation(Point location) {
??????? this.location = location;
??????? if (location.equals(destination))
??????????? dispatcher.notifyAvailable(this);
??? }
}

class Dispatcher {
??? @GuardedBy("this") private final Set<Taxi> taxis;
??? @GuardedBy("this") private final Set<Taxi> availableTaxis;

??? public Dispatcher() {
??????? taxis = new HashSet<Taxi>();
??????? availableTaxis = new HashSet<Taxi>();
??? }

??? public synchronized void notifyAvailable(Taxi taxi) {
??????? availableTaxis.add(taxi);
??? }

??? public synchronized Image getImage() {
??????? Image image = new Image();
??????? for (Taxi t : taxis)
??????????? image.drawMarker(t.getLocation());
??????? return image;
??? }
}
当一个线程A调用Dispatcher.getImage的时候, 进入了image.drawMarker(t.getLocation());, 这是就同时锁定了两个对象.假如此时线程B调用Taxi.setLocation, 就容易出现类似A-B:X-Y的死锁, 只是共享资源为A-B对象本身

* 当需要锁定多个资源的时候, 考虑锁定的顺序是非常重要的. 锁定应该成为设计的一部分, 并且清晰的在文档中说明

锁定设计的一个基本原则是, 尽量缩小锁定的粒度

* 有时间限制的锁定也是一种避免死锁的方法. 至少提供了一个机会去进行异常处理, 记录或者忽略这种死锁, 甚至可以在一段时间后重新尝试.

* 利用JVM的机制来调试线程信息
To trigger a thread dump, you can send the JVM process a SIGQUIT signal (kill -3) on Unix platforms, or press the Ctrl-\ key on Unix or Ctrl-Break on Windows platforms. Many IDEs can request a thread dump as well.

* 其他的生存性问题:
1) Starvation (条件不足). 有些情况是因为线程的优先级太低. 线程的优先级一般是和OS的优先级对应的, 在这种情况下不建议提升线程的优先级, 应对优先级高的线程使用Thread.sleep or Thread.yield来临时挂起, 让低优先级的线程有机会执行
2) 响应性差
3) livelock 指的是线程没有阻塞, 但是也没法执行完毕, 因为某个环节失败了, 并且不断的尝试这个失败的操作
liveLock大多出现在基于消息机制的系统, 如JMS. 也可能是出现在多个线程对某个资源的"相同节奏"的争夺. 比如线程A/B都需要资源X/Y, 当线程C锁定了X, A/B都很聪明, 转而去锁定Y...好比两人相遇, A往左,B也跟着往左, 两人永远是面对面, 过不去


* 考虑性能
如果一个程序涉及到资源很少, 那么多线程的益处并不大, 甚至可能带来副作用, 因为线程本身的创建, 调度, 锁定, 同步, 上下文切换等因素也耗费这系统资源. 如果使用单线程足以完成任务并且性能在可接受范围内, 那么, 不要考虑多线程

* 当一个程序从单线程被扩展为多线程, 它的伸缩性情况如何?

* 在数据量不大的情况下,一个简单的"冒泡排序"算法比"快速排序"算法更有效, 这里要说明的是, 如果要采用某种策略, 而实际需求没有清楚的情况下, 很难决定那种策略更好. 所以, 设计和实现当中经常出现的问题是过早优化.

* 记住一点, 先做对了, 再优化
重要的是, 做对不是意味质量差, 不是意味可以随便实现功能, 而是简洁有效的实现功能

* 性能优化的几点考虑
1) 如何定义更快?
2) 在什么情况下会更快? 高负载还是低负载的情况下更快? 这些情况经常出现吗? 能定量测试吗?
3) 这其中有什么需要折中吗? 比如投入更多的资源去研发或者维护

在并发的情况下, 对高性能的追逐经常是导致问题的最大根源之一. 比如为了替换synchronized而使用了double-check locking的方式, 可能造成原子性问题.

* 记住一点, 要测试, 不要光估计

* Amdahl定理
Amdahl定理主要是给出并发过程中涉及到串行化操作对整个性能的影响. 串行化操作所占的比例越大, 优化空间就越小. 参见最后的介绍部分

* 并发程序只要跟可能出现阻塞的资源打交道, 就有串行化操作. 比如等待blockingQueue, 等待ValueLatch来最终合并计算结果.

* JDK5并发框架内的串行化操作
不要过度相信并发的神话, JDK5并发框架内也是有串行化操作, 用一段简单的代码 producer/comsumer来测试ConcurrentLinkedQueue, 吞吐量随着cpu呈直线增长, 但当cpu数量达到8个以后, 程序的吞吐量就保持不变. 这意味着, 更多的线程都在等待资源, 这就意味着串行化.

* 一个很简单的提高吞吐量的方法, 就是把一个大的synchronized块拆成几个小的, 虽然性能提高不会像JDK5的并发框架那么明显, 但是也许会足以满足实际的性能需求.

* 其他的并发代价
1) context switch
2) memory synchronization
3) blocking

* 线程context switch
当运行的线程超过了CPU数量, 那么OS就需要分时间段的挂起一个线程, 保存其计算结果, 然后让其他线程运行. OS和JVM都会参与这样的操作.

如果超过CPU数量的额外的线程被阻塞的情况月频繁, 那么context switch就越多, 意味着性能损耗越大. 非阻塞算法这个时候就可以大幅度减少context switch.

* 编译器对代码的优化
public String getStoogeNames() {
??? List<String> stooges = new Vector<String>();
??? stooges.add("Moe");
??? stooges.add("Larry");
??? stooges.add("Curly");
??? return stooges.toString();
}
考虑这段简单的代码, 如果编译器足够聪明, 就会发现stooges这个同步类Vector其实只是一个局部变量, 没有其他线程能访问, 可以完全忽略同步, 那么在编译的时候, 就把这四个步骤硬编译为没有不同的操作.


* 考虑锁定
1) 锁定的时间长短
2) 请求获取锁定的频率
3) 降低锁定粒度

然而同时要注意
1) 拆分过多的synchronized块是否真正对性能提高有意义.
??? 拆分后可以肯定的是提高了吞吐量, 但是synchronized本身也是有开销的, 如果这种开销抵消了提高吞吐量的并发性能也不值得.
??? 理想的情况是, 如果能把耗时或者阻塞的计算部分移动到synchronized之外, 则是很好的优化
2) 拆分过多的synchronized块也容易造成死锁
3) 也可以把一个大的锁定块修改为对多个小的对象的锁定

两种形式
private final Map shareData;
public void synchronized operate(String para) {
??? Object data = shareData.get(para);
??? // do something ...
}

改为:
方式一:
public void operate(String para) {
??? synchronized(this) {
??????? Object data = shareData.get(para);
??? }
??? // do something ...
}

方式二(假设有多个shareData) :
要注意死锁, 如果shareData1/2/3...之间没有依赖关系, 是非常好的尝试
public void operate(String para) {
??? synchronized(shareData1) {
??????? Object data = shareData1.get(para);
??? }
??? // do something ...
??? synchronized(shareData2) {
??????? Object data = shareData2.get(para);
??? }
??? // do something ...
}

4) lock stripping. 这是一种更细致的锁定拆分. 比如ConcurrentHashMap, 使用的是一个有16个元素的数组的锁, 每个所对应一部分Hash元素. 这样一来, 普通的HashMap的锁定粒度被拆分为16组, 只是某些方法需要获取对全部元素的锁定put/remove?. 当然, 如果在更多CPU的机器上还可以采用更大的分割因子, 但是应该是瓶颈出现在ConcurrentHashMap并且压力是足够大的情况.

lock stripping的例子
@ThreadSafe
public class StripedMap {
??? // Synchronization policy: buckets[n] guarded by locks[n%N_LOCKS]
??? private static final int N_LOCKS = 16;
??? private final Node[] buckets;
??? private final Object[] locks;

??? private static class Node { ... }

??? public StripedMap(int numBuckets) {
??????? buckets = new Node[numBuckets];
??????? locks = new Object[N_LOCKS];
??????? for (int i = 0; i < N_LOCKS; i++)
??????????? locks[i] = 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 (locks[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 (locks[i % N_LOCKS]) {
??????????????? buckets[i] = null;
??????????? }
??????? }
??? }
??? ...
}




避免:
1) 减少锁定的时间, 如拆分大多锁定块
2) 使用其他的协调机制来替代这个锁定


* 容器类的不稳定因素
包括诸如size, isEmpty这类的方法. 在并发的环境下, size或者isEmpty可能不是很准确, 因为元素可以在动态的增长/减少, 即使第一次算出来是正确的, 但下一次再访问就变了, 特别是在循环中使用的时候

ConcurrentHashMap 的做法
ConcurrentHashMap avoids this problem by having size enumerate the stripes and add up the number of elements in each stripe, instead of maintaining a global count. To avoid enumerating every element, ConcurrentHashMap maintains a separate count field for each stripe, also guarded by the stripe lock.[12]

* 如果不是用排它锁呢?
比如, 可以考虑ReadWriteLock(多个读线程,一个写线程), AtomicLong等.

* 监控CPU
如果某些CPU使用率明显高/低于其他CPU, 说明只有一小部分线程在处理任务, 就要考虑使用并发. 如果所有CPU使用率都很高, 那就说明出问题了.
??? - 是否压力不足? 要保证客户端向服务器端施加足够的压力, 否则测试就没意义了
??? - I/O限制? 网络带宽限制?
??? - lock的获取造成的? 这要依靠性能分析工具

* java对于对象的分配速度要比c语言的malloc快?

* 不是有Object Pool. 我个人的意见是实践出真知, 看怎样控制pool. 然而在并发环境下, 可以预见出问题的几率非常大, 因为池化的对象容易保留了过期的数据. object pool也会出现串行化的问题, 进而造成性能瓶颈

* 记住一点: Allocating objects is usually cheaper than synchronizing.

* 在两线程并发的情况下, ConcurrentHashMap的性能大约是synchronizedHashMap的15倍. 当并发量达到16线程, 性能比达到了30倍, 此后的性能比则是缓慢增长. synchronizedHashMap在超过两线程的情况下性能曲线保持平稳, 但却只有单线程访问的1/10


Amdahl定理介绍
==============
Amdahl's law主要的用途是指出了在计算机体系结构设计过程中,某个部件的优化对整个结构的优化帮助是有上限的,这个极限就是当S->∞ 时,speedup(E) = 1/(1-P);也从另外一个方面说明了在体系结构的优化设计过程中,应该挑选对整体有重大影响的部件来进行优化,以得到更好的结果。

但是过后又仔细想想,感觉不大对头,因为Amdahl他老人家费尽心血搞出来的这个定律同时从另一个方面说明:很多时候做事情只要做到好这样的程度就可以了,不必要事事追求完美,也就是说这是个悲观的定律。
附图片:


下面给出Amdahl's law的完整定义:
基本描述
加速比 并行计算中的加速比是用并行前的执行速度和并行后的执行速度之比来表示的,它表示了在并行化之后的效率提升情况。

Amdahl 定理 是固定负载(计算总量不变时)是的量化标准。可用公式:<math>\frac{W_s + W_p}{W_s + \frac{W_p}{p}}</math>来表示。式中<math>W_s, W_p</math>分别表示问题规模的串行分量(问题中不能并行化的那一部分)和并行分量,p表示处理器数量。

讨论
只要注意到<math>p\to \infty</math>时,上式的极限是<math>\frac{W_s}{W}</math>,这意味着无论我们如何增大处理器数目,加速比是无法高于这个数的。

转自:http://hi.baidu.com/iwishyou2/blog/item/df5941d056bd2687a1ec9c22.html

读书人网 >编程

热点推荐