【问题标题】:ArrayList<object> manipulation using Thread in Java在 Java 中使用 Thread 操作 ArrayList<object>
【发布时间】:2015-08-04 17:03:32
【问题描述】:

我试图通过在 java 中使用 Thread 来解决有关 list.addlist.remove 的问题。 假设我们在玩 Stack

这是我的堆栈定义类..

import java.util.ArrayList;

public class Stack {

    private int size;
    private int maxSize;

    private final ArrayList<Object> list;

    public Stack(int size) {
        this.size = 0;
        this.maxSize = size;
        this.list = new ArrayList<Object>(size);
    }

    public boolean push(Object o) {
        if (size >= maxSize) {
            return false;
        }

        this.list.add(0, o);
        this.size++;
        return true;
    }

    public Object pop() {
        Object o;
        if (this.size == 0) {
            return null;
        }

        o = this.list.remove(0);
        this.size--;
        return o;
    }

    public int size() {
        return this.size;
    }
}

这是我们如何在 Java 中使用线程中的堆栈

final Stack stack = new Stack(4);
for(int i = 0; i < 10000; i++) {
    final String data = "hello " + i;
    final int x = i;
    new Thread(new Runnable() {
        public void run() {
            if(x % 2 == 0) {
                System.out.println(stack.push(data));
            } else {
                System.out.println(stack.pop());
            }
        }
    }).start();
}

所以基本上我们只需要创建 10000 个线程来操作 Stack 对象。 stack.push 结果为真(如果堆栈尚未满)或假(如果堆栈已满) stack.pop 如果堆栈为空,则返回 null

问题是:上面的 Stack 实现有什么问题以及如何解决?

到目前为止,我的分析是线程如何在 java 中运行。线程并行运行而不是顺序运行。我试图执行程序,有时会弹出异常IndexOutOfBounds。如果我的分析是真的(或接近),有没有办法避免异常?也许在 Stack 类中包含一些检查方法?

如果我的分析是错误的,那么上面的实现有什么问题?以及如何解决?

【问题讨论】:

  • 您的堆栈实现不是线程安全的。线程安全就是保护可变状态,以便对象在并发访问时经历一致、可靠的状态转换。不幸的是,您的课程会受到竞争条件的影响,这将使您的对象处于未定义或不一致的状态。使用锁定来强制线程互斥是一种确保原子状态转换的简单(如果不是高性能)方法。

标签: java multithreading object arraylist


【解决方案1】:

上面的 Stack 实现有什么问题

当只有一个线程可以访问堆栈对象时,您的实现是好的。但是,如果至少 2 个线程可以在同一个堆栈上执行弹出和推送操作 - data races 可能会发生。 java多线程的主要描述见JSR-133

pop方法中的这段代码想象一下情况:

if (this.size == 0) {
    return null;
}
o = this.list.remove(0);
  1. 如果条件,大小为 1,则执行第一个线程。
  2. 第二个线程执行相同的 if 条件 - 大小仍为 1。

  3. 第一个线程尝试从列表中删除索引为 0 的元素 - 成功,大小变为 0。

  4. 当大小为 0 时,第二个线程尝试从列表中删除索引为 0 的元素 - 引发异常。

您需要确保某些事件发生在其他事件之前。一种方法是同步您的poppush 方法。这可以通过在方法返回类型前添加synchronized 关键字轻松完成。

public synchronized boolean push(Object o) {...}

public synchronized Object pop() { ...}

这两种方法都是synchronized 在同一个对象上 - this。因此,当一个线程通过执行poppush 获得this 锁时,没有其他线程可以进入被同一个this 对象锁定(同步)的代码块或方法。使用这些方法是完全线程安全的。

如果您使用一些同步集合而不是常规的ArrayList,您仍然会遇到麻烦,因为您的逻辑依赖于size 变量并且之前的错误场景仍然有效。如果您需要并发 Stack 实现,您可以使用 java.util.concurrent 包中的 LinkedBlockingDeque 类。它也会更有效,因为在ArrayList 的开头添加元素的成本非常高。但是如果你想自己实现它,你可以将同步修饰符添加到 poppush 方法并将你的列表更改为 LinkedList

【讨论】:

  • 我将复制其他名为 erhun 的用户在其他问题线程中所说的内容。 “好吧,但是如果你有两种方法:addFinisher 和 delFinisher 怎么办?这两种方法都是线程安全的,但由于它们都访问同一个 ArrayList,你仍然会遇到麻烦”。如您所见,我的堆栈中也有 2 个方法。对此有何建议?非常感谢
  • 编辑:在这种情况下使用 syncronizedlist 或 lock 怎么样?非常感谢
【解决方案2】:

@ka4eli 告诉你为什么你的 Stack 类不是线程安全的,但这也是错误的:

if(x % 2 == 0) {
    System.out.println(stack.push(data));
} else {
    System.out.println(stack.pop());
}

即使你的堆栈是完全线程安全的,在 else 情况下你也一定会得到 NullPointerExceptions。

非同步线程可以任意顺序运行。仅仅因为您的程序在启动线程 1 之前启动线程 0 并不意味着线程 1 不会在线程 0(或任何其他线程)推送某些内容之前尝试从堆栈中弹出字符串。

【讨论】:

  • 但是那些块访问同一个 Stack 对象,对吧?因此,如果线程 1 运行 else 块(pop),线程将访问堆栈对象并尝试使用其资源,但是由于线程 0 以同步模式使用它,所以线程 1 必须等到线程 0 完成它的工作,对吧?
  • @Ship.Asriadie,不,看看 OP 的实现:如果堆栈为空,stack.pop() 返回null
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-02
  • 1970-01-01
  • 2015-06-29
相关资源
最近更新 更多