Verschil tussen Thread en Virtual Thread in Java

1. Inleiding

In deze tutorial laten we het verschil zien tussen traditionele threads in Java en de virtuele threads die in Project Loom zijn geïntroduceerd.

Vervolgens delen we verschillende use-cases voor virtuele threads en de API's die het project heeft geïntroduceerd.

Voordat we beginnen, moeten we er rekening mee houden dit project is in actieve ontwikkeling. We zullen onze voorbeelden uitvoeren op loom VM met vroege toegang: openjdk-15-loom + 4-55_windows-x64_bin.

Nieuwere versies van de builds zijn gratis om de huidige API's te wijzigen en te verbreken. Dat gezegd hebbende, was er al een grote verandering in de API, zoals eerder werd gebruikt java.lang.Fiber class is verwijderd en vervangen door het nieuwe java.lang.VirtualThread klasse.

2. Overzicht op hoog niveau van thread versus virtuele thread

Op een hoog niveau, een thread wordt beheerd en gepland door het besturingssysteem, terwijl een virtuele thread wordt beheerd en gepland door een virtuele machine. Nu, om een ​​nieuwe kernel-thread te maken, moeten we een systeemaanroep doen, en dat is een kostbare operatie.

Daarom gebruiken we threadpools in plaats van threads opnieuw toe te wijzen en ongedaan te maken als dat nodig is. Als we vervolgens onze applicatie willen schalen door meer threads toe te voegen, vanwege de contextomschakeling en hun geheugenvoetafdruk, kunnen de kosten voor het onderhouden van die threads aanzienlijk zijn en de verwerkingstijd beïnvloeden.

Meestal willen we die threads dan niet blokkeren, en dit resulteert in het gebruik van niet-blokkerende I / O-API's en asynchrone API's, waardoor onze code onoverzichtelijk kan worden.

Integendeel, virtuele threads worden beheerd door de JVM. Daarom zijn hun toewijzing vereist geen systeemaanroep, en ze zijn vrij van de contextschakelaar van het besturingssysteem. Bovendien draaien virtuele threads op de carrier-thread, de feitelijke kerneldraad die onder de motorkap wordt gebruikt. Dientengevolge, aangezien we vrij zijn van de contextomschakeling van het systeem, zouden we veel meer van dergelijke virtuele threads kunnen voortbrengen.

Vervolgens is een belangrijke eigenschap van virtuele threads dat ze onze carrier-thread niet blokkeren. Daarmee wordt het blokkeren van een virtuele thread een veel goedkopere operatie, omdat de JVM een andere virtuele thread zal plannen, waardoor de carrier-thread gedeblokkeerd blijft.

Uiteindelijk hoeven we geen contact op te nemen met NIO- of Async-API's. Dit zou moeten resulteren in beter leesbare code die gemakkelijker te begrijpen en te debuggen is. Niettemin, de voortzetting kan mogelijk een carrier-thread blokkeren - specifiek, wanneer een thread een native methode aanroept en vanaf daar blokkeringsbewerkingen uitvoert.

3. Nieuwe Thread Builder API

In Loom hebben we de nieuwe builder-API in de Draad klasse, samen met verschillende fabrieksmethoden. Laten we eens kijken hoe we standaard en virtuele fabrieken kunnen maken en deze kunnen gebruiken voor onze thread-uitvoering:

Runnable printThread = () -> System.out.println (Thread.currentThread ()); ThreadFactory virtualThreadFactory = Thread.builder (). Virtual (). Factory (); ThreadFactory kernelThreadFactory = Thread.builder (). Factory (); Draad virtualThread = virtualThreadFactory.newThread (printThread); Thread kernelThread = kernelThreadFactory.newThread (printThread); virtualThread.start (); kernelThread.start ();

Dit is de uitvoer van de bovenstaande run:

Thread [Thread-0,5, main] VirtualThread [, ForkJoinPool-1-worker-3, CarrierThreads]

Hier is de eerste invoer de standaard toString uitvoer van de kerneldraad.

Nu zien we in de uitvoer dat de virtuele thread geen naam heeft en wordt uitgevoerd op een werkthread van de Fork-Join-pool vanuit de CarrierThreads thread groep.

Zoals we kunnen zien, ongeacht de onderliggende implementatie, de API is hetzelfde, en dat betekent dat we gemakkelijk bestaande code op de virtuele threads kunnen draaien.

We hoeven ook geen nieuwe API te leren om er gebruik van te maken.

4. Virtuele draadsamenstelling

Het is een voortzetting en een planner die samen een virtuele draad vormen. Nu kan onze planner in de gebruikersmodus elke implementatie van het Uitvoerder koppel. Het bovenstaande voorbeeld heeft ons laten zien dat we standaard draaien op de ForkJoinPool.

Nu, vergelijkbaar met een kerneldraad - die kan worden uitgevoerd op de CPU, vervolgens kan worden geparkeerd, opnieuw kan worden gepland en vervolgens kan worden hervat - is een voortzetting een uitvoeringseenheid die kan worden gestart, vervolgens kan worden geparkeerd (overgedragen), opnieuw kan worden ingepland en hervat. de uitvoering op dezelfde manier vanaf het punt waar het was gebleven en nog steeds worden beheerd door een JVM in plaats van te vertrouwen op een besturingssysteem.

Merk op dat de voortzetting een API op laag niveau is en dat programmeurs API's van een hoger niveau, zoals de bouwer-API, moeten gebruiken om virtuele threads uit te voeren.

Om echter te laten zien hoe het onder de motorkap werkt, zullen we nu onze experimentele voortzetting uitvoeren:

var scope = new ContinuationScope ("C1"); var c = new Continuation (scope, () -> {System.out.println ("Start C1"); Continuation.yield (scope); System.out.println ("End C1");}); while (! c.isDone ()) {System.out.println ("Start run ()"); c.run (); System.out.println ("End run ()"); }

Dit is de uitvoer van de bovenstaande run:

Start run () Start C1 End run () Start run () End C1 End run ()

In dit voorbeeld hebben we onze voortzetting uitgevoerd en op een gegeven moment besloten om de verwerking te stoppen. Toen we het eenmaal opnieuw uitvoerden, ging onze voortzetting verder waar het was gebleven. Aan de output zien we dat de rennen() methode werd twee keer aangeroepen, maar de voortzetting werd één keer gestart en vervolgens werd de uitvoering voortgezet tijdens de tweede run waar het was gebleven.

Dit is hoe blokkeringsoperaties moeten worden verwerkt door de JVM. Zodra een blokkeeroperatie plaatsvindt, zal de voortzetting meegeven, waardoor de carrier-thread gedeblokkeerd blijft.

Dus wat er gebeurde, is dat onze hoofdthread een nieuw stackframe heeft gemaakt op zijn call-stack voor de rennen() methode en ging verder met de uitvoering. Vervolgens, nadat de voortzetting opleverde, bewaarde de JVM de huidige staat van uitvoering.

Vervolgens heeft de hoofdthread zijn uitvoering voortgezet alsof het rennen() methode geretourneerd en vervolgd met de terwijl lus. Na de tweede oproep tot vervolg rennen methode herstelde de JVM de toestand van de hoofdthread tot het punt waarop de voortzetting heeft opgehouden en de uitvoering is voltooid.

5. Conclusie

In dit artikel hebben we het verschil tussen de kerneldraad en de virtuele thread besproken. Vervolgens hebben we laten zien hoe we een nieuwe thread builder API van Project Loom konden gebruiken om de virtuele threads uit te voeren.

Ten slotte hebben we laten zien wat een vervolg is en hoe het werkt onder de motorkap. We kunnen de staat van Project Loom verder onderzoeken door de VM met vroege toegang te inspecteren. Als alternatief kunnen we meer van de reeds gestandaardiseerde Java-concurrency-API's verkennen.


$config[zx-auto] not found$config[zx-overlay] not found