对象的共享
通过学习线程安全的问题和对象的共享的知识,你就了解了构建线程安全类以及通过java.util.concurrent类库来构建并发程序的重要基础。
前面讲了原子性或者称为临界区的知识,其实加锁还有一个方面就是内存可见性。其实我们不希望一个进程在进行更改的时候,另一个线程可以访问该对象;等修改完成后,其他的线程可以看到这个变量的状态。
可见性
除了正常的竞态资源的争夺,还有就是指令重拍序带来的各种问题。
关于你的代码可能不是按照书写的顺序进行执行,执行的顺序可能被打断,但是理论上在单线程环境下是没有问题的,但在竞态资源下是有点小问题的。
失效数据
失效数据是没有进行同步产生的最大危害。
非原子的64位操作
使用volatile或者是加锁的方式来解决。
加锁和可见性
内置锁可以用于确保某个线程以一种可预测的方式来查看另一个线程的执行结果。只有加锁才能避免读取到失效的值。
volatile变量
volatile修改立即可见,比加锁性能好一些,但是volatile变量通常用作某个操作完成、发生中断或者状态的标识。
volatile做不到加锁的原子性。
使用限制(并不是完全的立即可见),所以必须满足一下的条件才可用:
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
- 该变量不会与其他状态变量一起纳入不变性条件中。
- 在访问变量时不需要加锁。
发布与溢出
发布:发布一个对象的意思是使对象能够在当前作用域之外的代码中使用。发布对象必须要考虑到线程的安全性。
发布对象最简单的方法是将其放入静态域里面;还有全局定义,作为参数传递等等。
溢出:当发布一个对象时,也会间接地发布一些对象(可以在别的空间进行调用)。所以对象的封装是很重要的。最要的是在构造的过程中,你可能会把this溢出,这个就太恐怖了。
将this溢出的常见错误是在构造函数中启动一个线程,this会被线程共享,在对象还没有构建完成,线程就可以看到它了。在构造函数中,初始化内部类的时候也会溢出this对象。
可以使用工厂方法来防止this引用在构造的过程中溢出。其实就是包装了一下。
线程封闭
避免同步就不要在线程间共享数据,这种技术就称作为线程封闭。
常见的就是JDBC的Connection对象,java没有要求Connection对象必须是线程安全的。这个对象会被封闭在线程中。
线程封闭是程序设计的一个考虑因素,必须在程序中实现。Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类。但即便如此,程序员依然要确保封闭在线程中的对象不会从线程中溢出。
Ad-hoc线程封闭
维护线程封闭性的职责完全由程序实现来承担。
这个短语通常用来形容一些特殊的、不能用于其它方面的的,为一个特定的问题、任务而专门设定的解决方案。
这个现在几乎不使用。
栈封闭
栈封闭是线程封闭的一种特例,在栈封闭中,只能通过局部变量才能访问对象。
由于基本类型是没有办法取得它的引用的,所以基础类型是封闭在线程中的。
如果在线程内部上下文使用非线程安全的对象,那么该对象任然是线程安全的。然而要小心的是,只有编码人员才知道哪些对象需要被封闭到执行线程中,以及被封闭对象是否是线程安全的。
ThreadLocal类
这个实现就是将线程中的某个值和保存的对象关联起来。
不变性
不可变对象一定是线程安全的。
不可变对象的条件
- 对象创建后其状态就不能更改
- 将所有的域都加上final(如果属性是对象引用,还有保证这个对象也是不可变的)
- 对象时正确创建的(在对象创建期间,this引用没有逸出)。
如果想改变状态,就是新建一个对象替换旧的对象就可以了。
final域
final域可以确保初始化过程中的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无需同步。
如果一个类是线程安全的,可以加上注解ThreadSafe,这里有很多基本注解的介绍!Java 标注(Annotation)详解
安全发布
不正确的发布:正确的对象被破坏。
不可变对象和初始化安全
如果final指向的是可变对象,那么这个同步还是必须加上去的。
安全发布的常用模式
一个正确构造的对象可以通过一下的方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中。
- 将对象的引用保存到某个正确构造对象的final类型域中。
- 将对象的引用保存到一个由锁保护的域中。
线程安全库中有以下的安全保障:
- 通过将一个键或者值放入HashTable、SynchronizedMap或者ConcurrentMap中,可以直接发布给访问它的线程。
- 通过将元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、SynchronizedList或者SynchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
- 通过将元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列访问该元素的线程。
类库中的其他数据传输机制(例如Future和Exchanger)同样能实现安全发布,在介绍这些机制时将讨论他们的安全发布功能。
事实不可变对象
对象的状态在发布之后不会再改变,那么把这种对象称为“事实不可变对象”。
可变对象
对象的发布取决于它的可变性:
- 不可变对象可以通过任意机制来发布
- 事实不可变对象必须通过安全方式来发布
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁保护起来。
在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:
- 线程封闭 : 只有线程内部访问
- 只读共享 : 不可变对象和事实不可变对象
- 线程安全共享:内部加锁,外部调用不用加锁
- 保护对象:外部需要加锁