可见性
为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
非原子的64位操作
Java内存模型要求,变量的读取操作和写入操作都必须是原子操作,但对于非volatile
类型的double
和long
变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile
类型的long
变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long
和double
等类型的变量也是不安全的,除非用关键字volatile
来声明它们,或者用锁保护起来。
加锁与可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
volatile变量
Java语言提供了一种稍弱的同步机制,即volatile
变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile
类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile
变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile
类型的变量时总会返回最新写入的值。
加锁机制既可以确保可见性又可以确保原子性,而
volatile
变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile
变量:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中,
- 在访问变量时不需要加锁。
线程封闭
当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术被称为线程封闭,它是实现线程安全性的最简单方式之一。
Ad-hoc线程封闭
Ad-hoc
线程封闭是指,维护线程封闭性的职责完全由程序实现来承担。
Ad-hoc
线程封闭是非常脆弱的,因为没有任何一种语言特性,能将对象封闭到目标线程上。
栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。局部变量的固有属性之一就是封闭在执行线程中,它们位于执行线程的栈中,其他线程无法访问这个栈。栈封闭比Ad-hoc
线程封闭更易于维护,也更加健壮。
在维持对象引用的栈封闭性时,程序员需要多做一些工作以确保被引用的对象不会逸出。
ThreadLocal类
维持线程封闭性的一种更规范方法是使用ThreadLocal
,这个类能使线程中的某个值与保存值的对象关联起来。ThreadLocal
提供了get
和set
等访问接口或方法,这些方法为每个使用该变量线程都存有一份独立的副本,因此get
总是返回由当前执行线程在调用set
时设置的最新值。
1 | private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { |
ThreadLocal
变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
不变性
满足同步需求的另一种方法是使用不可变对象。如果某个对象在被创建后其状态就不能被修改,那么这个对象就称为不可变对象。
不可变对象一定是线程安全的。
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改。
- 对象的所有域都是
final
类型。 - 对象是正确创建的(在对象的创建期间,
this
引用没有逸出)。
安全地共享对象
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
线程封闭:线程封闭的对象只能由一个线程拥有,对象被封闭在该线程中,并且只能由这个线程修改。
只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。
线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
保护对象:被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。