并发编程之安全发布对象

打印 上一主题 下一主题

主题 666|帖子 666|积分 1998

堆栈封闭(Stack Confinement) 是一种 线程封闭(Thread Confinement) 的策略,主要用于确保数据仅在单个线程的调用栈(stack)中被访问,从而制止并发问题。
堆栈封闭的原理

堆栈封闭的基本思想是:
如果一个对象只存在于方法的局部变量中(即存储在线程的栈帧内),那么它不会被多个线程共享,因此是线程安全的。
在 Java 里,每个线程都有自己的调用栈,局部变量存储在栈帧(stack frame)中,因此局部变量不会被其他线程访问。
堆栈封闭的特点


  • 局部变量是线程私有的:每个线程都有自己的栈帧,因此局部变量只能被当前线程访问。
  • 不需要同步:由于局部变量不会在线程之间共享,不需要额外的同步步伐来保证安全性。
  • 生命周期短:局部变量的生命周期通常只存在于方法调用的过程中,不会长时间存在。
示例

  1. public class StackConfinementExample {
  2.     public void doSomething() {
  3.         int localVariable = 42; // 线程安全,因为是局部变量
  4.         String localString = "Hello"; // 线程安全
  5.         SomeObject localObject = new SomeObject(); // 线程安全,因为它只在当前方法内使用
  6.         process(localVariable, localString, localObject);
  7.     }
  8.     private void process(int value, String text, SomeObject obj) {
  9.         // 这里的 value、text 和 obj 仍然是线程安全的
  10.         System.out.println(value + " " + text);
  11.     }
  12.     private static class SomeObject {
  13.         // 示例对象
  14.     }
  15. }
复制代码
在这个例子中:


  • localVariable、localString 和 localObject 只在 doSomething 方法内部存在,并不会被其他线程访问,因此是线程安全的。
  • process 方法也只是对局部变量举行操作,不会影响共享状态。
什么时候堆栈封闭可能失效?

如果对象的引用逃出了方法作用域,那么它就可能被多个线程共享,导致线程安全问题。比方:
  1. public class UnsafeExample {
  2.     private SomeObject sharedObject; // 可能被多个线程访问
  3.     public void unsafeMethod() {
  4.         sharedObject = new SomeObject(); // 这个对象可能被其他线程访问,不再是堆栈封闭的
  5.     }
  6. }
复制代码
在 unsafeMethod 中,sharedObject 被赋值给了类的成员变量,而成员变量是可以被多个线程共享的,如许就违背了堆栈封闭原则。
总结



  • 堆栈封闭是一种简朴的线程安全策略,适用于局部变量的利用场景。
  • 局部变量默认是线程安全的,由于它们存储在方法栈帧中,每个线程都有独立的栈。
  • 制止将局部变量的引用泄露到方法外部,否则对象可能被多个线程访问,从而粉碎线程安全性。
这种策略在并发编程中非常有用,由于它制止了加锁的开销,提高了步伐的效率。

局部变量的引用泄露到方法外部,会粉碎堆栈封闭(Stack Confinement),可能导致线程安全问题。以下是几个典型的示例:

1. 返回可变对象的引用

如果方法返回了局部变量的引用,而这个对象是可变的,那么它可能会被其他线程修改,从而导致线程安全问题:
  1. public class EscapeExample {
  2.     public StringBuilder getStringBuilder() {
  3.         StringBuilder sb = new StringBuilder("Hello");
  4.         return sb; // 逃逸:局部变量的引用被返回
  5.     }
  6.     public static void main(String[] args) {
  7.         EscapeExample example = new EscapeExample();
  8.         StringBuilder sb = example.getStringBuilder();
  9.         sb.append(" World!"); // 其他线程可能修改 sb
  10.         System.out.println(sb); // 可能导致不可预期的修改
  11.     }
  12. }
复制代码
问题:


  • getStringBuilder() 方法返回了 StringBuilder 的引用,而 StringBuilder 是可变的。
  • 调用者可以修改返回的 StringBuilder,如果多个线程同时访问 getStringBuilder(),可能会发生数据竞争。
办理方案:


  • 利用 不可变对象(如 String)。
  • 返回 副本,而不是原始对象:
    1. public String getSafeString() {
    2.     return new String("Hello"); // 返回不可变对象,线程安全
    3. }
    4. public StringBuilder getSafeStringBuilder() {
    5.     return new StringBuilder("Hello"); // 线程不安全
    6. }
    7. public String getSafeStringBuilderContent() {
    8.     return new StringBuilder("Hello").toString(); // 线程安全
    9. }
    复制代码

2. 赋值给类的成员变量

如果一个局部变量的引用被赋值给类的成员变量,那么它可能被多个线程访问:
  1. public class EscapeExample {
  2.     private SomeObject sharedObject;
  3.     public void unsafeMethod() {
  4.         SomeObject obj = new SomeObject();
  5.         sharedObject = obj; // 逃逸:局部变量的引用被赋值给成员变量
  6.     }
  7.     public SomeObject getSharedObject() {
  8.         return sharedObject;
  9.     }
  10. }
  11. class SomeObject {
  12.     private int value = 0;
  13.     public void increment() {
  14.         value++;
  15.     }
  16.     public int getValue() {
  17.         return value;
  18.     }
  19. }
复制代码
问题:


  • unsafeMethod() 让 SomeObject 的引用逃逸到 sharedObject 变量中。
  • sharedObject 可能被多个线程访问,而 SomeObject 不是线程安全的,可能发生并发问题。
办理方案:


  • 利用 synchronizedvolatile 保护 sharedObject,防止竞态条件。
  • 利用 ThreadLocal 确保变量在每个线程内独立:
    1. private ThreadLocal<SomeObject> threadLocalObject = ThreadLocal.withInitial(SomeObject::new);
    复制代码

3. 在内部类中泄露 this 引用

如果 this 在对象构造期间被泄露到外部类,可能会导致未完全初始化的对象被其他线程访问:
  1. public class ThisEscape {
  2.     private final int value;
  3.     public ThisEscape() {
  4.         value = 42;
  5.         new Thread(() -> System.out.println(this.value)).start(); // this 逃逸
  6.     }
  7.     public static void main(String[] args) {
  8.         new ThisEscape();
  9.     }
  10. }
复制代码
问题:


  • this 在构造函数未完成时被传递到线程中,可能导致 value 变量还未正确初始化,就被访问。
办理方案:


  • 制止在构造函数中启动线程,可以利用工厂方法:
    1. public class SafeEscape {
    2.     private final int value;
    3.     private SafeEscape() {
    4.         value = 42;
    5.     }
    6.     public static SafeEscape createInstance() {
    7.         SafeEscape instance = new SafeEscape();
    8.         new Thread(() -> System.out.println(instance.value)).start();
    9.         return instance;
    10.     }
    11. }
    复制代码

总结

方式线程安全问题办理方案方法返回可变对象外部线程可以修改对象返回 不可变对象副本赋值给成员变量其他线程可能访问和修改利用 synchronized、volatile 或 ThreadLocal在构造方法中泄露 this可能访问未初始化的对象制止 this 逃逸,利用 工厂方法 核心原则


  • 保持局部变量的引用在方法内部,制止暴露给其他线程。
  • 利用不可变对象 代替可变对象,大概返回 对象副本
  • 制止在构造方法中启动线程,防止 this 逃逸。
如许可以确保堆栈封闭,制止并发问题。

在并发编程中,安全发布对象(Safe Publication)是指确保对象在多个线程之间可见状态正确,制止未初始化对象数据竞争问题。
如果对象在没有正确同步的环境下发布(即另一个线程可以访问它),可能会发生 "发布逸出"(Publication Escape),导致未初始化或部分初始化的对象被访问,引发 空指针异常数据差别等问题

安全发布对象的几种方式

以下几种方法可以确保对象被正确发布,并且对所有线程都可见:
1. 在静态初始化器中发布

静态初始化器(static {} 块)是在 类加载 时执行的,类加载是线程安全的,因此可以安全发布对象:
  1. public class SafePublication {
  2.     public static final SomeObject instance = new SomeObject();
  3. }
  4. class SomeObject {
  5.     private final int value = 42;
  6. }
复制代码
为什么安全?


  • instance 变量在 类加载 期间初始化,JVM保证类加载是线程安全的,不会发生 重排序部分初始化 的问题。

2. 利用 volatile 变量

volatile 关键字可以确保对象的可见性,即:

  • 保证写操作对所有线程立即可见
  • 禁止指令重排序,防止对象被部分初始化
  1. public class SafePublication {
  2.     private static volatile SomeObject instance;
  3.     public static SomeObject getInstance() {
  4.         if (instance == null) {
  5.             synchronized (SafePublication.class) {
  6.                 if (instance == null) {
  7.                     instance = new SomeObject(); // 安全发布
  8.                 }
  9.             }
  10.         }
  11.         return instance;
  12.     }
  13. }
复制代码
为什么安全?


  • volatile 保证了对象的可见性和初始化顺序,制止 指令重排序 导致的 半初始化 问题(即 instance 被分配内存,但对象还未完全初始化)。

3. 利用 final 变量

如果一个对象的字段是 final,那么它在构造完成后不会被改变,并且在正确构造的环境下,可以安全发布:
  1. public class SafePublication {
  2.     private final SomeObject instance;
  3.     public SafePublication() {
  4.         instance = new SomeObject();
  5.     }
  6.     public SomeObject getInstance() {
  7.         return instance; // 安全发布
  8.     }
  9. }
复制代码
为什么安全?


  • final 变量的写入和对象构造不能被重排序,不会出现半初始化问题。
  • 只要对象 不会逃逸(比方 this 不能在构造函数中发布到其他线程),那么它是线程安全的。

4. 利用 synchronized

Synchronized 关键字提供了 可见性互斥性,可以确保对象在正确同步的代码块中发布:
  1. public class SafePublication {
  2.     private SomeObject instance;
  3.     public synchronized SomeObject getInstance() {
  4.         if (instance == null) {
  5.             instance = new SomeObject(); // 安全发布
  6.         }
  7.         return instance;
  8.     }
  9. }
复制代码
为什么安全?


  • synchronized 保证了可见性,所有线程在获取锁之前都会刷新共享变量,确保对象的完备性。

5. 利用 ConcurrentHashMap、CopyOnWriteArrayList 等并发容器

Java 并发容器(如 ConcurrentHashMap、CopyOnWriteArrayList)可以安全地存储和共享对象:
  1. import java.util.concurrent.ConcurrentHashMap;
  2. public class SafePublication {
  3.     private static final ConcurrentHashMap<String, SomeObject> map = new ConcurrentHashMap<>();
  4.     public static void publishObject(String key, SomeObject obj) {
  5.         map.put(key, obj); // 安全发布
  6.     }
  7.     public static SomeObject getObject(String key) {
  8.         return map.get(key);
  9.     }
  10. }
复制代码
为什么安全?


  • ConcurrentHashMap 通过 volatile 变量CAS 操作 保证了线程安全性,确保对象的可见性。

6. 利用 ThreadLocal

ThreadLocal 使每个线程拥有自己的 独立实例,制止多个线程同时访问共享对象:
  1. public class SafePublication {
  2.     private static final ThreadLocal<SomeObject> threadLocalInstance = ThreadLocal.withInitial(SomeObject::new);
  3.     public static SomeObject getInstance() {
  4.         return threadLocalInstance.get();
  5.     }
  6. }
复制代码
为什么安全?


  • ThreadLocal 变量不会被多个线程共享,因此不需要额外的同步步伐。

哪些方式是不安全的?

以下是一些 非安全 的对象发布方式:
方式问题直接赋值给静态变量可能会发生 指令重排序,导致对象被部分初始化。在构造函数中发布 this可能导致 未初始化的对象 被其他线程访问。没有同步的普通变量可能发生 可见性问题,导致差别线程看到的值差别。 示例:
  1. public class UnsafePublication {
  2.     private static SomeObject instance;
  3.     public static SomeObject getInstance() {
  4.         if (instance == null) {
  5.             instance = new SomeObject(); // 线程不安全
  6.         }
  7.         return instance;
  8.     }
  9. }
复制代码
问题:


  • instance 可能在 未完全初始化 的环境下被其他线程访问。
  • 线程 A 执行 instance = new SomeObject(); 时,线程 B 可能会看到一个 部分初始化的 instance

总结

方法是否安全缘故起因静态初始化器✅ 安全类加载时初始化,JVM 保证线程安全。volatile 变量✅ 安全确保可见性,防止指令重排序。final 变量✅ 安全final 变量的赋值不会被重排序。synchronized 方法/代码块✅ 安全确保可见性和互斥性。并发容器(ConcurrentHashMap)✅ 安全线程安全的数据布局,提供可见性保障。ThreadLocal✅ 安全每个线程拥有独立变量,不共享数据。普通静态变量(无同步)❌ 不安全可能发生指令重排序或可见性问题。构造函数中发布 this❌ 不安全可能导致对象未完全初始化就被访问。 推荐利用


  • 如果对象是不可变的,可以利用 final 变量或静态初始化器。
  • 如果对象是可变的,可以利用 volatile、synchronized 或并发容器。
  • 对于线程独立对象,可以利用 ThreadLocal。
如许可以确保对象在多线程环境下正确初始化,并安全地发布到其他线程。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

星球的眼睛

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表