【Java】多线程和高并发编程(三):锁(下)深入ReentrantReadWriteLock
文章目录
- 4、深入ReentrantReadWriteLock
- 4.1 为什么要出现读写锁
- 4.2 读写锁的实现原理
- 4.3 写锁分析
- 4.3.1 写锁加锁流程概述
- 4.3.2 写锁加锁源码分析
- 4.3.3 写锁释放锁流程概述&释放锁源码
- 4.4 读锁分析
- 4.4.1 读锁加锁流程概述
- 4.4.1.1 基础读锁流程
- 4.4.1.2 读锁重入流程
- 4.4.1.3 读锁加锁的后续逻辑fullTryAcquireShared
- 4.4.1.4 读线程在AQS队列获取锁资源的后续操作
- 4.4.2 读锁的释放锁流程
个人主页:道友老李
欢迎加入社区:道友老李的学习社区
4、深入ReentrantReadWriteLock
4.1 为什么要出现读写锁
synchronized和ReentrantLock都是互斥锁。
如果说有一个操作是读多写少的,还要保证线程安全的话。如果采用上述的两种互斥锁,效率方面很定是很低的。
在这种情况下,咱们就可以使用ReentrantReadWriteLock读写锁去实现。
读读之间是不互斥的,可以读和读操作并发执行。
但是如果涉及到了写操作,那么还得是互斥的操作。
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();public static void main(String[] args) throws InterruptedException {new Thread(() -> {readLock.lock();try {System.out.println("子线程!");try {Thread.sleep(500000);} catch (InterruptedException e) {e.printStackTrace();}} finally {readLock.unlock();}}).start();Thread.sleep(1000);writeLock.lock();try {System.out.println("主线程!");} finally {writeLock.unlock();}
}
4.2 读写锁的实现原理
ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队。
读锁操作:基于state的高16位进行操作。
写锁操作:基于state的低16为进行操作。
ReentrantReadWriteLock依然是可重入锁。
写锁重入:读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+1操作即可,只要确认持有锁资源的线程,是当前写锁线程即可。只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中写锁范围就变小了。
读锁重入:因为读锁是共享锁。读锁在获取锁资源操作时,是要对state的高16位进行 + 1操作。因为读锁是共享锁,所以同一时间会有多个读线程持有读锁资源。这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数。为了去记录读锁重入的次数,每个读操作的线程,都会有一个ThreadLocal记录锁重入的次数。
写锁的饥饿问题:读锁是共享锁,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可。在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿资源,会造成写锁长时间无法获取到写锁资源。
读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队。如果队列的前面需要写锁资源的线程,那么后续读线程是无法拿到锁资源的。持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源
4.3 写锁分析
4.3.1 写锁加锁流程概述
4.3.2 写锁加锁源码分析
写锁加锁流程
// 写锁加锁的入口
public void lock() {sync.acquire(1);
}// 阿巴阿巴!!
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}// 读写锁的写锁实现tryAcquire
protected final boolean tryAcquire(int acquires) {// 拿到当前线程Thread current = Thread.currentThread();// 拿到state的值int c = getState();// 得到state低16位的值int w = exclusiveCount(c);// 判断是否有线程持有着锁资源if (c != 0) {// 当前没有线程持有写锁,读写互斥,告辞。// 有线程持有写锁,持有写锁的线程不是当前线程,不是锁重入,告辞。if (w == 0 || current != getExclusiveOwnerThread())return false;// 当前线程持有写锁。 锁重入。if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 没有超过锁重入的次数,正常 + 1setState(c + acquires);return true;}// 尝试获取锁资源if (writerShouldBlock() ||// CAS拿锁!compareAndSetState(c, c + acquires))return false;// 拿锁成功,设置占有互斥锁的线程setExclusiveOwnerThread(current);// 返回truereturn true;
}// ================================================================
// 这个方法是将state的低16位的值拿到
int w = exclusiveCount(c);
state & ((1 << 16) - 1)
00000000 00000000 00000000 00000001 == 1
00000000 00000001 00000000 00000000 == 1 << 16
00000000 00000000 11111111 11111111 == (1 << 16) - 1
&运算,一个为0,必然为0,都为1,才为1
// ================================================================
// writerShouldBlock方法查看公平锁和非公平锁的效果
// 非公平锁直接返回false执行CAS尝试获取锁资源
// 公平锁需要查看是否有排队的,如果有排队的,我是否是head的next
4.3.3 写锁释放锁流程概述&释放锁源码
释放的流程和ReentrantLock一致,只是在判断释放是否干净时,判断低16位的值
// 写锁释放锁的tryRelease方法
protected final boolean tryRelease(int releases) {// 判断当前持有写锁的线程是否是当前线程if (!isHeldExclusively())throw new IllegalMonitorStateException();// 获取state - 1int nextc = getState() - releases;// 判断低16位结果是否为0,如果为0,free设置为trueboolean free = exclusiveCount(nextc) == 0;if (free)// 将持有锁的线程设置为nullsetExclusiveOwnerThread(null);// 设置给statesetState(nextc);// 释放干净,返回true。 写锁有冲入,这里需要返回false,不去释放排队的Nodereturn free;
}
4.4 读锁分析
4.4.1 读锁加锁流程概述
- 分析读锁加速的基本流程
- 分析读锁的可重入锁实现以及优化
- 解决ThreadLocal内存泄漏问题
- 读锁获取锁自后,如果唤醒AQS中排队的读线程
4.4.1.1 基础读锁流程
针对上述简单逻辑的源码分析
// 读锁加锁的方法入口
public final void acquireShared(int arg) {// 竞争锁资源滴干活if (tryAcquireShared(arg) < 0)// 没拿到锁资源,去排队doAcquireShared(arg);
}// 读锁竞争锁资源的操作
protected final int tryAcquireShared(int unused) {// 拿到当前线程Thread current = Thread.currentThread();// 拿到stateint c = getState();// 拿到state的低16位,判断 != 0,有写锁占用着锁资源// 并且,当前占用锁资源的线程不是当前线程if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)// 写锁被其他线程占用,无法获取读锁,直接返回 -1,去排队return -1;// 没有线程持有写锁、当前线程持有写锁// 获取读锁的信息,state的高16位。int r = sharedCount(c);// 公平锁:就查看队列是由有排队的,有排队的,直接告辞,进不去if,后面也不用判断(没人排队继续走)// 非公平锁:没有排队的,直接抢。 有排队的,但是读锁其实不需要排队,如果出现这个情况,大部分是写锁资源刚刚释放,// 后续Node还没有来记得拿到读锁资源,当前竞争的读线程,可以直接获取if (!readerShouldBlock() &&// 判断持有读锁的临界值是否达到r < MAX_COUNT &&// CAS修改state,对高16位进行 + 1compareAndSetState(c, c + SHARED_UNIT)) {// 省略部分代码!!!!return 1;}return fullTryAcquireShared(current);
}
// 非公平锁的判断
final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;return (h = head) != null && // head为null,可以直接抢占锁资源(s = h.next) != null && // head的next为null,可以直接抢占锁资源!s.isShared() && // 如果排在head后面的Node,是共享锁,可以直接抢占锁资源。s.thread != null; // 后面排队的thread为null,可以直接抢占锁资源
}
4.4.1.2 读锁重入流程
=============重入操作
前面阐述过,读锁为了记录锁重入的次数,需要让每个读线程用ThreadLocal存储重入次数
ReentrantReadWriteLock对读锁重入做了一些优化操作
============记录重入次数的核心
ReentrantReadWriteLock在内部对ThreadLocal做了封装,基于HoldCount的对象存储重入次数,在内部有个count属性记录,而且每个线程都是自己的ThreadLocalHoldCounter,所以可以直接对内部的count进行++操作。
=============第一个获取读锁资源的重入次数记录方式
第一个拿到读锁资源的线程,不需要通过ThreadLocal存储,内部提供了两个属性来记录第一个拿到读锁资源线程的信息
内部提供了firstReader记录第一个拿到读锁资源的线程,firstReaderHoldCount记录firstReader的锁重入次数
==============最后一个获取读锁资源的重入次数记录方式
最后一个拿到读锁资源的线程,也会缓存他的重入次数,这样++起来更方便
基于cachedHoldCounter缓存最后一个拿到锁资源现成的重入次数
==============最后一个获取读锁资源的重入次数记录方式
重入次数的流程执行方式:
1、判断当前线程是否是第一个拿到读锁资源的:如果是,直接将firstReader以及firstReaderHoldCount设置为当前线程的信息
2、判断当前线程是否是firstReader:如果是,直接对firstReaderHoldCount++即可。
3、跟firstReader没关系了,先获取cachedHoldCounter,判断是否是当前线程。
3.1、如果不是,获取当前线程的重入次数,将cachedHoldCounter设置为当前线程。
3.2、如果是,判断当前重入次数是否为0,重新设置当前线程的锁从入信息到readHolds(ThreadLocal)中,算是初始化操作,重入次数是0
3.3、前面两者最后都做count++
上述逻辑源码分析
protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;int r = sharedCount(c);if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// ===============================================================// 判断r == 0,当前是第一个拿到读锁资源的线程if (r == 0) {// 将firstReader设置为当前线程firstReader = current;// 将count设置为1firstReaderHoldCount = 1;} // 判断当前线程是否是第一个获取读锁资源的线程else if (firstReader == current) {// 直接++。firstReaderHoldCount++;} // 到这,就说明不是第一个获取读锁资源的线程else {// 那获取最后一个拿到读锁资源的线程HoldCounter rh = cachedHoldCounter;// 判断当前线程是否是最后一个拿到读锁资源的线程if (rh == null || rh.tid != getThreadId(current))// 如果不是,设置当前线程为cachedHoldCountercachedHoldCounter = rh = readHolds.get();// 当前线程是之前的cacheHoldCounterelse if (rh.count == 0)// 将当前的重入信息设置到ThreadLocal中readHolds.set(rh);// 重入的++rh.count++;}// ===============================================================return 1;}return fullTryAcquireShared(current);
}
4.4.1.3 读锁加锁的后续逻辑fullTryAcquireShared
// tryAcquireShard方法中,如果没有拿到锁资源,走这个方法,尝试再次获取,逻辑跟上面基本一致。
final int fullTryAcquireShared(Thread current) {// 声明当前线程的锁重入次数HoldCounter rh = null;// 死循环for (;;) {// 再次拿到stateint c = getState();// 当前如果有写锁在占用锁资源,并且不是当前线程,返回-1,走排队策略if (exclusiveCount(c) != 0) {if (getExclusiveOwnerThread() != current)return -1;} // 查看当前是否可以尝试竞争锁资源(公平锁和非公平锁的逻辑)else if (readerShouldBlock()) {// 无论公平还是非公平,只要进来,就代表要放到AQS队列中了,先做一波准备// 在处理ThreadLocal的内存泄漏问题if (firstReader == current) {// 如果当前当前线程是之前的firstReader,什么都不用做} else {// 第一次进来是null。if (rh == null) {// 拿到最后一个获取读锁的线程rh = cachedHoldCounter;// 当前线程并不是cachedHoldCounter,没到拿到if (rh == null || rh.tid != getThreadId(current)) {// 从自己的ThreadLocal中拿到重入计数器rh = readHolds.get();// 如果计数器为0,说明之前没拿到过读锁资源if (rh.count == 0)// remove,避免内存泄漏readHolds.remove();}}// 前面处理完之后,直接返回-1if (rh.count == 0)return -1;}}// 判断重入次数,是否超出阈值if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");// CAS尝试获取锁资源if (compareAndSetState(c, c + SHARED_UNIT)) {if (sharedCount(c) == 0) {firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {if (rh == null)rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;cachedHoldCounter = rh; // cache for release}return 1;}}
}
4.4.1.4 读线程在AQS队列获取锁资源的后续操作
1、正常如果都是读线程来获取读锁资源,不需要使用到AQS队列的,直接CAS操作即可
2、如果写线程持有着写锁,这是读线程就需要进入到AQS队列排队,可能会有多个读线程在AQS中。
当写锁释放资源后,会唤醒head后面的读线程,当head后面的读线程拿到锁资源后,还需要查看next节点是否也是读线程在阻塞,如果是,直接唤醒
源码分析
// 读锁需要排队的操作
private void doAcquireShared(int arg) {// 声明Node,类型是共享锁,并且扔到AQS中排队final Node node = addWaiter(Node.SHARED);boolean failed = true;try {boolean interrupted = false;for (;;) {// 拿到上一个节点final Node p = node.predecessor();// 如果prev节点是head,直接可以执行tryAcquireSharedif (p == head) {int r = tryAcquireShared(arg);if (r >= 0) {// 拿到读锁资源后,需要做的后续处理setHeadAndPropagate(node, r);p.next = null; // help GCif (interrupted)selfInterrupt();failed = false;return;}}// 找到prev有效节点,将状态设置为-1,挂起当前线程if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}private void setHeadAndPropagate(Node node, int propagate) {// 拿到head节点Node h = head; // 将当前节点设置为head节点setHead(node);// 第一个判断更多的是在信号量有处理JDK1.5 BUG的操作。if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {// 拿到当前Node的next节点Node s = node.next;// 如果next节点是共享锁,直接唤醒next节点if (s == null || s.isShared())doReleaseShared();}
}
4.4.2 读锁的释放锁流程
1、处理重入以及state的值
2、唤醒后续排队的Node
源码分析
// 读锁释放锁流程
public final boolean releaseShared(int arg) {// tryReleaseShared:处理state的值,以及可重入的内容if (tryReleaseShared(arg)) {// AQS队列的事!doReleaseShared();return true;}return false;
}// 1、 处理重入问题 2、 处理state
protected final boolean tryReleaseShared(int unused) {// 拿到当前线程Thread current = Thread.currentThread();// 如果是firstReader,直接干活,不需要ThreadLocalif (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} // 不是firstReader,从cachedHoldCounter以及ThreadLocal处理else {// 如果是cachedHoldCounter,正常--HoldCounter rh = cachedHoldCounter;// 如果不是cachedHoldCounter,从自己的ThreadLocal中拿if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;// 如果为1或者更小,当前线程就释放干净了,直接remove,避免value内存泄漏if (count <= 1) {readHolds.remove();// 如果已经是0,没必要再unlock,扔个异常if (count <= 0)throw unmatchedUnlockException();}// -- 走你。--rh.count;}for (;;) {// 拿到state,高16位,-1,成功后,返回state是否为0int c = getState();int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc))return nextc == 0;}
}// 唤醒AQS中排队的线程
private void doReleaseShared() {// 死循环for (;;) {// 拿到头Node h = head;// 说明有排队的if (h != null && h != tail) {// 拿到head的状态int ws = h.waitStatus;// 判断是否为 -1 if (ws == Node.SIGNAL) {// 到这,说明后面有挂起的线程,先基于CAS将head的状态从-1,改为0if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue; // 唤醒后续节点unparkSuccessor(h);}// 这里不是给读写锁准备的,在信号量里说。。。else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;}// 这里是出口if (h == head) break;}
}
相关文章:

【Java】多线程和高并发编程(三):锁(下)深入ReentrantReadWriteLock
文章目录 4、深入ReentrantReadWriteLock4.1 为什么要出现读写锁4.2 读写锁的实现原理4.3 写锁分析4.3.1 写锁加锁流程概述4.3.2 写锁加锁源码分析4.3.3 写锁释放锁流程概述&释放锁源码 4.4 读锁分析4.4.1 读锁加锁流程概述4.4.1.1 基础读锁流程4.4.1.2 读锁重入流程4.4.1.…...
讲解ES6中的变量和对象的解构赋值
在 ES6 中,解构赋值是一种非常方便的语法,它使得从数组或对象中提取值变得更加简洁和直观。解构赋值支持变量赋值,可以通过单独提取数组或对象的元素来赋值给变量。 下面我将分别讲解 数组解构 和 对象解构 的基本用法和一些高级特性。 1. …...

DeepSeek Coder + IDEA 辅助开发工具
开发者工具 我之前用的是Codegeex4模型,现在写一款DeepSeek Coder 本地模型 DeepSeek为什么火,我在网上看到一个段子下棋DeepSeek用兵法赢了ChatGpt,而没有用技术赢,这就是AI的思维推理,深入理解孙子兵法,…...
云计算——AWS Solutions Architect – Associate(saa)4.安全组和NACL
安全组一充当虚拟防火墙对于关联实例,在实例级别控制入站和出站流量。 网络访问控制列表(NACL)一充当防火墙关联子网,在子网级别控制入站和出站流量。 在专有网络中,安全组和网络ACL(NACL)一起帮助构建分层网络防御。 安全组在实例级别操作…...
动量+均线组合策略关键点
动量均线组合策略关键点: 趋势确认: MA系统判断主趋势方向动量指标判断趋势强度 入场条件: 价格站上重要均线(如20日线)动量指标向上并保持高位短期均线上穿长期均线 出场条件: 价格跌破均线系统动量指标见顶回落短期均线下…...

Blazor-<select>
今天我们来说说<select>标签的用法,我们还是从一个示例代码开始 page "/demoPage" rendermode InteractiveAuto inject ILogger<InjectPage> logger; <h3>demoPage</h3> <select multiple>foreach (var item in list){<…...

Synchronized使用
文章目录 synchronized使用基本概念使用方法实现原理锁的粒度并发编程注意事项与Lock锁对比比较线程安全性与性能 synchronized使用 当涉及到多线程编程时,保证数据的正确性和一致性是至关重要的。而synchronized关键字是Java语言中最基本的同步机制之一࿰…...
OpenStack四种创建虚拟机的方式
实例(Instances)是在云内部运行的虚拟机。您可以从以下来源启动实例: 一、上传到镜像服务的镜像(Image) 使用已上传到镜像服务的镜像来启动实例。 二、复制到持久化卷的镜像(Volume) 使用已…...
Expo运行模拟器失败错误解决(xcrun simctl )
根据你的描述,问题主要涉及两个方面:xcrun simctl 错误和 Expo 依赖版本不兼容。以下是针对这两个问题的解决方案: 解决 xcrun simctl 错误 错误代码 72 通常表明 simctl 工具未正确配置或路径未正确设置。以下是解决步骤: 确保 …...
Docker从入门到精通- 容器化技术全解析
第一章:Docker 入门 一、什么是 Docker? Docker 就像一个超级厉害的 “打包神器”。它能帮咱们把应用程序和它运行所需要的东东都整整齐齐地打包到一起,形成一个独立的小盒子,这个小盒子在 Docker 里叫容器。以前呢,…...

开启对话式智能分析新纪元——Wyn商业智能 BI 携手Deepseek 驱动数据分析变革
2月18号,Wyn 商业智能 V8.0Update1 版本将重磅推出对话式智能分析,集成Deepseek R1大模型,通过AI技术的深度融合,致力于打造"会思考的BI系统",让数据价值触手可及,助力企业实现从数据洞察到决策执…...

RabbitMQ 消息顺序性保证
方式一:Consumer设置exclusive 注意条件 作用于basic.consume不支持quorum queue 当同时有A、B两个消费者调用basic.consume方法消费,并将exclusive设置为true时,第二个消费者会抛出异常: com.rabbitmq.client.AlreadyClosedEx…...

防御保护作业二
拓扑图 需求 需求一: 需求二: 需求三: 需求四: 需求五: 需求六: 需求七: 需求分析 1.按照要求进行设备IP地址的配置 2.在FW上开启DHCP功能,并配置不同的全局地址池,为…...

Spring Boot中实现多租户架构
文章目录 Spring Boot中实现多租户架构多租户架构概述核心思想多租户的三种模式优势挑战租户识别机制1. 租户标识(Tenant Identifier)2. 常见的租户识别方式3. 实现租户识别的关键点4. 租户识别示例代码5. 租户识别机制的挑战数据库隔离的实现1. 数据库隔离的核心目标2. 数据…...
【AI-27】DPO和PPO的区别
DPO(Direct Preference Optimization)和 PPO(Proximal Policy Optimization)有以下区别: 核心原理 DPO:基于用户偏好或人类反馈直接优化,核心是对比学习或根据偏好数据调整策略,将…...

Git stash 暂存你的更改(隐藏存储)
一、Git Stash 概述 在开发的时候经常会遇到切换分支时需要你存储当前的更改,如果你暂时不想应用当前更改也不想放弃更改,那么你可以使用 git stash先将其隐藏存储,这样代码就会变成未修改的状态,等解决其他问题后,在…...
负载测试和压力测试的原理分别是什么
负载测试和压力测试是性能测试的两种主要类型,它们的原理和应用场景有所不同。 负载测试(Load Testing) 原理: 负载测试通过模拟实际用户行为,逐步增加系统负载,观察系统在不同负载下的表现。目的是评估系…...
shell脚本控制——定时运行作业
在使用脚本时,你也许希望脚本能在以后某个你无法亲临现场的时候运行。Linux系统提供了多个在预选时间运行脚本的方法:at命令、cron表以及anacron。每种方法都使用不同的技术来安排脚本的运行时间和频率。接下来将依次介绍这些方法。 1.使用at命令调度作…...

LeetCode 热题 100 回顾
目录 一、哈希部分 1.两数之和 (简单) 2.字母异位词分组 (中等) 3.最长连续序列 (中等) 二、双指针部分 4.移动零 (简单) 5.盛最多水的容器 (中等) 6…...
HTML5--网页前端编程(上)
HTML5–网页前端编程(上) 1.网页 (1)网站是根据一定的规则,使用html制作的相关的网页的集合。 网页是网站上的一页,通常是html格式的文件,他要通过浏览器来阅读。网页是网站的基本元素,由图片链接声音文字等元素造成,以.html或.htm后缀结尾的文件称为html文件。 (2…...

微信小程序之bind和catch
这两个呢,都是绑定事件用的,具体使用有些小区别。 官方文档: 事件冒泡处理不同 bind:绑定的事件会向上冒泡,即触发当前组件的事件后,还会继续触发父组件的相同事件。例如,有一个子视图绑定了b…...
CVPR 2025 MIMO: 支持视觉指代和像素grounding 的医学视觉语言模型
CVPR 2025 | MIMO:支持视觉指代和像素对齐的医学视觉语言模型 论文信息 标题:MIMO: A medical vision language model with visual referring multimodal input and pixel grounding multimodal output作者:Yanyuan Chen, Dexuan Xu, Yu Hu…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...

如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
API网关Kong的鉴权与限流:高并发场景下的核心实践
🔥「炎码工坊」技术弹药已装填! 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 引言 在微服务架构中,API网关承担着流量调度、安全防护和协议转换的核心职责。作为云原生时代的代表性网关,Kong凭借其插件化架构…...