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

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


  • 首页

  • 归档

  • 搜索

JVM类加载机制-类加载的时机,类加载的过程,双亲委派模型与

发表于 2020-05-09

本文为《深入理解Java虚拟机》一书第七章的读书记录笔记,同时伴着一些网络上资料的整理。

  1. 概述

JVM虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被JVM直接使用的Java类型,这就是JVM的类加载机制。

Java语言里面,类的加载、连接和初始化过程都是在程序运行期间完成的。

  1. 类加载的时机

从类被加载进内存开始直到卸载出内存为止,类的生命周期包括装载、验证、准备、解析、初始化、使用和卸载7个过程。

  1. 什么情况下需要开始类加载过程的第一个阶段:加载?这个Java虚拟机规范中并没有进行强制约束,可以交给虚拟机的具体实现来自由把握。
  2. 但是,对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(加载、验证、准备则在此之前就已经完成):

(1) 遇到new,getstatic,putstatic或者invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发器初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态变量的时候(被final修饰、已在编译期把结果放入常量池的静态常量字段除外)以及调用一个类的静态方法的时候。

(2) 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行初始化,则需要先触发其初始化。

(3) 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。

(4) 当虚拟机启动时,用户需要制定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

(5) 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上5种场景的行为称为对一个类进行主动引用。除此之外,所有其他引用类的方式都不会触发初始化,称为被动引用。常见的被动引用的例子包括:

(1) 通过子类引用父类的静态变量,不会导致子类初始化。
System.out.println(SubClass.staticValue);

(2) 通过数组定义来引用类,不会触发此类的初始化。
SuperClass[] arr = new SuperClass[10];

(3) 静态常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的。
System.out.println(ConstClass.finalStaticValue);

  1. 类加载的过程

JVM类加载的全过程包括:加载、验证、准备、解析和初始化。

3.1 加载

在加载阶段,JVM需要完成以下3件事:

  1. 通过一个类的全限定名(如:java.lang.String)来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class的对象实例,作为方法区在这个类的各种数据访问的入口。

3.2 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。从整体上看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、
元数据验证、字节码验证、符号引用验证。

3.2.1 文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。

3.2.2 元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范
的要求,这个阶段可能包括的验证点如下:

  1. 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
  2. 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
    等等

3.2.3 字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

3.2.4 符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将
在连接的第三阶段:解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中
的各种符号引用)的信息进行匹配性校验。

3.3 准备

准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。

  1. 进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。
  2. 这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
1
复制代码public static int value=123;

​ 那么,变量value在准备阶段过后的值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法()之中,所以把value赋值为123的动作将在初始化阶段才会执行。

  1. “特殊情况”是:当类字段的字段属性是ConstantValue时(比如静态常量),会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。
1
复制代码public static final intvalue =123;

3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  1. 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。引用的目标并不一定已经加载到内存中。
  2. 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

3.5 初始化

初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。

  1. 初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
1
2
3
4
5
6
复制代码    public classTest{
static {
i =0;// 给变量赋值可以正常编译通过System.out.print(i);// 这句编译器会提示“非法向前引用”
}
static int i =1;
}
  1. <clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下代码中,字段B的值将会是2而不是1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    static class Parent {
public static int A = 1;
static {
A = 2;
}
}

static class Sub extends Parent {
public static int B = A;
}

public static void main (String[] args){
System.out.println(Sub.B); // 输出结果是父类中的静态变量A的值,也就是2。
}
  1. <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  2. 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  3. 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞[2],在实际应用中这种阻塞往往是很隐蔽的。
  1. 双亲委派模型

4.1 类与类加载器

  1. 类加载器:实现类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”的动作的代码模块称为”类加载器”。虚拟机设计团队把这个动作放到Java虚拟机外部去实现,以便让应用程序自己觉得如何去获取所需要的类。
  2. 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

4.2 类加载器的种类

从Java虚拟机的角度,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分。
  2. 另一种就是所有其他类的加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。

从Java开发人员角度,类加载器还可以分为3种系统提供的类加载器和用户自定义类加载器。

  1. 启动类加载器(Bootstrap ClassLoader):负责加载存放<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的类。
  2. 扩展类加载器(Extension ClassLoader): 这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  3. 应用程序类加载器(Application ClassLoader): 这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
  4. 自定义类加载器(User ClassLoader): 用户自定义的类加载器。用户在编写自己定义的类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。
  • 如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑)。
  • 要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。

这些类加载器之间的关系一般如下图所示:

4.3 双亲委派模型

上图展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。

  1. 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。
  2. 类加载器的双亲委派模型在JDK 1.2被引入,当它并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器的实现方式。

双亲委派模型的工作过程是:

  1. 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
  2. 每一个层次的类加载器都是如此,因此所有的类加载请求最终都是应该传送到顶层的类加载器中。
  3. 只有父加载器反馈自己无法完成这个加载请求,子加载器才会尝试自己去加载。

双亲委派模型可以用来解释一个问题:为什么不能自定义java.lang.String类?

回答:通过双亲委派模型我们知道: 如果用户自定义的类加载器收到了加载java.lang.String类的请求并且java.lang.String类还未加载,那么自定义的类加载器会将加载请求委派给父加载器,直到被委派到启动类加载器(Bootstrcp ClassLoader)。可启动类加载器并不认识用户自定义的java.lang.String类,它只会加载JDK中的java.lang.String类。

4.4 破坏双亲委派模型

上文提到过双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外,到目前为止,双亲委派模型主要出现过3较大规模的“被破坏”情况。

  1. 为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的loadClass()。
  2. 第二次破坏双亲委派模型是由于这个模型自身的缺陷导致,它无法解决解决基础类又要调用用户代码的问题。

(1)是JNDI服务(Java Naming and Directory Interface),JNDI的代码由启动类加载器去加载,但JNDI的目的是对资源进行集中管理和查找,它需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但是启动类加载器可能不认识这些代码。于是Java设计团队引入了:线程上下文类加载器(Thread Context ClassLoader)。JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这破坏了双亲委派模型。

(2) 比如tomcat,根据Java Servlet规范,要求Web应用自己的类优先级高于Web容器提供的类。tomcat对于一些未加载的非基础类,各个Web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委派模型。

  1. 双亲委派模型的第三次“被破坏”是由于用户对程序的动态性的追求导致的。为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。例如OSGi(Open Service Gateway Initiative)的出现。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。

更多内容

之前写过一篇从类的生命周期深入理解Java类静态/非静态成员/函数, 结合本文食用更佳~

参考与感谢

1.Tomcat类加载器破坏双亲委派

2.双亲委派模型,类的加载机制,搞定大厂高频面试题

本文转载自: 掘金

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

【译】使用 GraphQL 的 6 个月 使用 GraphQ

发表于 2020-05-09
  • 原文地址:6 Months Of Using GraphQL
  • 原文作者:Manish Jain
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:YueYongDEV
  • 校对者:JohnieXu、cyril

使用 GraphQL 的 6 个月

在使用 GraphQL 进行了 6 个月的后端项目开发后,我开始考量该技术是否适合在开发工作中使用。

The output from my terminal

首先

GraphQL 是一种实现 API 的查询语言,也是使用现有数据完成这些查询的运行时。GraphQL 为你的 API 中的数据提供了完整且易于理解的描述,并且让用户有权决定他们所需要的东西,仅此而已。

它由 Facebook 开发,作为其移动应用程序的内部解决方案,后来向社区开放了源代码。

优点

务实的数据交换

使用 GraphQL,可以为客户需要的字段指定一个查询,不多也不少。真的就是这么简单。如果前端只需要一个人的**名字和年龄字段,直接请求相应的字段就可以了。这个人的姓氏和地址**等其他字段不会返回在请求结果中。

使用数据加载器(Dataloaders)减少网络调用

虽然 Dataloaders 不是 GraphQL 库本身的一部分,但是它的确是一个很有用的第三方库,可以用来解耦应用程序中不相关的部分,同时不会牺牲批量数据加载的性能。虽然加载器提供了一个加载各个独立值的 API,但是所有并发请求都将被合并起来才分送给你的批处理加载函数。这使你的应用程序可以安全地在整个应用程序进行数据的分发与获取。

这方面的一个例子是,从另一个称为事务服务的服务中获取人的银行信息,后端可以从事务服务中获取银行信息,然后将结果与人的**姓名和年龄**结合起来后作为结果返回。

公开数据和数据库模型之间的解耦

GraphQL 的一大优点是可以将数据库建模数据和给用户公开的数据解耦。这样,在设计持久层时,我们可以专注于该层的需求,然后分别考虑如何采取最好的方式将数据暴露给使用者。这与 dataloader 的使用密切相关,因为你可以在将数据发送给用户之前将它们组合在一起,从而使得公开数据的设计模型变得非常容易。

忘记 API 的版本控制

API 的版本控制是一个常见问题,通常一个简单的解决方案是,在相同的 API 前面添加一个v2标识。但一旦有了 GraphQL,情况就不同了。虽然你仍然可以使用相同的解决方案,但这与 GraphQL 的理念不合。官方文档明确指出你应该改进你的 API,这意味着向已有端点添加更多的字段并不会破坏原有的 API。前端仍然可以使用相同的 API 进行查询,并且可以根据需要查询新字段。这种处理方式真的很巧妙。

在与前端团队协作时,这个特性非常有用。他们可以发出请求,并添加由于设计更改而需要的新字段,而后端可以轻松地添加该字段,同时不会破坏现有的 API。

独立团队

使用 GraphQL,前端和后端可以独立工作。因为 GraphQL 具有严格的类型化架构,因此两个团队可以并行工作互不影响。首先,前端无需查看后端代码即可轻松地生成数据模型,且生成的数据模型可以直接用于创建数据查询。其次,前端可以使用模拟(mock)出来的 API 来测试代码。这样便不会阻碍前后端的开发工作,大大的提升了程序员的开发体验。

Photo by [Perry Grone](https://unsplash.com/@perrygrone?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)

缺点

并非所有的 API 都能改进

有时,会因业务或设计而产生一些变化,这需要对 API 的实现进行彻底更改。在这种情况下,你将不得不依靠旧的方式进行版本控制。

不可读的代码

由于经历了多次迭代,所以有时在使用 Dataloader 读取数据时代码会分散到多个位置,这可能很难维护。

响应时间更长

由于查询会不断发展并变得臃肿,因此有可能会延长响应时间。为避免这种情况,请确保简明扼要的响应资源。有关指导原则,请查看Github GraphQL API。

缓存

缓存 API 响应的目的主要是为了更快地从将来的请求中获取响应。与 GraphQL 不同,RESTful API 可以利用 HTTP 规范中内置的缓存。正如前面提到的,GraphQL 查询可以请求资源的任何字段,因此本质上是很难实现缓存的。

结论

我强烈建议使用 GraphQL 替代 REST API。GraphQL 所提供的灵活性绝对可以取代它的痛点。这里提到的优缺点可能并不总适用,但是探索如何借助 GraphQL 来帮助你完成项目是很值得思考的。

如果你有任何意见,请在下面回复。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

本文转载自: 掘金

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

面试官:小伙子,听说你看过ThreadLocal源码?(万字

发表于 2020-05-09

前言

Ym8V9H.png

前几天写了一篇AQS相关的文章:我画了35张图就是为了让你深入 AQS,反响不错,这次趁热打铁再写一篇ThreadLocal的文章,同样是深入原理,图文并茂。

全文共10000+字,31张图,这篇文章同样耗费了不少的时间和精力才创作完成,原创不易,感谢。

对于ThreadLocal,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下:

  • ThreadLocal的key是弱引用,那么在 threadLocal.get()的时候,发生GC之后,key是否为null?
  • ThreadLocal中ThreadLocalMap的数据结构?
  • ThreadLocalMap的Hash算法?
  • ThreadLocalMap中Hash冲突如何解决?
  • ThreadLocalMap扩容机制?
  • ThreadLocalMap中过期key的清理机制?探测式清理和启发式清理流程?
  • **ThreadLocalMap.set()**方法实现原理?
  • **ThreadLocalMap.get()**方法实现原理?
  • 项目中ThreadLocal使用情况?遇到的坑?
  • ……

上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析ThreadLocal的点点滴滴。

全文目录

  1. ThreadLocal代码演示
  2. ThreadLocal的数据结构
  3. GC 之后key是否为null?
  4. **ThreadLocal.set()**方法源码详解
  5. ThreadLocalMap Hash算法
  6. ThreadLocalMap Hash冲突
  7. **ThreadLocalMap.set()**详解
  8. 1 **ThreadLocalMap.set()**原理图解
  9. 2 **ThreadLocalMap.set()**源码详解
  10. ThreadLocalMap过期key的探测式清理流程
  11. ThreadLocalMap扩容机制
  12. **ThreadLocalMap.get()**详解
  13. 1 **ThreadLocalMap.get()**图解
  14. 2 **ThreadLocalMap.get()**源码详解
  15. ThreadLocalMap过期key的启发式清理流程
  16. InheritableThreadLocal
  17. ThreadLocal项目中使用实战
  18. 1 ThreadLocal使用场景
  19. 2 分布式TraceId解决方案

注明: 本文源码基于JDK 1.8

ThreadLocal代码演示

我们先看下ThreadLocal使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class ThreadLocalTest {
private List<String> messages = Lists.newArrayList();

public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);

public static void add(String message) {
holder.get().messages.add(message);
}

public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();

System.out.println("size: " + holder.get().messages.size());
return messages;
}

public static void main(String[] args) {
ThreadLocalTest.add("一枝花算不算浪漫");
System.out.println(holder.get().messages);
ThreadLocalTest.clear();
}
}

打印结果:

1
2
java复制代码[一枝花算不算浪漫]
size: 0

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。

ThreadLocal的数据结构

image.png

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

GC 之后key是否为null?

回应开头的那个问题, ThreadLocal 的key是弱引用,那么在 threadLocal.get()的时候,发生GC之后,key是否是null?

为了搞清楚这个问题,我们需要搞清楚Java的四种引用类型:

  • 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

接着再来看下代码,我们使用反射的方式来看看GC后ThreadLocal中的数据情况:(下面代码来源自:blog.csdn.net/thewindkee/…)

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
java复制代码public class ThreadLocalDemo {

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}

private static void test(String s,boolean isGC) {
try {
new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

结果如下:

1
2
3
4
java复制代码弱引用key:java.lang.ThreadLocal@433619b6,值:abc
弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
--gc后--
弱引用key:null,值:def

image.png

如图所示,因为这里创建的ThreadLocal并没有指向任何值,也就是没有任何引用:

1
java复制代码new ThreadLocal<>().set(s);

所以这里在GC之后,key就会被回收,我们看到上面debug中的referent=null, 如果改动一下代码:

image.png

这个问题刚开始看,如果没有过多思考,弱引用,还有垃圾回收,那么肯定会觉得是null。

其实是不对的,因为题目说的是在做 threadlocal.get() 操作,证明其实还是有强引用存在的,所以 key 并不为 null,如下图所示,ThreadLocal的强引用仍然是存在的。

image.png

如果我们的强引用不存在的话,那么 key 就会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏。

ThreadLocal.set()方法源码详解

image.png

ThreadLocal中的set方法原理如上图所示,很简单,主要是判断ThreadLocalMap是否存在,然后使用ThreadLocal中的set方法进行数据处理。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

主要的核心逻辑还是在ThreadLocalMap中的,一步步往下看,后面还有更详细的剖析。

ThreadLocalMap Hash算法

既然是Map结构,那么ThreadLocalMap当然也要实现自己的hash算法来解决散列表数组冲突问题。

1
java复制代码int i = key.threadLocalHashCode & (len-1);

ThreadLocalMap中hash算法很简单,这里i就是当前key在散列表中对应的数组下标位置。

这里最关键的就是threadLocalHashCode值的计算,ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}

每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。

这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。

我们自己可以尝试下:

YKbSGn.png

可以看到产生的哈希码分布很均匀,这里不去细纠斐波那契具体算法,感兴趣的可以自行查阅相关资料。

ThreadLocalMap Hash冲突

注明: 下面所有示例图中,绿色块Entry代表正常数据,灰色块代表Entry的key值为null,已被垃圾回收。白色块表示Entry为null。

虽然ThreadLocalMap中使用了黄金分隔数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突。

HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。

而ThreadLocalMap中并没有链表结构,所以这里不能适用HashMap解决冲突的方式了。

Ynzr5D.png

如上图所示,如果我们插入一个value=27的数据,通过hash计算后应该落入第4个槽位中,而槽位4已经有了Entry数据。

此时就会线性向后查找,一直找到Entry为null的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了Entry不为null且key值相等的情况,还有Entry中的key值为null的情况等等都会有不同的处理,后面会一一详细讲解。

这里还画了一个Entry中的key为null的数据(Entry=2的灰色块数据),因为key值是弱引用类型,所以会有这种数据存在。在set过程中,如果遇到了key过期的Entry数据,实际上是会进行一轮探测式清理操作的,具体操作方式后面会讲到。

ThreadLocalMap.set()详解

ThreadLocalMap.set()原理图解

看完了ThreadLocal hash算法后,我们再来看set是如何实现的。

往ThreadLocalMap中set数据(新增或者更新数据)分为好几种情况,针对不同的情况我们画图来说说明。

第一种情况: 通过hash计算后的槽位对应的Entry数据为空:

YuSniD.png

这里直接将数据放到该槽位即可。

第二种情况: 槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致:

image.png

这里直接更新该槽位的数据。

第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,没有遇到key过期的Entry:

image.png

遍历散列数组,线性往后查找,如果找到Entry为null的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了key值相等的数据,直接更新即可。

第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entry为null的槽位之前,遇到key过期的Entry,如下图,往后遍历过程中,一到了index=7的槽位数据Entry的key=null:

Yu77qg.png

散列数组下标为7位置对应的Entry数据key为null,表明此数据key值已经被垃圾回收掉了,此时就会执行replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。

初始化探测式清理过期数据扫描的开始位置:slotToExpunge = staleSlot = 7

以当前staleSlot开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标slotToExpunge。for循环迭代,直到碰到Entry为null结束。

如果找到了过期的数据,继续向前迭代,直到遇到Entry=null的槽位才停止迭代,如下图所示,slotToExpunge被更新为0:

YuHSMT.png

以当前节点(index=7)向前迭代,检测是否有过期的Entry数据,如果有则更新slotToExpunge值。碰到null则结束探测。以上图为例slotToExpunge被更新为0。

上面向前迭代的操作是为了更新探测清理过期数据的起始下标slotToExpunge的值,这个值在后面会讲解,它是用来判断当前过期槽位staleSlot之前是否还有过期元素。

接着开始以staleSlot位置(index=7)向后迭代,如果找到了相同key值的Entry数据:

YuHEJ1.png

从当前节点staleSlot向后查找key值相等的Entry元素,找到后更新Entry的值并交换staleSlot元素的位置(staleSlot位置为过期元素),更新Entry数据,然后开始进行过期Entry的清理工作,如下图所示:

Yu4oWT.png

向后遍历过程中,如果没有找到相同key值的Entry数据:

YuHMee.png

从当前节点staleSlot向后查找key值相等的Entry元素,直到Entry为null则停止寻找。通过上图可知,此时table中没有key值相同的Entry。

创建新的Entry,替换table[stableSlot]位置:

YuH3FA.png

替换完成后也是进行过期元素清理工作,清理工作主要是有两个方法:expungeStaleEntry()和cleanSomeSlots(),具体细节后面会讲到,请继续往后看。

ThreadLocalMap.set()源码详解

上面已经用图的方式解析了set()实现的原理,其实已经很清晰了,我们接着再看下源码:

java.lang.ThreadLocal.ThreadLocalMap.set():

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
java复制代码private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

这里会通过key来计算在散列表中的对应位置,然后以当前key对应的桶的位置向后查找,找到可以使用的桶。

1
2
3
java复制代码Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

什么情况下桶才是可以使用的呢?

  1. k = key 说明是替换操作,可以使用
  2. 碰到一个过期的桶,执行替换逻辑,占用过期桶
  3. 查找过程中,碰到桶中Entry=null的情况,直接使用

接着就是执行for循环遍历,向后查找,我们先看下nextIndex()、prevIndex()方法实现:

YZSC5j.png

1
2
3
4
5
6
7
java复制代码private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}

接着看剩下for循环中的逻辑:

  1. 遍历当前key值对应的桶中Entry数据为空,这说明散列数组这里没有数据冲突,跳出for循环,直接set数据到对应的桶中
  2. 如果key值对应的桶中Entry数据不为空
  3. 1 如果k = key,说明当前set操作是一个替换操作,做替换逻辑,直接返回
  4. 2 如果key = null,说明当前桶位置的Entry是过期数据,执行replaceStaleEntry()方法(核心方法),然后返回
  5. for循环执行完毕,继续往下执行说明向后迭代的过程中遇到了entry为null的情况
  6. 1 在Entry为null的桶中创建一个新的Entry对象
  7. 2 执行++size操作
  8. 调用cleanSomeSlots()做一次启发式清理工作,清理散列数组中Entry的key过期的数据
  9. 1 如果清理工作完成后,未清理到任何数据,且size超过了阈值(数组长度的2/3),进行rehash()操作
  10. 2 rehash()中会先进行一轮探测式清理,清理过期key,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑(扩容逻辑往后看)

接着重点看下replaceStaleEntry()方法,replaceStaleEntry()方法提供替换过期数据的功能,我们可以对应上面第四种情况的原理图来再回顾下,具体代码如下:

java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry():

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
java复制代码private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))

if (e.get() == null)
slotToExpunge = i;

for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {

ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

slotToExpunge表示开始探测式清理过期数据的开始下标,默认从当前的staleSlot开始。以当前的staleSlot开始,向前迭代查找,找到没有过期的数据,for循环一直碰到Entry为null才会结束。如果向前找到了过期数据,更新探测清理过期数据的开始下标为i,即slotToExpunge=i

1
2
3
4
5
6
7
8
java复制代码for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){

if (e.get() == null){
slotToExpunge = i;
}
}

接着开始从staleSlot向后查找,也是碰到Entry为null的桶结束。
如果迭代过程中,碰到k == key,这说明这里是替换逻辑,替换新数据并且交换当前staleSlot位置。如果slotToExpunge == staleSlot,这说明replaceStaleEntry()一开始向前查找过期数据时并未找到过期的Entry数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的index,即slotToExpunge = i。最后调用cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);进行启发式过期数据清理。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

if (slotToExpunge == staleSlot)
slotToExpunge = i;

cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

cleanSomeSlots()和expungeStaleEntry()方法后面都会细讲,这两个是和清理相关的方法,一个是过期key相关Entry的启发式清理(Heuristically scan),另一个是过期key相关Entry的探测式清理。

如果k != key则会接着往下走,k == null说明当前遍历的Entry是一个过期数据,slotToExpunge == staleSlot说明,一开始的向前查找数据并未找到过期的Entry。如果条件成立,则更新slotToExpunge 为当前位置,这个前提是前驱节点扫描时未发现过期数据。

1
2
java复制代码if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;

往后迭代的过程中如果没有找到k == key的数据,且碰到Entry为null的数据,则结束当前的迭代操作。此时说明这里是一个添加的逻辑,将新的数据添加到table[staleSlot] 对应的slot中。

1
2
java复制代码tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

最后判断除了staleSlot以外,还发现了其他过期的slot数据,就要开启清理数据的逻辑:

1
2
java复制代码if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

ThreadLocalMap过期key的探测式清理流程

上面我们有提及ThreadLocalMap的两种过期key数据清理方式:探测式清理和启发式清理。

我们先讲下探测式清理,也就是expungeStaleEntry方法,遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些。操作逻辑如下:

YuH2OU.png

如上图,set(27) 经过hash计算后应该落到index=4的桶中,由于index=4桶已经有了数据,所以往后迭代最终数据放入到index=7的桶中,放入后一段时间后index=5中的Entry数据key变为了null

YuHb6K.png

如果再有其他数据set到map中,就会触发探测式清理操作。

如上图,执行探测式清理后,index=5的数据被清理掉,继续往后迭代,到index=7的元素时,经过rehash后发现该元素正确的index=4,而此位置已经已经有了数据,往后查找离index=4最近的Entry=null的节点(刚被探测式清理掉的数据:index=5),找到后移动index= 7的数据到index=5中,此时桶的位置离正确的位置index=4更近了。

经过一轮探测式清理后,key过期的数据会被清理掉,没过期的数据经过rehash重定位后所处的桶位置理论上更接近i= key.hashCode & (tab.len - 1)的位置。这种优化会提高整个散列表查询性能。

接着看下expungeStaleEntry()具体流程,我们还是以先原理图后源码讲解的方式来一步步梳理:

Yuf301.png

我们假设expungeStaleEntry(3) 来调用此方法,如上图所示,我们可以看到ThreadLocalMap中table的数据情况,接着执行清理操作:

YufupF.png

第一步是清空当前staleSlot位置的数据,index=3位置的Entry变成了null。然后接着往后探测:

YufAwq.png

执行完第二步后,index=4的元素挪到index=3的槽位中。

继续往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算slot位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置

YuWjTP.png

在往后迭代的过程中碰到空的槽位,终止探测,这样一轮探测式清理工作就完成了,接着我们继续看看具体实现源代码:

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
java复制代码private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

这里我们还是以staleSlot=3 来做示例说明,首先是将tab[staleSlot]槽位的数据清空,然后设置size--
接着以staleSlot位置往后迭代,如果遇到k==null的过期数据,也是清空该槽位数据,然后size--

1
2
3
4
5
6
7
java复制代码ThreadLocal<?> k = e.get();

if (k == null) {
e.value = null;
tab[i] = null;
size--;
}

如果key没有过期,重新计算当前key的下标位置是不是当前槽位下标位置,如果不是,那么说明产生了hash冲突,此时以新计算出来正确的槽位位置往后迭代,找到最近一个可以存放entry的位置。

1
2
3
4
5
6
7
8
9
java复制代码int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

while (tab[h] != null)
h = nextIndex(h, len);

tab[h] = e;
}

这里是处理正常的产生Hash冲突的数据,经过迭代后,有过Hash冲突数据的Entry位置会更靠近正确位置,这样的话,查询的时候 效率才会更高。

ThreadLocalMap扩容机制

在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()逻辑:

1
2
java复制代码if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();

接着看下rehash()具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码private void rehash() {
expungeStaleEntries();

if (size >= threshold - threshold / 4)
resize();
}

private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}

这里首先是会进行探测式清理工作,从table的起始位置往后清理,上面有分析清理的详细流程。清理完成之后,table中可能有一些key为null的Entry数据被清理掉,所以此时通过判断size >= threshold - threshold / 4 也就是size >= threshold* 3/4 来决定是否扩容。

我们还记得上面进行rehash()的阈值是size >= threshold,所以当面试官套路我们ThreadLocalMap扩容机制的时候 我们一定要说清楚这两个步骤:

YuqwPs.png

接着看看具体的resize()方法,为了方便演示,我们以oldTab.len=8来举例:

Yu2QOI.png

扩容后的tab的大小为oldLen * 2,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中了。重新计算tab下次扩容的阈值,具体代码如下:

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
java复制代码private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

ThreadLocalMap.get()详解

上面已经看完了set()方法的源码,其中包括set数据、清理数据、优化数据桶的位置等操作,接着看看get()操作的原理。

ThreadLocalMap.get()图解

第一种情况: 通过查找key值计算出散列表中slot位置,然后该slot位置中的Entry.key和查找的key一致,则直接返回:

YuWfdx.png

第二种情况: slot位置中的Entry.key和要查找的key不一致:

YuWyz4.png

我们以get(ThreadLocal1)为例,通过hash计算后,正确的slot位置应该是4,而index=4的槽位已经有了数据,且key值不等于ThreadLocal1,所以需要继续往后迭代查找。

迭代到index=5的数据时,此时Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,执行完后,index 5,8的数据都会被回收,而index 6,7的数据都会前移,此时继续往后迭代,到index = 6的时候即找到了key值相等的Entry数据,如下图所示:

YuW8JS.png

ThreadLocalMap.get()源码详解

java.lang.ThreadLocal.ThreadLocalMap.getEntry():

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
java复制代码private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

ThreadLocalMap过期key的启发式清理流程

上面多次提及到ThreadLocalMap过期可以的两种清理方式:探测式清理(expungeStaleEntry())、启发式清理(cleanSomeSlots())

探测式清理是以当前Entry 往后清理,遇到值为null则结束清理,属于线性探测清理。

而启发式清理被作者定义为:Heuristically scan some cells looking for stale entries.

YK5HJ0.png

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}

InheritableThreadLocal

我们使用ThreadLocal的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。

为了解决这个问题,JDK中还有一个InheritableThreadLocal类,我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class InheritableThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("父类数据:threadLocal");
inheritableThreadLocal.set("父类数据:inheritableThreadLocal");

new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程获取父类threadLocal数据:" + threadLocal.get());
System.out.println("子线程获取父类inheritableThreadLocal数据:" + inheritableThreadLocal.get());
}
}).start();
}
}

打印结果:

1
2
java复制代码子线程获取父类threadLocal数据:null
子线程获取父类inheritableThreadLocal数据:父类数据:inheritableThreadLocal

实现原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}

但InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。

当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal组件就可以解决这个问题,这里就不再延伸,感兴趣的可自行查阅资料。

ThreadLocal项目中使用实战

ThreadLocal使用场景

我们现在项目中日志记录用的是ELK+Logstash,最后在Kibana中进行展示和检索。

现在都是分布式系统统一对外提供服务,项目间调用的关系可以通过traceId来关联,但是不同项目之间如何传递traceId呢?

这里我们使用org.slf4j.MDC来实现此功能,内部就是通过ThreadLocal来实现的,具体实现如下:

当前端发送请求到服务A时,服务A会生成一个类似UUID的traceId字符串,将此字符串放入当前线程的ThreadLocal中,在调用服务B的时候,将traceId写入到请求的Header中,服务B在接收请求时会先判断请求的Header中是否有traceId,如果存在则写入自己线程的ThreadLocal中。

YeMO3t.png

图中的requestId即为我们各个系统链路关联的traceId,系统间互相调用,通过这个requestId即可找到对应链路,这里还有会有一些其他场景:

Ym3861.png

针对于这些场景,我们都可以有相应的解决方案,如下所示

Feign远程调用解决方案

服务发送请求:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Component
@Slf4j
public class FeignInvokeInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
String requestId = MDC.get("requestId");
if (StringUtils.isNotBlank(requestId)) {
template.header("requestId", requestId);
}
}
}

服务接收请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
@Component
public class LogInterceptor extends HandlerInterceptorAdapter {

@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) {
MDC.remove("requestId");
}

@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) {
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String requestId = request.getHeader(BaseConstant.REQUEST_ID_KEY);
if (StringUtils.isBlank(requestId)) {
requestId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("requestId", requestId);
return true;
}
}

线程池异步调用,requestId传递

因为MDC是基于ThreadLocal去实现的,异步过程中,子线程并没有办法获取到父线程ThreadLocal存储的数据,所以这里可以自定义线程池执行器,修改其中的run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

@Override
public void execute(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> run(runnable, context));
}

@Override
private void run(Runnable runnable, Map<String, String> context) {
if (context != null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
MDC.remove();
}
}
}

使用MQ发送消息给第三方系统

在MQ发送的消息体中自定义属性requestId,接收方消费消息后,自己解析requestId使用即可。

本文转载自: 掘金

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

推荐三个 Vue 后台管理模版,配合 Spring Boot

发表于 2020-05-09

最近因为因为项目的原因,一直在寻找一款合适的前端模版,之前的 Vue 前端页面都是自己写的,写多了就烦了,因为功能都差不多,写来写去就没意思了。

所以在新项目中想看看市面上的 Vue 后台管理模版,找个现成的,改一改基本就 OK 了,团队也省事一些。

老实说,Vue 的生态还是相当丰富的,经典的、新生的后台管理框架都有,我这里和大家分享我们这次重点比较的三个。

vue-element-admin

  • GitHub 地址:github.com/PanJiaChen/…
  • 演示地址:panjiachen.github.io/vue-element…

这个项目名气还是挺大的,在我刚开始做 vhr 的时候,就有了解过这个项目,那个时候这个项目才刚刚开发没多久,star 都没几个,现在已然混成一方霸主了。

vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui 实现。它使用了最新的前端技术栈,内置了 i18n 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件,它可以帮助你快速搭建企业级中后台产品原型。

可以说,这个项目的功能还是比较全的,它里边将一些常见的功能模块如权限管理等都做好了,有的时候,你甚至会觉得这个项目有些臃肿。

我们来看一张效果图:

这个开源项目也是我们这里最终选定的方案,选定它有几方面的原因:

  1. 这个项目的开发时间较早,目前相对来说可能 BUG 较少,比较稳定。
  2. 该项目目前依然非常活跃,作者还在不断的完善。
  3. 功能齐全,和其他项目相比,功能上基本没有劣势。
  4. 相对于其他 UI 框架,ElementUI 使用更多一些,这对于一个商用项目还是非常重要的。

当然,使用开源项目肯定不可能完全契合自己的需求的,这种就要在开发中,再去慢慢克服了。

ant-design-vue-pro

  • GitHub 地址:github.com/vueComponen…
  • 演示地址:preview.pro.loacg.com

这个也是我们当时重点比较的项目之一。

Ant Design Pro 是一个企业级中后台前端/设计解决方案,秉承 Ant Design 的设计价值观,致力于在设计规范和基础组件的基础上,继续向上构建,提炼出典型模板/业务组件/配套设计资源,进一步提升企业级中后台产品设计研发过程中的『用户』和『设计者』的体验。随着『设计者』的不断反馈,持续迭代,逐步沉淀和总结出更多设计模式和相应的代码实现。

我们来看个效果图:

这个项目也很优秀,但是最终却“落榜”了,主要是考虑到以下几个原因:

  1. 这个项目看起来没有 vue-element-admin 那么“臃肿”,当然也意味着它的功能比较少。
  2. ant design 本身发展时间挺久了,但是一直都是跟 React 玩,去年才开始支持 Vue,这也是这个 Ant Design Pro 看起来比较简单的原因。
  3. 这个项目权限部分的代码,和 vue-element-admin 具有高度相似性,如出一辙,所以还不如直接上 vue-element-admin。

话说回来,无论用哪个开源项目,想让项目契合自己的实际应用场景,都是需要大刀阔斧的修改的,开源项目拿来直接就能用的很少,从这个角度讲,其实用哪个都无所谓,反正自己都还要改不少东西。但是由于只能选择一个,我们还是选择了更懂 Vue 的 vue-element-admin。

iview-admin

  • GitHub 地址:github.com/iview/iview…
  • 演示地址:admin.iviewui.com

iView-admin 是 iView 生态中的成员之一,是一套采用前后端分离开发模式,基于 Vue 的后台管理系统前端解决方案。iView-admin2.0 脱离 1.x 版本进行重构,换用 Webpack4.0 + Vue-cli3.0 作为基本开发环境。内置了开发后台管理系统常用的逻辑功能,和开箱即用的业务组件,旨在让开发者能够以最小的成本开发后台管理系统,降低开发量。

看惯了 ElementUI ,偶尔看一看 iview 或者 ant design,感觉还蛮清新的。

来看一看它的效果图:

老实说,这些框架同质化太严重了,除了 UI 不一样,其他的基本上没啥区别,所以我们最终还是选择了更加经典的 vue-element-admin。

我们主要是在这三款中进行比较的,所以这里主要和大家介绍这三个,小伙伴们要是用过其他好用的,欢迎留言区提出来哦~

如何学习

最后我再啰嗦一句,这种前端框架要如何学习的问题。

如果你是一个前端新手,刷过 Vue 官网但是还没做过 SPA 应用(甚至没听说过 SPA 应用),那么这三个框架对你来说还是蛮有挑战的,上手并不容易,我也不是特别建议使用这个来入门。

原因很简单,这三个前端框架都算是比较专业的前端框架,里边集成了很多东西,如果你是新手的话,很容易就搞懵了。

最好自己先做过一些 SPA 应用,对前端的整体流程有一个基本把握,再去学习这些框架,相对而言就会容易很多。说到这里,如果大家感兴趣,不妨看看松哥的:微人事项目视频教程。

本文转载自: 掘金

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

Serverless,会将工程师带入“不归路”!

发表于 2020-05-09

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

别被标题吓尿了。

技术的发展,从来不以个人的意志为主转移,程序员的某些分工也必将随着技术的演进而消失。

在开始之前,我们首先来看一下Serverless是个什么概念。

Serverless直译为“无服务器”,并不是说代码运行就不需要在服务器上跑了。它的意思是,未来的开发,无需关注服务器这种比较底层的设施,代码将会直接跑起来。这些资源将变得不可见。

危险了!基础运维人员们。

想象一个Java应用的开发过程。你需要经过开发、测试、部署,才能正常上线。如果是一个高并发的应用,就需要预估一个峰值,比如,需要100台服务器,才能支撑用户的请求。有产品经理脑袋一热,加了秒杀的功能,那么你的峰值,就需要按照秒杀的峰值进行设计,即使这个峰值过程持续了不到10秒,一哆嗦之后所有的资源都会被闲置。也就是说,服务器放在那里即使不用,你也要为此付费。

老板捂着心口说痛,猝死了。

事情会更加的复杂,服务器的数量一多,你需要考虑怎么快速部署;每台机器的配置,都需要进行精细的计算,包括JVM要分配多大的内存,网络的安全策略要如何配置,高可用,异地多活…,诸多细节需要大量的准备工作和基础设施。

这种现状,我们现在的很多程序员,乐在其中。

就拿基础组件和一些中间件来说,什么分库分表、缓存、数据同步、监控、虚拟化、CI/CD….。内容都相差无几,但每一个上规模的公司,基本上都会自己搞一套。这养活了大量的从业人员,但从整个社会的投入和产出来说,这不合理。

为什么面试要造火箭,因为现阶段这玩意有时候还真用得着。

事情在变化

一个公司在发展到一定阶段的时候,会有大量的内部系统。比如运维系统,财务系统,员工管理系统等。除了少部分的开发人员持续迭代这些系统,后续的使用人员其实充当了变相的“运维”角色。

你需要申请一台机器。ok,填个工单吧。工单审批完毕之后,“运维”人员,在后台点巴点巴,一台虚拟机就创建好了。

你需要拿一下系统的用户列表。ok,填个工单吧。审批完毕之后,“运维”人员为你开放一个令牌,拿着令牌到我们的微服务里去拉信息去吧。

这些抽象的服务器,用户接口,不需要你去准备硬件、准备网络,只需要填个工单,唾手可得,已经有了一点Serverless的样子了。不过是企业内部的,规模也有限。

在这里,我们就需要了解一下几个缩写的名词。

IaaS Infrastructure-as-a-Service(基础设施即服务)。此部分属于基础设施,比如服务器的购买、搭建,与虚拟化、容器等技术息息相关。

PaaS Platform-as-a-Service(平台即服务)。比如操作系统、虚拟机,以及在其之上,提供的与业务弱相关的基础组件。通常,一些耳熟能详的名词,如持续集成、中间件、公共组件、微服务等,属于此列。

SaaS Software-as-a-Service(软件即服务)。生活中,几乎我们每一天都在接触SaaS云服务,但一般是指集中式部署的服务提供者。在这种模式下,商业模式变成了租赁的形态,销售变成了运营,项目变成了产品。

这4个名词的第一个字母,既预示着某项从业环境的改变。比如,IaaS的完备,使得专攻基础设施服务的工程师,逐渐没了发展空间;PaaS的完备,使得广大平台开发工程师就业路子越来越窄。除了少部分能够享受平台的红利,大部分只能向更高层的抽象去过渡。

这三者大家耳熟能详,但还有两个缩写,这才是Serverless的关键。

BaaS Backend as a Service(后端即服务)。是指公司为移动应用开发者提供整合云后端的边界服务。

这个词非常可怕。按照我们上面的逻辑,等它普及的时候,大部分后端开发工程师将消亡。稳定了这么多年的后端技术栈,终于要自己搞死自己了。​

FaaS Functions as a Service (功能即服务)。可以广义的理解为功能服务化,也可以解释为函数服务化。使用FaaS只需要关注业务代码逻辑,无需关注服务器资源,所以FaaS也跟开发者无需关注服务器Serverless密切相关。可以说FaaS提供了一个更加细分和抽象的服务化能力。

这就可以想象到,如果各项设施完备之后,只需要水平一般的前端集成人员,就可以完成企业级的开发。

信息革命的本质就是自我升级然后自我替换,程序员能选择的路子将越来越窄。计算机技术将回归工具的本质,人人得而用之,不再是一部分人的专利。

你可能觉得,这玩意,和现在的云环境有什么区别?区别还是有的,否则不会有这么多大厂趋之若鹜。现阶段,Serverless是云厂商的事,和普通开发者关系不大。但最后,这些新生事物,都会慢慢侵蚀我们传统的领地。

弹性!成本!

我们上面就已经举例了,现阶段的服务端软件开发,需要根据峰值进行设计。买了服务器之后,哪怕是云主机,大部分时间也是空跑。空跑也是要付费的,这也是为什么企业的IT费用居高不下,它要为很多压根用不着的东西一直花钱。

Serverless是按需收费的,用多少收多少。比如你的服务QPS在10w的时候,给你分配10台机器,降到2W的时候,给你分配2台机器,那就可以足足省下4/5的money。

这种弹性看似神奇,不过也是建立在一些目前的技术上的。比如kube,docker等。但这是云厂商的事,对企业来说,那就是服务拥有了巨大的弹性,节省了大量的成本。等老板们尝到这种功能的甜头,还养一堆眼里只有技术的人干什么呢?

时间!敏捷!

Serverless的形态,肯定是大的生态,也就是有非常多的完善的功能积木,开发人员进行组装即可。

云厂商会保证功能函数的正常运行,以及升级迭代,向后兼容等特性,企业压根不会再投入精力到一些既有的功能上来。

比如,建设一个直播的基础设施,是非常昂贵且漫长的。如果Serverless提供了这样的功能模块,企业就可以直接拿来用。

这种租用的方式,不仅便宜,而且快捷。以往需要开发一年半载的产品创意,到现在只需要几天就可以上线。这就是复用的魔力。

一些变化

可以看到,在这种模式下,很多职业都将产生变化。

运维工程师?不再需要了,只需要操作配置后台,就能够获取稳定、安全、便宜的主机。

中间件工程师?需求变小了。企业不会再养这部分又贵又难找的人来做这些基础设施。只需要操作配置后台,使用云厂商暴露的功能函数,就可以漂亮的完成工作。

后台开发工程师?需求变小了。不再需要非常复杂的逻辑,做什么前后端分离,做什么复杂的分布式锁和数据同步等。只需要关注业务逻辑就可以了。

前端工程师?这个时候前端还叫做前端么?应该叫全栈。只要会玩乐高积木,就能根据图纸拼接零件,在功能模块足够丰富的前提下,这些都不是魔幻的。

程序员的API,不再是什么Kafka,Redis等,反而会是云厂商提供的一大批自定义的函数。

这对安于现状的工程师来说,真的是挑战。其实从最近几年的云主机普及就能够看出来,有些职业,真的是慢慢的变成了后台管理员。本质上,这与淘宝小二操作的后台,没有什么不同。

本质都是:了解平台的规则(函数),作出相应的推广(集成)。

面向Serverless编程,实在是无趣的多。

在时代的浪潮中,个体总是容易被抛弃的,我们毫无疑问已经进入了终身学习的年代—苦逼的程序员尤其如此。


工具抽象程度高了,并不意味着工程师的素养要求就低了。由于Serverless并没有什么标准,它仅是一种理念,那么各个厂商的实现方式肯定不相同。

作为企业的老板,肯定会有这样的担忧:我不能把鸡蛋放在一个篮子里,受一个厂商的绑架。比如现在有的老板用了阿里云,还会考虑腾讯云、信仰云等等。

使用了A平台提供的功能函数开发的应用,是很难迁移到B平台的。厂商们都希望垄断,往往也不会这么干,那就会造成工程师的学习成本加大。

另外,Serverless厂商的能力毕竟有限,无法覆盖所有的基础业务功能场景。肯定会有厂商,采用开发者平台的方式,吸纳外部的功能组件。开发这部分工功能组件的开发者,也有较高的要求。

我猜测未来会有插件模式的云功能市场,用来丰富这个生态。

各种开源软件的版权也应该有所变化。就像现在的云主机,拿着开源的软件大赚特赚,白嫖了这么多年,而软件开发者却无法从中受益,这是严重不合理的。

那未来的程序员是什么样子呢?

平台开发工程师。搭建云Serverless平台,保证基础设施的完善。这是大厂才玩的事情。

插件功能工程师。按照平台的规则,开发相应的功能,享受售卖的提成;或者受雇于特定的公司,开发此类功能。这是相对头部的工程师。

业务开发工程师。从海量的功能组件中,寻找积木,搭建产品。这和现在在gayhub上寻找功能,集成到项目中是一样的道理。这也就是占比最大的人员。

Serverless一旦普及,就会把大部分工程师带入不归路。还好,由于各大厂商并不是铁板一块,它们相互竞争和攻击,会让建设过程持续非常长的时间。

当然,如果一家独大,也是不必担心的。有三个原因:

第一,天下大势,分久必合,合久必分。大厂的霸道会造成众叛亲离。造成一定程度上的技能回归。

第二,怀旧会让人如痴如醉,很多东西不会消亡。比如我现在还喜欢超级玛丽。

第三,等能活到那一天再说吧。可能和传说中的人工智能一块到来。

不多说了,我要研究AWS Lamba去了。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,​进一步交流。​

本文转载自: 掘金

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

【译】【Google工程师】 刚刚发布了 Fragment

发表于 2020-05-09
  • 原标题: Android Fragments: Fragment Result
  • 原文地址: proandroiddev.com/android-fra…
  • 原文作者: Husayn Hakeem

就在 2020/05/07 号 Now in Android #17 更新了,发布 Android 的新特性,其中就包括 Fragment 间通信的新方式,大家可以点击这里前往,看看都有那些更新

通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案

  • 新 Fragment 间通信的方式的使用?
  • 新 Fragment 间通信的源码分析?
  • 汇总 Fragment 之间的通信的方式?

译文

Frrgament 间传递数据可以通过多种方式,包括使用 target Fragment APIs (Fragment.setTargetFragment() 和 Fragment.getTargetFragment()),ViewModel 或者 使用 Fragments’ 父容器 Activity,target Fragment APIs 已经过时了,现在鼓励使用新的 Fragment result APIs 完成 Frrgament 之间传递数据,其中传递数据由 FragmentManager 处理,并且在 Fragments 设置发送数据和接受数据

在 Frrgament 之间传递数据

使用新的 Fragment APIs 在 两个 Frrgament 之间的传递,没有任何引用,可以使用它们公共的 FragmentManager,它充当 Frrgament 之间传递数据的中心存储。

接受数据

如果想在 Fragment 中接受数据,可以在 FragmentManager 中注册一个 FragmentResultListener,参数 requestKey 可以过滤掉 FragmentManager 发送的数据

1
2
3
4
5
6
复制代码FragmentManager.setFragmentResultListener(
requestKey,
lifecycleOwner,
FragmentResultListener { requestKey: String, result: Bundle ->
// Handle result
})

参数 lifecycleOwner 可以观察生命周期,当 Fragment 的生命周期处于 STARTED 时接受数据。如果监听 Fragment 的生命周期,您可以在接收到新数据时安全地更新 UI,因为 view 的创建(onViewCreated() 方法在 onStart() 之前被调用)。

当生命周期处于 LifecycleOwner STARTED 的状态之前,如果有多个数据传递,只会接收到最新的值

当生命周期处于 LifecycleOwner DESTROYED 时,它将自动移除 listener,如果想手动移除 listener,需要调用 FragmentManager.setFragmentResultListener() 方法,传递空的 FragmentResultListener

在 FragmentManager 中注册 listener,依赖于 Fragment 发送返回的数据

  • 如果在 FragmentA 中接受 FragmentB 发送的数据,FragmentA 和 FragmentB 处于相同的层级,通过 parent FragmentManager 进行通信,FragmentA 必须使用 parent FragmentManager 注册 listener
1
复制代码parentFragmentManager.setFragmentResultListener(...)
  • 如果在 FragmentA 中接受 FragmentB 发送的数据,FragmentA 是 FragmentB 的父容器, 他们通过 child FragmentManager 进行通信
1
复制代码childFragmentManager.setFragmentResultListener(...)

listener 必须设置的Fragment 相同的 FragmentManager

发送数据

如果 FragmentB 发送数据给 FragmentA,需要在 FragmentA 中注册 listener,通过 parent FragmentManager 发送数据

1
2
3
4
复制代码parentFragmentManager.setFragmentResult(
requestKey, // Same request key FragmentA used to register its listener
bundleOf(key to value) // The data to be passed to FragmentA
)

测试 Fragment Results

测试 Fragment 是否成功接收或发送数据,可以使用 FragmentScenario API

接受数据

如果在 FragmentA 中注册 FragmentResultListener 接受数据,你可以模拟 parent FragmentManager 发送数据,如果在 FragmentA 中正确注册了 listener,可以用来验证 FragmentA 是否能收到数据,例如,如果在 FragmentA 中接受数据并更新 UI, 可以使用 Espresso APIs 来验证是否期望的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Test
fun shouldReceiveData() {
val scenario = FragmentScenario.launchInContainer(FragmentA::class.java)

// Pass data using the parent fragment manager
scenario.onFragment { fragment ->
val data = bundleOf(KEY_DATA to "value")
fragment.parentFragmentManager.setFragmentResult("aKey", data)
}

// Verify data is received, for example, by verifying it's been displayed on the UI
onView(withId(R.id.textView)).check(matches(withText("value")))
}

发送数据

可以在 FragmentB 的 parent FragmentManager 上注册一个 FragmentResultListener 来测试 FragmentB 是否成功发送数据,当发送数据结束时,可以来验证这个 listener 是否能收到数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码@Test
fun shouldSendData() {
val scenario = FragmentScenario.launchInContainer(FragmentB::class.java)

// Register result listener
var receivedData = ""
scenario.onFragment { fragment ->
fragment.parentFragmentManager.setFragmentResultListener(
KEY,
fragment,
FragmentResultListener { key, result ->
receivedData = result.getString(KEY_DATA)
})
}

// Send data
onView(withId(R.id.send_data)).perform(click())

// Verify data was successfully sent
assertThat(receivedData).isEqualTo("value")
}

总结

虽然使用了 Fragment result APIs,替换了过时的 Fragment target APIs,但是新的 APIs 在Bundle 作为数据传传递方面有一些限制,只能传递简单数据类型、Serializable 和 Parcelable 数据,Fragment result APIs 允许程序从崩溃中恢复数据,而且不会持有对方的引用,避免当 Fragment 处于不可预知状态的时,可能发生未知的问题

译者的思考

这是译者的一些思考,总结一下 Fragment 1.3.0-alpha04 新增加的 Fragment 间通信的 API

数据接受

1
2
3
4
5
6
复制代码FragmentManager.setFragmentResultListener(
requestKey,
lifecycleOwner,
FragmentResultListener { requestKey: String, result: Bundle ->
// Handle result
})

数据发送

1
2
3
4
复制代码parentFragmentManager.setFragmentResult(
requestKey, // Same request key FragmentA used to register its listener
bundleOf(key to value) // The data to be passed to FragmentA
)

那么 Fragment 间通信的新 API 给我们带来哪些好处呢:

  • 在 Fragment 之间传递数据,不会持有对方的引用
  • 当生命周期处于 ON_START 时开始处理数据,避免当 Fragment 处于不可预知状态的时,可能发生未知的问题
  • 当生命周期处于 ON_DESTROY 时,移除监听

我们一起来从源码的角度分析一下 Google 是如何做的

源码分析

按照惯例从调用的方法来分析,数据接受时,调用了 FragmentManager 的 setFragmentResultListener 方法

androidx.fragment/fragment/1.3.0-alpha04……androidx/fragment/app/FragmentManager.java

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
复制代码private final ConcurrentHashMap<String, LifecycleAwareResultListener> mResultListeners =
new ConcurrentHashMap<>();

@Override
public final void setFragmentResultListener(@NonNull final String requestKey,
@NonNull final LifecycleOwner lifecycleOwner,
@Nullable final FragmentResultListener listener) {
// mResultListeners 是 ConcurrentHashMap 的实例,用来储存注册的 listener
// 如果传递的参数 listener 为空时,移除 requestKey 对应的 listener
if (listener == null) {
mResultListeners.remove(requestKey);
return;
}

// Lifecycle是一个生命周期感知组件,一般用来响应Activity、Fragment等组件的生命周期变化
final Lifecycle lifecycle = lifecycleOwner.getLifecycle();
// 当生命周期处于 DESTROYED 时,直接返回
// 避免当 Fragment 处于不可预知状态的时,可能发生未知的问题
if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
return;
}

// 开始监听生命周期
LifecycleEventObserver observer = new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
// 当生命周期处于 ON_START 时开始处理数据
if (event == Lifecycle.Event.ON_START) {
// 开始检查受到的数据
Bundle storedResult = mResults.get(requestKey);
if (storedResult != null) {
// 如果结果不为空,调用回调方法
listener.onFragmentResult(requestKey, storedResult);
// 清除数据
setFragmentResult(requestKey, null);
}
}

// 当生命周期处于 ON_DESTROY 时,移除监听
if (event == Lifecycle.Event.ON_DESTROY) {
lifecycle.removeObserver(this);
mResultListeners.remove(requestKey);
}
}
};
lifecycle.addObserver(observer);
mResultListeners.put(requestKey, new FragmentManager.LifecycleAwareResultListener(lifecycle, listener));
}
  • Lifecycle是一个生命周期感知组件,一般用来响应Activity、Fragment等组件的生命周期变化
  • 获取 Lifecycle 去监听 Fragment 的生命周期的变化
  • 当生命周期处于 ON_START 时开始处理数据,避免当 Fragment 处于不可预知状态的时,可能发生未知的问题
  • 当生命周期处于 ON_DESTROY 时,移除监听

接下来一起来看一下数据发送的方法,调用了 FragmentManager 的 setFragmentResult 方法

androidx.fragment/fragment/1.3.0-alpha04……androidx/fragment/app/FragmentManager.java

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
复制代码private final ConcurrentHashMap<String, Bundle> mResults = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, LifecycleAwareResultListener> mResultListeners =
new ConcurrentHashMap<>();

@Override
public final void setFragmentResult(@NonNull String requestKey, @Nullable Bundle result) {
if (result == null) {
// mResults 是 ConcurrentHashMap 的实例,用来存储数据传输的 Bundle
// 如果传递的参数 result 为空,移除 requestKey 对应的 Bundle
mResults.remove(requestKey);
return;
}

// Check if there is a listener waiting for a result with this key
// mResultListeners 是 ConcurrentHashMap 的实例,用来储存注册的 listener
// 获取 requestKey 对应的 listener
LifecycleAwareResultListener resultListener = mResultListeners.get(requestKey);
if (resultListener != null && resultListener.isAtLeast(Lifecycle.State.STARTED)) {
// 如果 resultListener 不为空,并且生命周期处于 STARTED 状态时,调用回调
resultListener.onFragmentResult(requestKey, result);
} else {
// 否则保存当前传输的数据
mResults.put(requestKey, result);
}
}
  • 获取 requestKey 注册的 listener
  • 当生命周期处于 STARTED 状态时,开始发送数据
  • 否则保存当前传输的数据

源码分析到这里结束了,我们一起来思考一下,在之前我们的都有那些数据传方式

汇总 Fragment 之间的通信的方式

  • 通过共享 ViewModel 或者关联 Activity来完成,Fragment 之间不应该直接通信 参考 Google: ViewModel#sharing
  • 通过接口,可以在 Fragment 定义接口,并在 Activity 实现它 参考 Google: 与其他 Fragment 通信
  • 通过使用 findFragmentById 方法,获取 Fragment 的实例,然后调用 Fragment 的公共方法 参考 Google: 与其他 Fragment 通信
  • 调用 Fragment.setTargetFragment() 和 Fragment.getTargetFragment() 方法,但是注意 target fragment 需要直接访问另一个 fragment 的实例,这是十分危险的,因为你不知道目标 fragment 处于什么状态
  • Fragment 新的 API, setFragmentResult() 和 setFragmentResultListener()

综合以上通信方式,那么你认为 Fragment 之间通信最好的方式是什么?

参考文献

  • Now in Android #17: https://medium.com/androiddeve……
  • Pass data between fragments: https://developer.android.com/training/basi……
  • ViewModel#sharing: https://developer.android.com/topic/librari……
  • 与其他 Fragment 通信: https://developer.android.com/training/basic……

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,不仅仅是翻译,还有翻译背后对每篇文章思考,如果你喜欢这片文章,请帮我点个赞,感谢,期待与你一起成长

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以前往我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 更多

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 如何高效获取视频截图
  • 10分钟入门 Shell 脚本编程
  • 如何在项目中封装 Kotlin + Android Databinding

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

蚂蚁金服对研发高要求的领域建模能力是指什么?

发表于 2020-05-08

0 前言

最近,由于工作需要,我接触了网商银行的一个项目。项目里对应的业务模型设计,是我工作这三年来见过的所有模型里最复杂的。于是,利用五一这个短暂的假期,我温习了一遍领域建模相关的知识,对于领域模型的设计,有了一些额外的思考。

在领域界有一本书 《Domain-Driven Design》,是Eric编写的,这本书在Goodreads上的评分是4.15分(相当高)。书中有这样的一段话:

“软件开发人员几乎总是专注于技术,把技术作为自己能力的展示和成功的度量……当领域很复杂时,这是一项艰巨的任务,要求高水平技术人员的共同努力。开发人员必须钻研领域以获取业务知识。他们必须磨砺其建模技巧,并精通领域设计。“

书中所强调的领域设计的能力,是对技术人员综合能力评估的一种体现。在我看来,这是属于在软件工程这个方向中,T-1级别的能力。而所谓的领域建模,是一种通过日常不断实践,来强化开发人员思维,逼迫开发人员进入深度思考的过程,并通过在这个过程中的不断锤炼,可以使得开发人员形成结构化思考方式的方法论。

但是,需要注意的是,领域模型本身的定义,在不同的方法论和流派中,是有一些区别的。领域建模的方法也有多种。不过,求同存异,没有对错之分。

在本文,我主要是写一写我对于领域建模的理解,以及介绍一些基础的领域模型知识。算是对这个五一长假以及过去几年工作经验的一个总结。也会简单提提,如何将领域建模的思路,与我们的日常生活结合起来思考。希望能对各位有所帮助。

ok, start with why.

1 领域模型

1.1 领域模型是什么

在why之前,我们先插入一个what,介绍一下领域模型的基本概念。

领域模型是对领域内的概念累或现实世界中对象的可视化表示。又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。 ———摘自度娘

这么解释看着有点迷糊,简单地说,领域模型,是用来描述事物本身的一个模型。它关注事物的特征,事物之间的联系。由于在日常业务开发中,所面临的场景是多样化的,所以可能同一个事物,在建模过程中,我们所获取的特征是不一样的。例如某人A,他是员工,是程序员,是男人,也是父亲。在特定的场景下,我们只提取指定的特征即可。

领域模型之间的联系,我们要怎么理解呢?

以上面这位某人A为例,本次我们所要提取的特征,是父亲。父亲与母亲,丈夫与配偶,关系是1对1的。也就是一夫一妻制。父亲与孩子,关系是1对N的,一位父亲,可以有一个孩子,也可以有多个孩子。母亲与孩子的关系,也是1对N的。所以这一家三口之间的联系,可以简单用这张图来表示:

当然领域模型的图形化还有很多种方式,例如UML类图、状态及图、时许图等,这边就不一一介绍。

1.2 为什么要学习领域模型

“基础设施决定上层建筑” —— 马克思。

老祖宗教导我们,万丈高楼平地起,告诉我们建高楼,要深地基。倘若把一个软件比喻成一栋高楼,那么领域模型,就可以理解为高楼地下的基础设施。

从架构上来说,领域模型是处于应用架构的最底层,上图的Domain层,这一层涵盖了模型治理、流程抽象、流程治理等方面的知识。我们可以很清楚地看到,如果领域模型没有把控好,那么就相当于大楼地基没有打好,带来的后续建筑或是维护成本之高,是难以想象的。

Problem space 与 Solution space,是一套通用的问题分析解决方法论。如果我们以Problem space 与 Solution space来理解的话,领域模型所做的事情,是处于Problem space阶段的,而Solution space 对应的则是系统模型。

领域模型与系统模型之间,我们要如何区分呢。

很简单,领域模型专注的还是事物本身,是高度抽象的。到了系统模型,就是到了具体的设计阶段。还是以上面的某人A为例,他有三个孩子,那么我们在设计阶段,要去怎么建立系统模型呢?可以有以下这三种方案:

  • 父亲表里有多个孩子字段,孩子1、孩子2、孩子3,用于保存孩子的信息
  • 父亲和孩子是两张独立的表,通过外键关联
  • 通过字符串的格式存储,例如搞个大json串,放在父亲的表里

这是三种不同的实现方案,但是在领域模型层面,依旧是父亲与孩子,是1对N。

所以,我们是通过领域模型,进而推导出系统模型,设计出对应的解决方案的。如果领域模型初期建模有比较大的问题,可能就会导致开发人员做很多的无用功。

但是我们为什么要按照这种流程来做开发呢,语言有面向过程和面向对象之分,我们可以对应到开发流程上,也可以理解有面向过程和面向对象(模型)之分。前期不做分析与设计,脑海中有个初步的实现思路,直接操起键盘一把梭,岂不是很畅快?而且从物理学的角度来看,系统的演变,随着时间推移,混乱度总是不断增加的(熵增),难道说领域模型可以做到熵减这种反科学的能力么?

让我们来回忆一下这两个场景,看看你是否遇到过:

1、接到PD的需求,大概看了一眼,操起键盘一把梭,梭了一阶段以后,发现了某个环节存在问题,可能是PD的需求有不足,也可能是你实现方案存在局限性,找PD撕逼讨论过后,发现需求入口侧得不到解决。就只好推翻已有结果,从头再来。或者是写出一坨非常恶心人的代码,告诉自己后面有空再重构。

2、应用上线运行了一段时间后,PD想做个小优化,过来找你讨论。你发现虽然是个小优化,却有大的改动。原因是你之前设计方案,不能够很好的支持这次变更,或者说没有很好地和PD需求连接起来。结果导致代码越改越乱,难以维护。

当然,这么做,可能PD看到你为了她的小改动天天加班,心里过意不去,左一杯奶茶右一杯果汁,接触得多了,一段职场姻缘就此开始。

如果我们选择使用领域建模,这种自上而下的设计,能彻底解决上面的这两个问题么?

答案应该是不能的,要解决上面的两个问题,除了开发自身建模能力的提升之外,系统的设计能力、PD对于模型的理解能力,以及PD和RD之间的沟通有效性,都是值得考量的因素。但是,通过领域建模,我们至少可以保证代码的实现与层级结构是相对科学稳定的,符合业内规范的。在领域建模的过程中划分清楚域的边界,对于后续的系统稳定性提升、依赖区分、业务逻辑清晰性,都是有很大的帮助的。(领域建模能力在蚂蚁金服的某BU,是作为应用架构高P的考核标准之一)

所以做好领域建模,不止是提升开发人员的综合素质能力,也是设计出可以低成本维护的可持续发展的稳定系统的必备要素。而领域模型到系统模型这一设计指导方针,可以帮助我们降低软件设计与现实需求的差异性。

1.3 如何进行领域建模

进行领域建模的方式是有多种的。需要注意的一点在于,领域建模不是纯粹的技术,不是简单学习后就能设计出完美的模型。设计出好的模型,需要依赖于大量的经验和思考。下面介绍给出一些常见的分析方法,然后我们选择一个比较有趣的方法进行深入介绍。

常见分析方法

  • 用例分析法
  • DDD(Domain-Driven Design)
  • DoDAF
  • 四色建模法(Java Modeling In Color With UML)
  • 飞马模型(蚂蚁金服内部)

例子

我们以用例分析法为例,这是最常见也是最简单的分析方法。

用例描述:金牛发布了一篇文章。

这个用例似乎有点太简单的,从主谓宾的角度来看,主语是金牛,谓语是发布,宾语是文章。

在这里我们补充一个点,领域模型是一个模型,模型本身是有属性的。例如上面的某人A例子,他是一个人,那么就会有年龄、性别、身高等属性。

为了引入属性这个概念,我们给上面的用例描述加上定语。

用例描述:金牛发布了一篇领域建模相关的文章。

1
2
3
4
复制代码主语:金牛,名词,可抽象为领域模型;
谓语:发布,动词,可抽象为模型关系;
定语:领域建模相关,形容词,可抽象为模型属性;
宾语:文章,名词,可抽象为领域模型;

当然,实际的用例描述会比上面这个复杂得多,具体的语与用途也会有更多映射关系,例如我们除了可以将定语抽象成宾语的属性外,还可以将其抽象成宾语的关联模型。

这时候我们根据上面这个简单的用例描述,就可以抽象出一个简单的领域模型,如图:

整个流程是很清晰易懂的。我们来抽象一下具体的步骤:

  • 收集用例描述集合
    • 一系列需求文字描述的用例集合
  • 寻找概念
    • 对用例描述进行语言分析,识别名词
  • 添加模型关联
    • 名词之间存在语义联系,则往往存在模型关联,例如上面的发布,联系了金牛和文章两个名词
  • 属性完善
    • 形容词完善,例如上面的领域建模相关,如果文章存在标签属性,那么它的值在我们这个用例里就是领域建模。

简单的步骤就是这四个,然而实际的工程中领域建模,远远比这个复杂。例如还存在子域划分、模型组合等手段。

接下来我们来看一个比较有趣的例子,内容来自于
《Object-Oriented Analysis from Textual Specifications》
论文,中文翻译为《基于文本规范的面向对象分析》,文章所讲的内容,是如何通过自然语言处理技术,从语义和句法的角度分析用例描述,进而通过程序提取出领域模型。

用例描述:

the Static Requirements are: Vendors may be sales employees or companies. Sales
employees receive a basic wage and a commission, whereas companies only receive a commission. Each order corresponds to one vendor only, and each vendor has made
at least one order, which is identified by an order number. One basic wage may be paid to several sales employees. The same commission may be paid to several sales employees and companies.

the Dynamic Requirements are:
A monthly payment is made to vendors. When a vendor
makes a sale, he/she reports the order to the system. The
system then confirms the order to the customer, and orders
are delivered to customers weekly.

分为static requirements 和 dynamic requirements,静态需求与动态需求。人工翻译一下,大意内容是:

静态需求:
供应商可能是销售人员或者公司。销售人员收取基本工资和佣金,而公司只收取佣金。每个订单只对应一个供应商,且每个供应商已经至少制造了一个订单。订单由订单号进行标识。一份基本工资可以支付给几个销售人员。同样的佣金可以支付给几个销售人员和公司。

动态需求:
每个月向供应商付款,当供应商进行销售时,他会向系统报告订单。然后系统确认订单给客户。订单每周完成对客户的交付。

这个例子会比一开始举例的发布文章复杂一点,我们还是用回上面的简单四个步骤。标记出以下名词:vendor, order, sales employee, company, order number, basic wage, commission.

这些名词,有一些是主语,有一些是宾语。我们再结合动词,能初步画出这样一张图

有些名词,可以作为另一部分名词的属性。例如销售人员与基本工资。基本工资可以作为销售人员的属性。文中写到提取这类属性,可以通过分析模型中的聚合与二元关联的关系。可以得到接下来这张图:

2 领域建模与日常生活

学会领域建模,只对开发人员有帮助么?

其实不是的,领域建模,本质是工程的高度抽象。如果我们把生活当成一个项目来看待,那么我们也可以对生活进行领域建模。又或者,我们遇到了一些问题,或者想去做成一件事情,那在这过程中,领域建模的能力可以帮到我们什么?

以解决一个问题,我们需要考虑哪些因素?
问题、目标、现状、方案。

高度抽象出这四个模型,然后我们对其进行建模。如果方案可行,那么这个 问题-目标-现状-方案 的领域模型可以叫做什么?

我们可以理解为这是我们的处事模型,或者称之为原则。

软件工程教会了我们工程化的思维,领域建模训练了我们高度抽象的能力。

从大学毕业后,我懵懵懂懂感觉到了这些知识与技能对于我日常生活的帮助,也初步有了模型化的概念。以至于在毕业后一两年,我和友人交谈的时候,经常扯到模型这个词语。直到后面,我看了Ray Dalio《Principles》(中文名《原则》)
后才明白,原来这就是所谓的处事模型 ——— 原则。

通过领域建模,得到日常生活的原则,这对于我们有什么好处呢?

举个例子,我们将人脑的记忆部分比喻成磁盘。你遇到了一个问题,解决了一个问题,你告诉自己重复的错误不能再犯,于是你把这件事情记录了起来。过了一段时间,你遇到了一个类似的问题,又重复了上面的步骤,将这件事情记录到了磁盘里。

这么做有什么不好的地方呢,随着时间的推移,你的记忆里会塞满了各种各样实例化后的犯错经历,等你想回忆某一件事的时候,你需要到磁盘里去扫描得这个数据,那估计得费一阵子功夫。再加上人脑是有记忆曲线的,早期的犯错经历,可能很快就被数据淘汰掉了。

但是如果在早期就有了抽象的思维,你会发现随着时间推移,你所需要建立的“原则”越来越少,已有的原则会越来越完善。

所以说学会领域建模,有助于提升自己的抽象能力。有助于自己,to be a better man。

本文转载自: 掘金

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

Java 经典面试题:聊一聊 JUC 下的 LinkedBl

发表于 2020-05-08

本文聊一下 JUC 下的 LinkedBlockingQueue 队列,先说说 LinkedBlockingQueue 队列的特点,然后再从源码的角度聊一聊 LinkedBlockingQueue 的主要实现~

LinkedBlockingQueue 有以下特点:

  • LinkedBlockingQueue 是阻塞队列,底层是单链表实现的~
  • 元素从队列尾进队,从队列头出队,符合FIFO~
  • 可以使用 Collection 和 Iterator 两个接口的所有操作,因为实现了两者的接口~
  • LinkedBlockingQueue 队列读写操作都加了锁,但是读写用的是两把不同的锁,所以可以同时读写操作~

LinkedBlockingQueue 队列继承了 AbstractQueue 类,实现了 BlockingQueue 接口,LinkedBlockingQueue 主要有以下接口:

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
复制代码//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
//在成功时返回 true,如果此队列已满,则抛IllegalStateException。
boolean add(E e);

//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
// 将指定的元素插入此队列的尾部,如果该队列已满,
//则在到达指定的等待时间之前等待可用的空间,该方法可中断
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;

//将指定的元素插入此队列的尾部,如果该队列已满,则一直等到(阻塞)。
void put(E e) throws InterruptedException;

//获取并移除此队列的头部,如果没有元素则等待(阻塞),
//直到有元素将唤醒等待线程执行该操作
E take() throws InterruptedException;

//获取并移除此队列的头,如果此队列为空,则返回 null。
E poll();
//获取并移除此队列的头部,在指定的等待时间前一直等到获取元素, //超过时间方法将结束
E poll(long timeout, TimeUnit unit) throws InterruptedException;

//从此队列中移除指定元素的单个实例(如果存在)。
boolean remove(Object o);

//获取但不移除此队列的头元素,没有则跑异常NoSuchElementException
E element();

//获取但不移除此队列的头;如果此队列为空,则返回 null。
E peek();

LinkedBlockingQueue 队列的读写方法非常的多,但是常用的是 put()、take()方法,因为它们两是阻塞的,所以我们就从源码的角度来聊一聊 LinkedBlockingQueue 队列中这两个方法的实现。

先来看看 put()方法,源码如下:

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
复制代码public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// 预先设置 c 的值为 -1,表示失败
int c = -1;
Node<E> node = new Node<E>(e);
// 获取写锁
final ReentrantLock putLock = this.putLock;
// 获取当前队列的大小
final AtomicInteger count = this.count;
// 设置可中断锁
putLock.lockInterruptibly();
try {
// 队列满了
// 当前线程阻塞,等待其他线程的唤醒(其他线程 take 成功后就会唤醒此处线程)
while (count.get() == capacity) {
// 无限期等待
notFull.await();
}
// 新增到队列尾部
enqueue(node);
// 获取当前的队列数
c = count.getAndIncrement();
// 如果队列未满,尝试唤醒一个put的等待线程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 释放锁
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}

put()方法的源码并不难,非常容易就看懂,put()方法的过程大概如下:

  • 1、先加锁,保证容器的并发安全~
  • 2、队列新增数据,将数据追加到队列尾部~
  • 3、新增时,如果队列满了,当前线程是会被阻塞的,等待被唤醒~
  • 4、新增数据成功后,在适当时机,会唤起 put 的等待线程(队列不满时),或者 take 的等待线程(队列不为空时),这样保证队列一旦满足 put 或者 take 条件时,立马就能唤起阻塞线程,继续运行,保证了唤起的时机不被浪费offer 就有两两种,一种是直接返回 false,另一种是超过一定时间后返回 false~
  • 5、释放锁~

其他的新增方法,例如 offer,可以查看源码,跟put() 方法大同小异,相差不大~

再来看看 take()方法,源码如下:

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
复制代码public E take() throws InterruptedException {
E x;
// 默认负数
int c = -1;
// 当前链表的个数
final AtomicInteger count = this.count;
//获取读锁
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 当队列为空时,阻塞,等待其他线程唤醒
while (count.get() == 0) {
notEmpty.await();
}
// 从队列的头部拿出一个元素
x = dequeue();
//减一操作,C比真实的队列数据大一
c = count.getAndDecrement();
// c 大于 0 ,表示队列有值,可以唤醒之前被阻塞的读线程
if (c > 1)
notEmpty.signal();
} finally {
// 释放锁
takeLock.unlock();
}
// 队列未满,可以唤醒 put 等待线程~
if (c == capacity)
signalNotFull();
return x;
}

take()方法跟 put() 方法类似,是一个相反的操作,我就不做过多的说明了~

以上就是 LinkedBlockingQueue 队列的简单源码解析,希望对你的面试或者工作有所帮助,感谢你的阅读~

欢迎关注公众号【互联网平头哥】,一起成长,一起进步~。

互联网平头哥

本文转载自: 掘金

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

《包你懂系列》一文讲清楚 Spring IoC 实现原理和过

发表于 2020-05-08

我是风筝,公众号「古时的风筝」,一个不只有技术的技术公众号,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的 6 的斜杠开发者。
Spring Cloud 系列文章已经完成,可以到 我的github 上查看系列完整内容。也可以在公众号内回复「pdf」获取我精心制作的 pdf 版完整教程。

之前发了一个 Spring IoC 的预热篇 「想要理解 Spring IoC,先要知道如何扩展 Spring 自定义 Bean」,有兴趣的可以看一看,如何在 Spring 中扩展自定义的 Bean,比如 标签中有属性 id 和 name,是如何实现的,我们怎么样能够扩展出一个和 功能类似的标签,但是属性却不一样的功能呢?

了解了自定义扩展 Bean 之后,再理解 Spring IoC 的过程相信会更加清楚。

好了,正文开始。

Spring IoC,全称 Inversion of Control - 控制反转,还有一种叫法叫做 DI( Dependency Injection)-依赖注入。也可以说控制反转是最终目的,依赖注入是实现这个目的的具体方法。

什么叫控制反转

为什么叫做控制反转呢。

在传统的模式下,我想要使用另外一个非静态对象的时候会怎么做呢,答案就是 new 一个实例出来。

举个例子,假设有一个 Logger 类,用来输出日志的。定义如下:

1
2
3
4
5
6
复制代码public class Logger {

public void log(String text){
System.out.println("log:" + text);
}
}

那现在我要调用这个 log 方法,会怎么做呢。

1
2
复制代码Logger logger = new Logger();
logger.log("日志内容");

对不对,以上就是一个传统的调用模式。何时 new 这个对象实例是由调用方来控制,或者说由我们开发者自己控制,什么时候用就什么时候 new 一个出来。

而当我们用了 Spring IoC 之后,事情就变得不一样了。简单来看,结果就是开发者不需要关心 new 对象的操作了。还是那个 Logger 类,我们在引入 Spring IoC 之后会如何使用它呢?

1
2
3
4
5
6
7
8
9
10
复制代码public class UserController {

@Autowired
private Logger logger;

public void log(){
logger.log("please write a log");
}

}

开发者不创建对象,但是要保证对象被正常使用,不可能没有 new 这个动作,这说不通。既然如此,肯定是谁帮我们做了这个操作,那就是 Spring 框架做了,准确的说是 Spring IoC Container 帮我们做了。这样一来,控制权由开发者转变成了第三方框架,这就叫做控制反转。

什么叫依赖注入

依赖注入的主谓宾补充完整,就是将调用者所依赖的类实例对象注入到调用者类。拿前面的那个例子来说,UserController 类就是调用者,它想要调用 Logger 实例化对象出来的 log 方法,logger 作为一个实例化(也就是 new 出来的)对象,就是 UserController 的依赖对象,我们在代码中没有主动使用 new 关键字,那是因为 Spring IoC Container 帮我们做了,这个对于开发者来说透明的操作就叫做注入。

注入的方式有三种:构造方法的注入、setter 的注入和注解注入,前两种方式基本上现在很少有人用了,开发中更多的是采用注解方式,尤其是 Spring Boot 越来越普遍的今天。我们在使用 Spring 框架开发时,一般都用 @Autowired,当然有时也可以用 @Resource

1
2
3
4
5
复制代码@Autowired
private IUserService userService;

@Autowired
private Logger logger;

Spring IoC Container

前面说了注入的动作其实是 Spring IoC Container 帮我们做的,那么 Spring IoC Container 究竟是什么呢?


本次要讨论的就是上图中的 Core Container 部分,包括 Beans、Core、Context、SpEL 四个部分。

Container 负责实例化,配置和组装Bean,并将其注入到依赖调用者类中。Container 是管理 Spring 项目中 Bean 整个生命周期的管理者,包括 Bean 的创建、注册、存储、获取、销毁等等。

先从一个基础款的例子说起。前面例子中的 @Bean 是用注解的方式实现的,这个稍后再说。既然是基础款,那就逃不掉 xml 的,虽然现在都用 Spring Boot 了,但通过原始的 xml 方式能更加清晰的观察依赖注入的过程,要知道,最早还没有 Spring Boot 的时候,xml 可以说是 Spring 项目的纽带,配置信息都大多数都来自 xml 配置文件。

首先添加一个 xml 格式的 bean 声明文件,假设名称为 application.xml,如果你之前用过 Spring MVC ,那大多数情况下对这种定义会非常熟悉。

1
2
3
4
5
6
7
8
9
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bean="http://www.springframework.org/schema/c"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="logger" class="org.kite.spring.bean.Logger" />
</beans>

通过 <bean> 元素来声明一个 Bean 对象,并指定 id 和 class,这是 xml 方式声明 bean 对象的标准方式,如果你自从接触 Java 就用 Spring Boot 了,那其实这种方式还是有必要了解一下的。

之后通过通过一个控制台程序来测试一下,调用 Logger 类的 log 方法。

1
2
3
4
5
6
7
8
复制代码public class IocTest {

public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
Logger logger = (Logger) ac.getBean("logger");
logger.log("hello log");
}
}

ApplicationContext是实现容器的接口类, 其中 ClassPathXmlApplicationContext就是一个 Container 的具体实现,类似的还有 FileSystemXmlApplicationContext,这两个是都是解析 xml 格式配置的容器。我们来看一下 ClassPathXmlApplicationContext 的继承关系图。


有没有看起来很复杂的意思,光是到 ApplicationContext 这一层就经过了好几层。

这是我们在控制台中主动调用 ClassPathXmlApplicationContext,一般在我们的项目中是不需要关心 ApplicationContext的,比如我们使用的 Spring Boot 的项目,只需要下面几行就可以了。

1
2
3
4
5
6
复制代码@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

但是,这几行并不代表 Spring Boot 就不做依赖注入了,同样的,内部也会实现 ApplicationContext,具体的实现叫做 AnnotationConfigServletWebServerApplicationContext,下面看一下这个实现类的继承关系图,那更是复杂的很,先不用在乎细节,了解一下就可以了。


注入过程分析


继续把上面那段基础款代码拿过来,我们的分析就从它开始。

1
2
3
4
5
6
7
复制代码public class IocTest {
public static void main(String[] args){
ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
Logger logger = (Logger) ac.getBean("logger");
logger.log("hello log");
}
}

注入过程有好多文章都进行过源码分析,这里就不重点介绍源码了。

简单介绍一下,我们如果只分析 ClassPathXmlApplicationContext 这种简单的容器的话,其实整个注入过程的源码很容易读,不得不说,Spring 的源码写的非常整洁。我们从 ClassPathXmlApplicationContext的构造函数进去,一步步找到 refresh() 方法,然后顺着读下去就能理解 Spring IoC 最基础的过程。以下代码是 refresh 方法的核心方法:

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
复制代码@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
}
destroyBeans();
cancelRefresh(ex);
throw ex;
}

finally {
resetCommonCaches();
}
}
}

注释都写的非常清楚,其中核心注入过程其实就在这一行:

1
复制代码ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

我把这个核心部分的逻辑调用画了一个泳道图,这个图只列了核心方法,但是已经能够清楚的表示这个过程了。(获取矢量格式的可以在公众号回复「矢量图」获取)

题外话:关于源码阅读
大部分人都不太能读进去源码,包括我自己,别说这种特别庞大的开源框架,就算是自己新接手的项目也看不进去多少。读源码最关键的就是细节,这儿说的细节不是让你抠细节,恰恰相反,千万不能太抠细节了,谁也不能把一个框架的所有源码一行不落的全摸透,找关键的逻辑关系就可以了,不然的话,很有可能你就被一个细节搞到头疼、懊恼,然后就放弃阅读了。

有的同学一看图或者源码会发现,怎么涉及到这么多的类啊,这调用链可真够长的。没关系,你就把它们当做一个整体就可以了(理解成发生在一个类中的调用),通过前面的类关系图就看出来了,继承关系很复杂,各种继承、实现,所以到最后调用链变得很繁杂。

简单概括

那么简单来概括一下注入的核心其实就是解析 xml 文件的内容,找到 元素,然后经过一系列加工,最后把这些加工后的对象存到一个公共空间,供调用者获取使用。

而至于使用注解方式的 bean,比如使用 @Bean、@Service、@Component 等注解的,只是解析这一步不一样而已,剩下的操作基本都一致。

所以说,我们只要把这里面的几个核心问题搞清楚就可以了。

BeanFactory 和 ApplicationContext 的关系

上面的那行核心代码,最后返回的是一个 ConfigurableListableBeanFactory对象,而且后面多个方法都用这个返回的 beanFactory 做为参数。

BeanFactory 是一个接口,ApplicationContext 也是一个接口,而且,BeanFactory 是 ApplicationContext的父接口,有说 BeanFactory才是 Spring IoC 的容器。其实早期的时候只有 BeanFactory,那时候它确实是 Spring IoC 容器,后来由于版本升级扩展更多功能,所以加入了 ApplicationContext。它们俩最大的区别在于,ApplicationContext 初始化时就实例化所有 Bean,而BeanFactory 用到时再实例化所用 Bean,所以早期版本的 Spring 默认是采用懒加载的方式,而新版本默认是在初始化时就实例化所有 Bean,所以 Spring 的启动过程不是那么快,这是其中的一个原因。

BeanDefinition 保存在哪儿

上面概括里提到保存到一个公共空间,那这个公共空间在哪儿呢?其实是一个 Map,而且是一个 ConcurrentHashMap ,为了保证并发安全。它的声明如下,在 DefaultListableBeanFactory 中。

1
复制代码private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256)

其中 beanName 作为 key,也就是例子中的 logger,value 是 BeanDefinition 类型,BeanDefinition 用来描述一个 Bean 的定义,我们在 xml 文件中定义的 元素的属性都在其中,还包括其他的一些必要属性。

向 beanDefinitionMap 中添加元素,叫做 Bean 的注册,只有被注册过的 Bean 才能被使用。

Bean 实例保存在哪儿

另外,还有一个 Map 叫做 singletonObjects,其声明如下:

1
2
复制代码/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

在 refresh() 过程中,还会将 Bean 存到这里一份,这个存储过程发生在 finishBeanFactoryInitialization(beanFactory) 方法内,它的作用是将非 lazy-init 的 Bean 放到singletonObjects 中。

除了存我们定义的 Bean,还包括几个系统 Bean。


例如我们在代码中这样调用:

1
2
复制代码ApplicationContext ac = new ClassPathXmlApplicationContext("application.xml");
StandardEnvironment env = (StandardEnvironment) ac.getBean("environment");

使用已注册的 Bean

在这个例子中,我们是通过 ApplicationContext 的 getBean() 方法显示的获取已注册的 Bean。前面说了我们定义的 Bean 除了放到 beanDefinitionMap,还在 singletonObjects 中存了一份,singletonObjects 中的就是一个缓存,当我们调用 getBean 方法的时候,会先到其中去获取。如果没找到(对于那些主动设置 lazy-init 的 Bean 来说),再去 beanDefinitionMap 获取,并且加入到 singletonObjects 中。

获取 Bean 的调用流程图如下(公众号回复「矢量图」获取高清矢量图)


以下是 lazy-init 方式设置的 Bean 的例子。

1
复制代码<bean id="lazyBean" lazy-init="true" class="org.kite.spring.bean.lazyBean" />

如果不设置的话,默认都是在初始化的时候注册。

注解的方式

现在已经很少项目用 xml 这种配置方式了,基本上都是 Spring Boot,就算不用,也是在 Spring MVC 中用注解的方式注册、使用 Bean 了。其实整个过程都是类似的,只不过注册和获取的时候多了注解的参与。Srping 中 BeanFactory和ApplicationContext都是接口,除此之外,还有很多的抽象类,使得我们可以灵活的定制属于自己的注册和调用流程,可以认为注解方式就是其中的一种定制。只要找到时机解析好对应的注解标示就可以了。

但是看 Spring Boot 的注册和调用过程没有 xml 方式的顺畅,这都是因为注解的特性决定的。注解用起来简单、方便,好处多多。但同时,注解会割裂传统的流程,传统流程都是一步一步主动调用,只要顺着代码往下看就可以了,而注解的方式会造成这个过程连不起来,所以读起来需要额外的一些方法。

Spring Boot 中的 IoC 过程,我们下次有机会再说。

获取本文高清泳道图请在公众号内回复「矢量图」

感觉还好给个赞吧,总是被白嫖,身体吃不消!

参考文档:

https://docs.spring.io/spring/docs

我是风筝,公众号「古时的风筝」,一个在程序圈混迹多年,主业 Java,另外 Python、React 也玩儿的很 6 的斜杠开发者。可以在公众号中加我好友,进群里小伙伴交流学习,好多大厂的同学也在群内呦。

技术交流还可以加群或者直接加我微信。

本文转载自: 掘金

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

MongoDB快速入门,掌握这些刚刚好!

发表于 2020-05-08

SpringBoot实战电商项目mall(30k+star)地址:github.com/macrozheng/…

摘要

虽说现在关系型数据库还是主流,但是面对某些需求的时候,需要非关系型数据库来补充它,学习一个主流的NoSQL数据库还是很有必要的。MongoDB是一个功能丰富的NoSQL数据库,本文整理了它最常用的部分形成了这篇入门教程,希望对大家有所帮助。

简介

MongoDB是一个基于分布式文件存储的数据库。由C++语言编写,旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系型数据库和非关系型数据库之间的产品,是非关系型数据库当中功能最丰富,最像关系型数据库的。

安装

以前写的MongoDB安装教程是基于3.2版本的,发现有的朋友使用新版本安装有问题,这次我们使用最新版本再来安装一次,本文所使用的MongoDB版本为4.2.5,总的来说,新版本的安装更简单了。

Windows下的安装

  • 下载MongoDB安装包,选择Windows x64版本安装,下载地址:www.mongodb.com/download-ce…

  • 运行MongoDB安装包并选择自定义安装,设置好安装路径;

  • 配置MongoDB,让MongoDB作为服务运行,并配置好数据目录和日志目录;

  • 取消MongoDB Compass的安装选项(不取消安装极慢),需要可自行安装;

  • 双击mongo.exe可以运行MongoDB自带客户端,操作MongoDB;

  • 连接成功后会显示如下信息;

  • 如果需要移除MongoDB服务,只需使用管理员权限运行cmd工具,并输入如下命令。
1
复制代码sc.exe delete MongoDB

Linux下的安装

  • 下载MongoDB的Docker镜像;
1
复制代码docker pull mongo:4.2.5
  • 使用Docker命令启动MongoDB服务;
1
2
3
复制代码docker run -p 27017:27017 --name mongo \
-v /mydata/mongo/db:/data/db \
-d mongo:4.2.5
  • 有时候我们需要为MongoDB设置账号,可以使用如下命令启动;
1
2
3
复制代码docker run -p 27017:27017 --name mongo \
-v /mydata/mongo/db:/data/db \
-d mongo:4.2.5 --auth
  • 然后我们需要进入容器中的MongoDB客户端;
1
复制代码docker exec -it mongo mongo
  • 之后在admin集合中创建一个账号用于连接,这里创建的是基于root角色的超级管理员帐号;
1
2
3
4
5
复制代码use admin
db.createUser({
user: 'mongoadmin',
pwd: 'secret',
roles: [ { role: "root", db: "admin" } ] });
  • 创建完成后验证是否可以登录;
1
复制代码db.auth("mongoadmin","secret")
  • 整个账号创建过程可以参考下图。

客户端工具

MongoDB的客户端工具有很多,上面没安装的MongoDB Compass就是其中之一,另外Navicat 15版本也有MongoDB的管理功能。这里我们使用的是一款免费的客户端工具Robo 3T(以前叫Robomongo)。

  • 首先下载客户端工具,下载地址:robomongo.org/download

  • 下载完成后解压,双击robo3t.exe即可使用;

  • 之后创建一个到MongoDB的连接;

  • 创建连接成功以后,就可以操作MongoDB了。

相关概念

MongoDB是非关系型数据库当中最像关系型数据库的,所以我们通过它与关系型数据库的对比,来了解下它的概念。

SQL概念 MongoDB概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
primary key primary key 主键,MongoDB自动将_id字段设置为主键

数据库操作

  • 创建数据库,使用use命令去创建数据库,当插入第一条数据时会创建数据库,例如创建一个test数据库;
1
2
3
4
5
6
7
8
9
复制代码> use test
switched to db test
> db.article.insert({name:"MongoDB 教程"})
WriteResult({ "nInserted" : 1 })
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
test 0.000GB
  • 删除数据库,使用db对象中的dropDatabase()方法来删除;
1
2
3
4
5
6
复制代码> db.dropDatabase()
{ "dropped" : "test", "ok" : 1 }
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB

集合操作

  • 创建集合,使用db对象中的createCollection()方法来创建集合,例如创建一个article集合;
1
2
3
4
5
6
复制代码> use test
switched to db test
> db.createCollection("article")
{ "ok" : 1 }
> show collections
article
  • 删除集合,使用collection对象的drop()方法来删除集合,例如删除一个article集合;
1
2
3
复制代码> db.article.drop()
true
> show collections

文档操作

上面的数据库和集合操作是在MongoDB的客户端中进行的,下面的文档操作都是在Robomongo中进行的。

插入文档

  • MongoDB通过collection对象的insert()方法向集合中插入文档,语法如下;
1
复制代码db.collection.insert(document)
  • 使用collection对象的insert()方法来插入文档,例如插入一个article文档;
1
2
3
4
5
6
7
复制代码db.article.insert({title: 'MongoDB 教程', 
description: 'MongoDB 是一个 Nosql 数据库',
by: 'Andy',
url: 'https://www.mongodb.com/',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100
})
  • 使用collection对象的find()方法可以获取文档,例如获取所有的article文档;
1
复制代码db.article.find({})
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{
"_id" : ObjectId("5e9943661379a112845e4056"),
"title" : "MongoDB 教程",
"description" : "MongoDB 是一个 Nosql 数据库",
"by" : "Andy",
"url" : "https://www.mongodb.com/",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100.0
}

更新文档

  • MongoDB通过collection对象的update()来更新集合中的文档,语法如下;
1
2
3
4
5
6
7
8
9
10
复制代码db.collection.update(
<query>,
<update>,
{
multi: <boolean>
}
)
# query:修改的查询条件,类似于SQL中的WHERE部分
# update:更新属性的操作符,类似与SQL中的SET部分
# multi:设置为true时会更新所有符合条件的文档,默认为false只更新找到的第一条
  • 将title为MongoDB 教程的所有文档的title修改为MongoDB;
1
复制代码db.article.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}},{multi:true})
  • 除了update()方法以外,save()方法可以用来替换已有文档,语法如下;
1
复制代码db.collection.save(document)
  • 这次我们将ObjectId为5e9943661379a112845e4056的文档的title改为MongoDB 教程;
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码db.article.save({
"_id" : ObjectId("5e9943661379a112845e4056"),
"title" : "MongoDB 教程",
"description" : "MongoDB 是一个 Nosql 数据库",
"by" : "Andy",
"url" : "https://www.mongodb.com/",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 100.0
})

删除文档

  • MongoDB通过collection对象的remove()方法来删除集合中的文档,语法如下;
1
2
3
4
5
6
7
8
复制代码db.collection.remove(
<query>,
{
justOne: <boolean>
}
)
# query:删除的查询条件,类似于SQL中的WHERE部分
# justOne:设置为true只删除一条记录,默认为false删除所有记录
  • 删除title为MongoDB 教程的所有文档;
1
复制代码db.article.remove({'title':'MongoDB 教程'})

查询文档

  • MongoDB通过collection对象的find()方法来查询文档,语法如下;
1
2
3
复制代码db.collection.find(query, projection)
# query:查询条件,类似于SQL中的WHERE部分
# projection:可选,使用投影操作符指定返回的键
  • 查询article集合中的所有文档;
1
复制代码db.article.find()
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
复制代码/* 1 */
{
"_id" : ObjectId("5e994dcb1379a112845e4057"),
"title" : "MongoDB 教程",
"description" : "MongoDB 是一个 Nosql 数据库",
"by" : "Andy",
"url" : "https://www.mongodb.com/",
"tags" : [
"mongodb",
"database",
"NoSQL"
],
"likes" : 50.0
}

/* 2 */
{
"_id" : ObjectId("5e994df51379a112845e4058"),
"title" : "Elasticsearch 教程",
"description" : "Elasticsearch 是一个搜索引擎",
"by" : "Ruby",
"url" : "https://www.elastic.co/cn/",
"tags" : [
"elasticearch",
"database",
"NoSQL"
],
"likes" : 100.0
}

/* 3 */
{
"_id" : ObjectId("5e994e111379a112845e4059"),
"title" : "Redis 教程",
"description" : "Redis 是一个key-value数据库",
"by" : "Andy",
"url" : "https://redis.io/",
"tags" : [
"redis",
"database",
"NoSQL"
],
"likes" : 150.0
}
  • MongoDB中的条件操作符,通过与SQL语句的对比来了解下;
操作 格式 SQL中的类似语句
等于 {<key>:<value>} where title = 'MongoDB 教程'
小于 {<key>:{$lt:<value>}} where likes < 50
小于或等于 {<key>:{$lte:<value>}} where likes <= 50
大于 {<key>:{$gt:<value>}} where likes > 50
大于或等于 {<key>:{$gte:<value>}} where likes >= 50
不等于 {<key>:{$ne:<value>}} where likes != 50
  • 条件查询,查询title为MongoDB 教程的所有文档;
1
复制代码db.article.find({'title':'MongoDB 教程'})
  • 条件查询,查询likes大于50的所有文档;
1
复制代码db.article.find({'likes':{$gt:50}})
  • AND条件可以通过在find()方法传入多个键,以逗号隔开来实现,例如查询title为MongoDB 教程并且by为Andy的所有文档;
1
复制代码db.article.find({'title':'MongoDB 教程','by':'Andy'})
  • OR条件可以通过使用$or操作符实现,例如查询title为Redis 教程或MongoDB 教程的所有文档;
1
复制代码db.article.find({$or:[{"title":"Redis 教程"},{"title": "MongoDB 教程"}]})
  • AND 和 OR条件的联合使用,例如查询likes大于50,并且title为Redis 教程或者"MongoDB 教程的所有文档。
1
复制代码db.article.find({"likes": {$gt:50}, $or: [{"title": "Redis 教程"},{"title": "MongoDB 教程"}]})

其他操作

Limit与Skip操作

  • 读取指定数量的文档,可以使用limit()方法,语法如下;
1
复制代码db.collection.find().limit(NUMBER)
  • 只查询article集合中的2条数据;
1
复制代码db.article.find().limit(2)
  • 跳过指定数量的文档来读取,可以使用skip()方法,语法如下;
1
复制代码db.collection.find().limit(NUMBER).skip(NUMBER)
  • 从第二条开始,查询article集合中的2条数据;
1
复制代码db.article.find().limit(2).skip(1)

排序

  • 在MongoDB中使用sort()方法对数据进行排序,sort()方法通过参数来指定排序的字段,并使用1和-1来指定排序方式,1为升序,-1为降序;
1
复制代码db.collection.find().sort({KEY:1})
  • 按article集合中文档的likes字段降序排列;
1
复制代码db.article.find().sort({likes:-1})

索引

  • 索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。
  • MongoDB使用createIndex()方法来创建索引,语法如下;
1
2
3
4
复制代码db.collection.createIndex(keys, options)
# background:建索引过程会阻塞其它数据库操作,设置为true表示后台创建,默认为false
# unique:设置为true表示创建唯一索引
# name:指定索引名称,如果没有指定会自动生成
  • 给title和description字段创建索引,1表示升序索引,-1表示降序索引,指定以后台方式创建;
1
复制代码db.article.createIndex({"title":1,"description":-1}, {background: true})
  • 查看article集合中已经创建的索引;
1
复制代码db.article.getIndexes()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码/* 1 */
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "test.article"
},
{
"v" : 2,
"key" : {
"title" : 1.0,
"description" : -1.0
},
"name" : "title_1_description_-1",
"ns" : "test.article",
"background" : true
}
]

聚合

  • MongoDB中的聚合使用aggregate()方法,类似于SQL中的group by语句,语法如下;
1
复制代码db.collection.aggregate(AGGREGATE_OPERATION)
  • 聚合中常用操作符如下;
操作符 描述
$sum 计算总和
$avg 计算平均值
$min 计算最小值
$max 计算最大值
  • 根据by字段聚合文档并计算文档数量,类似与SQL中的count()函数;
1
复制代码db.article.aggregate([{$group : {_id : "$by", sum_count : {$sum : 1}}}])
1
2
3
4
5
6
7
8
9
10
11
复制代码/* 1 */
{
"_id" : "Andy",
"sum_count" : 2.0
}

/* 2 */
{
"_id" : "Ruby",
"sum_count" : 1.0
}
  • 根据by字段聚合文档并计算likes字段的平局值,类似与SQL中的avg()语句;
1
复制代码db.article.aggregate([{$group : {_id : "$by", avg_likes : {$avg : "$likes"}}}])
1
2
3
4
5
6
7
8
9
10
11
复制代码/* 1 */
{
"_id" : "Andy",
"avg_likes" : 100.0
}

/* 2 */
{
"_id" : "Ruby",
"avg_likes" : 100.0
}

正则表达式

  • MongoDB使用$regex操作符来设置匹配字符串的正则表达式,可以用来模糊查询,类似于SQL中的like操作;
  • 例如查询title中包含教程的文档;
1
复制代码db.article.find({title:{$regex:"教程"}})
  • 不区分大小写的模糊查询,使用$options操作符;
1
复制代码db.article.find({title:{$regex:"elasticsearch",$options:"$i"}})

结合SpringBoot使用

具体参考:《mall整合Mongodb实现文档操作》

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

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

1…815816817…956

开发者博客

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