Today
we will see the difference between Lock and Synchronized block but before we go
into details, let's understand what is Lock and how it works.
What
is Lock ?
Lock
interface was introduced in jdk 1.5 along with other concurrency utilities like
countdown latch etc. ReentrantLock is the main concrete implementation of lock
interface which behaves as mutually exclusive lock like synchronized block.
So the
next question comes in mind is why, or most importantly when, one should prefer
Lock when we already have synchronized. There are some trade offs between these
two and Reentrant Lock can be used in couple of conditions.
For
example, Lock provides explicit locking and so it has more readability
comparing to synchronized. You will see this in below examples.
1.
Fairness:
The
ReentrantLock constructor offers a choice of two fairness options: create a
non-fair lock or a fair lock. With fair locking, threads can acquire locks only
in the order in which they were requested, whereas an unfair lock allows a lock
to acquire it out of its turn. This is called barging (breaking
the queue and acquiring the lock when it became available).
Fair
locking has a significant performance cost because of the overhead of
suspending and resuming threads. There could be cases where there is a
significant delay between when a suspended thread is resumed and when it
actually runs. Let's see a situation:
A
-> holds a lock.
B
-> has requested and is in a suspended state waiting for A to release
the lock.
C
-> requests the lock at the same time that A releases the lock, and has
not yet gone to a suspended state.
As C
has not yet gone to a suspended state, there is a chance that it can acquire
the lock released by A, use it, and release it before B even finishes waking
up. So, in this context, unfair lock has a significant performance advantage.
2.
Polled and Timed Lock Acquisition:
Let's
see some example code:
public
void transferMoneyWithSync(Account fromAccount, Account toAccount,
float amount) throws
InsufficientAmountException {
synchronized (fromAccount) {
// acquired lock on fromAccount Object
synchronized (toAccount) {
// acquired lock on toAccount Object
if (amount >
fromAccount.getCurrentAmount()) {
throw new InsufficientAmountException(
"Insufficient Balance");
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
In the transferMoney() method above, there is a possibility of deadlock when two threads
A and B are trying to transfer money at almost the same time.
A:
transferMoney(acc1, acc2, 20);
B:
transferMoney(acc2, acc1 ,25);
It is
possible that thread A has acquired a lock on the acc1 object and is waiting to
acquire a lock on the acc2 object. Meanwhile, thread B has acquired a lock on
the acc2 object and is waiting for a lock on acc1. This will lead to deadlock,
and the system would have to be restarted! There is, however, a way to avoid
this, which is called "lock ordering." Personally, I find this a bit
complex.
A
cleaner approach is implemented by ReentrantLock with the use of tryLock()
method. This approach is called the "timed and polled
lock-acquisition." It lets you regain control if you cannot acquire all
the required locks, release the ones you have acquired and retry.
So, using
tryLock, we will attempt to acquire both locks. If we cannot attain both, we
will release if one of these has been acquired, then retry
public
boolean transferMoneyWithTryLock(Account fromAccount,
Account toAccount, float amount) throws
InsufficientAmountException, InterruptedException
{
// we are defining a stopTime
long stopTime = System.nanoTime() + 5000;
while (true) {
if (fromAccount.lock.tryLock()) {
try {
if (toAccount.lock.tryLock()) {
try {
if (amount >
fromAccount.getCurrentAmount()) {
throw new
InsufficientAmountException(
"Insufficient Balance");
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
} finally {
toAccount.lock.unlock();
}
}
} finally {
fromAccount.lock.unlock();
}
}
if(System.nanoTime() < stopTime)
return false;
Thread.sleep(100);
}//while
}
Here
we implemented a timed lock, so if the locks cannot be acquired within the
specified time, the transferMoney method will return a failure notice and exit
gracefully. We can also maintain time budget activities using this
concept.
3.
Interruptible Lock Acquisition:
Interruptible
lock acquisition allows locking to be used within cancellable activities.
The lockInterruptibly method
allows us to try and acquire a lock while being available for interruption. So,
basically it allows the thread to immediately react to the interrupt signal
sent to it from another thread.
This
can be helpful when we want to send a KILL signal to all the waiting locks.
Let's see one example: Suppose we have a shared line to send messages. We would
want to design it in such a way that if another thread comes and interrupts the
current thread, the lock should release and perform the exit or shut down
operations to cancel the current task.
public
boolean sendOnSharedLine(String message) throws InterruptedException{
lock.lockInterruptibly();
try{
return cancellableSendOnSharedLine(message);
} finally {
lock.unlock();
}
}
private
boolean cancellableSendOnSharedLine(String message){
.......
4.
Non-block Structured Locking:
In
intrinsic locks, acquire-release pairs are block-structured. In other words, a
lock is always released in the same basic block in which it was acquired,
regardless of how control exits the block. Extrinsic locks allow the facility
to have more explicit control. Some concepts, like Lock Strapping, can be
achieved more easily using extrinsic locks. Some use cases are seen in
hash-bashed collections and linked lists.
private
ReentrantLock lock;
public void foo() {
public void foo() {
...
lock.lock();
...
}
public
void bar() {
...
lock.unlock();
...
}
Intrinsic
locks and extrinsic locks have the same mechanism inside for locking, so the
performance improvement is purely subjective. It depends on the use cases we
discussed above. Extrinsic locks give a more explicit control mechanism for
better handling of deadlocks, starvation, and so on
When
should you use ReentrantLocks?
The
answer is pretty simple - use it when you actually need something it provides
that synchronized doesn't, like timed lock waits, interruptible lock waits,
non-block-structured locks, multiple condition variables, or lock polling.
ReentrantLock also has scalability benefits, and you should use it if you
actually have a situation that exhibits high contention, but remember that the
vast majority of synchronized blocks hardly ever exhibit any contention, let
alone high contention. I would advise developing with synchronization until
synchronization has proven to be inadequate, rather than simply assuming
"the performance will be better" if you use ReentrantLock.
Remember,
these are advanced tools for advanced users. (And truly advanced users tend to
prefer the simplest tools they can find until they're convinced the simple
tools are inadequate.) As always, make it right first, and then worry about
whether or not you have to make it faster.
No comments:
Post a Comment