史上最全的Java并发系列之Java中的锁的使用和实现介绍(二)

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在

前言


文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…

种一棵树最好的时间是十年前,其次是现在

絮叨


上节是锁的第一小节,其实讲的是底层的定义,怎么去定义一把锁,设置这把锁能同时有几个钥匙,这些,这节的话,我们来具体来看看官方的实现。

重入锁


重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。 我们回顾下TestLock的lock方法,在 tryAcquire(int acquires)方法时没有考虑占有锁的线程再次获取锁的场景,而在调用tryAcquire(int acquires)方法时返回了false,导致该线程被阻塞。

在绝对时间上,先对锁进行获取的请求一定先被满足,那么这个锁是公平的,反之,是不公平的。 事实上,公平的锁机制往往没有非公平的效率高,但是,并不是任何场景都是以TPS作为唯一的指标,公平锁能够减少“饥饿”发生的概率,等待越久的请求越是能够得到优先满足。

下面我们来分析下ReentrantLock 的实现:

  • 实现重进入
  • 重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题:
  • 线程再次获取锁
  • 锁的最终释放

下面是ReentrantLock通过组合自定义同步器来实现锁的获取与释放,以非公平性(默认的)实现:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
复制代码


此方法通过判断 当前线程是否为获取锁的线程 来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。

公平与非公平获取锁的区别

公平性与否是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO。


读写锁


之前提到锁(如TestLock和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。 读写锁维护了一对锁,一个 读锁 和一个 写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

一般情况下,读写锁 的性能都会比 排它锁 好,因为大多数场景 读是多于写 的。在读多于写的情况下,读写锁 能够提供比 排它锁 更好的 并发性 和 吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock


  • 公平性选择 :支持公平和非公平的方式获取锁,吞吐量非公平优于公平。
  • 重进入 : 读锁在获取锁之后再获取读锁,写锁在获取锁之后再获取读锁和写锁。
  • 锁降级 :遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级为读锁。

读写锁的接口与示例 ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法,而其实现类ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法如下:

  • getReadLockCount():返回当前读锁获取的次数
  • getReadHoldCount():返回当前线程获取读锁的次数
  • isWriteLocked():判断写锁是否被获取
  • getWriteHoldCount():返回当前写锁被获取的次数


通过读写锁保证 非线程安全的HashMap的读写是线程安全的。

static Map<String, Object> map = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
/**
 * 获取一个key对应的value
 */
public static final Object get(String key) {
    r.lock();
    try {
        return map.get(key);
    } finally {
        r.unlock();
    }
}
/**
 * 设置key对应的value,并返回旧的value
 */
public static final Object put(String key, Object value) {
    w.lock();
    try {
        return map.put(key, value);
    } finally {
        w.unlock();
    }
}
/**
 * 清空所有的内容
 */
public static final void clear() {
    w.lock();
    try {
        map.clear();
    } finally {
        w.unlock();
    }
}
复制代码


锁降级

锁降级指的是 写锁降级成为读锁。 如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

//当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false
public void processData() {
    readLock.lock();
    if (!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if (!update) {
                // 准备数据的流程(略)
                update = true;
            }
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
        // 锁降级完成,写锁降级为读锁
    }
    try {
        // 使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}
复制代码


上述示例中,当数据发生变更后,布尔类型且volatile修饰update变量被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。


锁降级中读锁的获取是否必要呢?

答案是必要的。主要是为了 保证数据的可见性。 如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么 当前线程无法感知线程T的数据更新。 如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

RentrantReadWriteLock不支持锁升级。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。


LockSupport工具

当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作。 LockSupport定义了一组的公共静态方法,这些方法提供了最基本的 线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具。

LockSupport提供的 阻塞和唤醒的方法 如下:

  • park():阻塞当前线程,只有调用 unpark(Thread thread)或者被中断之后才能从park()返回。
  • parkNanos(long nanos):再park()的基础上增加了超时返回。
  • parkUntil(long deadline):阻塞线程知道 deadline 对应的时间点。
  • park(Object blocker):Java 6时增加,blocker为当前线程在等待的对象。
  • parkNanos(Object blocker, long nanos):Java 6时增加,blocker为当前线程在等待的对象。
  • parkUntil(Object blocker, long deadline):Java 6时增加,blocker为当前线程在等待的对象。
  • unpark(Thread thread):唤醒处于阻塞状态的线程 thread。

有对象参数的阻塞方法在线程dump时,会有更多的现场信息


Condition接口


任意一个Java对象,都拥有一组监视器方法,定义在java.lang.Object),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。

Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现 等待/通知 模式,但是这两者在使用方式以及功能特性上还是有差别的。

以下是Object的监视器方法与Condition接口的对比:


Condition的使用方式比较简单,需要注意在调用方法前获取锁,如下:

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}
public void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}
复制代码


Condition 接口方法介绍:

  • void await() throws InterruptedException : 当前线程进入等待状态直到被通知或中断
  • void awaitUninterruptibly() :当前线程进入等待状态直到被通知,对中断不敏感
  • long awaitNanos(long var1) throws InterruptedException :当前线程进入等待状态直到被通知、中断或超时
  • boolean await(long var1, TimeUnit var3) throws InterruptedException :当前线程进入等待状态直到被通知、中断或超时
  • boolean awaitUntil(Date var1) throws InterruptedException :当前线程进入等待状态直到被通知、中断或到某一时间
  • void signal() :唤醒Condition上一个在等待的线程
  • void signalAll() :唤醒Condition上全部在等待的线程

获取一个Condition必须通过Lock的newCondition()方法。


Condition的实现分析

主要包括 等待队列、等待和通知。

  • 等待队列
  • 等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。同步队列和等待队列中节点类型都是同步器的静态内部类AbstractQueuedSynchronizer.Node。
  • 线程调用Condition.await(),即以当前线程构造节点,并加入等待队列的尾部。


锁的知识点


  • Lock接口提供的方法lock()、unlock()等获取和释放锁的介绍
  • 队列同步器的使用 以及 自定义队列同步器
  • 重入锁 的使用和实现介绍
  • 读写锁 的 读锁 和 写锁
  • LockSupport工具实现 阻塞和唤醒线程
  • Condition接口实现 等待/通知模式


结尾


本章介绍了Java并发包中与锁相关的API和组件,通过示例讲述了这些API和组件的使用 方式以及需要注意的地方,并在此基础上详细地剖析了队列同步器、重入锁、读写锁以及 Condition等API和组件的实现细节,只有理解这些API和组件的实现细节才能够更加准确地运 用它们

因为博主也是一个开发萌新 我也是一边学一边写 我有个目标就是一周 二到三篇 希望能坚持个一年吧 希望各位大佬多提意见,让我多学习,一起进步。

相关文章
|
1月前
|
Java 大数据 Go
从混沌到秩序:Java共享内存模型如何通过显式约束驯服并发?
并发编程旨在混乱中建立秩序。本文对比Java共享内存模型与Golang消息传递模型,剖析显式同步与隐式因果的哲学差异,揭示happens-before等机制如何保障内存可见性与数据一致性,展现两大范式的深层分野。(238字)
65 4
|
1月前
|
缓存 安全 Java
如何理解Java中的并发?
Java并发指多任务交替执行,提升资源利用率与响应速度。通过线程实现,涉及线程安全、可见性、原子性等问题,需用synchronized、volatile、线程池及并发工具类解决,是高并发系统开发的关键基础。(238字)
200 4
|
4月前
|
Java API 调度
从阻塞到畅通:Java虚拟线程开启并发新纪元
从阻塞到畅通:Java虚拟线程开启并发新纪元
355 83
|
4月前
|
存储 Java 调度
Java虚拟线程:轻量级并发的革命性突破
Java虚拟线程:轻量级并发的革命性突破
331 83
|
7月前
|
消息中间件 算法 安全
JUC并发—1.Java集合包底层源码剖析
本文主要对JDK中的集合包源码进行了剖析。
|
6月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
232 0
|
6月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
254 0
|
4月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
237 0
|
5月前
|
Java 物联网 数据处理
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%
Java Solon v3.2.0 是一款性能卓越的后端开发框架,新版本并发性能提升700%,内存占用节省50%。本文将从核心特性(如事件驱动模型与内存优化)、技术方案示例(Web应用搭建与数据库集成)到实际应用案例(电商平台与物联网平台)全面解析其优势与使用方法。通过简单代码示例和真实场景展示,帮助开发者快速掌握并应用于项目中,大幅提升系统性能与资源利用率。
180 6
Java Solon v3.2.0 史上最强性能优化版本发布 并发能力提升 700% 内存占用节省 50%