详解JDK锁01:结合源码一文弄懂Lock接口!

简介: 详解JDK锁01:结合源码一文弄懂Lock接口!

详解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 quite different properties, and may support multiple associated Condition objects.

其实这段话就简单概括了Lock的三大优点:

  1. 灵活的结构:可以显式地获取与释放锁
  2. 多种不同的属性与方法
  3. 引入了 Condition 对象

接下来的部分将着重介绍这几点

2. Lock锁的灵活性

2.1 Lock接口方法

在 JDK5.0 之前,Java是借助于 Synchronized 关键字实现加锁功能,而这个功能是通过JVM实现的。而在 JDK5.0 之后,JUC包中新增了Lock接口实现锁功能。

虽然该Lock接口不具备 Synchronized 关键字隐式获取锁的便捷性,但是其提供了一系列手动操作锁的方法:

  1. 阻塞式地获取锁
    该方法有一定的缺陷:如果当前锁被占用,那么当前线程将被禁用,进入阻塞状态,直到获取到锁为止。
void lock();
  1. 阻塞式地可打断地获取锁
void lockInterruptibly() throws InterruptedException;
  1. 虽然是阻塞式地获取锁,但是如果该线程被中断后,会抛出异常,停止继续阻塞。
  2. 非阻塞式地获取锁尝试非阻塞地获取锁,调用该方法后立即返回
  • 若能够获取到锁,则返回 true
  • 若锁已被占用,则返回 false
boolean tryLock();

该方法的典型使用场景为:

// 伪代码
Lock lock = new ...;
if (lock.tryLock() {
    try {
        // 操作共享资源
    } finally {
        // 释放锁
        lock.unlock();
    }
} else {
    // 未获取到锁,执行其余操作
}
  1. 带有超时时间地获取锁尝试在指定的一段时间内获取锁
  • 若在指定时间 time 内能够获取到锁,且未被中断,则返回 true
  • 若指定时间 time 结束后仍未获取到锁,则返回 false
  • 若在指定时间 time 内被打断,则抛出 InterruptedException
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

其中 time 代表指定的超时时间,unit 代表时间单位

  1. 释放锁

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

  1. 实现公平锁与非公平锁。实例化ReentrantLock时,其有参构造方法中传入的值为boolean类型。若传入值为true,为公平锁;否则为非公平锁

public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}
  1. 判断锁是否已经被持有

public boolean isLocked() {
   return sync.isLocked();
}
  1. 判断锁是否为公平锁

public final boolean isFair() {  
   return sync instanceof FairSync;  
}

相较于Lock接口,Synchronized 只实现了非公平锁。

4. Condition基本使用

回顾 Synchronized 关键字,其实现 等待/通知 的模式是通过 Object 类内部的 waitnotify 以及 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 对比

  1. Lock 所处的层面是JDK,是人为通过Java代码而实现的;而 Synchronized 是Java的关键字,是底层C++语言实现,处于JVM层面。
  2. Lock 获取和释放锁的顺序不固定,因为其内置了手动操作锁的方法;而 Synchronized 必须按照获取锁的相反顺序去释放锁。
  3. Lock 可以非阻塞式地获取锁( tryLock 方法);而 Synchronized 只能通过阻塞式地获取锁,若当前锁已被其他线程获取,那么该线程只能阻塞等待。
  4. Lock 既可实现公平锁,也可实现非公平锁;而 Synchronized只能实现非公平锁。
  5. lock 等待锁过程中可以用 lockInterruptibly 来中断等待,而synchronized只能等待锁的释放,不能响应中断;

6. 写在后面

参考文献:

  1. JDK5.0源码
  2. 《Java并发编程的艺术》
  3. 黑马Java并发编程教程

这个系列大概会有5篇左右的样子,我尽可能把自己对于JUC的理解通俗易懂地写出来

但如果有错误的地方,请大家指出来,我会及时去学习与改进~

如果大家觉得我的内容写的还不错,可以在评论区留言支持一下呀~

欢迎大家来逛一逛我的个人博客~

相关文章
|
4月前
|
安全 前端开发 Java
JDK源码级别彻底剖析JVM类加载机制
JDK源码级别彻底剖析JVM类加载机制
|
14天前
|
Oracle Java 关系型数据库
【颠覆性升级】JDK 22:超级构造器与区域锁,重塑Java编程的两大基石!
【9月更文挑战第6天】JDK 22的发布标志着Java编程语言在性能和灵活性方面迈出了重要的一步。超级构造器和区域锁这两大基石的引入,不仅简化了代码设计,提高了开发效率,还优化了垃圾收集器的性能,降低了应用延迟。这些改进不仅展示了Oracle在Java生态系统中的持续改进和创新精神,也为广大Java开发者提供了更多的可能性和便利。我们有理由相信,在未来的Java编程中,这些新特性将发挥越来越重要的作用,推动Java技术不断向前发展。
|
27天前
|
算法 安全 Java
深入JDK源码:揭开ConcurrentHashMap底层结构的神秘面纱
【8月更文挑战第24天】`ConcurrentHashMap`是Java并发编程中不可或缺的线程安全哈希表实现。它通过精巧的锁机制和无锁算法显著提升了并发性能。本文首先介绍了早期版本中使用的“段”结构,每个段是一个带有独立锁的小型哈希表,能够减少线程间竞争并支持动态扩容以应对高并发场景。随后探讨了JDK 8的重大改进:取消段的概念,采用更细粒度的锁控制,并引入`Node`等内部类以及CAS操作,有效解决了哈希冲突并实现了高性能的并发访问。这些设计使得`ConcurrentHashMap`成为构建高效多线程应用的强大工具。
33 2
|
1月前
|
Oracle 安全 Java
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
|
1月前
|
Java Maven 容器
JDK版本特性问题之想使用接口中的私有方法,如何实现
JDK版本特性问题之想使用接口中的私有方法,如何实现
JDK版本特性问题之想使用接口中的私有方法,如何实现
|
3月前
|
Java
JavaSE——JDk8新特性(1/2):Lambda表达式(具体实现、函数式接口、简化setAll、Comparator),Lambda表达式的省略写法
JavaSE——JDk8新特性(1/2):Lambda表达式(具体实现、函数式接口、简化setAll、Comparator),Lambda表达式的省略写法
44 1
|
3月前
|
Java
JDK 1.8 函数接口(收藏用)
JDK 1.8 函数接口(收藏用)
|
3月前
|
Java Spring
深入解析Spring源码,揭示JDK动态代理的工作原理。
深入解析Spring源码,揭示JDK动态代理的工作原理。
41 0
|
4月前
|
Java
【JAVA进阶篇教学】第四篇:JDK8中函数式接口
【JAVA进阶篇教学】第四篇:JDK8中函数式接口
|
4月前
|
设计模式 Java
根据JDK源码Calendar来看工厂模式和建造者模式
根据JDK源码Calendar来看工厂模式和建造者模式