Why did Java and C# add intrinsic lock to every object?
Asked Answered
C

4

18

Making every object lockable looks like a design mistake:

  1. You add extra cost for every object created, even though you'll actually use it only in a tiny fraction of the objects.
  2. Lock usage become implicit, having lockMap.get(key).lock() is more readable than synchronization on arbitrary objects, eg, synchronize (key) {...}.
  3. Synchronized methods can cause subtle error of users locking the object with the synchronized methods
  4. You can be sure that when passing an object to a 3rd parting API, it's lock is not being used.

eg

class Syncer {
    synchronized void foo(){}
}
...
Syncer s = new Syncer();
synchronize(s) {
    ...
}
// in another thread
s.foo() // oops, waiting for previous section, deadlocks potential
  1. Not to mention the namespace polution for each and every object (in C# at least the methods are static, in Java synchronization primitives have to use await, not to overload wait in Object...)

However I'm sure there is some reason for this design. What is the great benefit of intrinsic locks?

Contravention answered 29/8, 2012 at 19:3 Comment(6)
Your code in #3 is actually just fine: synchronized(o) { synchronized(o) { synchronized(o) { ... } } } is perfectly safe in Java, and equivalent to synchronized(o) { ... }. The thread won't "lock itself out", or anything like that, if that's what you're expecting.Bairam
I wouldn't expect the "extra cost for every object created" to be much at all, actually; this page seems to suggest that the only cost when you're not actually using synchronization is ~2 bits.Torrey
@Bairam I know locks are reentrant, but the problem is people will use the same lock you use without noticing. Doing things like locking the map object in one thread, and putting something into it (which is synchronized) in other thread.Contravention
@LouisWasserman the cost is not only in bytes, the cost is in API weight, code size (more problematic to provide VM with no synchronization support) etc. And anyway I don't see any benefit.Contravention
Dear closers, I was looking for the benefits and design considerations of having intrinsic locks. How can I improve the answer to reopen it?Contravention
"You can be sure that when passing", did you mean "you can't"?Starnes
D
1

You add extra cost for every object created, even though you'll actually use it only in a tiny fraction of the objects.

That's determined by the JVM implementation. The JVM specification says, "The association of a monitor with an object may be managed in various ways that are beyond the scope of this specification. For instance, the monitor may be allocated and deallocated at the same time as the object. Alternatively, it may be dynamically allocated at the time when a thread attempts to gain exclusive access to the object and freed at some later time when no thread remains in the monitor for the object."

I haven't looked at much JVM source code yet, but I'd be really surprised if any of the common JVMs handled this inefficiently.

Lock usage become implicit, having lockMap.get(key).lock() is more readable than synchronization on arbitrary objects, eg, synchronize (key) {...}.

I completely disagree. Once you know the meaning of synchronize, it's much more readable than a chain of method calls.

Synchronized methods can cause subtle error of users locking the object with the synchronized methods

That's why you need to know the meaning of synchronize. If you read about what it does, then avoiding these errors becomes fairly trivial. Rule of thumb: Don't use the same lock in multiple places unless those places need to share the same lock. The same thing could be said of any language's lock/mutex strategy.

You can be sure that when passing an object to a 3rd parting API, it's lock is not being used.

Right. That's usually a good thing. If it's locked, there should be a good reason why it's locked. Other threads (third party or not) need to wait their turns.

If you synchronize on myObject with the intent of allowing other threads to use myObject at the same time, you're doing it wrong. You could just as easily synchronize the same code block using myOtherObject if that would help.

Not to mention the namespace polution for each and every object (in C# at least the methods are static, in Java synchronization primitives have to use await, not to overload wait in Object...)

The Object class does include some convenience methods related to synchronization, namely notify(), notifyAll(), and wait(). The fact that you haven't needed to use them doesn't mean they aren't useful. You could just as easily complain about clone(), equals(), toString(), etc.

Dragonroot answered 15/5, 2018 at 22:28 Comment(6)
You need at least bit per object, to know if lock was used. I'm pro synchronized (lck) {...} but only for ILockables. So most of the other points are invalid given that. Document what's lockable, we both agree with that.Contravention
The JVM only requires an object reference (not an actual object) to acquire a lock. If the JVM implementation uses that reference as a key to look up the lock in a separate table, then no additional data needs to be added to objects. Objects that aren't used for synchronization simply wouldn't be included in such a table, so the memory requirement would be minimal.Dragonroot
I checked the source for OpenJDK 6, and found in BytecodeInterpreter.cpp that the interpreter state includes a stack of monitors that are currently in use. It doesn't appear to waste memory by storing lock state as part of every object created. I suspect all the popular JVMs use a similar data structure. (After all, why would a JVM be popular if it's written in a naive and wasteful way?)Dragonroot
Looks like I was wrong. The OpenJDK JVM actually does use three bits in an object's header to store information about the lock state. The header is exactly one word in size and contains other useful data, so these bits aren't causing additional memory to be allocated. More details at wiki.openjdk.java.net/display/HotSpot/SynchronizationDragonroot
happy you found out that indeed it does takes at least three bits! Indeed, every decision has cost, and as you mentioned, sometimes you already paid the cost of allocation (but not cost of testing those bits).Contravention
That cost of testing those bits doesn't really matter because: 1) Those bits aren't tested unless the object is used for synchronization. 2) The cost of testing those bits would be about the same no matter where they resided. In fact, putting them in the object header might speed things up a bit if reduces the number of memory fetches required.Dragonroot
V
0

Actually you only have reference to that monitor in each object; the real monitor object is created only when you use synchronization => not so much memory is lost.

The alternative would be to add manually monitor to those classes that you need; this would complicate the code very much and would be more error-prone. Java has traded performance for productivity.

Venturesome answered 29/8, 2012 at 19:8 Comment(5)
I don't see why is it complicated and error prone. The best practice is already using manual private locks. I think Effective Java advice this.Contravention
@ElazarLeibovich Java has evolved; read the second edition of Effective Java.Venturesome
I've read it. If I understand you correctly, you're claiming that it was indeed an early design mistake.Contravention
@ElazarLeibovich: The existence of a better practice does not inherently invalidate the lesser practice. E.g. string+string concatenation is often avoided in favor of better approaches (String.Format, interpolation, StringBuilder), but that doesn't mean that the language should make it impossible to do string+string altogether. Similarly, even if manual private locks are a better practice, that does not mean that other methods of locking should therefore be made impossible. There is always a degree of acceptability. It's not a matter of "only the best option should be allowed".Manon
@Manon as specified in the question, it is a bad idea regardless of other practices.Contravention
T
0

One benefit is automatic unlock on exit from synchronized block, even by exception.

Tessy answered 29/8, 2012 at 19:8 Comment(3)
Yeah, but this could be gained by having synchronized block accepting Locker interface.Contravention
@ElazarLeibovich Can only guess, but it would make compiler a bit more complex. And it may make internal JVM features such as thread dumps more complex as well.Tessy
I really don't see why thread dumps would be more complex, the complexity is equivalent to other "magic" interface feature, ie, SerializableContravention
R
0

I assume that like toString(), the designers thought that the benifits outweighed the costs.

Lots of decisions had to be made and a lot of the concepts were untested (Checked exceptions-ack!) but overall I'm sure it's pretty much free and more useful than an explicit "Lock" object.

Also do you add a "Lock" object to the language or the library? Seems like a language construct, but objects in the library very rarely (if ever?) have special treatment, but treating threading more as a library construct might have slowed things down..

Rowena answered 29/8, 2012 at 19:11 Comment(4)
OK, but what are those benefits which outweigh the costs?Contravention
And even if you want a language construct, you could at least use a magic interface like Cloneable.Contravention
My guess is that it made a difference in performance to not have locks tied to a specific object in a library. Since I didn't design the language I can't say for sure, It could be that assuming the lock byte is in the same place in an object allowed them to do some threading trickery and speed processing beyond what it could have done if the lock objects were in a library. It's unlikely that they would have encumbered Object for something that could easily have been done in a library, and with java most early tradeoffs were for performance.Rowena
BUT I do think that they assumed the lock(this) style semantic would have been much more usable/maintainable than it is. Java pioneered a lot of stuff and they certainly didn't make ALL the right decisions, but they did come up with a language that is much easier to use than C++ and pretty much never performs significantly worse for similar code.Rowena

© 2022 - 2024 — McMap. All rights reserved.