摘要: 不变对象是指对象的状态在构造后不可改变。这从根本上消除了线程间同步的需求,与锁或者阻塞策略不同的是,不变对象对运行时和设计时不会带来任何额外的开销,因此不变对象是多线程编程中一个很基本的策略。最简单的不变对象是没有任何状态变量(静态或实例变量)的对象,但在实际编程中出现得更多的是构造后状态不可变的对象,下面是一个简单的例子: [代码]对习惯了普通编程语言的程序员来说,很少编写不变对象,但... 阅读全文
posted @ 2008-12-02 10:13 在路上的牛 阅读(1448) | 评论 (3)编辑

在多线程编程中,安全是我们考虑的最重要的因素。通常程序员都会使用锁来满足安全要求,但是只用锁并不能写出良好的多线程代码,因此我们有必要更深入一点,对线程安全策略进行更加全面的了解。首先谈谈影响线程安全的因素:

影响线程安全的因素

有三个因素影响到了多线程下的安全性:原子性、可见性和指令顺序

  • 一个原子操作是单独的、不可分割的。但是高级语言中的大多数语句,包括一些简单的读写语句,都不具有原子性,最常见的例子是++,同样,long类型的读写在许多操作系统中也不是原子性操作,因为它牵涉到了2个32位的原子操作,如果此时另一个线程去读取内存,就可能出现高位和低位一个老值一个新值的情况。
  • 可见性和指令顺序是两个有关联的问题,在夏天是个好季节"Loads are not reorderd with other loads" is a FACT!! 再续:.NET MM IS BROKEN中有详细的描述,花开花落CLR 2.0 Memory Model也是个不错的参考。简单地说,在多核系统中,CPU单元不会在每次读写操作时都去刷新主内存区域,如果在一个线程的写操作完成但刷新主内存之前,另一个线程读到了主内存中的同一个变量值,程序的正确性就得不到保证。编译器和处理器对指令顺序的优化也有类似的影响。

        请看下面的代码:

不安全的属性读写

 

        Short变量的读写能满足原子性要求,但是在没有使用同步或InterLock类的情况下,不能保证可见性要求,因此仍不是线程安全的。

实现线程安全的策略

从状态机的角度来看,每个对象都具有若干个一致性状态,在运行过程中,所有的公共方法执行前后,对象都不应违反一致性约束,这样的代码我们通常认为是安全的。比如对于基于链表的队列,队列尾节点的Next变量总是指向null,不论在出列/入列完成前后,这一约束都应该满足。

满足对象的一致性约束的两类策略

  • 使用独占技术来避免另一个线程读写到对象的中间状态,这是最常见的策略。
  • 允许不同线程访问到对象的不一致状态,但是线程必须能够检测这些状态,并修正它们对安全性的影响。这种策略常见于各类非阻塞算法中,在多处理器系统中有独特的优势,但是只适合特定的场合。

以基于链表的队列为例来说明这2类策略:

 

对上面的过程,基于锁的策略将使用类似下面的代码来保证另一个线程不能访问到图2的状态

基于锁的入列伪代码

 

如果使用非阻塞算法,当一个线程执行到图2后被中断,而另一个入列的线程发现队列已经处于图2的状态时,将会试图更新Trail到图3的状态,然后再执行增加的操作,具体的代码可参考这里。不论是锁还是非阻塞算法,都能正确维护对象的一致性状态。

实现独占技术的策略

实现独占技术有三种基本的策略:

  • 使用不变对象,一个不变对象肯定是线程安全的,也不会受到可见性和指令顺序优化的影响。最近比较火热的Erlang语言就把这种策略发挥到了极致,变量一旦赋值就不能再改变。
  • 使用和相关机制保证代码执行过程中对象只能被一个线程访问,这个是用得最多的。
  • 限制对象只能被一个线程所访问,这种策略依赖数据封装和隐藏技术,需要在设计上进行仔细地考虑。

三种策略中,锁是最常用的独占机制,但是锁也会带来额外的开销和可扩展性问题。基于锁的算法很容易具有不佳的可扩展性,因为它的实质是将并行化过程改为串行化,很显然当几个并发线程需要排队执行时,增加CPU单元是没有意义的。因此在多核时代,我们应该对不变对象和限制访问技术给予更多的关注。

                                                                                                                    待续。。。




posted @ 2008-12-01 18:51 在路上的牛 阅读(1456) | 评论 (10)编辑

1. 并发和并行:一个好的并发算法未必是一个好的并行算法

2. 并发编程的2个设计要点:安全性能,在多CPU环境下还要考虑可扩展性

3. 安全性:

    a. 最常见的方法是要求对象在任意时刻都应该满足一致的状态(不变约束

    b. 在某些无阻塞算法中,允许对象暂时处于不一致状态,但是应该在运行中检测并立刻修正它

4. 影响安全的因素:原子性、可见性和指令顺序

5. 满足安全性的基本策略:

    a. 通过对象的不可变性保证对象总是满足不变约束:一个不变对象是线程安全的

        a. 无状态变量的对象,静态方法

        b. 所有状态变量均不可变的对象

        c. 可以尝试把原来可变的对象变成不可变对象(使用建立新实例再替换的方法)以减少同步的要求

        d. 实现不变对象要特别注意:在构造函数完全前,不能使用对象!危险在于构造函数中对this的泄漏(对象的逸出问题)

    b. 同步

        a. 锁

        b. 原子变量

        c. 其他工具类 (信号量,监视器)

        e. 防止死锁 (顺序化资源)

    d. 限制:限制对对象的并发访问

6. 提高扩展性的策略:

    关键在于减少串行代码:

    a. 减少锁的使用

        a. 是否可以允许过期数据?

            a. 

        a. 减少锁的范围,有可能阻塞的操作应该排除在锁的范围外,将共享变量的内容保存到本地变量,然后再锁的范围外操纵本地变量

        b. 分解锁

            对互相独立的共享变量,可以使用不同的锁(分拆锁);例子:对队列,可以使用2个锁分别锁定头尾

            使用读写锁来代替普通的锁;

            可以考虑用多个锁来代替一个大锁(分离锁),JDK1.5总的并发hashmap使用了16个锁来代替一个大锁以提高吞吐率,同样,在计算hashmap.size时,将分别计算各部分的size再汇总,以避免锁定整个对象

        c.  当在一个操作中需要同步多个共享变量时,为了减少同步多个变量带来的复杂性,可将多个变量放入一个单独的状态对象,然后使用新建并替换的方式减少甚至消除同步的要求

        d. 某些情况下可以用原子变量来代替锁

        e. 使用非阻塞算法来代替锁

        f. 遍历集合很难避免锁,但是可以考虑使用版本化迭代变量或者缓存的数据来减少锁的需要

        g. 考虑使用不变量/只读适配器(p98)

7. 线程协调和依赖

8. 管理线程

posted @ 2008-11-28 10:29 在路上的牛 阅读(88) | 评论 (0)编辑

在学习Java并发编程的时候看到了单例的讨论,发现CAS挺适应这个场合的,于是顺手写了个简单的,实现了延迟实例化和多线程安全,也算是无锁的一种用法吧:

使用CAS的单例模式实现

 

 

posted @ 2008-11-24 18:59 在路上的牛 阅读(559) | 评论 (0)编辑
     摘要: 最近开始学习无锁编程,和传统的基于Lock的算法相比,无锁编程具有其独特的优点,Angel Lucifer的关于无锁编程一文对此有详细的描述。无锁编程的目标是在不使用Lock的前提下保证并发过程中共享数据的一致性,其主要的实现基础是CAS操作,也就是compare_and_swap,通过处理器提供的指令,可以原子地更新共享数据,并同时监测其他线程的干扰,.Net中的对应实现是InterLocked... 阅读全文
posted @ 2008-11-20 18:32 在路上的牛 阅读(1828) | 评论 (12)编辑
     摘要: C#不支持多继承,只支持多接口实现,这在某些情况下带来了一些不便。现在在C#3.0中,我们可以利用C#3.0的扩展方法来实现一个“受限的多继承”。
C#3.0中引入了扩展方法,可以利用一个静态类的静态方法为一个类或者接口添加方法,关键是:添加的方法是包含实现的,由此我们可以在C#3.0中为接口提供一个带实现的方法声明,而无需额外的实现类。如果一个类实现了多个这样的接口,就可以达到类似多继承的效果。
  阅读全文
posted @ 2008-03-24 19:48 在路上的牛 阅读(2170) | 评论 (14)编辑