并发编程之对象的发布和逸出
一、对象的发布和逸出
发布(publish)对象意味着其作用域之外的代码可以访问操作此对象。例如将对象的引用保存到其他代码可以访问的地方,或者在非私有的方法中返回对象的引用,或者将对象的引用传递给其他类的方法。
为了保证对象的线程安全性,很多时候我们要避免发布对象,但是有时候我们又需要使用同步来安全的发布某些对象。
逸出即为发布了本不该发布的对象。
使用静态变量引用对象是发布对象最直观和最简单的方式。例如以下代码示例,在示例中我们也看到,由于任何代码都可以遍历我们发布persons集合,导致我们间接的发布了Person实例,自然也就造成可以肆意的访问操作集合中的Person元素。
package com.codeartist; import java.util.HashSet; public class ObjectPublish { public static HashSet<Person> persons ; public void init() { persons = new HashSet<Person>(); } }
在非私有的方法内返回一个私有变量的引用会导致私有变量的逸出,例如以下代码
package com.codeartist; import java.util.HashSet; public class ObjectPublish { private HashSet<Person> persons= new HashSet<Person>(); public HashSet<Person> getPersons() { return this.persons; } }
发布一个对象也会导致此对象的所有非私有的字段对象的发布,其中也包括方法调用返回的对象。
在构造函数中使用直接初始化或者调用可改写的实例方法都会导致隐式的this逸出也是经常发生的事情,例如以下代码,在EventListener的实例中也通过this隐含的发布了尚未构造完成的ConstructorEscape实例,可能会造成无法预知的结果。
package com.codeartist; public class ConstructorEscape { public ConstructorEscape(EventSource eventSource) { eventSource.registerListener( new EventListener(){ public void OnEvent(Event e) { doSomeThing(e); } } ); } }
我们可以使用工厂方法防止隐式的this逸出问题,例如以下代码
package com.codeartist; public class ConstructorEscape { private final EventListener listener; private ConstructorEscape() { this.listener= new EventListener(){ public void OnEvent(Event e) { doSomeThing(e); } }; } public static ConstructorEscape getInstance(EventSource eventSource) { ConstructorEscape instance = new ConstructorEscape(); eventSource.registerListener(instance.listener); return instance; } }
二、避免对象发布之线程封闭
线程封闭可以使数据的访问限制在单个线程之内,相对锁定同步来说,其实实现线程安全比较简单的方式。
java提供了ThreadLocal类来实现线程封闭,其可以使针对每个线程存有共享状态的独立副本。其通常用于防止对可变的单实例变量和全局变量进行共享,例如每个请求作为一个逻辑事务需要初始化自己的事务上下文,这个事务上下文应该使用ThreadLocal来实现线程封闭。
栈封闭是线程封闭的特例,即数据作为局部变量封闭在执行线程中,对于值类型的局部变量不存在逸出的问题,如果是引用类型的局部变量,开发人员需要确保其不要作为返回值或者其他的关联引用等而被逸出。
三、避免对象发布之不变性
某个对象创建之后就不能修改其状态,那么我们就说这个对象是不可变对象。
由于多线程操作可变状态会导致原子性、可见性一系列问题,所以线程安全性是不可变对象与生俱来的特性。
不可变对象由构造函数初始化状态,并可以安全的传递给任何不可信代码使用。
所有字段标记为final的对象,由于引用字段的对象可能可以直接修改,所以其并不一定是不可变对象,其需要满足以下条件
对象的所有字段都用final标记
对象创建之后任何状态都不能修改
对象不存在this隐式构造函数逸出
package com.codeartist; import java.util.HashSet; public class ObjectPublish { private HashSet<Person> persons= new HashSet<Person>(); public ObjectPublish() { persons.add(new Person("wufengtinghai")); persons.add(new Person("codeartist")); } public Person getPerson(String name) { Person result = null; for(Person p : this.persons) { if(p.name == name) { result = p.Clone(); } } return result; } }
四、对象的安全发布
很多时候我们是希望在多线程之间共享数据的,此时我们就必须确保安全的发布共享对象。
要安全的发布一个对象,对象的引用以及对象的状态对其他线程都是可见的,一个正确构造的对象可以通过以下方式安全的发布
在静态构造函数中初始化对象引用
使用volatile和AtomicReferance限定对象引用
使用final限定对象引用
将对象引用保存到有锁保护的字段中
1.不可变对象
任何线程都可以在不需要同步的情况下安全的访问不可变对象及其final字段,如果字段的引用可以改变则需要进行同步。
不可变对象在确保初始化安全的前提,可以自由的发布
有时为了确保不可变对象对于多个线程呈现一致的状态,需要使用同步不可变对象的初始化。
需要具备以上说的三个不变性限制条件。
2.隐式约定不可变对象
实际可以改变对象在发布后不会再改变状态,则此对象成为隐式约定不可变对象。
虽然任何线程都可以无需同步即可安全的访问隐式约定不可变对象,但是由于其本身还是可变的,所以其需要以安全方式进行发布。
3.可变对象
需要使用同步来确保发布和共享的安全性。
五、对象的安全共享
为了确保使用共享对象的安全性,我们需要遵循其既定的规则(例如是否是不可变对象)来确定我们访问对象的方式
不可变对象和隐式约定不可变对象可以直接由多线程并发访问。
线程封闭对象只能由创建线程持有并修改。
自己内部实现安全性的线程安全对象也可以直接由多线程访问。
一般的可变对象只能通过持有锁进行同步来实现安全共享。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!