在Java中,如何使两个线程在无限循环中同时向列表中添加内容和从列表中删除内容



我正在编写一些应该有2个线程的代码。其中一个线程将已填充列表1中的随机图书添加到不同的图书列表2中,另一个线程从同一列表2中删除图书,这两个线程都处于无限循环中。第二个线程在启动之前会等待一段时间,所以第一个线程有足够的时间用一些要删除的书填充List2。

以下是我现在想到的:

public class BookFabric implements Runnable {
private List<Book> bookList = new ArrayList<Book>();
private List<Book> filledBookList = new ArrayList<Book>();
public BookFabric() {
}
public BookFabric(List<Book> list) {
this.filledBookList = list;
}
private void addBook(Book b) {
bookList.add(b);
}
public void addRandomBook() {
Random rand = new Random();
addBook(filledBookList.get(rand.nextInt(filledBookList.size())));
System.out.println("Book added");
}
public void deleteRandomBook() {
int r = 0;
if (!bookList.isEmpty()) {
while (r > bookList.size()) {
r = (int) ((Math.random() * 50));
}
Book b = null;
b = bookList.remove(r);
if (b.equals(null)) {
System.out.println("Random Book removed");
}
}
}
public void showBooks() {
for (Book temp : bookList) {
System.out.println(temp.getBookName());
}
}
@Override
public void run() {
while (true) {
try {
this.addRandomBook();
} catch (Exception e) {
}
}
}
}

public class Book {
String bookName;

public Book(String name) {
this.bookName = name;
}
public String getBookName() {
return bookName;
}


}


public class Thread1 extends BookFabric implements Runnable {

BookFabric b; 
public Thread1(BookFabric bF) {
this.b = bF;
}

@Override
public void run() {

while(true) {
try {
Thread.sleep(5);
b.deleteRandomBook();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
} 

}

public class Main {

public static void main(String[] args) {
List<Book> bookList = new ArrayList<Book>(); 

Book b1 = new Book("first book");
Book b2 = new Book("second book ");
Book b3 = new Book("third book");

bookList.add(b1);
bookList.add(b2);
bookList.add(b3);

BookFabric b = new BookFabric(bookList);

Thread1 t = new Thread1(b);

Thread t1 = new Thread(b);
Thread t2 = new Thread(t);

t1.start();
t2.start();
}
}

主方法中的t1按预期工作。它在无限循环中创建随机书籍。但由于某种原因,t2甚至根本没有开始,也没有从列表中删除任何一本随机创建的书。我对这件事几乎没有什么想法。我会非常感谢任何关于我可以做什么不同的想法。

Java内存模型(JMM)是这里的相关文档。

主方法中的

t1按预期工作

不,不是。这是JMM的第一个问题:JMM规定JVM可以自由做任何事情的区域。这意味着JVM的输出实际上是不确定的:JVM可以自由地总是做A,或者总是做B,或者扔硬币,或者看月相等等,而且它们仍然遵守规范。这不仅仅是学术性的:流行的JVM impl实际上都是这样做的,因为这样它们可以更高效地运行。

这意味着,实际上你不能通过测试你的代码来检测你搞砸了什么(依靠JVM的选择——这很糟糕),如果它现在看起来可以工作,就不能保证明天可以工作。

基本上,JMM允许JVM有时抛出一枚邪恶的硬币:邪恶是指在开发和测试周期中,它每次都会以相同的方式翻转,然后在进行重要演示时以另一种方式翻转。

玩游戏的唯一方法是,以确保邪恶的硬币永远不会被翻转

要做到这一点,你需要记住以下规则:

除非首先设置了HB/HA关系,否则决不能访问线程之间共享的所有字段,这是整个并发进程的只读概念

换言之,这里有一个字段(bookList,或者更具体地说,bookList所指向的arraylist中的后备数组),当另一个线程正在运行时,它也想要读取它,因此,无论测试是否如此,都会发生邪恶的共翻,并且该代码被破坏。你会"随机"观察到一些或所有的变化,或者至少JVM可以随意地让线程B看看线程A做了什么或没有做什么,你不能依赖任何一个。

HB/HA代表"发生在之前/发生在之后"。JMM描述了某些情况,JVM保证不可能观察到由Happens After行中的Happens Before行引起的缺少的更改。

没有HB/HA,你就没有这样的保证。举个例子来说明这一点:

class State {
static int a = 0, b = 0;
}
class T1 implements Runnable {
public void run() {
Thread.sleep(randomAmount);
State.a = 10;
State.b = 20;
}
}
class T2 implements Runnable {
public void run() {
Thread.sleep(randomAmount);
int myB = State.b;
int myA = State.a;
System.out.println(myA + " " + myB);
}
}

如果运行这段代码,启动两个线程,那么这些输出中的任何一个都是"合法的"——这不是一个有缺陷的JVM。您的代码是有缺陷的代码,请注意JVM永远不会产生一个或多个这样的答案也是合法的。这真的应该说明邪恶硬币类比的意义——你不能先写不解决HB/HA的软件,因为它是一个随机的集群炸弹:

  • 0 0
  • 10 20
  • 10 0
  • 0 20

请特别注意最后一个。那个确定的似乎是不可能的。线程1总是在写b之前写a,线程2在写a之前读b,所以可能是b读了20,但不知何故a仍然是0吗然而,这是合法的,原因是JVM喜欢快速运行,而一些JVM实现(实际上大多数)将并行运行代码。JVM只是保证您不能观察并行运行的事情,除非您使用计时来观察(并且根本不能保证计时)。如果你不能观察到它,出于速度的原因,它会并行运行。

因此,JVM可以自由地并行运行a=10和b=20。

那么,你是如何建立HB/HA的呢?这是一个相当复杂的话题;它涉及基元synchronizedvolatile,或者使用与线程相关的Java API,例如AtomicLongConcurrentHashMapsomeThread.start()等。文档会说(他们建立HB/HA的原因通常是因为他们的实现在后台使用同步/易失性)。

您没有做任何这些,因此没有HB/HA关系,因此此代码是完全损坏的,测试无法可靠地捕获。

然而,在实践中,尝试手动管理这些东西是困难的,而且容易出错。很明显,你所犯的任何错误都是不稳定的。

使用更适合它的类型。使用java.util.concurrent包。例如,如果您希望一个线程添加书籍,而另一个线程删除书籍,则可能需要查看BlockingQueue。

删除一本"随机"的书相当复杂,除非你使用锁定世界模型,否则似乎无法实际解决。这也很有效,您只需要在一个同步块中挂起与bookList的所有交互。但这在很大程度上违背了线程的观点,因为实际上只有一个线程在运行(另一个线程正在等待获取同步锁)。

教训是:

  • 这是火箭科学
  • 测试不会发现错误
  • 如果您真的想这样做,请阅读JMM。了解建立HB/HA并应用它们的方法。这需要知道synchronizedvolatile实际做了什么,而且学习起来很简单。如果你误解了一些事情,但你没有意识到你误解了,那么你很可能永远不会知道,直到后来你的代码有时似乎随机失败。这不是一个第一年java新手应该解决的问题
  • 对同一字段的任何并发访问(从不同线程交错读取/写入)都不会以您希望的方式工作,除非您非常小心地注意并设置HB/HA以使其可靠
  • 尽量避免并发访问字段,这样做并不难:
  • 使用j..concurrent包并使用其中的内容,或者
  • 使用擅长的机制安排线程之间的通信,例如数据库(具有事务)或消息队列(如RabbitMQ)

最新更新