规则引擎入门

关于规则引擎,我们在工作中应该会经常遇到,例如我们对不同的用户给不同的折扣。前一段时间在网上闲逛,发现一个很简单的规则引擎,一下是学习笔记。 在使用之前,我们要先导入 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

Mac:终端和 shell 配置

今天不写博客了,水一篇玩玩。老祖宗说,工欲善其事,必先利其器。很多做开发的同学都喜欢 Mac,我也是,自从用了之后爱不释手,但是当帮助一些同学解决问题的时候,总是发现,有些同学的终端使用的是 Mac 自带的终端和 shell,特别难用,完全无法发挥 Mac 的威力,然后给他们推荐怎么配置一下更好用,但是发现很多同学都是,现在已经懒得一个一个同学的说了,所以今天我就写一篇文章,怎么配置更好用的终端和 shell,希望下次再遇到直接能甩给他这篇文章就行。 一. 终端,iterm2 很多同学首先使用的终端是原生终端,那个终端说实话太难用了,我都想不出来理由,这么好用的电脑,苹果是如何忍受这么难用的终端的,这里给大家推荐一个好用的终端:iterm2。官网地址:https://iterm2.com/,GitHub 地址:https://github.com/gnachman/iTerm2,怎么安装这个就不用说了,傻瓜式的。 需要说明的是,安装完成之后,iterm2 默认窗口的大小,个人感觉是有点小的,所以做了一点点修改,希望默认窗口能大一些,修改步骤如下:打开工具 iTerm –> 点击mac左上角的 iTerm2 –> Preferences –> 选择Profiles –> Window –> Settings for New Windows,修改:Columns 和 Rows,个人设置的是 140 和 36,感觉还行,然后关闭,重新打开iTerm。就可以看到你更改后的效果。 二. shell,Oh My Zsh shell 是什么,我也不想解释了,大家可以自己搜索,另外如果想查看自己电脑有几种 shell,可以使用如下命令: cat /etc/shells 在 Linux 系统里执行这个命令和 Mac 略有不同,你会发现 Mac 多了一个 zsh,也就是说,mac 为用户预装了个 zsh。不过由于早期配置过于复杂,无人问津,很多人跑来看看 zsh 的配置指南,二话不说扭头就走了。直到有一天,国外有个穷极无聊的程序员开发出了一个能够让你快速上手的 zsh 项目,叫做:oh my zsh,官网地址:https://ohmyz.sh/,Github 地址是:https://github.com/ohmyzsh/ohmyzsh 使它的配置一下子简单起来了,下面就简单说说这个 Oh My Zsh。 安装,就一步: 由于目前系统的默认 Shell 都是 bash(可以通过:echo $SHELL 查看),所以需要使用如下命令修改当前用户使用 zsh: ...

May 2, 2020 · 2 min · 270 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

Spring AOP 实现自定义注解

自工作后,除了一些小项目配置事务使用过 AOP,真正自己写 AOP 机会很少,另一方面在工作后还没有写过自定义注解,一直很好奇注解是怎么实现他想要的功能的,刚好做项目的时候,经常有人日志打得不够全,经常出现问题了,查日志的才发现忘记打了,所以趁此机会,搜了一些资料,用 AOP + 自定义注解,实现请求拦截,自定义打日志,玩一下这两个东西,以下是自己完的一个小例子,也供需要的同学参考。 注解如下: package cn.bridgeli.demo.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author bridgeli */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog { /** * 方法描述 * * @return */ String desc() default ""; } 切面 package cn.bridgeli.demo.annotation; import cn.bridgeli.utils.AuthorizeUtil; import cn.bridgeli.entity.Principal; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; /** * @author bridgeli * 1. 这是一个切面类 */ @Aspect @Component @Slf4j public class MyLogAspect { /** * 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名 * 切面最主要的就是切点,所有的故事都围绕切点发生 * logPointCut()代表切点名称 */ @Pointcut("@annotation(cn.bridgeli.demo.annotation.MyLog)") public void logPointCut() { } /** * 3. 环绕通知 * * @param joinPoint * @param myLog * @return */ @Around(value = "logPointCut() && @annotation(myLog)", argNames = "joinPoint,myLog") public Object logAround(ProceedingJoinPoint joinPoint, MyLog myLog) { // 获取方法名 String methodFullPathName = joinPoint.getTarget().getClass().getName() + "#" + joinPoint.getSignature().getName(); // 获取参数 String params = StringUtils.join(joinPoint.getArgs(), ";"); Principal currentUser = AuthorizeUtil.getCurrentUser(); log.info("当前登陆用户:" + (null == currentUser ? "" : currentUser.toString()) + ",进入 [ " + methodFullPathName + " ] 方法, 方法的描述:" + myLog.desc() + ",参数为:" + params); // 继续执行方法 long startTime = System.currentTimeMillis(); Object result = null; try { result = joinPoint.proceed(); } catch (Throwable e) { log.error("切面执行报错,参数:" + params, e); } long elapsed = System.currentTimeMillis() &#8211; startTime; log.info("[ " + methodFullPathName + " ] 方法执行结束,返回值为:" + (null == result ? "" : result.toString()) + ",用时:" + elapsed); return result; } } 然后只需要在想使用的地方 @MyLog 就可以了,当然也可以加上 @MyLog(desc = “这是方法描述”),这样打出来的日志还会有方法是做什么的,别人看日志的时候能够一目了然。 ...

March 15, 2020 · 2 min · 248 words · Bridge Li

Java 使用 FFmpeg 处理视频文件示例

Java 使用 FFmpeg 处理视频文件示例 目前在公司做一个小东西,里面用到了 FFmpeg 简单处理音视频,感觉功能特别强大,在做之前我写了一个小例子,现在记录一下。 首先说明,我是在 https://ffmpeg.zeranoe.com/builds/ 这个地方下载的软件,Windows 和 Mac 解压之后即可使用。具体代码如下: package cn.bridgeli.demo; import org.junit.Test; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; /** * @author BridgeLi * @date 2020/2/29 15:40 */ public class FfmpegTest { private static final String OS = System.getProperty("os.name").toLowerCase(); private static final String FFMPEG_PATH = "/Users/bridgeli/ffmpeg-20200216-8578433-macos64-static/bin/ffmpeg"; @Test public void testFfmpeg() { String inputWavFile = "/Users/bridgeli/inputWavFile.wav"; String inputMp3File = "/Users/bridgeli/inputMp3File.mp3"; String inputMp4File = "/Users/bridgeli/inputMp4File.mp4"; String outMergeMp3File = "/Users/bridgeli/outMergeMp3File.mp3"; String outMergeMp3AndMp4File = "/Users/bridgeli/outMergeMp3AndMp4File.mp4"; String outConcatMp3File = "/Users/bridgeli/outConcatMp3File.mp3"; // 拼接 String command = null; if (OS.contains("mac") || OS.contains("linux")) { command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex \[0:0\]\[1:0\]concat=n=2:v=0:a=1[a] -map [a] " + outConcatMp3File; } else if (OS.contains("windows")) { command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex \"\[0:0\]\[1:0\]concat=n=2:v=0:a=1[a]\" -map \"[a]\" " + outConcatMp3File; } // 合并(视频和音频) // String command = FFMPEG_PATH + " -i " + inputMp4File + " -i " + outConcatMp3File + " -c:v copy -c:a aac -strict experimental " + outMergeMp3AndMp4File; // 合并 // String command = FFMPEG_PATH + " -i " + inputMp3File + " -i " + inputWavFile + " -filter_complex amerge -ac 2 -c:a libmp3lame -q:a 4 " + outMergeMp3File; System.out.println(command); Process process = null; try { process = Runtime.getRuntime().exec(command); } catch (IOException e) { e.printStackTrace(); } if (null == process) { return; } try { process.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } try (InputStream errorStream = process.getErrorStream(); InputStreamReader inputStreamReader = new InputStreamReader(errorStream); BufferedReader br = new BufferedReader(inputStreamReader)) { String line = null; StringBuffer context = new StringBuffer(); while ((line = br.readLine()) != null) { context.append(line); } System.out.println("error message: " + context); } catch (IOException e) { e.printStackTrace(); } process.destroy(); } } 在我的认知中,完成任务是第一位的,所以按照这个简单处理一下音视频是没有问题的,具体更强大的语法,大家可以自己查询相关文档,也可以参考 https://blog.csdn.net/shshjj/article/details/98185454 这篇文中,其中我个人也在学习中。下面说两个在使用的过程中遇到的问题。 ...

February 29, 2020 · 2 min · 307 words · Bridge Li

日期中用 YYYY 一定会报错吗?

今年春节真是打破了 N 多传统,很多人都是在家连门都没出过,从今天开始也要开始在家远程办公了,因为和小伙伴合作开发一个功能,但是接口目前还没给到,然后记得今年元旦前后,关于 YYYY 报错的问题,突然火了,据说有 N 多程序员被火速召回公司改 bug,所以决定写篇小文章说说这个问题:YYYY 一定会报错吗? 首先需要说明的是:我用的 JDK 版本为:jdk-8u131-macosx-x64,所以具体表现为应该显示:2019-12-31,结果确是:2020-12-31,另外经过我的测试其实不仅 format 的时候报错,parse 的时候同样也会报错,我写了一段示例代码如下: package cn.bridgeli.demo; import org.joda.time.DateTime; import org.junit.Test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * Created by bridgeli on 2019/02/03. */ public class DateTest { @Test public void testSimpleDateFormat() throws ParseException { SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); DateTime dateTime1 = new DateTime(2019, 12, 31, 23, 59, 59); String date1 = simpleDateFormat1.format(dateTime1.toDate()); System.out.println("date1: " + date1); SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd"); DateTime dateTime2 = new DateTime(2020, 01, 01, 23, 59, 59); String date2 = simpleDateFormat2.format(dateTime2.toDate()); System.out.println("date2: " + date2); SimpleDateFormat simpleDateFormat3 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); DateTime dateTime3 = new DateTime(2019, 12, 31, 23, 59, 59); String date3 = simpleDateFormat3.format(dateTime3.toDate()); System.out.println("date3: " + date3); SimpleDateFormat simpleDateFormat4 = new SimpleDateFormat("YYYY-MM-dd"); DateTime dateTime4 = new DateTime(2020, 01, 01, 23, 59, 59); String date4 = simpleDateFormat4.format(dateTime4.toDate()); System.out.println("date4: " + date4); SimpleDateFormat simpleDateFormat5 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date5 = simpleDateFormat5.parse("2019-12-31 23:59:59"); System.out.println("date5: " + date5); SimpleDateFormat simpleDateFormat6 = new SimpleDateFormat("yyyy-MM-dd"); Date date6 = simpleDateFormat6.parse("2020-01-01"); System.out.println("date6: " + date6); SimpleDateFormat simpleDateFormat7 = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss"); Date date7 = simpleDateFormat7.parse("2019-12-31 23:59:59"); System.out.println("date7: " + date7); SimpleDateFormat simpleDateFormat8 = new SimpleDateFormat("YYYY-MM-dd"); Date date8 = simpleDateFormat8.parse("2020-01-01"); System.out.println("date8: " + date8); } } 上面这段代码的输出结果为: ...

February 3, 2020 · 3 min · 541 words · Bridge Li

纠错:Java 内存模型(JMM)

当一个 Java 程序员工作一段时间之后,不可避免的要去了解 JVM,而了解 JVM 的时候,自然就会看到 Java 的内存模型,但是个人看过有太多的人概念不清不楚,有太多的人把 Java 内存结构,记得曾经看过一篇文章,一个同学去面试,面试官问他:简单聊聊 Java 的内存模型,他说完之后,面试官说:你说的不对啊,内存模型应该是堆、栈、常量池、方法区、程序计数器等等,然后他瞬间就不想去这家公司了,这不是让一个半吊子去当面试官吗,自己概念都没搞清楚就去说别人,个人也看过太多文章把这个搞错,所以今天就写了一篇小文章,说说自己对 JMM 的理解,当然我说的可能也不对,也不可能说透。 主内存和工作内存 Java 内存模型规定了所有的变量都存储在主内存,每个线程还有自己的工作内存,线程的工作内存中存储了被该线程使用到的变量的主内存存储拷贝,程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递均需要通过主内存来完成,主内存和工作内存之间的交互有 8 个人原子性的操作来实现,具体详细的可以再查资料。 重排序 重排序是编译器和处理器为了优化程序性能,而对指令顺序进行重新排序的一种手段。但 as-if-serial 语义的意思是:不管怎么重排序,单线程程序的执行结果都不能被改变,所以编译器和处理器都不会对存在数据依赖关系的操作做重排序。 happens-before 如果操作 1 happens-before 操作 2,那么第操作1的执行结果将对操作 2 可见,而且操作1的执行顺序排在第操作 2 之前。两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。另外 happens-before 具有传递性,由于这些规则的存在,Java 内存模型会保证重排序后的执行,在线程内看起来和串行的效果是一样的,这就是程序的顺序一致性。 对于 volatile 型变量的特殊规则 当一个变量被定义为 volatile 之后,他将具备两种特性,1. 保证此变量对所有线程的可见行,这里的可见行是指当一个线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的,而普通变量做不到这一点。但是这个要多说一点,有些同学看到这可能认为:volatile 变量在各个线程中是一致的,所以基于 volatile 变量的运算在并发下是安全的,其实这句话的论据对,但结论是错的,因为 Java 中的运算并非原子的,volatile 的应用场景在于读多写少的地方:例如修饰一个 boolean 变量作为一个开关。2. 禁止指令重拍优化,这个在写单例的时候,我想大家都已经知道了,不再赘述。 final 被 final 修饰的字段,一旦完成了初始化,其他线程就能看到它,并且它也不会再变了。即只要不可变对象被正确的构建出来(没有发生 this 引用溢出),它就是线程安全的。

January 1, 2020 · 1 min · 64 words · Bridge Li