`

多线程摘录 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

分享到:
评论

相关推荐

    delphi多线程编程学习

    在Delphi编程环境中,多线程技术是一种强大的工具,它允许程序同时执行多个任务,从而提高应用程序的响应性和效率。本教程将引导你深入理解Delphi中的多线程编程,并通过实例来帮助你掌握相关技能。 一、多线程概念...

    c语言多进程多线程编程.pdf

    《C语言多进程多线程编程》是一本深入探讨C语言在并发编程领域的专业书籍。在计算机科学中,进程和线程是操作系统中并行执行任务的基本单位,理解和掌握它们对于提升程序性能和优化资源利用至关重要。这本书籍针对...

    C#下多线程实现实例----一个图形化的排序算法演示程序.pdf

    从内容摘录中无法直接得知程序是如何处理线程同步问题的,但可以确定在多线程环境中运行排序算法时,这一点尤为重要。 图形化展示排序过程的核心是ShowData方法。这个方法使用了Windows Forms中的Graphics类来绘制...

    一个很不错的VC++ 多线程应用例子

    摘要:VC/C++源码,系统相关,多线程 与VC++爱好者们分享一个很不错的多线程MultiThread应用例子,你可将之中的模块摘录出来供需要者使用。本多线程模块实例以新建文件写入数据为操作目的,创建多个线程分别创建文件,...

    《Java与模式 阎宏 摘录》.doc 更新中……

    同时,他可能会讨论Java的特性,如多线程、反射、泛型等,以及这些特性和设计模式的结合使用。 通过对《Java与模式 阎宏 摘录》的学习,开发者不仅可以提升自己的编程技巧,还能深入了解如何在实际工作中选择和应用...

    VB异步执行线程的实例源代码

     A:晕,这最终还是调用了老汉多线程……那和线程也没什么区别吧……你应该再试一试线程池……  B:不完全是,因为纤程要先ConvertThreadToFiber,才能CreateFiber,VB中就一个线程,你把它Convert成纤程,那纤程...

    ucgui源程序摘录

    这部分可能涉及到在多线程环境中保护GUI资源的同步机制。在Windows环境下,可能使用标准的互斥对象来实现这一功能,而在其他环境(如单片机)中,可能需要自定义的解决方案。 总的来说,ucgui是一个提供图形用户...

    C++经典笔试和面试摘录

    包括了c++经典笔试题,多线程编程,操作系统,数据库,网络相关知识。以及一些经典面经

    ACE摘录&总结

    ACE Reactor框架是其核心部分之一,它是一个事件多路分离器,能够在一个进程或线程中处理多个客户端连接的事件。使用Reactor框架开发用户程序相对简单,主要包括三个步骤:首先,从`ACE_Event_Handler`派生出子类并...

    vilatile

    根据提供的文件信息,我们可以推断出这部分内容主要讨论了多线程编程中的关键概念与技术。下面将基于这些有限的信息,展开对多线程编程中的一些核心知识点进行详细阐述。 ### 多线程编程概述 多线程编程是现代软件...

    摘录有关vb的资料

    《精通VB.pdf》:这是一本高级教程,旨在帮助读者从熟悉VB到精通,内容可能涉及更复杂的编程概念和技术,如面向对象编程、高级控件应用、多线程处理、网络编程等,有助于提升编程技能。 《VB-函数-速查手册.pdf》:...

    单例设计模式个人总结+摘录

    2. **控制共享资源的访问**:在多线程环境下,可以确保共享资源被正确地管理,避免资源竞争问题。 3. **简化配置过程**:单例模式下的对象通常作为配置或参数传递给其他对象使用,简化了配置过程。 #### 实现方式 ...

    全国各个软件公司面试题---DOTNET笔试题集(摘录)

    可以通过锁(lock)机制、读写锁、线程静态字段(ThreadStaticAttribute)等方式确保多线程环境下的安全性。 #### 4.3 什么是装箱与拆箱? 装箱是指将值类型转换为引用类型的过程;拆箱则是相反的过程。这在使用泛型...

    资深攻城狮解读5个被误解的CPU/GPU概念

    相反,多线程技术可以在较少核心的情况下提供更好的性能提升,因为它可以在同一核心上同时执行多个线程,增加处理效率,并且在开发难度、功耗和面积上,多线程技术更有优势。作者提出未来CPU发展不应只是在多核上...

    Python语言程序设计教程.pptx

    本资源是关于Python语言程序设计的教程,总共分为10章,涵盖了Python编程语言的基础知识、数据类型、控制结构、函数、模块、文件处理、异常处理、面向对象编程、多线程编程等方面的内容。 第一章至第四章主要介绍了...

    参考文献辅助管理软件CvtCNKI

    线程.rar可能包含的是关于CvtCNKI的多线程下载技术文档,这通常意味着软件可能利用多线程技术加速从CNKI等网站下载文献,提高效率,尤其对于大量文献的下载来说,这是一个非常实用的功能。 CvtCNKI-v2.0.1是软件的...

    C++ Concurrency in Action 完整版

    并发编程是一个广泛的话题,涉及到线程的创建、管理、数据共享、同步、内存模型、设计无锁数据结构、设计基于锁的数据结构、并发代码设计、高级线程管理和多线程应用程序的测试与调试等多个方面。 在描述中,书籍的...

    电子阅读器修正版

    在IT行业中,电子阅读器是一种专门用于阅读电子书的软件或设备,它们通常具有文本...这个修复过程涉及到了C#中对剪贴板的操作、可能的多线程同步问题以及异常处理策略,这些都是开发高质量软件时必须考虑的关键因素。

Global site tag (gtag.js) - Google Analytics