同步:
- 原子性
- 内存可见性
3.1 可见性
重排序(Reordering):在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。
只要有数据在多个线程之间共享,就使用正确的同步。
3.1.1 失效数据
3.1.2 非原子的64位操作
最低安全性:当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。
最低安全性不适用于非volatile类型的64位数值变量(double和long)。变量的读取和写入操作都必须是原子操作,但对于非volatile类型的long和double变量,JVM允许将64位的读操作或写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。
3.1.3 加锁和可见性
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
3.1.4 volatile变量
当把变量声明为volatile类型后,编译器与运行时(虚拟机)都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新的写入值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
写入volatile变量相当于退出同步块,而读取volatile变量相当于进入同步块。
不建议过度依赖volatile变量提供的可见性,如果在代码中依赖volatile变量来控制状态的可见性,通常比使用锁的代码更脆弱,也更难以理解。
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile变量的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生。
典型用法:
![](https://img.haomeiwen.com/i4740988/af48e1b2e178f525.png)
加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。
当且仅当满足以下所有条件时,才应该使用volatile变量:**
- 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。(非竞争条件)
- 该变量不会与其他状态变量一起纳入不变性条件中。(非复合操作)
- 在访问变量时不需要加锁。
3.2 发布与逸出
发布:使对象能够在当前作用域之外的代码中使用
逸出:某个不应该发布的对象被发布
发布对象:
- 将对象的引用保存到一个公有的静态变量中
- 发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布
- 发布一个内部类的实例(内部类持有外部类的引用)
安全的对象构造过程
当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象。即使发布对象的语句位于构造函数的最后一行也是如此。如果this引用在构造过程中逸出,那么这种对象就被认为是不正确构造。
不要在构造过程中使this引用逸出。
在构造函数中调用一个可改写的实例方法时(非private,非final),同样会导致this引用在构造过程中逸出。
如果想在构造函数中注册一个事件监听器或启动线程,那么可以使用一个私有的构造函数和一个公共的工厂方法,从而避免不正确的构造过程。
![](https://img.haomeiwen.com/i4740988/1d7afbde0bea5489.png)
3.3 线程封闭
线程封闭:仅在单线程内访问数据
Java语言及其核心库提供了一些机制来帮助维持线程封闭性,例如局部变量和ThreadLocal类,但即便如此,程序员仍然需要负责确保封闭在线程中的对象不会从线程中逸出。**
3.3.1 Ad-hoc线程封闭
Ad-hoc线程封闭:维护线程封闭性的职责完全由程序实现来承担
当决定使用线程封闭技术时,通常是因为要将某个特定的子系统实现为一个单线程子系统。在某些情况下,单线程子系统提供的简便性要胜过Ad-hoc线程封闭性技术的脆弱性。
只要确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享的volatile变量上执行“读取-修改-写入”的操作。这相当于将修改操作封闭在单个线程中以防止发生竞争条件,并且volatile变量的可见性保证还确保了其他线程能看到最新的值。**
由于Ad-hoc线程封闭技术的脆弱性,因此在程序中尽量少用它,在可能的情况下,应该使用更强的线程封闭技术(栈封闭或ThreadLocal类)。**
3.3.2 栈封闭
在栈封闭中,只能通过局部变量才能访问对象。
局部变量的固有属性之一就是封闭在执行线程中。它们位于执行线程的栈中,其他线程无法访问这个栈。
栈封闭比Ad-hoc线程封闭更易于维护,也更加健壮。
基本类型的局部变量始终封闭在线程内。
如果线程内部上下文中使用非线程安全的对象,那么该对象仍然是线程安全的。**
3.3.3 ThreadLocal类
ThreadLocal对象通常用于防止对可变的单实例变量或全局变量进行共享。
当某个频繁执行的操作需要一个临时对象,例如一个缓冲区,而同时又希望避免在每次执行时都重新分配该临时对象,就可以使用这项技术。
ThreadLocal变量类似于全局变量,它能降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。
3.4 不变性
不可变对象一定是线程安全的。
当满足以下条件时,对象才是不可变的:
- 对象创建以后其状态就不能修改
- 对象的所有域都是final类型(从技术上看,不可变对象并不需要将其所有的域都声明为final类型)
- 对象是正确创建的(在对象创建期间,this引用没有逸出)
在不可变对象的内部仍可以使用可变对象来管理它们的状态。
![](https://img.haomeiwen.com/i4740988/641a1e59af8edfd2.png)
3.4.1 final域
在java内存模型中,final域能确保初始化过程的安全性,从而可以不受限制地访问不可变对象,并在共享这些对象时无须同步。
即使对象是可变地,通过将对象的某些域声明为final类型,仍然可以简化对状态的判断。
除非某个域是可变的,否则将其声明为final域。
3.4.2 示例:使用volatile类型来发布不可变对象
每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据。**
对于在访问和更新多个相关变量时出现的竞争条件问题,可以通过将这些变量全部保存在一个不可变对象中来消除。
如果是一个可变的对象,那么就必须使用锁来确保原子性。如果是一个不可变对象,那么当线程获得了对该对象的引用后,就不必担心另一个线程会修改对象的状态。
![](https://img.haomeiwen.com/i4740988/ea63e41b329de576.png)
![](https://img.haomeiwen.com/i4740988/f87ba7a37f637b84.png)
如果要更新这些变量,那么可以创建一个新的容器对象,但其他使用原有对象的线程仍然会看到对象处于一致的状态。**
通过使用包含多个状态变量的容器对象(不可变对象)来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得在没有显式地使用锁的情况下仍然是线程安全的。**
3.5 安全发布
3.5.1 不正确地发布:正确的对象被破坏
![](https://img.haomeiwen.com/i4740988/c146b83fbbe58217.png)
![](https://img.haomeiwen.com/i4740988/824947fd207e8936.png)
没有使用同步确保可见性
3.5.2 不可变对象与初始化安全性
由于不可变对象是一种非常重要的对象,因此Java内存模型为不可变对象的共享提供了一种特殊的初始化安全性保证。
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
这种保证还将延伸到被正确创建对象中所有final类型的域。在没有额外同步的情况下,也可以安全地访问final类型的域。然而,如果final类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
3.5.3 安全发布的常用模式
可变对象在发布和使用的线程都必须使用同步。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
- 在静态初始化函数中初始化一个对象引用。
- 将对象的引用保存到volatile类型的域或者AtomicReference对象中。
- 将对象的引用保存到某个正确构造对象的final类型域中。
- 将对象的引用保存到一个由锁保护的域中(在线程安全容器内部的同步)。
要发送一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器。
![](https://img.haomeiwen.com/i4740988/6bd3b93fbd74709f.png)
静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布。
3.5.4 事实不可变对象
事实不可变对象(Effectively Immutable Object):对象从技术上来看是可变地,但其状态在发布后不会再改变。
程序只需将之视为不可变对象即可。
在没有额外同步地情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
3.5.5 可变对象
对象的发布需求取决于它的可变性:
- 不可变对象可以通过任意机制来发布。
- 事实不可变对象必须通过安全方式来发布。
- 可变对象必须通过安全方式来发布,并且必须是线程安全的或者由某个锁来保护起来。
3.5.6 安全地共享对象
当发布一个对象时,必须明确地说明对象的访问方式。
并发中使用和共享对象的实用策略:
- 线程封闭。
- 只读共享。共享的只读对象包括不可变对象和事实不可变对象。
- 线程安全共享。线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。
- 保护对象。被保护的对象只能通过持有特定的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。
网友评论