疯狂小兵

疯狂小兵 查看完整档案

北京编辑山东科技大学  |  软件工程 编辑美团  |  后台开发 编辑 followtry.cn 编辑
编辑

专注做后端,用java和go做工具,编写世界

个人动态

疯狂小兵 回答了问题 · 2019-11-12

SourceTree提交调用git模板

额 16 年的问题,也回答下吧。

不确定在 window 下的 sourceTree 是否和 MacOS 下相同。
在 MacOS 下的设置如图。

菜单 -> 仓库 -> 仓库设置 -> 弹窗(提交模板) -> 自定义 -> 输入自定义的内容格式。

clipboard.png

关注 2 回答 1

疯狂小兵 发布了文章 · 2019-10-14

深入理解 Volatile 的实现原理

Volatile 的官方定义

Java 语言规范第三版中对 volatile 的定义如下: java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java 语言提供了 volatile,在某些情况下比锁更加方便。如果一个字段被声明成 volatile,java 线程内存模型确保所有线程看到这个变量的值是一致的。

什么情况下可使用 volatile

  1. 在多线程并发编程时,为了保持共享变量在多个线程的一致性。 =》 可见性
  2. 为了保证代码执行按编码的顺序执行。 =》 有序性

并发编程中的三个特性:原子性,有序性和可见性。volatile就作用了其中的两个。

为什么使用 volatile

恰当的使用,它的使用和执行成本比synchronized更低,因为不会引起线程上下文的切换和调度。

volatile的实现原理是什么

volatile 是依赖于硬件层面的支持,即需要 CPU 的指定来实现。

对于volatile修饰的变量,在汇编语言层面会多一行指令 0x01a3de24: lock addl $0x0,(%esp);。而该lock指令通过查IA-32架构可知主要做两件事 :

  1. 将当前处理器缓存行的数据写回到系统内存中。
  2. 该写回内存操作会引起其他CPU里缓存了该内存地址的数据失效。 =》 CPU 的嗅探机制。

详细原理如下:

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2 或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了 Volatile 变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。

但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里

LOCK 前缀指令的改进

Lock 前缀指令会引起处理器缓存回写到内存。 Lock 前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号,在该信号期间,会独占使用任何共享内存

  1. 第一阶段:

    1. LOCK指令会锁住总线,导致其他的处理器不能访问总线,也就不能访问系统内存。将多线程的并发变成了串行执行。
  2. 优化后

    1. LOCK指令不再锁总线,而是锁缓存行。并将数据会写到该缓存,使用缓存一致性来保证原子性

缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

新的 CPU 会使用 MESI(修改,独占,共享,无效)控制协议来维护内部缓存和其他处理器的缓存的一致性。

可以看出硬件技术的进步对于软件的性能提升有质的飞越。

volatile在软件层面的优化

并发编程大师 Doug lea在 JDK1.7 中新增了队列集合类 LinkedTransferQueue,在使用Volatile时用追价字节的方式优化队列出栈和入栈的性能。

为什么追加 64 字节能够提高并发编程的效率呢?

因为对于英特尔酷睿 i7,酷睿, Atom 和 NetBurst, Core Solo 和 Pentium M 处理器的 L1,L2 或 L3 缓存的高速缓存行是 64 个字节宽,不支持部分填充缓存行,这意味着如果队列的头节点和尾节点都不足 64 字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器试图修改头接点时会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作是需要不停修改头接点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea 使用追加到 64 字节的方式来填满高速缓冲区的缓存行,避免头接点和尾节点加载到同一个缓存行,使得头尾节点在修改时不会互相锁定。

上一段话核心意思是: 队列A的尾节点和队列B的头节点 在同一缓存行,队列 B 修改头节点时会锁住整个缓存行,导致队列A 不能访问自己的尾节点。因此需要补全 64 字节,让尾节点独占一个缓存行。

该方式是对空间和性能的一个折中和取巧方案。如果并发较大,修改比较频繁,可以使用该方式。主要是为了避免相互锁定

那什么情况下不适合呢?

  1. 缓存行非 64 字节宽的处理器。比较老一些的处理器,如 P6 和奔腾处理器的缓存行是 32 字节宽。
  2. 共享变量不会被频繁的写,因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,共享变量如果不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。

感想

越底层的知识越基础越重要。CPU 和内存,磁盘的交互机制不了解,就不能很好的在软件层面利用硬件能力进行性能提升。

附上执行指令和 volatile 执行原理图

CPU 的执行指令

volatile 执行原理图

参考文献

https://www.infoq.cn/article/...
查看原文

赞 1 收藏 1 评论 0

疯狂小兵 发布了文章 · 2019-06-25

lambda架构的问题

原文:https://www.oreilly.com/ideas...

Storm的作者Nathan Marz提出了 lambda 架构,该架构是在 MapReduce 上和 Storm 上构建流式处理的应用。lambda 架构是捕获不可变的数据序列并将其并行的发送给批处理系统和流式处理系统。但是你需要分别在批处理系统和流式处理系统中实现一次数据处理逻辑。而在查询的时候需要将两个系统计算的结果合并在一起,以完成查询返回给请求端。

对于两种处理系统,你可以灵活替换实现系统,比如使用 kafka+storm 实现流出处理,使用 hadoop 实现批式处理,输出结果通常在分开的两个数据库中,一个是为了流式而优化,另一个是为了批式更新而优化。

但是对于Jay Kreps【原文作者】因为一直在从事实时数据管道的建设,虽然其中有一些风格就是 lambda 架构,但是他更喜欢一个新的替换的方案。

Lambda 架构图

Lambda 架构

为什么会提出lambda 架构呢

因为那些试图构建流式处理系统的人并没有过多的考虑数据重计算的问题,最终造成系统没有便利的方法来处理数据重计算。

lambda 架构强调保持输入的原始数据不可变并且显示的将数据重新计算的问题给展现了出来。通过 lambda 架构可以比较好的解决了流式数据和历史数据的计算问题。

为什么数据会可能重新计算呢?

因为随着时间的推移,代码可能会改变。改变的原因可能是你想在输出结果中新加一个字段,或者是因为代码有 bug 需要修复。不管是什么原因,要使得历史数据得到新的预期结果,就需要将数据重新计算。

Jay Kreps对 lambda 架构的反驳

因为 lambda 架构提出其中一个观点认为流式系统是近似的,不准确的,准确度不如批式处理。
Jay Kreps对此观点不敢苟同,他认为现存的流式处理的框架不如 MapReduce成熟,但不代表流式系统不能如批式系统那样提供强大的语义保证。而且 lambda 架构提出的标题是"beats the CAP theorem",即干掉 CAP 理论,但是实际上尽管在流处理上对延时和可用性间存在权衡,但因为这是一种异步处理架构。所以异步计算的结果也不能立即保持与输入的数据一致,因此 CAP 理论仍然没有被打破。

lambda 架构存在的问题是什么?

lambda 架构需要维护在两个复杂的分布式系统中输出相同结果的代码,就像看起来那么痛苦,而且Jay Kreps也不认为该问题是可以解决的。

因为 storm 和 Hadoop 分布式框架非常的复杂,因此不可避免的代码会针对其运行的框架进行设计。

为什么 Lambda 会这样令人兴奋呢?

Jay Kreps建议如果对延时性不敏感就仅使用如 MapReduce 这样的批处理系统。如果延迟敏感则使用流式处理框架,除非特别必须才同时使用这两种系统。

但是需求总是千奇百怪的,人们需要构建复杂的,低延时的处理系统,(而且在天朝 PM 都想要大而全的功能下,这样需求更盛)。

他们拥有的两件事情并不能解决他们的问题:一个可以处理历史数据的可扩展高延迟批处理系统和一个无法重新处理结果的低延迟流处理系统。但通过将两个东西连接在一起,实际上构成了一个可行的方案,也就是 lambda 架构。但尽管lambda 架构让人很痛苦,但确实也解决了重新计算这样通常让人忽略的问题。
但是Jay Kreps只认为 lambda 架构只是临时解决方案,它不是新的编程范例也不是大数据的未来方向。

Jay Kreps的经验

因为在 linkedin 内部已经进行过多次的讨论和尝试。发现保持两个不同系统中编写的代码完全同步非常非常困难。用于隐藏底层框架的API 被证明是最 Low 的抽象,因为这样设计会需要深入的 Hadoop 知识和对实时层的深入了解,而且当你在调试或者因为性能问题排查原因时,还加上需要深入了解抽象层是怎么转换到底层的处理框架的。 也许简单的才是最有效的。

Jay Kreps的灵魂拷问

  1. 为什么不能改进流式处理系统来处理它的目标域内的完整问题呢?
  2. 为什么需要粘在另一个系统上?
  3. 为什么不能同时进行实时处理并在代码更改时处理重新处理?
  4. 为什么不通过增加并行性和重播历史非常非常快地处理重新计算的问题?

Jay Kreps 的观点是什么?

Jay Kreps在思考为什么不能改进流式处理系统来处理它的目标域中完整的问题集呢。
因此出现了两种思路

  1. 使用一种语言或者框架来抽象实时和批处理框架,可以用更高层的框架 api 写代码,然后该框架会编译后选择使用实时处理或者批处理。

    这种方式肯定使得事情变得更好些,但是不能解决问题。
    即使这样可以避免编写两次代码,但是运行和调试两个系统的负担也会非常高,而且新的抽象只能提供两个系统特性的并集(但现在 Beam 不是在做这样的事情吗)。而且这样做与跨数据库 透明的 ORM一样的臭名昭著。
    在原有系统之上提供相似的接口和界面化语言,在几乎不稳定的分布式系统上构建统一的抽象层比构建完全不同的编程范式要难得多。

  2. 增强流式系统的能力,以解决其目标域内的完整问题。流处理的基本抽象是数据流DAG,它与传统数据仓库中的底层抽象完全相同,也是MapReduce后继Tez的基本抽象。流处理只是这种数据流模型的一种推广,它将中间结果的检查点和持续输出暴露给最终用户。

Jay Kreps提出的在流式处理系统中进行重新计算的逻辑

  1. 使用 kafka 或其他系统保留全部想要重新计算的 log 数据,比如保留 30 天内的数据。
  2. 当想要重新计算时,开启一个新的流式处理 job 的实例从头开始计算,并将计算结果输出到一个新的 table 中。
  3. 当第二个实例处理的数据追赶上前一个时,将应用切换到读新表。
  4. 停用老版本的 JOB,并删除老的表数据

该方式只在代码更改时需要重新计算。当然重新计算只是统一代码的改进版本,运行在相同的框架上,消费同样的输入数据。当然也可以提高 job 的并行度,以便快速完成。

而该架构被称为 Kappa架构

Lambda 架构

而且新旧两张表也可以同时存在,这样就可以通过将应用切换到旧表来恢复旧的逻辑。在特别重要的情况下,也可以使用AB 测试或 bandit 算法来确保无论是修复bug还是代码改进都不会意外降级。

同样的,数据还是可以存储在 HDFS 上的,但是数据的重新计算不会再在 HDFS 做了。

而对于 Kappa 系统,Linkedin 内部使用的Samza就正在使用。

比较

两种方法之间的效率和资源权衡在某种程度上有所不同

  1. Lambda架构需要始终运行重新处理和实时处理
  2. Kappa 架构只需要在需要重新处理时运行第二份作业,但是,Kappa 架构要求在输出数据库中暂时拥有2倍的存储空间,并且需要一个支持高容量写入的数据库来重新加载。

在这两种情况下,再加工的额外负荷可能会平均。如果你有很多这样的工作,他们不会一次全部重新处理,所以在一个有几十个这样的工作的共享集群上,你可能会为在任何给定时间激活重新处理的少数Job提供额外的几个百分点的容量预算。

lambda 架构kappa 架构
数据处理能力可处理超大规模的历史数据历史数据处理能力有限
机器开销批处理和实时计算需一直运行,机器开销大必要时进行全量计算,机器开销相对较小
存储开销只需要保存一份查询结果,存储开销较小需要存储新老实例结果,存储开销相对较大。但如果是多 Job 共用的集群,则只需要预留出一小部分的存储即可
开发、测试难易程度实现两套代码,开发、测试难度较大只需面对一个框架,开发、测试难度相对较小
运维成本维护两套系统,运维成本大只需维护一个框架,运维成本小

对比表格参考自:http://bigdata.51cto.com/art/...

Kappa 的优势

Kappa 的真正优势不是关于效率,而是关于允许人们在单个处理框架之上开发,测试,调试和操作他们的系统。因此,在简单性很重要的情况下,请将Kappa 架构视为Lambda架构的替代方案。

查看原文

赞 1 收藏 0 评论 0

疯狂小兵 收藏了文章 · 2019-05-24

IntelliJ IDEA 2017.1 JDK 8 性能调优

IntelliJ IDEA 问题描述

IntelliJ IDEA 在 多窗口、多项目协作开发时,MacBook Pro的散热风扇凶猛地转动,相关配置如下:

MacBook Pro 配置

MacBook Pro (Retina, 15-inch, Mid 2015)
型号名称:    MacBook Pro
型号标识符:    MacBookPro11,4
处理器名称:    Intel Core i7
处理器速度:    2.2 GHz
处理器数目:    1
核总数:    4
L2 缓存(每个核):    256 KB
L3 缓存:    6 MB
内存:    16 GB
Boot ROM 版本:    MBP114.0172.B16
SMC 版本(系统):    2.29f24

IntelliJ IDEA 版本

IntelliJ IDEA 2017.1.5
Build #IC-171.4694.70, built on July 4, 2017
JRE: 1.8.0_131-b11 x86_64
JVM: Java HotSpot(TM) 64-Bit Server VM by Oracle Corporation
Mac OS X 10.12.5

问题原因

默认的IDEA JVM参数配置较低,其中配置存放在 /Applications/IntelliJ IDEA CE.app/Contents/bin/idea.vmoptions 文件中,该文件为IDEA 全局配置文件:

-Xms128m
-Xmx750m
-XX:ReservedCodeCacheSize=240m

解决方法

修改Info.plist文件

定位Info.plist文件

该文件存放在/Applications/IntelliJ IDEA CE.app/Contents 目录下:

total 32
16 -rw-r--r--    1 Mercy  admin  4210  7 11 18:43 Info.plist
 0 drwxr-xr-x@   3 Mercy  admin   102  7 11 16:21 MacOS
 0 drwxr-xr-x@   7 Mercy  admin   238  7  5 14:06 Resources
 0 drwxr-xr-x@   3 Mercy  admin   102  7  5 14:06 _CodeSignature
 0 drwxr-xr-x@  13 Mercy  admin   442  7 11 18:00 bin
 0 drwxr-xr-x@ 116 Mercy  admin  3944  7  5 14:06 lib
 0 drwxr-xr-x@  34 Mercy  admin  1156  4 25 15:49 license
 0 drwxr-xr-x@  33 Mercy  admin  1122  4 25 15:49 plugins
 0 drwxr-xr-x@   3 Mercy  admin   102  7  5 14:06 redist

修改VMOptions

用 vi 工具打开Info.plist ,其中存在一个 key 元素内容为VMOptions的设置,如下所示:

<key>VMOptions</key>
<string>-Dfile.encoding=UTF-8 -XX:+UseConcMarkSweepGC -XX:SoftRefLRUPolicyMSPerMB=50 -ea -Dsun.io.useCanonCaches=false -Djava.net.preferIPv4Stack=true -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Xverify:none -XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log -XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof -Xbootclasspath/a:../lib/boot.jar</string>

其中JVM 参数 -XX:+UseConcMarkSweepGC 为 IDEA 默认配置GC 算法,将其移除,修改为:

<key>VMOptions</key>
<string>-Dfile.encoding=UTF-8 -XX:SoftRefLRUPolicyMSPerMB=50 -ea -Dsun.io.useCanonCaches=false -Djava.net.preferIPv4Stack=true -XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Xverify:none -XX:ErrorFile=$USER_HOME/java_error_in_idea_%p.log -XX:HeapDumpPath=$USER_HOME/java_error_in_idea.hprof -Xbootclasspath/a:../lib/boot.jar</string>

修改用户idea.vmoptions文件

切换当前用户的IDEA 配置目录

通过命令行,cd到~/Library/Preferences/IntelliJIdeaXX/目录下,如本人的机器路径:

/Users/Mercy/Library/Preferences/IdeaIC2017.1

新建或更新用户idea.vmoptions文件

将新建或者待更新的idea.vmoptions文件,更新以下JVM 配置项

-server
-XX:+UseG1GC
-XX:+UseNUMA
-Xms512m
-Xmn512m
-Xmx8g
-XX:MaxMetaspaceSize=512m
-XX:ReservedCodeCacheSize=240m

调优后观察

风扇旋转情况

启动 IntelliJ IDEA 2017.1 后一小时有余,发现风扇狂转的问题基本上没有发生。

JVM 概要情况

通过工具JConsole 连接 IDEA 进程,观察相关数据。

连接名称: pid: 9743                                        运行时间: 1 小时 39 分钟
虚拟机: Java HotSpot(TM) 64-Bit Server VM版本 25.131-b11   进程 CPU 时间: 6 分钟

JVM 参数情况

VM 参数:-Dfile.encoding=UTF-8 -XX:SoftRefLRUPolicyMSPerMB=50 -ea -Dsun.io.useCanonCaches=false -Djava.net.preferIPv4Stack=true 
-XX:+HeapDumpOnOutOfMemoryError -XX:-OmitStackTraceInFastThrow -Xverify:none 
-XX:ErrorFile=/Users/Mercy/java_error_in_idea_%p.log -XX:HeapDumpPath=/Users/Mercy/java_error_in_idea.hprof -Xbootclasspath/a:../lib/boot.jar 
-server -XX:+UseG1GC -XX:+UseNUMA -Xms512m -Xmx8g -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=240m 
-Djb.vmOptionsFile=/Users/Mercy/Library/Preferences/IdeaIC2017.1/idea.vmoptions -Didea.java.redist=jdk-bundled 
-Didea.home.path=/Applications/IntelliJ IDEA CE.app/Contents -Didea.executable=idea -Didea.platform.prefix=Idea -Didea.paths.selector=IdeaIC2017.1 

其中用户idea.vmoptions文件中的配置信息已经追加到JVM 启动参数中:

-server -XX:+UseG1GC -XX:+UseNUMA -Xms512m -Xmx8g -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=240m

JVM 内存情况

当前堆大小:   376,068 KB
最大堆大小: 8,388,608 KB
提交的内存: 524,288 KB
暂挂最终处理: 0对象
垃圾收集器: 名称 = 'G1 Young Generation', 收集 = 58, 总花费时间 = 1.583 秒
垃圾收集器: 名称 = 'G1 Old Generation', 收集 = 2, 总花费时间 = 1.930 秒

GC 算法已经由CMS切换成了G1算法!

为什么要选择用户idea.vmoptions文件

IDEA 官方的说明

Since version 14.0.0, the file /Applications/IntelliJ Idea XX.app/Contents/bin/idea.vmoptions or /Applications/IntelliJ Idea CE XX.app/Contents/bin/idea.vmoptions should be copied to ~/Library/Preferences/IntelliJIdeaXX/idea.vmoptions or ~/Library/Preferences/IdeaICXX/idea.vmoptions.

避免升级配置覆盖

IntelliJ IDEA 版本升级时,除非用户自行控制,默认情况IDEA会将全局的idea.vmoptions文件覆盖,因此,选择用户的idea.vmoptions文件可避免升级配置覆盖。

查看原文

疯狂小兵 评论了文章 · 2019-05-22

Intellij Idea 中进行 Mybatis逆向工程

开篇

Mybatis有个实用的功能就是逆向工程,能根据表结构反向生成实体类,这样能避免手工生成出错。市面上的教程大多都很老了,大部分都是针对mysql5的,以下为我执行mysql8时的经验。

引入工程

这里使用的是maven包管理工具,在pom.xml添加以下配置,以引入mybatis.generator

<build>
    <finalName>SpringMVCBasic</finalName>
    <!-- 添加mybatis-generator-maven-plugin插件 -->
    <plugins>
      <plugin>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-maven-plugin</artifactId>
        <version>1.3.2</version>
        <configuration>
          <verbose>true</verbose>
          <overwrite>true</overwrite>
        </configuration>
      </plugin>
    </plugins>
  </build>

配置文件

在maven项目下的src/main/resources 目录下新建generatorConfig.xml和generator.properties文件

generator.properties

jdbc.driverLocation=F:\\maven-repository\\mysql\\mysql-connector-java\\8.0.16\\mysql-connector-java-8.0.16.jar
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.connectionURL=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf-8
jdbc.userId=test
jdbc.password=test123

注意:
1,generator.properties里面的jdbc.driverLocation指向是你本地maven库对应mysql-connector地址
2,与老版本不同,这里driversClass为com.mysql.cj.jdbc.Driver

generatorConfig.xml


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <!--导入属性配置-->
    <properties resource="generator.properties"></properties>

    <!--指定特定数据库的jdbc驱动jar包的位置(绝对路径)-->
    <classPathEntry location="${jdbc.driverLocation}"/>

    <context id="default" targetRuntime="MyBatis3">

        <!-- optional,旨在创建class时,对注释进行控制 -->
        <commentGenerator>
            <!--是否去掉自动生成的注释 true:是-->
            <property name="suppressDate" value="true"/>
            <property name="suppressAllComments" value="true"/>
        </commentGenerator>

        <!--jdbc的数据库连接:驱动类、链接地址、用户名、密码-->
        <jdbcConnection
                driverClass="${jdbc.driverClass}"
                connectionURL="${jdbc.connectionURL}"
                userId="${jdbc.userId}"
                password="${jdbc.password}">
        </jdbcConnection>

        <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false"/>
        </javaTypeResolver>

        <!-- Model模型生成器,用来生成含有主键key的类,记录类 以及查询Example类
            targetPackage     指定生成的model生成所在的包名
            targetProject     指定在该项目下所在的路径
        -->
        <javaModelGenerator targetPackage="com.ifly.outsourcing.entity"
                            targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
            <property name="trimStrings" value="true"/>
        </javaModelGenerator>


        <!--Mapper映射文件生成所在的目录 为每一个数据库的表生成对应的SqlMap文件 -->
        <sqlMapGenerator targetPackage="mappers"
                         targetProject="src/main/resources">
            <property name="enableSubPackages" value="false"/>
        </sqlMapGenerator>

        <!-- 客户端代码,生成易于使用的针对Model对象和XML配置文件 的代码
                type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper对象
                type="MIXEDMAPPER",生成基于注解的Java Model 和相应的Mapper对象
                type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口
        -->

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.ifly.outsourcing.dao"
                             targetProject="src/main/java">
            <property name="enableSubPackages" value="true"/>
        </javaClientGenerator>

        <!-- 数据表进行生成操作 tableName:表名; domainObjectName:对应的DO -->
        <table tableName="user" domainObjectName="user"
               enableCountByExample="false" enableUpdateByExample="false"
               enableDeleteByExample="false" enableSelectByExample="false"
               selectByExampleQueryId="false">
        </table>

    </context>
</generatorConfiguration>

注意:这里主要注意修改对应的javaModelGenerator ,sqlMapGenerator,javaClientGenerator 为自己的生成路径。以及添加自己的数据表。

在Intellij IDEA添加一个“Run运行”选项

点击菜单栏的run,新建一个选项为maven的configurations,name为自己方便看,比如generator,commnd line注意写为:

mybatis-generator:generate -e

点击run即可生成对应文件。

本文最早发布于 Rootrl's blog

查看原文

疯狂小兵 发布了文章 · 2019-05-22

IDEA 常用插件

帮助 JAVA 开发者提升开发效率的插件工具

插件名称功能描述备注
lombok注解方式实现 Setter 和 Getter 等方法可以大量减少模板代码的编写工作,代码更整洁
backgroundimage plus设置IDEA 的背景图片
CodeGlance相当于当前代码文件的缩略图,可以快速移动到代码块IDEA 代码界面右侧的缩略预览
Idea restartIDEA 的重启功能因 IDEA 默认未自带重启功能,使用该插件可实现
mybatis log pluginMybatis 的日志插件,可在 IDEA 控制台看到完整可执行的 Sql 语句
translation翻译插件
markdown supportmarkdown 文件支持可以打开 markdown 文件并以 markdown 的语法渲览文档
free mybatis pluginMybatis 的插件,可以在interface 和 sql 的 xml 文件间跳转
maven helpermaven 依赖包的可视化展示对于 maven 项目的依赖包的处理很有帮助
gson format将 JSON 转为 java 类可以直接根据 JSON 数据定义出 java 类及其 field
grep console控制台日志展示不同级别的日志可以以不同的颜色展示
Material ThemeIDEA 的主题插件,可更换 IDEA 的展示风格可选主题比较多,个人比较喜欢
string manipulationString快速操作变量拼写切换,Un/Escape,Encode/Decode,Increment/Decrement,sort,trim 等功能。比较强大
CamelCase变量风格变换将驼峰、下划线、全大写、全小写等风格的转换。可快速装换变量风格
generateallsetter一键生成对象的所有 setter 方法调用快捷便利
查看原文

赞 1 收藏 1 评论 0

疯狂小兵 赞了文章 · 2019-05-20

线程池没你想的那么简单

前言

原以为线程池还挺简单的(平时常用,也分析过原理),这次是想自己动手写一个线程池来更加深入的了解它;但在动手写的过程中落地到细节时发现并没想的那么容易。结合源码对比后确实不得不佩服 Doug Lea

我觉得大部分人直接去看 java.util.concurrent.ThreadPoolExecutor 的源码时都是看一个大概,因为其中涉及到了许多细节处理,还有部分 AQS 的内容,所以想要理清楚具体细节并不是那么容易。

<!--more-->

与其挨个分析源码不如自己实现一个简版,当然简版并不意味着功能缺失,需要保证核心逻辑一致。

所以也是本篇文章的目的:

自己动手写一个五脏俱全的线程池,同时会了解到线程池的工作原理,以及如何在工作中合理的利用线程池。

再开始之前建议对线程池不是很熟悉的朋友看看这几篇:

这里我截取了部分内容,也许可以埋个伏笔(坑)。


具体请看这两个链接。

由于篇幅限制,本次可能会分为上下两篇。

创建线程池

现在进入正题,新建了一个 CustomThreadPool 类,它的工作原理如下:

简单来说就是往线程池里边丢任务,丢的任务会缓冲到队列里;线程池里存储的其实就是一个个的 Thread ,他们会一直不停的从刚才缓冲的队列里获取任务执行。

流程还是挺简单。

先来看看我们这个自创的线程池的效果如何吧:

初始化了一个核心为3、最大线程数为5、队列大小为 4 的线程池。

先往其中丢了 10 个任务,由于阻塞队列的大小为 4 ,最大线程数为 5 ,所以由于队列里缓冲不了最终会创建 5 个线程(上限)。

过段时间没有任务提交后(sleep)则会自动缩容到三个线程(保证不会小于核心线程数)。

构造函数

来看看具体是如何实现的。

下面则是这个线程池的构造函数:

会有以下几个核心参数:

  • miniSize 最小线程数,等效于 ThreadPool 中的核心线程数。
  • maxSize 最大线程数。
  • keepAliveTime 线程保活时间。
  • workQueue 阻塞队列。
  • notify 通知接口。

大致上都和 ThreadPool 中的参数相同,并且作用也是类似的。

需要注意的是其中初始化了一个 workers 成员变量:

    /**
     * 存放线程池
     */
    private volatile Set<Worker> workers;
    
    public CustomThreadPool(int miniSize, int maxSize, long keepAliveTime,
                            TimeUnit unit, BlockingQueue<Runnable> workQueue, Notify notify) {
       
        workers = new ConcurrentHashSet<>();
    }

workers 是最终存放线程池中运行的线程,在 j.u.c 源码中是一个 HashSet 所以对他所有的操作都是需要加锁。

我这里为了简便起见就自己定义了一个线程安全的 Set 称为 ConcurrentHashSet

其实原理也非常简单,和 HashSet 类似也是借助于 HashMap 来存放数据,利用其 key 不可重复的特性来实现 set ,只是这里的 HashMap 是用并发安全的 ConcurrentHashMap 来实现的。

这样就能保证对它的写入、删除都是线程安全的。

不过由于 ConcurrentHashMapsize() 函数并不准确,所以我这里单独利用了一个 AtomicInteger 来统计容器大小。

创建核心线程

往线程池中丢一个任务的时候其实要做的事情还蛮多的,最重要的事情莫过于创建线程存放到线程池中了。

当然我们不能无限制的创建线程,不然拿线程池来就没任何意义了。于是 miniSize maxSize 这两个参数就有了它的意义。

但这两个参数再哪一步的时候才起到作用呢?这就是首先需要明确的。

从这个流程图可以看出第一步是需要判断是否大于核心线程数,如果没有则创建。


结合代码可以发现在执行任务的时候会判断是否大于核心线程数,从而创建线程。

worker.startTask() 执行任务部分放到后面分析。

这里的 miniSize 由于会在多线程场景下使用,所以也用 volatile 关键字来保证可见性。

队列缓冲

结合上面的流程图,第二步自然是要判断队列是否可以存放任务(是否已满)。

优先会往队列里存放。

上至封顶

一旦写入失败则会判断当前线程池的大小是否大于最大线程数,如果没有则继续创建线程执行。

不然则执行会尝试阻塞写入队列(j.u.c 会在这里执行拒绝策略)

以上的步骤和刚才那张流程图是一样的,这样大家是否有看出什么坑嘛?

时刻小心

从上面流程图的这两步可以看出会直接创建新的线程

这个过程相对于中间直接写入阻塞队列的开销是非常大的,主要有以下两个原因:

  • 创建线程会加锁,虽说最终用的是 ConcurrentHashMap 的写入函数,但依然存在加锁的可能。
  • 会创建新的线程,创建线程还需要调用操作系统的 API 开销较大。
所以理想情况下我们应该避免这两步,尽量让丢入线程池中的任务进入阻塞队列中。

执行任务

任务是添加进来了,那是如何执行的?

在创建任务的时候提到过 worker.startTask() 函数:

    /**
     * 添加任务,需要加锁
     * @param runnable 任务
     */
    private void addWorker(Runnable runnable) {
        Worker worker = new Worker(runnable, true);
        worker.startTask();
        workers.add(worker);
    }

也就是在创建线程执行任务的时候会创建 Worker 对象,利用它的 startTask() 方法来执行任务。

所以先来看看 Worker 对象是长啥样的:

其实他本身也是一个线程,将接收到需要执行的任务存放到成员变量 task 处。

而其中最为关键的则是执行任务 worker.startTask() 这一步骤。

    public void startTask() {
        thread.start();
    }

其实就是运行了 worker 线程自己,下面来看 run 方法。

  • 第一步是将创建线程时传过来的任务执行(task.run),接着会一直不停的从队列里获取任务执行,直到获取不到新任务了。
  • 任务执行完毕后将内置的计数器 -1 ,方便后面任务全部执行完毕进行通知。
  • worker 线程获取不到任务后退出,需要将自己从线程池中释放掉(workers.remove(this))。

从队列里获取任务

其实 getTask 也是非常关键的一个方法,它封装了从队列中获取任务,同时对不需要保活的线程进行回收。

很明显,核心作用就是从队列里获取任务;但有两个地方需要注意:

  • 当线程数超过核心线程数时,在获取任务的时候需要通过保活时间从队列里获取任务;一旦获取不到任务则队列肯定是空的,这样返回 null 之后在上文的 run() 中就会退出这个线程;从而达到了回收线程的目的,也就是我们之前演示的效果

  • 这里需要加锁,加锁的原因是这里肯定会出现并发情况,不加锁会导致 workers.size() > miniSize 条件多次执行,从而导致线程被全部回收完毕。

关闭线程池

最后来谈谈线程关闭的事;

还是以刚才那段测试代码为例,如果提交任务后我们没有关闭线程,会发现即便是任务执行完毕后程序也不会退出。

从刚才的源码里其实也很容易看出来,不退出的原因是 Worker 线程一定还会一直阻塞在 task = workQueue.take(); 处,即便是线程缩容了也不会小于核心线程数。

通过堆栈也能证明:

恰好剩下三个线程阻塞于此处。

而关闭线程通常又有以下两种:

  • 立即关闭:执行关闭方法后不管现在线程池的运行状况,直接一刀切全部停掉,这样会导致任务丢失。
  • 不接受新的任务,同时等待现有任务执行完毕后退出线程池。

立即关闭

我们先来看第一种立即关闭

    /**
     * 立即关闭线程池,会造成任务丢失
     */
    public void shutDownNow() {
        isShutDown.set(true);
        tryClose(false);
    }
    
    /**
     * 关闭线程池
     *
     * @param isTry true 尝试关闭      --> 会等待所有任务执行完毕
     *              false 立即关闭线程池--> 任务有丢失的可能
     */
    private void tryClose(boolean isTry) {
        if (!isTry) {
            closeAllTask();
        } else {
            if (isShutDown.get() && totalTask.get() == 0) {
                closeAllTask();
            }
        }

    }

    /**
     * 关闭所有任务
     */
    private void closeAllTask() {
        for (Worker worker : workers) {
            //LOGGER.info("开始关闭");
            worker.close();
        }
    }
    
    public void close() {
        thread.interrupt();
    }

很容易看出,最终就是遍历线程池里所有的 worker 线程挨个执行他们的中断函数。

我们来测试一下:


可以发现后面丢进去的三个任务其实是没有被执行的。

完事后关闭

正常关闭则不一样:

    /**
     * 任务执行完毕后关闭线程池
     */
    public void shutdown() {
        isShutDown.set(true);
        tryClose(true);
    }

他会在这里多了一个判断,需要所有任务都执行完毕之后才会去中断线程。

同时在线程需要回收时都会尝试关闭线程:


来看看实际效果:

回收线程

上文或多或少提到了线程回收的事情,其实总结就是以下两点:

  • 一旦执行了 shutdown/shutdownNow 方法都会将线程池的状态置为关闭状态,这样只要 worker 线程尝试从队列里获取任务时就会直接返回空,导致 worker 线程被回收。

  • 一旦线程池大小超过了核心线程数就会使用保活时间来从队列里获取任务,所以一旦获取不到返回 null 时就会触发回收。

但如果我们的队列足够大,导致线程数都不会超过核心线程数,这样是不会触发回收的。

比如这里我将队列大小调为 10 ,这样任务就会累计在队列里,不会创建五个 worker 线程。

所以一直都是 Thread-1~3 这三个线程在反复调度任务。

总结

本次实现了线程池里大部分核心功能,我相信只要看完并动手敲一遍一定会对线程池有不一样的理解。

结合目前的内容来总结下:

  • 线程池、队列大小要设计的合理,尽量的让任务从队列中获取执行。
  • 慎用 shutdownNow() 方法关闭线程池,会导致任务丢失(除非业务允许)。
  • 如果任务多,线程执行时间短可以调大 keepalive 值,使得线程尽量不被回收从而可以复用线程。

同时下次会分享一些线程池的新特性,如:

  • 执行带有返回值的线程。
  • 异常处理怎么办?
  • 所有任务执行完怎么通知我?

本文所有源码:

https://github.com/crossoverJie/JCSprout/blob/master/src/main/java/com/crossoverjie/concurrent/CustomThreadPool.java

你的点赞与分享是对我最大的支持

查看原文

赞 16 收藏 11 评论 0

疯狂小兵 发布了文章 · 2019-05-13

mybatis处理枚举类

mybatis自带对枚举的处理类

  • org.apache.ibatis.type.EnumOrdinalTypeHandler<E> :该类实现了枚举类型和Integer类型的相互转换。

但是给转换仅仅是将对应的枚举转换为其索引位置,也就是"ordinal()"方法获取到的值。对应自定义的int值,该类无能为力。

  • org.apache.ibatis.type.EnumTypeHandler<E>:该类实现了枚举类型和String类型的相互转换。

对于想将枚举在数据库中存储为对应的int值的情况,该类没办法实现。

基于以上mybatis提供的两个枚举处理类的能力有限,因此只能自己定义对枚举的转换了。

自定义mybatis的枚举处理类EnumValueTypeHandler

该类需要继承org.apache.ibatis.type.BaseTypeHandler<E>,然后在重定义的方法中实现自有逻辑。

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.MappedTypes;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

/**
 * 处理实现了{@link EsnBaseEnum}接口的枚举类
 * @author followtry
 * @time 2016年8月16日 下午8:06:49
 * @since 2016年8月16日 下午8:06:49
 */
 //在 xml 中添加该 TypeHandler 时需要使用该注解
@MappedTypes(value = {
        QcListTypeEnum.class,
        SellingQcBizTypeEnum.class
})
public class EnumValueTypeHandler<E extends EsnBaseEnum> extends BaseTypeHandler<E> {

    private Class<E> type;

    private final E[] enums;

    public EnumValueTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        this.enums = type.getEnumConstants();
        if (this.enums == null) {
            throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
        }
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
        //获取非空的枚举的int值并设置到statement中
        ps.setInt(i, parameter.getValue());
    }

    @Override
    public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
        int i = rs.getInt(columnName);
        if (rs.wasNull()) {
            return null;
        } else {
            try {
                return getEnumByValue(i);
            } catch (Exception ex) {
                throw new IllegalArgumentException(
                        "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex);
            }
        }
    }

    /**
     * 通过枚举类型的int值,获取到对应的枚举类型
     * @author jingzz
     * @param i
     */
    protected E getEnumByValue(int i) {
        for (E e : enums) {
            if (e.getValue() == i) {
                return e;
            }
        }
        return null;
    }

    @Override
    public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        int i = rs.getInt(columnIndex);
        if (rs.wasNull()) {
            return null;
        } else {
            try {
                return getEnumByValue(i);
            } catch (Exception ex) {
                throw new IllegalArgumentException(
                        "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex);
            }
        }
    }

    @Override
    public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        int i = cs.getInt(columnIndex);
        if (cs.wasNull()) {
            return null;
        } else {
            try {
                return getEnumByValue(i);
            } catch (Exception ex) {
                throw new IllegalArgumentException(
                        "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex);
            }
        }
    }

}

该处理器是处理继承了EsnBaseEnum接口的枚举类,因为该接口中定义了获取自定义int值的方法。

如果在 mybatis 的 xml 中配置 该 typehandler,则需要添加注解@MappedTypes。在添加 typeHandler 注册时使用具体的实现类注册。

配置文件如下:

<typeAliases>
<!-- 为自定义的 TypeHandler 指定别名,在 sql 的 xml 中即可使用别名访问了 -->
    <typeAlias type="cn.followtry.mybatis.EnumValueTypeHandler" alias="enumValueTypeHandler"/>
</typeAliases>
<typeHandlers>
    <!--处理枚举的值转换-->
    <typeHandler handler="cn.followtry.mybatis.EnumValueTypeHandler"/>
</typeHandlers>

自定义的 Enum 需要实现的接口

/**
 * @author jingzz
 * @time 2016年8月17日 上午9:22:46
 * @since 2016年8月17日 上午9:22:46
 */
public interface EsnBaseEnum {
    
    public int getValue();
}

而实现了EsnBaseEnum的枚举示例如下:


/**
 * 同步的类型
 * @author jingzz
 * @time 2016年8月3日 上午11:13:06
 */
public enum SyncType implements EsnBaseEnum {
    
    DEPT(3),//部门
    PERSON(1);//人员
    
    private int value;
    
    private SyncType(int value) {
        
        this.value = value;
    }
    
    @Override
    public int getValue(){
        return this.value;
    }
}

只有在实现了EsnBaseEnum的接口,EnumValueTypeHandler才能通过接口的getValue方法获取到对应枚举的值。

到此,对于枚举的简单处理逻辑已经实现完成了,接下来就是如何配置来使用该自定义枚举处理逻辑

配置对枚举的处理

首先,mybatis中对于处理逻辑的设置是在sql的映射文件中,如EsnSyncLogMapper.xml

关键的设置枚举处理的位置如下:

<resultMap id="BaseResultMap" type="com.test.dao.model.EsnSyncLog" >
    <id column="id" property="id" jdbcType="INTEGER" />
    <result column="sync_time" property="syncTime" jdbcType="TIMESTAMP" />
    <result column="total_num" property="totalNum" jdbcType="INTEGER" />
    <result column="success_total_num" property="successTotalNum" jdbcType="INTEGER" />
    <result column="type" property="type" typeHandler="com.test.common.EnumValueTypeHandler" />
    <result column="msg" property="msg" jdbcType="VARCHAR" />
  </resultMap>

其中type列设置了属性typeHandler,其值为自定义的枚举处理逻辑。

而在具体sql中也需要使用typeHandler属性,如:

 <if test="type != null" >
    #{type, typeHandler=com.test.common.EnumValueTypeHandler},
  </if>

在mybatis处理到该位置时,就会调用typeHandler指定的处理类来处理枚举类型。

查看原文

赞 1 收藏 1 评论 0

疯狂小兵 发布了文章 · 2019-04-03

5种顶级思维

墨菲定律

越害怕什么就越会发生什么

吉德林法则

把问题清楚的写下来就已经解决一半了。

吉尔伯特定律

工作中的最大问题就是没人跟你说该如何去做。

沃尔森法则

把信息和金钱排在第一位,金钱自然就会到来

福克然定律

没必要做决定时就不要做决定

查看原文

赞 0 收藏 0 评论 0

疯狂小兵 发布了文章 · 2019-04-03

Mybatis源码分析(1) - Mybatis包目录简介

Mybatis核心包

Map类图

包名称包内内容简介
annotation注解目录。包括所有的注解。如@SELECT,@UPDATE
bindingMapper类的实例反射生成工具目录
builder主要是注解,mapper和SqlSuorce的构造器及转换器
cacheMybatis内部缓存接口。实现了一些特定的缓存策略。FifoCache,LruCache,BlockingCache,LoggingCache
cursor默认的游标处理类
dataSource数据源工厂类及实现。实现类包括JndiDataSourceFactoryPooledDataSourceFactoryUnpooledDataSourceFactory。 数据源实现类: UnpooledDataSourcePooledDataSource
exceptionsMybatis自定义的三个异常类。ExceptionFactoryPersistenceExceptionTooManyResultsExceptionIbatisException。都继承自RuntimeException
executor执行器相关包。包括Key生成器、加载器(包括Cglib、Javassist的代理,结果加载器)、参数处理器接口、结果处理器、结果集(resultSet)处理器、Statement处理器(实现类:BaseStatementHandlerCallableStatementHandlerPreparedStatementHandlerRoutingStatementHandlerSimpleStatementHandler)、执行器(SimpleExecutorReuseExecutorCachingExecutorBatchExecutorBaseExecutor)
io主要是定义的几个VFS(VFSDefaultVFSClassLoaderWrapper)
javassistjavassist的字节码处理包
jdbc与Sql相关的操作。如Sql运行器,脚本运行器和Sql封装类等
lang指定是用java7还是java8的API的注解.UsesJava7UsesJava8
logging各个类型的日志适配器,都实现了Log接口。StdOutImplSlf4jImplNoLoggingImplLog4j2ImplLog4jImplJdk14LoggingImplBaseJdbcLoggerJakartaCommonsLoggingImpl
mapping主要是接口参数,sql和返回结果的映射类,主要类包括:MappedStatement,ParameterMap,ParameterMapping,ResultMap,ResultMapping,BoundSql,SqlSource等类
ognlognl包在Mybatis中的内部代码引用
parsing变量解析.如解析${},#{}
plugin主要包含插件的定义接口。如Interceptor,Plugin,InterceptorChain
reflection主要是一些反射操作的工具方法和对象工厂类,以及一些常用的包装类,如BaseWrapper,BeanWrapper,CollectionWrapper,MapWrapper,ObjectWrapper,,,
scripting执行驱动和动态Sql解析的老巢
session主要是SqlSession和SqlSessionFactory
transaction主要是mybatis简单封装的jdbc事务操作类
type各个类型数据的处理器。用于动态的设置参数和转换数据。如IntegerTypeHandler用来处理Integer类型的值的set和get操作。除了八大基本类型。还有常用的集合及Map类型,还增加了各种时间类型的处理器

总结

通过整理每个包的主要功能。通览整个mybatis的代码结构,了解各个组件的位置和大概的处理关系。为后续分析源码打下基础。

查看原文

赞 7 收藏 5 评论 0

认证与成就

  • 获得 43 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2016-01-15
个人主页被 882 人浏览