Hacker News new | ask | show | jobs
by mrkeen 2233 days ago
> When is concurrency in Java ever hard?

Potentially-racey stuff:

* Synchronized primitives don't compose. You can safely `synchronized get(...)` and safely `synchronized put(...)`. But their composition put(get(...)+1) isn't synchronized. And it's hard to mentally revisit it at the end of the day: if you have a class with some methods marked synchronized, nothing will tell whether you've synchronized the right methods. You just have to think it through again and hope you reach the same conclusions as before.

Other (non-racey) stuff:

* Threads are heavy, CompletableFutures are light. But CFs lack the functionality of Threads. A CF can't decide to sleep for a while, nor can it be cancelled. (As an aside, BEAM threads are super light).

3 comments

Java has a large set of higher level abstractions for concurrency. You don't have to use low level locks but you can. (And that's just Java, there's also Scala, clojure ...)
I'm pretty firmly in the "shared memory parallelism is a Good Thing" camp, but the counter argument to your point is that having a larger set of concurrency abstractions is a Bad Thing in that any particular piece of code has to consider all of the different permutations. In a shared-nothing world, there's a lot less to worry about (except occasionally performance).
The world writeable cross thread also has implications on how your GC algorithm is designed.

Erlang for instance scopes gc pools per process so short lived processes just drop the pool. Also GC of one worker doesn't stop any others. Can't remember if it even needs to be generational because the heaps are already sliced by process. It's the closest thing to heap arenas I've seen in a VM based language.

Or take Lua which is single threaded and doesn't require VM safepoints since everything is done via cooperative coroutines.

Java needs to assume worst case and as such has to be conservative in some of it's approaches.

As written here: https://github.com/l3nz/SlicedBread - "the over 400 rich pages of "Java concurrency in practice" show how hard it is to write and debug a good-mannered multithreaded application in standard Java."
... what are they?
https://docs.oracle.com/javase/8/docs/api/java/util/concurre...

And given that you didn't know that, you really need to study them.

java.util.concurrent is one of the greatest gems of software ever written.

So I describe the shortcomings of CompletableFutures, and you point out that there's a java.util.concurrent package?

Is this method one of its gems? A cancel() which doesn't cancel? https://docs.oracle.com/javase/8/docs/api/java/util/concurre...

> Synchronized primitives don't compose. You can safely `synchronized get(...)` and safely `synchronized put(...)`. But their composition put(get(...)+1) isn't synchronized.

So is there anything in java.util.concurrent that does compose? put(get()) has exactly the same problem up here at the 'high level' (of CountdownLatches and Semaphores) as it does at the 'low level' (of synchronized methods.)

Java does not allow one thread to kill another due to its shared memory concurrency model. It actually used to, but this feature was removed because it caused so many deadlocks. The reason is that killing threads won't always release monitors and locks. Lack of the feature is intentional.

You can get a lot better nonblocking support with third party libraries. Like RxJS in javascript, RxJava is almost a requirement when doing non-blocking code.

You are right that true green threads would allow thread cancellation one day. Right now, Java can't because it relies on OS threads which aren't safe to cancel. Userspace threads don't have that problem

> Java does not allow one thread to kill another due to its shared memory concurrency model. The reason is that killing threads won't always release monitors and locks.

I can't follow the reasoning here. To refute it, you'd only need to show a language with shared memory and futures with cancel(), right? Aside from that, a second shortcoming isn't a good excuse for the first.

> You can get a lot better nonblocking support with third party libraries.

I don't doubt this. Another example is vavr.io, which gives much better Lists/Optionals/Streams than the standard Java 8 ones. And Joda-Time before that.

> RxJava is almost a requirement when doing non-blocking code.

Why can't CompletableFutures do what RxJava did?

> it relies on OS threads which aren't safe to cancel. Userspace threads don't have that problem

This bit was the most confusing to me. In my shitty understanding of the model, Java Threads are based on OS threads, which is why they're relatively heavy. CFs on the other hand are are lighter and managed by the Java runtime. Why is it then that I can cancel Threads, but I cannot cancel CFs. Your explanation would seem to justify the opposite.

What do you mean the cancel method doesn't cancel ? It does cancel with a CancellationException stored in the future and an optional interrupt send to the thread running the blocking operation.
> optional interrupt send to the thread running the blocking operation

I'm not 100% sure on what you mean by an optional interrupt, but I'm guessing it's the boolean flag on the cancel(); Let's look!

    public boolean cancel(boolean mayInterruptIfRunning) {
        boolean cancelled = this.result == null && this.internalComplete(new CompletableFuture.AltResult(new CancellationException()));
        this.postComplete();
        return cancelled || this.isCancelled();
    }
Doesn't look like it's used.
> What do you mean the cancel method doesn't cancel?

Try it.

    @Test
    public void cannotCancelARunningFuture() {

        // Future that just sleeps
        CompletableFuture<Void> sleeping =
                CompletableFuture.supplyAsync(() -> {
                    int i = 0;
                    while (true) {
                        System.out.println("zzzz: " + i++);
                        threadSleep(300);
                    }
                });

        // Make sure it started
        threadSleep(1000);

        sleeping.cancel(true);
        System.out.println("Sleep was cancelled");

        threadSleep(1000);
        System.out.println("Haha no it wasn't");
    }
===============================

  zzzz: 0
  zzzz: 1
  zzzz: 2
  zzzz: 3
  Sleep was cancelled
  zzzz: 4
  zzzz: 5
  zzzz: 6
  Haha no it wasn't
original Futures can't be cancelled. CompleteableFuture can be, but only if the thread agrees to die.

You can achieve arbitrary non-blocking delays by using the cruft scheduled thread executor or doing it sanely with RxJava. Really its just dangerous to do nonblocking stuff in Java without a wrapper like RxJava. That's not a good thing, I look forward to the day there's real fibers

> > A CF can't decide to sleep for a while, nor can it be cancelled.

> How can't a CF be cancelled?

So glad you brought up the docs. CF implements cancel(boolean mayInterruptIfRunning)... which does nothing ;)

More precisely, it will not cancel a running CF. If the CF hasn't started yet, by all means cancel it. But if it's running, that cancel method does nothing.