【问题标题】:Which object should I synchronize on?我应该在哪个对象上同步?
【发布时间】:2014-03-05 15:06:13
【问题描述】:

一个 java 应用程序有 3 个对象,等等。一个 Student 对象,其中包含有关学生的所有需要​​的信息。一个包含 Student 对象的列表 ArrayList。还有一个 RandomAccessFile,用于读取学生信息并将其写入文件。

如果多个线程将访问列表、添加、删除和修改学生对象,同时写入磁盘,我应该在添加、删除、修改和写入磁盘时同步哪个对象?学生、ArrayList 还是 RandomAccessFile?如果我选择一个而不是另一个,会有性能增益/损失吗?

在我正在阅读的一本书中,同步是在 Student 对象上完成的,但我在网上找到了同步是在 RandomAccessFile 上完成的源代码?

非常感谢您的澄清

【问题讨论】:

标签: java multithreading thread-safety


【解决方案1】:

在性能方面,您同步的对象没有区别(只要确保它始终是 same 实例,在所有情况下)。不过……

您最好提供一个类的单个实例(例如 StudentData),它隐藏列表和值(学生)并提供您需要的基本操作:add(Student)、get(String studentId)?、remove( String studentId)? 等等等等(另外:如果你的 Student 对象 ids,你最好把它们放在地图中,而不是列表中。)

所有这些方法都将被声明为同步(因此将在 StudentData 实例上同步),因此每个方法都将自动完成,然后另一个线程对整个 StudentData 结构进行任何后续更改。

两个警告:

  • 如果您的 Student 对象是可变的(即具有 getter/setter),您将需要在从 get() 返回时返回 Student 实例的副本,并将数据从传入的 Student 值复制到在 update()/add() 上的 StudentData 中列出(或映射)。否则,在 StudentData 方法完成后,可以通过对 Student 的引用来修改 Student 的状态(允许在同步代码之外进行更新)。

  • 您不应该在同步代码中执行 I/O。如果你这样做了,阻塞 I/O 的东西会阻止同步锁完成,一切都会停止,直到 I/O 解除阻塞。 (事实上​​,您应该始终将同步代码中完成的工作限制在所需的最低限度。)如果您的数据集不是太大,一种方法是同步获取整个数据集的副本,然后在同步代码,将其全部写入磁盘。

  • 但是,如果您打算使用 RandomAccessFile 来管理某种磁盘上的记录结构(而不​​是数据的保存文本表示形式 - 例如 XML、JSON),为什么不只是改用 RDBMS?

【讨论】:

  • +1 你提出了一些好观点。 wrt RandomAccessFile ,这可能是他自己进行的课堂练习的要求,但如果是为了工作,那么是的,使用现有工具会更简单。
  • [[ 你不应该在同步代码中做 I/O。 ]] 一个问题 - 如果只有一个文件写入学生列表,那么 I/O 应该在哪里完成,如果不是在同步代码中。
  • @SatishMotwani - 如果这是课堂练习,您可能不想担心它。如果是针对实际项目,使用单独的命令处理线程可以解决这个问题。
  • 谢谢@DonBranson。不是真正的项目。这是为了我的学习。 [[ 使用单独的命令处理线程解决了这个问题 ]] 你的意思是说我可以有另一个 Timer 和 TimerTask 定期将 studentList 写入 RandomAccessFile。它还需要在内存中的 common studentList 上进行同步,并在同步块中执行写入文件。这是为了防止在线程写入文件时更改 studentList。
  • @SatishMotwani - 不,不是计时器。我在想thegreyblog.blogspot.com/2011/12/…之类的东西。一旦该线程运行,您的主线程或其他线程就可以将项目添加到该队列中,并且线程执行器将按照它们到达的顺序执行它们。
【解决方案2】:

考虑到应用于这些成员变量的单一职责原则,为什么要尝试使用它们中的任何一个来实现两个目的:存储数据和锁定?我会为锁创建一个单独的成员变量:

class Student {
    private String name;

    Student(String name) {
        this.name = name;
    }

    String getName() {
        return name;
    }
}

private List<Student> students = new ArrayList<Student>();
private final Object lock = new Object();

...

public void updateList(){
    Student newGuy = new Student("Joe");
    synchronized (lock){
        students.add(newGuy);
    }
}

话虽如此,Paul 提出了一个很好的观点,即不在同步块内进行 IO。将 IO 移出同步块的锁的替代方法虽然更复杂,但它是结合使用命令模式和执行队列命令的单个线程。一个命令可以将学生添加到列表中,随后的命令可以将列表写入文件。您可以拥有任意数量的线程,将新命令扔到队列中,并且它们都将按照它们到达的顺序执行。同样,在这里应用 SRP。每个命令应该只做一件事。

编辑:

因此,虽然这可能比你想在课堂上大吃一惊,但它详细说明了命令队列的想法。

每个命令都是可运行的,其中 run() 方法可以执行您想要的操作 - 添加学生、写入磁盘等等。一旦你启动了执行线程,它就坐在那里等待新命令出现在队列中。如果没有命令,它会阻塞,但其他线程可以继续执行。如果有命令,则与其他线程并行运行。

【讨论】:

  • 如果我遵循您的方法,则应在修改数据的 Student 类中声明单独的成员变量。将类编码为锁是否有意义?使它成为一个单例类。那会有什么好处,或者只是成员变量就足够了。
  • "将类编码为锁有意义吗?"如果您的意思是创建一个新类来充当锁,则不。只需使用对象。而且我不会把锁放在 Student 中,我会放在有 Student 对象列表的类中,如上所示。
  • 这就是所需的所有同步吗?或者我是否还必须同步修改 Student 类的变量的方法?
  • 您希望同步任何修改列表的块。学生是可变的吗?有必要吗?如果它是不可变的就更好了。
  • Student 是可变的,我希望它保持不变。现在,如果我使它不可变,只需要同步访问列表的任何块,如果需要修改 Student 属性,则创建一个新对象并更新引用。如果 Student 类保持可变,需要什么?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-02
  • 1970-01-01
相关资源
最近更新 更多