测试和调试多线程程序非常困难,因为并发危险通常不能统一或可靠地表现出来。 大多数线程问题从本质上是无法预测的,并且可能根本不会在某些平台(如单处理器系统)上或低于一定负载水平下发生。 由于测试多线程程序的正确性非常困难,并且错误可能需要很长时间才能出现,因此从一开始就考虑线程安全性的应用程序的开发就显得尤为重要。 在本文中,我们将探讨一个特定的线程安全问题-允许在构造过程中this引用进行转义(我们将其称为“ 转义的参考问题”)会产生一些非常不理想的结果。 然后,我们将建立一些编写线程安全构造函数的准则。
遵循“安全施工”技术
分析程序是否违反线程安全性可能非常困难,并且需要专业经验。 幸运的是,也许令人惊讶的是,从一开始就创建线程安全的类并不那么困难,尽管它需要不同的专业技能:纪律。 大多数并发错误源于程序员试图以方便,可感知的性能优势或仅仅是懒惰为名打破规则。 与许多其他并发问题一样,在编写构造函数时,可以遵循一些简单的规则来避免转义引用问题。
危险比赛条件
大多数并发危害归结为某种数据竞争 。 当多个线程或进程正在读取和写入共享数据项时,就会发生数据争用或争用情况 ,最终结果取决于线程调度的顺序。 清单1给出了一个简单的数据争用示例,其中程序可以根据线程的调度打印0或1。
清单1.简单的数据竞赛
public class DataRace {
static int a = 0;
public static void main() {
new MyThread().start();
a = 1;
}
public static class MyThread extends Thread {
public void run() {
System.out.println(a);
}
}
}
第二线程可以立即调度,在打印的0初始值a 。 或者,第二个线程可能不会立即运行,从而导致打印值1。 该程序的输出可能取决于您使用的JDK,底层操作系统的调度程序或随机时序工件。 多次运行可能会产生不同的结果。
可见度危害
清单1中实际上还有另一个数据竞争,除了明显的竞争,即第二线程是在第一个线程将a设置为1之前还是之后开始执行。第二个竞争是可见性竞争:两个线程没有使用同步,这将导致确保跨线程的数据更改可见性。 因为没有同步,所以如果第二个线程在第一个线程完成对a的分配后运行,则第一个线程所做的更改可能会或可能不会立即显示给第二个线程。 即使第一个线程已将其分配为值1,第二个线程也可能仍将a视为a的值为0。第二类数据争用,其中两个线程在没有适当的情况下访问同一变量同步是一个很复杂的主题,但是幸运的是,每当您读取一个可能由另一个线程最后写入的变量,或者写入一个可能随后被另一个线程读取的变量时,可以通过使用同步来避免此类数据争用。 我们不会在这里进一步探讨这种类型的数据争用,但是请参见“与Java内存模型同步”侧边栏和“ 相关主题”部分,以获取有关此复杂问题的更多信息。
在构建过程中不要发布“ this”参考
可能在类中引入数据争用的错误之一是在构造函数完成之前, this引用公开给另一个线程。 有时引用是明确的,如直接存储this在静态字段或集合,但其他时候,它可以是隐式的,当你在一个构造函数中发布引用非静态内部类的实例,例如。 构造函数不是普通的方法-它们具有用于初始化安全性的特殊语义。 在构造函数完成后,假定对象处于可预测的一致状态,并且发布对未完整构造的对象的引用很危险。 清单2显示了将这种竞争条件引入构造函数的示例。 它看起来可能无害,但其中包含严重的并发问题的种子。
清单2.将竞争条件引入构造函数
public class EventListener {
public EventListener(EventSource eventSource) {
// do our initialization
...
// register ourselves with the event source
eventSource.registerListener(this);
}
public onEvent(Event e) {
// handle the event
}
}
第一次检查时, EventListener类看起来是无害的。 侦听器的注册是构造函数所做的最后一件事,该注册将发布对新对象的引用,其他线程可能在该引用中看到该对象。 但是,即使忽略所有Java内存模型(JMM)问题,例如线程间可见性的差异以及内存访问的重新排序,该代码仍然有将未完整构造的EventListener对象暴露给其他线程的危险。 考虑对EventListener进行子类化时会发生什么,如清单3所示:
清单3.子类化EventListener
public class RecordingEventListener extends EventListener {
private final ArrayList list;
public RecordingEventListener(EventSource eventSource) {
super(eventSource);
list = Collections.synchronizedList(new ArrayList());
}
public onEvent(Event e) {
list.add(e);
super.onEvent(e);
}
public Event[] getEvents() {
return (Event[]) list.toArray(new Event[0]);
}
}
因为Java语言规范要求对super()的调用是子类构造函数中的第一条语句,所以在完成子类字段的初始化之前,尚未构造的事件侦听器已在事件源中注册。 现在,我们对list字段进行了数据竞争。 如果事件侦听器决定从注册调用中发送事件,或者我们不幸,而事件恰好在错误的时刻到达,则RecordingEventListener.onEvent()可能会被调用,而list仍具有默认值null ,并且然后抛出NullPointerException异常。 诸如onEvent()类的类方法不必针对未初始化的最终字段进行编码。
清单2的问题在于EventListener在构造完成之前发布了对所构造对象的引用。 虽然它可能看起来像对象几乎完全构造,因此通过this到事件源似乎安全,外表具有欺骗性的。 如清单2所示,从构造函数内部发布this引用是一个等待爆炸的定时炸弹。
不要隐式公开“ this”引用
完全不使用this参考就可以创建转义参考问题。 非静态内部类维护其父对象的this引用的隐式副本,因此创建匿名内部类实例并将其传递给从当前线程外部可见的对象与暴露this引用本身具有相同的风险。 考虑清单4,它与清单2具有相同的基本问题,但是没有明确使用this引用:
清单4.没有明确使用此引用
public class EventListener2 {
public EventListener2(EventSource eventSource) {
eventSource.registerListener(
new EventListener() {
public void onEvent(Event e) {
eventReceived(e);
}
});
}
public void eventReceived(Event e) {
}
}
EventListener2类与清单2中的 EventListener表亲具有相同的疾病:正在发布对正在构造的对象的引用-在这种情况下是间接发布的-另一个线程可以看到它。 如果要对EventListener2进行子类化,则会EventListener2同样的问题,即在子类构造函数完成之前可以调用子类方法。
不要从构造函数中启动线程
清单4中问题的一种特殊情况是从构造函数中启动线程,因为通常当一个对象拥有一个线程时,该线程要么是内部类,要么将this引用传递给其构造函数(或者类本身扩展了该线程)。 Thread类)。 如果对象要拥有线程,则最好像Thread一样,提供一个start()方法,并从start()方法而不是构造函数中启动线程。 尽管这确实通过接口公开了该类的一些实现细节(例如可能存在拥有的线程),但这通常是不希望的,但在这种情况下,从构造函数启动线程的风险大于实现隐藏的好处。
“发布”是什么意思?
并不是在构造过程中this参考的所有引用都是有害的,只有那些在其他线程可以看到它的地方发布该参考的参考才是有害的。 要确定与其他对象共享this引用是否安全,需要详细了解该对象的可见性以及该对象将如何使用该引用。 清单5包含一些安全和不安全做法的示例,它们在构造过程中使this引用转义:
清单5.与此相关的安全和不安全做法
public class Safe {
private Object me;
private Set set = new HashSet();
private Thread thread;
public Safe() {
// Safe because "me" is not visible from any other thread
me = this;
// Safe because "set" is not visible from any other thread
set.add(this);
// Safe because MyThread won't start until construction is complete
// and the constructor doesn't publish the reference
thread = new MyThread(this);
}
public void start() {
thread.start();
}
private class MyThread(Object o) {
private Object theObject;
public MyThread(Object o) {
this.theObject = o;
}
...
}
}
public class Unsafe {
public static Unsafe anInstance;
public static Set set = new HashSet();
private Set mySet = new HashSet();
public Unsafe() {
// Unsafe because anInstance is globally visible
anInstance = this;
// Unsafe because SomeOtherClass.anInstance is globally visible
SomeOtherClass.anInstance = this;
// Unsafe because SomeOtherClass might save the "this" reference
// where another thread could see it
SomeOtherClass.registerObject(this);
// Unsafe because set is globally visible
set.add(this);
// Unsafe because we are publishing a reference to mySet
mySet.add(this);
SomeOtherClass.someMethod(mySet);
// Unsafe because the "this" object will be visible from the new
// thread before the constructor completes
thread = new MyThread(this);
thread.start();
}
public Unsafe(Collection c) {
// Unsafe because "c" may be visible from other threads
c.add(this);
}
}
如您所见, Unsafe类中的许多不安全构造都与Safe类中的安全构造非常相似。 确定this引用是否对其他线程可见可能很棘手。 最好的策略是避免在构造函数中完全(直接或间接)使用this引用。 但是,实际上并非总是如此。 只需记住要小心使用this引用以及在构造函数中创建非静态内部类的实例。
在构造过程中不让引用逃逸的更多原因
当我们考虑同步的影响时,上面详细介绍的线程安全构造的实践显得尤为重要。 例如,当线程A启动线程B时,Java语言规范(JLS)保证线程A在启动线程B时对线程A可见的所有变量对线程B可见,这实际上就像在Thread.start()具有隐式同步一样。 Thread.start() 。 如果从构造函数内部启动线程,则正在构造的对象不会完全构造,因此将失去这些可见性保证。
由于某些更令人困惑的方面,JMM正在Java Community Process JSR 133下进行修订,这将(除其他事项外)更改volatile和final的语义,使它们更加符合一般直觉。 例如,在当前的JMM语义下,线程有可能看到final字段在其生命周期内具有多个值。 新的内存模型语义将阻止这种情况,但前提是必须正确定义构造函数-这意味着在构造过程中不要让this引用转义。
结论
显然,不希望引用另一个线程可见的不完整构造的对象。 毕竟,如何从不完整的对象中分辨出正确构造的对象? 但是通过从构造函数内部发布this的引用(直接或通过内部类间接),我们可以做到这一点,并带来不可预测的结果。 为避免这种危险,请尝试避免使用this ,创建内部类的实例或从构造函数启动线程。 如果无法避免使用this在构造函数中直接或间接,非常肯定的说你是不是做了this参考可见于其他线程。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp0618/index.html
本文探讨了多线程环境中构造函数的线程安全性问题,特别是this引用逃逸带来的隐患。文章详细解释了并发危险,如数据竞争和可见性问题,并提供了编写线程安全构造函数的指南。
354

被折叠的 条评论
为什么被折叠?



