0%

Java并发编程(一)

本文介绍了线程安全和共享对象。

线程安全

  1. 无状态对象永远是线程安全的

    无状态对象指的是没有成员域变量的对象

  2. 原子操作
    自增操作是三个离散操作的简写形式: 获取当前值,加1,写回新值。这是一个 读-改-写 操作的实例,其中,结果的状态衍生自它先前的状态。

  3. 竞争条件
    当计算的正确性依赖于运行时中的相关的时序或者多线程的交替时,会产生竞争条件;换句话说,想要得到正确的答案,要依赖于“幸运”的时序。最常见的一种竞争条件是 检查再运行 ,使用一个潜在的过期值作为决定下一步操作的依据。例如: 你观察到一些事情为真(文件X不存在),然后(then)基于你的观察去执行一些操作(创建文件X);但事实上,从观察到执行操作的这段时间内,观察结果可能已经失效了(有人在此期间创建了文件X),从而引发错误(非预期的异常,重写数据或者破坏文件)。

  4. 复合操作和原子操作

    假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指: 该操作对于所有的操作,包括它自己,都满足前面描述的状态。

    java.util.concurrent.atomic 包中包括了原子变量类这些类用来实现数字和对象引用的原子状态转换。例如 long 类型替换为 AtomicLong 类型。

  5. 内部锁
    Java提供了强制原子性的内置锁机制: synchronized 块。一个 synchronized 块共有两部分: 锁对象的引用,以及这个锁保护的代码块。 synchronized 方法是对跨越了整个方法体的 synchronized 块的简短描述,至于 synchronized 方法的锁,就是该方法所在的对象本身。

    1
    2
    3
    synchronized (lock){
    // 访问或修改被锁保护的共享状态
    }

执行 synchronized 块的线程,不可能看到会有其他线程能同时执行由同一个锁保护的 synchronized 块。

  1. 可重进入性
    一个具有锁的方法内可以调用另一个具有锁的方法,称为可重进入。它是通过对锁进行计数实现的。

  2. 活跃度与性能
    若并发 : 请求排队等候并依次被处理。

    通常简单性与性能是相互牵制的。实现一个同步策略是,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协)。
    有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成,执行这些操作期间不要占有锁。

共享对象

  1. 可见性
    重排序现象 : 在单个线程中,只要重排序不会对结果产生影响,那么就不能保证其中的操作一定按照程序写定的顺序执行——即使重排序对于其他线程来说会产生明显的影响。

    在没有同步的情况下,编译器,处理器,运行时安排操作的执行顺序可能完全出人意料。在没有进行适当同步的多线程程序中,尝试推断那些“必然”发生在内存中的动作时,你总是会判断错误。

  2. 非原子的64位操作
    当一个线程在没有同步的情况下读取变量,它可能会得到一个过期值。但是至少它可以看到某个线程在那里设定的一个真实数值,而不是一个凭空而来的值。这样的安全保证被称为是最低限的安全性
    最低限的安全性应用于所有的变量,除了一个例外: 没有申明为 volatile 的64位数值变量(double和long)。Java存储模型要求获取和存储操作都为原子的,但是对于非volatile的long和double变量,JVM允许将64位的读或写划分为两个32位的操作。如果读和写发生在不同的线程,这种情况读取一个非volatile类型long就可能会出现得到一个值的高32位和另一个值的低32位。因此,即使你并不关心过期数据,但仅仅在多线程程序中使用共享的、可变的long和double变量也可能是不安全的,除非将它们声明为volatile类型,或者用锁保护起来

  3. 锁不仅仅是关于同步与互斥的,也是关于内存可见的。为了保证所有线程都能够看到共享的、可变变量的最新值,读取和写入线程必须使用公共的锁进行同步。

  4. Volatile变量
    Java语言也提供了其他的选择,即一种同步的弱形式: volatile变量。它确保对一个变量到更新以可预见的方式告知其他的线程。当一个一域声明为volatile类型后,编译器与运行时会监视这个变量: 它是共享的,而且对它的操作不会与其他的内存操作一起被重排序。volatile变量不会缓存在寄存器或者缓存在对其他处理器隐藏的地方。所以,读一个volatile类型的变量时,总会返回由某一线程所写入到最新值。
    但是我们并不推荐过度依赖volatile变量所提供的可见性。依赖volatile变量来控制状态可见性的代码,比使用锁的代码更脆弱,更难以理解。

    只有当volatile变量能够简化实现和同步策略的验证时,才使用它们。当验证正确性必须推断可见性问题时,应该避免使用volatile变量。正确使用volatile变量到方式包括:

    • 用于确保它们所引用的对象状态的可见性
    • 或者用于标识重要的生命周期事件(比如初始化或关闭)的发生。
  5. 加锁可以保证可见性与原子性;volatile变量只能保证可见性。
    只有满足了下面所有的标准后,才能使用volatile变量:

    • 写入变量时并不依赖变量的当前值(自增操作);或者能够确保只有单一的线程修改变量的值;
    • 变量不需要与其他的状态变量共同参与不变约束
    • 而且,访问变量时,没有其他的原因需要加锁
  6. 发布与逸出
    发布 : 使一个对象能够被当前范围之外的代码所使用
    逸出 : 一个对象在尚未准备好时就将它发布

    不要允许可变的数据逸出,比如:

    1
    2
    3
    4
    5
    6
    class UnsafeStates {
    private Stringp[ states = new String[]{"AK", "AL" ... };
    // 任何一个调用者都可以修改内容
    // 逸出所属的范围
    public String[] getSates() { return states; }
    }

    也不要隐式地允许this引用逸出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class ThisEscape {
    public ThisEscape(EventSource source){
    source.registerListener (
    new EventListener(){
    public void onEvent(Event e){
    // 能够操作ThisEscape私有成员
    // 而且this还没有构造完成
    doSomething(e);
    }
    });
    }
    }

    不要让this引用在构造期间逸出,甚至即使是在构造函数的最后一行。

  7. 线程封闭
    当对象封闭在一个线程时,这种做法会自动成为线程安全的,即使被封闭的对象本身并不是。例如:

    • Swing: UI组件只有主线程才能够访问
    • JDBC: Connection是从池中取得的,池负责管理不将其分配给其他线程

    线程限制是你在程序设计中需要考虑的一个元素,它是在程序的实现中完成的。语言自身以及核心库提供了某些机制(本地变量和ThreadLocal类)有助于维护线程限制,尽管如此,程序员仍然要自己负责确保线程限制对象不会从它所在的线程中逸出。

    7.1 Ad-hoc 线程限制
    Ad-hoc 线程限制 是指维护线程限制性的任务 全部落在实现上的这种情况。

    7.2 栈限制
    栈限制 是线程限制的一种特例,在栈限制中,只能通过本地变量才能触及对象。

    7.3 ThreadLocal
    一种维护线程限制的更加规范的方式是使用 ThreadLocal ,它允许你将每个线程与持有数值的对象关联在一起。 ThreadLocal 提供了getset访问器,为每个使用它的线程维护一份单独的拷贝。所以get总是返回由当前执行线程通过set设置的最新值。 ThreadLocal<T> 可以看做是 map<Thread, T> ,不过它内部不是这样实现的。

  8. 不可变对象
    不可变对象永远是线程安全的。
    只有满足如下状态,一个对象才是不可变的:

    • 它的状态不能在创建后再被修改
    • 所有域都是final类型
    • 它被正确创建(创建期间没有发生this引用的逸出)

    正如“将所有域声明为私有的,除非它们需要更高的可见性”一样,“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践。

  9. 安全发布
    不可变对象可以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时亦不需要同步。

    为了安全地发布对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确的对象可以通过下列条件安全地发布:

    • 通过静态初始化器初始化的引用
    • 将它的引用存储到volatile域或AtomicReference
    • 将它的引用存储到正确创建的对象的final域中
    • 或者将它的引用存储到由锁正确保护的域中(置入线程安全容器,如Hashtable、ConcurrentMap、Vector、CopyOnWriteArrayList、BlockingQueue等)