Redisson 源码解析 - 分布式锁实现过程
一、Redisson 分布式锁源码解析
Redisson是架设在Redis基础上的一个Java驻内存数据网格。在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
其中比较具体特色的就是 Redisson 对分布式锁的支持,不仅简化了分布式锁的应用过程还支持 Fair Lock、MultiLock、RedLock、ReadWriteLock 等锁的实现。本文就 Redisson 分布式锁的加锁和解锁过程的源码进行大致的解析。
下面是Redisson 源码地址:
https://github.com/redisson/redisson
如果对 Redisson 的使用还不了解的小伙伴可以先看下下面这篇文章:
https://xiaobichao.blog.csdn.net/article/details/112726748
Redisson 中的分布式锁在使用起来非常简便,例如:
public class TestLock {@ResourceRedissonClient redissonClient;@Testpublic void test() {RLock lock = null;try {// 获取可重入锁lock = redissonClient.getLock("redislock");// 获取锁,如果获取不到会等待lock.lock();Thread.sleep(30000000);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {if (lock != null) {// 释放锁lock.unlock();}}}@Testpublic void test1() {RLock lock = null;try {// 获取可重入锁lock = redissonClient.getLock("redislock");// 尝试获取锁,返回获取锁的状态Boolean isLock = lock.tryLock();Thread.sleep(30000000);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {if (lock != null) {// 释放锁lock.unlock();}}}
}
下面分别从 lock、tryLock、unlock 、三个地方进行源码的解析。
二、lock 获取锁和看门狗机制
先看下 redissonClient.getLock 方法,它默认创建了一个 RedissonLock 对象,并将锁的key传递进来:

而 RedissonLock 对象又继承至RedissonBaseLock 类:

因此我们下面大多的源码分析都基于这两个类进行。
首先进到 RedissonLock 类下的 lock() 方法中:

这里主要又调用了 lock(long leaseTime, TimeUnit unit, boolean interruptibly) 方法,注意如果没有指定过期时间默认为 -1 ,下面看到 lock(long leaseTime, TimeUnit unit, boolean interruptibly) 方法中:
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {// 当前线程IDlong threadId = Thread.currentThread().getId();// 尝试获取锁,如果已经有锁的话返回锁的剩余时间Long ttl = tryAcquire(-1, leaseTime, unit, threadId);// 获取锁成功if (ttl == null) {return;}// 如果获取锁失败,订阅当前线程,以便后续获取锁时得到通知。CompletableFuture<RedissonLockEntry> future = subscribe(threadId);//设置超时处理,当订阅的future完成时,触发超时处理。pubSub.timeout(future);//定义一个RedissonLockEntry对象,用于表示当前线程在分布式锁中的状态。RedissonLockEntry entry;if (interruptibly) {// 可中断entry = commandExecutor.getInterrupted(future);} else {entry = commandExecutor.get(future);}try {// 循环尝试获取锁while (true) {// 尝试获取锁ttl = tryAcquire(-1, leaseTime, unit, threadId);// 获取锁成功if (ttl == null) {break;}// 如果已经存在锁的过期时间大于等于0,需要等待通知if (ttl >= 0) {try {// 通过Semaphore 的 tryAcquire方法等待指定时间entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} catch (InterruptedException e) {if (interruptibly) {throw e;}entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);}} else { //如果剩余时间小于0,就一直等待。if (interruptibly) {entry.getLatch().acquire();} else {entry.getLatch().acquireUninterruptibly();}}}} finally {// 无论加锁成功或失败,都取消订阅unsubscribe(entry, threadId);}
}
代码中加了注释,这里我总结下,首先调用 tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) 方法尝试获取锁,如果锁存在的话则返回过期时间,为 null 的话表示获取锁成功。如果获取锁失败,则将自己加入到订阅中,然后开启一个死循环,在循环中再次尝试获取锁,如果还是没有获取到的话则使用 Semaphore 的 tryAcquire 方法阻塞当前线程,如果其他线程释放了锁,则这里继续循环再次尝试获取锁。
下面主要看下 tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) 尝试获取锁的逻辑,看到该方法下:

tryAcquire 方法又调用了 tryAcquireAsync0 方法,然后又主要调用了 tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) 方法,下面主要看到这个方法下:
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime > 0) {//如果指定了锁持有时间,则根据指定的时间设置 key 的过期时间ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 没指定,默认锁持有 30sttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 执行 lua 操作CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// 如果加锁成功if (ttlRemaining == null) {if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 没指定的话// 启动看门狗,延长锁持有时间scheduleExpirationRenewal(threadId);}}// 返回锁的过期时间return ttlRemaining;});return new CompletableFutureWrapper<>(f);
}
这里其中 tryLockInnerAsync 方法主要是指定了 Lua 脚本,主要注意的是如果没有指定了锁的过期则默认为 30s 的时间,然后在 Lua 脚本执行后,同样的判断,如果获取到锁的话并且没有指定锁的过期时间则开启看门狗机制,为锁延长时间续命的操作。
这里先看下核心操作 tryLockInnerAsync 方法中 Lua 脚本:
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, command,// 如果锁不存在,或者哈希表中锁对应的线程ID存在的话"if ((redis.call('exists', KEYS[1]) == 0) " +"or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +// 对hash中的内容值 +1"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +// 设置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +//表示脚本执行成功,且不需要返回特定的值。"return nil; " +"end; " +// 如果if条件不满足,返回剩余过期时间(以毫秒为单位)"return redis.call('pttl', KEYS[1]);",// 对应这 lua 脚本中的参数,第一个参数就是 KEYS[1],以此类推Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
这里主要利于 Lua 的原子性将整个判断操作过程给原子化了,其中这里锁的结构是以 hash 的形式存放的,key为锁的名称,hash中的key为线程ID(UUID+线程ID的形式),因为分布式情况下线程ID也有可能重复,value为数字表示锁重入的次数, lua 脚本如果执行加锁逻辑成功则返回 null,否则返回锁的过期时间,也就对应前面获取锁的时候判断的依据。
下面回到上面的 tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) 方法中,在 ttlRemainingFuture.thenApply 中如果获取锁成功,并且没有指定锁的过期时间则会开启看门狗机制为锁进行续命操作,主要调用的是 scheduleExpirationRenewal(long threadId) 方法,下面看到该方法下的逻辑:
protected void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry = new ExpirationEntry();// 加入看门狗记录中ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);// 如果已经存在if (oldEntry != null) {// 重新指定线程IDoldEntry.addThreadId(threadId);} else { // 如果不存在的话就开启看门狗entry.addThreadId(threadId);try {// 启动看门狗renewExpiration();} finally {// 如果线程已经终止,则关闭看门狗if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}
}
主要的逻辑在 renewExpiration() 方法下,继续看到该方法中:
private void renewExpiration() {// 获取当前信息ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}// 执行计时任务Timeout task = getServiceManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//再次获取信息ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}// 获取线程IDLong threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 延长锁的过期时间CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) { //如果有异常删除该任务log.error("Can't update lock {} expiration", getRawName(), e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) { // 如果执行成功// 递归继续执行renewExpiration();} else { // 执行失败// 关闭看门狗cancelExpirationRenewal(null);}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}
这里主要通过递归延时任务的方式实现循环执行的效果,其中延时的时间为 internalLockLeaseTime 的三分之一,也就是默认 10s 触发一次,在任务中主要通过 renewExpirationAsync(long threadId) 方法,对锁进行了延时续命操作,看到该方法中:
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 如果锁和线程ID存在"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +// 重置过期时间"redis.call('pexpire', KEYS[1], ARGV[1]); " +// 成功返回 1"return 1; " +"end; " +// 失败返回 0"return 0;",// lua 脚本中对应的参数Collections.singletonList(getRawName()),internalLockLeaseTime, getLockName(threadId));
}
这里还是依靠 Lua 脚本的方式,如果锁存在的话就重置过期时间,达到续命的效果。
三、tryLock 获取锁
tryLock和lock是两种获取分布式锁的方法,它们的主要区别在于获取锁的方式和阻塞行为。tryLock默认是一种非阻塞的获取锁的方法,也可以通过设置 waitTime 变成阻塞的。而lock默认就是一种阻塞的获取锁的方法。
他们俩的最终处理逻辑都是一样的,只不过默认的 tryLock 没有订阅阻塞的操作。
下面看下默认的 tryLock 的操作 ,进到 RedissonLock 下的 tryLock() 中:

再进入 tryLockAsync() 方法中:

这里调用了 tryLockAsync 方法,并将当前线程的ID传递了进来,继续看到 tryLockAsync 方法中:

在看到 tryAcquireOnceAsync 方法中,注意这里的等待时间和上面 lock() 默认一样,是 -1 :
private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {CompletionStage<Boolean> acquiredFuture;if (leaseTime > 0) {//如果指定了锁持有时间,则根据指定的时间设置 key 的过期时间acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);} else {// 没指定,默认锁持有 30sacquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);}// 执行 lua 操作acquiredFuture = handleNoSync(threadId, acquiredFuture);CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {// 如果加锁成功if (acquired) {// 如果指定了锁持有时间if (leaseTime > 0) {internalLockLeaseTime = unit.toMillis(leaseTime);} else { // 没指定的话,// 看门狗,延长锁持有时间scheduleExpirationRenewal(threadId);}}// 返回获取锁的状态return acquired;});return new CompletableFutureWrapper<>(f);
}
这里的逻辑相比于前面 lock() 的逻辑就差不多了,只不过缺少了订阅和阻塞等待重试的操作,再下面的操作和lock() 的逻辑是一致的。
四、unlock 解锁和关闭看门狗
解锁的逻辑看到 RedissonBaseLock 下的 unlock() 方法中:

继续看到 unlockAsync 方法中:

主要逻辑在 unlockAsync0 方法中:
private RFuture<Void> unlockAsync0(long threadId) {// 解锁CompletionStage<Boolean> future = unlockInnerAsync(threadId);CompletionStage<Void> f = future.handle((opStatus, e) -> {// 关闭看门狗cancelExpirationRenewal(threadId);if (e != null) { // 如果执行有异常if (e instanceof CompletionException) {throw (CompletionException) e;}throw new CompletionException(e);}if (opStatus == null) { // 如果结果为空的话,表示锁不存在IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "+ id + " thread-id: " + threadId);throw new CompletionException(cause);}return null;});return new CompletableFutureWrapper<>(f);
}
主要做了两件事,解锁和关闭看门狗,先看下 unlockInnerAsync(long threadId) 方法解锁的过程:
protected final RFuture<Boolean> unlockInnerAsync(long threadId) {String id = getServiceManager().generateId();MasterSlaveServersConfig config = getServiceManager().getConfig();int timeout = (config.getTimeout() + config.getRetryInterval()) * config.getRetryAttempts();timeout = Math.max(timeout, 1);// 解锁RFuture<Boolean> r = unlockInnerAsync(threadId, id, timeout);CompletionStage<Boolean> ff = r.thenApply(v -> {CommandAsyncExecutor ce = commandExecutor;if (ce instanceof CommandBatchService) {ce = new CommandBatchService(commandExecutor);}ce.writeAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.DEL, getUnlockLatchName(id));if (ce instanceof CommandBatchService) {((CommandBatchService) ce).executeAsync();}// 释放锁的结果return v;});return new CompletableFutureWrapper<>(ff);
}
这里的重点主要关注 unlockInnerAsync 方法,通过使用 Lua 脚本进行解锁的操作:
protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {// lua 脚本return evalWriteSyncedAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,// 从Redis中获取锁的状态。"local val = redis.call('get', KEYS[3]); " +//如果不是false"if val ~= false then " +//将其转换为数字并返回,也就是 true 返回 1"return tonumber(val);" +"end; " +// 如果哈希表锁中不存在线程ID,表示锁已经被释放,返回nil。"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +"return nil;" +"end; " +//对锁中的线程ID的值减1,并将结果存储在 counter 变量中。这是一个计数器的操作。"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +//如果计数器值大于0,表示锁仍然被持有。"if (counter > 0) then " +// 更新哈希表锁的过期时间。"redis.call('pexpire', KEYS[1], ARGV[2]); " +// 设置键锁的状态值为0,并设置过期时间,表示锁仍然被持有。"redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +//返回0,表示锁仍然被持有"return 0; " +"else " + //如果计数器值不大于0,表示锁即将被释放。//删除锁"redis.call('del', KEYS[1]); " +"redis.call(ARGV[4], KEYS[2], ARGV[1]); " +// 设置键锁的状态值为1,并设置过期时间,表示锁已经被释放。"redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +//返回1,表示锁已经被释放"return 1; " +"end; ",Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
}
需要注意的是,在 Lua 脚本中,如果锁还存在的话,就对 hash 中的 value 减一,如果此时 value 结果还大于 0 的话,则表示这是重入锁的场景,此时不能直接删除锁,而是对重入的次数进行减一,并且要重置过期时间。
下面再回到 unlockAsync0(long threadId) 方法中,释放锁通过 Lua 脚本实现了,下面看下 cancelExpirationRenewal(Long threadId) 关闭看门狗的操作:
protected void cancelExpirationRenewal(Long threadId) {// 从记录中获取信息ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (task == null) {return;}if (threadId != null) {// 移除线程IDtask.removeThreadId(threadId);}if (threadId == null || task.hasNoThreads()) {// 关闭计时任务Timeout timeout = task.getTimeout();if (timeout != null) {timeout.cancel();}// 从缓存记录中删除EXPIRATION_RENEWAL_MAP.remove(getEntryName());}
}
这里就比较好理解了,停止计时任务,从缓存记录中移除。
相关文章:
Redisson 源码解析 - 分布式锁实现过程
一、Redisson 分布式锁源码解析 Redisson是架设在Redis基础上的一个Java驻内存数据网格。在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常…...
玩转贝启科技BQ3588C开源鸿蒙系统开发板 —— 开发板详情与规格
本文主要参考: BQ3588C_开发板详情-开源鸿蒙技术交流-Bearkey-开源社区 BQ3588C_开发板规格-开源鸿蒙技术交流-Bearkey-开源社区 厦门贝启科技有限公司-Bearkey-官网 1. 开发板详情 RK3588 核心板是一款由贝启科技自主研发的基于瑞芯微 RK3588 AI 芯片的智能核心…...
Qt pro文件
1. 项目通常结构 2.pri文件 pri文件可定义通用的宏,例如创建一个COMMON.pri文件内容为 COMMON_PATH D:\MyData 然后其它pri或者pro文件如APPTemplate.pro文件中通过添加include(Common.pri) ,QtCreator就会自动在项目结构树里面创建对应的节点 3.变量…...
实验笔记之——服务器链接
最近需要做NeRF相关的开发,需要用到GPU,本博文记录本人配置服务器远程链接的过程,本博文仅供本人学习记录用~ 连上服务器 首先先确保环境是HKU的网络环境(HKU AnyConnect也可)。伙伴已经帮忙创建好用户(第一次登录会提示重新设置密码)。用cmd ssh链接ssh -p 60001 <u…...
微服务-java spi 与 dubbo spi
Java SPI 通过一个案例来看SPI public interface DemoSPI {void echo(); } public class FirstImpl implements DemoSPI{Overridepublic void echo() {System.out.println("first echo");} } public class SecondImpl implements DemoSPI{Overridepublic void ech…...
redis复习笔记03(小滴课堂)
Redis6常见数据结构概览 0代表存在,1代表不存在。 1表示删除成功,0表示失败。 查看类型,默认string类型。 也可以设置set类型。 list类型。 查看key的过期时间: Redis6数据结构之String类型介绍和应用场景 批量设置: …...
【Spring Cloud】关于Nacos配置管理的详解介绍
🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《Spring Cloud》。🎯🎯 &am…...
基于Java SSM框架实现校园网络维修系统项目【项目源码】
基于java的SSM框架实现校园网络维修系统演示 java简介 Java主要采用CORBA技术和安全模型,可以在互联网应用的数据保护。它还提供了对EJB(Enterprise JavaBeans)的全面支持,java servlet API,JSP(java serve…...
项目框架构建之3:Nuget服务器的搭建
本文是“项目框架构建”系列之3,本文介绍一下Nuget服务器的搭建,这是一项简单的工作,您或许早已会了。 1.打开vs2022创建Asp.net Web应用程序 框架选择.net framework4.8,因为nuget服务器只支持.net framework。 2.选择空项目和保…...
外包干了1个月,技术退步一大半。。。
先说一下自己的情况,本科生,19年通过校招进入广州某软件公司,干了接近4年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试…...
167. 木棒(dfs剪枝,经典题)
167. 木棒 - AcWing题库 乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。 然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。 请你设计一个程序࿰…...
用HTML的原生语法实现两个div子元素在同一行中排列
代码如下: <div id"level1" style"display: flex;"><div id"level2-1" style"display: inline-block; padding: 10px; border: 1px solid #ccc; margin: 5px;">这是第一个元素。</div><div id"…...
C++进阶--map和set的介绍及使用
map和set的介绍及使用 一、关联式容器与键值对关联式容器键值对pair树形结构的关联式容器 二、set2.1 set的介绍2.2 set的使用2.2.1 set的模板参数列表2.2.2 set的构造2.2.3 set的迭代器2.2.4 set的容量2.2.5 set修改操作2.2.6 set的使用举例 三、multiset3.1 multiset的介绍3.…...
MIML-DA
图3呢?且作者未提供代码...
[ROS2 Foxy]#1.3 安装使用 turtlesim
官网教程: https://docs.ros.org/en/foxy/Tutorials/Turtlesim/Introducing-Turtlesim.html 1.turtlesim安装和使用 turtlesim是一个轻量级的模拟程序,用来学习ROS2 .通过turtlesim来介绍ROS2在一个基础的水平上都要做了那些事,以此让我们了解将来在真的 robot或者模拟器上使用…...
嵌入式培训机构四个月实训课程笔记(完整版)-Linux系统编程第三天-Linux进程(物联技术666)
更多配套资料CSDN地址:点赞+关注,功德无量。更多配套资料,欢迎私信。 物联技术666_嵌入式C语言开发,嵌入式硬件,嵌入式培训笔记-CSDN博客物联技术666擅长嵌入式C语言开发,嵌入式硬件,嵌入式培训笔记,等方面的知识,物联技术666关注机器学习,arm开发,物联网,嵌入式硬件,单片机…...
1-01初识C语言
一、概述 C语言是贝尔实验室的Ken Thompson(肯汤普逊)、Dennis Ritchie(丹尼斯里奇)等人开发的UNIX 操作系统的“副产品”,诞生于1970年代初。 Thompson和Ritchie共同创作完成了Unix操作系统,他们都被称为…...
Python字符串
定义字符串 Python中要定义一个字符串,有比较多的一种方式。 示例代码: s "你好,张大鹏" print(s, type(s))s 你好,张大鹏 print(s, type(s))s """你好,张大鹏""" prin…...
PHP 基础编程 1
文章目录 前后端交互尝试php简介php版本php 基础语法php的变量前后端交互 - 计算器体验php数据类型php的常量和变量的区别php的运算符算数运算符自增自减比较运算符赋值运算符逻辑运算 php的控制结构ifelseelse if 前后端交互尝试 前端编程语言:JS (Java…...
Android studio BottomNavigationView 应用设计
一、新建Bottom Navigation Activity项目: 二、修改bottom_nav_menu.xml: <itemandroid:id="@+id/navigation_beijing"android:icon="@drawable/ic_beijing_24dp"android:title="@string/title_beijing" /><itemandroid:id="@+i…...
日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...
华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
从零开始打造 OpenSTLinux 6.6 Yocto 系统(基于STM32CubeMX)(九)
设备树移植 和uboot设备树修改的内容同步到kernel将设备树stm32mp157d-stm32mp157daa1-mx.dts复制到内核源码目录下 源码修改及编译 修改arch/arm/boot/dts/st/Makefile,新增设备树编译 stm32mp157f-ev1-m4-examples.dtb \stm32mp157d-stm32mp157daa1-mx.dtb修改…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...
