jdk21中virtual thread需要注意的坑(jdk24之前 synchronized尽量不用)

jdk21正式释放了virtual thread特性,可以在io阻塞的高并发开发中,让传统的编码方式达到一个质的飞跃。

⚠️但是,这里需要注意,synchronized关键字,在jdk21、jdk22、jdk23中,会让当前虚拟线程,绑定(Pinning)在os平台线程上,包裹的代码执行完成后,才会从os平台线程卸载。
而一个os的平台线程,一般是当前机器的CPU核心数。例如4核带了超线程=4*2=8个。
换句话说:如果在synchronized关键字的代码中,有一段block阻塞的操作。同时有8个线程都执行了到这段代码中,会导致其他的虚拟线程没有os平台线程可用,一直阻塞等待有空闲的os平台线程。

这个现象已经被JDK研发团队感知,收集到jep中,可能在jdk24修复,预计在jdk25 LTS版本中完全释放。
https://openjdk.org/jeps/491
https://openjdk.org/projects/jdk/24/
当前解决方案:使用ReentrantLock来替代synchronized

从下面的代码可以进行测试

public class TestVirtual {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    private ReentrantReadWriteLock.WriteLock writeLock = new ReentrantReadWriteLock().writeLock();

    private static final Object lock = new Object();

    // 因为是有synchronized,pinning了os线程,每次执行8个线程,导致后续虚拟线程一直等待
    public void testSync() {
        int i = atomicInteger.incrementAndGet();
        log.info("testSync atomicInteger start = {}", i);
        synchronized (this) {
            log.info("testSync atomicInteger = {}", i);
            try {
                Thread.sleep(10 * 1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.info("testSync atomicInteger after = {}", i);
        }
    }

    // 无影响,不会pinning os线程,所有线程都能正常执行
    public void testReentrantLock() {
        int i = atomicInteger.incrementAndGet();
        log.info("testReentrantLock atomicInteger start = {}", i);
        writeLock.lock();
        try {
            log.info("testReentrantLock atomicInteger = {}", i);
            Thread.sleep(10 * 1000L);
            log.info("testReentrantLock atomicInteger after = {}", i);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            writeLock.unlock();
        }
    }

    // 因为sync的是static变量,导致每个os线程上都pinning了一个虚拟线程,并且每次只能执行1个,导致性能损失更大
    public void testSyncGlobal() {
        int i = atomicInteger.incrementAndGet();
        log.info("testSyncGlobal atomicInteger start = {}", i);
        synchronized (lock) {
            log.info("testSyncGlobal atomicInteger = {}", i);
            try {
                Thread.sleep(10 * 1000L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            log.info("testSyncGlobal atomicInteger after = {}", i);
        }
    }

}
// 因为是有synchronized,pinning了os线程,每次执行8个线程,导致后续虚拟线程一直等待
for (int i = 0; i < 100; i++) {
    TestVirtual virtual = new TestVirtual();
    Thread.startVirtualThread(new Runnable() {
        @Override
        public void run() {
            virtual.testSync();
        }
    });
}
// 无影响,不会pinning os线程,所有线程都能正常执行
for (int i = 0; i < 100; i++) {
    TestVirtual virtual = new TestVirtual();
    Thread.startVirtualThread(new Runnable() {
        @Override
        public void run() {
            virtual.testReentrantLock();
        }
    });
}
// 因为sync的是static变量,导致每个os线程上都pinning了一个虚拟线程,并且每次只能执行1个,导致性能损失更大
for (int i = 0; i < 100; i++) {
    TestVirtual virtual = new TestVirtual();
    Thread.startVirtualThread(new Runnable() {
        @Override
        public void run() {
            virtual.testSyncGlobal();
        }
    });
}

转载请备注引用地址:编程记忆 » jdk21中virtual thread需要注意的坑(jdk24之前 synchronized尽量不用)