开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

IntelliJ IDEA 202024款 神级超级牛逼

发表于 2020-12-29

满满的都是干货 所有插件都是在 ctrl+alt+s 里的plugins 里进行搜索安装

在这里插入图片描述

1、CodeGlance 代码迷你缩放图插件

在这里插入图片描述

2、 Codota

代码提示工具,扫描你的代码后,根据你的敲击完美提示

Codota基于数百万个开源Java程序和您的上下文来完成代码行,从而帮助您以更少的错误更快地进行编码。

在这里插入图片描述

安装:

在这里插入图片描述

3、Material Theme UI

那就顺便推荐一下这个吧,超多的主题插件,各种颜色,各种模式,感兴趣的可以试一下,图我就不截了

在这里插入图片描述

4、Alibaba Java Coding Guidelines

阿里巴巴的编码规约检查插件

检查你的编码习惯,让你更规范

在这里插入图片描述

都是在plugins里搜索,我就不截图了

5、 Alibaba Cloud Toolkit

快速部署到服务器,超级牛逼 超级推荐

在这里插入图片描述

在这里插入图片描述

6、GenerateAllSetter

快速生成get set

在这里插入图片描述

7、idea zookeezper

管理zookeeper的idea插件本代码是根据github.com/linux-china…

可以图形化的查看zk 节点信息了,非常简单

在这里插入图片描述

8、JRebel 热加载插件,也是超级牛逼,就是收费。

JRebel是一种生产力工具,允许开发人员立即重新加载代码更改。它跳过了Java开发中常见的重建,重新启动和重新部署周期。JRebel使开发人员可以在相同的时间内完成更多工作,并在编码时保持顺畅。JRebel支持大多数现实世界的企业Java堆栈,并且易于安装到现有的开发环境中。

在这里插入图片描述

9、Json Parser json串格式化工具,不用打开浏览器了

厌倦了打开浏览器来格式化和验证JSON?为什么不安装JSON Parser并在具有脱机支持的IDE内进行呢?JSON Parser是用于验证和格式化JSON字符串的轻量级插件。安装并传播:)

在这里插入图片描述

10、Lombok 这个太牛逼了,应该大家都在用吧

只需加上注解 什么get set 什么toString 等等方法都不需要写

在这里插入图片描述

在这里插入图片描述

11、JUnitGenerator

自动生成测试代码。

在这里插入图片描述

在这里插入图片描述

12、MyBatis Log Plugin 神级

根据执行sql 替换掉 ? 显示完整 sql, 直接复制粘贴到数据库 就可以执行

在这里插入图片描述

13、MyBatisCodeHelperPro 超级牛逼神级

支持mapper互跳,方法自动生成,代码自动生成

在这里插入图片描述

只输入了一个fin 的各种提示就处理了,当你选择一个回车的时候 mapper.xml 也就给你生成了。

在这里插入图片描述

还可以根据数据库表自动生成xml、mapper service 和个增删改查代码,可一建生成所有表 真的超级牛逼啊

在这里插入图片描述

14、RESTfultoolkit 根据url 查找controller

一套 RESTful 服务开发辅助工具集。

1.根据 URL 直接跳转到对应的方法定义 ( Ctrl \ or Ctrl Alt N );

2.提供了一个 Services tree 的显示窗口;

3.一个简单的 http 请求工具;

4.在请求方法上添加了有用功能:复制生成 URL;复制方法参数…

5.其他功能:

  • java 类上添加 Convert to JSON 功能,格式化 json 数据 ( Windows: Ctrl + Enter; Mac: Command + Enter )。
  • 支持 Spring 体系 (Spring MVC / Spring Boot 1.x,2.x)
  • 支持 JAX-RS
  • 支持 Java 和 Kotlin 语言。

在这里插入图片描述

15、Translation 翻译插件 灰常牛逼

在这里插入图片描述

在这里插入图片描述

翻译中文,给接口起名字就不用费劲啦!

在这里插入图片描述

作者:荡漾-

来源:blog.csdn.net/qq_38380025/article/details/105247548

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JVM 内存分析工具 MAT 的深度讲解与实践——进阶篇 1

发表于 2020-12-29

注:本文原创,转发需标明作者及原文链接。欢迎关注 【0广告微信公众号:Q的博客】。

本系列共三篇文章, 本文是系列第2篇——进阶篇,详细讲解 MAT 各种工具的核心功能、用法、适用场景,并在具体实战场景下讲解帮大家学习如何针对各类内存问题。

  • 《JVM 内存分析工具 MAT 的深度讲解与实践——入门篇》 介绍 MAT 产品功能、基础概念、与其他工具对比、Quick Start 指南。
  • 《JVM 内存分析工具 MAT 的深度讲解与实践——进阶篇》 展开并详细讲解 MAT 各种工具的核心功能、用法、场景,并在具体实战场景下讲解帮大家加深体会。
  • 《JVM 内存分析工具 MAT 的深度讲解与实践——高阶篇》 总结复杂内存问题的系统性分析方法,并通过一个综合案例提升大家的实战能力。

  1. 前言

熟练掌握 MAT 是 Java 高手的必备能力,但实践时大家往往需面对众多功能,眼花缭乱不知如何下手,小编也没有找到一篇完善的教学素材,所以整理本文帮大家系统掌握 MAT 分析工具。

本文详细讲解 MAT 众多内存分析工具功能,这些功能组合使用异常强大,熟练使用几乎可以解决所有的堆内存离线分析的问题。我们将功能划分为4类:内存分布详情、对象间依赖、对象状态详情、按条件检索。每大类有多个功能点,本文会逐一讲解各功能的场景及用法。此外,添加了原创或引用案例加强理解和掌握。

注:在该系列开篇文章《JVM 内存分析工具 MAT 的深度讲解与实践——入门篇》中介绍了 MAT 的使用场景及安装方法,不熟悉 MAT 的读者建议先阅读上文并安装,本文案例很容易在本地实践。另外,上文中产品介绍部分顺序也对应本文功能讲解的组织,如下图:

为减少对眼花缭乱的菜单的迷茫,可以通过下图先整体熟悉下各功能使用入口,后续都会讲到。

  1. 内存分布详解及实战

2.1 全局信息概览

功能:展现堆内存大小、对象数量、class 数量、class loader 数量、GC Root 数量、环境变量、线程概况等全局统计信息。

使用入口:MAT 主界面 → Heap Dump Overview。

举例:下面是对象数量、class loader 数量、GC Root 数量,可以看出 class loader 存在异常。

举例:下图是线程概况,可以查看每个线程名、线程的 Retained Heap、daemon 属性等。

使用场景
全局概览呈现全局统计信息,重点查看整体是否有异常数据,所以有效信息有限,下面几种场景有一定帮助:

  • 方法区溢出时(Java 8后不使用方法区,对应堆溢出),查看 class 数量异常多,可以考虑是否为动态代理类异常载入过多或类被反复重复加载。
  • 方法区溢出时,查看 class loader 数量过多,可以考虑是否为自定义 class loader 被异常循环使用。
  • GC Root 过多,可以查看 GC Root 分布,理论上这种情况极少会遇到,笔者只在 JNI 使用一个存在 BUG 的库时遇到过。
  • 线程数过多,一般是频繁创建线程但无法执行结束,从概览可以了解异常表象,具体原因可以参考本文线程分析部分内容,此处不展开。

2.2 Dominator tree

注:笔者使用频率的 Top1,是高效分析 Dump 必看的功能。

功能

  • 展现对象的支配关系图,并给出对象支配内存的大小(支配内存等同于 Retained Heap,即其被 GC 回收可释放的内存大小)
  • 支持排序、支持按 package、class loader、super class、class 聚类统计

使用入口:全局支配树: MAT 主界面 → Dominator tree。

举例: 下图中通过查看 Dominator tree,了解到内存主要是由 ThreadAndListHolder-thread 及 main 两个线程支配(后面第2.6节会给出整体案例)。

使用场景

  • 开始 Dump 分析时,首先应使用 Dominator tree 了解各支配树起点对象所支配内存的大小,进而了解哪几个起点对象是 GC 无法释放大内存的原因。
  • 当个别对象支配树的 Retained Heap 很大存在明显倾斜时,可以重点分析占比高的对象支配关系,展开子树进一步定位到问题根因,如下图中可看出最终是 SameContentWrapperContainer 对象持有的 ArrayList 过大。

  • 在 Dominator tree 中展开树状图,可以查看支配关系路径(与 outgoing reference 的区别是:如果 X 支配 Y,则 X 释放后 Y必然可释放;如果仅仅是 X 引用 Y,可能仍有其他对象引用 Y,X 释放后 Y 仍不能释放,所以 Dominator tree 去除了 incoming reference 中大量的冗余信息)。
  • 有些情况下可能并没有支配起点对象的 Retained Heap 占用很大内存(比如 class X 有100个对象,每个对象的 Retained Heap 是10M,则 class X 所有对象实际支配的内存是 1G,但可能 Dominator tree 的前20个都是其他class 的对象),这时可以按 class、package、class loader 做聚合,进而定位目标。
+ 下图中各 GC Roots 所支配的内存均不大,这时需要聚合定位爆发点。
![](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/7847c47fe230fce98de3750c4d4b92a522cc2416fb77b102b075421fc6916b0c)
+ 在 Dominator tree 展现后按 class 聚合,如下图:
![](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/0ae04926f4c34f9a94f21e3c6d635c004ae8a145ea18fbd85020e0647cef4125)
+ 可以定位到是 SomeEntry 对象支配内存较多,然后结合代码进一步分析具体原因。
![](https://gitee.com/songjianzaina/juejin_p16/raw/master/img/309f540a3fc9d818d0dd744a88ebf9c3dc5715b3ae8092ee9e8e3f9ff6f4d8f7)
  • 在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。

2.3 Histogram 直方图

注:笔者使用频率 Top2

功能

  • 罗列每个类实例的数量、类实例累计内存占比,包括自身内存占用量(Shallow Heap)及支配对象的内存占用量(Retain Heap)。
  • 支持按对象数量、Retained Heap、Shallow Heap(默认排序)等指标排序;支持按正则过滤;支持按 package、class loader、super class、class 聚类统计,

使用入口:MAT 主界面 → Histogram;注意 Histogram 默认不展现 Retained Heap,可以使用计算器图标计算,如下图所示。

使用场景

  • 有些情况 Dominator tree 无法展现出热点对象(上文提到 Dominator tree 支配内存排名前20的占比均不高,或者按 class 聚合也无明显热点对象,此时 Dominator tree 很难做关联分析判断哪类对象占比高),这时可以使用 Histogram 查看所有对象所属类的分布,快速定位占据 Retained Heap 大头的类。
  • 使用技巧
    • Integer,String 和 Object[] 一般不直接导致内存问题。为更好的组织视图,可以通过 class loader 或 package 分组进一步聚焦,如下图。
    • Histogram 支持使用正则表达式来过滤。例如,我们可以只展示那些匹配com.q.*的类。
    • 可以在 Histogram 的某个类继续使用 outgoing reference 查看对象分布,进而定位哪些对象是大头

2.4 Leak Suspects

功能:具备自动检测内存泄漏功能,罗列可能存在内存泄漏的问题点。

使用入口:一般当存在明显的内存泄漏时,分析完Dump文件后就会展现,也可以如下图在 MAT 主页 → Leak Suspects。

使用场景:需要查看引用链条上占用内存较多的可疑对象。这个功能可解决一些基础问题,但复杂的问题往往帮助有限。

举例

  • 下图中 Leak Suspects 视图展现了两个线程支配了绝大部分内存。

  • 下图是点击上图中 Keywords 中 “Details” ,获取实例到 GC Root 的最短路径、dominator 路径的细信息。

2.5 Top Consumers

功能:最大对象报告,可以展现哪些类、哪些 class loader、哪些 package 占用最高比例的内存,其功能 Histogram 及 Dominator tree 也都支持。

使用场景:应用程序发生内存泄漏时,查看哪些泄漏的对象通常在 Dump 快照中会占很大的比重。因此,对简单的问题具有较高的价值。

2.6 综合案例一

使用工具项:Heap dump overview、Dominator tree、Histogram、Class Loader Explorer(见3.4节)、incoming references(见3.1节)

程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码package com.q.mat;

import java.util.*;
import org.objectweb.asm.*;

public class ClassLoaderOOMOps extends ClassLoader implements Opcodes {

public static void main(final String args[]) throws Exception {
new ThreadAndListHolder(); // ThreadAndListHolder 类中会加载大对象

List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();
final String className = "ClassLoaderOOMExample";
final byte[] code = geneDynamicClassBytes(className);

// 循环创建自定义 class loader,并加载 ClassLoaderOOMExample
while (true) {
ClassLoaderOOMOps loader = new ClassLoaderOOMOps();
Class<?> exampleClass = loader.defineClass(className, code, 0, code.length); //将二进制流加载到内存中
classLoaders.add(loader);
// exampleClass.getMethods()[0].invoke(null, new Object[]{null}); // 执行自动加载类的方法,通过反射调用main
}
}

private static byte[] geneDynamicClassBytes(String className) throws Exception {
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_1, ACC_PUBLIC, className, null, "java/lang/Object", null);

//生成默认构造方法
MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);

//生成构造方法的字节码指令
mw.visitVarInsn(ALOAD, 0);
mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
mw.visitInsn(RETURN);
mw.visitMaxs(1, 1);
mw.visitEnd();

//生成main方法
mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
//生成main方法中的字节码指令
mw.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

mw.visitLdcInsn("Hello world!");
mw.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mw.visitInsn(RETURN);
mw.visitMaxs(2, 2);
mw.visitEnd(); //字节码生成完成

return cw.toByteArray(); // 获取生成的class文件对应的二进制流

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
java复制代码package com.q.mat;

import java.util.*;
import org.objectweb.asm.*;

public class ThreadAndListHolder extends ClassLoader implements Opcodes {
private static Thread innerThread1;
private static Thread innerThread2;
private static final SameContentWrapperContainerProxy sameContentWrapperContainerProxy = new SameContentWrapperContainerProxy();

static {
// 启用两个线程作为 GC Roots
innerThread1 = new Thread(new Runnable() {
public void run() {
SameContentWrapperContainerProxy proxy = sameContentWrapperContainerProxy;
try {
Thread.sleep(60 * 60 * 1000);
} catch (Exception e) {
System.exit(1);
}
}
});
innerThread1.setName("ThreadAndListHolder-thread-1");
innerThread1.start();

innerThread2 = new Thread(new Runnable() {
public void run() {
SameContentWrapperContainerProxy proxy = proxy = sameContentWrapperContainerProxy;
try {
Thread.sleep(60 * 60 * 1000);
} catch (Exception e) {
System.exit(1);
}
}
});
innerThread2.setName("ThreadAndListHolder-thread-2");
innerThread2.start();
}
}

class IntArrayListWrapper {
private ArrayList<Integer> list;
private String name;

public IntArrayListWrapper(ArrayList<Integer> list, String name) {
this.list = list;
this.name = name;
}
}

class SameContentWrapperContainer {
// 2个Wrapper内部指向同一个 ArrayList,方便学习 Dominator tree
IntArrayListWrapper intArrayListWrapper1;
IntArrayListWrapper intArrayListWrapper2;

public void init() {
// 线程直接支配 arrayList,两个 IntArrayListWrapper 均不支配 arrayList,只能线程运行完回收
ArrayList<Integer> arrayList = generateSeqIntList(10 * 1000 * 1000, 0);
intArrayListWrapper1 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-1");
intArrayListWrapper2 = new IntArrayListWrapper(arrayList, "IntArrayListWrapper-2");
}

private static ArrayList<Integer> generateSeqIntList(int size, int startValue) {
ArrayList<Integer> list = new ArrayList<Integer>(size);
for (int i = startValue; i < startValue + size; i++) {
list.add(i);
}
return list;
}
}

class SameContentWrapperContainerProxy {
SameContentWrapperContainer sameContentWrapperContainer;

public SameContentWrapperContainerProxy() {
SameContentWrapperContainer container = new SameContentWrapperContainer();
container.init();
sameContentWrapperContainer = container;
}
}
1
2
ruby复制代码启动参数:-Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/gjd/Desktop/dump/heapdump.hprof 
-XX:-UseCompressedClassPointers -XX:-UseCompressedOops

引用关系图

分析过程

  1. 首先进入 Dominator tree,可以看出是 SameContentWrapperContainerProxy 对象与 main 线程两者持有99%内存不能释放导致 OOM。

  2. 先来看方向一,在 Heap Dump Overview 中可以快速定位到 Number of class loaders 数达50万以上,这种基本属于异常情况,如下图所示。
  3. 使用 Class Loader Explorer 分析工具,此时会展现类加载详情,可以看到有524061个 class loader。我们的案例中仅有ClassLoaderOOMOps 这样的自定义类加载器,所以很快可以定位到问题。

  4. 如果类加载器较多,不能确定是哪个引发问题,则可以将所有的 class loader对象按类做聚类,如下图所示。
  5. Histogram 会根据 class 聚合,并展现对象数量级其 Shallow Heap 及 Retained Heap(如Retained Heap项目为空,可以点击下图中计算机的图标并计算 Retained Heap),可以看到 ClassLoaderOOMOps 有524044个对象,其 Retain Heap 占据了370M以上(上述代码是100M左右)。
  6. 使用 incoming references,可以找到创建的代码位置。
  7. 再来看方向二,同样在占据319M内存的 Obejct 数组采用 incoming references 查看引用路径,也很容易定位到具体代码位置。并且从下图中我们看出,Dominator tree 的起点并不一定是 GC根,且通过 Dominator tree 可能无法获取到最开始的创建路径,但 incoming references 是可以的。
  1. 对象间依赖详解及实战

3.1 References

注:笔者使用频率 Top2

功能:在对象引用图中查看某个特定对象的所有引用关系(提供对象对其他对象或基本类型的引用关系,以及被外部其他对象的引用关系)。通过任一对象的直接引用及间接引用详情(主要是属性值及内存占用),提供完善的依赖链路详情。

使用入口:目标域右键 → List objects → with outgoing references/with incoming references.

使用场景

  • outgoing reference:查看对象所引用的对象,并支持链式传递操作。如查看一个大对象持有哪些内容,当一个复杂对象的 Retained Heap 较大时,通过 outgoing reference 可以查看由哪个属性引发。下图中 A 支配 F,且 F 占据大量内存,但优化时 F 的直接支配对象 A 无法修改。可通过 outgoing reference 看关系链上 D、B、E、C,并结合业务逻辑优化中间环节,这依托 dominator tree 是做不到的。
  • incoming reference:查看对象被哪些对象引用,并支持链式传递操作。如查看一个大对象都被哪些对象引用,下图中 K 占内存大,所以 J 的 Retained Heap 较大,目标是从 GC Roots 摘除 J 引用,但在 Dominator tree 上 J 是树根,无法获取其被引用路径,可通过 incoming reference 查看关系链上的 H、X、Y ,并结合业务逻辑将 J 从 GC Root 链摘除。

3.2 Thread overview

功能:展现转储 dump 文件时线程执行栈、线程栈引用的对象等详细状态,也提供各线程的 Retained Heap 等关联内存信息。

使用入口:MAT 主页 → Thread overview

使用场景

  • 查看不同线程持有的内存占比,定位高内存消耗线程(开发技巧:不要直接使用 Thread 或 Executor 默认线程名避免全部混合在一起,使用线程尽量自命名方便识别,如下图中 ThreadAndListHolder-thread 是自定义线程名,可以很容易定位到具体代码)
  • 查看线程的执行栈及变量,结合业务代码了解线程阻塞在什么地方,以及无法继续运行释放内存,如下图中 ThreadAndListHolder-thread 阻塞在 sleep 方法。

3.3 Path To GC Roots

功能:提供任一对象到 GC Root 的路径详情。

使用入口:目标域右键 → Path To GC Roots

使用场景:有时你确信已经处理了大的对象集合但依然无法回收,该功能能快速定位异常对象不能被 GC 回收的原因,直击异常对象到 GC Root 的引用路径。比 incoming reference 的优势是屏蔽掉很多不需关注的引用关系,比 Dominator tree 的优势是可以得到更全面的信息。

小技巧:在排查内存泄漏时,建议选择 exclude all phantom/weak/soft etc.references 排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被 GC 给回收,聚焦在对象否还存在 Strong 引用链即可。

3.4 class loader 分析

功能

  • 查看堆中所有 class loader 的使用情况(入口:MAT 主页菜单蓝色桶图标 → Java Basics → Class Loader Explorer)。
  • 查看堆中被不同class loader 重复加载的类(入口:MAT 主页菜单蓝色桶图标 → Java Basics → Duplicated Classes)。

使用场景

  • 当从 Heap dump overview 了解到系统中 class loader 过多,导致占用内存异常时进入更细致的分析定位根因时使用。
  • 解决 NoClassDefFoundError 问题或检测 jar 包是否被重复加载

具体使用方法在 2.6 及 3.5 两节的案例中有介绍。

3.5 综合案例二

使用工具项:class loader(重复类检测)、inspector、正则检索。

异常现象 :运行时报 NoClassDefFoundError,在 classpath 中有两个不同版本的同名类。

分析过程

  1. 进入 MAT 已加载的重复类检测功能,方式如下图。
  2. 可以看到所有重复的类,以及相关的类加载器,如下图。
  3. 根据类名,在<Regex>框中输入类名可以过滤无效信息。
  4. 选中目标类,通过Inspector视图,可以看到被加载的类具体是在哪个jar包里。(本例中重复的类是被 URLClassloader 加载的,右键点击 “_context” 属性,最后点击 “Go Into”,在弹出的窗口中的属性 “_war” 值是被加载类的具体包位置)


  1. 对象状态详解及实战

4.1 inspector

功能:MAT 通过 inspector 面板展现对象的详情信息,如静态属性值及实例属性值、内存地址、类继承关系、package、class loader、GC Roots 等详情数据。

使用场景

  • 当内存使用量与业务逻辑有较强关联的场景,通过 inspector 可以通过查看对象具体属性值。比如:社交场景中某个用户对象的好友列表异常,其 List 长度达到几亿,通过 inspector 面板获取到异常用户 ID,进而从业务视角继续排查属于哪个用户,本例可能有系统账号,与所有用户是好友。
  • 集合等类型的使用会较多,如查看 ArrayList 的 size 属性也就了解其大小。

举例:下图中左边的 Inspector 窗口展现了地址 0x125754cf8 的 ArrayList 实例详情,包括 modCount 等并不会在 outgoing references 展现的基本属性。

4.2 集合状态

功能:帮助更直观的了解系统的内存使用情况,查找浪费的内存空间。

使用入口:MAT 主页 → Java Collections → 填充率/Hash冲突等功能。

使用场景

  • 通过对 ArrayList 或数组等集合类对象按填充率聚类,定位稀疏或空集合类对象造成的内存浪费。
  • 通过 HashMap 冲突率判定 hash 策略是否合理。

具体使用方法在 4.3 节案例详细介绍。

4.3 综合案例三

使用工具项:Dominator tree、Histogram、集合 ratio。

异常现象 :程序 OOM,且 Dominator tree 无大对象,通过 Histogram 了解到多个 ArrayList 占据大量内存,期望通过减少 ArrayList 优化程序。

程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码package com.q.mat;

import java.util.ArrayList;
import java.util.List;

public class ListRatioDemo {

public static void main(String[] args) {
for(int i=0;i<10000;i++){
Thread thread = new Thread(new Runnable() {
public void run() {
HolderContainer holderContainer1 = new HolderContainer();
try {
Thread.sleep(1000 * 1000 * 60);
} catch (Exception e) {
System.exit(1);
}
}
});
thread.setName("inner-thread-" + i);
thread.start();
}

}
}

class HolderContainer {
ListHolder listHolder1 = new ListHolder().init();
ListHolder listHolder2 = new ListHolder().init();
}

class ListHolder {
static final int LIST_SIZE = 100 * 1000;
List<String> list1 = new ArrayList(LIST_SIZE); // 5%填充
List<String> list2 = new ArrayList(LIST_SIZE); // 5%填充
List<String> list3 = new ArrayList(LIST_SIZE); // 15%填充
List<String> list4 = new ArrayList(LIST_SIZE); // 30%填充

public ListHolder init() {
for (int i = 0; i < LIST_SIZE; i++) {
if (i < 0.05 * LIST_SIZE) {
list1.add("" + i);
list2.add("" + i);
}
if (i < 0.15 * LIST_SIZE) {
list3.add("" + i);
}
if (i < 0.3 * LIST_SIZE) {
list4.add("" + i);
}
}
return this;
}
}

分析过程

  1. 使用 Dominator tree 查看并无高占比起点。
  2. 使用 Histogram 定位到 ListHolder 及 ArrayList 占比过高,经过业务分析很多 List 填充率很低浪费内存。
  3. 查看 ArrayList 的填充率,MAT 首页 → Java Collections → Collection Fill Ratio。
  4. 查看类型填写 java.util.ArrayList。
  5. 从结果可以看出绝大部分 ArrayList 初始申请长度过大。
  1. 按条件检索详解及实战

5.1 OQL

功能:提供一种类似于SQL的对象(类)级别统一结构化查询语言,根据条件对堆中对象进行筛选。

语法

1
sql复制代码SELECT * FROM [ INSTANCEOF ] <class_name> [ WHERE <filter-expression> ]
  • Select 子句可以使用“*”,查看结果对象的引用实例(相当于 outgoing references);可以指定具体的内容,如 Select OBJECTS v.elementData from xx 是返回的结果是完整的对象,而不是简单的对象描述信息);可以使用 Distinct 关键词去重。
  • From 指定查询范围,一般指定类名、正则表达式、对象地址。
  • Where 用来指定筛选条件。
  • 全部语法详见:OQL 语法
  • 未支持的核心功能:group by value,如果有需求可以先导出结果到 csv 中,再使用 awk 等脚本工具分析即可。
  • 例子:查找 size=0 且未使用过的 ArrayList:select * from java.util.ArrayList where size=0 and modCount=0。

使用场景

  • 一般比较复杂的问题会使用 OQL,而且这类问题往往与业务逻辑有较大关系。比如大量的小对象整体占用内存高,但预期小对象应该不会过多(比如达到百万个),一个一个看又不现实,可以采用 OQL 查询导出数据排查。
  • 例子:微服务的分布式链路追踪系统,采集各服务所有接口名,共计200个服务却采集到了200万个接口名(一个服务不会有1万个接口),这时直接在 List 中一个个查看很难定位,可以直接用 OQL 导出,定位哪个服务接口名收集异常(如把 URL 中 ID 也统计到接口中了)

5.2 检索及筛选

功能:本文第二章内存分布,第三章对象间依赖的众多功能,均支持按字符串检索、按正则检索等操作。

使用场景:在使用 Histogram、Thread overview 等功能时,可以进一步添加字符串匹配、正则匹配条件过滤缩小排查范围。

5.3 按地址寻址

功能:根据对象的虚拟内存十六进制地址查找对象。

使用场景:仅知道地址并希望快速查看对象做后续分析时使用,其余可以直接使用 outgoing reference 了解对象信息。

5.4 综合案例四

使用工具项:OQL、Histogram、incoming references

异常现象及目的 :程序占用内存高,存在默认初始化较长的 ArrayList,需分析 ArrayList 被使用的占比,通过数据支撑是否采用懒加载模式,并分析具体哪块代码创建了空 ArrayList。

程序代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class EmptyListDemo {
public static void main(String[] args) {
EmptyValueContainerList emptyValueContainerList = new EmptyValueContainerList();
FilledValueContainerList filledValueContainerList = new FilledValueContainerList();
System.out.println("start sleep...");
try {
Thread.sleep(50 * 1000 * 1000);
} catch (Exception e) {
System.exit(1);
}
}
}

class EmptyValueContainer {
List<Integer> value1 = new ArrayList(10);
List<Integer> value2 = new ArrayList(10);
List<Integer> value3 = new ArrayList(10);
}

class EmptyValueContainerList {
List<EmptyValueContainer> list = new ArrayList(500 * 1000);

public EmptyValueContainerList() {
for (int i = 0; i < 500 * 1000; i++) {
list.add(new EmptyValueContainer());
}
}
}

class FilledValueContainer {
List<Integer> value1 = new ArrayList(10);
List<Integer> value2 = new ArrayList(10);
List<Integer> value3 = new ArrayList(10);

public FilledValueContainer init() {
value1.addAll(Arrays.asList(1, 3, 5, 7, 9));
value2.addAll(Arrays.asList(2, 4, 6, 8, 10));
value1.addAll(Arrays.asList(1, 1, 1, 1, 1, 1, 1, 1, 1, 1));
return this;
}
}

class FilledValueContainerList {
List<FilledValueContainer> list = new ArrayList(500);

public FilledValueContainerList() {
for (int i = 0; i < 500; i++) {
list.add(new FilledValueContainer().init());
}
}
}

分析过程

  1. 内存中有50万个 capacity = 10 的空 ArrayList 实例。我们分析下这些对象的占用内存总大小及对象创建位置,以便分析延迟初始化(即直到使用这些对象的时候才将之实例化,否则一直为null)是否有必要。
  2. 使用 OQL 查询出初始化后未被使用的 ArrayList(size=0 且 modCount=0),语句如下图。可以看出公有 150 万个空 ArrayList,这些对象属于浪费内存。我们接下来计算下总计占用多少内存,并根据结果看是否需要优化。
  3. 计算 150万 ArrayList占内存总量,直接点击右上方带黄色箭头的 Histogram 图标,这个图标是在选定的结果再用直方图展示,总计支配了120M 左右内存(所以这里点击结果,不包含 modCount 或 size 大于0的 ArrayList 对象)。这类在选定结果继续分析很多功能都支持,如正则检索、Histogram、Dominator tree等等。
  4. 查看下空 ArrayList 的具体来源,可用 incoming references,下图中显示了清晰的对象创建路径。

总结展望

至此本文讲解了 MAT 各项工具的功能、使用方法、适用场景,也穿插了4个实战案例,熟练掌握对分析 JVM 内存问题大有裨益,尤其是各种功能的组合使用。在下一篇《JVM 内存分析工具 MAT 的深度讲解与实践——高阶篇》会总结 JVM 堆内存分析的系统性方法,并在更复杂的案例中实践。

参考内容

  • MAT官网
  • 10 Tips for using the Eclipse Memory Analyzer
  • Finding Memory Leaks with SAP Memory Analyzer
  • An effective way to fight duplicated libs and version conflicting classes using Memory Analyzer Tool
  • Memory for nothing(Empty collection problem)

欢迎转发、关注,笔者微信公众号:Q的博客。 不定期发送干货,实践经验、系统总结、源码解读、技术原理。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MyBatis 使用 example 类

发表于 2020-12-29

MyBatis 的 Example

在 逆向工程 中,我们可以根据数据库的表自动生产 MyBatis 所需的 mapper.java、mapper.xml、po.java、poExample.java。前三个我们已经非常熟悉了,而未使用过 poExample 类,直到最近在项目中碰到了,在这里总结一下。

Example

Example类指定了如何构建一个动态的 where 子句. 表中的每个表的列可以被包括在 where 子句中。主要用于简化我们 sql 语句的编写。

Example类包含一个内部静态类 Criteria,而 Criteria中 的方法是定义 SQL 语句 where 后的查询条件。Criteria 对象可以通过 example 对象的 createCriteria 方法创建。

要生成 Example,需要注意下 generatorConfig.xml 里的配置:

1
2
3
4
5
6
7
ini复制代码    <table tableName="user" domainObjectName="User">
<!--
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false">
-->
</table>

Criteria 中的常用方法

案例

  1. 使用 criteria:
1
2
3
4
5
6
ini复制代码UserExample example =new UserExample();//由逆向工程生成
Criteria criteria = example.createCriteria();
criteria.setOrderByClause("id des");//按 id 字段降序
criteria.andUsernameEqualTo("zhangsan");
List<User> list = userMapper.selectByExample(example);
// sql:select * from t_user where name=zhangsan order by id des
  1. 不使用 criteria:
1
2
3
4
5
ini复制代码UserExample example = new UserExample(); 
example.and()
.andEqualTo("id", id)
.andEqualTo("name", name); //example.and()底层还是返回的criteria
List<User> list = userMapper.selectByExample(example);
  1. and 和 or
1
2
3
4
5
6
7
8
ini复制代码UserExample example = new UserExample(); 
Criteria cri1 = example.createCriteria();
Criteria cri2 = example.createCriteria();
cri1.andIn("id", ids);
cri2.orLike("des", "%" + des + "%");
cri2.orLike("name", "%" + name + "%");
example.and(cri2);//example.or(cri2);
// where (id in ids) and (name like %des% or des like %name%)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

连夜整理七个开源项目:练手/毕设/私活都不愁了 项目一:cl

发表于 2020-12-29

来源:mp.weixin.qq.com/s/HdRumApwO…

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目一:cloud-platform

学习重点:

  • 服务鉴权中心
  • 用户间鉴权
  • 服务之间鉴权
  • springcloud组件大回顾

图文笔记:

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目二:Guns

学习重点:

  • map+warpper模式
  • Api数据传输安全
  • 数据范围限定
  • 多数据源、jwt

图文笔记:

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目三:bootshiro

学习重点:

  • restful接口设计
  • 前后端分离
  • 数据传输动态密钥加密
  • jwt过期自动刷新

图文讲解:

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目四:vueblog

学习重点:

  • 如何搭建一个脚手架
  • 前后端分离如何对接
  • 如何开发Vue+element-ui项目
  • 从0到1开发一个项目的完整教程

图文讲解:

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目五:renren-fast

学习重点:

  • 项目技术框架分析
  • 前后端分离-token机制
  • 安全防范模块–预防xss攻击与sql注入
  • 多数据源的使用分析总结
  • 如何Docker部署项目

图文文档目录:

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目六:miaosha

学习重点:

  • 秒杀系统场景特点与设计要点分析
  • 高并发优化方向
  • 秒杀限流处理
  • 灵活使用redis五种数据类型
  • mysql的存储过程
  • 使用高并发测试,jmeter工具的使用

图文文档:

连夜整理七个开源项目:练手/毕设/私活都不愁了

项目七:eblog

学习重点:

  • 自定义Freemarker标签
  • redis的zset结构完成本周热议排行榜
  • t-io+websocket完成即时消息通知和群聊
  • rabbitmq+elasticsearch实现搜索引擎

七大开源项目地址感兴趣的可以关注我点击:

七大开源项目获取传送门 备注“掘金”即可

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JAVA与GO语言哪个更容易学?

发表于 2020-12-29

一,GO语言的优劣势

Go开发中的痛点

编译慢,失控的依赖,个工程师只是用了一个语言里面的一部分,程序难以维护(可读性差、文档不清晰等),更新的花费越来越长,交叉编译困难

Go语言的优势

学习曲线容易MGo语言语法简单,包含了类C语法。效率: 快速的编译时间,开发效率和运行效率高,自由高效: 组合的思想、无侵入式的接口,强大的标准库.

二,GO与java的比较

编译语言,速度适中(2.67s),目前的大型网站都是拿java写的,比如淘宝、京东等。 主要特点是稳定,开源性好,具有自己的一套编写规范,开发效率适中,目前最主流的语言。作为编程语言中的大腕。 具有最大的知名度和用户群。 无论风起云涌,我自巍然不动。 他强任他强,清风拂山岗; 他横由他横,明月照大江。

三,综合建议

go语言虽然有很多很强大的特性,但是由于推出时间相对不久,一些坑没有填,再加上各种库比较少,造成开发成本较高,不如java那样成熟,所以目前岗位不多,因为敢第一个吃螃蟹的人比较少。可以先学java就业,然后再利用业余时间学go,目前是java领先,未来go的前景会很不错。

零基础学习Java,推荐加入我的Java学习园地。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

linux 安装mysql80x

发表于 2020-12-29

1: 在 /usr/local下 创建mysql文件夹:

1
arduino复制代码mkdir mysql

2: 切换到mysql文件夹下:

1
bash复制代码cd mysql

3: 下载mysql:

1
bash复制代码wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz

4: 解压mysql:

1
复制代码tar xvJf mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz

5: 重命名文件夹:

1
bash复制代码mv mysql-8.0.20-linux-glibc2.12-x86_64 mysql-8.0

6: 进入mysql-8.0文件夹:

1
bash复制代码cd mysql-8.0

7: 创建data文件夹 存储文件

1
arduino复制代码mkdir data

8: 创建用户组以及用户和密码

1
2
复制代码groupadd mysql
useradd -g mysql mysql

9: 授权用户:

1
bash复制代码chown -R mysql.mysql /usr/local/mysql/mysql-8.0

10: 切换到bin目录下:

1
bash复制代码cd bin

11: 初始化基础信息(并获得密码):

1
2
3
4
5
6
7
bash复制代码    ./mysqld --user=mysql --basedir=/usr/local/mysql/mysql-8.0 --datadir=/usr/local/mysql/mysql-8.0/data/ --initialize

如果报错提示:
./mysqld: error while loading shared libraries: libaio.so.1: cannot open shared object file: No such file or directory
安装libaio:
yum install -y libaio
再次执行初始化

12: 编辑my.cnf文件:

1
bash复制代码vim /etc/my.cnf

1
2
3
4
5
6
7
8
ini复制代码[mysqld]
basedir=/usr/local/mysql/mysql-8.0/

datadir=/usr/local/mysql/mysql-8.0/data/

socket=/tmp/mysql.sock

character-set-server=UTF8MB4

13: 添加mysqld服务到系统:

1
bash复制代码cp -a ../support-files/mysql.server /etc/init.d/mysql

14: 授权以及添加服务

1
2
3
bash复制代码 chmod +x /etc/init.d/mysql

chkconfig --add mysql

15: 启动mysql

1
sql复制代码service mysql start

16: 查看启动状态

1
lua复制代码service mysql status

17: 将mysql命令添加到服务

1
bash复制代码ln -s /usr/local/mysql/mysql-8.0/bin/mysql /usr/bin

18: 登录mysql

1
2
3
4
5
6
7
8
9
10
11
bash复制代码mysql -uroot -p 

回车后输入第11步骤生成的随机密码

2022.5.24本人用腾讯云 centos8 复现评论区错误:

mysql: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory

执行下面操作即可:

cp /lib64/libtinfo.so.6 /lib64/libtinfo.so.5

19: 修改root密码

1
sql复制代码ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; 其中123456是新的密码自己设置

20: 使密码生效

1
arduino复制代码 flush privileges;

21: 选择mysql数据库

1
ini复制代码use mysql;

22: 修改远程连接并生效

1
2
3
sql复制代码update user set host='%' where user='root';

flush privileges;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用registry镜像构建Docker本地私人镜像仓库

发表于 2020-12-28

背景

DockerHub上为我们提供了许多官方镜像,我们可以从DockerHub中上传或者下载镜像,但是:

  • 由于网络的限制,会导致镜像的上传和下载速度较慢;
  • 生产使用的镜像中包含了许多隐私信息,若放到DockerHub上容易被外部人员获取。

为解决上述问题,官方提供了registry镜像,用于搭建本地私人镜像仓库,在内网中搭建Docker私有仓库可以让镜像仅允许内网人员下载,并且上传与下载速度也较快。

优点

  • 可限制外部人员访问
  • 上传下载速度快,不受外网带宽影响
  • 支持仓库认证
  • …

环境

  • 10.0.95.63 主机(暂时使用个人电脑作为私人镜像仓库服务器)KFDockerRegistry
  • 统一使用5566端口

注意

上传:

  • 需先登录到私有仓库:docker login 10.0.95.63:5566,然后输入账号密码;
  • 上传的镜像名称前需有私有仓库标识:10.0.95.63:5566,如10.0.95.63:5566/nginx:latest;
  • 上传完成后需注销登录:docker logout。

下载:

  • 下载的镜像名称前需有私有仓库标识:10.0.95.63:5566,如10.0.95.63:5566/nginx:latest。

搭建私有仓库

  1. 拉取私有仓库镜像
1
bash复制代码docker pull registry
  1. 修改Docker配置

修改daemon.json文件:vi /etc/docker/daemon.json,添加以下内容,用于让Docker信任私有仓库地址(==需要访问私有仓库的Docker客户端都需配置以下内容==):

1
2
3
4
5
json复制代码{
"insecure-registries": [
"10.0.95.63:5566"
]
}

若没有配置,则可能出现以下错误:

1
2
bash复制代码X509: cannot validate certificate for 10.0.95.63 because it does not contain any IP SANs
Get https://10.0.95.63:5566/v2/: http: server gave HTTP response to HTTPS client
  1. 重新加载配置以及重启Docker服务
1
2
bash复制代码sudo systemctl daemon-reload
sudo systemctl restart docker
  1. 重启完成后,即可运行私有仓库容器
1
bash复制代码docker run -id -p 5566:5000 --name registry -v /media/mes/file2/docker_registry:/var/lib/registry registry

其中:

-d:后台运行容器;

--name:为容器命名;

-p:映射端口,将本地的5566端口映射到容器的5000端口;

-v:将容器中的/var/lib/registry目录挂载到本地的/media/mes/file2/docker_registry目录;
5. 使用浏览器访问路径:http://10.0.95.63:5566/v2/_catalog,浏览器显示{"repositories":[]}则为搭建成功
6. 推送镜像到私有仓库

使用tag命令给镜像设置标签:

1
bash复制代码docker tag nginx:latest 10.0.95.63:5566/nginx:latest

然后使用push命令推送至私有仓库

1
bash复制代码docker push 10.0.95.63:5566/nginx:latest

然后通过浏览器访问路径:http://10.0.95.63:5566/v2/_catalog,即可看到:

image-20201221155248837

也可以在挂载的目录上查看到上传的镜像信息:

image-20201221155510351

配置私有仓库认证

为提高私有仓库安全性,设置一个安全认证证书

  1. 创建证书存储目录
1
bash复制代码sudo mkdir -p /usr/local/registry/certs
  1. 生成证书
1
bash复制代码sudo openssl req -newkey rsa:2048 -nodes -sha256 -keyout /usr/local/registry/certs/domain.key -x509 -days 365 -out /usr/local/registry/certs/domain.crt

其中:

openssl req:创建证书签名请求等功能;

-newkey:创建CSR证书签名文件和RSA私钥文件;

rsa:2048:指定创建的RSA私钥长度为2048;

-nodes:对私钥不进行加密;

-sha256:使用SHA256算法;

-keyout:创建的私钥文件名称及位置;

-x509:自签发证书格式;

-days:证书有效期;

-out:指定CSR输出文件名称及位置;

image-20201221160455526
3. 生成鉴权密码文件

1
2
3
4
5
6
7
bash复制代码# 创建存储鉴权密码文件目录
sudo mkdir -p /usr/local/registry/auth
# 安装httpd,这里选择apache2
sudo apt-get install apache2
# 创建用户和密码
sudo chmod -R 777 /usr/local/registry/auth
sudo htpasswd -Bbn root mes_2020 > /usr/bin/registry/auth/htpasswd
  1. 运行私有仓库容器
1
2
3
4
5
6
7
8
9
10
bash复制代码docker run -id --name registry -p 5566:5000 \
-v /mydata/docker_registry:/var/lib/registry \
-v /usr/local/registry/certs:/certs \
-v /usr/local/registry/auth:/auth \
-e "REGISTRY_AUTH=htpasswd" \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry
  1. 推送10.0.95.63:5566/nginx:latest到私有仓库会提示no basic auth credentials;

image-20201221161433349
6. 登录并上传

通过docker login命令登录私有仓库:

1
bash复制代码docker login 10.0.95.63:5566

然后推送镜像到私有仓库:

1
bash复制代码docker push 10.0.95.63:5566/nginx:latest
  1. 退出账号
1
bash复制代码docker logout 10.0.95.63:5566

参考文档:

  • Docker私有镜像仓库的搭建和认证

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JAVA中 ReentrantReadWriteLock读写

发表于 2020-12-28

一、读写锁简介

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

 针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁,描述如下:

线程进入读锁的前提条件:

没有其他线程的写锁,

没有写请求或者有写请求,但调用线程和持有锁的线程是同一个。

线程进入写锁的前提条件:

没有其他线程的读锁

没有其他线程的写锁

而读写锁有以下三个重要的特性:

(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。

(2)重进入:读锁和写锁都支持线程重进入。

(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。

二、源码解读

我们先来看下 ReentrantReadWriteLock 类的整体结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
scala复制代码public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {

/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;

/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;

final Sync sync;

/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}

/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }

/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }


abstract static class Sync extends AbstractQueuedSynchronizer {}

static final class NonfairSync extends Sync {}

static final class FairSync extends Sync {}

public static class ReadLock implements Lock, java.io.Serializable {}

public static class WriteLock implements Lock, java.io.Serializable {}
}

1、类的继承关系

1
2
java复制代码public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {}

说明:可以看到,ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的规范,具体需要实现类去实现;同时其还实现了Serializable接口,表示可以进行序列化,在源代码中可以看到ReentrantReadWriteLock实现了自己的序列化逻辑。

2、类的内部类

ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

file
说明:如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类(通过构造函数传入的布尔值决定要构造哪一种Sync实例);ReadLock实现了Lock接口、WriteLock也实现了Lock接口。

Sync类:

(1)类的继承关系

1
scala复制代码abstract static class Sync extends AbstractQueuedSynchronizer {}

说明:Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。

(2)类的内部类

Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用,其中,HoldCounter源码如下。

1
2
3
4
5
6
7
8
arduino复制代码// 计数器
static final class HoldCounter {
// 计数
int count = 0;
// Use id, not reference, to avoid garbage retention
// 获取当前线程的TID属性的值
final long tid = getThreadId(Thread.currentThread());
}

说明:HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。ThreadLocalHoldCounter的源码如下

1
2
3
4
5
6
7
8
csharp复制代码// 本地线程计数器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}

说明:ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象。

(3)类的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本序列号
private static final long serialVersionUID = 6317671515068378041L;
// 高16位为读锁,低16位为写锁
static final int SHARED_SHIFT = 16;
// 读锁单位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读锁最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁最大数量
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 本地线程计数器
private transient ThreadLocalHoldCounter readHolds;
// 缓存的计数器
private transient HoldCounter cachedHoldCounter;
// 第一个读线程
private transient Thread firstReader = null;
// 第一个读线程的计数
private transient int firstReaderHoldCount;
}

说明:该属性中包括了读锁、写锁线程的最大量。本地线程计数器等。

(4)类的构造函数

1
2
3
4
5
6
7
scss复制代码// 构造函数
Sync() {
// 本地线程计数器
readHolds = new ThreadLocalHoldCounter();
// 设置AQS的状态
setState(getState()); // ensures visibility of readHolds
}

说明:在Sync的构造函数中设置了本地线程计数器和AQS的状态state。

3、读写状态的设计

同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。

读写锁对于同步状态的实现是在一个整形变量上通过“按位切割使用”:将变量切割成两部分,高16位表示读,低16位表示写。

file
假设当前同步状态值为S,get和set的操作如下:

(1)获取写状态:

S&0x0000FFFF:将高16位全部抹去

(2)获取读状态:

S>>>16:无符号补0,右移16位

(3)写状态加1:

S+1

(4)读状态加1:

  S+(1<<16)即S + 0x00010000

在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则表示该读写锁的读锁已被获取。

4、写锁的获取与释放

看下WriteLock类中的lock和unlock方法:

1
2
3
4
5
6
7
csharp复制代码public void lock() {
sync.acquire(1);
}

public void unlock() {
sync.release(1);
}

可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。

写锁的获取,看下tryAcquire:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码 1 protected final boolean tryAcquire(int acquires) {
2 //当前线程
3 Thread current = Thread.currentThread();
4 //获取状态
5 int c = getState();
6 //写线程数量(即获取独占锁的重入数)
7 int w = exclusiveCount(c);
8
9 //当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁
10 if (c != 0) {
11 // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;
12 // 如果写锁状态不为0且写锁没有被当前线程持有返回false
13 if (w == 0 || current != getExclusiveOwnerThread())
14 return false;
15
16 //判断同一线程获取写锁是否超过最大次数(65535),支持可重入
17 if (w + exclusiveCount(acquires) > MAX_COUNT)
18 throw new Error("Maximum lock count exceeded");
19 //更新状态
20 //此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。
21 setState(c + acquires);
22 return true;
23 }
24
25 //到这里说明此时c=0,读锁和写锁都没有被获取
26 //writerShouldBlock表示是否阻塞
27 if (writerShouldBlock() ||
28 !compareAndSetState(c, c + acquires))
29 return false;
30
31 //设置锁为当前线程所有
32 setExclusiveOwnerThread(current);
33 return true;
34 }

其中exclusiveCount方法表示占有写锁的线程数量,源码如下:

1
arduino复制代码static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

说明:直接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位表示。

从源代码可以看出,获取写锁的步骤如下:

(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。

(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。

(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。

(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。

(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。

方法流程图如下:

file
写锁的释放,tryRelease方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scss复制代码 1 protected final boolean tryRelease(int releases) {
2 //若锁的持有者不是当前线程,抛出异常
3 if (!isHeldExclusively())
4 throw new IllegalMonitorStateException();
5 //写锁的新线程数
6 int nextc = getState() - releases;
7 //如果独占模式重入数为0了,说明独占模式被释放
8 boolean free = exclusiveCount(nextc) == 0;
9 if (free)
10 //若写锁的新线程数为0,则将锁的持有者设置为null
11 setExclusiveOwnerThread(null);
12 //设置写锁的新线程数
13 //不管独占模式是否被释放,更新独占重入数
14 setState(nextc);
15 return free;
16 }

写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。

说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。其方法流程图如下。

file
5、读锁的获取与释放

类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。

读锁的获取,看下tryAcquireShared方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
scss复制代码 1 protected final int tryAcquireShared(int unused) {
2 // 获取当前线程
3 Thread current = Thread.currentThread();
4 // 获取状态
5 int c = getState();
6
7 //如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级
8 if (exclusiveCount(c) != 0 &&
9 getExclusiveOwnerThread() != current)
10 return -1;
11 // 读锁数量
12 int r = sharedCount(c);
13 /*
14 * readerShouldBlock():读锁是否需要等待(公平锁原则)
15 * r < MAX_COUNT:持有线程小于最大数(65535)
16 * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
17 */
18 // 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
19 if (!readerShouldBlock() &&
20 r < MAX_COUNT &&
21 compareAndSetState(c, c + SHARED_UNIT)) {
22 //r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
23 if (r == 0) { // 读锁数量为0
24 // 设置第一个读线程
25 firstReader = current;
26 // 读线程占用的资源数为1
27 firstReaderHoldCount = 1;
28 } else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入
29 // 占用资源数加1
30 firstReaderHoldCount++;
31 } else { // 读锁数量不为0并且不为当前线程
32 // 获取计数器
33 HoldCounter rh = cachedHoldCounter;
34 // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
35 if (rh == null || rh.tid != getThreadId(current))
36 // 获取当前线程对应的计数器
37 cachedHoldCounter = rh = readHolds.get();
38 else if (rh.count == 0) // 计数为0
39 //加入到readHolds中
40 readHolds.set(rh);
41 //计数+1
42 rh.count++;
43 }
44 return 1;
45 }
46 return fullTryAcquireShared(current);
47 }

其中sharedCount方法表示占有读锁的线程数量,源码如下:

1
arduino复制代码static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

说明:直接将state右移16位,就可以得到读锁的线程数量,因为state的高16位表示读锁,对应的第十六位表示写锁数量。

读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。流程图如下。

file
注意:更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(23行至43行代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。

fullTryAcquireShared方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
ini复制代码final int fullTryAcquireShared(Thread current) {

HoldCounter rh = null;
for (;;) { // 无限循环
// 获取状态
int c = getState();
if (exclusiveCount(c) != 0) { // 写线程数量不为0
if (getExclusiveOwnerThread() != current) // 不为当前线程
return -1;
} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
} else { // 当前线程不为第一个读线程
if (rh == null) { // 计数器不为空
//
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) // 读锁数量为最大值,抛出异常
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // 比较并且设置成功
if (sharedCount(c) == 0) { // 读线程数量为0
// 设置第一个读线程
firstReader = current;
//
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}

说明:在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比较设置成功)则会进行fullTryAcquireShared函数中,它用来保证相关操作可以成功。其逻辑与tryAcquireShared逻辑类似,不再累赘。

读锁的释放,tryReleaseShared方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
scss复制代码 1 protected final boolean tryReleaseShared(int unused) {
2 // 获取当前线程
3 Thread current = Thread.currentThread();
4 if (firstReader == current) { // 当前线程为第一个读线程
5 // assert firstReaderHoldCount > 0;
6 if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
7 firstReader = null;
8 else // 减少占用的资源
9 firstReaderHoldCount--;
10 } else { // 当前线程不为第一个读线程
11 // 获取缓存的计数器
12 HoldCounter rh = cachedHoldCounter;
13 if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
14 // 获取当前线程对应的计数器
15 rh = readHolds.get();
16 // 获取计数
17 int count = rh.count;
18 if (count <= 1) { // 计数小于等于1
19 // 移除
20 readHolds.remove();
21 if (count <= 0) // 计数小于等于0,抛出异常
22 throw unmatchedUnlockException();
23 }
24 // 减少计数
25 --rh.count;
26 }
27 for (;;) { // 无限循环
28 // 获取状态
29 int c = getState();
30 // 获取状态
31 int nextc = c - SHARED_UNIT;
32 if (compareAndSetState(c, nextc)) // 比较并进行设置
33 // Releasing the read lock has no effect on readers,
34 // but it may allow waiting writers to proceed if
35 // both read and write locks are now free.
36 return nextc == 0;
37 }
38 }

说明:此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。其流程图如下。

file
在读锁的获取、释放过程中,总是会有一个对象存在着,同时该对象在获取线程获取读锁是+1,释放读锁时-1,该对象就是HoldCounter。

要明白HoldCounter就要先明白读锁。前面提过读锁的内在实现机制就是共享锁,对于共享锁其实我们可以稍微的认为它不是一个锁的概念,它更加像一个计数器的概念。一次共享锁操作就相当于一次计数器的操作,获取共享锁计数器+1,释放共享锁计数器-1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以HoldCounter的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。

先看读锁获取锁的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码if (r == 0) {//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//第一个读锁线程重入
firstReaderHoldCount++;
} else { //非firstReader计数
HoldCounter rh = cachedHoldCounter;//readHoldCounter缓存
//rh == null 或者 rh.tid != current.getId(),需要获取rh
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh); //加入到readHolds中
rh.count++; //计数+1
}

这里为什么要搞一个firstRead、firstReaderHoldCount呢?而不是直接使用else那段代码?这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。可能就看这个代码还不是很理解HoldCounter。我们先看firstReader、firstReaderHoldCount的定义:

1
2
java复制代码private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

这两个变量比较简单,一个表示线程,当然该线程是一个特殊的线程,一个是firstReader的重入计数。

HoldCounter的定义:

1
2
3
4
arduino复制代码static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}

在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。

诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:

1
2
3
4
5
6
7
csharp复制代码static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
ThreadLocalHoldCounter继承ThreadLocal,并且重写了initialValue方法。

故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。

三、总结

通过上面的源码分析,我们可以发现一个现象:

在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。

在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。

仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。

综上:

一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

来源:www.cnblogs.com/xiaoxi/
欢迎关注公众号 【码农开花】一起学习成长
我会一直分享Java干货,也会分享免费的学习资料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

强大:MyBatis ,三种流式查询方法

发表于 2020-12-28

关于MyBatis的知识点总结了个思维导图分享给大家

基本概念

流式查询指的是查询成功后不是返回一个集合而是返回一个迭代器,应用每次从迭代器取一条查询结果。流式查询的好处是能够降低内存使用。

如果没有流式查询,我们想要从数据库取 1000 万条记录而又没有足够的内存时,就不得不分页查询,而分页查询效率取决于表设计,如果设计的不好,就无法执行高效的分页查询。因此流式查询是一个数据库访问框架必须具备的功能。

流式查询的过程当中,数据库连接是保持打开状态的,因此要注意的是:执行一个流式查询后,数据库访问框架就不负责关闭数据库连接了,需要应用在取完数据后自己关闭。

MyBatis 流式查询接口

MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeable 和 java.lang.Iterable 接口,由此可知:

  1. Cursor 是可关闭的;
  2. Cursor 是可遍历的。

除此之外,Cursor 还提供了三个方法:

  1. isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  2. isConsumed():用于判断查询结果是否全部取完。
  3. getCurrentIndex():返回已经获取了多少条数据

因为 Cursor 实现了迭代器接口,因此在实际使用当中,从 Cursor 取数据非常简单:

1
scss复制代码cursor.forEach(rowObject -> {...});

但构建 Cursor 的过程不简单

我们举个实际例子。下面是一个 Mapper 类:

1
2
3
4
5
less复制代码@Mapper
public interface FooMapper {
@Select("select * from foo limit #{limit}")
Cursor<Foo> scan(@Param("limit") int limit);
}

方法 scan() 是一个非常简单的查询。通过指定 Mapper 方法的返回值为 Cursor 类型,MyBatis 就知道这个查询方法一个流式查询。

然后我们再写一个 SpringMVC Controller 方法来调用 Mapper(无关的代码已经省略):

1
2
3
4
5
6
less复制代码@GetMapping("foo/scan/0/{limit}")
public void scanFoo0(@PathVariable("limit") int limit) throws Exception {
try (Cursor<Foo> cursor = fooMapper.scan(limit)) { // 1
cursor.forEach(foo -> {}); // 2
}
}

上面的代码中,fooMapper 是 @Autowired 进来的。注释 1 处调用 scan 方法,得到 Cursor 对象并保证它能最后关闭;2 处则是从 cursor 中取数据。

上面的代码看上去没什么问题,但是执行 scanFoo0() 时会报错:

1
csharp复制代码java.lang.IllegalStateException: A Cursor is already closed.

这是因为我们前面说了在取数据的过程中需要保持数据库连接,而 Mapper 方法通常在执行完后连接就关闭了,因此 Cusor 也一并关闭了。

所以,解决这个问题的思路不复杂,保持数据库连接打开即可。我们至少有三种方案可选。

方案一:SqlSessionFactory

我们可以用 SqlSessionFactory 来手工打开数据库连接,将 Controller 方法修改如下:

1
2
3
4
5
6
7
8
9
10
less复制代码@GetMapping("foo/scan/1/{limit}")
public void scanFoo1(@PathVariable("limit") int limit) throws Exception {
try (
SqlSession sqlSession = sqlSessionFactory.openSession(); // 1
Cursor<Foo> cursor =
sqlSession.getMapper(FooMapper.class).scan(limit) // 2
) {
cursor.forEach(foo -> { });
}
}

上面的代码中,1 处我们开启了一个 SqlSession (实际上也代表了一个数据库连接),并保证它最后能关闭;2 处我们使用 SqlSession 来获得 Mapper 对象。这样才能保证得到的 Cursor 对象是打开状态的。

方案二:TransactionTemplate

在 Spring 中,我们可以用 TransactionTemplate 来执行一个数据库事务,这个过程中数据库连接同样是打开的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码@GetMapping("foo/scan/2/{limit}")
public void scanFoo2(@PathVariable("limit") int limit) throws Exception {
TransactionTemplate transactionTemplate =
new TransactionTemplate(transactionManager); // 1

transactionTemplate.execute(status -> { // 2
try (Cursor<Foo> cursor = fooMapper.scan(limit)) {
cursor.forEach(foo -> { });
} catch (IOException e) {
e.printStackTrace();
}
return null;
});
}

上面的代码中,1 处我们创建了一个 TransactionTemplate 对象(此处 transactionManager 是怎么来的不用多解释,本文假设读者对 Spring 数据库事务的使用比较熟悉了),2 处执行数据库事务,而数据库事务的内容则是调用 Mapper 对象的流式查询。注意这里的 Mapper 对象无需通过 SqlSession 创建。

方案三:@Transactional 注解

这个本质上和方案二一样,代码如下:

1
2
3
4
5
6
7
less复制代码@GetMapping("foo/scan/3/{limit}")
@Transactional
public void scanFoo3(@PathVariable("limit") int limit) throws Exception {
try (Cursor<Foo> cursor = fooMapper.scan(limit)) {
cursor.forEach(foo -> { });
}
}

它仅仅是在原来方法上面加了个 @Transactional 注解。这个方案看上去最简洁,但请注意 Spring 框架当中注解使用的坑:只在外部调用时生效。在当前类中调用这个方法,依旧会报错。

以上是三种实现 MyBatis 流式查询的方法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习 Git 这一篇就够了 第一章 Git 概述 第二章 G

发表于 2020-12-28

目录第一章 Git 概述 1.1、Git 概述 1.2、Git 官网 1.3、Git 安装第二章 Git 工作流程 2.1、单人开发流程 2.2、团队内部协作第三章 Git 基本操作 3.1、配置操作 3.1.1、查看当前 Git……

目录

  • 第一章 Git 概述
    • 1.1、Git 概述
      • 1.2、Git 官网
      • 1.3、Git 安装
  • 第二章 Git 工作流程
    • 2.1、单人开发流程
      • 2.2、团队内部协作
  • 第三章 Git 基本操作
    • 3.1、配置的操作
        • 3.1.1、查看当前 Git 配置
          • 3.1.2、编辑当前 Git 配置
          • 3.1.3、添加当前 Git 签名
      • 3.2、初始化操作
      • 3.3、暂存区操作
        • 3.3.1、添加文件
          • 3.3.2、删除文件
          • 3.3.3、改名文件
          • 3.3.4、查看状态
      • 3.4、本地库操作
        • 3.4.1、从暂存区提交文件
          • 3.4.2、从远程库克隆文件
          • 3.4.3、从远程库更新文件
          • 3.4.4、查看推送历史版本
      • 3.5、远程库操作
        • 3.5.1、别名设置
          • 3.5.2、推送分支
      • 3.6、工作区操作
        • 3.6.1、从本地库检出
          • 3.6.2、从远程库拉取
          • 3.6.3、比较文件差异
      • 3.7、重置各区内容
      • 3.8、历史版本回退
      • 3.9、标签签名管理
  • 第四章 Git 分支管理
    • 4.1、分支概述
      • 4.2、创建分支
      • 4.3、查看分支
      • 4.4、切换分支
      • 4.5、合并分支
      • 4.6、删除分支
      • 4.7、解决冲突

第一章 Git 概述

1.1、Git 概述

Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。

Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

Git 与常用的版本控制工具 SVN,CVS,Subversion 等不同,它采用了分布式版本库的方式,不必服务器端软件支持。

Git 不仅仅是个版本控制系统,它也是个内容管理系统 (CMS),工作管理系统等。

如果你是一个具有使用 SVN 背景的人,你需要做一定的思想转换,来适应 Git 提供的一些概念和特征。

1.2、Git 官网

官方地址:git-scm.com/

下载地址:git-scm.com/downloads

1.3、Git 安装

安装版本:Git-2.22.0-64-bit.exe

img

img

img

img

img

img

img

img

img

img

img

img

img

第二章 Git 工作流程

2.1、单人开发流程

img

2.2、团队内部协作

img

第三章 Git 基本操作

3.1、配置的操作

3.1.1、查看当前 Git 配置

1
lua复制代码git config --list

3.1.2、编辑当前 Git 配置

编辑本地仓库级别的配置文件: 仅在当前本地仓库范围有效,该文件默认在工作空间/.git/config

1
arduino复制代码git config -e

编辑系统用户级别的配置文件: 登录操作系统用户范围有效,该文件默认在~/.gitconfig

1
lua复制代码git config -e --global

3.1.3、添加当前 Git 签名

本地仓库级别 Git 签名:

1
2
arduino复制代码git config user.name [用户名称]
git config user.email [用户邮箱]

系统用户级别 Git 签名:

1
2
css复制代码git config --global user.name [用户名称]
git config --global user.email [用户邮箱]

3.2、初始化操作

1
2
3
4
5
csharp复制代码# 在当前目录创建一个本地仓库
git init

# 在当前目录创建指定名称目录,并将其初始化本地仓库
git init [仓库名称]

3.3、暂存区操作

3.3.1、添加文件

1
2
3
4
5
6
7
8
csharp复制代码# 添加指定文件到暂存区
git add [文件1] [文件2] ...

# 添加指定目录到暂存区
git add [目录名]

# 添加当前目录的所有文件到暂存区
git add .

3.3.2、删除文件

1
2
3
4
5
6
7
8
css复制代码# 普通删除工作区文件,并且将这次删除放入暂存区
git rm [文件1] [文件2] ...

# 强制删除工作区文件,并且将这次删除放入暂存区
git rm --force [文件1] [文件2] ...

# 如果想把文件从暂存区域移除,但仍然希望保留在当前工作目录中,使用 --cached 选项即可
git rm --cached [文件1] [文件2] ...

3.3.3、改名文件

1
2
ini复制代码# 改名文件,并且将这个改名放入暂存区
git mv [旧文件名] [新文件名]

3.3.4、查看状态

1
2
bash复制代码# 文件,文件夹在工作区,暂存区的状态
git status

3.4、本地库操作

3.4.1、从暂存区提交文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码# 提交暂存区到本地仓库
git commit -m [备注信息]

# 提交暂存区的指定文件到本地仓库
git commit -m [备注信息] [文件1] [文件2] ...

# 提交工作区自上次commit之后的变化到本地仓库
git commit -a

# 提交时显示所有diff信息
git commit -v

# 使用一次新的commit,替代上一次提交
# 如果文件没有任何新变化,则用来改写上一次commit的备注信息
git commit --amend -m [备注信息]

# 重做上一次commit,并包括指定文件的新变化
git commit --amend [文件1] [文件2] ...

3.4.2、从远程库克隆文件

1
2
bash复制代码# 下载远程仓库的一个项目(包括历史记录版本)到本地的当前目录下
git clone [远程仓库项目地址]

3.4.3、从远程库更新文件

1
2
ini复制代码# 下载远程仓库的所有变动
git fetch [远程仓库项目地址]

3.4.4、查看推送历史版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码# 显示当前分支的版本历史
git log

# 显示当前分支的版本历史,美化输出格式(显示全部hash值)
git log --pretty=oneline

# 显示当前分支的版本历史,美化输出格式(显示部分hash值)
git log --oneline

# 显示当前分支的版本历史,以及每次commit发生变更的文件
git log --stat

# 根据关键词搜索提交历史
git log -S [关键词]

# 显示指定次数的提交历史
git log -10

3.5、远程库操作

3.5.1、别名设置

1
2
3
4
5
6
7
8
ini复制代码# 查看当前所有远程地址别名
git remote -v

# 增加一个新的远程地址别名
git remote add [远程地址别名] [远程地址]

# 显示指定的远程地址的信息
git remote show [远程地址别名]

3.5.2、推送分支

1
2
3
4
5
6
7
8
9
10
11
ini复制代码# 上传本地指定分支到远程仓库
git push [远程地址别名] [本地分支名]

# 强行推送当前分支到远程仓库,即使有冲突
git push --force [远程地址别名]

# 推送所有分支到远程仓库
git push --all [远程地址别名]

# 删除远程分支
git push [远程地址别名] --delete [远程分支名]

3.6、工作区操作

3.6.1、从本地库检出

1
2
3
4
5
6
7
8
ini复制代码# 恢复暂存区的指定文件到工作区
git checkout [文件1] [文件2] ...

# 恢复某个commit的指定文件到暂存区和工作区
git checkout [commithash] [文件1] [文件2] ...

# 恢复暂存区的所有文件到工作区
git checkout .

3.6.2、从远程库拉取

1
2
ini复制代码# 取回远程仓库的变化,并与本地分支合并
git pull [远程地址别名] [本地分支名]

3.6.3、比较文件差异

1
2
3
4
5
6
7
8
ini复制代码# 将工作区中的文件和暂存区进行比较
git diff [文件名]

# 将工作区中的文件和本地库历史记录比较
git diff [commithash] [文件名]

# 查看未提交的暂存
git diff --cached

3.7、重置各区内容

1
2
3
4
5
6
7
8
css复制代码# 用commithash的内容重置HEAD内容
git reset --soft [commithash]

# 用commithash的内容重置HEAD内容,重置暂存区
git reset --mixed [commithash]

# 用commithash的内容重置HEAD内容,重置暂存区,重置工作目录
git reset --hard [commithash]

3.8、历史版本回退

  • 基于哈希索引值的操作
    • git reset --hard [commithash]
    • 注意:该方法既可以前进,也可以后退,是推荐使用的方法
  • 使用 ^ 符号:只能后退
    • git reset --hard HEAD^
    • 注意:一个 ^ 表示后退一步,n 个表示后退 n 步
  • 使用~ 符号:只能后退
    • git reset --hard HEAD~n
    • 注意:表示后退 n 步

3.9、标签签名管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码# 列出所有tag
git tag

# 查看tag信息
git show [tag]

# 新建一个tag在当前commit
git tag [tag]

# 新建一个tag在指定commit
git tag [tag] [commithash]

# 删除本地tag
git tag -d [tag]

# 提交指定tag
git push [远程地址别名] [tag]

# 删除远程tag
git push [远程地址别名] :refs/tags/[tagName]

# 提交所有tag
git push [远程地址别名] --tags

第四章 Git 分支管理

4.1、分支概述

几乎所有的版本控制系统都以某种形式支持分支。

使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。

在很多版本控制系统中,这是一个略微低效的过程,常常需要完全创建一个源代码目录的副本。

对于大项目来说,这样的过程会耗费很多时间。

有人把 Git 的分支模型称为它的 “必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。

为何 Git 的分支模型如此出众呢?

Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。

与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。

理解和精通这一特性,你便会意识到 Git 是如此的强大而又独特,并且从此真正改变你的开发方式。

4.2、创建分支

1
2
ini复制代码# 创建指定名称的本地分支
git branch [分支名]

4.3、查看分支

1
2
3
4
5
6
7
8
9
10
11
bash复制代码# 列出所有本地分支
git branch

# 列出所有本地分支以及每个分支最新的版本
git branch -v

# 列出所有远程分支
git branch -r

# 列出所有本地分支和远程分支
git branch -a

4.4、切换分支

1
2
3
4
5
6
7
8
ini复制代码# 切换到指定的分支
git checkout [分支名]

# 切换到上一个分支
git checkout -

# 新建一个本地分支,指向某个tag
git checkout -b [新建分支名称] [tag]

4.5、合并分支

1
2
3
4
5
ini复制代码# 第一步:切换到被合并分支上
git checkout [被合并分支名]

# 第二步:执行 merge 命令
git merge [要合并分支名]

4.6、删除分支

1
2
ini复制代码# 删除本地分支
git branch -d [本地分支名]

4.7、解决冲突

冲突的表现:

img

冲突的解决:

  • 第一步:编辑文件,删除特殊符号
  • 第二步:把文件修改到满意的程度,保存退出
  • 第三步:git add [文件名]
  • 第四步:git commit -m “日志信息”

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…746747748…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%