Abstract: Locking on Integer objects has always been a bad idea. And we now have a way to find such bad code with a runtime switch DiagnoseSyncOnValueBasedClasses.
Welcome to the 299th edition of The Java(tm) Specialists' Newsletter, written and conceived in Greece, the birthplace of democracy. A couple of years ago, we were having a delicious birthday cake to celebrate our oldest daughter's 19th, when our son piped up: "Hey, we should get a pony." Being the ever practical, I immediately shot down the idea, listing all the reasons why this could not possibly work. Where would he sleep? What would he eat? Who would clean his stable every morning? After thinking about it for a few days, I realized that I had been a bit of an ass, close relative of said pony. We did have enough space for the critter, and maybe our youngest could one day go on pony rides with her friends? But I had no idea just how stubborn such an animal could be. He got into his head that our driveway was lava. With every fibre in his young body, he resisted any efforts to get him to venture off our lawns. We tried pulling him. Then we tried pushing him. We offered carrots, apples and sugar lumps, which he greedily munched down before jumping back onto the grass. The more we strained, the more he dug in his little hooves. Across the driveway was freedom, but he fought it as if his life depended on it. Eventually my son said: "Why don't we put down a green carpet?" We found an old abandoned olive harvesting net, and spread it across the driveway in the place where he was to cross. Apples coaxed him right to the edge, and with a bit of a tug, he hopped onto the green net. Amazingly, to him at least, he didn't instantly explode in a ball of flames. The driveway wasn't lava after all! After about a week of getting him used to the net, we now have him proudly trotting up and down the paving. I am pretty sure we will get some fun stories out of him, as we explore the countryside together. We are quite an odd couple wandering around Tersanas, these two large round creatures, with wild hair, and mulish stubbornness.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
We can synchronize on any Java object. This is thus perfectly legal Java code:
synchronized (new Object()) { System.out.println("I have a monitor - but what for?"); }
It is (mostly) pointless, because synchronized is used primarily for mutual exclusion. By making a new object every time, each thread would be synchronizing on their own monitor.
The BankAccount is even more bizarre. It violates several best-practices. Firstly, we are synchronizing on a non-final field, a definite no-no. Secondly, that field is changing every time we deposit or withdraw money, thus the lock also changes. It is not thread-safe at all.
// NOT thread-safe public class BankAccount { private Double balance; public BankAccount(double balance) { this.balance = balance; } public void deposit(double amount) { synchronized (balance) { // never lock on primitive wrapper! balance += amount; } } public void withdraw(double amount) { deposit(-amount); } public double getBalance() { synchronized (balance) { return balance; } } }
Our BankAccountDemo demonstrates the race condition:
import java.util.stream.*; public class BankAccountDemo { public static void main(String... args) { var account = new BankAccount(1000); IntStream.range(0, 1_000_000) .parallel() .forEach(i -> { account.deposit(100); account.withdraw(100); }); System.out.println(account.getBalance()); } }
With output something like:
387400.0
It was always considered a bad idea to synchronize on the
primitive wrapper classes. This has now been formalized with
the @ValueBased
annotation. According to the
Javadocs,
these are the requirements (quoted verbatim):
Some classes, such as java.lang.Integer
and
java.time.LocalDate
, are value-based. A
value-based class has the following properties:
equals()
,
hashCode()
, and toString()
compute their results solely from the values of
the class's instance fields (and the members of the objects
they reference), not from the instance's identity;equals()
produces no visible change in the
behavior of the class's methods;equals()
, they may also be equal according to
==
;
When two instances of a value-based class are equal
(according to equals()
), a program should not
attempt to distinguish between their identities, whether
directly via reference equality or indirectly via an appeal
to synchronization, identity hashing, serialization, or any
other identity-sensitive mechanism.
Synchronization on instances of value-based classes is strongly discouraged, because the programmer cannot guarantee exclusive ownership of the associated monitor.
Identity-related behavior of value-based classes may change in a future release. For example, synchronization may fail.
Value-based classes are not only good for improving our coding style, they serve as a preparation for Project Valhalla, where we will have, amongst other things, primitive classes. See also JEP 390: Warnings for Value-Based Classes.
The following classes are annotated with the
@ValueBased
annotation in Java 17:
The only value-based classes with public constructors are the primitive wrapper classes Long, Integer, Character, etc. However, these constructors have been deprecated for removal.
We can find out whether a thread is currently locking with
synchronized
. The
ThreadMXBean
provides ThreadInfo
objects on which we can get information about the locks
using getLockedMonitors()
. Our convenience class
Monitors
class finds the
MonitorInfo
objects on the current thread:
import java.lang.management.*; public class Monitors { private static final ThreadMXBean tmb = ManagementFactory.getThreadMXBean(); private Monitors() {} public static MonitorInfo[] findLockedMonitors() { long[] ids = {Thread.currentThread().getId()}; var threadInfo = tmb.getThreadInfo(ids, true, false)[0]; return threadInfo.getLockedMonitors(); } }
Here is a demo of how that could be used:
import java.lang.management.*; import java.util.*; public class MonitorsDemo { public static void main(String... args) { printInfo(); Object lock1 = new Object(); synchronized (lock1) { printInfo(); } Object lock2 = new Object(); synchronized (lock1) { synchronized (lock2) { printInfo(); } } } private static void printInfo() { System.out.println("Monitors locked by current thread:"); MonitorInfo[] monitors = Monitors.findLockedMonitors(); if (monitors.length == 0) System.out.println("\tnone"); else Arrays.stream(monitors) .forEach(monitor -> System.out.println("\t" + monitor)); System.out.println(); } }
The output is going to be something like this:
Monitors locked by current thread: none Monitors locked by current thread: java.lang.Object@776ec8df Monitors locked by current thread: java.lang.Object@58372a00 java.lang.Object@776ec8df
Note that we do not get access to the objects that we are locking on, but rather, its type (e.g. java.lang.Object) and its identity hash code.
In the value-based documentation above, it states clearly that we should not synchronize on these types of classes. The objects may be shared instances, and thus there is no guarantee that we will have exclusive access to them. Locking on the shared instances could introduce bad contention or deadlocks. So how can we find whether our code, or any library that we are using, synchronizes on value-based objects?
Fortunately the JVM has a diagnostic setting that allows us
to find them at runtime. Simply start your Java Virtual
Machine with -XX:+UnlockDiagnosticVMOptions
-XX:DiagnoseSyncOnValueBasedClasses=2
and it will log
whenever we try to synchronize on a value-based class.
For example:
import java.lang.management.*; import java.time.*; // Use these JVM flags: // -XX:+UnlockDiagnosticVMOptions // -XX:DiagnoseSyncOnValueBasedClasses=0..2 public class LockingOnValueBasedClasses { public static void main(String... args) { synchronize(42); synchronize("Hello"); synchronize(LocalDate.now()); } private static void synchronize(Object o) { synchronized (o) { System.out.println("We now hold the lock of " + o); for (MonitorInfo monitor : Monitors.findLockedMonitors()) { System.out.println("\t" + monitor); } } } }
Running this code without any flags, we see:
We now hold the lock of 42 java.lang.Integer@56a567a2 We now hold the lock of Hello java.lang.String@3b9a45b3 We now hold the lock of 2022-03-31 java.time.LocalDate@5f184fc6
When we run this with
-XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=2
, we see
the following output:
[0.071s][info][valuebasedclasses] Synchronizing on object 0x00000007ff8788b0 of klass java.lang.Integer [0.071s][info][valuebasedclasses] at LockingOnValueBasedClasses.synchronize(LockingOnValueBasedClasses.java:17) [0.071s][info][valuebasedclasses] - locked <0x00000007ff8788b0> (a java.lang.Integer) [0.071s][info][valuebasedclasses] at LockingOnValueBasedClasses.main(LockingOnValueBasedClasses.java:9) We now hold the lock of 42 java.lang.Integer@56a567a2 We now hold the lock of Hello java.lang.String@3b9a45b3 [0.120s][info][valuebasedclasses] Synchronizing on object 0x000000043fcb59e8 of klass java.time.LocalDate [0.120s][info][valuebasedclasses] at LockingOnValueBasedClasses.synchronize(LockingOnValueBasedClasses.java:17) [0.120s][info][valuebasedclasses] - locked <0x000000043fcb59e8> (a java.time.LocalDate) [0.120s][info][valuebasedclasses] at LockingOnValueBasedClasses.main(LockingOnValueBasedClasses.java:13) We now hold the lock of 2022-03-31 java.time.LocalDate@5f184fc6
Furthermore, if we run it with
-XX:DiagnoseSyncOnValueBasedClasses=1
, the
JVM crashes the first time that it encounters a synchronized
on a value-based class:
# # A fatal error has been detected by the Java Runtime Environment: # # Internal Error (synchronizer.cpp:397), pid=988, tid=6403 # fatal error: Synchronizing on object 0x00000007ff8788b0 of klass java.lang.Integer at LockingOnValueBasedClasses.synchronize(LockingOnValueBasedClasses.java:17) # etc.
We should strongly discourage synchronizing on String objects.
Constant strings can be shared throughout the JVM, causing
the same issues as we can imagine with value-based classes.
However, there are several characteristics that disqualify
String from being a @ValueBased
class:
hash
and
hashCode
, used to cache the O(n)
hashCode()
result;Kind regards
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.