A live JavaFX scene graph can only be accessed and modified on the JavaFX application thread. This restriction is usually not enforced, but there are some places where checks are built into the framework (for example, adding and removing nodes from the scene graph).
Performing such checks for FX properties would be nice, but could impose a performance penalty that we probably wouldn't be comfortable accepting. Here's an idea of how we can enforce the FX thread invariant in many more places on a best effort basis, without imposing a significant performance penalty. The idea is to build a heuristic into `StyleableProperty` implementations that might be able to catch at least some illegal writes (and potentially reads). Here's how the implementation would look like for `StyleableObjectProperty`: public abstract class StyleableObjectProperty<T> extends ObjectPropertyBase<T> implements StyleableProperty<T> { ... @Override public void applyStyle(StyleOrigin origin, T v) { try { suppressThreadCheck = true; set(v); this.origin = origin; } finally { suppressThreadCheck = false; } } ... @Override public void set(T v) { super.set(v); checkAccess(); origin = StyleOrigin.USER; } ... private StyleOrigin origin = null; private boolean suppressThreadCheck; private boolean disableThreadCheck; private void checkAccess() { if (!suppressThreadCheck && !disableThreadCheck && getBean() instanceof Node node) { Scene scene = node.getScene(); if (scene != null) { Window window = scene.getWindow(); if (window != null && WindowHelper.getPeer(window) != null) { Toolkit.getToolkit().checkFxUserThread(); disableThreadCheck = true; } } } } } Note that the two added boolean fields will not increase the memory footprint of the property instance on most VM configurations, as there are some unused bytes available in the current memory layout. Here's how the heuristic works: 1. The calling thread is never checked when calling the `applyStyle()` method. The rationale is that `applyStyle()` is probably never called by user code, but it is often called by the CSS engine. No need to check the calling thread. 2. If the property is hosted on a `Node`, invoking write methods (set/bind/bindBidirectional) will call `checkAccess()`, which checks whether the node is connected to a live scene graph showing in a window. The presence of a window peer is a prerequisite for the FX thread check, as it is legal to configure JavaFX nodes on any thread as long as the nodes are not connected to a live scene graph. Note that calling `Node::getScene`, `Scene::getWindow` or `WindowHelper::getPeer` of a live scene graph on a non-FX thread is illegal. If connected to a live scene graph, `checkAccess()` is not legally callable from any thread other than the FX thread. However, in many cases, it will still be possible to determine the presence of a window peer. 3. If `checkAccess()` is legally called on the FX thread, the first such call will disable the FX thread check for all subsequent calls. Otherwise an exception is thrown. It might be interesting to notice that no attempt is made to synchronize access to `disableThreadCheck`, even though the field is potentially read by multiple threads. This means that while a legal call to `set()` on the FX thread will set the `disableThreadCheck` flag, a call to the same method on a non-FX thread might not be able to see the new value of the field. But this is not a problem, as the call will fail with an exception anyway. Now, since this scheme is biased toward very little runtime overhead, it will not be able to catch all illegal writes. There are two basic assumptions here: 1. The first mutation of a property after the node has been configured and added to the scene graph is often the offending call of an invalid cross-thread write. This is the only point in time when we perform the expensive thread check. 2. Nodes are usually configured before being added to a live scene graph. If an application first adds nodes to a live scene graph and then configures their properties, the thread checks for all affected properties will be disabled instantly by the manual configuration. We won't be able to catch illegal writes of the affected properties after that. Here are some things to think about: 1. Instead of disabling the thread check after the first legal write, we could also opt to disable it after the _second_ legal write. This would allow us to catch cases where a property is legally configured after the node is added to a live scene graph, but then written on a non-FX thread. 2. We could also call `checkAccess()` from the property's getter if we are comfortable to read the two boolean fields every time the value of the property is retrieved. However, this might not catch many illegal calls to the getter, as it is likely that the thread check would often be disabled by the JavaFX framework itself (as calling getters happens quite often).