线程安全,主要是对共享的、可变的状态的访问要保证其安全性。共享意味着变量可以由多个线程同时访问,可变则意味着变量的值在其生命周期内可以发生变化。一个对象是否需要是线程安全的,取决于它是否被多个线程访问。需要采用同步机制来保证对象的线程安全。

Java中的同步机制主要采用关键字synchronized,这是一种独占的加锁方式。除此之外,还包括volatile变量,显示锁以及原子变量。

线程安全性

当多个线程访问某个类时,这个类始终都能表现出正确的行为,就称这个类是线程安全的。

无状态的对象一定是线程安全的。

一个对象是无状态的,首先它不包括任何域,也就是私有的数据成员;然后它也不包含任何对其他类中域的引用。在计算过程中,临时状态只保存在线程栈上的局部变量中,只能由正在执行的线程访问。

原子性

竞态条件:在并发编程中,由于不恰当的执行时序而出现不正确的结果的情况。当某个计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

一种典型的竞态条件就是“先检查后执行”:先看到某个条件为真,然后根据这个结果采取相应的动作。但是,在观察到结果到采取动作之间,观察结果可能是无效的,从而导致了各种问题。比如,在单例模式中,延迟初始化。

public class MyObj{
    private MyObj obj=null;
    public MyObj getInstance(){
        if(obj==null)
            obj=new MyObj();
        return obj; 
    }
}
复制代码

在竞态条件中,包含一组需要以原子方式执行的操作。要避免这种情况,需要确保其他线程只能在修改操作完成之前或者之后读取和修改状态,而不是在修改状态的过程之中。

加锁机制

在线程安全性的定义中要求,多个线程之间的操作无论使用何种执行时序或交替方式,都要保证不变性条件不被破坏。当在不变性条件中涉及多个变量时,各个变量之间并不是彼此独立的,而是某个变量的值会对其他变量的值产生约束。因此,当更新一个变量时,需要在同一个原子操作中对其他变量同时进行更新。

Java提供了同步代码块(synchronized)这一内置的锁机制来支持原子性。每个Java对象都可以用作锁,这些锁称为内置锁或监视器锁。线程在进入同步代码块之前会自动获得锁,并且在退出同步代码块时自动释放锁(无论正常退出还是抛出异常退出)。

Java的内置锁相当于一种互斥锁,也就是最多只有一个线程能持有这种锁。由于每次只有一个线程执行内置锁保护的代码块,因此,由这个锁保护的同步代码块会以原子方式执行,多个线程在执行该代码块时也不会互相干扰。

内置锁是可重入的,也就是说如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。主要用于避免死锁,使用的场景包括:递归调用同步方法,线程调用其他同步方法。

一种常见的加锁约定是将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问。

可见性

我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能看到发生的状态变化。

可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。

有以下三种实现可见性的方式:

  • volatile
  • synchronized:对一个变量执行unlock之前,必须把变量值同步回主内存
  • final

加锁的含义不仅仅局限于互斥行为,还包括了内存可见性。

volatile变量用于确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时会注意到这个变量是共享的,不会将该变量上的操作与其他内存操作一起重排序。但是,在访问volatile变量时,不会执行加锁操作,因此也就不会使得执行线程阻塞。当且仅当满足以下条件时,才应该使用volatile变量:

  • 对变量的写入操作不依赖变量当前值,或者能保证只有单个线程更新变量的值。
  • 该变量不会与其他变量一起纳入不变性条件中。
  • 在访问变量时不需要加锁。

发布与逸出

发布:对象能够在当前作用域之外的代码中使用,也就是其他对象可以引用该对象。

逸出:当某个不应该被发布的对象被发布时,就是逸出。

当在对象构造完成之前发布该对象到其他线程,就会破坏线程安全性。当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。

//这是一个构造过程中this逸出的例子
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }

    void doSomething(Event e) {
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
    void onEvent(Event e);
    }

    interface Event {
    }
}
复制代码

在实例化ThisEscape对象时,调用了source.registerListener方法,启动了一个线程,且该线程调用了ThisEscape的doSomething()方法,但是此时该对象的初始化还没有完成,因为还没有返回一个引用。也就是造成了this引用逸出。也就是还没有完成实例化的动作,但是暴露了对象的引用。正确的构造过程如下:

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = new EventListener() {
            public void onEvent(Event e) {
            doSomething(e);
            }
        };
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }

    void doSomething(Event e) {
    }

    interface EventSource {
        void registerListener(EventListener e);
    }

    interface EventListener {
        void onEvent(Event e);
    }

    interface Event {
    }
}
复制代码

在这段代码中,我们首先通过私有构造方法构造好了SafeListener对象,然后在newInstance()中才启动了监听器线程。

具体来说,只有当构造函数执行完毕返回时,this引用才应该从线程中逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会再构造函数完成之前使用它。

线程封闭

一种避免使用同步的方式是不共享数据,如果仅仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭。当某个对象封闭在一个线程中时,这种用法将自动实现线程安全性。

维持线程封闭性的一种规范方法是使用ThreadLocal,这个类能使得线程中的某个值与保存值的对象关联起来。ThreadLocal提供了set,get方法,为每个使用该变量的线程都存有一份独立的副本,因此get总是返回由当前执行线程在调用set时设置的最新的值。

不可变性

不可变的对象一定是线程安全的

满足以下条件,对象是不可变的:

  • 对象创建以后其状态就不能修改
  • 对象的所有域都是final类型(保证了初始化过程的安全性)
  • 正确的创建(没有this引用逸出)

final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并在共享这些对象时无需同步。

安全发布

如果没有使用同步确保同一对象对所有线程可见,则这是一个未被正确发布的对象,存在如下两个问题:

  • 除了发布对象的线程之外,其他的线程可能看到的该对象的域是一个失效值。
  • 线程看到该对象的域是最新的,但是此时该对象的域却已经失效了。

Java内存模型为不可变对象的共享提供了初始化安全性保证。任何线程都可以在不需要额外同步的情况下安全的访问不可变对象,即使在发布这些对象时没有使用同步。

正确构造对象的安全发布方式:

  • 在静态初始化函数中初始化一个对象引用(单例饿汉)
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象之中(原子类)
  • 将对象的引用保存草某个正确构造对象的final域中
  • 将对象的引用保存到一个由锁保护的域中(加锁,或放入线程安全容器内)

可以将对象放到线程安全的容器中:Hashtable, synchronizedMap, concurrentMap, vector, copyOnWriterArrayList, copyOnWriterSet, synchronizedList, synchronizedSet, blockingQueue, concurrentLinkedQueue。

使用静态初始化器来发布一个静态构造的对象: public static Object obj=new Object(); 由于静态初始化器由JVM在类的初始化阶段执行,在JVM内部存在同步机制,因此通过这种方式初始化的任何对象都可以被安全的发布。

对象的可变性与发布:

  • 不可变对象可以通过任意机制来发布
  • 事实不可变对象必须通过安全方式来发布
  • 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。

事实不可变对象是指对象从技术上看是可变的,但是其状态在发布后不会再改变,比如讲一个可变对象放入到同步容器中,该对象不会再修改其状态。