JUnit 5's @ResourceLock takes a ResourceAccessMode of READ or READ_WRITE. What is the difference in what JUnit allows to run concurrently, and how do you choose?
answer
- READ = shared, READ_WRITE = exclusive (default)
- readers overlap readers only
- mutation in @BeforeEach counts as writing
- same key ⇒ serialised even for disjoint sub-resources
- wrong-direction READ silently restores the race
basics
~20 sREAD is a shared lock: any number of tests declaring READ on the same key may run at once. READ_WRITE is exclusive: while a test holds it, no other test with that key runs, whether READ or READ_WRITE. Use READ when the test only observes the resource, READ_WRITE when it mutates it. READ_WRITE is the default.
solid answer
~50 sIt is the classic readers–writer distinction applied to test scheduling. - `ResourceAccessMode.READ` — a **shared** lock. Many tests holding READ on the same key run concurrently with each other, because observing a resource does not disturb another observer. - `ResourceAccessMode.READ_WRITE` — an **exclusive** lock, and the default when you write `@ResourceLock("key")` with no mode. While it is held, no other test declaring that key runs at all, readers included. Choosing is about intent: does this test *change* the resource? A test that asserts on the current default time zone declares READ; a test that calls `TimeZone.setDefault` declares READ_WRITE. Getting it wrong in the safe direction (over-declaring READ_WRITE) costs throughput; getting it wrong in the unsafe direction (declaring READ from a test that mutates) reintroduces the race silently, because readers overlap. Both are declarative — JUnit enforces the schedule, it never checks what the test actually touches.
code
java · 25 linesimport org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;
class TimeZoneSensitiveTest {
@Test
@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ)
void formatsUsingCurrentDefaultZone() {
assertNotNull(TimeZone.getDefault().getID());
}
@Test
@ResourceLock(value = Resources.TIME_ZONE, mode = ResourceAccessMode.READ_WRITE)
void formatsMidnightRolloverInTokyo() {
TimeZone original = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
try {
assertEquals("00:00", Clock.midnightLabel());
} finally {
TimeZone.setDefault(original);
}
}
}go deeper
State that READ is shared, READ_WRITE is exclusive and is the default, and that mutation means READ_WRITE.
Give the compatibility matrix, stress that setup and teardown count as part of the locked node, and note that the mode is a promise JUnit does not verify.
Discuss throughput: readers overlapping is the whole point, key granularity is a design choice, and long-held exclusive locks on a hot key become a queue.
Frame it as the concurrency contract of the suite — who defines keys, what granularity the organisation standardises on, and when contention justifies partitioning a key.
## The readers–writer model `@ResourceLock` implements a standard shared/exclusive lock keyed by a string. The access mode says which side of that lock the test wants: | Held by | Requested READ | Requested READ_WRITE | |---|---|---| | nothing | runs | runs | | READ (one or more tests) | runs concurrently | waits | | READ_WRITE | waits | waits | So READ holders are compatible with each other and with nobody else; a READ_WRITE holder is compatible with nobody. The rationale is the same as in any concurrent data structure: concurrent observation of unchanging state is safe, concurrent mutation, or mutation during observation, is not. `READ_WRITE` is the default, so `@ResourceLock("app.database")` means exclusive access. That default is deliberately the conservative one — if you forget to think about the mode, you get correctness rather than a subtle race. ## Choosing the mode The test to apply is simple: **does this test change the resource at any point during its execution, including setup and teardown?** If yes, `READ_WRITE`. If it only observes, `READ`. The important subtlety is that setup and teardown count. A test whose body merely asserts on a value, but whose `@BeforeEach` sets a system property and whose `@AfterEach` clears it, is a *writer*. The lock spans those callbacks precisely so the mutation is covered — but only if you declared `READ_WRITE`. Declaring `READ` on such a test means several of them run at once, each stamping and clearing the same property, which is exactly the failure mode you were trying to prevent. A second subtlety: 'resource' means whatever the key names, and the key's granularity is your choice. If tests A and B write disjoint system properties, they could in principle run together — but if they both declare `Resources.SYSTEM_PROPERTIES` with `READ_WRITE`, JUnit serialises them because the key is the same. You can express the finer granularity with a more specific key of your own (e.g. `"sysprop:feature.newCheckout"`), at the price of a convention only your team knows. Use the canonical constant when other people's tests might also touch the resource, and a finer key when the resource really is partitionable and the contention is measurable. ## What the modes do not do - They do not inspect the test. JUnit has no idea whether a `READ`-declaring test actually mutates something. The annotation is a promise the author makes and JUnit schedules on. - They do not create memory visibility guarantees for your production code. They control *scheduling* of tests; the shared object still needs to be safely published if the code under test uses it across threads. - They do not order tests. Two READ_WRITE tests on the same key are serialised, but in an unspecified order. If a test only passes when it runs after another, you have an ordering dependency, and locks are not the tool for that. ## Throughput consequences The reason READ exists is throughput. In a suite where twenty tests read a shared fixture and two mutate it, declaring all twenty as `READ` lets them overlap freely and only pauses them around the two writers. Declaring all twenty `READ_WRITE` — the lazy option — turns a mostly-parallel section into a fully sequential one, and on a big suite that difference is easily minutes. The cost model to remember: a hot key with an exclusive holder is a queue. The queue length is driven by how many tests declare the key and how long each holds it. So the two levers are declaring READ where truthful, and keeping the locked node fast — which argues again for locking individual methods rather than entire classes. ## Practical checklist 1. Mutates the resource anywhere in setup, body or teardown → `READ_WRITE`. 2. Purely observes it → `READ`, and check that no shared fixture in a base class writes it behind your back. 3. Unsure → `READ_WRITE`, and leave a comment; the safe direction costs time, not correctness. 4. Many readers, few writers → make sure the readers really are declared `READ`, or the mode distinction buys you nothing.
- A test's body only asserts, but its @BeforeEach sets a system property. Which mode should it declare?READ_WRITE. The lock covers the whole node including its lifecycle callbacks, and the callback mutates the resource, so the test is a writer regardless of what the body does. Declaring READ would let several such tests overlap, each setting and clearing the same property, which is precisely the race the lock was meant to prevent.
- Two tests write completely different system properties. Will declaring Resources.SYSTEM_PROPERTIES with READ_WRITE on both let them run concurrently?No — they share the key, so JUnit serialises them even though the underlying sub-resources are disjoint. If that contention is measurable you can use a finer-grained key of your own, such as one per property name, but you lose interoperability with any other test that locks the canonical constant. That trade-off is worth making only when profiling shows the key is actually hot.
A library reference book: many people can read it at the same table at once, but the moment someone wants to annotate it, everyone else has to put it down and wait.
saying these in an interview costs you the question
- Thinking READ locks are ignored entirely and provide no protection
- Declaring READ from a test that mutates the resource in setup or teardown
- Believing two READ_WRITE tests on the same key run in a defined order
- Assuming JUnit verifies that a READ test does not write
- Expecting different sub-resources under the same key to run concurrently