再谈 ThreadLocal

几年前我曾经写过两篇关于 ThreadLocal 的文章,分别是ThreadLocal类之简单理解和ThreadLocal类之简单应用示例,不过限于当时的水平,有些问题并没有说的很明白,所以今天再写一篇文章,重新说说这个类。 我们首先看一个例子: package cn.bridgeli.demo; /** * @author BridgeLi * @date 2021/4/21 11:02 */ public class User { String name = "Denny"; } 然后我们有一个操作: package cn.bridgeli.demo; import org.junit.Test; /** * @author BridgeLi * @date 2021/4/21 10:28 */ public class ThreadTest { private User user = new User(); @Test public void testThreadLocal() { new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(user.name); }).start(); new Thread(() -> user.name = "BridgeLi").start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } 这个时候我们就知道一定会有线程安全问题,所以我们怎么解决这个问题呢?就是 ThreadLocal,请看下面: ...

April 22, 2021 · 3 min · 478 words · Bridge Li

以 Java 为例简单说明常见 IO 模型

BIO 我们先看一个 Java 例子: package cn.bridgeli.demo; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * @author bridgeli */ public class SocketBIO { public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(9090, 20); System.out.println("step1: new ServerSocket(9090) "); while (true) { Socket client = server.accept(); System.out.println("step2:client: " + client.getPort()); new Thread(new Runnable() { @Override public void run() { InputStream inputStream = null; BufferedReader reader = null; try { inputStream = client.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream)); while (true) { String dataLine = reader.readLine(); //阻塞2 if (null != dataLine) { System.out.println(dataLine); } else { client.close(); break; } } System.out.println("客户端断开"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != reader) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } if (null!= inputStream) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); } } } BIO 是最初始的 IO 模型,该模型有两个大问题:1. accept 是阻塞的;2. read 也是阻塞的,也就是说我们的服务器起来之后,首先会在 accept 处阻塞,等待客户端连接,但有一个客户端连接的时候,我们可以从客户端处读取数据,这个时候也是阻塞的,所以我们的系统只能是单连接的,当有多个客户端连接的时候,只能一个一个的排着队连接,然后从客户端中读取数据,为了实现多连接,这就要求我们必须启用线程来解决,最开始等待客户端连接,然后有一个客户端连上了之后,启动一个线程读取客户端的数据,然后主线程继续等待客户端连接。 ...

March 30, 2021 · 3 min · 586 words · Bridge Li

Java 的引用类型和使用场景

每种编程语言都有自己操作内存中元素的方式,例如在 C 和 C++ 里是通过指针,而在 Java 中则是通过“引用”。在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱,今天这篇文章就简单介绍一下这四种类型,并简单说一下他们的使用场景。 1, 强引用(Strong Reference) 强引用类型,是我们最常讲的一个类型,我们先看一个例子: package cn.bridgeli.demo.reference; /** * @author BridgeLi * @date 2021/2/26 10:02 */ public class User { @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize"); } } package cn.bridgeli.demo.reference; import org.junit.Test; /** * @author BridgeLi * @date 2021/2/26 10:03 */ public class StrongReferenceTest { @Test public void testStrongReference() { User user = new User(); user = null; System.gc(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } 我们都知道当一个实例对象具有强引用时,垃圾回收器不会回收该对象,当内存不足时,宁愿 OOM,也就是抛出 OutOfMemeryError 异常也不会回收强引用的对象,因为 JVM 认为强引用的对象是用户正在使用的对象,它无法分辨出到底该回收哪个,强行回收有可能导致系统严重错误。但是当对象被赋值为 null 之后,会被回收,并且会执行对象的 finalize 函数,此时我们可以通过该函数拯救自己,但是有两点需要注意一个是只能拯救一次,当再次被垃圾回收的时候就不能拯救了,另一个就是有事没事千万不要重写次函数,本例只是为了说明问题重写了此函数,如果在工作中误重写了此函数,可能会导致垃圾不能回收,最终 OOM,另外有熟悉 GC 的同学没?猜一下我为什么要 sleep 一下? ...

February 28, 2021 · 3 min · 449 words · Bridge Li

用两个线程交替打印数字和字母

前一段时间听马士兵老师讲课,讲到某公司的一个面试,两个线程,其中一个线程输出ABC,另一个线程输出123,如何控制两个线程交叉输出1A2B3C,由于本人多线程掌握的一直不是很好,所以听完这道题,个人感觉收获良多,这是一个学习笔记。这道题有多种解法,不过有些属于纯炫技,所以只记录常见的三种解法。首先看第一种 park 和 unpark package cn.bridgeli.demo; import com.google.common.collect.Lists; import java.util.List; import java.util.concurrent.locks.LockSupport; /** * @author BridgeLi * @date 2021/2/6 16:14 */ public class Thread_Communication_Park_Unpark { static Thread t1 = null; static Thread t2 = null; public static void main(String[] args) { final List<Integer> integers = Lists.newArrayList(1, 2, 3, 4, 5, 6, 7); final List<String> strings = Lists.newArrayList("A", "B", "C", "D", "E", "F", "G"); t1 = new Thread(() -> integers.forEach(item -> { System.out.print(item); LockSupport.unpark(t2); LockSupport.park(); }), "t1"); t2 = new Thread(() -> strings.forEach(item -> { LockSupport.park(); System.out.print(item); LockSupport.unpark(t1); }), "t2"); t1.start(); t2.start(); } } 这个是最简单的实现方法,LockSupport.park() 使当前线程阻塞,而 LockSupport.unpark() 则表示唤醒一个线程,所以他需要一个参数,表示你要唤醒哪个线程,很好理解,也比较简单。 ...

February 7, 2021 · 2 min · 413 words · Bridge Li

利用 DeferredResult 实现 http 轮询实时返回数据接口

博客有半年没更新了,不是我偷懒,而是之前服务器到期了,开博客这么多年,钱其实花了不少,但是没有一点收益,所以上了谷歌广告,如果文章对你稍稍有一点帮助,希望能花一秒钟帮忙点一下广告,谢谢。 今天这篇文章呢,不难,其实是解答我一直以来心里的一个疑问。是这样的,之前看五八技术委员会主席沈剑老师的公众号架构师之路的一篇文章:http 如何像 tcp 一样实时的收消息,里面其中的一个方案是用 http 短连接轮询的方式实现“伪长连接”。但是对于轮询,我们的第一反应肯定是有延时,但是标题不是说的是实时吗?当然我们可以把轮询的时长缩短一些,先不说这样大部分时间的轮询调用,可能都没消息返回,造成服务器资源浪费,轮询时间再短也是有延时啊,所以难道是伪实时?反正一般消息延时个三五秒,甚至十秒八秒一分钟,大家也不会在意,只会认为对方返回慢,对不起,这是我们程序员的锅,但是 http 真的不能实现实时吗?沈剑老师提出了一种方法:首选 webim 和 webserver 之间建立一条 http 连接,专门用作消息通道,这条连接叫 http 消息连接。然后会有如下处理: 没有消息到达的时候,这个 http 消息连接将被夯住,不返回,由于 http 是短连接,这个 http 消息连接最多被夯住 90 秒,就会被断开(这是浏览器或者 webserver 的行为); 在 1 的情况下,如果 http 消息连接被断开,立马再发起一个 http 消息连接; 此时在在 1 和 2 的配合下,浏览器与 webserver 之间将永远有一条消息连接在,然后还有一种情况 每次收到消息时,这个消息连接就能及时将消息带回浏览器页面,并且在返回后,会立马再发起一个 http 消息连接 这样就能做到使用 http 端连接轮询的方式实现了实时收消息。不过需要说明的是,其实还有一种情况:消息到达时,上一个 http 消息连接正在返回,也就是第二种情况的时候突然来了一个消息,此时没有 http 消息连接可用。虽然理论上 http 消息连接的返回是瞬时的,没有消息连接可用出现的概率极小,但是根据墨菲定律我们知道,这种情况肯定会出现,所以这种情况下我们可以将消息暂存入消息池中,下一个消息连接到达后,无需等待,直接去消息池中取消息,将将消息带回,然后立刻返回生成新的消息连接即可。 以上过程,可以参考沈剑老师的公众号,链接:https://mp.weixin.qq.com/s/6BCucq6QsH8lfDGLtQCl2A 不过以上都不是今天这篇文章的重点,和今天这篇文章的标题也没有任何关系。重点是当时看了沈剑老师的这篇文章后我一直有一个疑问:第一步的时候如何夯住?总不能 sleep 吧,这多不优雅啊,由于一直以为没有遇到过类似的需求,所以这么几年来我也没深究这个问题,但是心里确实一直记着,直到前一段时间,听马士兵教育的公开课,当时再讲类似的问题的时候提到了夯住 http 的连接(具体是哪个问题,还真不记得了),虽然当时上课的老师没提怎么实现,但是评论区我问了一下,如何夯住不返回?然后有一个同学回复说,用 DeferredResult,然后下课后搜了一下资料,果然可以,如下是实现的笔记,所以这才是重点,希望对有这个疑问的同学也有一点帮助。 消息返回实体类,大家可以根据实际情况,自己定义即可: package cn.bridgeli.deferredresulttest.entity; import lombok.Data; import lombok.Getter; /** * @author bridgeli */ @Data public class DeferredResultResponse { private Integer code; private String msg; public enum Msg { TIMEOUT("超时"), FAILED("失败"), SUCCESS("成功"); @Getter private String desc; Msg(String desc) { this.desc = desc; } } } controller 接口: package cn.bridgeli.deferredresulttest.controller; import cn.bridgeli.deferredresulttest.entity.DeferredResultResponse; import cn.bridgeli.deferredresulttest.service.DeferredResultService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import javax.annotation.Resource; /** * @author bridgeli */ @RestController @RequestMapping(value = "/deferred-result") public class DeferredResultController { @Resource private DeferredResultService deferredResultService; /** * 为了方便测试,简单模拟一个 * 多个请求用同一个requestId会出问题 */ private final String requestId = "test"; @GetMapping(value = "/get") public DeferredResult<DeferredResultResponse> get(@RequestParam(value = "timeout", required = false, defaultValue = "10000") Long timeout) { DeferredResult<DeferredResultResponse> deferredResult = new DeferredResult<>(timeout); deferredResultService.process(requestId, deferredResult); return deferredResult; } /** * 设置DeferredResult对象的result属性,模拟异步操作 * * @param desired * @return */ @GetMapping(value = "/result") public String settingResult(@RequestParam(value = "desired", required = false, defaultValue = "成功") String desired) { DeferredResultResponse deferredResultResponse = new DeferredResultResponse(); if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)) { deferredResultResponse.setCode(HttpStatus.OK.value()); deferredResultResponse.setMsg(desired); } else { deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc()); } deferredResultService.settingResult(requestId, deferredResultResponse); return "Done"; } } 其中:/get 接口模拟沈剑老师说的消息连接,/result 接口模拟有一条新消息来了,然后 /get 接口会立即返回。主要注意的是 requestId,在实际项目中不能使用同一个,否则会出现问题,这个测一下就知道了,也很容易想到原因。 ...

January 9, 2021 · 2 min · 370 words · Bridge Li

规则引擎入门

关于规则引擎,我们在工作中应该会经常遇到,例如我们对不同的用户给不同的折扣。前一段时间在网上闲逛,发现一个很简单的规则引擎,一下是学习笔记。 在使用之前,我们要先导入 jar 包: <dependency> <groupId>org.jeasy</groupId> <artifactId>easy-rules-core</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>org.jeasy</groupId> <artifactId>easy-rules-mvel</artifactId> <version>3.3.0</version> </dependency> 一. 使用零配置的方式: 规则引擎入口: package cn.bridgeli.demo.rule; import org.jeasy.rules.api.Facts; import org.jeasy.rules.api.Rules; import org.jeasy.rules.api.RulesEngine; import org.jeasy.rules.core.DefaultRulesEngine; import org.jeasy.rules.core.RulesEngineParameters; import org.junit.Test; /** * @author bridgeli */ public class ThreeEightRuleTest { @Test public void testRule() { /** * 创建规则执行引擎 * 注意: skipOnFirstAppliedRule意思是,只要匹配到第一条规则就跳过后面规则匹配 */ RulesEngineParameters parameters = new RulesEngineParameters().skipOnFirstAppliedRule(true); RulesEngine rulesEngine = new DefaultRulesEngine(parameters); //创建规则 Rules rules = new Rules(); rules.register(new EightRule()); rules.register(new ThreeRule()); rules.register(new ThreeEightRuleUnitGroup(new EightRule(), new ThreeRule())); rules.register(new OtherRule()); Facts facts = new Facts(); for (int i = 1; i <= 50; i++) { //规则因素,对应的name,要和规则里面的@Fact 一致 facts.put("number", i); //执行规则 rulesEngine.fire(rules, facts); System.out.println(); } } } 这个是判断 1- 50 里面,哪些是 3 的倍数、哪些是 8 的倍数、哪些是 3 和 8 的倍数。 ...

July 12, 2020 · 3 min · 583 words · Bridge Li

关于 CPU 乱序执行的证明

在学习 volatile 关键字的时候,我们都知道他有两个作用:1. 内存可见性;2. 禁止指令重排序。但是我们一般都是说,那么怎么证明呢?请看下面这段代码: package cn.bridgeli.demo; /** * @author BridgeLi * @date 2020/7/4 10:27 */ public class Disorder { private static int x = 0; private static int y = 0; private static volatile int a = 0; private static volatile int b = 0; public static void main(String[] args) throws InterruptedException { int i = 0; for (; ; ) { i++; x = 0; y = 0; a = 0; b = 0; Thread one = new Thread(new Runnable() { @Override public void run() { a = 1; x = b; } }, "one"); Thread two = new Thread(new Runnable() { @Override public void run() { b = 1; y = a; } }, "two"); one.start(); two.start(); one.join(); two.join(); if (0 == x && 0 == y) { System.out.println("第 " + i + " 次(" + x + ", " + y + ")"); break; } } } } 如果仔细分析这段代码,我们就会发现,如果 CPU 没有乱序执行,那么无论任何时候 x 和 y 都不可能同时为零,但是事实上,这段代码是有可能出现 x 和 y 同时为零的,具体大家可以自己测试,需要说明的时候,什么时候指令重排了,要看运气,可能很快出现,也可能要等一会。 ...

July 5, 2020 · 2 min · 257 words · Bridge Li

Redis 实现布隆过滤器

昨天听马士兵教育张福刚讲公开课,里面讲解了布隆过滤器,今天无聊没事干,整理了一下笔记。关于布隆过滤器是什么东西,有什么应用场景就不做讨论了,网上有很多,大家可以自行了解,只记录实现: pom 依赖 <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> 具体实现 package cn.bridgeli.demo; import com.google.common.hash.Funnels; import com.google.common.hash.Hashing; import org.junit.Before; import org.junit.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Pipeline; import java.nio.charset.StandardCharsets; /** * @author BridgeLi * @date 2020/6/6 16:38 */ public class BloomFilter { private Jedis jedis = null; /** * 预估的数据量 */ private static long n = 10000; /** * 容忍的错误率 */ private static double fpp = 0.01; private static long numBits = optimalNumOfBits(n, fpp); private static int numHashFunctions = optimalNumOfHashFunctions(n, numBits); /** * 根据预估数据量 n 和允许的错误率 fpp 计算需要的 bit 数组的长度 * * @param n * @param fpp * @return */ private static long optimalNumOfBits(long n, double fpp) { if (0 == fpp) { fpp = Double.MIN_VALUE; } return (long) (-n \* Math.log(fpp) / (Math.log(2) \* Math.log(2))); } /** * 根据预估的数据量和计算出来的需要的 bit 数组的长度,计算所需要的 hash 函数的个数 * * @param n * @param numBits * @return */ private static int optimalNumOfHashFunctions(long n, long numBits) { return Math.max(1, (int) Math.round((double) numBits / n * Math.log(2))); } /** * 预热数据 */ @Before public void testBloomFilterBefore() { BloomFilter bloomFilter = new BloomFilter(); bloomFilter.init(); for (int i = 0; i < n; i++) { bloomFilter.put("bf", String.valueOf(i + 100)); } } /** * 过滤数据 */ @Test public void testBloomFilter() { BloomFilter bloomFilter = new BloomFilter(); bloomFilter.init(); int ex_count = 0; int ne_count = 0; for (int i = 0; i < 2 * n; i++) { boolean exist = bloomFilter.isExist("bf", String.valueOf(i + 100)); if (exist) { ex_count++; } else { ne_count++; } } System.out.println("ex_count: " + ex_count + ", ne_count: " + ne_count); } private void init() { JedisPool jedisPool = new JedisPool("127.0.0.1", 6379); jedis = jedisPool.getResource(); } public boolean isExist(String where, String key) { long[] indexs = getIndexs(key); boolean result = false; try (Pipeline pipeline = jedis.pipelined()) { for (long index : indexs) { pipeline.getbit(where, index); } // 只要有一个位置为 false,即代表该数据不存在 result = !pipeline.syncAndReturnAll().contains(false); } catch (Exception e) { } return result; } public void put(String where, String key) { long[] indexs = getIndexs(key); try (Pipeline pipeline = jedis.pipelined()) { for (long index : indexs) { pipeline.setbit(where, index, true); } pipeline.sync(); } catch (Exception e) { } } private long[] getIndexs(String key) { long hash1 = Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(StandardCharsets.UTF_8)).asLong(); long hash2 = hash1 >>> 16; long[] result = new long[numHashFunctions]; for (int i = 0; i < numHashFunctions; i++) { long combinedHash = hash1 + i * hash2; if (combinedHash < 0) { combinedHash = ~combinedHash; } result[i] = combinedHash % numBits; } return result; } }

June 6, 2020 · 2 min · 411 words · Bridge Li

关于 JPA 连表查询和 redis 序列化遇到的小问题

一、JPA 连表查询时数据长度正常,内容都是重复的,MySQL 数据库运行查询语句结果正常 先看写法: package cn.bridgeli.demo.repository; import cn.bridgeli.demo.entity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import java.util.List; /** * @author BridgeLi */ public interface E1Repository extends JpaRepository<E1, Integer> { @Query(value = "SELECT t1.id, t1.name, t2.score FROM t1 LEFT JOIN t2 ON t1.id = t2.t1_id LIMIT ?1, ?2", nativeQuery = true) List<E1> queryE1s(Integer pageNum, Integer pageSize); } package cn.bridgeli.demo.entity; import lombok.Data; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Transient; /** * @author BridgeLi */ @Data @Entity public class E1 { @Id private Integer id; private String name; @Transient private String course; private Integer score; } 整体大概就是有两张表 t1 和 t2,一对多的关系,t1 的主键是 t2 的外键,执行的截图我就不做了,问题呢,大概就是上面描述的那样,有一个连表查询的需求,JPA 做的,返回给前端的数据,返回长度是对的,但是内容都是重复的,当时第一次看到这个问题的时候,怀疑是 SQL 的问题,然后就把 ...

April 11, 2020 · 1 min · 204 words · Bridge Li

Dubbo 自定义拦截器

写了 Spring AOP 实现自定义注解,打印日志之后,感觉在调用第三方 dubbo 接口的时候,依然会有同样的问题,然后看了一下 dubbo 的官方文档,决定下一个 filter,实现 dubbo 接口的日志拦截,以下是自己完的一个小例子,同样也是供需要的同学参考。 filter 具体实现如下: package cn.bridgeli.demo.filter; import com.alibaba.dubbo.rpc.Filter; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Result; import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.service.GenericService; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author bridgeli */ public class DubboServiceFilter implements Filter { private static final Logger LOGGER = LoggerFactory.getLogger(DubboServiceFilter.class); @Override public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { // 打印入参日志 String className = invocation.getInvoker().getInterface().getName(); String methodName = invocation.getMethodName(); String arguments = StringUtils.join(invocation.getArguments(), ";"); LOGGER.info("调用 dubbo 服务接口: " + className + "#" + methodName + ",参数:" + arguments); //开始时间 long startTime = System.currentTimeMillis(); //执行接口调用逻辑 Result result = invoker.invoke(invocation); //调用耗时 long elapsed = System.currentTimeMillis() &#8211; startTime; //如果发生异常 则打印异常日志 if (result.hasException() && invoker.getInterface() != GenericService.class) { LOGGER.error("dubbo执行异常,接口:" + className + "#" + methodName + ",参数:" + arguments, result.getException()); } else { //打印响应日志 LOGGER.info("dubbo服务响应成功:" + className + "#" + methodName + ",参数:" + arguments + ",返回值:" + result.getValue() + ",用时:" + elapsed); } //返回结果响应结果 return result; } } 在/src/main/resources/META-INF/dubbo目录下新增纯文本文件 com.alibaba.dubbo.rpc.Filter 内容为: dubboServiceFilter=cn.bridgeli.demo.filter.DubboServiceFilter 最后在服务提供者配置文件中添加配置使拦截器生效: <dubbo:provider filter="dubboServiceFilter"/> 或者 <dubbo:service filter="dubboServiceFilter"/> 这样即可实现。不过需要说明的是,因为我们项目用的 dubbo 版本是:2.5.3,所以包名和配置名还都是:com.alibaba.dubbo,而最新的版本阿里已经捐献给 apache,所以都变成了:org.apache.dubbo。最后的最后想说的是,具体大家可以参考 dubbo 的官方文档,个人认为 dubbo 的官方文档写的是极好的,各种通俗易懂。 ...

March 22, 2020 · 1 min · 176 words · Bridge Li