diff --git a/core-java-modules/core-java-concurrency-advanced-7/pom.xml b/core-java-modules/core-java-concurrency-advanced-7/pom.xml index 32ac6448175d..0d8557a69133 100644 --- a/core-java-modules/core-java-concurrency-advanced-7/pom.xml +++ b/core-java-modules/core-java-concurrency-advanced-7/pom.xml @@ -20,10 +20,72 @@ ${awaitility.version} test + + org.openjdk.jmh + jmh-core + ${jmh.core.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.core.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source.version} + ${maven.compiler.target.version} + false + + --enable-preview + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.core.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + --enable-preview + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + shade + + benchmarks + + + org.openjdk.jmh.Main + + + + + + + + + 1.7.0 + 21 + 21 + 1.37 diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java new file mode 100644 index 000000000000..4ddf22ca3b8b --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classinit/HeavyClass.java @@ -0,0 +1,29 @@ +package com.baeldung.virtualthread.classinit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HeavyClass { + + private static final Logger LOGGER = LoggerFactory.getLogger(HeavyClass.class); + + static { + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + + LOGGER.info("static initialization done"); + } + + { + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + + LOGGER.info("initialization done"); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java new file mode 100644 index 000000000000..27a7fcb15f58 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/CustomClassLoader.java @@ -0,0 +1,55 @@ +package com.baeldung.virtualthread.classloader; + +import java.io.IOException; +import java.nio.file.Path; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class CustomClassLoader extends ClassLoader { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomClassLoader.class); + private final Path classDir; + + public CustomClassLoader(Path classDir) { + super(ClassLoader.getSystemClassLoader()); + this.classDir = classDir; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + LOGGER.info("Load class for {}", name); + + Class clazz = findLoadedClass(name); + + if (clazz == null) { + try { + clazz = findClass(name); + } catch (ClassNotFoundException ex) { + clazz = super.loadClass(name, resolve); + } + } + + if (resolve) { + resolveClass(clazz); + } + + return clazz; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + LOGGER.info("Finding class for {}", name); + + try { + Path file = classDir.resolve(name.replace('.', '/') + ".class"); + byte[] bytes = java.nio.file.Files.readAllBytes(file); + Thread.sleep(100); + + return defineClass(name, bytes, 0, bytes.length); + } catch (InterruptedException | IOException ex) { + LOGGER.error("Error while finding class file {}", ex.getMessage()); + throw new ClassNotFoundException(ex.getMessage(), ex); + } + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java new file mode 100644 index 000000000000..10ef734d65bb --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/classloader/MyClass.java @@ -0,0 +1,6 @@ +package com.baeldung.virtualthread.classloader; + +public class MyClass { + public MyClass() { + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java new file mode 100644 index 000000000000..b13368c95b64 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/foreignfunction/ForeignFunctionClass.java @@ -0,0 +1,33 @@ +package com.baeldung.virtualthread.foreignfunction; + +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.JAVA_LONG; + +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.SymbolLookup; +import java.lang.invoke.MethodHandle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ForeignFunctionClass { + + private static final Logger LOGGER = LoggerFactory.getLogger(ForeignFunctionClass.class); + + public void execute() { + LOGGER.info("Running foreign function sleep..."); + + Linker linker = Linker.nativeLinker(); + SymbolLookup stdlib = linker.defaultLookup(); + MethodHandle sleep = linker.downcallHandle(stdlib.find("sleep") + .orElseThrow(), FunctionDescriptor.of(JAVA_INT, JAVA_LONG)); + + try { + sleep.invoke(100); + } catch (Throwable ex) { + System.out.println("Error in native sleep..."); + throw new RuntimeException(ex); + } + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java new file mode 100644 index 000000000000..122d97601ae0 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/nativemethod/NativeDemo.java @@ -0,0 +1,10 @@ +package com.baeldung.virtualthread.nativemethod; + +public class NativeDemo { + + static { + System.loadLibrary("native-lib"); + } + + public native String nativeCall(); +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java new file mode 100644 index 000000000000..7a6d5d4391cb --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/BenchmarkVirtualThread.java @@ -0,0 +1,48 @@ +package com.baeldung.virtualthread.synchronize; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; + +@BenchmarkMode({ Mode.AverageTime, Mode.Throughput }) +@OutputTimeUnit(TimeUnit.SECONDS) +@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(value = 2) +@State(Scope.Benchmark) +public class BenchmarkVirtualThread { + + private final CartService cartService = new CartService(); + + @Param({ "100", "1000", "10000" }) + private int CONCURRENCY; + + @Benchmark + public void benchmark() throws InterruptedException, IOException { + List threads = new ArrayList<>(); + IntStream.range(0, CONCURRENCY).forEach(i -> threads.add(Thread.startVirtualThread(() -> cartService.update(UUID.randomUUID() + .toString(), 2)))); + + threads.forEach(th -> { + try { + th.join(); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + }); + } +} \ No newline at end of file diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java new file mode 100644 index 000000000000..e3a0a0c772a4 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/CartService.java @@ -0,0 +1,43 @@ +package com.baeldung.virtualthread.synchronize; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CartService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CartService.class); + + private final Map products; + private final Map locks = new ConcurrentHashMap<>(); + + public CartService() { + this.products = new HashMap<>(); + } + + public void update(String productId, int quantity) { + Object lock = locks.computeIfAbsent(productId, k -> new Object()); + + synchronized (lock) { + simulateAPI(); + products.merge(productId, quantity, Integer::sum); + } + + LOGGER.info("Updated Cart for {} {}", productId, quantity); + } + + public Map getProducts() { + return Map.copyOf(products); + } + + private void simulateAPI() { + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java new file mode 100644 index 000000000000..a252e8ca9fc1 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/main/java/com/baeldung/virtualthread/synchronize/fixed/CartService.java @@ -0,0 +1,53 @@ +package com.baeldung.virtualthread.synchronize.fixed; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CartService { + + private static final Logger LOGGER = LoggerFactory.getLogger(CartService.class); + + private final Map products; + private final Map locks = new ConcurrentHashMap<>(); + + public CartService() { + this.products = new HashMap<>(); + } + + public void update(String productId, int quantity) { + Lock lock = locks.computeIfAbsent(productId, k -> new ReentrantLock()); + + try { + if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { + try { + simulateAPI(); + products.merge(productId, quantity, Integer::sum); + } finally { + lock.unlock(); + } + LOGGER.info("Updated Cart for {} {}", productId, quantity); + } + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + private void simulateAPI() { + try { + Thread.sleep(50); + } catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + public Map getProducts() { + return Map.copyOf(products); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java new file mode 100644 index 000000000000..539b2c4eeee9 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classinit/HeavyClassTest.java @@ -0,0 +1,51 @@ +package com.baeldung.virtualthread.classinit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +public class HeavyClassTest { + + @Test + void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws IOException, InterruptedException { + Path file = Path.of("pinning_1.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th = Thread.ofVirtual() + .start(HeavyClass::new); + th.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertTrue(rf.hasMoreEvents()); + + while (rf.hasMoreEvents()) { + RecordedEvent event = rf.readEvent(); + System.out.println(event); + assertEquals("jdk.VirtualThreadPinned", event.getEventType() + .getName()); + assertEquals("Virtual Thread Pinned", event.getEventType() + .getLabel()); + } + } + + Files.delete(file); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java new file mode 100644 index 000000000000..a023d6f55051 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/classloader/CustomClassLoaderTest.java @@ -0,0 +1,67 @@ +package com.baeldung.virtualthread.classloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +public class CustomClassLoaderTest { + + @Test + void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws Exception { + Path classDir = Paths.get(CustomClassLoader.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()); + + CustomClassLoader loader = new CustomClassLoader(classDir); + Path file = Path.of("pinning_3.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th = Thread.ofVirtual() + .start(() -> { + try { + Class clazz = Class.forName("com.baeldung.virtualthread.classloader.MyClass", + true, loader); + + System.out.println(Thread.currentThread() + " loaded class : " + clazz.getName()); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + }); + + th.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertTrue(rf.hasMoreEvents()); + + while (rf.hasMoreEvents()) { + RecordedEvent event = rf.readEvent(); + + assertEquals("jdk.VirtualThreadPinned", event.getEventType() + .getName()); + assertEquals("Virtual Thread Pinned", event.getEventType() + .getLabel()); + } + } + + Files.delete(file); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java new file mode 100644 index 000000000000..84e1d29568f4 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/CartServiceTest.java @@ -0,0 +1,84 @@ +package com.baeldung.virtualthread.synchronize; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +public class CartServiceTest { + + private final CartService cartService = new CartService(); + + @Test + void givenJFRRecIsEnabled_whenVThreadIsBlocked_thenDetectVThreadPinned() throws Exception { + Path file = Path.of("pinning_4.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th = Thread.ofVirtual().start(() -> + cartService.update("test1", 2)); + + th.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertTrue(rf.hasMoreEvents()); + + while (rf.hasMoreEvents()) { + RecordedEvent event = rf.readEvent(); + + System.out.println(event); + assertEquals("jdk.VirtualThreadPinned", event.getEventType().getName()); + assertEquals("Virtual Thread Pinned", event.getEventType().getLabel()); + } + } + + Files.delete(file); + } + + @Test + void givenProductsIsPresent_whenProductIsAdded_thenProductIsUpdated() throws InterruptedException { + String productId = "test2"; + Thread th1 = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + + Thread th2 = Thread.ofVirtual().start(() -> + cartService.update(productId, 3)); + + th1.join(); + th2.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(5, products.get(productId)); + } + + @Test + void givenProductIsNotPresent_whenProductIsAdded_thenProductIsUpdated() throws InterruptedException { + String productId = "test3"; + Thread th = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + th.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(2, products.get(productId)); + } +} diff --git a/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java new file mode 100644 index 000000000000..dfc64fd0ad93 --- /dev/null +++ b/core-java-modules/core-java-concurrency-advanced-7/src/test/java/com/baeldung/virtualthread/synchronize/fixed/CartServiceTest.java @@ -0,0 +1,80 @@ +package com.baeldung.virtualthread.synchronize.fixed; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordingFile; + +public class CartServiceTest { + + private final CartService cartService = new CartService(); + + @Test + void givenJFRIsEnabled_whenVThreadIsBlocked_thenDetectVirtualThreadPinned() throws Exception { + Path file = Path.of("no-pinning.jfr"); + + try (Recording recording = new Recording()) { + recording.enable("jdk.VirtualThreadPinned") + .withThreshold(Duration.ofMillis(1)); + recording.start(); + + Thread th1 = Thread.ofVirtual().start(() -> + cartService.update("test1", 2)); + + Thread th2 = Thread.ofVirtual().start(() -> + cartService.update("test1", 3)); + + th1.join(); + th2.join(); + + recording.stop(); + recording.dump(file); + } + + try (RecordingFile rf = new RecordingFile(file)) { + assertFalse(rf.hasMoreEvents()); + } + + Files.delete(file); + } + + @Test + void givenProductsIsPresent_whenProductIsAdded_thenProductIsUpdated() throws InterruptedException { + String productId = "test4"; + Thread th1 = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + + Thread th2 = Thread.ofVirtual().start(() -> + cartService.update(productId, 3)); + + th1.join(); + th2.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(5, products.get(productId)); + } + + @Test + void givenProductIsNotPresent_whenProductIsAdded_thenProductIsUpdate() throws InterruptedException { + String productId = "test5"; + Thread th = Thread.ofVirtual().start(() -> + cartService.update(productId, 2)); + th.join(); + + Map products = cartService.getProducts(); + + assertTrue(products.containsKey(productId)); + assertEquals(2, products.get(productId)); + } +}