内容导读:
1. synchronized 可以放到方法和代码块上,方法和代码块运行完了自动解锁, Lock API 有lock和unlock方法。
2.synchronized 不支持公平锁(抢占式的), Lock API 支持公平锁(按排队顺序,先进入队列的先获取锁)
3. synchronized 如果获取不到锁会一直阻塞等待
4. Lock API 提供了多种策略 lock()会等待, tryLock()加锁成功立即返回true否则返回false ,tryLock(long time, TimeUnit unit) 可以根据参数有限等待。
5. synchronized 加锁等待期间线程不可以被interrupt, Lock API 可以用lockInterruptibly()这个方法加锁,这个方法加锁等待期间,该线程可以被interrupt
6. ReentrantLock 可重入锁,可重入的概念是同一个线程可以的多次调用lock() 返回成功,比如递归调用中,会出现多次lock() ; synchronized 也是可重入的。
7. ReentrantReadWriteLock 可重入读写锁;读锁也叫共享锁,可以被多个线程占用;写锁只能被某个线程独占,写锁也叫排他锁。
8. Lock API 提供了newCondition() , 一个lock可以创建多个Condition ,每个Condition自己控制wait和notify , 和 synchronized 块里的的 wait功能一样,但synchronized至于一个(不分条件)
9. StampedLock 同时支持乐观锁和悲观锁,和锁转换的功能
- 邮戳(Stamp):StampedLock的关键特性之一,是一个long类型的数字,用于标识锁的版本和状态。在获取锁时,会返回一个邮戳值,该值在后续的锁操作中用于验证锁的有效性。
- 不可重入性:StampedLock不支持可重入性,即同一个线程在没有释放锁的情况下无法再次获得锁。
- 锁转换:StampedLock支持锁之间的转换,例如可以将读锁转换为写锁,或将写锁转换为读锁。但需要注意的是,锁转换可能会失败,需要根据返回值判断是否成功。
- 性能优势:StampedLock通过引入乐观读锁和锁转换等特性,在读多写少的场景下提供了更高的并发性能。
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
//下面看看乐观读锁案例
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead(); //获得一个乐观读锁
double currentX = x, currentY = y; //将两个字段读入本地局部变量
if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?
stamp = sl.readLock(); //如果没有,我们再次获得一个读悲观锁
try {
currentX = x; // 将两个字段读入本地局部变量
currentY = y; // 将两个字段读入本地局部变量
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
//下面是悲观读锁案例
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) { //循环,检查当前状态是否符合
long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁
if (ws != 0L) { //这是确认转为写锁是否成功
stamp = ws; //如果成功 替换票据
x = newX; //进行状态改变
y = newY; //进行状态改变
break;
}
else { //如果不能成功转换为写锁
sl.unlockRead(stamp); //我们显式释放读锁
stamp = sl.writeLock(); //显式直接进行写锁 然后再通过循环再试
}
}
} finally {
sl.unlock(stamp); //释放读锁或写锁
}
}
}
1. Overview
Simply put, a lock is a more flexible and sophisticated thread synchronization mechanism than the standard synchronized block.
The Lock interface has been around since Java 1.5. It’s defined inside the java.util.concurrent.lock package, and it provides extensive operations for locking.
In this tutorial, we’ll explore different implementations of the Lock interface and their applications.
2. Differences Between Lock and Synchronized Block
There are a few differences between the use of synchronized block and using Lock APIs:
- A synchronized block is fully contained within a method. We can have Lock APIs lock() and unlock() operation in separate methods.
- A synchronized block doesn’t support the fairness. Any thread can acquire the lock once released, and no preference can be specified. We can achieve fairness within the Lock APIs by specifying the fairness property. It makes sure that the longest waiting thread is given access to the lock.
- A thread gets blocked if it can’t get an access to the synchronized block. The Lock API provides tryLock() method. The thread acquires lock only if it’s available and not held by any other thread. This reduces blocking time of thread waiting for the lock.
- A thread that is in “waiting” state to acquire the access to synchronized block can’t be interrupted. The Lock API provides a method lockInterruptibly() that can be used to interrupt the thread when it’s waiting for the lock.
3. Lock API
Let’s take a look at the methods in the Lock interface:
- void lock() – Acquire the lock if it’s available. If the lock isn’t available, a thread gets blocked until the lock is released.
- void lockInterruptibly() – This is similar to the lock(), but it allows the blocked thread to be interrupted and resume the execution through a thrown java.lang.InterruptedException.
- boolean tryLock() – This is a nonblocking version of lock() method. It attempts to acquire the lock immediately, return true if locking succeeds.
- boolean tryLock(long timeout, TimeUnit timeUnit) – This is similar to tryLock(), except it waits up the given timeout before giving up trying to acquire the Lock.
- void unlock() unlocks the Lock instance.
A locked instance should always be unlocked to avoid deadlock condition.
A recommended code block to use the lock should contain a try/catch and finally block:
Lock lock = ...;
lock.lock();try {
// access to the shared resource
} finally {
lock.unlock();
}
In addition to the Lock interface, we have a ReadWriteLock interface that maintains a pair of locks, one for read-only operations and one for the write operation. The read lock may be simultaneously held by multiple threads as long as there is no write.
ReadWriteLock declares methods to acquire read or write locks:
- Lock readLock() returns the lock that’s used for reading.
- Lock writeLock() returns the lock that’s used for writing.
4. Lock Implementations
4.1. ReentrantLock
ReentrantLock class implements the Lock interface. It offers the same concurrency and memory semantics as the implicit monitor lock accessed using synchronized methods and statements, with extended capabilities.
Let’s see how we can use ReentrantLock for synchronization:
public class SharedObjectWithLock {
//...
ReentrantLock lock = new ReentrantLock();
int counter = 0;
public void perform() {
lock.lock();
try {
// Critical section here
count++;
} finally {
lock.unlock();
}
}
//...
}
We need to make sure that we are wrapping the lock() and the unlock() calls in the try-finally block to avoid the deadlock situations.
Let’s see how the tryLock() works:
public void performTryLock(){
//...
boolean isLockAcquired = lock.tryLock(1, TimeUnit.SECONDS);
if(isLockAcquired) {
try {
//Critical section here
} finally {
lock.unlock();
}
}
//...
}
In this case, the thread calling tryLock() will wait for one second and will give up waiting if the lock isn’t available.
4.2. ReentrantReadWriteLock
ReentrantReadWriteLock class implements the ReadWriteLock interface.
Let’s see the rules for acquiring the ReadLock or WriteLock by a thread:
- Read Lock – If no thread acquired the write lock or requested for it, multiple threads can acquire the read lock.
- Write Lock – If no threads are reading or writing, only one thread can acquire the write lock.
Let’s look at how to make use of the ReadWriteLock:
public class SynchronizedHashMapWithReadWriteLock {
Map<String,String> syncHashMap = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
// ...
Lock writeLock = lock.writeLock();
public void put(String key, String value) {
try {
writeLock.lock();
syncHashMap.put(key, value);
} finally {
writeLock.unlock();
}
}
...
public String remove(String key){
try {
writeLock.lock();
return syncHashMap.remove(key);
} finally {
writeLock.unlock();
}
}
//...
}
For both write methods, we need to surround the critical section with the write lock — only one thread can get access to it:
Lock readLock = lock.readLock();//...public String get(String key){
try {
readLock.lock();
return syncHashMap.get(key);
} finally {
readLock.unlock();
}
}
public boolean containsKey(String key) {
try {
readLock.lock();
return syncHashMap.containsKey(key);
} finally {
readLock.unlock();
}
}
For both read methods, we need to surround the critical section with the read lock. Multiple threads can get access to this section if no write operation is in progress.
4.3. StampedLock
StampedLock is introduced in Java 8. It also supports both read and write locks.
However, lock acquisition methods return a stamp that is used to release a lock or to check if the lock is still valid:
public class StampedLockDemo {
Map<String,String> map = new HashMap<>();
private StampedLock lock = new StampedLock();
public void put(String key, String value){
long stamp = lock.writeLock();
try {
map.put(key, value);
} finally {
lock.unlockWrite(stamp);
}
}
public String get(String key) throws InterruptedException {
long stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlockRead(stamp);
}
}
}
Another feature provided by StampedLock is optimistic locking. Most of the time, read operations don’t need to wait for write operation completion, and as a result of this, the full-fledged read lock isn’t required.
Instead, we can upgrade to read lock:
public String readWithOptimisticLock(String key) {
long stamp = lock.tryOptimisticRead();
String value = map.get(key);
if(!lock.validate(stamp)) {
stamp = lock.readLock();
try {
return map.get(key);
} finally {
lock.unlock(stamp);
}
}
return value;
}
5. Working With Conditions
The Condition class provides the ability for a thread to wait for some condition to occur while executing the critical section.
This can occur when a thread acquires the access to the critical section but doesn’t have the necessary condition to perform its operation. For example, a reader thread can get access to the lock of a shared queue that still doesn’t have any data to consume.
Traditionally Java provides wait(), notify() and notifyAll() methods for thread intercommunication.
Conditions have similar mechanisms, but we can also specify multiple conditions:
public class ReentrantLockWithCondition {
Stack<String> stack = new Stack<>();
int CAPACITY = 5;
ReentrantLock lock = new ReentrantLock();
Condition stackEmptyCondition = lock.newCondition();
Condition stackFullCondition = lock.newCondition();
public void pushToStack(String item){
try {
lock.lock();
while(stack.size() == CAPACITY) {
stackFullCondition.await();
}
stack.push(item);
stackEmptyCondition.signalAll();
} finally {
lock.unlock();
}
}
public String popFromStack() {
try {
lock.lock();
while(stack.size() == 0) {
stackEmptyCondition.await();
}
return stack.pop();
} finally {
stackFullCondition.signalAll();
lock.unlock();
}
}
}
6. Conclusion
In this article, we saw different implementations of the Lock interface and the newly introduced StampedLock class.
We also explored how we can make use of the Condition class to work with multiple conditions.