简栈文化

Java技术人的成长之路~

俗话说,站在巨人的肩膀上看世界,一般学习的时候也是先总览一下整体,然后逐个部分个个击破,最后形成思路,了解具体细节,Tomcat的结构很复杂,但是 Tomcat 非常的模块化,找到了 Tomcat最核心的模块,问题才可以游刃而解,了解了Tomcat的整体架构对以后深入了解Tomcat来说至关重要!

一、Tomcat顶层架构

先上一张Tomcat的顶层结构图(图A),如下:

http://static.cyblogs.com/5j8j72c1yz.jpg

Tomcat中最顶层的容器是Server,代表着整个服务器,从上图中可以看出,一个Server可以包含至少一个Service,用于具体提供服务。

Service主要包含两个部分:ConnectorContainer。从上图中可以看出 Tomcat 的心脏就是这两个组件,他们的作用如下:

1、Connector用于处理连接相关的事情,并提供SocketRequestResponse相关的转化;

2、Container用于封装和管理Servlet,以及具体处理Request请求;

一个Tomcat中只有一个Server,一个Server可以包含多个Service,一个Service只有一个Container,但是可以有多个Connectors,这是因为一个服务可以有多个连接,如同时提供HttpHttps链接,也可以提供向相同协议不同端口的连接,示意图如下(EngineHostContext下边会说到):

img

多个 Connector 和一个 Container 就形成了一个 Service,有了 Service 就可以对外提供服务了,但是 Service 还要一个生存的环境,必须要有人能够给她生命、掌握其生死大权,那就非 Server 莫属了!所以整个 Tomcat 的生命周期由 Server 控制。

另外,上述的包含关系或者说是父子关系,都可以在tomcat的conf目录下的server.xml配置文件中看出,下图是删除了注释内容之后的一个完整的server.xml配置文件(Tomcat版本为8.0)

img

详细的配置文件文件内容可以到Tomcat官网查看:http://tomcat.apache.org/tomcat-8.0-doc/index.html

上边的配置文件,还可以通过下边的一张结构图更清楚的理解:

img

Server标签设置的端口号为8005shutdown=”SHUTDOWN” ,表示在8005端口监听“SHUTDOWN”命令,如果接收到了就会关闭Tomcat。一个Server有一个Service,当然还可以进行配置,一个Service有多个,Service左边的内容都属于Container的,Service下边是Connector

二、Tomcat顶层架构小结:

(1)Tomcat中只有一个Server,一个Server可以有多个Service,一个Service可以有多个Connector和一个Container

(2) Server掌管着整个Tomcat的生死大权;

(3)Service 是对外提供服务的;

(4)Connector用于接受请求并将请求封装成RequestResponse来具体处理;

(5)Container用于封装和管理Servlet,以及具体处理request请求;

知道了整个Tomcat顶层的分层架构和各个组件之间的关系以及作用,对于绝大多数的开发人员来说ServerService对我们来说确实很远,而我们开发中绝大部分进行配置的内容是属于ConnectorContainer的,所以接下来介绍一下ConnectorContainer

三、Connector和Container的微妙关系

由上述内容我们大致可以知道一个请求发送到Tomcat之后,首先经过Service然后会交给我们的ConnectorConnector用于接收请求并将接收的请求封装为RequestResponse来具体处理,RequestResponse封装完之后再交由Container进行处理,Container处理完请求之后再返回给Connector,最后在由Connector通过Socket将处理的结果返回给客户端,这样整个请求的就处理完了!

Connector最底层使用的是Socket来进行连接的,RequestResponse是按照HTTP协议来封装的,所以Connector同时需要实现TCP/IP协议和HTTP协议!

Tomcat既然处理请求,那么肯定需要先接收到这个请求,接收请求这个东西我们首先就需要看一下Connector

四、Connector架构分析

Connector用于接受请求并将请求封装成RequestResponse,然后交给Container进行处理,Container处理完之后在交给Connector返回给客户端。

因此,我们可以把Connector分为四个方面进行理解:

(1)Connector如何接受请求的?

(2)如何将请求封装成RequestResponse的?

(3)封装完之后的RequestResponse如何交给Container进行处理的?

(4)Container处理完之后如何交给Connector并返回给客户端的?

首先看一下Connector的结构图(图B),如下所示:

img

Connector就是使用ProtocolHandler来处理请求的,不同的ProtocolHandler代表不同的连接类型,比如:Http11Protocol使用的是普通Socket来连接的,Http11NioProtocol使用的是NioSocket来连接的。

其中ProtocolHandler由包含了三个部件:EndpointProcessorAdapter

(1)Endpoint用来处理底层Socket的网络连接,Processor用于将Endpoint接收到的Socket封装成RequestAdapter用于将Request交给Container进行具体的处理。

(2)Endpoint由于是处理底层的Socket网络连接,因此Endpoint是用来实现TCP/IP协议的,而Processor用来实现HTTP协议的,Adapter将请求适配到Servlet容器进行具体的处理。

(3)Endpoint的抽象实现AbstractEndpoint里面定义的AcceptorAsyncTimeout两个内部类和一个Handler接口。Acceptor用于监听请求,AsyncTimeout用于检查异步Request的超时,Handler用于处理接收到的Socket,在内部调用Processor进行处理。

至此,我们应该很轻松的回答(1)(2)(3)的问题了,但是(4)还是不知道,那么我们就来看一下Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?

五、Container架构分析

Container用于封装和管理Servlet,以及具体处理Request请求,在Connector内部包含了4个子容器,结构图如下(图C):

img

4个子容器的作用分别是:

(1)Engine:引擎,用来管理多个站点,一个Service最多只能有一个Engine

(2)Host:代表一个站点,也可以叫虚拟主机,通过配置Host就可以添加站点;

(3)Context:代表一个应用程序,对应着平时开发的一套程序,或者一个WEB-INF目录以及下面的web.xml文件;

(4)Wrapper:每一Wrapper封装着一个Servlet

下面找一个Tomcat的文件目录对照一下,如下图所示:

img

ContextHost的区别是Context表示一个应用,我们的Tomcat中默认的配置下webapps下的每一个文件夹目录都是一个Context,其中ROOT目录中存放着主应用,其他目录存放着子应用,而整个webapps就是一个Host站点。

我们访问应用Context的时候,如果是ROOT下的则直接使用域名就可以访问,例如:www.ledouit.com,如果是`Host(webapps)`下的其他应用,则可以使用www.ledouit.com/docs进行访问,当然默认指定的根应用(ROOT)是可以进行设定的,只不过Host站点下默认的主营用是ROOT目录下的。

看到这里我们知道Container是什么,但是还是不知道Container是如何进行处理的以及处理完之后是如何将处理完的结果返回给Connector的?别急!下边就开始探讨一下Container是如何进行处理的!

六、Container如何处理请求的

Container处理请求是使用Pipeline-Value管道来处理的!

Pipeline-Value是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将处理后的请求返回,再让下一个处理着继续处理。

img

但是!Pipeline-Value使用的责任链模式和普通的责任链模式有些不同!区别主要有以下两点:

(1)每个Pipeline都有特定的Value,而且是在管道的最后一个执行,这个Value叫做BaseValueBaseValue是不可删除的;

(2)在上层容器的管道的BaseValue中会调用下层容器的管道。

我们知道Container包含四个子容器,而这四个子容器对应的BaseValue分别在:StandardEngineValueStandardHostValueStandardContextValueStandardWrapperValue

Pipeline的处理流程图如下(图D):

img

(1)Connector在接收到请求后会首先调用最顶层容器的Pipeline来处理,这里的最顶层容器的Pipeline就是EnginePipelineEngine的管道);

(2)在Engine的管道中依次会执行EngineValue1EngineValue2等等,最后会执行StandardEngineValue,在StandardEngineValue中会调用Host管道,然后再依次执行HostHostValue1HostValue2等,最后在执行StandardHostValue,然后再依次调用Context的管道和Wrapper的管道,最后执行到StandardWrapperValue

(3)当执行到StandardWrapperValue的时候,会在StandardWrapperValue中创建FilterChain,并调用其doFilter方法来处理请求,这个FilterChain包含着我们配置的与请求相匹配的FilterServlet,其doFilter方法会依次调用所有的FilterdoFilter方法和Servletservice方法,这样请求就得到了处理!

(4)当所有的Pipeline-Value都执行完之后,并且处理完了具体的请求,这个时候就可以将返回的结果交给Connector了,Connector在通过Socket的方式将结果返回给客户端。

总结

至此,我们已经对Tomcat的整体架构有了大致的了解,从图A、B、C、D可以看出来每一个组件的基本要素和作用。我们在脑海里应该有一个大概的轮廓了!如果你面试的时候,让你简单的聊一下Tomcat,上面的内容你能脱口而出吗?当你能够脱口而出的时候,这位面试官一定会对你刮目相看的!

参考地址

什么是布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”

相比于传统的 ListSetMap 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

实现原理

HashMap 的问题

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMapKey,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

http://static.cyblogs.com/QQ20200618-210117@2x.jpg

数组里面的值就只会存在truefalse

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成**多个哈希值,**并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值则上图转变为:

http://static.cyblogs.com/QQ20200618-210604@2x.jpg

而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回图中的3bit位,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置为了 1 ,那么程序还是会判断 “taobao” 这个值存在。

支持删除么

传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。可以参考文章 Counting Bloom Filter 的原理和实现:https://cloud.tencent.com/developer/article/1136056

Guava里的布隆过滤器:com.google.common.hash.BloomFilter

1
2
3
4
5
6
7
8
9
// 可能存在
public boolean mightContain(T object) {
return strategy.mightContain(object, funnel, numHashFunctions, bits);
}

// 放入值
public boolean put(T object) {
return strategy.put(object, funnel, numHashFunctions, bits);
}

如何选择哈希函数个数和布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

http://static.cyblogs.com/QQ20200618-212220@2x.jpg

我们可以参考网站给的一个参考值:https://krisives.github.io/bloom-calculator/

http://static.cyblogs.com/QQ20200618-212520@2x.jpg

比如:我们有1000的数量,误判率是0.1,那么需要3.32hash函数,位的长度为4793

Redis-BloomFilter实践

Redis4.0版本推出了 module 的形式,可以将 module 作为插件额外实现Redis的一些功能。官网推荐了一个 RedisBloom 作为 Redis 布隆过滤器的 Module

RedisBloom需要先进行安装,推荐使用Docker进行安装,简单方便:

1
2
3
4
5
docker pull redislabs/rebloom:latest
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest
docker exec -it redis-redisbloom bash
# redis-cli
# 127.0.0.1:6379> bf.add tiancheng hello

熟悉一下布隆过滤器基本指令:

  • bf.add 添加元素到布隆过滤器
  • bf.exists 判断元素是否在布隆过滤器
  • bf.madd 添加多个元素到布隆过滤器,bf.add只能添加一个
  • bf.mexists 判断多个元素是否在布隆过滤器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> bf.add tiancheng tc01
(integer) 1
127.0.0.1:6379> bf.add tiancheng tc02
(integer) 1
127.0.0.1:6379> bf.add tiancheng tc03
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc01
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc02
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc03
(integer) 1
127.0.0.1:6379> bf.exists tiancheng tc04
(integer) 0
127.0.0.1:6379> bf.madd tiancheng tc05 tc06 tc07
1) (integer) 1
2) (integer) 1
3) (integer) 1
127.0.0.1:6379> bf.mexists tiancheng tc05 tc06 tc07 tc08
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0

Redis Bitmap实现简单的布隆过滤器

BitmapRedis中并不是一个单独的数据类型,而是由字符串类型(Redis内部称Simple Dynamic String,SDS)之上定义的与比特相关的操作实现的,此时SDS就被当做位数组了。下面是在redis-cli中使用getbitsetbit指令的操作示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 字符串"meow"的二进制表示:01101101 01100101 01101111 01110111
es1:19000> set bitmap_cat "meow"
OK
# 最低位下标为0。取得第3位的比特(0)
es1:19000> getbit bitmap_cat 3
(integer) 0
# 取得第23位的比特(1)
es1:19000> getbit bitmap_cat 23
(integer) 1
# 将第7位设为0
es1:19000> setbit bitmap_cat 7 0
(integer) 1
# 将第14位设为1
es1:19000> setbit bitmap_cat 14 1
(integer) 0
# 修改过后的字符串变成了"lgow"
es1:19000> get bitmap_cat
"lgow"

RedisBitmap是自动扩容的,亦即get/set到高位时,就会主动填充0。此外,还有bitcount指令用于计算特定字节范围内1的个数,bitop指令用来执行位运算(支持andorxornot)。相应的用法可以查询Redis官方文档等。

参考地址

当你看到这个标题的时候,以为我是一个标题党?(其实也是…)

但是你真的不能小瞧这个单例模式,不信的话,你可以继续往下看。

小白些单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Created with vernon-test
*
* @description: 设计模式-单例模式
* @author: chenyuan
* @date: 2020/6/7
* @time: 9:25 PM
*/
public class Singleton {

private static Singleton instance = null;

private Singleton() {
}

public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
System.out.println("我开始new对象了~");
}
return instance;
}
}

这种单例估计是我们第一眼就能想到的,咋一眼看没问题,因为我们的大脑一眼反应我们的程序都是单线程的。实际上我们系统在初始化的时候就有可能存在多线程的情况。我们模拟并发写一个小程序来验证下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Created with vernon-test
*
* @description: 测试类
* @author: chenyuan
* @date: 2020/6/7
* @time: 9:27 PM
*/
public class TestCase {

public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Singleton instance = Singleton.getInstance();
System.out.println(instance);
}
});
thread.start();
}
}
}

看一眼运行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
我开始new对象了~
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@a963d08
我开始new对象了~
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a
com.vernon.test.designpattern.singleton.Singleton@773d663a

很明显,new了2次对象,看打印的对象地址也是不对的。

那我们如何去改进呢?– 加锁

加synchronized锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Created with vernon-test
*
* @description: 设计模式-单例模式
* @author: chenyuan
* @date: 2020/6/7
* @time: 9:25 PM
*/
public class Singleton2 {

private static Singleton2 instance = null;

private Singleton2() {
}

// 加锁不就搞定了吗?
public static synchronized Singleton2 getInstance() {
if (instance == null) {
instance = new Singleton2();
System.out.println("我开始new对象了~");
}
return instance;
}
}

这个确实能解决掉问题,但是后续每次获取它的时候每次都要加锁排队,性能存在一定的问题。

Double Check

那是不是在方法里面做2次校验就能解决掉问题呢?

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
/**
* Created with vernon-test
*
* @description: 设计模式-单例模式
* @author: chenyuan
* @date: 2020/6/7
* @time: 9:25 PM
*/
public class Singleton3 {

private static Singleton3 instance = null;

private Singleton3() {
}
// double check
public static Singleton3 getInstance() {
if (instance == null) {
synchronized (Singleton3.class) {
if (instance == null) {
instance = new Singleton3();
System.out.println("我开始new对象了~");
}
}
}
return instance;
}
}

从代码看上去,不管是单线程还是多线程,起码在instance==null的时候都能被hold住。但是表现懵逼了我们的双眼。如果这里发生了重排序,就会存在问题。

创建一个对象可以划分为3步:

  • 1、分配内存空间;
  • 2、初始化对象;
  • 3、将内存空间的地址赋值给对象的引用;

如果发生重排序的话,步骤2~3有可能颠倒过来。

防止重排序

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
/**
* Created with vernon-test
*
* @description: 设计模式-单例模式
* @author: chenyuan
* @date: 2020/6/7
* @time: 9:25 PM
*/
public class Singleton4 {

// 加上volatile关键字,防止重排序
private static volatile Singleton4 instance = null;

private Singleton4() {
}

// double check
public static Singleton4 getInstance() {
if (instance == null) {
synchronized (Singleton4.class) {
if (instance == null) {
instance = new Singleton4();
System.out.println("我开始new对象了~");
}
}
}
return instance;
}
}

在这里加上volatile关键字就可以很好的解决了。传送门:https://mp.weixin.qq.com/s/HtMphMK8lyrieg-663q9og

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Created with vernon-test
*
* @description: 设计模式-单例模式
* @author: chenyuan
* @date: 2020/6/7
* @time: 9:25 PM
*/
public class Singleton5 {

private static class SingletonHolder {
public static Singleton5 instance = new Singleton5();
}

public static Singleton5 getInstance() {
return SingletonHolder.instance;
}
}

该方法采用内部类来解决,加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。

参考地址

为什么用分布式锁?在讨论这个问题之前,我们先来看一个业务场景。

为什么用分布式锁?

系统 A 是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。

由于系统有一定的并发,所以会预先将商品的库存保存在 Redis 中,用户下单的时候会更新 Redis 的库存。

此时系统架构如下:

img

但是这样一来会产生一个问题:假如某个时刻,Redis 里面的某个商品库存为 1。

此时两个请求同时到来,其中一个请求执行到上图的第 3 步,更新数据库的库存为 0,但是第 4 步还没有执行。

而另外一个请求执行到了第 2 步,发现库存还是 1,就继续执行第 3 步。这样的结果,是导致卖出了 2 个商品,然而其实库存只有 1 个。

很明显不对啊!这就是典型的库存超卖问题。此时,我们很容易想到解决方案:用锁把 2、3、4 步锁住,让他们执行完之后,另一个线程才能进来执行第 2 步。

http://static.cyblogs.com/ce0fee95ef6880c9ac5ba670fe5be268.jpg

按照上面的图,在执行第 2 步时,使用 Java 提供的 Synchronized 或者 ReentrantLock 来锁住,然后在第 4 步执行完之后才释放锁。

这样一来,2、3、4 这 3 个步骤就被“锁”住了,多个线程之间只能串行化执行。

但是好景不长,整个系统的并发飙升,一台机器扛不住了。现在要增加一台机器,如下图:

http://static.cyblogs.com/99f3874bd484b4ea8000ca41023be3dc.jpg-wh_600x-s_592420448.jpg

增加机器之后,系统变成上图所示,我的天!假设此时两个用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。

为什么呢?因为上图中的两个 A 系统,运行在两个不同的 JVM 里面,他们加的锁只对属于自己 JVM 里面的线程有效,对于其他 JVM 的线程是无效的。

因此,这里的问题是:Java 提供的原生锁机制在多机部署场景下失效了,这是因为两台机器加的锁不是同一个锁(两个锁在不同的 JVM 里面)。

那么,我们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?此时,就该分布式锁隆重登场了。

分布式锁的思路是:在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

至于这个“东西”,可以是 Redis、Zookeeper,也可以是数据库。文字描述不太直观,我们来看下图:

http://static.cyblogs.com/3667d7cff8435d47f5a187ad596bdb09.jpg-wh_600x-s_1198484587.jpg

通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用 Java 原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。

那么,如何实现分布式锁呢?接着往下看!

基于 Redis 实现分布式锁

上面分析为啥要使用分布式锁了,这里我们来具体看看分布式锁落地的时候应该怎么样处理。

①常见的一种方案就是使用 Redis 做分布式锁

使用 Redis 做分布式锁的思路大概是这样的:在 Redis 中设置一个值表示加了锁,然后释放锁的时候就把这个 Key 删除。

具体代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取锁 
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000

// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

这种方式有几大要点:

  • 一定要用 SET key value NX PX milliseconds 命令。如果不用,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(Key ***存在)
  • Value 要具有唯一性。这个是为了在解锁的时候,需要验证 Value 是和加锁的一致才删除 Key。

这时避免了一种情况:假设 A 获取了锁,过期时间 30s,此时 35s 之后,锁已经自动释放了,A 去释放锁,但是此时可能 B 获取了锁。A 客户端就不能删除 B 的锁了。

除了要考虑客户端要怎么实现分布式锁之外,还需要考虑 Redis 的部署问题。

Redis 有 3 种部署方式:

  • 单机模式
  • Master-Slave+Sentinel 选举模式
  • Redis Cluster 模式

使用 Redis 做分布式锁的缺点在于:如果采用单机部署模式,会存在单点问题,只要 Redis 故障了。加锁就不行了。

采用 Master-Slave 模式,加锁的时候只对一个节点加锁,即便通过 Sentinel 做了高可用,但是如果 Master 节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。

基于以上的考虑,Redis 的作者也考虑到这个问题,他提出了一个 RedLock 的算法。

这个算法的意思大概是这样的:假设 Redis 的部署模式是 Redis Cluster,总共有 5 个 Master 节点。

通过以下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒。
  • 轮流尝试在每个 Master 节点上创建锁,过期时间设置较短,一般就几十毫秒。
  • 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点(n / 2 +1)。
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了。
  • 要是锁建立失败了,那么就依次删除这个锁。
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。

http://static.cyblogs.com/da6b5468501519f5688e47824b56ba21.jpg-wh_600x-s_4091681668.jpg

②另一种方式:Redisson

此外,实现 Redis 的分布式锁,除了自己基于 Redis Client 原生 API 来实现之外,还可以使用开源框架:Redission。

Redisson 是一个企业级的开源 Redis Client,也提供了分布式锁的支持。我也非常推荐大家使用,为什么呢?

回想一下上面说的,如果自己写代码来通过 Redis 设置一个值,是通过下面这个命令设置的:

1
SET anyLock unique_value NX PX 30000 

这里设置的超时时间是 30s,假如我超过 30s 都还没有完成业务逻辑的情况下,Key 会过期,其他线程有可能会获取到锁。

这样一来的话,***个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。

所以我们还需要额外的去维护这个过期时间,太麻烦了~我们来看看 Redisson 是怎么实现的?

先感受一下使用 Redission 的爽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Config config = new Config(); 
config.useClusterServers()
.addNodeAddress("redis://192.168.31.101:7001")
.addNodeAddress("redis://192.168.31.101:7002")
.addNodeAddress("redis://192.168.31.101:7003")
.addNodeAddress("redis://192.168.31.102:7001")
.addNodeAddress("redis://192.168.31.102:7002")
.addNodeAddress("redis://192.168.31.102:7003");

RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("anyLock");
lock.lock();
lock.unlock();

就是这么简单,我们只需要通过它的 API 中的 Lock 和 Unlock 即可完成分布式锁,他帮我们考虑了很多细节:

  • Redisson 所有指令都通过 Lua 脚本执行,Redis 支持 Lua 脚本原子性执行。
  • Redisson 设置一个 Key 的默认过期时间为 30s,如果某个客户端持有一个锁超过了 30s 怎么办?
  • Redisson 中有一个 Watchdog 的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔 10s 帮你把 Key 的超时时间设为 30s。

这样的话,就算一直持有锁也不会出现 Key 过期了,其他线程获取到锁的问题了。

  • Redisson 的“看门狗”逻辑保证了没有死锁发生。(如果机器宕机了,看门狗也就没了。此时就不会延长 Key 的过期时间,到了 30s 之后就会自动过期了,其他线程可以获取到锁)

http://static.cyblogs.com/a7fbd80edbe1bc035e0a6e1c3c691ed7.jpg-wh_600x-s_1313345474.jpg

这里稍微贴出来其实现代码:

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
81
82
83
84
// 加锁逻辑 
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
// 调用一段lua脚本,设置一些key、过期时间
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
@Override
public void operationComplete(Future<Long> future) throws Exception {
if (!future.isSuccess()) {
return;
}

Long ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining == null) {
// 看门狗逻辑
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}


<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);

return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}



// 看门狗最终会调用了这里
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}

// 这个任务会延迟10s执行
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {

// 这个操作会将key的过期时间重新设置为30s
RFuture<Boolean> future = renewExpirationAsync(threadId);

future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName() + " expiration", future.cause());
return;
}

if (future.getNow()) {
// reschedule itself
// 通过递归调用本方法,***循环延长过期时间
scheduleExpirationRenewal(threadId);
}
}
});
}

}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
task.cancel();
}
}

另外,Redisson 还提供了对 Redlock 算法的支持,它的用法也很简单:

1
2
3
4
5
6
7
RedissonClient redisson = Redisson.create(config); 
RLock lock1 = redisson.getFairLock("lock1");
RLock lock2 = redisson.getFairLock("lock2");
RLock lock3 = redisson.getFairLock("lock3");
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
multiLock.lock();
multiLock.unlock();

小结:本节分析了使用 Redis 作为分布式锁的具体落地方案以及其一些局限性,然后介绍了一个 Redis 的客户端框架 Redisson,这也是我推荐大家使用的,比自己写代码实现会少 Care 很多细节。

基于 Zookeeper 实现分布式锁

常见的分布式锁实现方案里面,除了使用 Redis 来实现之外,使用 Zookeeper 也可以实现分布式锁。

在介绍 Zookeeper(下文用 ZK 代替)实现分布式锁的机制之前,先粗略介绍一下 ZK 是什么东西:ZK 是一种提供配置管理、分布式协同以及命名的中心化服务。

ZK 的模型是这样的:ZK 包含一系列的节点,叫做 Znode,就好像文件系统一样,每个 Znode 表示一个目录。

然后 Znode 有一些特性:

  • 有序节点:假如当前有一个父节点为 /lock,我们可以在这个父节点下面创建子节点,ZK 提供了一个可选的有序特性。

例如我们可以创建子节点“/lock/node-”并且指明有序,那么 ZK 在生成子节点时会根据当前的子节点数量自动添加整数序号。

也就是说,如果是***个创建的子节点,那么生成的子节点为 /lock/node-0000000000,下一个节点则为 /lock/node-0000000001,依次类推。

  • 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,ZK 会自动删除该节点。
  • 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,ZK 会通知客户端。

当前 ZK 有如下四种事件:

  • 节点创建
  • 节点删除
  • 节点数据修改
  • 子节点变更

基于以上的一些 ZK 的特性,我们很容易得出使用 ZK 实现分布式锁的落地方案:

  • 使用 ZK 的临时节点和有序节点,每个线程获取锁就是在 ZK 创建一个临时有序的节点,比如在 /lock/ 目录下。
  • 创建节点成功后,获取 /lock 目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点。
  • 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
  • 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。

比如当前线程获取到的节点序号为 /lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对 /lock/002 这个节点添加一个事件监听器。

如果锁释放了,会唤醒下一个序号的节点,然后重新执行第 3 步,判断是否自己的节点序号是最小。

比如 /lock/001 释放了,/lock/002 监听到时间,此时节点集合为[/lock/002,/lock/003],则 /lock/002 为最小序号节点,获取到锁。

整个过程如下:

http://static.cyblogs.com/fb6c27bb22de93cbeb965718156f578f.jpg

具体的实现思路就是这样,至于代码怎么写,这里比较复杂就不贴出来了。

Curator 介绍

Curator 是一个 ZK 的开源客户端,也提供了分布式锁的实现。它的使用方式也比较简单:

1
2
3
InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock"); 
interProcessMutex.acquire();
interProcessMutex.release();

其实现分布式锁的核心源码如下:

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
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception 
{
boolean haveTheLock = false;
boolean doDelete = false;
try {
if ( revocable.get() != null ) {
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}

while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {
// 获取当前所有节点排序后的集合
List<String> children = getSortedChildren();
// 获取当前节点的名称
String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash
// 判断当前节点是否是最小的节点
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
if ( predicateResults.getsTheLock() ) {
// 获取到锁
haveTheLock = true;
} else {
// 没获取到锁,对当前节点的上一个节点注册一个监听器
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this){
Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
if ( stat != null ){
if ( millisToWait != null ){
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
if ( millisToWait <= 0 ){
doDelete = true; // timed out - delete our node
break;
}
wait(millisToWait);
}else{
wait();
}
}
}
// else it may have been deleted (i.e. lock released). Try to acquire again
}
}
}
catch ( Exception e ) {
doDelete = true;
throw e;
} finally{
if ( doDelete ){
deleteOurPath(ourPath);
}
}
return haveTheLock;
}

其实 Curator 实现分布式锁的底层原理和上面分析的是差不多的。这里我们用一张图详细描述其原理:

https://s5.51cto.com/oss/201907/16/84636c96585ff2611ff55e284a57c6ab.jpg-wh_600x-s_3685227374.jpg

小结:本节介绍了 ZK 实现分布式锁的方案以及 ZK 的开源客户端的基本使用,简要的介绍了其实现原理。

两种方案的优缺点比较

学完了两种分布式锁的实现方案之后,本节需要讨论的是 Redis 和 ZK 的实现方案中各自的优缺点。

对于 Redis 的分布式锁而言,它有以下缺点:

  • 它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
  • 另外来说的话,Redis 的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮。
  • 即便使用 Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100% 没有问题,关于 Redlock 的讨论可以看 How to do distributed locking。
  • Redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

但是另一方面使用 Redis 实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场景”。

所以使用 Redis 作为分布式锁也不失为一种好的方案,最重要的一点是 Redis 的性能很高,可以支撑高并发的获取、释放锁操作。

对于 ZK 分布式锁而言:

  • ZK 天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
  • 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

但是 ZK 也有其缺点:如果有较多的客户端频繁的申请加锁、释放锁,对于 ZK 集群的压力会比较大。

小结:综上所述,Redis 和 ZK 都有其优缺点。我们在做技术选型的时候可以根据这些问题作为参考因素。

一些建议

通过前面的分析,实现分布式锁的两种常见方案:Redis 和 ZK,他们各有千秋。应该如何选型呢?

就个人而言的话,我比较推崇 ZK 实现的锁:因为 Redis 是有可能存在隐患的,可能会导致数据不对的情况。但是,怎么选用要看具体在公司的场景了。

如果公司里面有 ZK 集群条件,优先选用 ZK 实现,但是如果说公司里面只有 Redis 集群,没有条件搭建 ZK 集群。

那么其实用 Redis 来实现也可以,另外还可能是系统设计者考虑到了系统已经有 Redis,但是又不希望再次引入一些外部依赖的情况下,可以选用 Redis。这个是要系统设计者基于架构来考虑了。

参考地址

增量同步

rsync [args] SRC [DEST]
情形:同时维护着两份不同的data_center,但以old_data_center为标准。因为权限的缘故没有开启rsync自动同步,只是每隔一段时间手动同步一下。SRCDEST都是采用mount形式,如果每一次都完整地copy,耗时很长,这时候就想到采用增量同步的方法,因为两份data_center同时由不同人维护,所以内容略有不同,data_center同步的时候不光要完全同步old_data_center的所有内容,而且要删除自身多余的内容,保持完全一致。

http://static.cyblogs.com/1559267-20190509170938551-832776092.png

1
2
3
rsync -a 
--delete
--progress /old_vip_data_center/test_envs/trainer/resource /vip_data_center/test_envs/trainer/resource/

–delete: 删除DEST端存在但是SRC端不存在的文件,如果不使用此参数,则DEST端会同步SRC端的文件,但DEST端已有的文件不受影响。

快速删除大量文件

  1. 先建一个空目录,随便位置

    1
    mkdir /local/empty_dir
  2. 用rsync删除目标目录

    1
    rsync --delete-before -avH --progress /local/empty_dir/ /local/trainer_test/

trainer_test清空之后可以再用rm -rf trainer_test删除

注意不要忘了文件夹最后的/

rsync提供了一些跟删除相关的参数

1
2
3
rsync --help | grep delete
--del an alias for --delete-during
--delete-before receiver deletes before transfer (default)

选项说明:
-a 递归方式传输文件,并保持文件属性
–delete-before 接收者在传输之前进行删除操作
–progress 在传输时显示传输过程
– 归档模式,表示以递归方式传输文件,并保持所有文件属性
-H 保持硬连接的文件
-v 详细输出模式
-stats 给出某些文件的传输状态

不过在使用上面的命令进行清理时,存在一个问题,清空后,目标目录的权限会和源目录的权限一样。如:/tmp/emptyroot:root,而maildrop之前是postfix:postdrop ,执行之后也会maildrop目录的权限也会变成root:root 。由于-a权限是-rlptogD几个参数的集合,所以可以将og(owner:group)两个参数去掉。清空时自动保持之前的目录权限,如下:

1
rsync --delete -rlptD /tmp/empty/ /var/spool/postfix/maildrop/

为什么rsync这么快呢?

rm删除内容时,将目录的每一个条目逐个删除(unlink),需要循环重复操作很多次;

rsync删除内容时,建立好新的空目录,替换掉老目录,基本没开销。

If you want to conquer fear, don’t sit home and think about it. Go out and get busy.

实战

今天因为用代码生成SQL脚本的时候,本来是说100W的数据生成一个的,结果因为一个运算符的问题导致了生成上百万的小文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while ((line = br.readLine()) != null) {
if (count < skipHeadCount) {
count++;
continue;
}
// 每MAX_SIZE就会生成一个,MAX_SIZE=1000000
int fileExtName = (count - skipHeadCount) / MAX_SIZE; // 当时种类count - skipHeadCount忘记打括号了
if (fileExtName > currentFileExtName) {
bw.flush();
currentFileExtName = fileExtName;
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(String.format(fileOutputPath, currentFileExtName)))));
}
String formatStr = genService.format(line);
bw.write(formatStr);
bw.newLine();
log.info("count:{}", count);
count++;
}

删除的时候会报错

1
Argument list too long

实战后发现效率贵高的一种方式:

http://static.cyblogs.com/WechatIMG461.png

参考地址

CPU飚高分析

一般可以使用

  • ps -Lfp pid
  • ps -mp pid -o THREAD, tid, time
  • top -Hp pid
1
2
3
4
5
6
7
8
9
10
[root@redis webapps]# top -Hp 22272
top - 10:09:30 up 9 days, 22:10, 1 user, load average: 0.00, 0.00, 0.00
Tasks: 30 total, 0 running, 30 sleeping, 0 stopped, 0 zombie
Cpu(s): 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 3923196k total, 3795588k used, 127608k free, 153056k buffers
Swap: 6160376k total, 0k used, 6160376k free, 3079244k cached

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
22272 root 20 0 2286m 122m 11m S 0.0 3.2 0:00.00 java
22278 root 20 0 2286m 122m 11m S 0.0 3.2 0:00.00 java

TIME列就是各个Java线程耗费的CPU时间,CPU时间最长的是线程ID为22283 的线程。

printf "%x\n" 22283

1
2
[root@redis webapps]# printf ‘%x\n’ 22283
570b得到22283 的十六进制值为570b。

下一步轮到jstack上场了,它用来输出进程22272 的堆栈信息,然后根据线程ID的十六进制值grep,如下:

1
2
[root@redis webapps]# jstack 22272 | grep 570b
“SchedulerThread” prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait()

可以看到CPU消耗在SchedulerThread这个类的Object.wait(),定位到下面的代码:

1
2
3
4
5
6
7
8
9
10
// Idle wait
synchronized(sigLock) {
try {
if(!halted.get()) {
sigLock.wait(timeUntilContinue);
}
}
catch (InterruptedException ignore) {
}
}

它是轮询任务的空闲等待代码,上面的sigLock.wait(timeUntilContinue)就对应了前面的Object.wait()。

总结:可以通过PID找到对应的的线程,然后通过JVM的jstack找到栈里对应的线程信息。通过找到对应的代码一般就能分析出CPU占用高的原因。

利用JVM命令分析

jstat -gcutil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@xxxx-nccz8-b57dd64fc-nt9dj logs]# jstat -gcutil 1 2000 20
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 8.64 2.62 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 2.81 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 2.81 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 2.81 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 3.06 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 3.10 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 3.10 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 3.21 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 3.22 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 3.61 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 6.47 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 6.76 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 6.81 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.07 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.08 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.38 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.38 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.62 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.62 27.32 80.59 73.86 1350 22.705 5 1.449 24.154
0.00 8.64 7.76 27.32 80.59 73.86 1350 22.705 5 1.449 24.154

S0:Heap上的 Survivor space 0 区已使用空间的百分比
S1:Heap上的 Survivor space 1 区已使用空间的百分比
E:Heap上的 Eden space 区已使用空间的百分比
O:Heap上的 Old space 区已使用空间的百分比
M:Metaspace 区已使用空间的百分比
YGC:从应用程序启动到采样时发生 Young GC 的次数
YGCT:从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC:从应用程序启动到采样时发生 Full GC 的次数
FGCT:从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT:从应用程序启动到采样时用于垃圾回收的总时间(单位秒)

jmap -heap
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
[root@xxxx-nccz8-b57dd64fc-nt9dj startup]# jmap -heap 1
Attaching to process ID 1, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.191-b12

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 805306368 (768.0MB)
MaxNewSize = 805306368 (768.0MB)
OldSize = 1342177280 (1280.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 268435456 (256.0MB)
CompressedClassSpaceSize = 260046848 (248.0MB)
MaxMetaspaceSize = 268435456 (256.0MB)
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 724828160 (691.25MB)
used = 284988360 (271.7860794067383MB)
free = 439839800 (419.4639205932617MB)
39.318058503687276% used
Eden Space:
capacity = 644349952 (614.5MB)
used = 275398000 (262.63999938964844MB)
free = 368951952 (351.86000061035156MB)
42.74043928228616% used
From Space:
capacity = 80478208 (76.75MB)
used = 9590360 (9.146080017089844MB)
free = 70887848 (67.60391998291016MB)
11.916716634644748% used
To Space:
capacity = 80478208 (76.75MB)
used = 0 (0.0MB)
free = 80478208 (76.75MB)
0.0% used
concurrent mark-sweep generation:
capacity = 1342177280 (1280.0MB)

通过heap命令能看出当前整个堆的一个使用情况,used与free的一个实际占用比。

jmap -dump
1
jmap -dump:live,format=b,file=xxxxxx.20200707.hprof 1

我们一般会在Dockerfile里面配置好如果出现OOM的情况,保留一下现场。-XX:HeapDumpPath=/alidata1/admin/xxxxx/logs

利用JProfiler分析

发现大对象,这里是因为我们用了Jeager链路跟踪,但是用过多线程导致ThreadLocal没有释放掉。

http://static.cyblogs.com/QQ20200507-183901@2x.jpg

发现char[]占用比较多,并且找出是从哪儿产生的?

http://static.cyblogs.com/QQ20200507-194442@2x.jpg

通过Outgoing references找到具体的实例情况。

http://static.cyblogs.com/QQ20200507-194529@2x.jpg

什么是outgoing references与incoming references?让我们通过示例来了解有关 Incoming references 和 Outgoing references 的更多知识。例如,一个应用程序的源代码如下所示:

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
public class A {     
private C c1 = C.getInstance();
}

public class B {
private C c2 = C.getInstance();
}

public class C {
private static C myC = new C();

public static C getInstance() {
return myC;
}

private D d1 = new D();
private E e1 = new E();
}

public class D {
}

public class E {
}

public class SimpleExample {
public static void main (String argsp[]) throws Exception {
A a = new A();
B b = new B();
}
}
  • 对象 A 和对象 B 持有对象 C 的引用
  • 对象 C 持有对象 D 和对象 E 的引用

在这个示例项目中,让我们具体分析下对象 C 的 Incoming references 和 Outgoing references 。

对象 C 的 Incoming References

拥有对象 C 的引用的所有对象都称为 Incoming references。在此示例中,对象 C 的“Incoming references”是对象 A、对象 B 和 C 的类对象 。

对象 C 的 Outgoing References

对象 C 引用的所有对象都称为 Outgoing References。在此示例中,对象 C 的“outgoing references”是对象 D、对象 E 和 C 的类对象。

然后通过Show in graph的菜单,一层一层的点击。直到你看到你最熟悉的类。

http://static.cyblogs.com/WechatIMG459.png

http://static.cyblogs.com/WechatIMG460.png

如何看GC日志

设置gc日志配置

1
2
3
4
5
6
7
8
-XX:+PrintGC 输出简要GC日志 
-XX:+PrintGCDetails 输出详细GC日志
-Xloggc:gc.log 输出GC日志到文件
-XX:+PrintGCTimeStamps 输出GC的时间戳(以JVM启动到当期的总时长的时间戳形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-verbose:gc
-XX:+PrintReferenceGC 打印年轻代各个引用的数量以及时长
-XX:+PrintGC

如果只设置-XX:+PrintGC那么打印的日志如下所示:

1
2
3
4
5
6
7
8
[GC (Allocation Failure)  61805K->9849K(256000K), 0.0041139 secs]

1、GC 表示是一次YGC(Young GC)
2、Allocation Failure 表示是失败的类型
3、61805K->9849K 表示年轻代从61805K降为9849K
4、256000K表示整个堆的大小
5、0.0041139 secs表示这次GC总计所用的时间
在JDK 8中,-verbose:gc是-XX:+PrintGC一个别称,日志格式等价与:-XX:+PrintGC,。
-XX:+PrintGCDetails
1
2
3
4
5
6
7
8
9
10
11
[GC (Allocation Failure) [PSYoungGen: 53248K->2176K(59392K)] 58161K->7161K(256000K), 0.0039189 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]
1、GC 表示是一次YGC(Young GC)
2、Allocation Failure 表示是失败的类型
3、PSYoungGen 表示年轻代大小
4、53248K->2176K 表示年轻代占用从53248K降为2176K
5、59392K表示年轻带的大小
6、58161K->7161K 表示整个堆占用从53248K降为2176K
7、256000K表示整个堆的大小
8、 0.0039189 secs 表示这次GC总计所用的时间
9、[Times: user=0.02 sys=0.01, real=0.00 secs] 分别表示,用户态占用时长,内核用时,真实用时。
时间保留两位小数,四舍五入。
-XX:+PrintGCTimeStamps
1
1.963: [GC (Allocation Failure)  61805K->9849K(256000K), 0.0041139 secs]

如果加上-XX:+PrintGCTimeStamps那么日志仅仅比1.1介绍的最前面多了一个时间戳: 1.963, 表示从JVM启动到打印GC时刻用了1.963秒。

-XX:+PrintGCDateStamps
1
2019-03-05T16:56:15.108+0800: [GC (Allocation Failure)  61805K->9849K(256000K), 0.0041139 secs]

如果加上-XX:+PrintGCDateStamps那么日志仅仅比1.1介绍的最前面多了一个日期时间: 2019-03-05T16:56:15.108+0800, 表示打印GC的时刻的时间是2019-03-05T16:56:15.108+0800。+0800表示是东8区。

CMS GC日志详细分析
1
2
3
4
5
6
7
8
9
10
11
12
[GC (CMS Initial Mark) [1 CMS-initial-mark: 19498K(32768K)] 36184K(62272K), 0.0018083 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.011/0.011 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[CMS-concurrent-abortable-preclean-start]
CMS: abort preclean due to time [CMS-concurrent-abortable-preclean: 0.558/5.093 secs] [Times: user=0.57 sys=0.00, real=5.09 secs]
[GC (CMS Final Remark) [YG occupancy: 16817 K (29504 K)][Rescan (parallel) , 0.0021918 secs][weak refs processing, 0.0000245 secs][class unloading, 0.0044098 secs][scrub symbol table, 0.0029752 secs][scrub string table, 0.0006820 secs][1 CMS-remark: 19498K(32768K)] 36316K(62272K), 0.0104997 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.007/0.007 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

CMS日志分为两个STW(stop the world)

分别是init remark(1) 与 remark(7)两个阶段。一般耗时比YGC长约10倍(个人经验)。

(1)、[GC (CMS Initial Mark) [1 CMS-initial-mark: 19498K(32768K)] 36184K(62272K), 0.0018083 secs][Times: user=0.01 sys=0.00, real=0.01 secs]

会STW(Stop The World),这时候的老年代容量为 32768K, 在使用到 19498K 时开始初始化标记。耗时短。

(2)、[CMS-concurrent-mark-start]

并发标记阶段开始

(3)、[CMS-concurrent-mark: 0.011/0.011 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

并发标记阶段花费时间。

(4)、[CMS-concurrent-preclean-start]

并发预清理阶段,也是与用户线程并发执行。虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。

(5)、[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

并发预清理阶段花费时间。

(6)、[CMS-concurrent-abortable-preclean-start] CMS: abort preclean due to time [CMS-concurrent-abortable-preclean: 0.558/5.093 secs][Times: user=0.57 sys=0.00, real=5.09 secs]

并发可中止预清理阶段,运行在并行预清理和重新标记之间,直到获得所期望的eden空间占用率。增加这个阶段是为了避免在重新标记阶段后紧跟着发生一次垃圾清除

(7)、[GC (CMS Final Remark) [YG occupancy: 16817 K (29504 K)][Rescan (parallel) , 0.0021918 secs][weak refs processing, 0.0000245 secs][class unloading, 0.0044098 secs][scrub symbol table, 0.0029752 secs][scrub string table, 0.0006820 secs][1 CMS-remark: 19498K(32768K)] 36316K(62272K), 0.0104997 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]

会STW(Stop The World),收集阶段,这个阶段会标记老年代全部的存活对象,包括那些在并发标记阶段更改的或者新创建的引用对象

(8)、[CMS-concurrent-sweep-start]

并发清理阶段开始,与用户线程并发执行。

(9)、[CMS-concurrent-sweep: 0.007/0.007 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

并发清理阶段结束,所用的时间。

(10)、[CMS-concurrent-reset-start]

开始并发重置。在这个阶段,与CMS相关数据结构被重新初始化,这样下一个周期可以正常进行。

(11)、[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

并发重置所用结束,所用的时间。

参考地址

背景

一直在使用Lombok以及MapStruct,但是对于它们能够在编译阶段直接生成实例代码却没有仔细了解过。最近刚好在部门内做了一次分享,也在这里对具体原理做一个详细阐述。

Lombok以及MapStruct实现大体思路

Lombok以及MapStruct都是通过在目标代码上标记注解,编译器能够根据注解生成对应的实现代码。比如Lombok在属性上标记@Getter,那么在这个Java Bean内就会生成对应属性的get方法。

本质上来说,不管是Lombok或者MapStruct,都是通过Java的一个标准API来实现的;这个API即为Pluggable Annotation Processing API,简称为JSR269

JSR269

借用JSR269官方原文定义(附带原文地址:https://jcp.org/en/jsr/detail?id=269):

J2SE 1.5 added a new Java language mechanism “annotations” that allows annotation types to be used to annotate classes, fields, and methods. These annotations are typically processed either by build-time tools or by run-time libraries to achieve new semantic effects. In order to support annotation processing at build-time, this JSR will define APIs to allow annotation processors to be created using a standard pluggable API. This will simplify the task of creating annotation processors and will also allow automation of the discovery of appropriate annotation processors for a given source file.
The specification will include at least two sections, a section of API modeling the Java programming language and a distinct section for declaring annotation processors and controlling how they are run. Since annotations are placed on program elements, an annotation processing framework needs to reflect program structure. Annotation processors will be able to specify what annotations they process and multiple processors will be able to run cooperatively.
The processors and program structure api can be accessed at build-time; i.e. this functionality supplements core reflection support for reading annotations.

译文如下:

J2SE 1.5 增加了一种新的Java语言机制”annotations“,它允许注解被用于类、字段以及方法上。这些注解由 build-time 工具以及 run-time 库处理,来达到新的语义效果。为了支持在 build-time 时处理注解,这个JSR定义了通用的插入式API用于创建标准的注解处理器。这将简化创建注解处理器的任务,并且还能够根据源文件自动匹配响应的注解处理器。
该规范将至少包含两个部分,一部分用于建模Java编程语言的API,另一部分用于声明注解处理器以及他们的运作机制。由于注解被用于程序元素上,一个注解处理框架需要反映程序结构。注解处理器将能指定哪些注解是它们可以处理的,以及多个注解处理器如何协同工作。
注解处理器以及程序框架api可以在 build-time 时访问;举个例子,此功能提供了核心反射支持用于读取注解(注:一般指从源码内读取注解或者只读取标注为source-only的注解)。

即,在J2SE 1.6版本加入的JSR269的主要点如下:

  • 专门用于支持 J2SE 1.5 无法处理的 build-time 注解处理场景,并定义通用注解处理API
  • 引入程序框架api

JSR269运行机制

javac/Mainmain/Mainmain/JavaCompilerprocessing/JavaProcessingEnvironmentprocessing/Roundprocessing/DiscoveredProcessorsnew main.Main("javac)编译参数传递Javac上下文创建JavaFileManager.preRegister预注册执行compile,并传递编译参数以及上下文参数有误显示帮助信息参数解析(获取class以及files并初始化注解处理器路径)无source.files.classesJavaCompiler实例创建传递源文件列表以及类列表不初始化初始化注解设置Processors从参数获取processorsprocessors初始化并赋值给discoveredProcsalt[ PROC为none值 ][ PROC非none值 ]处理注解将需要的package注解以及class注解加入待处理列表传递上下文、待处理的包类注解列表构造Round构造JavacRoundEnvironment,传递包类通过顶层的包类找到所有支持的注解通过变量annotationsPresent判断这个注解是否支持加入待处理列表;

matchedNames添加支持的注解名不做处理,继续下一步alt[ 注解支持 ][ 注解不支持 ]调用注解处理器的process方法标记注解处理器已被执行unmatchedAnnotations移除matchedNames内所有匹配的项不做处理,继续下一步alt[ 执行结果是为true ][ 执行结果为false ]不做处理,继续下一步

  • alt[ matchedNames是否不为空且处理器是否已被执行 ]
  • loop[ 循环discoveredProcs ]

一次round循环完成构建下一个roundloop[ 循环直到没有新文件产生(moreToDo方法) ]执行最后一次run方法所有执行过的processor会带上空注解集合参数再次执行一遍完成执行完成最后一次round判断本次是否存在错误,存在则标记errorStatus清理所有包类构建为最终编译使用的JavaCompiler错误数不为0返回编译器错误数为0日志记录无错误+1enterTreesalt[ errorStatus状态为true ][ erroStatus状态为false ]返回编译器执行编译完成执行错误执行正确alt[ 错误数大于0 ][ 为空 ]alt[ 解析的文件为空且含有注解处理参数 ][ 存在文件或者无注解处理参数 ]javac/Mainmain/Mainmain/JavaCompilerprocessing/JavaProcessingEnvironmentprocessing/Roundprocessing/DiscoveredProcessors

样例

构建三个模块:

  1. 注解处理类以及目标注解类
  2. 注解注册为服务
  3. 调用方
注解类以及处理类

注解类:

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
String value();
}

注解处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@SupportedAnnotationTypes(value = {"com.whatakitty.learn.jsr269.Test"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class AnnotationProcessor extends AbstractProcessor {

private Messager messager;
private AtomicInteger atomicInteger;

@Override
public void init(ProcessingEnvironment env) {
messager = env.getMessager();
atomicInteger = new AtomicInteger(0);
super.init(env);
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(Diagnostic.Kind.NOTE, "Hello World!" + atomicInteger.incrementAndGet());
return true;
}
}
注册服务

在文件夹resources/META-INF/services下创建文件javax.annotation.processing.Processor,内容如下:

1
com.whatakitty.learn.jsr269.AnnotationProcessor
调用方
1
2
3
4
5
6
7
8
9
10
11
12
public class Test1 {

public static void main(String[] args) throws Exception {
System.out.println("success");
test();
}

@Test("method is test")
public static void test() throws Exception {
}

}
运行结果

http://static.cyblogs.com/191455.jpg

可以看到,上图中已经输出两次Hello World!。至于为什么会输出两次,是由于第一次是本身注解的处理调用;最后一次是,jdk会在所有注解处理完成后,将所有处理过的注解全部传入空注解再次执行一遍,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Run all remaining processors on the procStateList that
* have not already run this round with an empty set of
* annotations.
*/
public void runContributingProcs(RoundEnvironment re) {
if (!onProcInterator) {
// 构造空的注解元素集合
Set<TypeElement> emptyTypeElements = Collections.emptySet();
// 遍历所有注册的注解处理器
while(innerIter.hasNext()) {
ProcessorState ps = innerIter.next();
// 判断该注解处理器是否在之前处理过注解,未参与过的不会调用
if (ps.contributed)
// 传入空的注解元素集合,重新调用一次注解处理器
callProcessor(ps.processor, emptyTypeElements, re);
}
}
}

Lombok原理

Lombok基于JSR269 API实现了通过特定注解生成对应代码的功能。

Lombok主要在类LombokProcessor处理了自己的注解通过AST生成代码。如下,主要看两个重写方法:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 初始化本次处理的一些变量等
@Override public void init(ProcessingEnvironment procEnv) {
super.init(procEnv);
// 判断lombok是否被禁用
if (System.getProperty("lombok.disable") != null) {
lombokDisabled = true;
return;
}

this.processingEnv = procEnv;
this.javacProcessingEnv = getJavacProcessingEnvironment(procEnv);
this.javacFiler = getJavacFiler(procEnv.getFiler());

// 替换类加载器、对netbeans IDE相关hook处理、替换JavaFileManager
placePostCompileAndDontMakeForceRoundDummiesHook();
trees = Trees.instance(javacProcessingEnv);
transformer = new JavacTransformer(procEnv.getMessager(), trees);
// 获取标记HandlerPriority注解的所有优先级
SortedSet<Long> p = transformer.getPriorities();
if (p.isEmpty()) {
this.priorityLevels = new long[] {0L};
this.priorityLevelsRequiringResolutionReset = new HashSet<Long>();
} else {
this.priorityLevels = new long[p.size()];
int i = 0;
// 循环所有获取到的注释或者visit处理优先级
for (Long prio : p) this.priorityLevels[i++] = prio;
this.priorityLevelsRequiringResolutionReset = transformer.getPrioritiesRequiringResolutionReset();
}
}

/** {@inheritDoc} */
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 如果禁用,则不处理lombok注解
if (lombokDisabled) return false;
// 是否已经处理结束
if (roundEnv.processingOver()) {
cleanup.run();
return false;
}

// We have: A sorted set of all priority levels: 'priorityLevels'

// Step 1: Take all CUs which aren't already in the map. Give them the first priority level.

String randomModuleName = null;

// 标记所有编译单元的优先级
// 如果是第二次循环,因为roots已经包含了这个编译单元,所以会忽略
for (Element element : roundEnv.getRootElements()) {
if (randomModuleName == null) randomModuleName = getModuleNameFor(element);
JCCompilationUnit unit = toUnit(element);
if (unit == null) continue;
if (roots.containsKey(unit)) continue;
roots.put(unit, priorityLevels[0]);
}

while (true) {
// Step 2: For all CUs (in the map, not the roundEnv!), run them across all handlers at their current prio level.

// 循环优先级列表
for (long prio : priorityLevels) {
List<JCCompilationUnit> cusForThisRound = new ArrayList<JCCompilationUnit>();
// 获取在该优先级下的所有编译单元并加入该优先级下需要处理的编译单元列表内
for (Map.Entry<JCCompilationUnit, Long> entry : roots.entrySet()) {
Long prioOfCu = entry.getValue();
if (prioOfCu == null || prioOfCu != prio) continue;
cusForThisRound.add(entry.getKey());
}
// 按照优先级顺序执行编译单元
// 访问AST树并编译目标注解
transformer.transform(prio, javacProcessingEnv.getContext(), cusForThisRound, cleanup);
}

// Step 3: Push up all CUs to the next level. Set level to null if there is no next level.

// 排除掉列表第一个优先级准备执行下一次循环
Set<Long> newLevels = new HashSet<Long>();
for (int i = priorityLevels.length - 1; i >= 0; i--) {
Long curLevel = priorityLevels[i];
Long nextLevel = (i == priorityLevels.length - 1) ? null : priorityLevels[i + 1];
List<JCCompilationUnit> cusToAdvance = new ArrayList<JCCompilationUnit>();
for (Map.Entry<JCCompilationUnit, Long> entry : roots.entrySet()) {
if (curLevel.equals(entry.getValue())) {
cusToAdvance.add(entry.getKey());
newLevels.add(nextLevel);
}
}
for (JCCompilationUnit unit : cusToAdvance) {
roots.put(unit, nextLevel);
}
}
newLevels.remove(null);

// Step 4: If ALL values are null, quit. Else, either do another loop right now or force a resolution reset by forcing a new round in the annotation processor.

// 判断是否将所有优先级排除,优先级列表为空,则结束
if (newLevels.isEmpty()) return false;
newLevels.retainAll(priorityLevelsRequiringResolutionReset);
if (!newLevels.isEmpty()) {
// Force a new round to reset resolution. The next round will cause this method (process) to be called again.
forceNewRound(randomModuleName, javacFiler);
return false;
}
// None of the new levels need resolution, so just keep going.
}
}

init方法主要是做一些初始化;

process方法内主要是将注解以及visitor处理器的按照优先级划分,然后每次执行完成后,排除最开始的一个优先级后,重新开始下一轮编译。知道所有优先级排除完毕。这么做的原因,应该是为了在高优先级处理器处理完成生成文件后,能够让低优先级处理器根据高优先级处理器生成的文件重新执行一遍防止遗漏生成的新的代码

总结

  • 详细了解了JSR269内部的执行逻辑
  • 了解了JAVAC的编译过程
  • 了解了Lombok内部的执行原理,可以依托现有Lombok处理器,自定义注解

参考地址

本地安装GDB

1
2
3
4
5
6
7
8
brew install gdb

➜ ~ gdb --version
GNU gdb (GDB) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

除了这个,在Mac系统系统里面还要配置证书相关的操作。

按入下步骤创建代码签名的证书:

  1. 打开 Keychain Access 应用程序(/Applications/Utilities/Keychain Access.app)
  2. 执行菜单 钥匙串访问 -> 证书助理 -> 创建证书
  3. 填写如下信息:
    • 名称:gdb_codesign
    • 身份类型:自签名根证书
    • 证书类型:代码签名
    • 钩选:让我覆盖这些默认设置
      http://static.cyblogs.com/QQ20200524-221553@2x.jpg
  4. 一路确定,直到指定证书位置的步骤,选择系统
    http://static.cyblogs.com/QQ20200524-221644@2x.jpg
  5. 点击“创建”,会提示用输入系统登录密码,创建完成
  6. 钥匙串访问程序中,选择左侧栏的系统我的证书,找到你刚刚创建的gdb_codesign证书并双击打开证书信息窗口,展开信任项,设置使用此证书时:始终信任
  7. 关闭证书信息窗口,系统会再次要求输入系统登录密码。

因为我现在的系统是MacOS Catania,是在 Mojave (10.14) 之后的系统。所以还需要创建一个配置文件gdb-entitlement.xml,其内容如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.debugger</key>
<true/>
</dict>
</plist>
</pre>

最后执行命令:

1
➜  Desktop  codesign --entitlements gdb-entitlement.xml -fs gdb_codesign $(which gdb)

终端中 gdb 断点进入源码调试 hotspot

编译class
1
2
3
4
5
6
7
# 在我的桌面创建一个Test.java文件
vim Test.java
public class Test{
public static void main(String[] args){
System.out.println("hello world !");
}
}

找到我对应的openjdk8的build地址

1
/Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk

利用javacjava命令运行Test.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
50
51
52
➜  Desktop  /Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk/bin/javac Test.java
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGILL (0x4) at pc=0x000000010267c7eb, pid=89116, tid=0x0000000000004f03
#
# JRE version: OpenJDK Runtime Environment (8.0) (build 1.8.0-internal-chenyuan_2020_05_24_03_17-b00)
# Java VM: OpenJDK 64-Bit Server VM (25.71-b00 mixed mode bsd-amd64 compressed oops)
# Problematic frame:
# V [libjvm.dylib+0x47c7eb] PerfDataManager::destroy()+0xab
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/chenyuan/Desktop/hs_err_pid89116.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
#

[error occurred during error reporting , id 0x4]

[1] 89116 abort Test.java
➜ Desktop ll
total 112
-rw-r--r-- 1 chenyuan staff 415B May 24 21:33 Test.class
-rw-r--r-- 1 chenyuan staff 116B May 24 21:31 Test.java
-rw-r--r-- 1 chenyuan staff 46K May 24 21:33 hs_err_pid89116.log
➜ Desktop /Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk/bin/java Test
hello world ! ------- 牛逼的打印,无敌的HelloWorkd
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGILL (0x4) at pc=0x000000010be7c7eb, pid=89512, tid=0x0000000000002403
#
# JRE version: OpenJDK Runtime Environment (8.0) (build 1.8.0-internal-chenyuan_2020_05_24_03_17-b00)
# Java VM: OpenJDK 64-Bit Server VM (25.71-b00 mixed mode bsd-amd64 compressed oops)
# Problematic frame:
# V [libjvm.dylib+0x47c7eb] PerfDataManager::destroy()+0xab
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/chenyuan/Desktop/hs_err_pid89512.log
#
# If you would like to submit a bug report, please visit:
# http://bugreport.java.com/bugreport/crash.jsp
#

[error occurred during error reporting , id 0x4]

[1] 89512 abort Test
gdb测试
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
➜  Desktop  gdb --args /Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk/bin/java Test
GNU gdb (GDB) 9.1
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin19.3.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk/bin/java...
(gdb) break init.cpp:95
No source file named init.cpp.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (init.cpp:95) pending.
(gdb) run
Starting program: /Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk/bin/java Test
[New Thread 0x1803 of process 22052]
[New Thread 0x2503 of process 22052]
[New Thread 0x2403 of process 22052]
warning: unhandled dyld version (16)
[New Thread 0x180f of process 22052]
[New Thread 0x2303 of process 22052]

Thread 4 received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x180f of process 22052]
0x00000001040002b4 in ?? ()
(gdb) l
111 // add one more to mark the end
112 margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
113 {
114 int i = 0;
115 StdArg *stdargs = JLI_GetStdArgs();
116 for (i = 0 ; i < margc ; i++) {
117 margv[i] = stdargs[i].arg;
118 }
119 margv[i] = NULL;
120 }
(gdb) quit
A debugging session is active.

Inferior 1 [process 22052] will be killed.

Quit anyway? (y or n) y

我在这里发现l这里查看代码跟我debug的地方并不同,我就看看日志发现日志中当时有一个提示:No source file named init.cpp. 然后又找了一翻文章,找到这个时候当时编译的时候没有添加g参数。详细请看:https://blog.csdn.net/wenceng9/article/details/21372265 (我是在不想再重新编译一次了,因为想早点睡觉。哈哈~)

Clion中调试不香吗?

打开 clion,选择 File->ImportProject,选择到 /Users/chenyuan/Workspaces/Openjdk/openjdk8/hotspot 作为 jvm 源码的根目录,这里导入的过程无脑点击 next 即可

对于可能遇到的头文件不包含问题,解决如下:

clion 导入源码之后遇到头文件找不到的问题,而实际上这些头文件在源码里面是存在的,只不过在某些源文件里面是以相对路径的方式来搜索,可以在 CMakeLists.txt 里面添加一些根路径。

http://static.cyblogs.com/QQ20200524-223658@2x.jpg

1
2
3
4
include_directories(./src/share/vm)
include_directories(./src/cpu/x86/vm)
include_directories(./src/share/vm/precompiled)
include_directories(./src/share/vm/utilities)

另外,如果某些头文件依然找不到,可以手工导入,然后把导入的头文件加到
hotspot/src/share/vm/precompiled/precompiled.hpp 里,因为大多数源文件都会包含这个源文件

http://static.cyblogs.com/QQ20200524-223840@2x.jpg

1
2
3
4
5
6
7
8
# include <cstdlib>
# include <cstdint>
# include "register_x86.hpp"
# include "assembler_x86.hpp"
# include "globalDefinitions.hpp"
# include "globalDefinitions_x86.hpp"
# include "assembler_x86.hpp"
# include <stubRoutines_x86.hpp>

进入如下界面,添加 Application:openjdk8Execuable 中选择/Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/jdk/bin

http://static.cyblogs.com/QQ20200524-224033@2x.jpg

配置完成后,就可以执行openjdk8了。

http://static.cyblogs.com/QQ20200524-224305@2x.jpg

参考地址

1.什么是进程?

进程是系统中正在运行的一个程序,程序一旦运行就是进程。

进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

2.什么是线程?

是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

3.线程的实现方式?

1.继承Thread类

2.实现Runnable接口

3.使用Callable和Future

4.Thread 类中的start() 和 run() 方法有什么区别?

1.start()方法来启动线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码;通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。 2.run()方法当作普通方法的方式调用。程序还是要顺序执行,要等待run方法体执行完毕后,才可继续执行下面的代码; 程序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

5.线程NEW状态

new创建一个Thread对象时,并没处于执行状态,因为没有调用start方法启动改线程,那么此时的状态就是新建状态。

6.线程RUNNABLE状态

线程对象通过start方法进入runnable状态,启动的线程不一定会立即得到执行,线程的运行与否要看cpu的调度,我们把这个中间状态叫可执行状态(RUNNABLE)。

7.线程的RUNNING状态

一旦cpu通过轮询货其他方式从任务可以执行队列中选中了线程,此时它才能真正的执行自己的逻辑代码。

8.线程的BLOCKED状态

线程正在等待获取锁。

  • 进入BLOCKED状态,比如调用了sleep,或者wait方法
  • 进行某个阻塞的io操作,比如因网络数据的读写进入BLOCKED状态
  • 获取某个锁资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态

9.线程的TERMINATED状态

TERMINATED是一个线程的最终状态,在该状态下线程不会再切换到其他任何状态了,代表整个生命周期都结束了。

下面几种情况会进入TERMINATED状态:

  • 线程运行正常结束,结束生命周期
  • 线程运行出错意外结束
  • JVM Crash 导致所有的线程都结束

10.线程状态转化图

http://static.cyblogs.com/171dad791bf589b8.png

11.i–与System.out.println()的异常

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class XkThread extends Thread {

private int i = 5;

@Override
public void run() {
System.out.println("i=" + (i——) + " threadName=" + Thread.currentThread().getName());
}

public static void main(String[] args) {
XkThread xk = new XkThread();
Thread t1 = new Thread(xk);
Thread t2 = new Thread(xk);
Thread t3 = new Thread(xk);
Thread t4 = new Thread(xk);
Thread t5 = new Thread(xk);

t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
}
}

结果:

1
2
3
4
5
i=5 threadName=Thread-1
i=2 threadName=Thread-5
i=5 threadName=Thread-2
i=4 threadName=Thread-3
i=3 threadName=Thread-4

虽然println()方法在内部是同步的,但i–的操作却是在进入println()之前发生的,所以有发生非线程安全的概率。

println()源码:

1
2
3
4
5
6
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}

12.如何知道代码段被哪个线程调用?

1
System.out.println(Thread.currentThread().getName());

13.线程活动状态?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class XKThread extends Thread {

@Override
public void run() {
System.out.println("run run run is " + this.isAlive() );
}

public static void main(String[] args) {
XKThread xk = new XKThread();
System.out.println("begin ——— " + xk.isAlive());
xk.start();
System.out.println("end ————— " + xk.isAlive());

}
}

14.sleep()方法

方法sleep()的作用是在指定的毫秒数内让当前的“正在执行的线程”休眠(暂停执行)。

15.如何优雅的设置睡眠时间?

jdk1.5 后,引入了一个枚举TimeUnit,对sleep方法提供了很好的封装。

比如要表达2小时22分55秒899毫秒。

1
2
3
4
5
Thread.sleep(8575899L);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(22);
TimeUnit.SECONDS.sleep(55);
TimeUnit.MILLISECONDS.sleep(899);

可以看到表达的含义更清晰,更优雅。

16.停止线程

run方法执行完成,自然终止。

stop()方法,suspend()以及resume()都是过期作废方法,使用它们结果不可预期。

大多数停止一个线程的操作使用Thread.interrupt()等于说给线程打一个停止的标记, 此方法不回去终止一个正在运行的线程,需要加入一个判断才能可以完成线程的停止。

17.interrupted 和 isInterrupted

interrupted : 判断当前线程是否已经中断,会清除状态。

isInterrupted :判断线程是否已经中断,不会清除状态。

18.yield

放弃当前cpu资源,将它让给其他的任务占用cpu执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得cpu时间片。

测试代码:(cpu独占时间片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class XKThread extends Thread {

@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时 = " + (endTime - beginTime) + " 毫秒! ");
}

public static void main(String[] args) {
XKThread xkThread = new XKThread();
xkThread.start();
}

}

结果:

1
用时 = 20 毫秒! 

加入yield,再来测试。(cpu让给其他资源导致速度变慢)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class XKThread extends Thread {

@Override
public void run() {
long beginTime = System.currentTimeMillis();
int count = 0;
for (int i = 0; i < 50000000; i++) {
Thread.yield();
count = count + (i + 1);
}
long endTime = System.currentTimeMillis();
System.out.println("用时 = " + (endTime - beginTime) + " 毫秒! ");
}

public static void main(String[] args) {
XKThread xkThread = new XKThread();
xkThread.start();
}

}

结果:

1
用时 = 38424 毫秒! 

19.线程的优先级

在操作系统中,线程可以划分优先级,优先级较高的线程得到cpu资源比较多,也就是cpu有限执行优先级较高的线程对象中的任务,但是不能保证一定优先级高,就先执行。

Java的优先级分为1~10个等级,数字越大优先级越高,默认优先级大小为5。超出范围则抛出:java.lang.IllegalArgumentException。

20.优先级继承特性

线程的优先级具有继承性,比如a线程启动b线程,b线程与a优先级是一样的。

21.谁跑的更快?

设置优先级高低两个线程,累加数字,看谁跑的快,上代码。

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
public class Run extends Thread{

public static void main(String[] args) {
try {
ThreadLow low = new ThreadLow();
low.setPriority(2);
low.start();

ThreadHigh high = new ThreadHigh();
high.setPriority(8);
high.start();

Thread.sleep(2000);
low.stop();
high.stop();
System.out.println("low = " + low.getCount());
System.out.println("high = " + high.getCount());
} catch (InterruptedException e) {
e.printStackTrace();
}

}

}

class ThreadHigh extends Thread {
private int count = 0;

public int getCount() {
return count;
}


@Override
public void run() {
while (true) {
count++;
}
}
}

class ThreadLow extends Thread {
private int count = 0;

public int getCount() {
return count;
}


@Override
public void run() {
while (true) {
count++;
}
}
}

结果:

1
2
low  = 1193854568
high = 1204372373

22.线程种类

Java线程有两种,一种是用户线程,一种是守护线程。

23.守护线程的特点

守护线程是一个比较特殊的线程,主要被用做程序中后台调度以及支持性工作。当Java虚拟机中不存在非守护线程时,守护线程才会随着JVM一同结束工作。

24.Java中典型的守护线程

GC(垃圾回收器)

25.如何设置守护线程

Thread.setDaemon(true)

PS:Daemon属性需要再启动线程之前设置,不能再启动后设置。

25.Java虚拟机退出时Daemon线程中的finally块一定会执行?

Java虚拟机退出时Daemon线程中的finally块并不一定会执行。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class XKDaemon {
public static void main(String[] args) {
Thread thread = new Thread(new DaemonRunner(),"xkDaemonRunner");
thread.setDaemon(true);
thread.start();

}

static class DaemonRunner implements Runnable {

@Override
public void run() {
try {
SleepUtils.sleep(10);
} finally {
System.out.println("Java小咖秀 daemonThread finally run …");
}

}
}
}

结果:

1

没有任何的输出,说明没有执行finally。

26.设置线程上下文类加载器

​ 获取线程上下文类加载器

1
public ClassLoader getContextClassLoader() 

​ 设置线程类加载器(可以打破Java类加载器的父类委托机制)

1
public void setContextClassLoader(ClassLoader cl)

27.join

join是指把指定的线程加入到当前线程,比如join某个线程a,会让当前线程b进入等待,直到a的生命周期结束,此期间b线程是处于blocked状态。

28.什么是synchronized?

synchronized关键字可以时间一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对想的所有读写都将通过同步的方式来进行。

29.synchronized包括哪两个jvm重要的指令?

monitor enter 和 monitor exit

30.synchronized关键字用法?

可以用于对代码块或方法的修饰

31.synchronized锁的是什么?

普通同步方法 —————> 锁的是当前实力对象。

静态同步方法—————> 锁的是当前类的Class对象。

同步方法快 —————> 锁的是synchonized括号里配置的对象。

32.Java对象头

synchronized用的锁是存在Java对象头里的。对象如果是数组类型,虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,用2字宽存储对象头。

Tips:32位虚拟机中一个字宽等于4字节。

33.Java对象头长度

http://static.cyblogs.com/QQ20200506-230203@2x.jpg

34.Java对象头的存储结构

32位JVM的Mark Word 默认存储结构

http://static.cyblogs.com/QQ20200506-230326@2x.jpg

35.Mark Word的状态变化

Mark Word 存储的数据会随着锁标志为的变化而变化。

http://static.cyblogs.com/QQ20200506-230421@2x.jpg

64位虚拟机下,Mark Word是64bit大小的

http://static.cyblogs.com/QQ20200506-230503@2x.jpg

36.锁的升降级规则

Java SE 1.6 为了提高锁的性能。引入了“偏向锁”和轻量级锁“。

Java SE 1.6 中锁有4种状态。级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

锁只能升级不能降级。

37.偏向锁

大多数情况,锁不仅不存在多线程竞争,而且总由同一线程多次获得。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行 cas操作来加锁和解锁,只需测试一下对象头 Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁,如果失败,则需要测试下Mark Word中偏向锁的标示是否已经设置成1(表示当前时偏向锁),如果没有设置,则使用cas竞争锁,如果设置了,则尝试使用cas将对象头的偏向锁只想当前线程。

38.关闭偏向锁延迟

java6和7中默认启用,但是会在程序启动几秒后才激活,如果需要关闭延迟,

-XX:BiasedLockingStartupDelay=0。

39.如何关闭偏向锁

JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

Tips:如果你可以确定程序的所有锁通常情况处于竞态,则可以选择关闭。

40.轻量级锁

线程在执行同步块,jvm会现在当前线程的栈帧中创建用于储存锁记录的空间。并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用cas将对象头中的Mark Word替换为之乡锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

41.轻量锁的解锁

轻量锁解锁时,会使原子操作cas将 displaced Mark Word 替换回对象头,如果成功则表示没有竞争发生,如果失败,表示存在竞争,此时锁就会膨胀为重量级锁。

42.锁的优缺点对比

http://static.cyblogs.com/QQ20200506-230547@2x.jpg

43.什么是原子操作

不可被中断的一个或一系列操作

44.Java如何实现原子操作

Java中通过锁和循环cas的方式来实现原子操作,JVM的CAS操作利用了处理器提供的CMPXCHG指令来实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

45.CAS实现原子操作的3大问题

ABA问题,循环时间长消耗资源大,只能保证一个共享变量的原子操作

46.什么是ABA问题

问题:

因为cas需要在操作值的时候,检查值有没有变化,如果没有变化则更新,如果一个值原来是A,变成了B,又变成了A,那么使用cas进行检测时会发现发的值没有发生变化,其实是变过的。

解决:

添加版本号,每次更新的时候追加版本号,A-B-A —> 1A-2B-3A。

从jdk1.5开始,Atomic包提供了一个类AtomicStampedReference来解决ABA的问题。

47.CAS循环时间长占用资源大问题

如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。

一、它可以延迟流水线执行指令(de-pipeline),使cpu不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,有些处理器延迟时间是0。

二、它可以避免在退出循环的时候因内存顺序冲突而引起的cpu流水线被清空,从而提高cpu执行效率。

48.CAS只能保证一个共享变量原子操作

一、对多个共享变量操作时,可以用锁。

二、可以把多个共享变量合并成一个共享变量来操作。比如,x=1,k=a,合并xk=1a,然后用cas操作xk。

Tips:java 1.5开始,jdk提供了AtomicReference类来保证饮用对象之间的原子性,就可以把多个变量放在一个对象来进行cas操作。

49.volatile关键字

volatile 是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性“。

Java语言规范第3版对volatile定义如下,Java允许线程访问共享变量,为了保证共享变量能准确和一致的更新,线程应该确保排它锁单独获得这个变量。如果一个字段被声明为volatile,Java线程内存模型所有线程看到这个变量的值是一致的。

50.等待/通知机制

一个线程修改了一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。

51.wait

方法wait()的作用是使当前执行代码的线程进行等待,wait()是Object类通用的方法,该方法用来将当前线程置入“预执行队列”中,并在 wait()所在的代码处停止执行,直到接到通知或中断为止。

在调用wait之前线程需要获得该对象的对象级别的锁。代码体现上,即只能是同步方法或同步代码块内。调用wait()后当前线程释放锁。

52.notify

notify()也是Object类的通用方法,也要在同步方法或同步代码块内调用,该方法用来通知哪些可能灯光该对象的对象锁的其他线程,如果有多个线程等待,则随机挑选出其中一个呈wait状态的线程,对其发出 通知 notify,并让它等待获取该对象的对象锁。

53.notify/notifyAll

notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。

54.等待/通知经典范式

等待

1
2
3
4
5
6
synchronized(obj) {
while(条件不满足) {
obj.wait();
}
执行对应逻辑
}

通知

1
2
3
4
synchronized(obj) {
改变条件
obj.notifyAll();
}

55.ThreadLocal

主要解决每一个线程想绑定自己的值,存放线程的私有数据。

56.ThreadLocal使用

获取当前的线程的值通过get(),设置set(T) 方式来设置值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class XKThreadLocal {

public static ThreadLocal threadLocal = new ThreadLocal();

public static void main(String[] args) {
if (threadLocal.get() == null) {
System.out.println("未设置过值");
threadLocal.set("Java小咖秀");
}
System.out.println(threadLocal.get());
}

}

输出:

1
2
未设置过值
Java小咖秀

Tips:默认值为null

57.解决get()返回null问题

通过继承重写initialValue()方法即可。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadLocalExt extends ThreadLocal{

static ThreadLocalExt threadLocalExt = new ThreadLocalExt();

@Override
protected Object initialValue() {
return "Java小咖秀";
}

public static void main(String[] args) {
System.out.println(threadLocalExt.get());
}
}

输出结果:

1
Java小咖秀

58.Lock接口

锁可以防止多个线程同时共享资源。Java5前程序是靠synchronized实现锁功能。Java5之后,并发包新增Lock接口来实现锁功能。

59.Lock接口提供 synchronized不具备的主要特性

http://static.cyblogs.com/QQ20200506-230803@2x.jpg

60.重入锁 ReentrantLock

支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

61.重进入是什么意思?

重进入是指任意线程在获取到锁之后能够再次获锁而不被锁阻塞。

该特性主要解决以下两个问题:

一、锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。

二、所得最终释放。线程重复n次是获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。

62.ReentrantLock默认锁?

默认非公平锁

代码为证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

63.公平锁和非公平锁的区别

公平性与否针对获取锁来说的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。

64.读写锁

读写锁允许同一时刻多个读线程访问,但是写线程和其他写线程均被阻塞。读写锁维护一个读锁一个写锁,读写分离,并发性得到了提升。

Java中提供读写锁的实现类是ReentrantReadWriteLock。

65.LockSupport工具

定义了一组公共静态方法,提供了最基本的线程阻塞和唤醒功能。

http://static.cyblogs.com/QQ20200506-230912@2x.jpg

66.Condition接口

提供了类似Object监视器方法,与 Lock配合使用实现等待/通知模式。

67.Condition使用

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class XKCondition {
Lock lock = new ReentrantLock();
Condition cd = lock.newCondition();

public void await() throws InterruptedException {
lock.lock();
try {
cd.await();//相当于Object 方法中的wait()
} finally {
lock.unlock();
}
}

public void signal() {
lock.lock();
try {
cd.signal(); //相当于Object 方法中的notify()
} finally {
lock.unlock();
}
}

}

68.ArrayBlockingQueue?

一个由数据支持的有界阻塞队列,此队列FIFO原则对元素进行排序。队列头部在队列中存在的时间最长,队列尾部存在时间最短。

69.PriorityBlockingQueue?

一个支持优先级排序的无界阻塞队列,但它不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。

70.DelayQueue?

是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现Delayed接口和 Comparable接口,在创建元素时可以指定多久才能从队列中获取当前元素。

71.Java并发容器,你知道几个?

ConcurrentHashMap、CopyOnWriteArrayList 、CopyOnWriteArraySet 、ConcurrentLinkedQueue、

ConcurrentLinkedDeque、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、

LinkedBlockingQueue、LinkedBlockingDeque、PriorityBlockingQueue、SynchronousQueue、

LinkedTransferQueue、DelayQueue

72.ConcurrentHashMap

并发安全版HashMap,java7中采用分段锁技术来提高并发效率,默认分16段。Java8放弃了分段锁,采用CAS,同时当哈希冲突时,当链表的长度到8时,会转化成红黑树。(如需了解细节,见jdk中代码)

73.ConcurrentLinkedQueue

基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。它采用cas算法来实现。(如需了解细节,见jdk中代码)

74.什么是阻塞队列?

阻塞队列是一个支持两个附加操作的队列,这两个附加操作支持阻塞的插入和移除方法。

1、支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。

2、支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。

75.阻塞队列常用的应用场景?

常用于生产者和消费者场景,生产者是往队列里添加元素的线程,消费者是从队列里取元素的线程。阻塞队列正好是生产者存放、消费者来获取的容器。

76.Java里的阻塞的队列

1
2
3
4
5
6
7
ArrayBlockingQueue:    数组结构组成的 |有界阻塞队列
LinkedBlockingQueue: 链表结构组成的|有界阻塞队列
PriorityBlockingQueue: 支持优先级排序|无界阻塞队列
DelayQueue: 优先级队列实现|无界阻塞队列
SynchronousQueue: 不存储元素| 阻塞队列
LinkedTransferQueue: 链表结构组成|无界阻塞队列
LinkedBlockingDeque: 链表结构组成|双向阻塞队列

77.Fork/Join

java7提供的一个用于并行执行任务的框架,把一个大任务分割成若干个小任务,最终汇总每个小任务结果的后得到大任务结果的框架。

78.工作窃取算法

是指某个线程从其他队列里窃取任务来执行。当大任务被分割成小任务时,有的线程可能提前完成任务,此时闲着不如去帮其他没完成工作线程。此时可以去其他队列窃取任务,为了减少竞争,通常使用双端队列,被窃取的线程从头部拿,窃取的线程从尾部拿任务执行。

79.工作窃取算法的有缺点

优点:充分利用线程进行并行计算,减少了线程间的竞争。

缺点:有些情况下还是存在竞争,比如双端队列中只有一个任务。这样就消耗了更多资源。

80.Java中原子操作更新基本类型,Atomic包提供了哪几个类?

AtomicBoolean:原子更新布尔类型

AtomicInteger:原子更新整形

AtomicLong:原子更新长整形

81.Java中原子操作更新数组,Atomic包提供了哪几个类?

AtomicIntegerArray: 原子更新整形数据里的元素

AtomicLongArray: 原子更新长整形数组里的元素

AtomicReferenceArray: 原子更新饮用类型数组里的元素

AtomicIntegerArray: 主要提供原子方式更新数组里的整形

82.Java中原子操作更新引用类型,Atomic包提供了哪几个类?

如果原子需要更新多个变量,就需要用引用类型了。

AtomicReference : 原子更新引用类型

AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。

AtomicMarkableReference: 原子更新带有标记位的引用类型。标记位用boolean类型表示,构造方法时AtomicMarkableReference(V initialRef,boolean initialMark)

83.Java中原子操作更新字段类,Atomic包提供了哪几个类?

AtomiceIntegerFieldUpdater: 原子更新整形字段的更新器

AtomiceLongFieldUpdater: 原子更新长整形字段的更新器

AtomiceStampedFieldUpdater: 原子更新带有版本号的引用类型,将整数值

84.JDK并发包中提供了哪几个比较常见的处理并发的工具类?

提供并发控制手段: CountDownLatch、CyclicBarrier、Semaphore

线程间数据交换: Exchanger

85.CountDownLatch

允许一个或多个线程等待其他线程完成操作。

CountDownLatch的构造函数接受一个int类型的参数作为计数器,你想等待n个点完成,就传入n。

两个重要的方法:

countDown() : 调用时,n会减1。

await() : 调用会阻塞当前线程,直到n变成0。

await(long time,TimeUnit unit) : 等待特定时间后,就不会继续阻塞当前线程。

tips:计数器必须大于等于0,当为0时,await就不会阻塞当前线程。

不提供重新初始化或修改内部计数器的值的功能。

86.CyclicBarrier

可循环使用的屏障。

让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier默认构造放时CyclicBarrier(int parities) ,其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。

87.CountDownLatch与CyclicBarrier区别

CountDownLatch:

计数器:计数器只能使用一次。

等待: 一个线程或多个等待另外n个线程完成之后才能执行。

CyclicBarrier:

计数器:计数器可以重置(通过reset()方法)。

等待: n个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。

88.Semaphore

用来控制同时访问资源的线程数量,通过协调各个线程,来保证合理的公共资源的访问。

应用场景:流量控制,特别是公共资源有限的应用场景,比如数据链接,限流等。

89.Exchanger

Exchanger是一个用于线程间协作的工具类,它提供一个同步点,在这个同步点上,两个线程可以交换彼此的数据。比如第一个线程执行exchange()方法,它会一直等待第二个线程也执行exchange,当两个线程都到同步点,就可以交换数据了。

一般来说为了避免一直等待的情况,可以使用exchange(V x,long timeout,TimeUnit unit),设置最大等待时间。

Exchanger可以用于遗传算法。

90.为什么使用线程池

几乎所有需要异步或者并发执行任务的程序都可以使用线程池。合理使用会给我们带来以下好处。

  • 降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
  • 提高响应速度: 当任务到达时,任务不需要等到线程创建就可以立即执行。
  • 提供线程可以管理性: 可以通过设置合理分配、调优、监控。

91.线程池工作流程

1、判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务。是->走下个流程。

2、判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。

3、判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务,

是->走下个流程。

4、按照设置的策略来处理无法执行的任务。

92.创建线程池参数有哪些,作用?

1
2
3
4
5
6
7
public ThreadPoolExecutor(   int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

1.corePoolSize:核心线程池大小,当提交一个任务时,线程池会创建一个线程来执行任务,即使其他空闲的核心线程能够执行新任务也会创建,等待需要执行的任务数大于线程核心大小就不会继续创建。

2.maximumPoolSize:线程池最大数,允许创建的最大线程数,如果队列满了,并且已经创建的线程数小于最大线程数,则会创建新的线程执行任务。如果是无界队列,这个参数基本没用。

3.keepAliveTime: 线程保持活动时间,线程池工作线程空闲后,保持存活的时间,所以如果任务很多,并且每个任务执行时间较短,可以调大时间,提高线程利用率。

4.unit: 线程保持活动时间单位,天(DAYS)、小时(HOURS)、分钟(MINUTES、毫秒MILLISECONDS)、微秒(MICROSECONDS)、纳秒(NANOSECONDS)

5.workQueue: 任务队列,保存等待执行的任务的阻塞队列。

一般来说可以选择如下阻塞队列:

ArrayBlockingQueue:基于数组的有界阻塞队列。

LinkedBlockingQueue:基于链表的阻塞队列。

SynchronizedQueue:一个不存储元素的阻塞队列。

PriorityBlockingQueue:一个具有优先级的阻塞队列。

6.threadFactory:设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

  1. handler: 饱和策略也叫拒绝策略。当队列和线程池都满了,即达到饱和状态。所以需要采取策略来处理新的任务。默认策略是AbortPolicy。

    AbortPolicy:直接抛出异常。

    CallerRunsPolicy: 调用者所在的线程来运行任务。

    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

    DiscardPolicy:不处理,直接丢掉。

    当然可以根据自己的应用场景,实现RejectedExecutionHandler接口自定义策略。

93.向线程池提交任务

可以使用execute()和submit() 两种方式提交任务。

execute():无返回值,所以无法判断任务是否被执行成功。

submit():用于提交需要有返回值的任务。线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程知道任务完成。get(long timeout,TimeUnit unit)可以设置超市时间。

94.关闭线程池

可以通过shutdown()或shutdownNow()来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt来中断线程,所以无法响应终端的任务可以能永远无法停止。

shutdownNow首先将线程池状态设置成STOP,然后尝试停止所有的正在执行或者暂停的线程,并返回等待执行任务的列表。

shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。

只要调用两者之一,isShutdown就会返回true,当所有任务都已关闭,isTerminaed就会返回true。

一般来说调用shutdown方法来关闭线程池,如果任务不一定要执行完,可以直接调用shutdownNow方法。

95.线程池如何合理设置

配置线程池可以从以下几个方面考虑。

  • 任务是cpu密集型、IO密集型或者混合型

  • 任务优先级,高中低。

  • 任务时间执行长短。

  • 任务依赖性:是否依赖其他系统资源。

    cpu密集型可以配置可能小的线程,比如 n + 1个线程。

    io密集型可以配置较多的线程,如 2n个线程。

    混合型可以拆成io密集型任务和cpu密集型任务,

    如果两个任务执行时间相差大,否->分解后执行吞吐量将高于串行执行吞吐量。

    否->没必要分解。

    可以通过Runtime.getRuntime().availableProcessors()来获取cpu个数。

    建议使用有界队列,增加系统的预警能力和稳定性。

96.Executor

从JDK5开始,把工作单元和执行机制分开。工作单元包括Runnable和Callable,而执行机制由Executor框架提供。

97.Executor框架的主要成员

ThreadPoolExecutor :可以通过工厂类Executors来创建。

可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool、CachedThreadPool。

ScheduledThreadPoolExecutor :可以通过工厂类Executors来创建。

可以创建2中类型的ScheduledThreadPoolExecutor:ScheduledThreadPoolExecutor、SingleThreadScheduledExecutor

Future接口:Future和实现Future接口的FutureTask类来表示异步计算的结果。

Runnable和Callable:它们的接口实现类都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。Runnable不能返回结果,Callable可以返回结果。

98.FixedThreadPool

可重用固定线程数的线程池。

查看源码:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

corePoolSize 和maxPoolSize都被设置成我们设置的nThreads。

当线程池中的线程数大于corePoolSize ,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止,如果设为0,表示多余的空闲线程会立即终止。

工作流程:

1.当前线程少于corePoolSize,创建新线程执行任务。

2.当前运行线程等于corePoolSize,将任务加入LinkedBlockingQueue。

3.线程执行完1中的任务,会循环反复从LinkedBlockingQueue获取任务来执行。

LinkedBlockingQueue作为线程池工作队列(默认容量Integer.MAX_VALUE)。因此可能会造成如下赢下。

1.当线程数等于corePoolSize时,新任务将在队列中等待,因为线程池中的线程不会超过corePoolSize。

2.maxnumPoolSize等于说是一个无效参数。

3.keepAliveTime等于说也是一个无效参数。

4.运行中的FixedThreadPool(未执行shundown或shundownNow))则不会调用拒绝策略。

5.由于任务可以不停的加到队列,当任务越来越多时很容易造成OOM。

99.SingleThreadExecutor

是使用单个worker线程的Executor。

查看源码:

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

corePoolSize和maxnumPoolSize被设置为1。其他参数和FixedThreadPool相同。

执行流程以及造成的影响同FixedThreadPool.

100.CachedThreadPool

根据需要创建新线程的线程池。

查看源码:

1
2
3
4
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());

corePoolSize设置为0,maxmumPoolSize为Integer.MAX_VALUE。keepAliveTime为60秒。

工作流程:

1.首先执行SynchronousQueue.offer (Runnable task)。如果当前maximumPool 中有空闲线程正在执行S ynchronousQueue.poll(keepAliveTIme,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute方 法执行完成;否则执行下面的步骤2。

  1. 当初始maximumPool为空或者maximumPool中当前没有空闲线程时,将没有线程执行 SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1将失 败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。

3.在步骤2中新创建的线程将任务执行完后,会执行SynchronousQueue.poll (keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

一般来说它适合处理时间短、大量的任务。

参考地址

Mac系统安装

1
2
3
4
利用brew search查找mercurial
➜ ~ brew search mercurial
安装
➜ ~ brew install mercuria

Linux系统安装

1
sudo apt install mercurial

安装openjdk8

添加代理

一般在下载代码的时候都会很慢,故先配置好代理。我这里是V2Ray。

1
2
3
4
5
6
vim /usr/local/etc/mercurial/hgrc

[http_proxy]
host=127.0.0.1:8001
[https_proxy]
host=127.0.0.1:8001
下载jdk8u的代码
1
2
3
4
5
6
hg clone https://hg.openjdk.java.net/jdk8u/jdk8u openjdk8
chmod u+x get_source.sh
chmod u+x configure
./get_source.sh # 下载全部源代码
./configure --with-freetype-include=/usr/local/include/freetype2 --with-freetype-lib=/usr/local/lib/ # configure 编译环境,若编译报错,需要添加 `--disable-warnings-as-errors`
make all

漫长的等待,终于等到你

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## Finished docs (build time 00:01:52)

----- Build times -------
Start 2020-05-24 03:18:37
End 2020-05-24 03:30:07
00:00:19 corba
00:00:46 demos
00:01:52 docs
00:02:53 hotspot
00:01:28 images
00:00:12 jaxp
00:00:18 jaxws
00:03:07 jdk
00:00:26 langtools
00:00:09 nashorn
00:11:30 TOTAL
-------------------------
Finished building OpenJDK for target 'all'

http://static.cyblogs.com/QQ20200524-035223@2x.jpg

如何在CLion里面做调试,后面我再详细的写一篇。敬请期待~

遇到的坑

问题一:Xcode 4 is required to build JDK 8
1
2
configure: error: Xcode 4 is required to build JDK 8, the version found was 11.4.1. Use --with-xcode-path to specify the location of Xcode 4 or make Xcode 4 active by using xcode-select.
configure exiting with result code 1

解决方案:

1
2
3
4
5
6
7
➜  openjdk8  vim common/autoconf/generated-configure.sh
# Fail-fast: verify we're building on Xcode 4, we cannot build with Xcode 5 or later
#XCODE_VERSION=`$XCODEBUILD -version | grep '^Xcode ' | sed 's/Xcode //'`
#XC_VERSION_PARTS=( ${XCODE_VERSION//./ } )
#if test ! "${XC_VERSION_PARTS[0]}" = "4"; then
# as_fn_error $? "Xcode 4 is required to build JDK 8, the version found was $XCODE_VERSION. Use --with-xcode-path to specify the l ocation of Xcode 4 or make Xcode 4 active by using xcode-select." "$LINENO" 5
#fi
问题二:A gcc compiler is required
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
configure: error: A gcc compiler is required. Try setting --with-tools-dir.
configure exiting with result code


# 第一处代码 elif test "x$TOOLCHAIN_TYPE" = xgcc; then
# gcc --version output typically looks like
# gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
# Copyright (C) 2013 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions. There is NO
# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
COMPILER_VERSION_OUTPUT=`$COMPILER --version 2>&1`
# Check that this is likely to be GCC.
$ECHO "$COMPILER_VERSION_OUTPUT" | $GREP "Free Software Foundation" > /dev/null
# 条件语句注释掉
# if test $? -ne 0; then
# { $as_echo "$as_me:${as_lineno-$LINENO}: The $COMPILER_NAME compiler (located as $COMPILER) does not seem to be the required $TOOLCHAIN_TYPE compiler." >&5
# $as_echo "$as_me: The $COMPILER_NAME compiler (located as $COMPILER) does not seem to be the required $TOOLCHAIN_TYPE compiler." >&6;}
# { $as_echo "$as_me:${as_lineno-$LINENO}: The result from running with --version was: \"$COMPILER_VERSION\"" >&5
# $as_echo "$as_me: The result from running with --version was: \"$COMPILER_VERSION\"" >&6;}
# as_fn_error $? "A $TOOLCHAIN_TYPE compiler is required. Try setting --with-tools-dir." "$LINENO" 5
# fi

# 第二处代码,
elif test "x$TOOLCHAIN_TYPE" = xgcc; then
# gcc --version output typically looks like
# gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1
# Copyright (C) 2013 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions. There is NO
# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
COMPILER_VERSION_OUTPUT=`$COMPILER --version 2>&1`
# Check that this is likely to be GCC.
$ECHO "$COMPILER_VERSION_OUTPUT" | $GREP "Free Software Foundation" > /dev/null
# 条件语句注释掉
# if test $? -ne 0; then
# { $as_echo "$as_me:${as_lineno-$LINENO}: The $COMPILER_NAME compiler (located as $COMPILER) does not seem to be the required $TOOLCHAIN_TYPE compiler." >&5
# $as_echo "$as_me: The $COMPILER_NAME compiler (located as $COMPILER) does not seem to be the required $TOOLCHAIN_TYPE compiler." >&6;}
# { $as_echo "$as_me:${as_lineno-$LINENO}: The result from running with --version was: \"$COMPILER_VERSION\"" >&5
# $as_echo "$as_me: The result from running with --version was: \"$COMPILER_VERSION\"" >&6;}
# as_fn_error $? "A $TOOLCHAIN_TYPE compiler is required. Try setting --with-tools-dir." "$LINENO" 5

最后执行sh configure终于成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
====================================================
A new configuration has been successfully created in
/Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release
using default settings.

Configuration summary:
* Debug level: release
* JDK variant: normal
* JVM variants: server
* OpenJDK target: OS: macosx, CPU architecture: x86, address length: 64

Tools summary:
* Boot JDK: java version "1.8.0_162" Java(TM) SE Runtime Environment (build 1.8.0_162-b12) Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode) (at /Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home)
* Toolchain: gcc (GNU Compiler Collection)
* C Compiler: Version 11.0.3 (at /usr/bin/gcc)
* C++ Compiler: Version 11.0.3 (at /usr/bin/g++)

Build performance summary:
* Cores to use: 4
* Memory limit: 16384 MB
问题三:include path for libstdc++ headers not found
1
2
3
4
5
6
7
8
9
10
11
12
13
clang: clangerror: include path for libstdc++ headers not found; pass '-stdlib=libc++' on the command line to use the libc++ standard library instead [-Werror,-Wstdlibcxx-not-found]
: error: include path for libstdc++ headers not found; pass '-stdlib=libc++' on the command line to use the libc++ standard library instead [-Werror,-Wstdlibcxx-not-found]
make[6]: *** [../generated/adfiles/archDesc.o] Error 1
make[6]: *** Waiting for unfinished jobs....
make[6]: *** [../generated/adfiles/adlparse.o] Error 1
clang: error: include path for libstdc++ headers not found; pass '-stdlib=libc++' on the command line to use the libc++ standard library instead [-Werror,-Wstdlibcxx-not-found]
make[6]: *** [../generated/adfiles/arena.o] Error 1
make[5]: *** [ad_stuff] Error 2
make[4]: *** [product] Error 2
make[3]: *** [generic_build2] Error 2
make[2]: *** [product] Error 2
make[1]: *** [/Users/chenyuan/Workspaces/Openjdk/openjdk8/build/macosx-x86_64-normal-server-release/hotspot/_hotspot.timestamp] Error 2
make: *** [hotspot-only] Error 2

解决方法

原因:这个原因是Xcode升级到10以后就没有包含lstdc++库了。而 hotspot 居然还一直用着这个,于是编译器找不到 libstdc++ 的头文件就罢工了
解决办法:
打开:https://github.com/imkiwa/xcode-missing-libstdc- , clone 到本地,参考 install.sh 将文件链接或者复制到对应位置(慎重直接执行,请一定事先核对路径是否正确)!

问题四:fatal error: ‘iostream’ file not found
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/Users/chenyuan/Workspaces/Openjdk/openjdk8/hotspot/src/share/vm/adlc/archDesc.cpp:#include <iostream>/Users/chenyuan/Workspaces/Openjdk/openjdk8/hotspot/src/share/vm/adlc/adlc.hpp
: ^~~~~~~~~~35:10:27
:
fatal error/Users/chenyuan/Workspaces/Openjdk/openjdk8/hotspot/src/share/vm/adlc/adlc.hpp: :35'iostream' file not found:
10: fatal error: 'iostream' file not found
#include <iostream>
^~~~~~~~~~
#include <iostream>
^~~~~~~~~~
In file included from /Users/chenyuan/Workspaces/Openjdk/openjdk8/hotspot/src/share/vm/adlc/adlparse.cpp:27:
/Users/chenyuan/Workspaces/Openjdk/openjdk8/hotspot/src/share/vm/adlc/adlc.hpp:35:10: fatal error: 'iostream' file not found
#include <iostream>
^~~~~~~~~~
1 error generated.
make[6]: *** [../generated/adfiles/arena.o] Error 1
make[6]: *** Waiting for unfinished jobs....

解决方案

1
xcode-select --install
问题五:unknown argument: ‘-fpch-deps’

解决方案:

hotspot/make/bsd/makefiles/gcc.make

1
2
3
4
# Compiler warnings are treated as errors
ifneq ($(COMPILER_WARNINGS_FATAL),false)
WARNINGS_ARE_ERRORS = -Werror
endif

这一段也要干掉,否则在后续编译中可能会出现clang: error: unknown argument: '-fpch-deps'

1
2
3
4
5
ifeq ($(USE_CLANG),)
ifneq ($(CC_VER_MAJOR), 2)
DEPFLAGS += -fpch-deps
endif
endif
问题六:invalid argument ‘-std=gnu++98’ not allowed with ‘C’
1
2
3
Making signal interposition lib...
error: invalid argument '-std=gnu++98' not allowed with 'C'
make[6]: *** [libjsig.dylib] Error 1

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if test "x$TOOLCHAIN_TYPE" = xsolstudio; then
if test "x$OPENJDK_TARGET_CPU_ARCH" = "xsparc"; then
CFLAGS_JDKLIB_EXTRA="${CFLAGS_JDKLIB_EXTRA} -xregs=no%appl"
CXXFLAGS_JDKLIB_EXTRA="${CXXFLAGS_JDKLIB_EXTRA} -xregs=no%appl"
fi
elif test "x$TOOLCHAIN_TYPE" = xxlc; then
LDFLAGS_JDK="${LDFLAGS_JDK} -q64 -brtl -bnolibpath -liconv -bexpall"
CFLAGS_JDK="${CFLAGS_JDK} -qchars=signed -q64 -qfullpath -qsaveopt"
CXXFLAGS_JDK="${CXXFLAGS_JDK} -qchars=signed -q64 -qfullpath -qsaveopt"
elif test "x$TOOLCHAIN_TYPE" = xgcc; then
LEGACY_EXTRA_CFLAGS="$LEGACY_EXTRA_CFLAGS -fstack-protector"
LEGACY_EXTRA_CXXFLAGS="$LEGACY_EXTRA_CXXFLAGS -fstack-protector"
if test "x$OPENJDK_TARGET_OS" != xmacosx; then
LDFLAGS_JDK="$LDFLAGS_JDK -Wl,-z,relro"
LEGACY_EXTRA_LDFLAGS="$LEGACY_EXTRA_LDFLAGS -Wl,-z,relro"
fi
# CXXSTD_CXXFLAG="-std=gnu++98" # 注释掉这行

参考地址

0%