以线程安全方式创建对象
直接来自这个网站,我遇到了关于创建对象线程安全性的以下描述。
警告:构造将在线程之间共享的对象时,要非常小心,对对象的引用不会过早“泄漏”。 例如,假设您要维护一个包含每个类实例的List调用实例。 您可能想要将以下行添加到构造函数中:
instances.add(本);
但是其他线程可以在构造对象完成之前使用实例来访问对象。
是否有人能够用其他词语或其他更可抓住的例子来表达相同的概念?
提前致谢。
-
让我们假设,你有这样的课程:
class Sync { public Sync(List
list) { list.add(this); // switch // instance initialization code } public void bang() { } } -
并且你有两个线程(线程#1和线程#2),它们都有一个引用相同的
List
实例。list -
现在,线程#1创建了一个新的
Sync
实例,并且作为参数提供了对list
实例的引用:new Sync(list);
-
在
Sync
构造函数中执行第// switch
行// switch
,有一个上下文切换,现在线程#2正在工作。 -
线程#2执行这样的代码:
for(Sync elem : list) elem.bang();
-
线程#2在第3点创建的实例上调用
bang()
,但此实例尚未准备好使用 ,因为此实例的构造函数尚未完成。
因此,
- 在调用构造函数并将引用传递给几个线程之间共享的对象时,您必须非常小心
- 在实现构造函数时,您必须记住,提供的实例可以在几个线程之间共享
线程A正在创建对象A,在创建对象A的中间(在对象A的构造函数的第一行中)存在上下文切换。 现在线程B正在工作,线程B可以查看对象A(他已经参考了)。 但是,对象A尚未完全构造,因为线程A没有时间完成它。
这是你明确的例子:
比方说,有一个名为House
课程
class House { private static List listOfHouse; private name; // other properties public House(){ listOfHouse.add(this); this.name = "dummy house"; //do other things } // other methods }
和村庄:
class Village { public static void printsHouses(){ for(House house : House.getListOfHouse()){ System.out.println(house.getName()); } } }
现在如果你在一个线程中创建一个House
,“X”。 当执行线程刚刚完成波纹管线时,
listOfHouse.add(this);
并且上下文被切换(已将此对象的引用添加到列表listOfHouse
,而对象创建尚未完成)到另一个线程,“Y”正在运行,
printsHouses();
在里面! 然后printHouses()
将看到一个仍未完全创建的对象,这种类型的不一致称为Leak
。
这里有很多好的数据,但我想我会添加更多信息。
构造将在线程之间共享的对象时,要非常小心,对对象的引用不会过早“泄漏”。
在构造对象时,需要确保其他线程无法访问此对象, 然后才能对其进行构建。 这意味着在构造函数中你不应该,例如:
- 将对象分配给其他线程可访问的类上的
static
字段。 - 在构造函数中的对象上启动一个线程,该线程可能在对象初始化之前开始使用对象中的字段。
- 将对象发布到集合中或通过任何其他机制发布,这些机制允许其他线程在构造完成之前查看该对象。
您可能想要将以下行添加到构造函数中:
instances.add(this);
所以类似以下内容是不恰当的:
public class Foo { // multiple threads can use this public static List instances = new ArrayList (); public Foo() { ... // this "leaks" this, publishing it to other threads instances.add(this); ... // other initialization stuff } ...
另外一点复杂性是Java编译器/优化器能够重新排序构造函数内部的指令,以便它们在以后发生。 这意味着即使你做了instances.add(this);
作为构造函数的最后一行,这还不足以确保构造函数真正完成。
如果要访问此已发布对象的多个线程,则必须synchronized
它。 您不需要担心的唯一字段是final
字段,保证在构造函数完成时完成构造。 volatile
字段本身是同步的,因此您不必担心它们。
我认为以下示例说明了作者想说的内容:
public clsss MyClass { public MyClass(List> list) { // some stuff list.add(this); // self registration // other stuff } }
MyClass
在列表中注册,可以被其他线程使用。 但它在注册后运行“其他东西”。 这意味着如果其他线程在完成构造函数之前开始使用该对象,则该对象可能尚未完全创建。
它描述了以下情况:
Thread1: //we add a reference to this thread object.add(thread1Id,this); //we start to initialize this thread, but suppose before reaching the next line we switch threads this.initialize(); Thread2: //we are able to get th1, but its not initialized properly so its in an invalid state //and hence th1 is not valid Object th1 = object.get(thread1Id);
由于线程调度程序可以随时停止执行线程(甚至是像instances.push_back(this)
这样的高级指令的中途)并切换到执行不同的线程,如果不同步并行,可能会发生意外行为访问对象。
看下面的代码:
#include #include #include #include struct A { std::vector instances; A() { instances.push_back(this); } void printSize() { std::cout << instances.size() << std::endl; } }; int main() { std::unique_ptr a; // Initialized to nullptr. std::thread t1([&a] { a.reset(new A()); }); // Construct new A. std::thread t2([&a] { a->printSize(); }); // Use A. This will fail if t1 don't happen to finish before. t1.join(); t2.join(); }
由于对main()
函数的访问不同步,执行每隔一段时间就会失败。
当在完成对象A
构造并且执行线程t2
之前停止执行线程t1
时,会发生这种情况。 这导致线程t2
尝试访问包含nullptr
的unique_ptr
。
你必须确保,即使当一个线程没有初始化Object时,没有Thread会访问它(并获得NullpointerException)。
在这种情况下,它会发生在构造函数中(我想),但是另一个线程可以在它添加到列表和构造函数结尾之间访问该对象。