我听说过由于不正确的构造对象而在非线程安全代码中发生这种情况,但我真的没有这个概念,即使在阅读了Goetz的书中的内容之后也是如此。我想巩固我对这种代码味道的理解,因为我可能这样做了,甚至没有意识到。请在您的解释中提供代码以使其坚持下去,谢谢。
示例:在构造函数中,您创建一个事件监听器内部类(它具有对当前对象的隐式引用),并将其注册到监听器列表中。
=> 所以你的对象可以被另一个线程使用,即使它没有完成执行它的构造函数。
public class A {
private boolean isIt;
private String yesItIs;
public A() {
EventListener el = new EventListener() { ....};
StaticListeners.register(el);
isIt = true;
yesItIs = "yesItIs";
}
}
稍后可能发生的另一个问题:对象 A 可以完全创建,可供所有线程使用,由另一个线程使用...除了该线程可以看到创建的 A 实例, yesItIs 及其“yesItIs”值,但不是
isIt
!不管你信不信,这可能会发生!发生的事情是:
=> 同步只有一半是关于阻塞线程,另一半是关于线程间可见性。
Java 选择的原因是性能:如果所有数据都与所有线程共享,线程间可见性会降低性能,因此只有同步数据才能保证共享...
非常简单的例子:
public class Test
{
private static Test lastCreatedInstance;
public Test()
{
lastCreatedInstance = this;
}
}
这就是双重检查锁定不起作用的原因。天真的代码
if(obj == null)
{
synchronized(something)
{
if (obj == null) obj = BuildObject(...);
}
}
// do something with obj
不安全,因为对局部变量的赋值可能发生在其余构造(构造函数或工厂方法)之前。因此,线程 1 可以处于
BuildObject
步骤,当线程 2 进入同一块时,检测到非空 obj
,然后继续操作不完整的对象(线程 1 已在调用中被调度) .
public class MyClass{
String name;
public MyClass(String s)
{
if(s==null)
{
throw new IllegalArgumentException();
}
OtherClass.method(this);
name= s;
}
public getName(){ return name; }
}
在上面的代码中,
OtherClass.method()
传递了一个MyClass
的实例,此时该实例尚未完全构造,即尚未履行name
属性非空的契约。
Steve Gilham 对双重检查锁定被破坏的原因的评估是正确的。如果线程 A 进入该方法并且 obj 为 null,则该线程将开始创建该对象的实例并为其分配 obj。线程 B 可能会在线程 A 仍在实例化该对象(但未完成)时进入,然后将该对象视为非空,但该对象的字段可能尚未初始化。一个部分构造的对象。
但是,如果允许关键字 this 转义构造函数,可能会出现相同类型的问题。假设您的构造函数创建了一个分叉线程的对象实例,并且该对象接受您的对象类型。现在您的对象可能尚未完全初始化,即您的某些字段可能为空。现在,您在构造函数中创建的对象对您的对象的引用可以将您作为非空对象进行引用,但会获取空字段值。
更多解释:
您的构造函数可以初始化类中的每个字段,但如果您允许“this”在创建任何其他对象之前转义,则当其他线程查看时,如果满足以下条件,它们可能为 null(或默认基元): 1. 它们未声明Final 或 2。它们没有声明为 volatile
public class Test extends SomeUnknownClass{
public Test(){
this.addListner(new SomeEventListner(){
@Override
void act(){}
});
}
}
在此操作之后,SomeEventListner 实例将有一个指向 Test 对象的链接,就像通常的内部类一样。
更多示例可以在这里找到: http://www.ibm.com/developerworks/java/library/j-jtp0618/index.html
下面是如何从内部类内部访问
this
的未初始化 OuterClass
的示例:
public class OuterClass {
public Integer num;
public OuterClass() {
Runnable runnable = new Runnable() { // might lead to this reference escape
@Override
public void run() {
// example of how uninitialized this of outer class
// can be accessed from inside of inner class
System.out.println(OuterClass.this.num); // will print null
}
};
new Thread(runnable).start();
new Thread().start(); // just some logic to keep JVM busy
new Thread().start(); // just some logic to keep JVM busy
this.num = 8;
System.out.println(this.num); // will print 8
}
public static void main(String[] args) {
new OuterClass();
}
}
输出:
null
8
注意代码中的
OuterClass.this.num
说明
请看下面的课程:
public class Foo {
public Foo() {
}
class Bar {
public void innserClassDoSomthing(){
doSomthing();
}
}
public void doSomthing(){}}
如您所见,Foo 类有一个名为 Bar 的内部类。编译该类后,jvm 创建的 ByteCode 类如下:
class Foo$Bar {
Foo$Bar(Foo var1) {
this.this$0 = var1;
}
public void innserClassDoSomthing() {
this.this$0.doSomthing();
}
}
正如你所看到的,jvm 创建了一个新的类名称 Foo$Bar 来处理 Foo 及其内部类 Bar 之间的行为。而内部类方法innserClassDoSomthing在构造过程中调用了doSomthing(),而this.this$0在外部类构造函数结束之前就已经引用了它的外部类,所以这里发生了Escape。