5

直接从这个网站,我遇到了以下关于创建对象线程安全的描述。

警告:在构造将在线程之间共享的对象时,要非常小心,不要对对象的引用过早地“泄漏”。例如,假设您要维护一个名为实例的列表,其中包含类的每个实例。您可能想在构造函数中添加以下行:

实例.add(this);

但是在对象的构造完成之前,其他线程可以使用实例来访问对象。

有没有人能够用其他词或另一个更容易理解的例子来表达相同的概念?

提前致谢。

4

8 回答 8

5
  1. 让我们假设,您有这样的课程:

    class Sync {
        public Sync(List<Sync> list) {
            list.add(this);
            // switch
            // instance initialization code
        }
    
        public void bang() { }
    }
    
  2. 并且您有两个线程(线程#1 和线程#2),它们都引用了同一个List<Sync> list实例。

  3. 现在线程 #1 创建一个新Sync实例,并作为参数提供对该list实例的引用:

    new Sync(list);
    
  4. // switch在构造函数中执行行时,Sync有一个上下文切换,现在线程 #2 正在工作。

  5. 线程 #2 执行这样的代码:

    for(Sync elem : list)
        elem.bang();
    
  6. 线程#2 调用bang()了在第 3 点创建的实例,但是这个实例还没有准备好使用,因为这个实例的构造函数还没有完成。

所以,

  • 在调用构造函数并传递对在几个线程之间共享的对象的引用时必须非常小心
  • 在实现构造函数时,您必须记住提供的实例可以在几个线程之间共享
于 2013-05-23T15:41:34.303 回答
2

这里有很多好的数据,但我想我会添加更多信息。

在构造将在线程之间共享的对象时,要非常小心,不要对对象的引用过早“泄漏”。

在构造对象时,需要确保其他线程无法访问该对象,然后才能完全构造它。这意味着在构造函数中你不应该,例如:

  • 将对象分配给static其他线程可以访问的类上的字段。
  • 在构造函数中的对象上启动一个线程,该线程可以在对象完全初始化之前开始使用对象中的字段。
  • 将对象发布到集合中或通过任何其他允许其他线程在对象可以完全构造之前看到该对象的机制。

您可能想在构造函数中添加以下行:

   instances.add(this);

所以像下面这样的东西是不合适的:

  public class Foo {
      // multiple threads can use this
      public static List<Foo> instances = new ArrayList<Foo>();
      public Foo() {
         ...
         // this "leaks" this, publishing it to other threads
         instances.add(this);
         ...
         // other initialization stuff
      }
      ...

另一个复杂性是 Java 编译器/优化器能够重新排序构造函数内部的指令,以便它们在以后发生。这意味着即使你instances.add(this);作为构造函数的最后一行,这也不足以确保构造函数真的完成了。

如果多个线程要访问这个发布的对象,它必须是synchronized. 您不需要担心的唯一字段final是在构造函数完成时保证完成构造的字段。 volatile字段本身是同步的,因此您不必担心它们。

于 2013-05-23T16:07:56.033 回答
2

这是您的明确示例:

假设有一个名为House

class House {
    private static List<House> 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.

于 2013-05-23T15:48:46.743 回答
2

线程 A 正在创建对象 A,在创建对象 A 的中间(在对象 A 的构造函数的第一行)有上下文切换。现在线程 B 正在工作,线程 B 可以查看对象 A(他已经有引用)。然而,对象 A 尚未完全构造,因为线程 A 没有时间完成它。

于 2013-05-23T15:33:14.083 回答
1

我认为以下示例说明了作者想说的内容:

public clsss MyClass {
    public MyClass(List<?> list) {
        // some stuff 

        list.add(this); // self registration

        // other stuff 
    }
}

MyClass自身注册在列表中,可供其他线程使用。但它在注册后运行“其他东西”。这意味着如果其他线程在完成其构造函数之前开始使用该对象,则该对象可能尚未完全创建。

于 2013-05-23T15:35:30.273 回答
0

它描述了以下情况:

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); 
于 2013-05-23T15:38:02.663 回答
0

由于线程调度程序可以随时停止线程的执行(甚至在执行高级指令的中途instances.push_back(this))并切换到执行不同的线程,如果您不同步对对象的并行访问,可能会发生意外行为。

看下面的代码:

#include <vector>
#include <thread>
#include <memory>
#include <iostream>

struct A {
    std::vector<A*> instances;
    A() { instances.push_back(this); }
    void printSize() { std::cout << instances.size() << std::endl; }
};

int main() {
    std::unique_ptr<A> 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();
}

由于对ain main()-function 的访问不同步,因此每隔一段时间执行就会失败。

当线程的执行t1在完成对象的构造之前停止At2改为执行线程时,就会发生这种情况。这会导致线程t2尝试访问unique_ptr<A>包含 a 的nullptr.

于 2013-05-23T16:33:26.240 回答
-1

您只需要确保,即使一个线程尚未初始化对象,也没有线程会访问它(并获得 NullpointerException)。

在这种情况下,它会发生在构造函数中(我想),但是另一个线程可以在它添加到列表和构造函数末尾之间访问那个对象。

于 2013-05-23T15:31:04.603 回答