1. 简介

这一章主要引入了一些基本概念,这些基本概念会在之后更加详细的进行说明。

主要的一些基本概念和相关知识。

1.1 线程带来的问题

多线程相比原来单个线程的情况下更好的利用多核,方便异步化编程等操作

同样的,多线程也引入了一些问题:

  1. 安全性:即“永远不发生糟糕的事情”。执行顺序的不可预测,会导致一些错误的结果(最简单的例子就是自增操作,实际上是多个步骤),这就是一个安全性问题
  2. 活跃性:即“某件正确的事情最终会发生”。当某个操作无法继续执行下去的时候,就会发生活跃性问题
  3. 性能: 即“正确的事情尽快发生”

1.2 多线程与并发问题例子

多线程场景会引入并发性,如果不仔细设计就会出现安全性、活跃性等问题。例如:

  1. 使用框架:框架本身引入了并发性,这种并发性通过回调应用程序的代码会扩展到整个程序,如果没有提前按照并发程序去设计本身应用程序的代码,就会有严重的问题。
  2. 使用模块: 使用模块可能会引入并发性,需要注意。例如Timer类
  3. 远程方法调用(Remote Method Invocation,RMI): 调用其它JVM中运行的对象。其他JVM中可能是一个并发场景
  4. GUI应用程序

1.3 采用标注

并发性标注能更好的形成文档,方便维护并发代码。

1.3.1 类的标注

是非入侵式的标注,可以作为公开文档,对于使用者和维护者都有好处

@Immutable:不可变的类,包含了@ThreadSafe的含义
@ThreadSafe:类是线程安全的
@NotThreadSafe:类不是线程安全的

1.3.2 域和方法的标注

不作为公开文档,仅仅方便维护人员使用。主要说明哪些变量有哪些锁来保护

@GuardedBy(lock):表示只有持有了特定某个锁lock才能访问
@GuardedBy("this"): 表示被标注的方法和域是已经使用内置锁(比如方法用同步关键字)对象的成员
@Guardedby("fieldName"):表示与fieldName引用的对象相关联的锁,可以是显示锁业可以是隐式锁
@GuardedBy("Class Name.fieldName"):类似于@GuardedBy("fieldName"),但指向在另一个类的静态域中持有的锁对象
@GuardedBy("methodName()"):指通过调用命名方法返回的锁对象
@GuardedBy("ClassName.class"):指命名类的类字面量对象

2. 线程安全性

2.1 相关概念

  1. 对象的状态:存储在状态变量(例如实例或者静态域)中的数据;对象的状态可能包括其他依赖对象的域。例如,某个HashMap对象状态不仅存储在其本身,还存储在许多Map.Entry对象中。
  2. 共享:变量可以由多个线程同时访问
  3. 可变:变量的值在其生命周期内可以发生变化
  4. 同步:协同对对象可变状态的访问,从而保证多线程访问时的正确性;JAVA中的volatile、显示锁、原子变量、synchronized关键字都是为了同步
  5. 竞态条件(Race Condition):都某个计算的正确性取决于多个线程的交替执行时序时,那么就会发生竞态条件。常见的例子(复合操作):先用if判断检查一个可能失效的观测结果,来决定下一步操作。
  6. 原子性:多个操作要么一起成功,要么都失败。由于复合操作导致竞态条件,可以将复合操作处理成原子性操作来解决。

2.2 线程安全性定义

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

保障线程安全性的措施有:

  1. 采用合适的同步机制
  2. 不在线程之间共享"可变状态"的变量
  3. 使用不可变变量,无状态的变量和对象。(例如无状态的Servlet)

2.3 锁机制

JAVA中主要使用以下的锁来保证同步和线程安全性

  1. 内置锁:进入代码段获得锁,结束时释放锁。相当于于互斥锁

举例1(同步代码块):

    synchronized(lock){
        //访问或者修改由锁保护的共享状态
    }

举例2(同步方法):

    public synchronized void method(){
        //code  
    }
  1. 可重入锁:在加锁的内部代码再上范围更小的锁时,可以再进行加锁操作,这样可以避免死锁。

例如代码:

2.4 使用锁保护对象

变量由锁保护的定义:对于可能被多个线程同时访问的可变状态变量,在访问它时需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

常见的加锁来同步代码的方式(例如Vector和其他同步类):将所有的可变状态都封装在对象内部,并通过对象的内置锁对所有访问可变状态的代码路径进行同步,使得在该对象上不会发生并发访问

注意点: 每个共享的和可变的变量都应该只由一个锁来保护,从而使得维护人员知道是哪一个锁。

2.5 加锁与活跃性和性能问题

对所有方法加锁,会导致十分差的性能,严重降低并发性。要判断同步代码块的合理大小,需要在各种设计和需求之间进行权衡。包括安全性、简单性和性能。

合理采用细粒度的锁可以在保证安全性前提下提升并发性

最佳实践:

  1. 在简单性与性能之间存在相互制约因素。当实现某个同步策略时,一定不雅盲目地为了性能而牺牲简单性。(这可能会破坏安全性)
  2. 当执行时间较长的计算或者可能无法快速完成的操作时(例如,网络IO或控制台IO),一定不要持有锁

3.对象的共享

3.1 可见性

同步(例如加锁)的意义不仅在于实现原子性、确定临界区;还可以保证内存可见性。

在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结论

为了确保所有线程都能看到共享变量的最新之,所有执行读操作或者写操作的线程都必须在同一个锁上同步

3.2 volatile关键字

volatile是一种比较弱的同步机制,主要用于使一个变量是可见的。

不建议通过volatile来控制状态的可见性。只有当volatile变量能够简化代码的实现以及对同步策略的验证时,才应该使用。

最佳实践:当前仅当满足以下所有条件时,才应该使用volatile变量

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值(不支持自增操作,除非是单线程)
  2. 该变量不会与其他状态变量一起纳入不变性条件中
  3. 在访问变量时不需要加锁

3.3 发布与逸出

发布对象:使当前对象能够在当前作用于之外使用。例如将引用传递到其他类的方法中。

逸出:当某个不应该发布的对象被发布时,这种情况称为逸出

很多时候我们要确保内部状态不被发布,发布内部状态会破坏封装性。例如在对象构造之前就发布该对象,就会破坏线程安全性。

常见错误:在构造过程使this引用逸出
解决办法:使用工厂方法来防止this引用在构造过程中逸出

3.4 线程封闭(Thread Confinement)技术

之前提到过,不共享变量的话,即使不加锁也可以保证线程安全性。

线程封闭技术指的是将一个对象封闭在一个线程中,这时候将自动实现线程安全性,即使被封闭的对象本身不是线程安全的。

线程封闭式实现线程安全的最简单的方式:

使用线程封闭的场景例如:

  1. Swing:将可视化组建和数据模型对象封闭到SWING事件分发线程中
  2. JDBC的Connection对象。

volatile变量上存在一种特殊的线程封闭。如果能确保只有单个线程对共享的volatile变量执行写入操作,那么久可以安全地在这些共享的volatile变量上执行“读取-修改-写入”这种复合操作。相当于将修改操作封闭在单个线程中以防止发生竞态条件,同时还保障了可见性。

3.4.1 栈封闭

栈封闭:是线程封闭技术的一种特例。即只能通过局部变量才能访问对象。

正如封装能使得代码更容易维持不变性条件,同步变量也能够使对象更易于封闭在线程中。

3.4.2 ThreadLocal类

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

例子P37:使用ThreadLocal来保存JDBC的Connection对象

使用场景:当某个频繁执行的操作需要一个临时对象,例如上面的Connection对象,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。

注意:除非这个频繁执行频率非常高或者分配操作的开销非常高,否则这么做不会带来性能提升。例如分配临时缓冲区这种简单的对象,不会带来什么性能优势。

使用时要小心,不用滥用。ThreadLocal类似于全局变量,会降低代码的可重用性,并在类之间引入隐含的耦合性。

实现应用框架时大量使用了ThreadLocal,例如EJB调用期间,容器需要将一个事物上下文(Transaction Context)与某个执行中的线程关联起来。通过将事物上下文保存在静态的ThreadLocal对象中,可以很容易实现这个功能:当框架代码需要判断当前运行的是哪个事物时,只需要从这个ThreadLocal对象中读取事务上下文。这种机制很方便,因为它避免了在调用每个方法时都要传递执行上下文信息,然而这也将该机制的代码与框架耦合在一起。

3.5 不变性

如果某个对象在被创建后其状态就不能被修改,呢么这个对象就称为不可变对象。

线程安全是不可变对象的固有属性

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

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

  1. 对象创建以后其状态就不能修改
  2. 对象的所有域都是final类型
  3. 对象是正确创建的(在对象创建期间,this引用没有逸出)

易错点:不可变性不能等价于将对象中所有的域都声明为final类型,即使对象中所有的域都是final类型,这个对象也仍然是可变的,因为在final类型的域中可以保存对可变对象的引用

最佳实践:

  1. 除非需要更高的可见性,否则应该将所有的域都声明为私有域
  2. 除非需要某个域是可变的,否则应该讲其声明为final域

3.5.1 使用volatile类型来发布不可变对象保证线程安全(不使用锁)

有时候,多个原子变量或者不可变对象由于访问的时序不同仍然是线程不安全的。因此可以采用引入一个不可变的容器对象(一个类,域都是不可变的)来保存这些变量。

例如:

这里的注意点是:

  1. OneValueCache的构造函数中使用copyOf方法来拷贝,否则这个类就不是不可变的(传递引用的话,可能引用了一个可变的引用对象)
  2. 采用volatile来发布不可变容器类,使得所有操作可见。

综上可知,不可变容器来保证了容器类之间的访问互不干扰,而volatile保证各个操作之间的可见性

3.6 安全发布

不安全发布的例子:构造未完成,即被使用

不可变对象不用担心其不安全发布,因为JAVA内存模型为不可变对象提供了一种特殊的初始化安全性保证。

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步

3.6.1 安全发布常用模式

基本措施
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式安全地发布:

  1. 在静态初始化函数中初始化一个对象引用
  2. 将对象的引用保存到volatile类型的域或者AtomicRerence对象中
  3. 将对象的引用保存到某个正确构造对象的final类型域中
  4. 将对象的引用保存到一个由锁保护的域中

采用容器类
java的线程安全库容器提供了以下安全发布的保证:

  1. 将一个键或值放入Hashtable、synchronizedMap、ConcurrentMap中,可以安全地将它发布给任何从这些氢气中访问它的线程(无论是直接访问还是通过迭代器访问)
  2. 通过将某个元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
  3. 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

利用数据传递机制
利用Future和Exchanger类来完成安全发布

使用静态初始化器

这是最简单和最安全的方式。因为在JVM内部存在同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。

public static Holder holder = new Holder(42);

3.6.2 事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变(业务决定),那么把这种对象称为“事实不可变对象”(Effectively Immutable Obeject)。事实不可变对象在正确发布后可以当做不可变对象使用,其他线程可以安全地使用。这样避免了额外的同步操作,简化了开发过程而且提升了性能。

例如:

题外话: 因为这种事实不可变对象,是用户自己假定的。所以需要做好文档记录,避免以后对其状态做修改。其实我个人觉得还是少用事实不可变对象,免得为以后引入潜在的BUG

3.6.3 可变对象的发布

安全发布只能保证可变对象在“发布当时”状态的可见性。对于可变对象,不仅在发布对象时需要使用同步,而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象,这些对象就必须被安全地发布,并且必须是线程安全的或者由某各锁保护起来。

综上可以总结,对象的发布需求取决于其本身的可变性:

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

3.7 并发环境中共享对象的策略总结

  1. 线程封闭技术:包括栈封闭或者ThreadLocal类
  2. 只读共享:包括不可变对象和事实不可变对象
  3. 线程安全共享: 直接使用线程安全的对象,其内部已经实现同步,只要调用其共有接口来访问即可,不需要额外的同步
  4. 保护对象:通过锁来保护对象

4. 对象的组合

敬请期待~