Java Theory and Practice: a more flexible and scalable locking mechanism in JDK 5.0

Source: Internet
Author: User
Tags exception handling finally block variables variable thread thread class versions visibility
Telescopic
Content:

Synchronized Quick Review

The improvement of the synchronized

Compare the scalability of Reentrantlock and synchronized

Condition variable

It's not fair

Conclusion

Resources


About the author

The evaluation of the paper



Related content:

Java Theory and Practice series

Synchronization is not the enemy

Reducing contention

IBM developer Kits for the Java platform (downloads)




Subscription:

DeveloperWorks Newsletter

DeveloperWorks Subscriptions
(Subscribe to CDs and downloads)






New locking classes improve synchronization--but you can't just leave synchronized now.
Level: Intermediate



Brian Goetz (brian@quiotix.com)
Chief advisor, Quiotix
November 2004

JDK 5.0 provides developers with a number of powerful new choices for developing high-performance concurrent applications. For example, the class Reentrantlock in Java.util.concurrent.lock is replaced by the synchronized feature in the Java language, which has the same memory semantics, the same locks, but better performance under race conditions, plus Synchronized offers no additional features. Does this mean that we should forget about synchronized and use Reentrantlock instead? The concurrency expert Brian Goetz, who just returned from his summer vacation, will provide us with an answer.
Multithreading and concurrency are not new, but one of the innovations in the Java language design is that it is the first major language to integrate a cross-platform threading model and a formal memory model into the language. The Core class library contains a thread class that you can use to build, start, and manipulate threads, and the Java language includes constructs--synchronized and volatile that communicate concurrency constraints across threads. While simplifying the development of a platform-independent concurrency class, it has never made it much more cumbersome to write concurrent classes, just making it easier.

Synchronized Quick Review
Declaring a block of code as synchronized has two important consequences, usually the code has atomicity (atomicity) and visibility (visibility). Atomicity means that a thread can only execute code that is protected by a specified monitor object (lock) at a time, preventing multiple threads from conflicting with each other while updating the shared state. Visibility is more subtle, and it copes with the various perverse behaviors of memory caching and compiler optimizations. In general, threads are in a way that they do not have to be immediately visible to other threads, regardless of whether the threads are in registers, in processor-specific caching, or through command rearrangement or other compiler optimizations, is not constrained by the value of the cached variable, but if the developer uses synchronization, as shown in the following code, The runtime will ensure that a thread's update to a variable is preceded by an update to an existing synchronized block, and when you enter another synchronized block that is protected by the same monitor (lock), these changes to the variable are immediately visible. Similar rules exist on volatile variables. (For the contents of the synchronization and Java memory models, see resources.) )


Synchronized (lockobject) {
Update Object state
}




Therefore, the implementation of synchronization needs to consider all the necessary security to update multiple shared variables, there can be no race conditions, can not destroy the data (assuming the boundary of the synchronization is correct), and to ensure that other threads of correct synchronization can see the latest values of these variables. By defining a clear, cross-platform memory model (which is modified in JDK 5.0 to correct some of the errors in the original definition), it is possible to build the concurrent class "write once, run anywhere" by following this simple rule:

Whenever you write a variable that might then be read by another thread, or the variable you will read is finally written by another thread, you must synchronize.
But now it's a little bit better, in the most recent JVM, the performance costs of not competing synchronizations (when one thread owns a lock, and no other thread attempts to acquire a lock) are low. (And not always; synchronization in the early JVM has not yet been optimized, so many people think so, but now this becomes a misconception that, whether it's contention or not, synchronization has a high performance cost.) )

The improvement of the synchronized
So it seems that the synchronization is pretty good, right? So why did the JSR 166 team spend so much time developing the Java.util.concurrent.lock framework? The answer is simple-sync is good, but it's not perfect. It has some functional limitations-it can't interrupt a thread that is waiting to get a lock, and it can't get a lock by voting, and if you don't want to wait, you can't get a lock. Synchronization also requires that the release of a lock be made only in the same stack frame as the stack frame where the lock was obtained, which is, in most cases, not a problem (and it interacts well with exception handling), but there are cases where the locking of a block structure is more appropriate.

Reentrantlock class
The lock frame in Java.util.concurrent.lock is an abstraction of the lock, which allows the implementation of the lock as a Java class rather than as a language feature. This leaves space for the various implementations of lock, which may have different scheduling algorithms, performance characteristics, or locking semantics. The Reentrantlock class implements lock, which has the same concurrency and memory semantics as the synchronized, but adds features such as lock voting, timed lock waiting, and interruptible lock waiting. In addition, it provides better performance in the case of intense contention. (In other words, when many threads want to access shared resources, the JVM can spend less time scheduling threads, and more on execution threads.) )

What does a reentrant lock mean? In short, it has a lock-related fetch counter, and if a thread that owns the lock gets the lock again, the fetch counter is added 1, and the lock needs to be released two times to get real release. This mimics the semantics of the synchronized; If a thread enters a synchronized block protected by a monitor already owned by the thread, it allows the thread to proceed, not releasing the lock when the thread exits the second (or subsequent) synchronized block. The lock is released only if the thread exits the first synchronized block it enters into the monitor protection.

When you look at the code example in Listing 1, you can see that there is a noticeable difference between Lock and synchronized--lock must be released in the finally block. Otherwise, if the protected code throws an exception, the lock may never be released! This distinction may seem to be nothing, but in fact it is extremely important. Forgetting to release the lock in the finally block may leave a ticking time bomb in the program, and you'll have to spend a lot of effort to find out where the source is when the bomb explodes one day. With synchronization, the JVM will ensure that locks are automatically freed.

Listing 1. Protect blocks of code with Reentrantlock.

Lock lock = new Reentrantlock ();

Lock.lock ();
try {
Update Object state
}
finally {
Lock.unlock ();
}




In addition, contention Reentrantlock implementations are more scalable than the current synchronized implementations. (in future JVM versions, synchronized's contention performance is likely to improve.) This means that when many threads are competing for the same lock, the total cost of using reentrantlock is usually much less than synchronized.

Compare the scalability of Reentrantlock and synchronized
Tim Peierls constructs a simple evaluation using a simple linear congruent pseudo random number generator (PRNG) to measure the relative scalability between synchronized and Lock. This example is good because PRNG does some work every time nextrandom () is invoked, so the benchmark program is actually measuring a reasonable, real synchronized and Lock application. Rather than testing the code purely on paper or doing nothing (like many so-called benchmark programs). )

In this benchmark program, there is a pseudorandom interface, which has only one method nextrandom (int bound). This interface is very similar to the functionality of the Java.util.Random class. Because when the next random number is generated, PRNG uses the latest generated numbers as input, and maintains the last generated number as an instance variable, with the emphasis on keeping the code snippet that updates this state from being preempted by other threads, so I'm going to use some sort of lock to make sure of that. (The Java.util.Random class can do this, too.) We have built two implementations for pseudorandom, one using syncronized and the other using Java.util.concurrent.ReentrantLock. The driver generates a large number of threads, each of which is frantically vying for the time slice, and then calculates how many rounds can be executed per second by different versions. Figure 1 and Figure 2 summarize the results of the number of different threads. This evaluation is not perfect and runs on only two systems (one is dual Xeon running Hyper-threading Linux and the other is a single-processor Windows system), but it should be sufficient to demonstrate the scalability advantage of synchronized compared to Reentrantlock.

Figure 1. Throughput rate of synchronized and Lock, single CPU


Figure 2. Throughput rate of synchronized and Lock (after normalization), 4 CPUs


The graphs in Figure 1 and Figure 2 show throughput in units of calls per second, adjusting different implementations to 1 thread synchronized. Each implementation is concentrated relatively quickly on the throughput rate of a stable state that typically requires the processor to be fully utilized, most of the processor time spent on the actual work (computer random numbers), and only a fraction of the time spent on thread scheduling expenses. You will notice that the synchronized version behaves poorly when it handles any type of contention, while the Lock version spends a relatively small amount of time scheduling expenses, leaving room for higher throughput to enable more efficient CPU utilization.

Condition variable
The root class Object contains special methods for communicating between wait (), notify (), and Notifyall () of threads. These are advanced concurrency features that many developers have never used-and this can be a good thing because they are quite subtle and easy to use. Fortunately, with the introduction of Java.util.concurrent in JDK 5.0, there is almost no place for developers to use these methods.

There is an interaction between a notification and a lock--you must hold the lock of the object in order to wait or notify on the object. Just as the lock is a generalization of synchronization, the lock framework contains a generalization of wait and notify, a generalization called a condition (Condition). The lock object acts as a factory object that is bound to the condition variable of the lock, and, unlike the standard wait and notify methods, you can have more than one condition variable associated with the specified lock. This simplifies the development of many concurrent algorithms. For example, Javadoc for a condition (Condition) shows an example of a bounded buffer implementation that uses two conditional variables, "not full" and "not empty", which uses only one wait per lock The implementation of the settings is more readable (and more efficient). The Condition method is similar to the wait, notify, and Notifyall methods, named await, signal, and Signalall, respectively, because they cannot override the corresponding method on the Object.

It's not fair
If you look at Javadoc, you will see that one of the parameters of the Reentrantlock constructor is a Boolean value that allows you to choose whether you want a fair (fair) lock or an unfair (unfair) lock. Fair locks enable the threads to acquire locks in the order in which they are requested, whereas unfair locks allow bargaining, in which case the thread can sometimes get a lock first than the other thread that first requested the lock.

Why don't we just let all the locks be fair? After all, fairness is good, not fair is bad, isn't it? (When children want a decision, they always shout "it's not fair.") We think fairness is very important and the children know it. In reality, it is fair to ensure that locks are very robust locks and have significant performance costs. Ensuring that the accounting (bookkeeping) and synchronization required for fairness means that the competing fair locks are less than the rate of the unfair locks. As the default setting, you should set fair to false, unless fairness is critical to your algorithm, and it needs to be serviced strictly in the order in which the threads are queued.

So what about sync? is the built-in monitor lock fair? Many people are surprised by the answers, which are unfair and never fair. But no one complains about thread hunger, because the JVM guarantees that all threads will eventually get the locks they're waiting for. Ensuring statistical fairness, for the most part, is sufficient, and the cost is much lower than the absolute fairness guarantee. So, by default, Reentrantlock is "unfair", and the fact that it is just a matter of what is always in sync is superficial. If you don't mind this when synchronizing, then don't worry about it when you reentrantlock.

Figure 3 and Figure 4 contain the same data as Figure 1 and Figure 2, just adding a dataset for random-number benchmark detection, which uses a fair lock instead of the default negotiation lock. As you can see, there is a price for fairness. If you need to be fair, you have to pay the price, but please do not use it as your default choice.

Figure 3. The relative throughput rate of synchronization, negotiation locks, and fair locks when using 4 CPUs


Figure 4. Relative throughput rate for synchronization, negotiation, and fair locks when using 1 CPUs


Is it good everywhere?
It looks like Reentrantlock is better than synchronized in every way--all synchronized can do it, it has the same memory and concurrency semantics as synchronized, and it has a special and has better performance under load. So should we forget about synchronized and stop treating it as a good idea that has already been optimized? Or even rewrite our existing synchronized code with Reentrantlock? In fact, several introductory books in Java programming use this approach in their multithreaded chapters, using Lock as an example, and synchronized as history. But I think it's a good thing to do too much.

And don't leave synchronized.
Although Reentrantlock is a very moving implementation, relative synchronized, it has some important advantages, but I think the rush to synchronized as a indignity, is definitely a serious mistake. The locking class in Java.util.concurrent.lock is a tool for advanced users and advanced situations. In general, you should continue to use synchronized unless you have a clear need for one of the advanced features of Lock, or if you have clear evidence (rather than just suspicion) that synchronization has become a bottleneck for scalability in specific situations.

Why am I advocating conservatism in the use of a clearly "better" implementation? Because of the locking class in Java.util.concurrent.lock, synchronized still has some advantages. For example, when using synchronized, you cannot forget to release the lock, and the JVM will do this for you when you exit the synchronized block. It is easy to forget to release the lock with a finally block, which is very harmful to the program. Your program can pass the test, but it will have a deadlock in the actual work, which can be difficult to point out (which is a good reason why the primary developer is not allowed to use lock at all.) )

Another reason is that when the JVM uses synchronized to manage lock requests and releases, the JVM can include locking information when generating thread dumps. These are valuable for debugging because they can identify the source of deadlocks or other unusual behavior. The lock class is just a normal class, and the JVM does not know which thread owns the lock object. Also, almost every developer is familiar with synchronized, and it can work in all versions of the JVM. Before JDK 5.0 becomes standard (which can take two years from now), using the Lock class will mean that the features to be exploited are not available to every JVM and are not familiar to every developer.

When do you choose to replace synchronized with Reentrantlock?
In that case, when should we use Reentrantlock? The answer is very simple-when you really need something that synchronized does not have, such as time lock waiting, interruptible lock waiting, no block structure lock, multiple condition variables, or lock voting. Reentrantlock also has the benefit of scalability and should be used in highly competitive situations, but keep in mind that most synchronized blocks have almost never been used for contention, so you can put a high contention aside. I suggest using synchronized to develop until it is true that synchronized is not appropriate, not just to assume that if you use Reentrantlock "performance will be better". Keep in mind that these are advanced tools for use by advanced users. (And, true senior users prefer to choose the simplest tools they can find until they think the simple tools don't apply.) )。 As always, the first thing to do is to do it well and then consider whether it is necessary to do it faster.

Conclusion
The Lock framework is a synchronized, compatible alternative that provides many of the features not provided by synchronized, and its implementation provides better performance in contention. However, these obvious benefits are not enough to justify the substitution of synchronized with Reentrantlock. Instead, make a choice based on whether you need the ability to reentrantlock. In most cases, you should not choose it--synchronized works well, can work on all JVMs, more developers understand it, and is less prone to error. Use it only when you really need Lock. In these cases, you will be happy to have this tool.



Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.