We finish the section by examining Java's locking mechanisms at the class and object level and how we can synchronize methods and blocks of code to protect static or instance variables from concurrent access problems.
Lets take a look at the points outlined at the Oracle Website for this part of the certification.
- Section 4: Concurrency
- Given a scenario, write code that makes appropriate use of object locking to protect static or instance variables from concurrent access problems.
Running separate threads of execution at the same time is great and we can certainly improve the performance of our programs using multithreading. But what happens when concurrent threads need to modify the contents of a file or some state within an instance? We could end up with corrupt data, lost updates, unstable object state and all sorts of other unpleasantness when multiple threads are all trying to update the same resource at the same time. What we really need is a way to control access to sensitive data so that only one thread has access to the resource at a time. Java deals with this problem using synchronization and in this lesson we investigate how we can control access to our code by synchronizing at the method or block level.
In Java every class has a lock and each instance of a class we instantiate also has it's own lock. Most of the time we write code these locks are transparent and until reading this sentence you may have been totally
unaware about Javas locking system. Its only when we have to deal with situations where a resource can be updated concurrently that we have be aware of locking. So how does locking work? Well locking only comes into
play when we synchronize our code using the
synchronized keyword. As mentioned above we can lock methods or blocks of code this way and when we do this we lock this section of code from being executed
by other threads for the duration of the section of code.
The main emphasis of the lesson will be about synchronization of objects, but before we get into this we will talk briefly about
static initializer blocks and
static methods. We don't need
to worry about
static initializer blocks as these are inherently thread safe, as they only ever get called once per class-loader. When we apply synchronization to the
static methods of
a class, only one of the synchronized
static methods can execute at any point in time. This type of locking is done at the class level and a class lock is totally independent of any object locks, in
much the same way as
static members are independent of any instance. The way we synchronize
static methods is no different from the way we synchronize non-static methods; just
remember they use different locking mechanisms and apply to a class and an instance of a class respectively.
At the method level synchronization is achieved by checking the lock that every instance has associated with it. When a thread tries to enter a synchronized method, it first checks to see if the lock for this object is available, or put another way, unlocked. If it is then the thread is allowed entry to the method and the lock for this object is locked. This applies to all synchronized methods within the class, so once a lock has been set for an object all other synchronized methods are locked out for that object. When the object returns from the method that the lock was set on, the object becomes unlocked, so any threads waiting for this object's lock have a chance to enter a synchronized method and restart the locking/unlocking process. In this way we can guarantee that we only ever enter one synchronized method for an object at a time.
A really important thing to remember here is that we are only excluding other threads of the same object from entering the synchronized code. If we had three threads, representing three different objects, then they all have different locks and therefore could all use the same synchronized method concurrently. Also we don't need to worry about local variables as each thread gets its own set of these on entry to a method. Two threads running the same method concurrently will have different copies of the local variables that will work independently.
This is a lot of information to get our heads around so we will illustrate some synchronization usage with a slide show; just press the button below to step through each slide.
See Using the synchronized Keyword method for example code of the above slideshow.
Using synchroniaztion does come at a cost, when we synchronize a method there is extra processing that needs to be done every time the method is entered to check that the lock is available. There is also the fact that while one thread has the lock, any other threads for this object that need to use the synchronized method are locked out and queued, until the active thread releases the lock. Java also allows us to synchronize a block of code instead of a whole method, which can make the lock time less of an overhead and therefore our synchronized code more efficient.
See Synchronized Blocks for example code using synchronized blocks.
There is a situation that can arise when using synchronized code that we as programmers need to be aware, which is known as deadlocking. A deadlock can occur when an object is locked and the thread of execution tries to access another object. The other object is also locked and is trying to access this thread of executions locked object. The object locks are basically locking each other out forever and thus are deadlocked.
See Deadlock Scenario for example code that can cause a deadlock.