详解JDK锁01:Lock接口
1. Lock简介
先引用Lock接口源码中作者贴的一段话
Lock implementations provide more extensive locking operations than can be obtained using synchronized methods and statements. They allow more
flexible structuring
, may have quitedifferent properties
, and may supportmultiple associated Condition objects
.
其实这段话就简单概括了Lock的三大优点:
- 灵活的结构:可以显式地获取与释放锁
- 多种不同的属性与方法
- 引入了
Condition
对象
接下来的部分将着重介绍这几点
2. Lock锁的灵活性
2.1 Lock接口方法
在 JDK5.0 之前,Java是借助于 Synchronized
关键字实现加锁功能,而这个功能是通过JVM实现的。而在 JDK5.0 之后,JUC包中新增了Lock接口实现锁功能。
虽然该Lock接口不具备 Synchronized
关键字隐式获取锁的便捷性,但是其提供了一系列手动操作锁的方法:
- 阻塞式地获取锁
该方法有一定的缺陷:如果当前锁被占用,那么当前线程将被禁用,进入阻塞状态,直到获取到锁为止。
void lock();
- 阻塞式地可打断地获取锁
void lockInterruptibly() throws InterruptedException;
- 虽然是阻塞式地获取锁,但是如果该线程被中断后,会抛出异常,停止继续阻塞。
- 非阻塞式地获取锁尝试非阻塞地获取锁,调用该方法后立即返回
- 若能够获取到锁,则返回 true
- 若锁已被占用,则返回 false
boolean tryLock();
该方法的典型使用场景为:
// 伪代码 Lock lock = new ...; if (lock.tryLock() { try { // 操作共享资源 } finally { // 释放锁 lock.unlock(); } } else { // 未获取到锁,执行其余操作 }
- 带有超时时间地获取锁尝试在指定的一段时间内获取锁
- 若在指定时间
time
内能够获取到锁,且未被中断,则返回 true - 若指定时间
time
结束后仍未获取到锁,则返回 false - 若在指定时间
time
内被打断,则抛出InterruptedException
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
其中 time
代表指定的超时时间,unit
代表时间单位
- 释放锁
void unlock();
2.2 灵活性体现
使用 Synchronized
关键字进行获取锁与释放锁操作时:
当嵌套式地获取锁之后,其释放锁的顺序必须与获取锁的顺序相反
如下获取锁顺序为:lock1 -> lock2 -> lock3
释放锁顺序为:lock3 -> lock2 -> lock1
从外到内获取锁,从内到外释放锁
Object lock1 = new Object(); Object lock2 = new Object(); Object lock3 = new Object(); synchronized (lock1) { System.out.println("获取到lock1锁"); synchronized (lock2) { System.out.println("获取到lock2锁"); synchronized (lock3) { System.out.println("获取到lock3锁"); } } }
但是我们假设存在这一业务需求:
先获取锁A,再获取锁B,再释放锁A,再获取锁C,再释放锁B,再获取锁D。
这种获取锁的顺序与释放锁的顺序是不固定的,此时无法用 Synchronized
解决。
而采用Lock接口实现锁则可以完美解决这一问题,因为它提供了手动的加锁解锁方法!
3. Lock锁的多种功能
Lock接口中虽然只提供了简单的获取锁与释放锁的基本方法,但是其实现类ReentrantLock中实现了多种方法,提供了不同的功能。
这一篇文章只对Lock接口进行详细介绍,所以以下只做简单的文字介绍。
后续文章会通过源码解读
ReentrantLock
- 实现公平锁与非公平锁。实例化ReentrantLock时,其有参构造方法中传入的值为boolean类型。若传入值为true,为公平锁;否则为非公平锁
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
- 判断锁是否已经被持有
public boolean isLocked() { return sync.isLocked(); }
- 判断锁是否为公平锁
public final boolean isFair() { return sync instanceof FairSync; }
相较于Lock接口,Synchronized
只实现了非公平锁。
4. Condition基本使用
回顾 Synchronized
关键字,其实现 等待/通知
的模式是通过 Object
类内部的 wait
、notify
以及 notifyAll
实现的。
Object lock = new Object(); Thread thread1 = new Thread(() -> { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("Thread1已被唤醒"); } }); thread1.start(); Thread.sleep(2000); synchronized (lock) { System.out.println("唤醒Thread1"); lock.notify(); // lock.notifyAll(); }
其中,notify
方法是唤醒 lock
锁上的其中一个线程,notifyAll
方法是唤醒 lock
锁上的全部线程。
然而,这两种方法均不能指定想要唤醒的线程。
Condition的出现很好地解决了这一问题,可以分组唤醒想要唤醒的线程。
如下为Condition的基本实现方式:需要使用 ReentrantLock
实现 Lock
接口
后续文章会详细解读
Condition
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); new Thread(() -> { lock.lock(); try { try { System.out.println("Thread1进入等待"); condition.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("Thread1已被唤醒"); } finally { lock.unlock(); } }).start(); Thread.sleep(3000); new Thread(() -> { lock.lock(); try { System.out.println("唤醒Thread1"); condition.signal(); } finally { lock.unlock(); } }).start();
5. Lock与Synchronized 对比
Lock
所处的层面是JDK,是人为通过Java代码而实现的;而Synchronized
是Java的关键字,是底层C++语言实现,处于JVM层面。Lock
获取和释放锁的顺序不固定,因为其内置了手动操作锁的方法;而Synchronized
必须按照获取锁的相反顺序去释放锁。Lock
可以非阻塞式地获取锁(tryLock
方法);而Synchronized
只能通过阻塞式地获取锁,若当前锁已被其他线程获取,那么该线程只能阻塞等待。Lock
既可实现公平锁,也可实现非公平锁;而Synchronized
只能实现非公平锁。lock
等待锁过程中可以用lockInterruptibly
来中断等待,而synchronized只能等待锁的释放,不能响应中断;
6. 写在后面
参考文献:
- JDK5.0源码
- 《Java并发编程的艺术》
- 黑马Java并发编程教程
这个系列大概会有5篇左右的样子,我尽可能把自己对于JUC的理解通俗易懂地写出来
但如果有错误的地方,请大家指出来,我会及时去学习与改进~
如果大家觉得我的内容写的还不错,可以在评论区留言支持一下呀~
欢迎大家来逛一逛我的个人博客~