【JAVA多线程】JDK中的各种锁,看这一篇就够了

news2025/1/10 23:47:56

目录

1.概论

1.1.实现锁的要素

1.2.阻塞队列

1.3.Lock接口和Sync类

2.各种锁

2.1.互斥锁

2.1.1.概论

2.1.2.源码

1.lock()

2.unlock()

2.2.读写锁

2.3.Condition

2.3.1.概论

2.3.2.底层实现


1.概论

1.1.实现锁的要素

JAVA中的锁都是可重入的锁,因为不可重入的试用的时候很容易造成死锁。这个道理很好想明白:

当一个线程已经持有一个锁,并在持有该锁的过程中再次尝试获取同一把锁时,如果没有重入机制,第二次请求会被阻塞,因为锁已经被自己持有。这会导致线程自我死锁,因为它在等待自己释放的锁。

可重入是指获取锁的线程可以继续重复的获得此锁。其实我们想都能想到要实现一把锁需要些什么,首先肯定是:

  • 标志位,也叫信号量,标记锁的状态和重入次数,这样才能完成持有锁和释放锁。

接下来要考虑的是拒接策略,当前锁被持有期间,后续的请求线程该怎么处理,当然可以直接拒绝,JAVA的选择委婉点,选择了允许这些线程躺在锁上阻塞等待锁被释放。要实现让线程躺在锁上等待,我们想想无非要:

  • 需要支持对一个线程的阻塞、唤醒

  • 需要记录当前哪个线程持有锁

  • 需要一个队列维护所有阻塞在当前锁上的线程

OK,以上四点就是JAVA锁的核心,总结起来就是信号量+队列,分别用来记录持有者和等待者。

1.2.阻塞、唤醒操作

首先我们来看看阻塞和唤醒的操作,在JDK中提供了一个Unsafe类,该类中提供了阻塞或唤醒线程的一对操作 原语——park/unpark:

public native void unpark(Object var1);
public native void park(boolean var1, long var2);

这对原语最终会调用操作系统的程序接口执行线程操作。

1.2.阻塞队列

拿来维护所有阻塞在当前锁上的线程的队列能是个普通队列吗?很显然不是,它的操作必须是线程安全的是吧,所以这个队列用阻塞队列实现才合适。什么是阻塞队列:

阻塞队列提供了线程安全的元素插入和移除操作,并且在特定条件下会阻塞线程,直到满足操作条件。

说到JDK中的阻塞队列,其核心就是AbstractQueuedSynchronizer,简称AQS,由双向链表实现的一个元素操作绝对安全的队列,用来在锁的实现中维护阻塞在锁上的线程上的队列的这个角色。

来看看AQS的源码:

它有指向前后节点的指针、有一个标志位state、还有一个提供线程操作原原语(阻塞、唤醒)的unsafe类。

所以其实AQS就长这样:

点进源码可以看到其随便一个方法都是线程安全的:

由于本文不是专门聊AQS这里就不扩展了,反正知道AQS是一个线程安全的阻塞队列就对了。

1.3.Lock接口和Sync类

JAVA中所有锁的顶级父接口,用来规范定义一把锁应该有那些行为职责:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

JAVA中所有锁的实现都是依托AQS去作为阻塞队列,每个锁内部都会实现一个Sync内部类,在自身Sync内部以不同的策略去操作AQS实现不同种类的锁。

abstract static class Sync extends AbstractQueuedSynchronizer {......}

2.各种锁

2.1.互斥锁

2.1.1.概论

ReentrantLock,互斥锁,ReentrantLock本身没有任何代码逻辑,依靠内部类Sync干活儿:

public class ReentrantLock implements Lock, Serializable {
    private final ReentrantLock.Sync sync;
    public void lock() {
        this.sync.lock();
    }
    public void unlock() {
        this.sync.release(1);
    }
    ......
}

ReentrantLock的Sync继承了AQS

abstract static class Sync extends AbstractQueuedSynchronizer {......}

Sync是抽象类,有两个实现:

  • NonfairSync,公平锁

  • FairSync,非公平锁

实例化ReentrantLock的实例时,根据传入的标志位可以创建公平和公平的实现

public class ReentrantLock implements Lock, java.io.Serializable{
public ReentrantLock() {
        sync = new NonfairSync();
    }
​
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    ......
}
}

2.1.2.源码

1.lock()

公平锁的lock():

static final class FairSync extends Sync {
        final void lock() {
            acquire(1);//进来直接排队
        }

非公平锁的lock():

static final class NonfairSync extends Sync {
        final void lock() {
            if (compareAndSetState(0, 1))//进来直接抢锁
                setExclusiveOwnerThread(Thread.currentThread());//将锁的持有者设置为当前线程
            else
                acquire(1);//没抢过再去排队
        }
    }

acquire()是AQS的模板方法:

tryAcquire,尝试再去获取一次锁,公平锁依然是排队抢,去看看阻塞队列是否为空;非公平锁依然是直接抢。

acquireQueued,将线程放入阻塞队列。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquireQueued(..)是lock()最关键的一部分,addWaiter(..)把Thread对象加入阻塞队列,acquireQueued(..)完成对线程的阻塞。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {//如果发现自己在队头就去拿锁
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())//调用原语,阻塞自己
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued(..)函数有一个返回值,表示什么意思 呢?虽然该函数不会中断响应,但它会记录被阻塞期间有没有其他线 程向它发送过中断信号。如果有,则该函数会返回true;否则,返回false。所以才有了以下逻辑:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

当 acquireQueued(..) 返回 true 时,会调用 selfInterrupt (),自己给自己发送中断信号,也就是自己把自己的中断标志位设 为true。之所以要这么做,是因为自己在阻塞期间,收到其他线程中 断信号没有及时响应,现在要进行补偿。这样一来,如果该线程在loc k代码块内部有调用sleep()之类的阻塞方法,就可以抛出异常,响 应该中断信号。

2.unlock()

unlock的逻辑很简单,每次unlock,state-1,直到state=0时,将锁的拥有者置null,释放锁。由于只有锁的持有线程才能操作lock,所以unlock()不需要用CAS,操作时直接判断一下是不是锁的持有线程在操作即可。

public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {
        if (tryRelease(arg)) {//释放锁
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒阻塞队列中的后继者
            return true;
        }
        return false;
    }

释放锁:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//每次unlock,state减1
            if (Thread.currentThread() != getExclusiveOwnerThread())//判断是不是锁的持有线程
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//state为0表示该锁没有被持有
                free = true;
                setExclusiveOwnerThread(null);//将锁的持有者置null
            }
            setState(c);
            return free;
        }

唤醒后继者:

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

2.2.读写锁

读写锁是一个实现读写互斥的锁,读写锁包含一个读锁、一个写锁:

public interface ReadWriteLock{
    Lock readLock();
    Lock writeLock();
}

读写锁的使用就是直接调用对应锁进行锁定和解锁:

ReadWriteLock rwLock=new ReetrantReadWriteLock();
Lock rLock=rwLock.readLock();
rLock.lock();
rLock.unLock();
Lock wLock=rwLock.writeLock();
wLock.lock();
wLock.unLock();

读写锁的Sync内部类对读锁和写锁采用同一个int型的信号量的高16位和低16位分别表示读写锁的状态和重入次数,这样一次CAS就能统一处理进行读写互斥操作:

abstract static class Sync extends AbstractQueuedSynchronizer {
        static final int SHARED_SHIFT   = 16;
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}

2.3.Condition

2.3.1.概论

condition用于更加细粒度的控制锁上面的线程阻塞、唤醒。

以下以一个经典的生产、消费者问题为例:

队列空的时候进来的消费者线程阻塞,有数据放进来后唤醒阻塞的消费者线程。

队列满的时候进来的生产者线程阻塞,有空位后唤醒阻塞的生产者线程。

锁粒度的实现:

public void enqueue(){
    synchronized(queue){
        while(queue.full()){
            queue.wait();
        }
        //入队列
        ......
        //通知消费者,队列中有数据了
        queue.notify();
    }
}
​
public void dequeue(){
    synchronized(queue){
        while(queue.empty()){
            queue.wait();
        }
        //出队列
        ......
        //通知生产者,队列中有空位了,可以继续放数据
        queue.notify();
    }
}

可以发现,唤醒的时候把阻塞的生产消费线程一起唤醒了。

条件粒度的实现:

private final Lock lock = new ReentrantLock();
private final Condition notFull  = lock.newCondition(); // 用于等待队列不满
private final Condition notEmpty = lock.newCondition(); // 用于等待队列非空

public void enqueue(Object item) {
    try {
        while (queue.isFull()) {
            notFull.await(); // 等待队列不满
        }
        // 入队列操作
        // ...
        
        // 入队后,通知等待的消费者
        notEmpty.signal();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 保持中断状态
        // 处理中断逻辑
    } finally {
        queue.unlock();
    }
}

public void dequeue() {
    try {
        while (queue.isEmpty()) {
            notEmpty.await(); // 等待队列非空
        }
        // 出队列操作
        // ...
        
        // 出队后,通知等待的生产者
        notFull.signal();
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 保持中断状态
        // 处理中断逻辑
    } finally {
        queue.unlock();
    }
}

2.3.2.底层实现

Condition由Lock产生,因此Lock中持有Condition:

public interface Lock {
    ......
    Condition newCondition();
}

承担功能的其实就是Syn中的ConditionObject,也就是AQS中的ConditionObject:

final ConditionObject newCondition() {
            return new ConditionObject(this);
        }

一个Condition上面阻塞着多个线程,所以每个Condition内部都有一个队列,用来记录阻塞在这个condition上面的线程,这个队列其实也是AQS实现的,AQS中除了实现一个以Node为节点的队列,还实现了一个以ConditionObject为节点的队列:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
        public class ConditionObject implements Condition, java.io.Serializable {
        private static final long serialVersionUID = 1173984872572414699L;
        private transient Node firstWaiter;
        private transient Node lastWaiter;
        ......
        }
    }

Condition是个接口,定义了一系列条件操作:

public interface 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();
    void signalAll();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1889318.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

固定网国内数据传送业务经营许可证

一、国内固定网数据传送业务是什么&#xff1f; 固定网国内数据传送业务是指互联网数据传送业务以外的&#xff0c;在固定网中以有线方式提供的国内端到端数据传送业务。主要包括基于IP承载网、ATM网、X.25分组交换网、DDN网、帧中继网络的数据传送业务等。该业务属于A2类基础…

数字源表表征及测试纳米材料高温原位方案

01/纳米材料电学性能的表征和分析/ 与传统的材料相比&#xff0c;纳米材料具有原子级厚度、表面平整无悬空键、载流子迁移率好等优点&#xff0c;其导电性能很大程度依赖于材料本身的带隙、掺杂浓度和载流子迁移率。同样的掺杂浓度下&#xff0c;迁移率越大&#xff0c;电阻率…

计算机网络--网络层

一、网络层的服务和功能 网络层主要为应用层提供端对端的数据传输服务 网络层接受运输层的报文段&#xff0c;添加自己的首部&#xff0c;形成网络层分组。分组是网络层的传输单元。网络层分组在各个站点的网络层之间传输&#xff0c;最终到达接收方的网络层。接收方网络层将运…

nginx SSI(Server Side Include)服务端包含 合并拼装静态内容

一、什么是SSI 在被传送给浏览器之前&#xff0c;服务器会对 HTML 文档进行完全地读取、分析以及修改&#xff0c;使用SSI指令将文本、图片或代码信息包含到网页中。对于整个页面可以拆分成多个模块&#xff0c;通过SSI指令将几个模块拼接成一个完整的页面&#xff0c;当有内容…

大模型在软件测试领域的应用场景有哪些?_大模型在测试领域应用

在数字化转型的大背景下&#xff0c;在软件定义一切的趋势下&#xff0c;软件测试人员需要接触和理解的信息越来越多&#xff0c;并呈现加速增长的态势。需求越来越大&#xff0c;交付周期越来越短&#xff0c;受制于体力和能力限制&#xff0c;测试人员的效率和质量难以同步提…

基于模式识别的垃圾分类系统-计算机毕业设计源码96151

摘 要 随着城市化进程的加速和人口的不断增长&#xff0c;垃圾管理和环境保护成为了全球面临的重要挑战之一。垃圾分类作为一种可行的解决方案&#xff0c;旨在减少垃圾的数量、降低环境污染&#xff0c;并促进资源的回收与再利用。 本文旨在设计并开发一个垃圾分类系统。该系统…

电工电子革新风暴:在线电路仿真软件重塑行业格局

随着科技的不断进步&#xff0c;电工电子行业正迎来一场由在线电路仿真软件引领的革新风暴。这些功能强大的软件工具不仅极大地提高了电路设计的效率&#xff0c;更为整个行业带来了前所未有的冲击和机遇。 仿真软件&#xff1a;电工电子行业的“隐形推手” 在线电路仿真软件…

代码随想录算法训练营第42天| 198.打家劫舍、213.打家劫舍II 337.打家劫舍III、 337.打家劫舍III

198.打家劫舍 题目链接&#xff1a;198.打家劫舍 文档讲解&#xff1a;代码随想录 状态&#xff1a;不会 记忆化搜索思路&#xff1a; 可以从最后一间房子开始&#xff0c;每次面对一个房子要考虑打劫还是不打劫&#xff0c;如果打劫了就从它的下下个房子开始打劫&#xff0c;在…

idea Git操作

1、代码拉取&#xff08;左上角&#xff09; 或 2、代码push&#xff08;左上角&#xff09; 3、切换分支&#xff08;右下角&#xff09; 4、分支管理 5、当前分支和某一个分支对比差异 6、当前分支某一个提交需要恢复成提交前状态&#xff08;revert&#xff09; 7、其他分…

Python处理浮点数的实用技巧

更多Python学习内容&#xff1a;ipengtao.com 四舍五入是一种常见的数学操作&#xff0c;它用于将数字舍入到指定的精度。Python 提供了多种方法来实现四舍五入操作&#xff0c;从基本的 round 函数到高级的 decimal 模块&#xff0c;满足不同的需求。本文将详细介绍这些方法&a…

Profibus DP主站转Modbus网关连接伺服与电机通讯

在工业自动化领域&#xff0c;将Profibus DP主站转Modbus网关&#xff08;XD-MDPBM20&#xff09;用于连接伺服与电机通讯是一种常见且重要的应用方式。当使用Profibus DP主站转Modbus网关&#xff08;XD-MDPBM20&#xff09;连接伺服与电机进行通讯时&#xff0c;可以参考以下…

使用代理,在Mapper层面统一封装VO、Entity 之间的转换逻辑

无聊看看开源项目&#xff0c;无意间看到里面的业务代码用到了BaseMapperPlus&#xff0c;于是点进去发现里面封装了Vo、Entity 之间的转换逻辑。想着自己平时都是手动的进行 copy 转换来着。于是本地进行验证了一番&#xff0c;发现还挺好用的&#xff0c;懒人必备。但是前提是…

MySQL 9.0 悄悄上线,支持面向AI的向量数据库

MySQL狂热粉丝群已经发现MySQL官网上MySQL9.0这两天悄然上线&#xff0c;已经可以下载体验了&#xff0c;目前被定义为创新版本&#xff08;Innovation&#xff09;。 下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/ 支持主流的操作系统&#xff0c;安装后可以直…

101 个现实世界的新一代 AI 用例

自从一年半前生成式人工智能首次引起世界关注以来&#xff0c;人们一直在热烈讨论这项新技术的最佳用途。虽然我们都喜欢早期的有趣聊天和诙谐的打油诗&#xff0c;但我们很快发现&#xff0c;许多最大的人工智能机会显然都在企业中。 短短几个月内&#xff0c;这些组织就从 A…

tcp/ip, http 等协议的端口号

每当看到有人的简历上写着熟悉 tcp/ip, http 等协议时, 我就忍不住问问他们: 你给我说说, 端口是啥吧! 可惜, 很少有人能说得让人满意... 所以这次就来谈谈端口(port), 这个熟悉的陌生人. 在此过程中, 还会谈谈间接层, naming service 等概念, IoC, 依赖倒置等原则以及 TCP 协议…

大模型应用:一文搞懂Fine-tuning,模型微调有啥好处,从理论到实操

前言 我们前面几篇博文中出现的大模型&#xff0c;都是通用的大模型。但在更专业的领域&#xff0c;需要更专业的模型&#xff0c;这就需要用到模型微调的能力。 从NLP范式发展的趋势来看&#xff0c;prompt模型的方式已经成为主流&#xff0c;prompt learning已经很成熟了&a…

Linux 搭建 Kafka 环境 - 详细教程

目录 一. Kafka介绍 1. 应用场景 2. 版本对比 二. Kafka安装 1. 前置环境 &#xff08;1&#xff09;安装JDK 2. 软件安装 &#xff08;3&#xff09;环境变量配置 &#xff08;3&#xff09;服务启动 三. Console测试 基础命令 &#xff08;1&#xff09;列出Kafk…

大模型分布式训练的第四种境界

主要内容包括&#xff1a; \1. 历史背景 \2. 分布式训练挑战 \3. 分布式训练技术体系 \4. 未来挑战 \5. Q&A 01 历史背景 自 2019 年以来&#xff0c;大语言模型发展迅猛&#xff0c;不断有新的研究成果涌现&#xff0c;包括各类预训练模型及其应用&#xff0c;如 LL…

Vue86-Vuex中的getters属性

一、getters的使用 1-1、index.js中getters的书写 计算属性computed靠return获得返回值&#xff01; 1-2、组件中getters的调用 state是数据源&#xff0c;getters是拿着数据源里的东西进行一番加工。像极了&#xff1a;data和computed 二、小结

从0到1手写vue源码

模版引擎 数组join法(字符串) es6反引号法(模版字符串换行) mustache (小胡子) 引入mustache 模版引擎的使用 mustache.render(templatestr,data)