简栈

拥抱AI,持续成长

前言

通过我之前的Tomcat系列文章,相信看我博客的同学对Tomcat应该有一个比较清晰的了解了,在前几篇博客我们讨论了Tomcat在SpringBoot框架中是如何启动的,讨论了Tomcat的内部组件是如何设计以及请求是如何流转的,那么我们这篇博客聊聊Tomcat的异步Servlet,Tomcat是如何实现异步Servlet的以及异步Servlet的使用场景。

手撸一个异步的Servlet

我们直接借助SpringBoot框架来实现一个Servlet,这里只展示Servlet代码:

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
@WebServlet(urlPatterns = "/async",asyncSupported = true)
@Slf4j
public class AsyncServlet extends HttpServlet {

ExecutorService executorService =Executors.newSingleThreadExecutor();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//开启异步,获取异步上下文
final AsyncContext ctx = req.startAsync();
// 提交线程池异步执行
executorService.execute(new Runnable() {


@Override
public void run() {
try {
log.info("async Service 准备执行了");
//模拟耗时任务
Thread.sleep(10000L);
ctx.getResponse().getWriter().print("async servlet");
log.info("async Service 执行了");
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
//最后执行完成后完成回调。
ctx.complete();
}
});
}

上面的代码实现了一个异步的Servlet,实现了doGet方法注意在SpringBoot中使用需要再启动类加上@ServletComponentScan注解来扫描Servlet。既然代码写好了,我们来看看实际运行效果。

img

我们发送一个请求后,看到页面有响应,同时,看到请求时间花费了10.05s,那么我们这个Servlet算是能正常运行啦。有同学肯定会问,这不是异步servlet吗?你的响应时间并没有加快,有什么用呢?对,我们的响应时间并不能加快,还是会取决于我们的业务逻辑,但是我们的异步servlet请求后,依赖于业务的异步执行,我们可以立即返回,也就是说,Tomcat的线程可以立即回收,默认情况下,Tomcat的核心线程是10,最大线程数是200,我们能及时回收线程,也就意味着我们能处理更多的请求,能够增加我们的吞吐量,这也是异步Servlet的主要作用。

异步Servlet的内部原理

了解完异步Servlet的作用后,我们来看看,Tomcat是如何是先异步Servlet的。其实上面的代码,主要核心逻辑就两部分,final AsyncContext ctx = req.startAsync() ctx.complete()那我们来看看他们究竟做了什么?

阅读全文 »

当Leader崩溃或者Leader失去大多数的Follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的Leader,让所有的Server都恢复到一个正确的状态。Zookeeper中Leader的选举采用了三种算法:

  • LeaderElection
  • FastLeaderElection
  • AuthFastLeaderElection

并且在配置文件中是可配置的,对应的配置项为electionAlg。

背景知识

Zookeeper Server的状态可分为四种:

  • LOOKING:寻找Leader
  • LEADING:Leader状态,对应的节点为Leader。
  • FOLLOWING:Follower状态,对应的节点为Follower。
  • OBSERVING:Observer状态,对应节点为Observer,该节点不参与Leader选举。

成为Leader的必要条件: Leader要具有最高的zxid;当集群的规模是n时,集群中大多数的机器(至少n/2+1)得到响应并follow选出的Leader。

心跳机制:Leader与Follower利用PING来感知对方的是否存活,当Leader无法相应PING时,将重新发起Leader选举。

术语

zxid:zookeeper transaction id, 每个改变Zookeeper状态的操作都会形成一个对应的zxid,并记录到transaction log中。 这个值越大,表示更新越新。

阅读全文 »

Zookeeper使用了一种称为Zab(Zookeeper Atomic Broadcast)的协议作为其一致性的核心。Zab协议是Paxos协议的一种变形,下面将展示一些协议的核心内容。

考虑到Zookeeper的主要操作数据状态,为了保证一致性,Zookeeper提出了两个安全属性:

  • 全序(Total Order):如果消息A在消息B之前发送,则所有Server应该看到相同结果。
  • 因果顺序(Causal Order):如果消息A在消息B之前发生(A导致了B),并且一起发送,则消息A始终在消息B之前被执行。

为了保证上述两个安全属性,Zookeeper使用了TCP协议和Leader。通过使用TCP协议保证了消息的全序的特性(先发先到),通过Leader解决了因果顺序(先到Leader先执行)。因为有了Leader,Zookeeper的架构就变成为:Master-Slave模式,但在该模式中Master(Leader)会Crash,因此,Zookeeper引入Leader选举算法,以保证系统的健壮性。

当Zookeeper Server收到写操作,Follower会将其转发给Leader,由Leader执行操作。Client可以直接从Follower上读取数据,如果需要读取最新数据,则需要从Leader节点读取,Zookeeper设计的读写比大致为2:1。

Leader执行写操作可以简化为一个两段式提交的transaction:

  1. Leader发送proposal给所有的Follower。
  2. 收到proposal后,Follower回复ACK给Leader,接受Leader的proposal.
  3. 当Leader收到大多数的Follower的ACK后,将commit其proposal。

broadcast

在这个过程中,proposal的确认不需要所有节点都同意,如果有2n+1个节点,那么只要有n个节点同意即可,也就是说Zookeeper允许n个节点down掉。任何两个多数派必然有交集,在Leader切换(Leader down)时,这些交集依然保持着最新的系统状态。如果集群节点个数少于n+1个时,Zookeeper将无法进行同步,也就无法继续工作。

Zab与Paxos

阅读全文 »

背景

对于JVM这块儿的知识,我估计大部分的都是只有在需要面试的时候才会拿出来复习一下,然后就又放下来。也是因为这块儿是Java最底层的部分,非常难懂。其实如果真的说认真、细心的去撸一下,了解透彻,应该就不会那么容易忘记。

今天的主要目的也是根据Oracle的官方文档来一步一步的理解与学习,并且用用一些demo来验证理论。

Java虚拟机内存结构

我们先来看一下JVM一个大概的物理结构图(请注意,不叫内存模型):

http://static.cyblogs.com/QQ截图20191121231823.png

堆的划分

我们首先看一下官方地址对于运行时数据区域的一个划分:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html

2.5. Run-Time Data Areas

阅读全文 »

前言

最近线上出现了JVM 频繁FGC的问题,查询了很多GC相关的资料,做了一些整理翻译。文章比较长可以收藏后慢慢阅读。

一、什么是垃圾回收?(Garbage Collection)

一个垃圾回收器有一下三个职责

  • 分配内存
  • 确保有引用的对象能够在内存中保留。
  • 能够在正在执行的代码环境中回收已经死亡对象的内存。

这里提到的有引用是指存活的对象,后面会提到一些算法用来判断对象是否存活。不在有引用的对象将被认为是死亡的,也就是常说的垃圾garbage。找到并释放这些垃圾对象占用的空间的过程就被称作是垃圾回收garbage collection

垃圾回收可以解决很多内存分配的问题,但并不意味这全部。 比如:你可以不断地创建对象并保持对它们的引用直到没有可用的内存分配。垃圾回收本身就是一项非常复杂和消耗资源的过程。

二、理想的垃圾收集器需要哪些特性?

  1. 垃圾收集器必须是安全和全面的。这就意味着,存活的对象绝对不能被释放,相反垃圾对象在很少的垃圾回收循环里必须被回收。
  2. 垃圾回收必须是高效的,不允许出现正在运行的程序长时间暂停。
  3. 内存碎片整理,垃圾被收集以后内存会存在很多不连续的内存碎片,可能导致大对象无法分配到足够连续的内存。
  4. 扩展性,在多处理器系统、多线程应用中,内存分配和垃圾收集不能成为性能瓶颈。

三、设计选择

阅读全文 »

对象拷贝有哪些

对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部数据。

Java中有三种类型的对象拷贝:

  • 浅拷贝(Shallow Copy)
  • 深拷贝(Deep Copy)
  • 延迟拷贝(Lazy Copy)

理解浅拷贝

什么是浅拷贝?
  • 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
    • 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

    • http://static.cyblogs.com/QQ20200226-210909@2x.jpg

    • 在上图中,SourceObject有一个int类型的属性 “field1”和一个引用类型属性”refObj”(引用ContainedObject类型的对象)。当对SourceObject做浅拷贝时,创建了CopiedObject,它有一个包含”field1”拷贝值的属性”field2”以及仍指向refObj本身的引用。由于”field1”是基本类型,所以只是将它的值拷贝给”field2”,但是由于”refObj”是一个引用类型, 所以CopiedObject指向”refObj”相同的地址。因此对SourceObject中的”refObj”所做的任何改变都会影响到CopiedObject。

如何实现浅拷贝

下面来看一看实现浅拷贝的一个例子

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

private String name;
public Subject(String s) {
name = s;
}

public String getName() {
return name;
}

public void setName(String s) {
name = s;
}
}
阅读全文 »

前言

Java是一个安全的编程语言,它能最大程度的防止程序员犯一些低级的错误(大部分是和内存管理有关的)。但凡是不是绝对的,使用Unsafe程序员就可以操作内存,因此可能带来一个安全隐患。

这篇文章是就快速学习下sun.misc.Unsafe的公共API和一些有趣的使用例子。

Unsafe 实例化

在使用Unsafe之前我们需要先实例化它。但我们不能通过像Unsafe unsafe = new Unsafe()这种简单的方式来实现Unsafe的实例化,这是由于Unsafe的构造方法是私有的。Unsafe有一个静态的getUnsafe()方法,但是如果天真的以为调用该方法就可以的话,那你将遇到一个SecurityException异常,这是由于该方法只能在被信任的代码中调用。

1
2
3
4
5
6
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}

那Java是如何判断我们的代码是否是受信的呢?它就是通过判断加载我们代码的类加载器是否是根类加载器。

我们可是通过这种方法将我们自己的代码变为受信的,使用jvm参数bootclasspath。如下所示:

1
java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient

但这种方式太难了

阅读全文 »

Volatile简介

volatile被称为轻量级的synchronized,运行时开销比synchronized更小,在多线程并发编程中发挥着同步共享变量禁止处理器重排序的重要作用。建议在学习volatie之前,先看一下Java内存模型《什么是Java内存模型?》,因为volatile和Java内存模型有着莫大的关系。

Java内存模型

在学习volatie之前,需要补充下Java内存模型的相关(JMM)知识,我们知道Java线程的所有操作都是在工作区进行的,那么工作区和主存之间的变量是怎么进行交互的呢,可以用下面的图来表示。

http://static.cyblogs.com/e0e01e43ly1g186enjfwfj20k80degmr.jpg
Java通过几种原子操作完成工作区内存主存的交互

  1. lock:作用于主存,把变量标识为线程独占状态。
  2. unlock:作用于主存,解除变量的独占状态。
  3. read:作用于主存,把一个变量的值通过主存传输到线程的工作区内存。
  4. load:作用于工作区内存,把read操作传过来的变量值储存到工作区内存的变量副本中。
  5. use:作用于工作内存,把工作区内存的变量副本传给执行引擎。
  6. assign:作用于工作区内存,把从执行引擎传过来的值赋值给工作区内存的变量副本。
  7. store:作用于工作区内存,把工作区内存的变量副本传给主存。
  8. write:作用于主存,把store操作传过来的值赋值给主存变量。

8个操作每个操作都是原子性的,但是几个操作连着一起就不是原子性了!

volatile原理

上面介绍了Java模型的8个操作,那么这8个操作和volatile又有着什么关系呢。

volatile的可见性
阅读全文 »

前几篇文章讲述了IO的几种模式及netty的基本概念,netty基于多路复用模型下的reactor模式,对 大量连接、单个处理短且快 的场景很适用 。

那在往底层思考,linux对于IO又是如何处理的呢?

C10K 问题

http://www.52im.net/thread-566-1-1.html

最初的服务器都是基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的(往往出现效率低下甚至完全瘫痪)。如果是采用分布式系统,维持1亿用户在线需要10万台服务器,成本巨大。基于上述考虑,如何突破单机性能局限,是高性能网络编程所必须要直面的问题。这些局限和问题最早被Dan Kegel 进行了归纳和总结,并首次成系统地分析和提出解决方案,后来这种普遍的网络现象和技术局限都被大家称为 C10K 问题。

C10K 问题的最大特点是:设计不够良好的程序,其性能和连接数及机器性能的关系往往是非线性的

举个例子:如果没有考虑过 C10K 问题,一个经典的基于 select 的程序能在旧服务器上很好处理 1000 并发的吞吐量,它在 2 倍性能新服务器上往往处理不了并发 2000 的吞吐量。这是因为在策略不当时,大量操作的消耗和当前连接数 n 成线性相关。会导致单个任务的资源消耗和当前连接数的关系会是 O(n)。而服务程序需要同时对数以万计的socket 进行 I/O 处理,积累下来的资源消耗会相当可观,这显然会导致系统吞吐量不能和机器性能匹配。

以上这就是典型的C10K问题在技术层面的表现。C10K问题本质上是操作系统的问题。对于Web1.0/2.0时代的操作系统而言, 传统的同步阻塞I/O模型都是一样的,处理的方式都是requests per second,并发10K和100的区别关键在于CPU。创建的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!
可见,解决C10K问题的关键就是尽可能减少这些CPU等核心计算资源消耗,从而榨干单台服务器的性能,突破C10K问题所描述的瓶颈。

概念说明

用户空间与内核空间
阅读全文 »

推荐一个咕泡学院的视频资源:链接:https://pan.baidu.com/s/1SmSzrmfgbm6XgKZO7utKWg 密码:e54x

先回答一下之前发布的《使用HashMap的时候小心点》同学不补充的问题,说最好说下HashMap在JDK8下是怎么解决死循环的问题的。

链表部分对应上面 transfer 的代码:

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
  Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}

由于扩容是按两倍进行扩,即 N 扩为 N + N,因此就会存在低位部分 0 - (N-1),以及高位部分 N - (2N-1), 所以这里分为 loHead (low Head) 和 hiHead (high head)。

通过上面的分析,不难发现循环的产生是因为新链表的顺序跟旧的链表是完全相反的,所以只要保证建新链时还是按照原来的顺序的话就不会产生循环。

JDK8是用 headtail 来保证链表的顺序和之前一样,这样就不会产生循环引用。

传统 HashMap 的缺点

JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。

当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n),完全失去了它的优势。

阅读全文 »
0%