Geschiedenis van de vraag
Voor Java 5 was thread-coördinatie afhankelijk van primitieve methoden zoals Thread.suspend (verouderd vanwege inherente vastlooprisico's) of Object.wait/notify, die strikte bezit van monitors vereisten en te maken hadden met verloren wakker worden als de notificatie vóór de wacht kwam. Met de introductie van java.util.concurrent in Java 5 (JSR 166), werd LockSupport ontworpen als een laag-niveau deblokkering primitief om de constructie van high-performance synchronizers zoals AbstractQueuedSynchronizer mogelijk te maken, zonder de baggage van intrinsieke sloten.
Het probleem
In gelijktijdige programmering doet zich een klassieke raceconditie voor wanneer een signalerende thread de unpark-mechanisme voordat de doelthread daadwerkelijk parkeert. Met traditionele conditievariabelen zou dit signaal verloren gaan, waardoor de doelthread onbepaald in slaap valt. Een naïeve oplossing zou een tellingSemaphore kunnen gebruiken om vergunningen te accumuleren, maar dit introduceert onnodige complexiteit en potentiële hulpbronlekken als de producent de consument voorbijloopt.
De oplossing
LockSupport maakt gebruik van een niet-accumulerende, enkele-bit vergunning die aan elke thread is gekoppeld. Deze vergunning fungeert als een wegpas die kan worden weggegooid, specifiek voor de thread:
Omdat de vergunning niet cumulatief is (het verzadigt op 1), voorkomt het geheugenlekken door overmatige unpark, terwijl het garandeert dat één unpark die vóór de park is afgegeven, wordt herinnerd, waardoor het probleem van verloren wakker worden wordt geëlimineerd door een happens-before relatie.
import java.util.concurrent.locks.LockSupport; public class PermitExample { public static void main(String[] args) throws InterruptedException { Thread worker = new Thread(() -> { System.out.println("Worker: Eerste werk..."); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("Worker: Poging om te parkeren..."); LockSupport.park(); System.out.println("Worker: Succesvol ongeparkeerd!"); }); worker.start(); // Signaal voordat de worker daadwerkelijk parkeert Thread.sleep(50); System.out.println("Main: Unpark aanroepen voordat de worker parkeert"); LockSupport.unpark(worker); worker.join(); } }
Probleembeschrijving
Bij het ontwerpen van een order matching engine voor een high-frequency trading systeem, hadden we een backpressure mechanisme nodig waar consumentthreads de verwerking konden pauzeren wanneer de inkomende wachtrij de capaciteit bereikte, zonder sloten te houden die producenten zouden verhinderen de toestand van de wachtrij te controleren. Standaard ReentrantLock met Condition creëerde competitie op de slot van de wachtrij tijdens signaling, en Object.wait/notify had te maken met het risico van verloren wakker worden tijdens races met hoge omloopsnelheid.
Verschillende oplossingen overwogen
1. Object.wait/notifyAll
Deze aanpak gebruikte de intrinsieke lock van de wachtrij. Voordelen: Eenvoudig te implementeren met standaard monitors. Nadelen: Vereiste dat de producent de monitor verwerft om notify aan te roepen, wat een serialisatie bottleneck creëerde. Nog erger, als een producent notify aanriep tijdens het korte venster tussen het controleren van de grootte van de wachtrij door de consument en het aanroepen van wait, ging het signaal verloren, wat leidde tot permanente vastloop van de consument.
2. ReentrantLock met meerdere Conditions
We probeerden aparte voorwaarden te gebruiken voor "vol" en "leeg" staten. Voordelen: Flexibeler dan intrinsieke sloten, waardoor selectieve wakker worden mogelijk was. Nadelen: Vereiste nog steeds lock acquisitie voor signaling (signalAll), en de complexiteit van het correct overdragen van threads tussen voorwaarde wachtrijen introduceerde onderhoudsbelasting zonder de fundamentele lock overhead op te lossen.
3. LockSupport met expliciete atomische staat
De gekozen oplossing gebruikte een AtomicBoolean om "toestemming om door te gaan" weer te geven en LockSupport voor blokkeren. Wanneer de wachtrij vol was, stelde de consument atomisch een "needsParking" vlag in en parkeerde toen. Producenten, na het verwijderen van een item, controleerden de vlag en noemden unpark als deze was ingesteld. Voordelen: Signaling vereiste geen sloten, waardoor competitie tijdens wakker worden werd geëlimineerd. Het één-bit vergunningmodel zorgde ervoor dat zelfs als de producent unpark nanoseconden voor de consument parkeerde aanriep (door CPU-scheduling), het wakker worden niet verloren was.
Gekozen oplossing en resultaat
We selecteerden de LockSupport aanpak. Door het signaling mechanisme los te koppelen van de structurele lock van de wachtrij, reduceerden we de latency van de producent met 40% onder zware belasting en elimineerden we de verloren wakker worden scenario's die werden waargenomen tijdens stress testing. Het expliciete staatbeheer (dubbel controleren van de voorwaarde na unpark) verzekerde correctheid ondanks het spurious wakeup contract van park().
Verliest LockSupport.park het bezit van monitors die door de thread worden vastgehouden?
Nee. Dit is een cruciaal onderscheid van Object.wait(). Wanneer een thread LockSupport.park aanroept, gaat het in een wachtstatus, maar behoudt het het bezit van alle monitors die het momenteel vast houdt. Als een andere thread probeert om een van die monitors binnen te gaan (bijvoorbeeld een gesynchroniseerd blok op hetzelfde object), zal het blokkeren, wat mogelijk een vastloop veroorzaakt als de geparkeerde thread de enige is die deze kan vrijgeven. Kandidaten veronderstellen vaak ten onrechte dat park is zoals wait en sloten vrijgeeft; het is een puur thread-lokale scheduler primitief.
Wat is het gedrag van LockSupport.park wanneer aangeroepen op een thread wiens onderbreekstatus is ingesteld?
De methode retourneert onmiddellijk zonder te blokkeren en cleart niet de onderbreekstatus. Dit verschilt fundamenteel van Object.wait(), dat de onderbreekstatus wist en InterruptedException gooit. Met LockSupport moet de thread expliciet de onderbreekstatus controleren en wissen (via Thread.interrupted()) als het de onderbrekingsconventies wil respecteren. Dit ontwerp stelt park in staat om te worden gebruikt in niet-onderbreekbare contexten of waar onderbreking wordt behandeld als een aparte zorg van de parkeervergunning.
Hoe gaat LockSupport om met spurious wakerevents, en hoe beïnvloedt dit coderingspatronen?
LockSupport.park wordt gedocumenteerd om "om geen reden" (spurious wakeup) te retourneren, hoewel dit in de praktijk zeldzaam is op moderne JVM's. In tegenstelling tot de op vergunning gebaseerde wakker worden (unpark), consumeren spurious wakerevents niet de vergunning. Daarom moet de aanroeper altijd de voorwaarde die de parkeer aanriep opnieuw controleren in een lus:
while (!canProceed()) { LockSupport.park(); }
Kandidaten missen vaak dat simpelweg de voorwaarde eenmaal na park controleren onvoldoende is; de thread kan spurious wakker worden (of door een willekeurige onderbreking) zonder een unpark-aanroep, waardoor herbeoordeling van de staat voorwaarde vereist is. De vergunning zorgt ervoor dat een geldige unpark niet verloren gaat, maar voorkomt geen spurious returns.