如何在Java中创建阻塞线程安全的流?

时间:2019-05-11 13:12:05

标签: java thread-safety java-stream

我想创建一个java.util.stream.Stream,该块在terminal actions上阻塞,并使用任意对象进行同步。 Stream的方法本身必须以透明的方式进行操作,以便我可以安全地将Stream传递给不知道同步的代码。

考虑以下示例:

void libraryMethod(Stream<Whatever> s) {
    for (int i = 0; i < 10000000; ++i) { /* ... */ }
    s.filter(Library::foo).forEach(Library::bar);
}

/* Elsewhere in my code */

Set<Whatever> aSet = Collections.synchronizedSet(...);
/* ... */
libraryMethod(new MyBlockingStream<>(set.stream(), set));

在执行forEach之前,我希望aSet由MyBlockingStream本身获取 并仅在forEach终止时释放。这应该保证我不会得到ConcurrentModificationException,因为其他线程可能要修改该集合。我无法在整个synchronized (aSet)上使用libraryMethod,因为那样会阻塞aSet所需的时间更长。

是否可以这样做?如果是这样,是否有任何现有的实现方式可以执行此操作,或者我必须自己编写它?

注意:这个问题与Stream如何执行动作无关-我不在乎它是否并行。我知道存在本质上不可同步的iterator()spliterator()方法。我也不在乎他们。

2 个答案:

答案 0 :(得分:0)

您可以使用锁。

public class Example {
    public static ReentrantLock lock = new ReentrantLock();

    private static void sleep() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static Runnable createRunnable() {
        return () -> {
            try {
                Arrays.asList("a", "b", "c").stream().forEach(e -> {
                    if (!lock.isHeldByCurrentThread())
                        lock.lock();

                    sleep();
                    System.out.println(String.format("thread %s with element %s", Thread.currentThread().getId(), e));
                });
            } finally {
                if(lock.isHeldByCurrentThread())
                    lock.unlock();
            }
        };
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(createRunnable());
        Thread t2 = new Thread(createRunnable());
        Thread t3 = new Thread(createRunnable());

        t1.start();
        t2.start();
        t3.start();

        System.out.println("join all threads");
        t1.join();
        t2.join();
        t3.join();
    }
}

首先到达forEach的线程将锁定所有其他线程。

在这种情况下的输出是

join all threads
thread 16 with element a
thread 16 with element b
thread 16 with element c
thread 15 with element a
thread 15 with element b
thread 15 with element c
thread 14 with element a
thread 14 with element b
thread 14 with element c

编辑1

正如@Holger指出的,如果流被嵌套,这将不起作用。内部流将太早释放锁定。

答案 1 :(得分:0)

这就是我最终要做的:https://gist.github.com/OLEGSHA/bda28ffaa4b24e64b94a8c30c3ad9b0c。这些流包装器synchronize使用提供的对象public void forEach(Consumer<? super T> action) { synchronized (monitor) { parent.forEach(action); } } 进行所有终端操作,并包装来自中间操作的流。

这本质上是

Stream

用于IntStream界面中的每个终端操作。包括LongStreamDoubleStream和{{1}}版本。

我认为必须有更好的解决方案。