以线程安全的方式创建对象

5

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

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

instances.add(this);

但是,此时其他线程可以使用instances访问对象,在对象构造完成之前就访问该对象。

是否有人能用其他词语或更易理解的例子来表达相同的概念?

提前感谢。


这里有一个非常相似的问题:https://dev59.com/E3E85IYBdhLWcg3w_I38 - Jan Krakora
8个回答

5
  1. Let us assume, you have such class:

    class Sync {
        public Sync(List<Sync> list) {
            list.add(this);
            // switch
            // instance initialization code
        }
    
        public void bang() { }
    }
    
  2. and you have two threads (thread #1 and thread #2), both of them have a reference the same List<Sync> list instance.

  3. Now thread #1 creates a new Sync instance and as an argument provides a reference to the list instance:

    new Sync(list);
    
  4. While executing line // switch in the Sync constructor there is a context switch and now thread #2 is working.

  5. Thread #2 executes such code:

    for(Sync elem : list)
        elem.bang();
    
  6. Thread #2 calls bang() on the instance created in point 3, but this instance is not ready to be used yet, because the constructor of this instance has not been finished.

因此,当调用构造函数并传递一个被几个线程共享的对象的引用时,你必须非常小心。在实现构造函数时,必须记住提供的实例可以在几个线程之间共享。

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());
         }
    }
}

现在假设您正在一个名为"X"的线程中创建House对象。当执行该线程的最后一行代码如下时,
listOfHouse.add(this); 

上下文被切换(这个对象的引用已经添加到了列表listOfHouse中,但是对象创建还没有完成),切换到另一个线程“Y”运行,

printsHouses();

如果在它里面!然后printHouses()会看到一个仍未完全创建的对象,这种不一致性被称为泄漏


2

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

当构建一个将在多个线程之间共享的对象时,请务必小心,确保对该对象的引用不会提前“泄漏”。

在构造对象时,您需要确保其他线程无法在它完全构造之前访问此对象。这意味着在构造函数中,您不应该:

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

You might be tempted to add the following line to your constructor:

   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字段本身已同步,所以你不必担心它们。


在代码示例中,如何以同步的方式完成instances.add(this); - Weishi Z

2

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


1
我认为以下示例说明了作者想要表达的意思:
public clsss MyClass {
    public MyClass(List<?> list) {
        // some stuff 

        list.add(this); // self registration

        // other stuff 
    }
}

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


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); 

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();
}

由于在main()函数中访问a未同步,因此执行会偶尔失败。

当线程t1在完成对象A的构建之前停止执行,并且线程t2被执行时,就会发生这种情况。这导致线程t2尝试访问包含nullptrunique_ptr<A>


-1

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

在这种情况下,它可能会发生在构造函数中(我猜测),但是另一个线程可能会在将其添加到列表和构造函数结束之间访问该对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接