JAVA并发(六)对象的安全发布

本文探讨了JAVA并发中对象的不安全发布问题,重点是this逸出现象,即对象在未完全初始化时被外部访问。介绍了如何避免this逸出,如在构造器中延迟线程启动、使用工厂方法发布对象。同时,讨论了线程封闭策略,如单线程写入volatile变量、栈封闭以及ThreadLocal的使用。此外,文章还阐述了不变性在保证线程安全中的作用,强调final关键字在防止this逸出和确保final域安全性方面的意义。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对象的不安全发布主要有:this逸出,在对象还未实例化完成时,就能被其他对象锁获取(发布)

this逸出

什么是this逸出

对于一个类C来说,“外部方法”指的是行为不完全由类C规定的方法,包括其他类定义的方法,以及类C中可以被改写的方法。当把类C的对象传递给某个外部方法时,相当于发布了该对象,此时如果C的实例未完成实例化,就称为类C的实例的this逸出。最常见的“外部方法”使用场景是在构造器中生成内部类实例

1  // 定义一个事件监听的接口
2  public interface EventListener {
3      void onEvent();
4  }
5
6  // 定义一个管理事件监听器类
7  public class EventSource {
8      private List<EventListener> source = new ArrayList<>(10);
9 
10     public void registerListener(EventListener listener) {
11         try {
12              Thread.sleep(500L);
13         } catch (InterruptedException e) {
14              e.printStackTrace();
15         }
16         listener.onEvent(); //假设listener注册500ms就被调用了
17         source.add(listener);
18     }
19 }
20
21  // this逸出类
22  public class ThisEscape {
23    private String name;
24    private Thread t;
25
26    public ThisEscape(EventSource source, String initName) {
27        name = initName;
28
29        // 在构造器中启动线程
30        t = new Thread(new Runnable() {
31            @Override
32            public void run() {
33                name = "threadName"; // this可能逸出至其他线程
34            }
35        });
36        t.start(); // 一旦启动该线程,this就逸出了,name随时有可能被修改为threadName
37
38        source.registerListener(new EventListener() {
39            @Override
40            public void onEvent() {
41                name = "eventName"; // this隐式逸出
42            }
43        });
44        // 构造函数中需要耗时才能完成this构建,这里为了明显的看到this逸出的效果,设为1s
45        try {
46            Thread.sleep(1000L);
47        } catch (InterruptedException e) {
48            e.printStackTrace();
49        }
50    }
51
52    public static void main(String[] args) {
53        ThisEscape thisEscape = new ThisEscape(new EventSource(), "initName");
54        System.out.println("name = " + thisEscape.name);
55    }
56 }

上述代码中存在两处this逸出,一个是33行、另一个是在41行,最后name的值取决于1115行、3036行、45~49行这三处,它们分别是:

  • 11~15行:listener注册完成后多久才会调用到onEvent?
  • 30~36行:线程t何时启动?
  • 45~49行:thisEscape对象需要多久才能构造完成?

前两处在实际应用的过程中都有可能不是构造器能够控制的,无论是Runable还是EventListener,它们的本质是相同的:在构造器中初始化一个内部类的实例,导致this隐式的泄露

避免this逸出

为了避免this逸出,有如下策略:

  • 可以在构造器中创建线程,但不要直接启动该线程,应该确保在对象初始化完成后再启动该线程
  • 只要将构造器设置为private,然后使用工厂方法发布对象,就一定不存在this逸出。
// 基于工厂方法防止this引用逸出
public class SafeListener {
	private String name;
	private final EventListener listener;
	
	private SafeListener(String initName) {
		name = initName;
		listener = new EventListener() {
			public void onEvent() {
                name = "eventName"; 
			}
		};
	}
	
	public static SafeListener newInstance(EventSource source, String initName) {
		SafeListener listener = new SafeListener(initName);
		source.registerListener(listener); // listener已构造完成,不存在this泄露
		return listener;
	}
}

线程封闭

共享的对象在堆中可以被所有线程访问到,所以会存在线程不安全的问题,但如果某个对象只能被单线程访问,就不存在线程安全问题。这种仅在单线程内访问对象、将某个对象封闭起来的技术称为线程封闭。

单线程写入volatile变量

在volatile变量上存在一种特殊的线程封闭,只要能确保只有单个线程对共享的volatile变量执行写入操作,那么就可以安全地在这些共享volatile变量上执行“读取-修改-写入”操作,这种情况相当于将修改操作封闭在单个线程中,避免了竞态条件,并且volatile变量的可见性可以保证其他线程能够看到最新的修改。

栈封闭

栈封闭是线程封闭的一种特例,在栈封闭中,只有通过局部变量才能访问对象,局部变量都存在于栈中,因此,它是线程安全的。例如:

public int calculte(List<Integer> list) {
	List<Integer> localList = new ArrayList<>();
	
	localList.addAll(list);
	// 后续对localList进行操作,线程安全
}

ThreadLocal

维持线程封闭另一种做法是使用ThreadLocal,这个类能够使线程与对象关联起来,在线程的上下文都可以获取某一个对象,常用在服务器会话上下文变量的传递等场景下。详见ThreadLocal原理

不变性

不可变的对象一定是线程安全的。不可变对象指的是:只有一种状态,且该状态在构造函数内完成,一旦对象构造完成,不再改变。

final关键字

final关键字用于构造不可变对象,final类型的域是不能修改的。与c/c++的constant常量有些相似;但JMM中,final关键字被增强了,还有另外一层语义:初始化过程是安全的,final域禁止处理器把final域的写重排序到构造函数之外,一个对象的final域的初始化一定在该对象初始化完成之前完成

final与this逸出

如果出现了this逸出(this逸出:对象在构造函数执行结束前就能被其他对象或线程获取),final的特殊语义的意义也就不存在了,线程是否安全,完全取决于在构造函数执行结束前,究竟是先this逸出,还是先执行完final,这种随机性也谈不上final域是安全的,因此,final的安全性建立在没有this逸出的前提下。

final域的安全性

在没有this逸出的保证下,final关键字声明的域是可以安全发布的,一旦构造完成就不可变,对象为null时读取不到final域,对象不为null时,final域一定已经构造完成

综上,final保证对象只能被初始化一次,且初始化过程是安全的:

private final List<String> list = new ArrayList<>(16);

public void updateList(List newList) {
	list = newList;
}

然而,若final域引用的对象是可变的,这些被引用的对象可以被修改,还是存在线程不安全。比如下面的例子,虽然list不能被初始化两次,仍然可以修改list的内容。

private final List<String> list = new ArrayList<>(16);

public void add(String s) {
    list.add(s);
}

public void get() {
    return list;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值