前言
之前的文章我已经提到过一个jdk中的独占锁synchronized
synchronized是java的一个关键字,也就是java语言内置的特性。如果一个代码块被synchronized修饰,当一个线程获取到锁之后,其它需要获取该锁的线程便需要一直等待,等待获取锁的线程释放锁,获取锁的线程释放锁有三种情况:
1、持有锁的线程执行完代码块,然后线程会释放对锁的占有
2、线程执行抛出异常,此时JVM会让线程释放锁
3、持有锁的线程调用了wait()方法,在等待的时候立即释放锁。
正文
一、有了synchronized(内置锁)为什么还要使用Lock呢?
lock可以
1、尝试非阻塞的获取锁
2、获取锁的过程可以被中断
3、可以超时获取锁
二、java.util.concurrent.locks包下常用的类与接口
根据上图,我们来逐一分析
1、Lock接口
Lock接口源码
public interface Lock {
void lock(); // 获取锁
void lockInterruptibly() throws InterruptedException; // 可中断的获取锁
boolean tryLock(); // 尝试获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //超时获取锁
void unlock(); //释放锁
Condition newCondition(); //获取一个新的Condition实例
}
(1)、lock()
lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生,并且获取锁的操作应该方法try的外面,防止误释放锁。
lock()方法使用模板
/**
* Lock使用标准
*
* @Author YUBIN
*/
public class LockTemplete {
public static void main(String[] args) {
Lock lock = new ReentrantLock(); // 可重入锁
lock.lock();// 获取锁 放在try外面 防止释放了其它线程的锁
try {
// do my work ...
}finally {
lock.unlock(); // 释放锁
}
}
}
(2)、tryLock()、tryLock(time,unit)
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true;如果获取失败(即锁已被其他线程获取),则返回false,也就是说,这个方法无论如何都会立即返回(在拿不到锁时不会一直在那等待)。
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
tryLock()使用模板
/**
* tryLock()使用模板
*
* @Author YUBIN
*/
public class TryLockTemplate {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// 获取到锁的处理逻辑
} catch (Exception e) {
// 异常处理
} finally {
lock.unlock();
}
} else {
// 没有获取到锁的处理逻辑
}
}
}
(3)、lockInterruptibly()
lockInterruptibly()方法比较特殊,尝试获取锁,获取锁的过程是可以中断的。举个例子有两个线程threadA和threadB都调用了lockInterruptibly()在同一时刻只会有一个线程获取到锁,假如threadA获取到了锁那么在threadA获取到锁到释放锁的过程中threadB是一直处于等待状态的,如果在threadA中调用了threadB.interrupt()中断方法能够中断threadB线程的等待状态。
由于lock.lockInterruptibly()的声明中抛出了中断异常,所以lock.lockInterruptibly()必须方法try/catch块中,或者在调用该方法的地方抛出异常,但推荐使用后者。
lockInterruptibly()方法使用模板
public void method(Lock lock) throws InterruptedException {
lock.lockInterruptibly();
try {
//执行代码
} catch (Exception e) {
// 异常处理
} finally {
lock.unlock();
}
}
2、ReentrantLock可重入锁
ReentrantLock,即 可重入锁。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
什么叫做可重入呢?最典型的一个例子就是递归的时候发生锁的重入。它是通过当前线程和一个整型变量来实现的。
具体的使用方法会在下面的基于Lock和Condition实现一个阻塞队列来演示。
3、ReadWritrLock
ReadWriteLock也是一个接口,在它里面只定义了两个方法:
public interface ReadWriteLock {
Lock readLock(); // 获取读锁
Lock writeLock(); // 获取写锁
}
一个用来获取读锁,一个用来获取写锁。也就是说,将对临界资源的读写操作分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作,当有一个线程获取了写锁,其它不管是需要获取读锁还是写锁的线程都是处于阻塞的。下面的 ReentrantReadWriteLock 实现了 ReadWriteLock 接口。
4、ReentrantReadWriteLock读写锁
ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和writeLock()用来获取读锁ReadLock和写锁WriteLock。下面通过几个例子来看一下读写锁在读多写少的场景下与synchronized独占锁的区别
商品的实体类
public class Goods {
private final String id;
private Long totalSaleNumber; // 总销售数
private Long depotNumber; // 当前库存数
public Goods(String id, Long totalSaleNumber, Long depotNumber) {
this.id = id;
this.totalSaleNumber = totalSaleNumber;
this.depotNumber = depotNumber;
}
public Long getTotalSaleNumber() {
return totalSaleNumber;
}
public Long getDepotNumber() {
return depotNumber;
}
public void setGoodsNumber(Long changeNumber) {
this.totalSaleNumber += changeNumber;
this.depotNumber -= changeNumber;
}
}
操作商品数量的接口
public interface IGoodsNum {
Goods getGoodsNumber();
void setGoodsNumber(Long changeNumber);
}
使用synchronized操作商品数量
public class NumSync implements IGoodsNum {
private Goods goods;
public NumSync(Goods goods) {
this.goods = goods;
}
@Override
public synchronized Goods getGoodsNumber() {
try {
// 此处休眠5ms来演示从数据库中读取商品数据
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return goods;
}
@Override
public synchronized void setGoodsNumber(Long changeNumber) {
try {
// 此处休眠50ms来演示修改商品的数量
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.setGoodsNumber(changeNumber);
}
}
使用ReentrantReadWriteLock操作商品数量
public class RwNumSync implements IGoodsNum {
private Goods goods;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock rLock = lock.readLock(); // 获取读锁
private final Lock wLock = lock.writeLock(); // 获取写锁
public RwNumSync(Goods goods) {
this.goods = goods;
}
@Override
public Goods getGoodsNumber() {
rLock.lock();
try {
SleepUtils.second(1);
return goods;
}finally {
rLock.unlock();
}
}
@Override
public void setGoodsNumber(Long changeNumber) {
wLock.lock();
try {
SleepUtils.second(50);
goods.setGoodsNumber(changeNumber);
}finally {
wLock.unlock();
}
}
}
测试类
public class Test {
private static final int threadRatio = 10; // 读线程与写线程的比例
private static final int threadBaseCount = 3; // 写线程的数量
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) {
Goods goods = new Goods("goods001", 100000L, 10000L);
// 使用Synchronized操作商品数量
IGoodsNum iGoodsNum = new NumSync(goods);
// 使用ReentrantReadWriteLock操作商品的数量
//IGoodsNum iGoodsNum = new RwNumSync(goods);
for (int i = 0; i < threadBaseCount * threadRatio; i++) {
Thread rThread = new Thread(new ReadThread(iGoodsNum));
rThread.start();
}
for (int i = 0; i < threadBaseCount; i++) {
Thread wThread = new Thread(new WriteThread(iGoodsNum));
wThread.start();
}
countDownLatch.countDown();
}
// 读线程
private static class ReadThread implements Runnable {
private IGoodsNum iGoodsNum;
public ReadThread(IGoodsNum iGoodsNum) {
this.iGoodsNum = iGoodsNum;
}
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
iGoodsNum.getGoodsNumber();
}
long duration = System.currentTimeMillis() - start;
System.out.println(Thread.currentThread().getName() + "读取库存数据耗时:" + duration);
}
}
// 写线程
private static class WriteThread implements Runnable {
private IGoodsNum iGoodsNum;
public WriteThread(IGoodsNum iGoodsNum) {
this.iGoodsNum = iGoodsNum;
}
@Override
public void run() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long start = System.currentTimeMillis();
Random random = new Random();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
iGoodsNum.setGoodsNumber(Long.valueOf(random.nextInt(10)));
}
long duration = System.currentTimeMillis() - start;
System.out.println(Thread.currentThread().getName() + "写库存数据耗时:" + duration);
}
}
}
运行结果
(1)、使用Synchronized操作商品数量
Thread-22读取库存数据耗时:4428
Thread-31写库存数据耗时:5662
Thread-30写库存数据耗时:5882
Thread-21读取库存数据耗时:8202
Thread-32写库存数据耗时:8933
Thread-0读取库存数据耗时:12876
Thread-10读取库存数据耗时:13053
Thread-18读取库存数据耗时:13528
Thread-3读取库存数据耗时:13542
Thread-7读取库存数据耗时:13888
Thread-27读取库存数据耗时:14195
Thread-14读取库存数据耗时:14389
Thread-24读取库存数据耗时:14535
Thread-2读取库存数据耗时:14605
Thread-26读取库存数据耗时:14865
Thread-11读取库存数据耗时:15038
Thread-15读取库存数据耗时:15288
Thread-23读取库存数据耗时:15638
Thread-12读取库存数据耗时:15694
Thread-19读取库存数据耗时:15848
Thread-17读取库存数据耗时:15933
Thread-1读取库存数据耗时:16272
Thread-28读取库存数据耗时:16354
Thread-13读取库存数据耗时:16403
Thread-8读取库存数据耗时:16496
Thread-16读取库存数据耗时:16514
Thread-25读取库存数据耗时:16657
Thread-5读取库存数据耗时:16727
Thread-29读取库存数据耗时:16782
Thread-20读取库存数据耗时:16809
Thread-9读取库存数据耗时:16874
Thread-6读取库存数据耗时:16965
Thread-4读取库存数据耗时:17198
(2)、使用ReentrantReadWriteLock操作商品数量
Thread-7读取库存数据耗时:1129
Thread-3读取库存数据耗时:1130
Thread-11读取库存数据耗时:1135
Thread-15读取库存数据耗时:1135
Thread-19读取库存数据耗时:1125
Thread-23读取库存数据耗时:1124
Thread-27读取库存数据耗时:1124
Thread-31写库存数据耗时:1781
Thread-30写库存数据耗时:1826
Thread-32写库存数据耗时:1886
Thread-1读取库存数据耗时:1926
Thread-17读取库存数据耗时:1913
Thread-13读取库存数据耗时:1914
Thread-5读取库存数据耗时:1915
Thread-9读取库存数据耗时:1915
Thread-21读取库存数据耗时:1910
Thread-29读取库存数据耗时:1910
Thread-25读取库存数据耗时:1911
Thread-2读取库存数据耗时:1976
Thread-18读取库存数据耗时:1962
Thread-6读取库存数据耗时:1963
Thread-10读取库存数据耗时:1964
Thread-14读取库存数据耗时:1964
Thread-22读取库存数据耗时:1963
Thread-26读取库存数据耗时:1963
Thread-8读取库存数据耗时:2013
Thread-0读取库存数据耗时:2017
Thread-12读取库存数据耗时:2018
Thread-4读取库存数据耗时:2020
Thread-16读取库存数据耗时:2010
Thread-20读取库存数据耗时:2013
Thread-24读取库存数据耗时:2013
Thread-28读取库存数据耗时:2014
我们可以看到,使用ReentrantReadWriteLock操作商品数量比使用Synchronized高效的多,因为同一时刻允许多个读线程同时获取到读锁,这样就大大提升了读操作的效率。不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程也会一直等待释放写锁。