业务逻辑编排错误 & TTL浅拷贝导致参数丢失问题

前言 在DDD项目中,为了方便参数的传递,通常会使用ThreadLocal来保存一个对象来实现对参数的跨方法传递,避免通过形参的形式传递。在内部项目中,有一个项目使用的是 alibaba开源的 transmittable-thread-local来存储参数,新建了一个上下文对象(AbilityContext.java),使用HashMap来临时存储和获取参数。 AbilityContext 示例 public class AbilityContext { private static final ThreadLocal<Map<String, Object>> CONTEXT = new TransmittableThreadLocal<>(); private AbilityContext() { } /** * 初始化上下文 */ public static void initContext() { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); } else { CONTEXT.get().clear(); } } /** * 清除上下文 */ public static void clearContext() { CONTEXT.remove(); } public static Map<String, Object> getInnerMap() { return CONTEXT.get(); } /** * 获取上下文内容 */ public static <T> T getValue(String key) { Map<String, Object> con = CONTEXT.get(); if (con == null) { return null; } return (T) con.get(key); } /** * 设置上下文参数 */ public static void putValue(String key, Object value) { Map<String, Object> con = CONTEXT.get(); if (con == null) { CONTEXT.set(new HashMap<>(8)); con = CONTEXT.get(); } con.put(key, value); } } 项目情况介绍 通常来说,DDD项目的基本流程是由interface->application,中间封装一层来集中处理上下文的初始化和清空动作,如下图: 在正常情况下,上述流程可以正确的完成参数的写入和获取,但是,在项目运行过程中遇到了一个bug,正常写入参数后,偶现性(低频)获取值为NULL,导致程序出错,示例代码如下(隐去业务代码,重新写的伪代码): 其中demo()方法为当时复现的方法 demo2()为伪代码,是业务代码中调用了另一个application,假设其逻辑和demo()方法一致的业务代码。 @Slf4j public class AlibabaTtlWrongUsageExampleApplication { public static void main(String[] args) { demo(i); } private static void demo(int idx) { // 初始化 AbilityContext.initContext(); // 赋业务值 AbilityContext.putValue("main", "mainValue"); // 这里简化了代码,实际上经过了很多层业务代码调用后才出现了此方法 ThreadUtil.execute(() -> { execute->demo2(); }); // do something // 主线程再次获取业务值(偶现为null) String value = AbilityContext.getValue("main"); if (Objects.isNull(value)) { log.warn("lastGetNullValue, idx={}", idx); } } } 上述代码运行设置了一个key=main,值为mainValue。在下方AbilityContext.getValue("main")偶现获取==NULL。 展开分析 当时在分析的开始有推测是业务代码中参数被重新赋值为NULL,但通过对后续业务代码逐行查看,并没有找到重新赋值的逻辑。 在深入业务代码分析的过程中,发现主流程中有一个异步方法调用(ThreadUtil.execute()),再次调用了另一个领域服务(这是不符合DDD规范的!),而领域服务的入口都会AbilityContext.initContext()的逻辑,通过这个线索 ,继续展开了深入分析。 编码者的初衷可能是想到异步线程已经脱离了当前线程,再次调用 initContext()方法是初始化了一个新的对象上下文,但是由于项目使用的是 alibaba TTL,能够实现跨线程的传递,所以在子线程中依旧能拿到父线程的HashMap。并且TTL默认是使用的浅拷贝对象。由于initContext()中,调用了HashMap.clear()方法,相当于将父线程的HashMap给清空了!。 通过比对父子线程的hashCode值确定为同一对象 // 主线程获取hashCode final int hashCode = AbilityContext.getInnerMap().hashCode(); ThreadUtil.execute(() -> { // 子线程对比hashCode log.info("{}, ThreadUtil hashCode={}", idx, AbilityContext.getInnerMap().hashCode() == hashCode); // 子线程再次初始化(错误的根源) AbilityContext.initContext(); // do something }); 14:42:28.198 [pool-1-thread-26] INFO top.imyzt.learning.caseanalysis.ttl.AlibabaTtlWrongUsageExampleApplication -- 25, ThreadUtil hashCode=true 持续分析 有了上述的线索,基本把问题原因找到了,但是为什么是偶现的呢? 因为使用了异步线程,而线程的调度由操作系统的线程调度算法来决定,并不是一定保证顺序的,所以只要当操作系统优先调度异步线程,那么HashMap就被清空了,如果主线程优先往下走,那么就能够获取到完整的HashMap。 后记 至此,问题分析就告一段落了,整个过程中涉及到 TTL值的父子线程传递、对象浅拷贝、线程的调度,还涉及到了DDD的不规范逻辑编排,整个分析下来花费了一上午的时间,收获还是很大的。 transmittable-thread-local TransmittableThreadLocal的传递只有浅拷贝吗? 线程的优先级 我将源代码上传了GitHub,如果你想在本地调试运行上述案例,可以下载到本地调试,有问题可以评论区沟通。
Read More ~

使用Redis实现分布式锁的坑

分布式锁的关键在于对单一资源的竞争。获得资源的实例将继续执行,其余实例要么退出(互斥锁),要么等待(阻塞锁)。 实现分布式锁的方案有很多,既可以直接使用MySQL作为分布式锁(例如xxl-job),也可以利用ZooKeeper、Redis等。 在基于Spring Cloud的业务系统中,一般都会引入Redis作为分布式缓存中间件,因此更多的人会选择使用Redis来实现分布式锁。本文将介绍使用Redis作为分布式锁时常见的问题和解决方法。 1. 没有使用原子操作指令 错误写法 Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue); stringRedisTemplate.expire(lockKey, Duration.ofSeconds(expireTime)); if (!tryLock) { return; } 上述操作通常出现在新手阶段,在写入锁对象时,没有考虑到原子性问题。在Redis中有提供SET NX PX指令,支持在设置锁的同时指定过期时间,并且支持原子性判断key是否已存在。 NX 和 PX 是 Redis 命令中用于设置 key 的两个选项。 NX: 当指定 NX 选项时,只有在 key 不存在的情况下才会设置 key 的值。如果 key 已经存在,则不进行任何操作。 PX: PX 选项用于设置 key 的过期时间(以毫秒为单位)。例如,PX 10000 表示在 10 秒后将 key 设置为过期状态。 正确写法: Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS); 2. 释放了别人的锁 错误写法 try { Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS); if (!tryLock) { return; } // do something } finally { stringRedisTemplate.delete(lockKey); } 在加锁的过程中,没有设定唯一值作为Value存储到Redis中,在释放时,不判断直接对锁进行释放。其二,将获取锁的代码放在了try代码块中。 在上述代码中存在两个问题: 不该执行到finlly代码块:A请求获得了锁正在执行业务代码,而B请求没有获得锁,但是因为获取锁的代码在try代码块中,导致finally一定会执行,B请求就会将A请求的锁释放,而如果A请求依旧未执行完毕,此时C请求过来时,则C请求错误的拿到了锁。 不该删除别人的锁:在删除锁时,应该判断自己是否是上锁人,由于多次执行Redis指令不具备原子性,所以一般是交由LUA脚本来实现的。 if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end 正确写法 提前将LUA脚本载入到Redis服务端 script = new DefaultRedisScript<>(); script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource(new ClassPathResource("release_lock.lua"))); 获取和释放锁示例 Boolean tryLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.MILLISECONDS); if (!tryLock) { return; } try { // do something } finally { ArrayList<String> keys = new ArrayList<>(); keys.add(context.getLockKey()); stringRedisTemplate.execute(this.script, keys, context.getLockValue()); } 3. 事务未提交锁就释放了 错误代码 /** * 事务内获取分布式锁 */ @Transactional(rollbackFor = Exception.class) public void saveUserWithDistributedLock(String name) { String lockKey = "lock_key:" + name; RedisLock.LockContext lockContext = redisLock.tryLock(lockKey, 10000L); if (!lockContext.getTryLock()) { // printLog("没拿到锁"); return; } printLog("拿到锁了" + lockKey); try { this.save(name); } finally { redisLock.release(lockContext); printLog("释放锁了"); } } MySQL常规情况下是RR的隔离级别,只有等到事务提交数据才对其他事务可见,存在**“读视图”,在上述的代码中,A请求拿到了锁执行了业务代码,执行到redisLock.release时将锁释放了,但Spring的@Transactional依赖的是AOP,其需要等到方法执行完毕才会提交事务,在这个临界点,B请求可以正常拿到锁,但是A请求的事务还未提交,B请求的读视图**中还未查询到A请求提交的数据,最终造成了数据的不一致性。 正确代码 正确的情况是在另一个方法中获取到锁之后,再调用包含事务的业务代码。此时需要注意SpringAOP在本方法内代理失效的问题,通常需要新建一个Service来处理。 业务代码执行超过锁过期时间 错误代码 // Domain-Service public void save(String name) { String lockKey = "lock_key:" + name; RedisLock.LockContext lockContext = redisLock.tryLock(lockKey, 10000L); if (!lockContext.getTryLock()) { printLog("没拿到锁"); return; } printLog("拿到锁了" + lockKey); try { userService.save(name); } finally { redisLock.release(lockContext); printLog("释放锁了"); } } // UserService @Transactional(rollbackFor = Exception.class) public void save(String name) { List<User> users = userRepository.findUsersByName(name); if (CollUtil.isNotEmpty(users)) { printLog("已经写入, 不再写入" + users); return; } // 业务保存模拟执行很慢 TimeUnit.SECONDS.sleep(70); } 上述代码中,锁对象只有10s的时间,但是业务代码执行却需要70s,A请求虽然拿到了锁,此时后续10秒其他请求均无法获取锁,但是从第11秒开始的请求将可以拿到锁,而此时A请求还未执行完毕,此时开始出现错误的获取锁,最终造成数据的不一致。 正确写法 参考Redisson的WatchDog机制,另外开辟线程每隔 10s 就给还未执行完毕的 Key 自动续期 30s,保证业务代码能够安全的执行完毕再自行释放锁对象。 示例代码: // watch dog Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> { if (!LOCK_CONTEXTS.isEmpty()) { for (LockContext lockContext : LOCK_CONTEXTS) { // 如果执行线程还未释放锁, 续期30s(模拟Redisson) stringRedisTemplate.expire(lockContext.getLockKey(), Duration.ofSeconds(30)); Long expire = stringRedisTemplate.getExpire(lockContext.getLockKey()); log.info("WatchDog, expire 30s, lockKey={}, ttl={}", lockContext.getLockKey(), expire); } } }, 0, // 10秒检测一次 10, TimeUnit.SECONDS); 后记 分布式锁的错误还有很多,本篇主要是自己在工作过程中遇到的一些坑,着重介绍新手阶段在编写分布式锁时遇到的比较基础的问题,后面有空再进行其他场景的逐个介绍。 本文参考:聊聊redis分布式锁的8大坑 本文代码:redis-lua-distributed-lock
Read More ~

错用HashedWheelTimer导致的OOM问题

事件中心在私有化环境下,只要server一启动过几秒就oom,查看日志是 Failed to create a thread: retVal -1073741830, errno 11。 异常堆栈: Caused by: java.lang.OutOfMemoryError: Failed to create a thread: retVal -1073741830, errno 11 at java.lang.Thread.startImpl(Native Method) at java.lang.Thread.start(Thread.java:993) at io.netty.util.HashedWheelTimer.start(HashedWheelTimer.java:366) at io.netty.util.HashedWheelTimer.newTimeout(HashedWheelTimer.java:447) at 业务调用代码省略 在标品环境下没有问题,在其他KA客户上也没有问题 通过对日志的分析,最终发现是事件中心的延迟消息代码存在缺陷,使用了Netty的HashedWheelTimer,但是语法存在问题,理论上应该是new一个HashedWheelTimer来处理所有时间延迟,但是错用程每次new一个新的HashedWheelTimer,HashedWheelTimer内部每次都会new一个新的线程来处理做调度,一个线程占用1MB,最终内存资源被耗尽。 io.netty.util.HashedWheelTimer#HashedWheelTimer源码: public HashedWheelTimer( ThreadFactory threadFactory, long tickDuration, TimeUnit unit, int ticksPerWheel, boolean leakDetection, long maxPendingTimeouts, Executor taskExecutor) { checkNotNull(threadFactory, "threadFactory"); checkNotNull(unit, "unit"); checkPositive(tickDuration, "tickDuration"); checkPositive(ticksPerWheel, "ticksPerWheel"); this.taskExecutor = checkNotNull(taskExecutor, "taskExecutor"); // Normalize ticksPerWheel to power of two and initialize the wheel. wheel = createWheel(ticksPerWheel); mask = wheel.length - 1; // Convert tickDuration to nanos. long duration = unit.toNanos(tickDuration); // Prevent overflow. if (duration >= Long.MAX_VALUE / wheel.length) { throw new IllegalArgumentException(String.format( "tickDuration: %d (expected: 0 < tickDuration in nanos < %d", tickDuration, Long.MAX_VALUE / wheel.length)); } if (duration < MILLISECOND_NANOS) { logger.warn("Configured tickDuration {} smaller then {}, using 1ms.", tickDuration, MILLISECOND_NANOS); this.tickDuration = MILLISECOND_NANOS; } else { this.tickDuration = duration; } // 每次都new一个线程来处理 workerThread = threadFactory.newThread(worker); leak = leakDetection || !workerThread.isDaemon() ? leakDetector.track(this) : null; this.maxPendingTimeouts = maxPendingTimeouts; if (INSTANCE_COUNTER.incrementAndGet() > INSTANCE_COUNT_LIMIT && WARNED_TOO_MANY_INSTANCES.compareAndSet(false, true)) { reportTooManyInstances(); } } 因为标品和其他KA使用的是阿里云RocketMQ,此客户使用的是自建的开源版RocketMQ,开源RocketMQ是没有自定义时长的延迟消息的,所以我们自己实现了一套时间轮来实现任意时长的延迟消息,当小于60s的延迟消息会丢入我们的时间轮来处理延迟投递,当时此客户的环境中有大量的60s内的延迟消息,导致一启动就会崩溃。 不过在RocketMQ5.0也支持任意时长了。
Read More ~

CircuitBreak导致ThreadLocal参数丢失问题

背景 使用OpenFeign时,通常会实现RequestInterceptor接口来自定义FeignConfiguration,OpenFeign暴露了feign.RequestTemplate信息,给到我们在发送请求前自定义参数信息的扩展点。 在分布式系统中,通常会将本服务的信息(UserInfo、RequestId)透传至下游服务,从而实现分布式链路追踪等功能,对于像用户信息等,在Web系统中通常使用 ThreadLocal 来存储信息,在自定义的FeignConfiguration中获取ThreadLocal再塞入到feign.RequestTemplate中,实现向下游服务的传递,示例: public class FeignConfiguration implements RequestInterceptor { @Override public void apply(RequestTemplate template) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String userId = SubjectContext.get().getUserId(); if (null != attributes) { HttpServletRequest request = attributes.getRequest(); template.header("token", request.getHeader("TOKEN")); template.header("userId", userId); } } } 简单的Context示例: public class SubjectContext { protected static ThreadLocal<UserInfo> subjectContext = new ThreadLocal(); public static void remove() { subjectContext.remove(); } public static void set(UserInfo uerInfo) { subjectContext.set(uerInfo); } public static UserInfo get() { return (UserInfo)subjectContext.get(); } } 出现错误 上述代码在常规情况下,是能够按照预期执行的。 但是最近项目引入了CircuitBreaker作为服务熔断的断路器之后,上述代码在执行到SubjectContext.get()时,会抛出空指针,拿不到用户信息。 通过分析CircuitBreaker的源码,最终定位到代码出现在Resilience4JCircuitBreaker内部,在Resilience4JCircuitBreaker中有一个public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback)方法,方法入参的toRun就是封装过的我们定义的Feign接口,其包装过程在FeignCircuitBreakerInvocationHandler#asSupplier代码中,如下: private Supplier<Object> asSupplier(final Method method, final Object[] args) { final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); return () -> { try { RequestContextHolder.setRequestAttributes(requestAttributes); // 执行我们的真正方法 return dispatch.get(method).invoke(args); } catch (RuntimeException throwable) { throw throwable; } catch (Throwable throwable) { throw new RuntimeException(throwable); } }; } Spring Cloud CircuitBreaker Resilience4j 提供了两种实现: 使用 Semaphores 的 SemaphoreBulkhead。 一个 FixedThreadPoolBulkhead,它使用一个有界队列和一个固定的线程池。 默认情况下,Spring Cloud CircuitBreaker Resilience4j 使用 FixedThreadPoolBulkhead。要修改默认行为以使用 SemaphoreBulkhead,请将属性 spring.cloud.circuitbreaker.resilience4j.enableSemaphoreDefaultBulkhead 设为 true。 正是由于上述原因,默认将我们的FeignConfiguration提交给了线程池,由于我们使用的是ThreadLocal导致线程本地变量没有向子线程传递,在执行FeignConfiguration时子线程无法拿到Context信息,最终导致程序的报错。 解决办法 通过分析源码我们发现,执行任务的线程池Resilience4JCircuitBreaker#executorService是由外部传递过来进行初始化的,调用方在Resilience4JCircuitBreakerFactory#create(java.lang.String, java.lang.String, java.util.concurrent.ExecutorService) 在Resilience4JCircuitBreakerFactory中发现,是由本实例在create方法被调用时传入的本类的成员变量,即: private ExecutorService executorService = Executors.newCachedThreadPool(); private ConcurrentHashMap<String, ExecutorService> executorServices = new ConcurrentHashMap<>(); 而我们在没有定义自定义Feign Group时,默认使用的就是executorService,在本类中有一个Resilience4JCircuitBreakerFactory#configureExecutorService方法专门保留了外部传入自定义线程池的扩展,我们可以自己实现创建一个支持传递Context到子线程的线程池,即可将参数向下传递,比如像这样: @Configurable @AllArgsConstructor public class CircuitBreakerConfiguration implements ApplicationRunner { private final Resilience4JCircuitBreakerFactory factory; @Override public void run(ApplicationArguments args) throws Exception { ContextThreadPoolExecutor contextThreadPoolExecutor = new ContextThreadPoolExecutor(2, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<>(1024)); // **change ThreadPoolExecutor** factory.configureExecutorService(contextThreadPoolExecutor); } public static class ContextThreadPoolExecutor extends ThreadPoolExecutor { public ContextThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); } public void execute(Runnable command) { super.execute(wrap(command)); } private static Runnable wrap(Runnable runnable) { **SubjectContext context = SubjectContext.getContext();** return () -> { // 将参数向下传递 **SubjectContext.setContext(context);** try { runnable.run(); } finally { **SubjectContext.clear();** } }; } } } 后记 上述的方案只解决了没有自定义Group的情况,官方在自定义Group的情况下是没有保留扩展位的,所以给官方提了一个MR并且已成功合并到主分支,如下: Customizable groupExecutorService #180
Read More ~

IDEA 常用插件

编码类 Generate All Getter And Setter 如其名 AceJump 快速将光标导航到编辑器中可见的任何位置,ctrl+,然后输入字符,进行高亮定位 GsonFormat 将JSONObject格式的String 解析成实体。 Lombok 针对模型对象精简代码 MybatisCodeHelperPro 通过方法名来生成sql,全自动提示,自动检测 工具类 any-rule 可以搜索正则表达式 CamelCase 变量名转驼峰 Jrebel 快速实现热部署,跳过了构建和部署的过程,可以省去大量的部署用的时间。 jrebel-mybatisplus-extension 热部署中mybatis的扩展,他的功能就是配合 Jrebel 一起使用,修改 mybatis 的 mapper.xml 文件不用重启项目 CodeGlance 显示代码地图插件,进行语法突出显示 RestfulToolKit 可以搜索接口 POJO TO JSON 将pojo类转换为json Translation 翻译插件 Maven类 Maven Helper 依赖冲突分析利器 Maven Dependency Helper Maven中央仓库搜索 规范类 P3C 编码规范的一个插件,高亮提示违规代码 SonarLint 打开文件的时候自动进行代码审查 提效类 Key Promoter X 在IDEA里面使用鼠标的时候,如果这个鼠标操作是能够用快捷键替代,此插件会进行提示 POJO TO JSON 将pojo类转换为json 装修类 Nyan Progress Bar 漂亮的进度条 Rainbow Brackets 配对括号相同颜色,并且实现选中区域代码高亮的功能
Read More ~

IDEA 2023 非常用配置

配置文件全部使用UTF-8 Editor-File Encodings,将所有内容均设置为UTF-8。 Create UTF-8 files 选择 with NO BOM,避免在创建文件前3个字节来标志为UTF-8文件。 参数提示 默认情况下,是没有参数提示的,需要手动开启。 Editor-General-Code Completion-Parameter Info 注释默认在当前缩进 默认情况下,自动补全注释是在最前面,可以通过: Editor-Code Style-Java(其它语言位置类似)-Code Generation-Comment Code按如下图配置进行处理。 如果配置的内容,在新项目不生效,则注意此处 除了字体样式字体大小外,其它的配置IDEA官方认为,每个项目在创建时,可能都需要一些不同的配置,所以有很多内容都是跨项目不生效的(Maven、文件格式等),需要在此处配置(点进去和Settings一模一样,但是对应的是Next Project(新项目)的配置)
Read More ~

使用highlightjs让Gridea支持代码高亮

Gridea的Lemon主题,默认不支持代码高亮,需要自己补充。目前开源的highlight.js正合适。记录下处理的过程 打开官网,挑选自己喜欢的风格 highlightjs.org/examples 找一个CDN托管网站,避免走自己的小油管,这里推荐bootcdn(这么多年了,还是如此坚挺) www.bootcdn.cn/highlight.js 将上述找到的代码,应用在自己主题下的post.ejs的 <head> 代码块中。 1. ~/Gridea/themes/gridea-theme-lemon,找到自己的主题目录,gridea-theme-lemon是我的Lemon主题目录。 2. 编辑post.ejs文件,vim templates/post.ejs 3. 在<head>中补充如下代码 <link href="https://cdn.bootcdn.net/ajax/libs/highlight.js/11.8.0/styles/此处选择自己喜欢的主题.min.css" rel="stylesheet"> <script src="https://cdn.bootcss.com/highlight.js/9.12.0/highlight.min.js"></script> <script>hljs.initHighlightingOnLoad();</script>
Read More ~

Java 20~21 新特性

Java 20没有发布重要的新特性,本文以Java21版本为主。 Java 21 是新的LTS版本,其中发布了众多新的特性。 字符串模板(预览版) 使用官方的STR、FMT 支持通过STR、FMT来实现字符串拼接,可以自定义模板处理器组织字符串输出形式。 import java.time.LocalDate; import java.time.format.DateTimeFormatter; import static java.util.FormatProcessor.FMT; /** * @author imyzt * @date 2023/12/19 * @description 模板字符串 */ public class TemplateString { public static void main(String[] args) { String str = "world"; String result = STR."hello \{str}"; System.out.println(result); System.out.println(STR); String name = "yzt"; String[] blogAddress = {"imyzt.top", "blog.imyzt.top"}; String text = FMT.""" My name is \{name} My blog address is \{blogAddress[0].toUpperCase()}, \{blogAddress[1].toLowerCase()}"""; System.out.println(text); System.out.println(STR."\{Math.random()}"); System.out.println(STR."\{Integer.MAX_VALUE}"); System.out.println(STR."\{index++}, \{++index}"); //hello world //java.lang.StringTemplate$$Lambda/0x00000001260457f0@33c7353a //My name is yzt //My blog address is IMYZT.TOP, blog.imyzt.top //0.9361799484353136 //2147483647 //现在的时间是: 2023-12-20 //0, 2 } } 自定义StringTemplate import java.util.Iterator; /** * @author imyzt * @date 2023/12/20 * @description 自定义StringTemplate */ public class CustomTemplate { public static void main(String[] args) { var INTER = StringTemplate.Processor.of((StringTemplate st) -> { StringBuilder sb = new StringBuilder(); Iterator<String> fragments = st.fragments().iterator(); for (Object value : st.values()) { sb.append(fragments.next()); sb.append(value); } sb.append(fragments.next()); return sb.toString(); }); String name = "yzt"; int index = 0; String text = INTER.""" { "name":\{name}, "index":\{++index} } """; System.out.println(text); //{ //"name":yzt, //"index":1 //} } } Record Patterns 记录模式更强大了,支持直接在表达式中创建和使用。还支持嵌套 /** * @author imyzt * @date 2023/12/19 * @description 记录模式 */ public class RecordPatterns { public static void main(String[] args) { Object p = new Point(1, 2); if (p instanceof Point(int x, int y)) { System.out.println(x + y); } Object w = new Window(new Point(1, 2), 3); if (w instanceof Window(Point(int x, int y), int z)) { System.out.println(x + y + z); } } } record Point(int x, int y) {} record Window(Point p, int z) {} ScopedValue ScopedValue是一个隐藏的方法参数,只有方法可以访问ScopedValue,它可以让两个方法传递参数时无需声明形参(对于Web项目中传递用户信息是非常场景的操作) ThreadLocal的问题 通常对于上述场景,都会采用ThreadLocal来解决,但是由于ThreadLocal在设计上的瑕疵,导致一直有以下问题: 内存泄露,在使用完ThreadLocal之后,若没有调用remove方法,会出现内存泄漏。 增加开销,在具有继承关系的线程中,子线程需要为父线程中ThreadLocal里面的数据分配内存 不是不可变对象,在方法中可以随意调用set方法篡改。 随着虚拟线程的到来,内存泄漏问题不用担心了,因为虚拟线程会很快的终止,此时会自动删除ThreadLocal中的数据,这样就不用调用remove方法了。 但是虚拟线程的数量是非常多的,假如有上百万个虚拟线程都要拷贝一份ThreadLocal中的变量(问题2),内存将会被严重的浪费掉。 示例 示例代码中通过模拟一个“送礼物”的场景,来演示ScopedValue。 在 giveGift 方法中,将礼物送出,是500元 在 receiveMiddleMan 方法模拟中间人抽成的场景,抽成后送出仅 200元 在 receiveGift 方法中 ,对 ScopedValue 进行获取,得到的就是200元 但整个过程中,中间人 receiveMiddleMan 在送出前后获取到的信息都是 500元,这正是 Scope 的含义,修改只在本作用域(方法)中生效。 /** * @author imyzt * @date 2023/12/20 * @description ScopedValue */ public class ScopedValueTest { private static final ScopedValue<String> GIFT = ScopedValue.newInstance(); public static void main(String[] args) { ScopedValueTest test = new ScopedValueTest(); test.giveGift(); //中间人开始: 500 //收礼物: 200 //中间人结束: 500 } private void giveGift() { ScopedValue.where(GIFT, "500").run(this::receiveMiddleMan); } private void receiveMiddleMan() { System.out.println(STR."中间人开始: \{GIFT.get()}"); ScopedValue.where(GIFT, "200").run(this::receiveGift); System.out.println(STR."中间人结束: \{GIFT.get()}"); } private void receiveGift() { System.out.println(STR."收礼物: \{GIFT.get()}"); } } Switch 又升级了,支持when关键字了 switch 可谓是升级升级再升级,我还是在用 if/else 啊。 从12开始,一路升级。 支持 -> 表达式(Java 12) 支持返回值(Java 12) case支持单行写多个条件(Java 12) 新增yield关键字返回switch的值(Java 12) 引入模式匹配(Java 15) 这回又新增了一个when关键字(都快改成SQL了) (Java 21) Java switch升级之路 /** * @author imyzt * @date 2023/12/20 * @description Switch 又升级了~, 这次支持when关键字了 */ public class SwitchFuture { void main() { var str = "yes"; var result = switch (str) { case null -> "空对象"; case String s when "yes".equals(s) -> { System.out.println("确定"); yield "字符串的Yes"; } case String s when "no".equals(s) -> { System.out.println("取消"); yield "字符串的No"; } default -> "default"; }; System.out.println(result); //确定 //字符串的Yes } } 简化了main方法(注意看上一个章节的main方法) Class声明和强制的public访问修饰符是必须的。当用在外部组件定义良好的接口封装代码单元时,它们很有用。但在这个小例子中,它们毫无意义。 String[]参数主要用于将代码与外部组件(在本例中为操作系统的shell,接收命令传入的参数)连接。它在这里很神秘且无用,尤其是它从未被使用过。 static修饰符是Java类和对象模型的一部分。对于新手来说,这不仅是神秘的,而且是有害的:要添加更多可以调用和使用的方法或字段,学​​生必须要么将它们全部声明(传播一种既不常见也不是好习惯的用法),或者就要面对是否有static修饰的区别问题,并学习如何实例化对象。 未命名模式和变量(Go:和我有点像) /** * @author imyzt * @date 2023/12/21 * @description 像golang学习, 支持不使用的变量不命名了 */ public class UnnamedVariable { void main() { List<String> list = new ArrayList<>(); list.add("first"); list.add("last"); try { System.out.println(STR."first -> \{list.getFirst()}"); System.out.println(STR."last -> \{list.getLast()}"); System.out.println("try"); } catch (Exception _) { System.out.println("异常了, 但是我没有 Exception"); } } } 有序集合 Sequenced Collections Sequenced Collections引入了三个新接口: SequencedCollection SequencedMap SequencedSet 接口都附带了一些新方法,可以提高操作集合的效率。比如:java.util.SequencedCollection#reversed等等。 集合补充了获取收尾元素的方法(hutool:当我不存在?) List<String> list = new ArrayList<>(); list.add("first"); list.add("last"); System.out.println(STR."first -> \{list.getFirst()}"); System.out.println(STR."last -> \{list.getLast()}"); 加了一些工具类 StringBuilder中的repeat方法 补充java.lang.Character#isEmoji 等等 /** * @author imyzt * @date 2023/12/21 * @description 加了些新方法 */ public class SomethingExample { void main() { System.out.println(STR."repeat => \{new StringBuffer().repeat("*", 10)}"); //repeat => ********** var happy = "你开心吗? 😄"; System.out.println(STR."isEmoji => \{happy.codePoints().anyMatch(Character::isEmoji)}"); //isEmoji => true } }
Read More ~

Java 19 新特性

Java 19 最核心的特性就是虚拟线程(Virtual Threads) 简介 该特性在Java19中是预览特性,虚拟线程是用户态下的线程,和go语言中的goroutines,Erlang中的processes类似,虚拟线程并非比线程快,而是提高了应用的吞吐量,相比于传统的线程是由操作系统调度来看,虚拟线程是我们自己程序调度的线程。虚拟线程的出现并没有修改Java原有的并发模型,也不会替代原有的线程,虚拟线程主要作用是提高服务器端的吞吐量(主要解决IO密集型而非CPU密集型任务) 吞吐量的瓶颈 服务器应用程序的伸缩性受利特尔法则(Little's Law)的制约,与下面三点有关: 延迟:请求处理的耗时 并发量:同一时刻处理的请求数量 吞吐量:单位时间内处理的数据数量 如果一个服务处理延迟是50ms,处理10个并发请求,则吞吐量是200请求/秒(10/0.05),如果吞吐量要达到2000请求/秒,则处理的并发请求数量是100. 如果按照1个请求一个线程来看,要想提高吞吐量,线程数量也要增加。 Java中的线程在操作系统线程(OS Thread)进行了一层封装,而操作系统重线程是重量级资源,在硬件配置确定的前提下,不能无限制创建线程。 与虚拟地址可以映射到物理内存类似,Java将大量的虚拟线程映射到少量的操作系统线程上,虚拟线程的生命周期短暂,不会有很深的栈调用,一个虚拟线程的生命周期只运行一个任务,因此可以大量创建虚拟线程,且无需池化。 虚拟线程的应用场景 在服务器端的应用程序中,虚拟线程能够明显提高应用的吞吐量: 至少几千的并发任务量 任务是IO密集型 平台线程和虚拟线程 平台线程(platform thread):指Java中的线程,比如通过new Thread()创建的线程。 虚拟线程并不会直接分配给CPU执行,而是通过调度器分配给平台线程,平台线程再被调度器管理。Java中的虚拟线程的调度器采用了工作窃取的模式进行FIFO的操作,调度器的并行书默认是jvm获取的处理器数量(Runtime.getRuntime().availableProcessors()),调度器并非分时(time sharing)的。在使用虚拟线程编写程序时,不能控制虚拟线程合适分配给平台线程,也不能控制平台线程合适分配给CPU。 以前任务和平台线程的关系: 在使用虚拟线程之后,任务-虚拟线程-调度器-平台线程的关系,1个平台线程可以被调度器分配不同的虚拟线程: 携带器 调度器将虚拟线程挂载到平台线程之后,该平台线程叫做虚拟线程的携带器,调度器并不维护虚拟线程和携带器的关联关系,因此在一个虚拟线程的生命周期可以被分配到不同的携带器,即虚拟线程运行了一小段代码后,可能会脱离携带器,此时其他的虚拟线程会被分配到这个携带器上。 携带器和虚拟线程是相互独立的,比如: 虚拟线程不能使用携带器的标识,Thread.current()方法获取的是虚拟线程本身。 两者有各自的栈空间。 两者不能访问对方的ThreadLocal变量 在程序的执行过程中,虚拟线程遇到阻塞的操作是大部分情况下会被解除挂载,阻塞结束后,虚拟线程会被调度器重新挂载到携带器上,因此虚拟线程会频繁的挂载和解除挂载,这并不会导致操作系统线程的阻塞。 有些阻塞操作并不会导致虚拟线程接触挂载,这样会同时阻塞携带器和操作系统线程,例如:操作系统基本的文件操作,Java的Object.wait()方法,下面两种情况下不会导致虚拟线程的解除挂载: 执行synchronized同步代码(会导致携带器阻塞,所以建议使用ReentrantLock替换掉Synchronized) 执行本地方法或外部函数 虚拟线程和平台线程api的区别 从内存空间上来说,虚拟线程的栈空间可以看做是一个大块的站对象,他被存储在Java堆中,相比于单独存储对象,堆中存储虚拟线程会造成一些空间的浪费,这点在后续的Java版本中应该会得到改善,当然也有一些好处,就是可以重复利用这部分栈空间,不用多次申请开辟新的内存地址,虚拟线程的栈空间最大可以达到平台线程的栈空间容量。 虚拟线程并不是GC root,其中的引用不会出现Stop World,当虚拟线程阻塞之后比如BlockingQueue.take(),平台线程既不能获取到虚拟线程,也不能获取到queue队列,这样该平台线程可能被回收掉,虚拟线程在运行或阻塞时不会被GC 通过Thread构造方法创建的线程都是平台线程 虚拟线程是守护线程,不能通过setDaemon方法改为非守护线程 虚拟线程的优先级是默认的5,不能被修改,将来版本可能允许修改 虚拟线程不支持stop(), suspend(),resume()方法 创建虚拟线程的方式 Java中创建的虚拟线程本质都是通过Thread.Builder.OfVirtual对象进行创建的,有如下三种方式: Thread.startVirtualThread()直接创建一个虚拟线程 Thread.ofVirtual().name("virtual-thread-").unstarted(r) ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor() 示例代码 虚拟线程池 import java.time.Duration; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.IntStream; /** * @author imyzt * @date 2023/12/18 * @description VirtualThread 1 */ public class VirtualThread { public static void main(String[] args) throws InterruptedException { // 通过线程池创建虚拟线程池 try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { IntStream.range(0, 10000000).forEach(i -> { executor.submit(() -> { try { Thread.sleep(Duration.ofSeconds(1)); System.out.println("执行任务: " + i); } catch (InterruptedException e) { e.printStackTrace(); } }); }); } catch (Exception e) { e.printStackTrace(); } } } 创建虚拟线程 import java.util.concurrent.TimeUnit; /** * @author imyzt * @date 2023/12/18 * @description VirtualThread 2 */ public class CreateVirtualThread { public static void main(String[] args) throws InterruptedException { Runnable r = () -> System.out.println(Thread.currentThread().getName() + " --- 执行了"); // 创建虚拟线程, 方式1 Thread.startVirtualThread(r); Thread virtualThread = Thread.ofVirtual().name("virtual-thread-").unstarted(r); virtualThread.start(); System.out.println("是虚拟线程吗? " + virtualThread.isVirtual()); Thread platformThread = Thread.ofPlatform().priority(0).daemon(true).name("platform-thread-").unstarted(r); platformThread.start(); System.out.println("是虚拟线程吗? " + platformThread.isVirtual()); // --- 执行了 //virtual-thread- --- 执行了 //是虚拟线程吗? true //platform-thread- --- 执行了 //是虚拟线程吗? false // 主线程休眠 TimeUnit.SECONDS.sleep(1); } } 其它 除了提出虚拟线程外,还提出来新的并发编程模型结构化并发,
Read More ~