基础
Java
Java
  • 基础知识
    • Java 语言的特点
    • Java 基础
      • 语法基础
      • 类型
      • 泛型
      • 注解
      • 异常
      • 反射机制
      • Java 容器
    • Java IO
      • 基础IO
      • NIO
    • Java 并发
      • Java 内存模型
        • 主内存与工作内存
        • 对于 volatile 型变量的特殊规则
        • long 和 double 的非原子性协定
        • 原子性、可见性与有序性
        • 先行发生(Happens-Before)原则
      • 线程
        • 状态转换
        • 线程安全性
          • 对象的共享
            • 可见性
            • 线程封闭
            • 不可变性
            • 安全发布
          • 在现有的线程安全类中添加功能
        • 线程池
          • Executor 框架
          • ExecutorService
          • Executors
          • Future
          • CompletionService
          • 设置线程池的大小
          • ThreadPoolExecutor
      • 线程安全的容器
        • 同步容器类
        • 并发容器
          • ConcurrentHashMap
          • CopyOnWriteArrayList
          • BlockingQueue
            • 串行线程封闭
            • 双端队列与工作密取
      • 任务取消
        • 自定义的取消标志
        • 线程中断
        • 通过 Future 来实现取消
      • 条件队列
        • 内置条件队列
        • 显式的 Condition 对象
      • JUC 中的 AQS
        • AbstractQueuedSynchronizer
        • ReentrantLock
        • ReentrantReadWriteLock
        • Semaphore
        • CountDownLatch
      • 原子变量
        • CAS
        • 原子变量类
        • ABA 问题
        • 非阻塞算法
          • 非阻塞的栈
          • 非阻塞的链表(X)
    • Java 虚拟机
      • JVM 的运行机制
      • 类加载器
      • 运行时数据区
        • JVM 的内存区域
        • 永久代与元空间
        • OutOfMemoryError
      • Java 中的 4 种引用类型
      • 垃圾收集(GC)
        • 如何确定垃圾
        • 垃圾回收算法
        • 垃圾收集器
          • Serial 收集器
          • ParNew 收集器
          • Parallel Scavenge 收集器
          • Serial Old 收集器
          • Parallel Old 收集器
          • CMS 收集器
          • Garbage First 收集器
  • Group 1
    • JDK 与 JRE
    • JVM默认配置
    • java与HTTPS
    • 构建高效且可伸缩的结果缓存
    • 基础补充
      • 在 Switch 中使用 String
      • 为什么 Java 语言不支持多重继承?
      • 为什么在重写 equals 方法的时候需要重写 hashCode 方法
      • 为什么 String 要设计为不可变的?
      • 移位运算符
      • SPI 机制
      • 为何 HashMap 不是线程安全的
      • Class.forName() 和ClassLoader.loadClass() 区别
      • synchronized 关键字
    • 零拷贝
    • Java中的锁优化技术
      • 自旋锁与自适应自旋
      • 锁消除
      • 锁粗化
      • 轻量级锁
      • 偏向锁
    • Arthas
    • Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()
由 GitBook 提供支持
在本页
  • 发布与逸出
  • 安全的对象构造过程
  • 不可变对象与初始化安全性
  • 安全发布的常用模式
  1. 基础知识
  2. Java 并发
  3. 线程
  4. 线程安全性
  5. 对象的共享

安全发布

发布与逸出

“发布(Publish)”一个对象的意思是指:使对象能够在当前作用域之外的代码中使用。当某个不应该发布的对象被发布时,这种情况就被称为逸出。

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。

安全的对象构造过程

当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象,即使发布对象的语句位于构造函数的最后一行也是如此。

不要在构造函数中使 this 引用逸出。

  • 在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是 final 方法),同样会导致 this 引用在构造过程中逸出。

  • 在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个 start 或 initialize 方法来启动。

实例化一个对象其实可以分为三个步骤:

  1. 分配内存空间。

  2. 初始化对象。

  3. 将内存空间的地址赋值给对应的引用。

由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  1. 分配内存空间。

  2. 将内存空间的地址赋值给对应的引用。

  3. 初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。

不可变对象与初始化安全性

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

  • 这种保证还将延伸到被正确创建对象中所有 final 类型的域。在没有额外同步的情况下,也可以安全地访问 final 类型的域。

  • 然而,如果 final 类型的域所指向的是可变对象,那么在访问这些所指向的对象的状态时仍然需要同步。

写 final 域重排序规则:

编译器会在 final 域写之后,构造函数 return 之前,插入一个 storestore 屏障。这个屏障可以禁止处理器把 final 域的写重排序到构造函数之外。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域就不具有这个保障。

读 final 域重排序规则:

处理器会在读 final 域操作的前面插入一个 LoadLoad 屏障。这个屏障可以禁止“初次读对象引用”和“初次读该对象包含的 final 域”这两个操作的重排序。

读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个final 域的对象的引用。

安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。

一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用

  • 将对象的引用保存到 volatile 类型的域,或者 AtomicReference 对象中

  • 将对象的引用保存到某个正确构造对象的 final 类型域中

  • 将对象的引用保存到一个由锁保护的域中

所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的。

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布

  • 事实不可变对象必须通过安全发布方式来发布

  • 可变对象必须通过安全发布方式来发布,并且必须是线程安全的或者由某个锁保护起来

事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)”。

在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

对于可变对象,安全发布只能确保“发布当时”状态的可见性。

因此,对于可变对象,不仅在发布对象时需要使用同步,在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。

上一页不可变性下一页在现有的线程安全类中添加功能

最后更新于9个月前