一篇就理解Java的Lock锁

版权声明:

本公众号发布的所有文章,均属于原创,版权归本公众号所有。

允许有条件转载,转载请附带底部二维码。

一、前言

虽然在Java中可以通过synchroinzed关键字来加锁限定线程间的互斥,保持线程同步实现线程安全。除了synchroinzed之外,JDK5之后还提供了更高级的锁,Lock。

实际上,Lock是一个interface,而实际上开发人员需要面对的,通常只有ReentrantLock和ReentrantReadWriteLock,下面就围绕这两个类进行讲解。

二、ReentrantLock

1、Lock和Condition这两个接口

前面提到,Lock是一个接口,所有需要使用锁的类都需要继承这个接口,在其中定义了需要实现的最基本的方法。

  • lock():获取锁资源,如果没有争抢到,进入阻塞状态。
  • tryLock(time,unit):在一定时间范围内,尝试获取锁资源。
  • unlock():释放锁资源。

从Lock中提供的基本API可以看到,它只负责获取锁和释放锁。如果需要实现类似wait()/notify()这种等待/通知的机制,还需要借助Condition接口。

Condition接口提供了最基本的实现等待/通知机制的API,但是它比wait()/notify()更强大,这里先介绍一些它最基本的API。

  • await():让当前线程进入WAITING状态,同时释放锁,类似wait()的作用。
  • signal():通知某个处于WAITING状态的线程,可以继续获取锁执行。类似notify()的作用。
  • signalAll():通知所有处于WAITING状态的线程,开始争抢锁然后执行。类似notifyAll()的作用。

2、ReentrantLock

ReentrantLock是直接实现了Lock接口的。通过Lock中定义的方法,可以实现一个简单的线程间同步。

下面就单一生产者和消费者的模式,实现交替打印Log,进行一个简单的例子说明问题。

一篇就理解Java的Lock锁_第1张图片

可以看到,这里实现了一个单一生产者/消费者的模式,两边相互打印出执行的Log。其中使用ReentrantLock来获取锁,使用Condition来实现通信。

需要注意的是,和wait()、notify()方法一样,调用await()、signal()两个方法的时候,也必须是已经获取到锁的时候才允许这样调用,否者会抛出InterruptedException。

在上面的例子中,如果存在多个生产者或者消费者的情况,又会出现某些线程永远得不到执行的情况,这个使用和notifyAll()的解决方案一样,使用signalAll()方法即可解决,这里就不再Demo演示了。

3、多Condition

从ReentrantLock.newCondition()方法的实现可以看出来,其实这个Condition是每次都new出来的,所以是一个Lock可以存在多个Condition的。类似于一个说存在多个条件,各个条件管控自己的await()和signal()状态,相互之间并不影响。

一篇就理解Java的Lock锁_第2张图片

4、公平锁和非公平锁

锁也是有区分的,可以简单分为公平锁和非公平锁。公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即是一个先进先出的FIFO的队列;而非公平锁就是一种随机的分配,不关乎先后顺序,非公平锁会随机分配一个处于就绪状态的线程来获取锁并执行相应的逻辑。

公平锁和非公平锁各有优劣,公平锁需要维护一个队列,需要知道各个线程进入就绪状态的顺序,而非公平锁则不需要,只需要每次随机分配一个已经处于就绪状态的线程即可。

之前介绍的synchronized就是一种比较有显著特点的非公平锁。

在ReentrantLock中,存在两个构造函数,可以指定是使用那种锁,这样就更灵活了

一篇就理解Java的Lock锁_第3张图片

可以看到ReentrantLock主要是使用FairSync()和NonfairSync()来确定是使用公平锁还是非公平锁。默认情况下,如果不指定,则为非公平锁。

5、Lock相关的其他API

上面只是介绍了一些比较常用的API,其实还有一些高级点的API,这里简单进行讲解一下,就不用demo的形式样式了,有兴趣的可以自行coding验证一下。

  • Lock.getHoldCount():查询当前线程保持这个锁的个数,也就是调用lock()的线程个数。
  • Lock.getQueueLength():查询当前处于就绪状态,正在等待获取此锁的线程个数。
  • Lock.getWaitQueueLength(condition):查询指定个condition中,处于WAITING状态的线程个数。
  • Lock.hasQueuedThread(thread):查询指定线程是否正在等待获取此锁。
  • Lock.hasQueuedThreads():查询是否有线程正在等待获取此锁。
  • Lock.hasWaiters(condition):查询指定线程是否正在等待与此锁相关的condition条件。
  • isFair():判断当前锁是否是公平锁。
  • isHeldByCurrentThread():查询当前线程是否保留此锁。
  • isLocked():查询此锁定是否由任意线程持有。
  • lockInterruptibly():为当前线程获取锁,如果当前线程已经被中断了,抛出异常。
  • tryLock():尝试获取锁,如果未被其他线程持有锁,则获取锁。
  • tryLock(timeout,unit):在一定时间范围内,尝试获取锁,超时将不再尝试。

三、ReentrantReadWriteLock

1、什么是读写锁

前面介绍的ReentrantLock和synchronized全部是一种排他锁,也就是说,同一时间只有一个线程在执行加锁后面的任务。这样虽然可以保证了实例变量的线程安全性,但是效率也会相对低下,因为有时候我们只是需要读取数据,但是又怕读到的数据是脏数据,所以才被逼无奈加锁的。

那么实际上,JDK同样考虑到这一点,提供了一种读写锁ReentrantReadWriteLock的支持,它可以限定读锁和写锁,在不同的锁下有不同的互斥效果。

这种锁既然叫读写锁,表示也是两个锁,一个是读操作相关的,是共享锁;另一个是写操作相关的锁,它就是个单纯的排他锁。也就是说,多个读锁之间是不互斥的,而读锁和写锁之间或者多个写锁之间是互斥的。

总结来说,规律如下:

  • 读锁-写锁:互斥。
  • 写锁-写锁:互斥。
  • 读锁-读锁:共享不互斥。

2、读写锁怎么使用

ReentrantReadWriteLock看源码其实根本不实现Lock接口。但是内部维护了两个内部类:ReadLock、WriteLock,这两个就是读写锁中,分别实现的读锁和写锁,他们是实现了Lock接口的。

一篇就理解Java的Lock锁_第4张图片

所以只需要在获取读锁的时候,调用writeLock,获取写锁的时候,调用readLock即可。

使用的方式其实和ReentrantLock的模式一样,这里就不提供Demo了。

四、小结

Lock对象基本可以替代掉synchronized关键字,而且所具备的特性也是synchronized所不具备的,要根据需求灵活使用。


你可能感兴趣的:(一篇就理解Java的Lock锁)