Java线程之作用域值ScopedValue

一、概述

ScopedValue是基于结构化并发理念在JDK20中被孵化的一个功能,它显然不是为了取代ThreadLocal出现的,而是能让结构化并发中的虚拟线程也能各自享有外部的变量。其实结构化并发中也可以使用ThreadLocal,但是ThreadLocal本身存在一些很大的问题:

  1. 可变,任何运行在当前线程中的代码都可以修改该变量的值。
  2. 生命周期长,当使用ThreadLocal变量的set方法,为当前线程设置了值之后,这个值在线程的整个生命周期中都会保留,直到调用remove方法来删除。
  3. 可以被继承,如果一个子线程从父线程中继承ThreadLocal变量,那么该子线程需要独立存储父线程中的全部ThreadLocal变量,这会产生比较大的内存开销。

为了解决这些问题便孵化了ScopedValueScopedValue具备ThreadLocal的核心特征,也就是每个线程只有一个值。与ThreadLocal不同的是,ScopedValue是不可变的,并且有确定的作用域,这也是名字中scoped的含义。

1.1 核心特性

  1. 线程安全:ScopedValue是线程安全的,适合在并发场景中使用。它避免了ThreadLocal的一些常见问题(如内存泄漏)。
  2. 作用域明确:ScopedValue的值仅在特定的代码块中有效,超出作用域后会自动失效。
  3. 不可变性:ScopedValue的值是不可变的,确保了线程间数据的安全性。
  4. 适合虚拟线程:ScopedValue专为虚拟线程设计,避免了ThreadLocal在虚拟线程中的性能问题。

1.2 使用场景

  1. 虚拟线程中的上下文传递:在虚拟线程中传递用户信息、请求上下文等。
  2. 替代ThreadLocal:替代ThreadLocal,避免内存泄漏和性能问题。
  3. 作用域明确的值传递:在特定代码块中传递值,确保值的生命周期与代码块绑定。

1.3 ScopedValue与ThreadLocal的区别

特性 ScopedValue ThreadLocal
绑定方式 与代码块绑定 与线程绑定
线程安全
适合虚拟线程 否(性能较差)
值的可变性 不可变 可变
内存泄漏风险 有(如果未正确清理)
使用场景 高并发、虚拟线程 传统线程场景

二、使用示例

使用ScopedValue的第一步是创建ScopedValue对象,通过静态方法newInstance来完成,ScopedValue对象一般声明为static final

同时需要再启动参数VM Option中启用预览功能--enable-preview。下一步是指定ScopedValue对象的值和作用域,通过静态方法where来完成。where方法有3个参数:

  • ScopedValue对象
  • ScopedValue对象所绑定的值
  • RunnableCallable对象,表示ScopedValue对象的作用域

RunnableCallable对象执行过程中,其中的代码可以用ScopedValue对象的get方法获取到where方法调用时绑定的值。这个作用域是动态的,取决于RunnableCallable对象所调用的方法,以及这些方法所调用的其他方法。当RunnableCallable对象执行完成之后,ScopedValue对象会失去绑定,不能再通过get方法获取值。

在当前作用域中,ScopedValue对象的值是不可变的,除非再次调用where方法绑定新的值。这个时候会创建一个嵌套的作用域,新的值仅在嵌套的作用域中有效。使用作用域值有以下几个优势:

  • 提高数据安全性:由于作用域值只能在当前范围内访问,因此可以避免数据泄露或被恶意修改。
  • 提高数据效率:由于作用域值是不可变的,并且可以在线程之间共享,因此可以减少数据复制或同步的开销。
  • 提高代码清晰度:由于作用域值只能在当前范围内访问,因此可以减少参数传递或全局变量的使用。

2.1 基本使用

public class ScopedValueExample {
    // 定义 ScopedValue
    static final ScopedValue<String> USER = ScopedValue.newInstance();

    public static void main(String[] args) {
        // 在作用域中设置 ScopedValue 的值
        ScopedValue.where(USER, "Alice").run(() -> {
            System.out.println("CurrentScope: " + USER.get()); // 输出 "Alice"
        });
        // 超出作用域后,ScopedValue 的值不可访问
        System.out.println("OutsideScope: " + USER.get()); // 抛出 IllegalStateException
    }
}

输出:

CurrentScope: Alice
Exception in thread "main" java.lang.IllegalStateException: No value present

2.2 在虚拟线程中使用

public class VirtualThreadScopedValueExample {
    static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

    public static void main(String[] args) throws InterruptedException {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 5; i++) {
                int id = i;
                executor.submit(() -> {
                    ScopedValue.where(REQUEST_ID, "Request-" + id).run(() -> {
                        System.out.println("Thread: " + Thread.currentThread() 
                                           + ", Request ID: " + REQUEST_ID.get());
                    });
                });
            }
        }
    }
}

输出(示例):

Thread: VirtualThread[#1]/runnable@ForkJoinPool-1-worker-1, Request ID: Request-0
Thread: VirtualThread[#2]/runnable@ForkJoinPool-1-worker-2, Request ID: Request-1
Thread: VirtualThread[#3]/runnable@ForkJoinPool-1-worker-3, Request ID: Request-2
Thread: VirtualThread[#4]/runnable@ForkJoinPool-1-worker-4, Request ID: Request-3
Thread: VirtualThread[#5]/runnable@ForkJoinPool-1-worker-5, Request ID: Request-4

2.3 嵌套作用域

ScopedValue支持嵌套作用域,内层作用域可以覆盖外层作用域的值。

public class NestedScopedValueExample {
    static final ScopedValue<String> USER = ScopedValue.newInstance();
    public static void main(String[] args) {
        ScopedValue.where(USER, "Alice").run(() -> {
            System.out.println("OuterScope: " + USER.get()); // 输出 "Alice"
            setInnerScope("Bob");// 输出 "Bob"
            System.out.println("BackOuterScope: " + USER.get()); // 输出 "Alice"
        });
    }
    static void setInnerScope(String value) {
        ScopedValue.where(USER, value).run(() -> {
            System.out.println("InnerScope: " + USER.get());
        });
    }
}

输出:

OuterScope: Alice
InnerScope: Bob
BackOuterScope: Alice

2.4 上下文传递

public class Test {
    // 声明了一个静态的、最终的 ScopedValue<String> 实例
    // ScopedValue 是一个支持在特定范围内(如任务或线程)中传递值的类
    // 它的使用类似于 ThreadLocal,但更适合于结构化并发
    private static final ScopedValue<String> VALUE = ScopedValue.newInstance();
    public static void main(String[] args) throws Exception {
        stringScope();
    }
    public static Object[] stringScope() throws Exception {
        return ScopedValue.where(VALUE, "value", () -> {
            // 使用 try-with-resource 来绑定结构化并发的作用域
            // 用于自动管理资源的生命周期,这是一个结构化任务范围
            // 在这个范围内创建的所有子任务都将被视为范围的一部分
            // 如果范围中的任何任务失败,所有其他任务都将被取消
            try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
                // 使用了 scope.fork 来创建两个并行的任务
                // 每个任务都在执行上下文中获取 VALUE 的值,并对其进行操作
                StructuredTaskScope.Subtask<String> user = scope.fork(VALUE::get);
                StructuredTaskScope.Subtask<Integer> order = scope.fork(() -> VALUE.get().length());
                // join() 方法等待所有范围内的任务完成
                // throwIfFailed() 方法会检查所有任务的结果,如果任何任务失败,则会抛出异常
                scope.join().throwIfFailed();
                String userResult = user.get();
                Integer orderResult = order.get();
                System.out.println("userResult:" + userResult + ", orderResult:" + orderResult);
            }
        });
    }
}

三、源码分析

ScopedValue是一个对象,它被设置一次后,在执行期间由一个线程有限期地读取。ScopedValue允许在有限的执行期间内在不将数据作为方法参数传递的情况下安全、有效地共享数据。

ScopedValue定义了where(ScopedValue, Object, Runnable)方法,这个方法在一个线程执行runnablerun方法的有限执行期间内设置ScopedValue的值。由run执行的方法展开执行定义了一个动态作用域。在动态作用域中执行时,作用域值是绑定的,当run方法完成时(正常或异常),它恢复到未绑定状态。在动态作用域中执行的代码使用ScopedValueget方法来读取其值。与线程局部变量类似,作用域值有多个化身,每个线程一个。使用哪个化身取决于哪个线程调用其方法。

ScopedValue的一个典型用法是在finalstatic字段中声明。字段的可访问性将决定哪些组件可以绑定或读取其值。

ScopedValue中有3个内部类,分别是SnapshotCarrierCache,他们在ScopedValue中起着至关重要的角色。

3.1 Snapshot类

static final class Snapshot {
    final Snapshot prev;
    final Carrier bindings;
    final int bitmask;

    private static final Object NIL = new Object();

    static final Snapshot EMPTY_SNAPSHOT = new Snapshot();

    Snapshot(Carrier bindings, Snapshot prev) {
        this.prev = prev;
        this.bindings = bindings;
        this.bitmask = bindings.bitmask | prev.bitmask;
    }

    protected Snapshot() {
        this.prev = null;
        this.bindings = null;
        this.bitmask = 0;
    }

    Object find(ScopedValue<?> key) {
        int bits = key.bitmask();
        for (Snapshot snapshot = this;
             containsAll(snapshot.bitmask, bits);
             snapshot = snapshot.prev) {
            for (Carrier carrier = snapshot.bindings;
                 carrier != null && containsAll(carrier.bitmask, bits);
                 carrier = carrier.prev) {
                if (carrier.getKey() == key) {
                    Object value = carrier.get();
                    return value;
                }
            }
        }
        return NIL;
    }
}

Snapshot是一个从ScopedValue到值的不可变映射。除非特别说明,否则将null参数传递给这个类的构造器或方法会导致抛出NullPointerException异常。这个类的主要用途是为ScopedValue实例创建一个不可变的映射,这样在运行时,无论其它代码如何修改原始的ScopedValue实例,Snapshot中的值都不会发生变化。它为了提供一个安全的方式来在多线程环境下共享值。

3.2 Carrier类

public static final class Carrier {
    final int bitmask;
    final ScopedValue<?> key;
    final Object value;
    final Carrier prev;

    Carrier(ScopedValue<?> key, Object value, Carrier prev) {
        this.key = key;
        this.value = value;
        this.prev = prev;
        int bits = key.bitmask();
        if (prev != null) {
            bits |= prev.bitmask;
        }
        this.bitmask = bits;
    }

    private static <T> Carrier where(ScopedValue<T> key, T value, Carrier prev) {
        return new Carrier(key, value, prev);
    }

    public <T> Carrier where(ScopedValue<T> key, T value) {
        return where(key, value, this);
    }

    static <T> Carrier of(ScopedValue<T> key, T value) {
        return where(key, value, null);
    }

    Object get() {
        return value;
    }

    ScopedValue<?> getKey() {
        return key;
    }

    @SuppressWarnings("unchecked")
    public <T> T get(ScopedValue<T> key) {
        var bits = key.bitmask();
        for (Carrier carrier = this;
             carrier != null && containsAll(carrier.bitmask, bits);
             carrier = carrier.prev) {
            if (carrier.getKey() == key) {
                Object value = carrier.get();
                return (T)value;
            }
        }
        throw new NoSuchElementException();
    }

    public <R> R call(Callable<? extends R> op) throws Exception {
        Objects.requireNonNull(op);
        Cache.invalidate(bitmask);
        var prevSnapshot = scopedValueBindings();
        var newSnapshot = new Snapshot(this, prevSnapshot);
        return runWith(newSnapshot, op);
    }

    public <R> R get(Supplier<? extends R> op) {
        Objects.requireNonNull(op);
        Cache.invalidate(bitmask);
        var prevSnapshot = scopedValueBindings();
        var newSnapshot = new Snapshot(this, prevSnapshot);
        return runWith(newSnapshot, new CallableAdapter<R>(op));
    }


    private static final class CallableAdapter<V> implements Callable<V> {
        private /*non-final*/ Supplier<? extends V> s;
        CallableAdapter(Supplier<? extends V> s) {
            this.s = s;
        }
        public V call() {
            return s.get();
        }
    }

    @Hidden
    @ForceInline
    private <R> R runWith(Snapshot newSnapshot, Callable<R> op) {
        try {
            Thread.setScopedValueBindings(newSnapshot);
            Thread.ensureMaterializedForStackWalk(newSnapshot);
            return ScopedValueContainer.call(op);
        } finally {
            Reference.reachabilityFence(newSnapshot);
            Thread.setScopedValueBindings(newSnapshot.prev);
            Cache.invalidate(bitmask);
        }
    }

    public void run(Runnable op) {
        Objects.requireNonNull(op);
        Cache.invalidate(bitmask);
        var prevSnapshot = scopedValueBindings();
        var newSnapshot = new Snapshot(this, prevSnapshot);
        runWith(newSnapshot, op);
    }

    @Hidden
    @ForceInline
    private void runWith(Snapshot newSnapshot, Runnable op) {
        try {
            Thread.setScopedValueBindings(newSnapshot);
            Thread.ensureMaterializedForStackWalk(newSnapshot);
            ScopedValueContainer.run(op);
        } finally {
            Reference.reachabilityFence(newSnapshot);
            Thread.setScopedValueBindings(newSnapshot.prev);
            Cache.invalidate(bitmask);
        }
    }
}

Carrier类用于累积映射,以便可以执行一个操作(RunnableCallable),在该操作中,映射中的所有ScopedValue都绑定到值。Carrier是不可变的,并且是线程安全的。

where方法返回一个新的Carrier对象,不会改变现有的映射。这是用于在ScopedValue实例和对应值之间创建和保持映射关系的工具,使得这些映射关系可以在执行操作时被一并应用。

3.3 Cache类

private static final class Cache {
    static final int INDEX_BITS = 4;  // Must be a power of 2
    static final int TABLE_SIZE = 1 << INDEX_BITS;
    static final int TABLE_MASK = TABLE_SIZE - 1;
    static final int PRIMARY_MASK = (1 << TABLE_SIZE) - 1;

    // The number of elements in the cache array, and a bit mask used to
    // select elements from it.
    private static final int CACHE_TABLE_SIZE, SLOT_MASK;
    // The largest cache we allow. Must be a power of 2 and greater than
    // or equal to 2.
    private static final int MAX_CACHE_SIZE = 16;

    static {
        final String propertyName = "java.lang.ScopedValue.cacheSize";
        var sizeString = System.getProperty(propertyName, "16");
        var cacheSize = Integer.valueOf(sizeString);
        if (cacheSize < 2 || cacheSize > MAX_CACHE_SIZE) {
            cacheSize = MAX_CACHE_SIZE;
            System.err.println(propertyName + " is out of range: is " + sizeString);
        }
        if ((cacheSize & (cacheSize - 1)) != 0) {  // a power of 2
            cacheSize = MAX_CACHE_SIZE;
            System.err.println(propertyName + " must be an integer power of 2: is " + sizeString);
        }
        CACHE_TABLE_SIZE = cacheSize;
        SLOT_MASK = cacheSize - 1;
    }

    static int primaryIndex(ScopedValue<?> key) {
        return key.hash & TABLE_MASK;
    }

    static int secondaryIndex(ScopedValue<?> key) {
        return (key.hash >> INDEX_BITS) & TABLE_MASK;
    }

    private static int primarySlot(ScopedValue<?> key) {
        return key.hashCode() & SLOT_MASK;
    }

    private static int secondarySlot(ScopedValue<?> key) {
        return (key.hash >> INDEX_BITS) & SLOT_MASK;
    }

    static int primarySlot(int hash) {
        return hash & SLOT_MASK;
    }

    static int secondarySlot(int hash) {
        return (hash >> INDEX_BITS) & SLOT_MASK;
    }

    static void put(ScopedValue<?> key, Object value) {
        Object[] theCache = scopedValueCache();
        if (theCache == null) {
            theCache = new Object[CACHE_TABLE_SIZE * 2];
            setScopedValueCache(theCache);
        }
        // Update the cache to replace one entry with the value we just looked up.
        // Each value can be in one of two possible places in the cache.
        // Pick a victim at (pseudo-)random.
        int k1 = primarySlot(key);
        int k2 = secondarySlot(key);
        var usePrimaryIndex = chooseVictim();
        int victim = usePrimaryIndex ? k1 : k2;
        int other = usePrimaryIndex ? k2 : k1;
        setKeyAndObjectAt(victim, key, value);
        if (getKey(theCache, other) == key) {
            setKeyAndObjectAt(other, key, value);
        }
    }

    private static void setKeyAndObjectAt(int n, Object key, Object value) {
        var cache = scopedValueCache();
        cache[n * 2] = key;
        cache[n * 2 + 1] = value;
    }

    private static void setKeyAndObjectAt(Object[] cache, int n, Object key, Object value) {
        cache[n * 2] = key;
        cache[n * 2 + 1] = value;
    }

    private static Object getKey(Object[] objs, int n) {
        return objs[n * 2];
    }

    private static void setKey(Object[] objs, int n, Object key) {
        objs[n * 2] = key;
    }

    private static final JavaUtilConcurrentTLRAccess THREAD_LOCAL_RANDOM_ACCESS
            = SharedSecrets.getJavaUtilConcurrentTLRAccess();

    // Return either true or false, at pseudo-random, with a bias towards true.
    // This chooses either the primary or secondary cache slot, but the
    // primary slot is approximately twice as likely to be chosen as the
    // secondary one.
    private static boolean chooseVictim() {
        int r = THREAD_LOCAL_RANDOM_ACCESS.nextSecondaryThreadLocalRandomSeed();
        return (r & 15) >= 5;
    }

    // Null a set of cache entries, indicated by the 1-bits given
    static void invalidate(int toClearBits) {
        toClearBits = (toClearBits >>> TABLE_SIZE) | (toClearBits & PRIMARY_MASK);
        Object[] objects;
        if ((objects = scopedValueCache()) != null) {
            for (int bits = toClearBits; bits != 0; ) {
                int index = Integer.numberOfTrailingZeros(bits);
                setKeyAndObjectAt(objects, index & SLOT_MASK, null, null);
                bits &= ~1 << index;
            }
        }
    }
}

Cache是一个小型的固定大小的键值缓存。当调用一个ScopedValueget()方法时,我们在这个每线程缓存中记录查找的结果,以便在将来快速访问。这个类的主要作用是优化性能。通过缓存get()方法的结果,可以避免在多次获取同一个ScopedValue的值时进行重复的查找操作。只有当ScopedValue的值被更改时,才需要更新缓存。

3.4 where方法

where()方法是ScopedValue类的核心方法与入口,它接收三个参数。当操作完成时(正常或出现异常),ScopedValue将在当前线程中恢复为未绑定状态,或恢复为先前绑定时的先前值。

graph TB A("ScopedValue.where(key, value, op)") A --> B("ScopedValue.Carrier.of(key, value)") B --> C("ScopedValue.Carrier.where(key, value, prev)") C --> D("返回ScopedValue.Carrier对象")

作用域值旨在以结构化方式使用。如果op已经创建了一个StructuredTaskScope但没有关闭它,那么退出op会导致在动态范围内创建的每个StructuredTaskScope被关闭。这可能需要阻塞,直到所有子线程都完成了它们的子任务。关闭是按照创建它们的相反顺序完成的。

使用ScopedValue.where(key, value, op);等价于使用ScopedValue.where(key, value).call(op);

public static <T, R> R where(ScopedValue<T> key,
                             T value,
                             Callable<? extends R> op) throws Exception {
    return where(key, value).call(op);
}

这个方法会将前两个参数委派给Carrier.of(key, value);方法

/*
 * 返回由单个绑定组成的新集合
 */
static <T> Carrier of(ScopedValue<T> key, T value) {
    return where(key, value, null);
}
/**
 * 向该map添加绑定,返回一个新的 Carrier 实例
 */
private static <T> Carrier where(ScopedValue<T> key, T value, Carrier prev) {
    return new Carrier(key, value, prev);
}

Carrier类中where方法会返回一个新的Carrier对象,这是一种责任链的设计模式

3.5 call方法

where方法主要是构建Carrier对象,而后这些都会委派给后续的Carrier中的call方法来实现对Callable的一个调用。调用关系如下:

graph TB D("ScopedValue.Carrier") D --> E("ScopedValue.Carrier.call(op)") E -->|分支1| F("ScopedValue.Cache.invalidate()") E -->|分支2| G("ScopedValue.Carrier.runWith(newSnapshot, op)") G --> H("ScopedValueContainer.call(op)") H --> I("ScopedValueContainer.callWithoutScope(op)") I --> J("Callable.call()")

call方法调用链的方法中有很多细节是关于处理SnapshotCache的,这些内容可能在将来的Java版本中发生变化这里就不再赘述。

3.6 小结

where()方法里返回Carrier对象,执行run()时都会生成一个Snapshot对象,Snapshot对象中记录了所绑定的值,而Snapshot对象有一个prev属性指向上一次所生成的Snapshot对象,并且在Thread类中新增了一个属性scopedValueBindings,专门用来记录当前线程对应的Snapshot对象。

比如在执行main()方法中的run()时:

public class Test {
    private static final ScopedValue<String> VALUE = ScopedValue.newInstance();
    public static void main(String[] args) {
        ScopedValue.where(VALUE, "value1").run(() -> test2());
    }
    public static void test2() {
        ScopedValue.where(VALUE, "value2").run(() -> test3());
        System.out.println("test2:" + VALUE.get());// 输出value1
    }
    public static void test3() {
        System.out.println("test3:" + VALUE.get());// 输出value2
    }
}

会先生成Snapshot对象1,其prevnull,并将Snapshot对象1赋值给当前线程的scopedValueBindings属性,然后执行test2()方法

在执行test2()方法中的run()时,会先生成Snapshot对象2,其prevSnapshot对象1,并将Snapshot对象2赋值给当前线程的scopedValueBindings属性,使得在执行test2()方法时能从当前线程拿到Snapshot对象2从而拿到所绑定的值,run()内部在执行完test3()方法后会取prev,从而取出Snapshot对象1,并将Snapshot对象1赋值给当前线程的scopedValueBindings属性,然后继续执行test2()方法后续的逻辑,如果后续逻辑调用了get()方法,则会取当前线程的scopedValueBindings属性拿到Snapshot对象1,从Snapshot对象1中拿到所绑定的值就可以了,而对于Snapshot对象2由于没有引用则会被垃圾回收掉。

四、总结

ScopedValueJDK21中引入的一个现代化特性,专为虚拟线程设计,提供了一种线程安全、作用域明确的值传递机制。它的主要优势包括:

  • 替代ThreadLocal,避免内存泄漏。
  • 适合高并发和虚拟线程场景。
  • 简化上下文数据的传递。

ScopedValueJava并发编程的一次重要改进,特别适合现代化的高并发应用程序。

posted @ 2025-05-05 21:50  夏尔_717  阅读(564)  评论(0)    收藏  举报