Executive Summary
This is a long and involved post about a new still-in-preview feature
in Java called ScopedValues
. It demonstrates a use for them
that I think is very cool and fun, that is fairly magical, but without
involving any compile-time trickery or bytecode manipulation. Nor even
any reflection.
You will learn how to use ScopedValues to replace ThreadLocal patterns, and some reasons why it is better than using ThreadLocal.
I’ve intentionally glossed over some complicating details - thread safety in particular. The actual code makes use of non-blocking thread-safe tools, and some minor use of non-blocking thread-safe checks that are neither here nor there wrt the discussion of ScopedValues. For experienced developers, you’ll probably find some interest in thinking how that would work. For less experienced developers, try not to lose the overall thread in the weeds.
ScopedValue Introduction
Java’s Preview Feature, ScopedValue<T>
, provides a
utility similar to ThreadLocal<T>
, which, if you’ve
ever had the pleasure of using to spaghettifi a perfectly good codebase,
may not seem like a very good thing at all. However, just as Land Value
Taxes are similar to Property Taxes, and Universal Basic Income is
similar to welfare, it’s the seemingly minor differences that transform
these things into radically superior versions.
A ThreadLocal<T> userContext
, more-or-less
provides a hack that looks a lot like a
Map<Thread,UserContext> userContextByThread
. You can
use it to store some object:
// With ThreadLocal:
ThreadLocal<UserContext> userContext = new ThreadLocal<>();
// Set the value (and hope you remember to clean it later)
userContext.set(new UserContext("alice"));
// Access it from anywhere in the same thread
UserContext user = userContext.get(); // "alice"
// Oops, forget to clean up
// userContext.remove(); // This often gets missed!
And, so long as no one has done any funny business with threading in
your app between these calls, it’s all perfectly swell. Well, except for
the magical values your code depends on that the compiler cannot check
for. And, good luck with your unit and integration tests. Also, good
luck tracking down why a value wasn’t in the ThreadLocal
So, how does ScopedValue
// Define once
private static final ScopedValue<UserContext> USER_CONTEXT = ScopedValue.newInstance();
// Use in a controlled scope
ScopedValue.where(USER_CONTEXT, new UserContext("alice"))
.run(() -> {
// Only here can we access the value
UserContext user = USER_CONTEXT.get(); // "alice"
processUserRequest(user);
// No need to clean up - happens automatically!
});
Here we’re putting the value in the ScopedValue. And then within that scope, we run some code - a Runnable. Could be a ScopedValue.CallableOp<T,EX> which returns a value:
R processResult = ScopedValue.where(USER_CONTEXT,new UserContext("alice"))
.call(ScopedValue.CallableOp<R,EX> myCallable);
This will add UserContext("alice")
to the “scope”. It
can be retrieved like this:
USER_CONTEXT.get();
USER_CONTEXT.orElse(defaultSpecialObject);
It’s a lot like an Optional
Why is this better than ThreadLocal?
While the code is being run within this ScopedValue, if it uses StructuredTaskScope to run child threads, those threads will inherit the Scoped values. There is no such inheritance with ThreadLocal - if you spin up new child threads, you will have to copy values manually to it.
If code runs
ScopedValue.where(USER_CONTEXT,differentUser)
while running
within that scope, it will not erase the previous value held, but
instead stores it in a stack, so that when this inner scope is done, the
value of the Scope returns to what it was previously. When using
ThreadLocal, you’d have to implement such Stack logic yourself.
When a scope ends, the value reverts to null and is not available. When using ThreadLocal, old values often get left lying around, which is no bueno.
ThreadLocal is also memory intensive if you’re using hundreds of thousands or millions of Virtual Threads, as it stores those values with each thread. ScopedValue does things a bit more efficiently.
ThreadLocal<T> | ScopedValue<T>
---------------------------------------|----------------------------------------
Values persist indefinitely | Values automatically cleaned up when scope ends
No inheritance to child threads | Values inherit to child threads (with StructuredTaskScope)
Manual stack management needed | Built-in stack management for nested scopes
Prone to information leaks | Automatic cleanup prevents informational leaks
Doesn't play well with virtual threads | More efficient than ThreadLocal when using virtual threads
Now the fun
Implement an implicit observer system using ScopedValue.
An implicit observer system lets code automatically react to data changes without explicit registration (or deregistration) of listeners. When a piece of code accesses a value, it is automatically registered as interested in future changes to that value.
I’ll introduce you to the magic we can have with such a system before
describing how it works, so bear with me.
With an implicit observer system, we can write code like this:
public class ComplexGUIWidget {
private UserState currentUser;
private Label nameLabel;
public ComplexGUIWidget(UserState currentUser) {
this.currentUser = currentUser;
init(); // <- we create the layout for this component, add the nameLabel to the gui...
Reactor.begin(this::redraw);
}
public void redraw() {
nameLabel.setText(currentUser.get().getName());
}
}
// Somewhere else in the codebase
currentUser.set("Bob"); // <- triggers ComplexWidget redraw() to be called again!
And now the name label will update it’s value anytime any code
anywhere changes the userState:currentUser value. We might have some
kind of SystemState class that includes the currentUser field, and we
pass it around or dependency inject the SystemState to our various
classes. It’s handy because an application can focus on managing the
state of data (by talking to the SystemState class), and any code
anywhere can build itself to automatically react to it. The secret sauce
is in the Reactor.begin()
method, of course. It is all
enabled by the ScopedValue functionality.
public class Observing {
// The ScopedValue that tracks the CURRENT_OBSERVER
private static final ScopeValue<Observer> CURRENT_OBSERVER = ScopedValue.newInstance();
public static void runWithObserver(Observer observer, Runnable operation) {
ScopedValue.where(CURRENT_OBSERVER, observer).run(operation);
}
public static Observer getCurrentObserver() {
return CURRENT_OBSERVER.orElse(null);
}
}
public interface Observer {
void notifyChange();
default void observe(Runnable runnable) {
Observing.runWithObserver(this, runnable);
}
}
public class Reactor implements Observer {
private Runnable actor;
private boolean dirty = true;
public Reactor(Runnable a) {
this.actor = a;
}
public void notifyChange() {
if(!dirty) { //we don't act on every notification, only if we're not dirty.
dirty = true;
act();
}
}
private void act() {
// using default observe method inherited from Observer interface
observe(actor);
dirty = false; // we ran, we're now clean
}
public static Reactor begin(Runnable action) {
Reactor actor = new Reactor(action);
Thread.ofVirtual().start(() -> {
actor.act();
});
return actor;
}
}
By wrapping the Runnable actor in the call to “observe()”, we are adding ourselves as the CURRENT_OBSERVER to the ScopedValue. We will show the code that reads this ScopedValue in order to track access by Observers.
I didn’t show the thread-safe implementation of the
notifyChange
or act
, largely because it is
more complex by far if done thread safe. There would be multiple ways to
do this, including just slamming “synchronized” keywords down.
On the other side, we need our containers that actually call our
Observers notifyChange
methods. These can be pre-built
boxes that hold a single value:
public class ValueDataBox<T> implements DataBox<T> {
// here is the value we contain
private final AtomicReference<T> value = new AtomicReference<>();
// we track who called us so we can notify them of a change
private final ConcurrentLinkedQueue<Observer> dependents = new ConcurrentLinkedQueue<>();
public T get() {
addDependent();
return value.get();
}
private void addDependent() {
// refer to our interfaces above, THIS is where we get the CURRENT_OBSERVER from the ScopedValue
Observer dep = Observing.getCurrentObserver();
if (dep != null) {
dependents.add(dep);
}
}
public void set(T newValue) {
final T oldValue = value.getAndSet(newValue);
if (!Objects.equals(oldValue, newValue)) {
// if the value actually changes, let folks know
notifyDependents();
}
}
private void notifyDependents() {
dependents.foreach(Observer::notifyChange);
dependents.clear();
// non-thread-safe implementation for simplicity here
// also important, the list of dependents is cleared when we do this, and so observers are automatically unregistered as listeners.
// to become listeners again, they have to request our data, which they will do upon notification if they are still interested.
}
}
Walk Through of the Flow of Data
The Reactor
at the bottom is what triggers the
recalculation of all the values in the boxes. Without it, everything
just marks itself as “dirty” and then sits. So, a reactor is basically a
Runnable function that does some side effect usually, such as drawing to
a GUI.
Reactor function -> adds itself as the current Observer in the ScopedValue -> calls for values from boxes ->
Boxes ask who is the current Observer from the ScopedValue -> add observer as dependent -> return current value ->
Reactor uses values to draw something
When a box’s value changes:
iterate through dependents -> call `notifyChange` for each -> clear list of dependents
Note that ALL of these various data boxes, observers and reactors you create are serviced by a single ScopedValue -
class Observing {
private static final ScopeValue<Observer> CURRENT_OBSERVER = ScopedValue.newInstance();
}
it’s not like there are many being created to do all this tracking. Furthermore, as a user of this library, you don’t even need to know it exists!
Lazy Calculated Values
We can also have have purely calculated values, where we have a
DataBox implementation, but also we are an Observer, and our get()
method returns a cached value until we receive a
notifyChange
call, and we ditch our cached value. We don’t
eagerly recalculate, but only when our get() method is called and we’re
“dirty”, so to speak. In this way, the system is a push-pull system.
notifications are sent (pushed), but new values are only calculated on
demand (pull, or lazy eval).
Wrapped Containers
We can also do more complex things, such as tracking dependents and
sending notifications for values that we aren’t directly containing. We
can wrap a List, for example, and have multiple Trackers that we manage
somewhat manually upon requests for list data. Ie, when list.iterator()
is called, we have a tracker wholeListTracker
and:
public Iterator<T> iterator() {
wholeListTracker.trackAccess();
return wrappedList.iterator();
}
public boolean add(T newItem) {
wrappedList.add(newItem);
wholeListTracker.notifyChange();
}
And now our wholeListTracker will track observers and send notifications to whomever. Truly wrapping a List and enabling users of the list to depend on notification of any changes, no matter how they happen is involved, but once implemented, is simply usable as a regular list that magically notifies you when it’s changed. If you have a GUI method:
public void drawSomeGUIStuff() {
clearAwayCurrentButtons();
// myMagicNameList is a wrapped List as described above
for(String name : myMagicNameList) {
addButtonForName(name);
}
}
// somewhere, maybe in our constructor or init() method:
Reactor.begin(() -> drawSomeGUIStuff); // <- this starts the process
So, whenever the list changes, we clear the current buttons and redraw them. No other code has to be coupled to this code to make it happen. No other code has to “remember” to notify us, or even know this gui component exists. If the gui component is removed, it will close itself and via some code around the concept of “closing” these observers that I’ve not shown, our draw method is never run again and our component is removed as an observer and we have no listener memory leak.
Difference between Events and State Changes
It should be noted that this is not an event system (I do have an implicit dependency event stream implementation though). The difference between events and state changes is subtle, but crucial. If you have a List set up as a notifying box as above, and then someone adds 3 new items to the list, your reactor functions may get 1, 2, or 3 notifications of changes. The only guarantee is that the last notification received will be for the final and current value of the List.
This is an advantage, if used properly in a system where you are building elements that need to reflect current state, as opposed to in a system where you need to perform actions based on events. In the latter, you need a different mechanism. The advantage of the state reflection system is there is some built-in back-pressure handling, as repeated calls to notifyChange are essentially debounced.
Another advantage is we rarely get to think about our application logic purely in terms of state. We are typically using event-based systems to mimic state based changes, which generally proves awkward and verbose. Also tend to be error prone especially as a system grows and you continually have to fit new behaviors into the current scattered state using various events, and your event functions get more and more complex as they try to keep a growing list of objects correctly in sync. Whereas in a proper state system, you can keep your state together and gather the logic around them, and then build reactors anywhere they need to be to reflect the state. So long as your reactors don’t go changing state, it works very well.
Final Thoughts
This sort of implicit observer setup is not a new idea - you can find
something similar in javascript’s MobX library, and the concepts of
Functional Reactive Programming go back at least to the 90s. I first
encountered a similar framework working at Xerox on embedded printer
GUIs. In that case, we didn’t have ScopedValues, and so when the “cells”
called get() on each other, they actually sent themselves in the call so
the provider cell could add the dependent cell as a dependent. So, the
calls were SomeValue x = dataBoxWithValue.get(this)
. This
had the unfortunate effect that code that worked in this fashion had to
know that it was working as a Cell.
With the ScopedValue, it can be made implicit, and we can wrap otherwise straightforward methods using the Reactor.begin(Runnable) method. It also allows us to manually track read and write access to any property we want, alongside the property, and thus convert external classes to be DataBoxes.
When you build a GUI with this system, you need to shift your thinking about how you organize knowledge of state and interactions. Rather than building models and views and controllers, with controllers sort of wiring everything up between the models and views and views adding themselves as listeners to models, you set up graph structures of state. At the bottom are very simple single-value boxes where simple facts are stored (user is X, user has selected mode Y, mouse is over widget Z). Next are calculated values that look at those simple values, and maybe multiple of them, and calculate some intermediate value. I have used this sort of thing to build up complex Permissions structures, where at bottom is a basic file-level permssion information, but then built on that are more temporary and contextual changes to permissions that can happen due to perhaps multiple users looking at an object, or entering a “read-only” mode for some purpose, or maybe entering read-only mode because other processes are currently running and you want to prevent changes, etc. Your graph of state can be as capable and complex as you like.
And then your GUI representation of state is built in a way where you’re just writing functions that examine current state and draw themselves accordingly, and they will be re-called when any of the state they looked at changes. If you have hundreds of thousands of such elements, you can set up your state and functions such that these notifications go out very selectively, and only the parts of your gui that need to change run their updates.
ScopedValues of course can have many uses, I just think this one is pretty neat.
No comments:
Post a Comment