I’ve described already why instrumenting synchronized methods is an instrumentation with global effects: We need a “try to enter” logging call before the lock is claimed, and Java synchronized methods automatically do that, so the logging call needs to happen at the call site before the call is made.
Corky, my advisor, pointed towards an interesting 2004 paper on JaRec by Georges, et al. The implementation shares many aspect with the approach we are following and I’ll have to study it more closely, but one interesting idea I could glean from it already was a way to make instrumenting synchronized methods have only local effect:
A synchronized method
synchronized T foo(T0 t0, T1 t1, ...) {
S
}
is transformed into an unsynchronized method and a synchronized block, which can then later be instrumented in the usual manner:
T foo(T0 t0, T1 t1, ...) {
synchronized(this) {
S
}
}
For a static synchronized method, instead of this
‘s lock, the lock of the class object is claimed. On a Java bytecode level, case has to be taken that the lock is released properly in case of exceptions, since this now has to be handled manually.
I don’t know why I didn’t test this way myself in the first place. I can only assume I wasn’t sure about the precise semantics of synchronized methods at the time. Also, this scheme won’t work for synchronized native methods. They will still have to be handled with the wrapper approach, incurring global effects.
This is nonetheless a very interesting approach that I will have to inspect. On the one hand, it reduces the absolute number of changes that have to be made (by making the instrumentation have only local effects), on the other hand, it changes the number of ways in which class files need to be changed (by transforming synchronized methods to the canonical case of a synchronized block).
Unfortunately, the test I just ran showed me I still have problems instrumenting classes deep inside the runtime.