An example of multiple solutions selected from discuss.
Java
volatile
Using volatile
variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What’s more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.
volatile
has semantics for memory visibility. Basically, the value of a volatile field becomes visible to all readers (other threads in particular) after a write operation completes on it. Without volatile, readers could see some non-updated value.
In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn’t happen at all. No side effects of an atomic action are visible until the action is complete.
- Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
- Reads and writes are atomic for all variables declared
volatile
(including long and double variables).
[Java] Simple, yet effective. Using volatile without lock
1 | class Foo { |
36 / 36 test cases passed.
Runtime: 9 ms, faster than 88.93% of Java online submissions for Print in Order.
Memory Usage: 35.9 MB, less than 100.00% of Java online submissions for Print in Order.
Semaphore
Conceptually, a semaphore
maintains a set of permits. Each acquire()
blocks if necessary until a permit is available, and then takes it. Each release()
adds a permit, potentially releasing a blocking acquirer. However, no actual permit objects are used; the Semaphore
just keeps a count of the number available and acts accordingly.
Semaphores
are often used to restrict the number of threads that can access some (physical or logical) resource.
1 | import java.util.concurrent.Semaphore; |
36 / 36 test cases passed.
Runtime: 9 ms, faster than 89.05% of Java online submissions for Print in Order.
Memory Usage: 35.7 MB, less than 100.00% of Java online submissions for Print in Order.
synchronized
The Java programming language provides two basic synchronization idioms: synchronized methods
and synchronized statements
.
Unlike synchronized methods, synchronized statements
must specify the object that provides the intrinsic lock.
Synchronized statements
are also useful for improving concurrency with fine-grained synchronization.
[Java] Basic version with just a synchronized block & a counter
1 | class Foo { |
36 / 36 test cases passed.
Runtime: 9 ms, faster than 89.16% of Java online submissions for Print in Order.
Memory Usage: 36 MB, less than 100.00% of Java online submissions for Print in Order.
CountDownLatch
A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
A CountDownLatch
is initialized with a given count. The await
methods block until the current count reaches zero due to invocations of the countDown()
method, after which all waiting threads are released and any subsequent invocations of await return immediately. This is a one-shot phenomenon – the count cannot be reset.
A CountDownLatch is a versatile synchronization tool and can be used for a number of purposes. A CountDownLatch
initialized with a count of one serves as a simple on/off latch, or gate: all threads invoking await wait at the gate until it is opened by a thread invoking countDown()
. A CountDownLatch initialized to N can be used to make one thread wait until N threads have completed some action, or some action has been completed N times.
A useful property of a CountDownLatch
is that it doesn’t require that threads calling countDown
wait for the count to reach zero before proceeding, it simply prevents any thread from proceeding past an await until all threads could pass.
Another typical usage would be to divide a problem into N parts, describe each part with a Runnable that executes that portion and counts down on the latch, and queue all the Runnables to an Executor. When all sub-parts are complete, the coordinating thread will be able to pass through await.
1 | import java.util.concurrent.CountDownLatch; |
36 / 36 test cases passed.
Runtime: 9 ms, faster than 89.16% of Java online submissions for Print in Order.
Memory Usage: 35.4 MB, less than 100.00% of Java online submissions for Print in Order.
Phaser
A reusable synchronization barrier, similar in functionality to CyclicBarrier and CountDownLatch but supporting more flexible usage.
Methods arrive()
and arriveAndDeregister() record arrival. These methods do not block, but return an associated arrival phase number; that is, the phase number of the phaser to which the arrival applied.
Method awaitAdvance(int)
requires an argument indicating an arrival phase number, and returns when the phaser advances to (or is already at) a different phase.
Java Phaser solution, very fast
1 | import java.util.concurrent.Phaser; |
36 / 36 test cases passed.
Runtime: 10 ms, faster than 55.92% of Java online submissions for Print in Order.
Memory Usage: 36 MB, less than 100.00% of Java online submissions for Print in Order.