JDK中的 java.util.concurrent
包,提供了很多并发编程相关的工具。
今天我们来看看 J.U.C 中的 Lock 接口。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock 的作用与synchronized 相似,都是保证线程安全的解决方案。
Lock 中定义了与抢占锁和释放锁相关的操作:
lock()
抢占锁资源方法,如果当前线程没有抢到锁就阻塞。tryLock()
尝试抢占锁资源,成果返回true
,失败返回false
。unLock()
释放锁。
Lock接口的实现类有:
ReentrantLock
重入锁,属于排他锁,功能和synchronized相似,ReentrantReadWriteLock
可重入读写锁,其中包括了 ReadLock和WriteLock。StampedLock
是JDK8新增的锁,是ReentrantReadWriteLock的升级版。
ReentrantLock
是支持重入的排他锁。
排他锁:同一时刻只允许一个线程获得锁资源。
重入锁:如果某个线程已经获得了锁资源,该线程后续再去抢占相同的锁时,不需要再加锁,只需要记录重入次数。synchronized和ReentrantLock都是重入锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
static Lock reentrantLock = new ReentrantLock();
private int num = 0;
public void add(){
//加重入锁
reentrantLock.lock();
try {
num++;
}finally{
//释放锁
reentrantLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
Thread t1 = new Thread(()->{
for(int i=0;i<100;i++) {
reentrantLockDemo.add();
}
});
Thread t2 = new Thread(()->{
for(int i=0;i<100;i++){
reentrantLockDemo.add();
}
});
t1.start();
//保证main线程结束前执行
t1.join();
t2.start();
//保证main线程结束前执行
t2.join();
System.out.println("num = "+reentrantLockDemo.num);
}
}
代码执行结果:
num = 200
上述代码中,ReentrantLockDemo 有一个add()方法,对 非原子性的 num++ 操作加了锁,能够保证 num++ 的线程安全。
ReentrantReadWriteLock
可重入读写锁。
在程序中,针对同一个数据,假设有一个查询方法,一个写入方法。
查询方法是查询数据,不会对数据产生影响,实际上是不需要加锁的。
其实我们想做的是:允许多个线程同时调用查询方法,但是如果有一个线程在调用写入方法,那么这时其他线程不论是调用查询还是写入方法,都要阻塞。
在读多写少的场景中,这种方式可以提高性能。ReentrantReadWriteLock就是这样的锁。
具体表现在:
- 读/读不互斥:多个线程读操作,这些线程可以并发访问
- 读/写互斥:一个线程在读,另一个线程要写,那么写的线程要阻塞;反之相同。
- 写/写互斥:多个线程同时写,会按照互斥规则进行同步。
public class ReentrantReadWriteLockDemo {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
private List<String> list = new ArrayList<>();
public String get(int index){
readLock.lock();
try {
return list.get(index);
}finally {
readLock.unlock();
}
}
public void add(String str){
writeLock.lock();
try{
list.add(str);
}finally {
writeLock.unlock();
}
}
}
ReentrantReadWriteLock 通过readLock 和 writeLock把读和写的操作做了分离。
StampedLock
读写锁有一个问题,在读的过程中,不允许写。如果访问读的线程很多,那么写会一直被阻塞,
为了解决这个问题,引入来StampedLock,它优化了读锁,写锁的访问,其实它是提供了一种乐观锁策略。
StampedLock提供的三种锁的访问方法:
- writeLock 获取写锁,同ReentrantReadWriteLock的写锁
- readLock 获取读锁,同ReentrantReadWriteLock的读锁
- tryOptimisticRead,获取读锁,当有线程获得该读锁时,它不会阻塞其他线程的写操作。
我们来看下面这个代码示例:
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);
}
}
}
它定义了一个Point类,它有x,y两个类变量,表示横纵坐标值。
它的distanceFromOrigin()
方法,是计算当前坐标与原始点位的距离,使用了long stamp = sl.tryOptimisticRead();
来获取读锁,该方法会返回一个stamp值,相当于是一个版本号。通过sl.validate(stamp)
来判断当前线程在读取过程中,有没有其他线程对数据进行了修改,如果没有则返回true,否则返回false。如果验证失败,则通过 readLock() 来获取带阻塞机制的读锁: stamp = sl.readLock();
。
它的moveIfAtOrigin() 方法,是说如果在原始点位,那么就移动。这里,它使用了long ws = sl.tryConvertToWriteLock(stamp);
来将读锁转化为写锁。
StampLock使用乐观锁机制来避免在读多写少的场景中线程占用读锁造成写的阻塞,一定程度上提升来读写锁的并发性能。