codershamo

codershamo 查看完整档案

北京编辑大连理工大学  |  计算机 编辑去哪儿网  |  JAVA开发工程师 编辑填写个人主网站
编辑

大道至简

个人动态

codershamo 发布了文章 · 2020-01-30

浅谈Java网络编程(一)——非阻塞I/O

译自:https://medium.com/@copyconst...

文件描述符(descriptors)

Unix中I/O的基本组成元素是字节序列。大多数程序应用于字节流或I/O流。
进程通过描述符引用I/O流,也被称作文件描述符。管道、文件、POSIX IPC's(消息队列,信号量,共享内存),事件队列等都是通过文件描述符引用I/O流。

创建和释放描述符

描述符创建:

  • 通过系统命令调用(open,pipe,socket等)创建;
  • 继承自父进程。

描述符释放:

  • 进程退出
  • 系统调用close
  • 标记为close on exec的描述符在exec后释放

Close-on-exec

当进程forks时,所有描述符都会复制到子进程中。如果任意描述符被标记为close on exec,那么当子进程execs之前,父进程forks之后,这些描述符将关闭并且在子进程中不再可用。

使用描述符通过readwrite命令调用的数据转换

File Entry

每个描述符都指向内核中的File entry的数据结构。file entry为每个描述符维度了一个file offset。系统调用命令open创建file entry.

Fork/Dup and File Entries

fork创建的描述符被父子进程共享,在file entry中引用同一个offsetdup/dup2的系统调用与此类似。

#include <unistd.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <stdio.h>

int main(char \*argv\[\]) {  
    int fd = open("abc.txt", O\_WRONLY | O\_CREAT | O\_TRUNC, 0666);  
    fork();  
    write(fd, "xyz", 3);  
    printf("%ld\\n", lseek(fd, 0, SEEK\_CUR));  
    close(fd);  
    return 0;  
}

运行结果

3
6

Offset-per-descriptor

因为多个描述符可能引用同一个file entry, file entry为每个描述符维护了一个file offset。read和write操作从这个file offset开始,并且在数据转换之后file offset也将更新。offset决定了下次read write操作的位置。当进程终止时,内核将回收所有该进程所持有的描述符,如果此进程是引用file entry的最后一个进程,内核将回收整个file entry。

剖析File Entry

每个file entry包含:

  • 类型
  • 函数指针数组。这个函数指针数组将通用的对描述符的操作转换为具体文件类型的实现。

稍微解释下,所有的描述符都对外提供了一套通用的API操作,包含读、写、修改描述符模式、截断描述符、ioctl操作、polling等。
针对不同类型的文件,这些操作都有所不同,并且有不同的实现。对sockets的读操作与对pipes的读操作就有所不同,即使它们高层次的API是一样的。open命令并不在此列,因为不同类型的文件的open操作差异非常大。但是一旦file entry由open创建,剩下的操作都可以使用同一套通用的API

大多数的网络通讯使用sockets。sockets由描述符引用,作为传输的终点。两个进程可以创建两个sockets,通过连接这两个sockets建立可靠的字节流传输。一旦连接建立,描述符可以使用file offsets进行读写。内核可以将一个进程的输出重定向到另一台机器的另一个进程。对于字节流连接,统一使用read write命令读写,但对于不同类型的消息(比如网络数据包)使用不同的系统命令处理。

非阻塞描述符

默认情况下,在没有数据可用时,通过描述符read将阻塞。writesend也是如此。多数描述符的操作都是如此,但是磁盘文件除外,因为写磁盘并不是直接写,而是通过内核的buffer cache。只有当open磁盘文件时使用O_SYNC标识才会同步写磁盘。

任何描述符(pipes, FIFOs, sockets, terminals, pseudo-terminals等)都可以设置为非阻塞模式。当一个描述符设置为非阻塞模式时,对此描述符的I/O调用都将立即返回,即使此请求并不能马上完成(请求完成期间将使进程阻塞)。返回值分为下列情况:

  • an error: 操作完全不能完成
  • a partial count: 输入或输出可以部分完成
  • the entire result: I/O操作可以完全完成

通过设置非延迟标识O_NONBLOCK将描述符设置为非阻塞模式。这个标识也被叫做“open-file”状态标识。

描述符就绪

当进程通过描述符执行I/O操作时不被阻塞,称为描述符就绪。描述符就绪与操作是否会传输数据无关,而只与I/O操作是否可以无阻塞执行相关。

当有I/O事件发生时描述符进行就绪状态,例如新输入的到达、socket连接完成或者当TCP将列队中的数据传输后,socket的发送buffer出现可用容量时。

有两种方式可以判断一个描述符是否进入就绪状态——edge triggered和level triggered

Level Triggered

可以把level triggered看作是拉模式(pull或poll模式)。为了判断一个描述符是否就绪,进程尝试执行非阻塞的I/O操作。进程可以执行任意次这样的操作。这为随后的I/O操作提供了更多灵活性。比如,一个描述符进入就绪状态,进程可以读取所有可用数据,也可以不执行任何I/O操作,或者不读取buffer中的所有数据。
下面举例来看下

在t0时间,进程尝试使用非阻塞描述符进行I/O操作。如果I/O操作阻塞,系统调用返回error。

在t1时刻,进程再一次执行I/O,假设这次操作也阻塞并返回error。

在t2时刻,进程又执行了I/O,假设也阻塞或返回error。

假设到了t3时刻,进程拉取描述符的状态并且描述符就绪。进程可以执行整个I/O操作(例如读取socket上所有可用数据)

假设t4时刻,进程拉取描述符状态但描述符并没有就绪,这次调用将再次阻塞或返回error。

t5时刻,描述符就绪,进程只执行了部分I/O操作(例如只读取一半可用数据)

t6时刻,描述符就绪,进程什么I/O操作也没执行

Edge Triggered

当描述符就绪时,进程将收到一个通知(通常是描述符上有新事件发生)。可以把这种模式看作是push模式,这个描述符就绪的通知是被push给进程的。注意,push模式仅通知进程描述符已就绪,而不会通知其他信息,比如有多少数据已到达socket的buffer中。

因此,通过这种方式进程只能获取到不完整的数据,所以进程需要继续进行操作。当每次得到通知时,进程尝试进行最多的I/O操作,如果不这样做,进程不得不等到下一次得到通知时才能获取数据,即使在下一次通知到来前仍有部分数据可用。

下面举例说明

在t2时刻,进程得到描述符就绪的通知

可用的字节流存储在buffer中,假设有1024个字节可读。

假设进程只读取了其中的500个字节

这意味着在t3 t4 t5时刻,buffer中仍然有524个字节可使进程无阻塞地读取。但是因为只有在它得到下次通知时才会执行I/O操作,这524个字节的数据在这期间将一直留在buffer中。

假设进程在t6时刻接到下次通知,buffer中又有1024个字节可用。此时buffer中可用的数据为1548个字节——524字节是上次没读的,1024是新到达的。

假设进程这次读取了1024字节。

这意味着在这次I/O操作结束后仍有524字节的数据留在buffer中,直到一次通知到来进程才能读取到。

当一个描述符在通知来到时如果尝试执行所有I/O操作,可能造成其他描述符“饥饿”。即使使用level triggered,一次大量的writesend也可能导致阻塞。

多路复用I/O

上面我们只讨论了一个进程只处理一个描述符的情况。通常进程处理多个描述符。一个常见的场景是一个应用程序需要打印日志,同时接收socket连接并且和其他服务建立RPC连接。

有以下几种多路复用I/O方式:

  • 非阻塞I/O(描述符本身被标识为非阻塞,操作可能部分完成)
  • 信号驱动I/O(当I/O状态变化时通知拥有描述符的进程)
  • polling I/O(通过selectpoll系统调用,这两者都提供了level triggered方式的描述符就绪通知机制)
  • BSD 机制的内核事件polling(使用kevent系统调用)

非阻塞I/O的多路复用I/O

描述符

将所有描述符都设置为非阻塞模式

进程

进程尝试对描述符执行I/O操作,检查是否有任意I/O操作返回error。

内核

内核在描述符上执行I/O操作,返回error或部分输出或者是全部结果。

缺点

频繁检查:如果进程频繁尝试执行I/O操作,进程不得不持续地重复检查描述符是否就绪的操作。在tight循环中这样的busy-waiting可能会耗尽CPU周期。
不频繁检查:如果这样的操作执行不频繁,可能使进程对于有效的I/O事件长时间得不到响应。

何时使用

对于输出描述符(比如write)的操作并不总是阻塞的。在这种场景下,可以首先尝试执行I/O操作,如果返回error再回退到polling。当使用edge-triggered通知方式时也可以使用这种方式,此时描述符设置为非阻塞模式,进程一旦得到一个I/O事件的通知,进程可以重复执行I/O操作直到系统调用被阻塞(EAGAIN or EWOULDBLOCK)。

信号驱动I/O的多路复用I/O

描述符

当任意描述符上可执行I/O操作时,内核将发送通知给进程。

进程

进程等待任何描述符就绪的信号。

内核

跟踪描述符列表,当任意描述符就绪时给进程发送信号通知。

缺点

捕获信号的开销较大,当大量I/O操作时使用信号驱动I/O方式并不现实。

何时使用

通常在一些“特例条件”下使用,此时处理信号的开销低于不断使用select/poll/epollkevent的polling操作。一个“特例条件”的场景是socket上的带外(out-of-band)数据的到达。总之不常用。

Polling I/O的多路复用I/O

描述符

查看原文

赞 0 收藏 0 评论 0

codershamo 发布了文章 · 2020-01-23

浅谈Java性能优化(二)——String intern原理

String intern在Java6 7 8中实现原理不同,本文仅针对Java8的原理进行分析

intern方法解释

首先来看interna方法的说明:

    /**
     * Returns a canonical representation for the string object.
     * <p>
     * A pool of strings, initially empty, is maintained privately by the
     * class {@code String}.
     * <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     * It follows that for any two strings {@code s} and {@code t},
     * {@code s.intern() == t.intern()} is {@code true}
     * if and only if {@code s.equals(t)} is {@code true}.
     * <p>
     * All literal strings and string-valued constant expressions are
     * interned. String literals are defined in section 3.10.5 of the
     * <cite>The Java&trade; Language Specification</cite>.
     *
     * @return  a string that has the same contents as this string, but is
     *          guaranteed to be from a pool of unique strings.
     */

简单来说,在Java8中,String类维护了一个字符串常量池(注意此常量池在运行期间位于堆中),当调用intern方法时,首先在常量池中查看是否已有相同的字符串(字符串是否相同使用String的equal方法判断),如果常量池中已有,则直接返回该字符串的引用,如果没有,则将当前字符串对象加入常量池中,并返回当前字符串的引用。

字符串创建

字符串的创建,如果使用 new 创建时,是在堆中创建字符串对象,如果直接使用字面量,则先看常量池中是否有相等的字符串,如果有,则返回此字符串的引用,如果没有,则在常量池中创建。

举例

下面举例说明

String s1 = new String(String.valueOf(11));
String s2 = s1.intern();

这里使用String.valueOf(11)而不是“11”,是为了先说明非字符串常量的情况。

执行第一行代码时,内存中的数据如图1所示:
在堆中创建String "11" 对象,在栈中创建变量s1,指向String对象。

执行第二行代码时,调用String对象的intern方法,此时常量池中没有 "11",所以将"11"加入到常量池中,返回"11"对象的引用,赋值给s2, 内存中数据分布如图2所示:
image.png

所以此时s1==s2,执行结果:
image.png

下面看下字面量字符串和常量字符串的情况

String s1 = "11";  
String s2 = new String("11");  
String s3 = s2.intern();  
System.out.println(s1 == s2);  
System.out.println(s2 == s3);  
System.out.println(s1 == s3);

对于字面量字符串和常量字符串是直接在string pool中创建, 所以s1指向常量池中的字符串对象;
s2使用new操作符,在堆中创建,所以s2指向堆中的字符串对象;
s2调用intern方法后,因为常量池中已经有“11”的字符串对象,所以直接返回常量池中的字符串引用。 内存中的数据分布如图3:

image.png

所以执行结果是:
image.png

优化配置

string pool是使用Map结构存储字符串及引用,如果想要增加string pool的大小,可以设置JVM参数:

 -XX:StringTableSize=1000003

Java8中默认是60013,设置的值最好是素数,以减少Hash碰撞,提高查询效率。

参考:
https://www.journaldev.com/79...
http://java-performance.info/...
https://www.baeldung.com/java...

查看原文

赞 2 收藏 1 评论 0

codershamo 收藏了文章 · 2018-07-31

HTTP2和HTTPS来不来了解一下?

一、前言

只有光头才能变强

HTTP博文回顾:

本文力求简单讲清每个知识点,希望大家看完能有所收获

二、HTTP协议的今生来世

最近在看博客的时候,发现有的面试题已经考HTTP/2了,于是我就顺着去了解一下。

到现在为止,HTTP协议已经有三个版本了:

  • HTTP1.0
  • HTTP1.1
  • HTTP/2

下面就简单聊聊他们三者的区别,以及整理一些必要的额外知识点。

2.1HTTP版本之间的区别

2.1.1HTTP1.0和HTTP1.1区别

HTTP1.0和HTTP1.1最主要的区别就是:

  • HTTP1.1默认是持久化连接

在HTTP1.0默认是短连接:

简单来说就是:每次与服务器交互,都需要新开一个连接

试想一下:请求一张图片,新开一个连接,请求一个CSS文件,新开一个连接,请求一个JS文件,新开一个连接。HTTP协议是基于TCP的,TCP每次都要经过三次握手,四次挥手,慢启动...这都需要去消耗我们非常多的资源的!

在HTTP1.1中默认就使用持久化连接来解决:建立一次连接,多次请求均由这个连接完成!(如果阻塞了,还是会开新的TCP连接的)

相对于持久化连接还有另外比较重要的改动:

  • HTTP 1.1增加host字段
  • HTTP 1.1中引入了Chunked transfer-coding,范围请求,实现断点续传(实际上就是利用HTTP消息头使用分块传输编码,将实体主体分块传输)
  • HTTP 1.1管线化(pipelining)理论,客户端可以同时发出多个HTTP请求,而不用一个个等待响应之后再请求

    • 注意:这个pipelining仅仅是限于理论场景下,大部分桌面浏览器仍然会选择默认关闭HTTP pipelining!
    • 所以现在使用HTTP1.1协议的应用,都是有可能会开多个TCP连接的!

参考资料:

2.1.2HTTP2基础

在说HTTP2之前,不如先直观比较一下HTTP2和HTTP1.1的区别:

上面也已经说了,HTTP 1.1提出了管线化(pipelining)理论,但是仅仅是限于理论的阶段上,这个功能默认还是关闭了的。

管线化(pipelining)和非管线化的区别

HTTP Pipelining其实是把多个HTTP请求放到一个TCP连接中一一发送,而在发送过程中不需要等待服务器对前一个请求的响应;只不过,客户端还是要按照发送请求的顺序来接收响应!

就像在超市收银台或者银行柜台排队时一样,你并不知道前面的顾客是干脆利索的还是会跟收银员/柜员磨蹭到世界末日(不管怎么说,服务器(即收银员/柜员)是要按照顺序处理请求的,如果前一个请求非常耗时(顾客磨蹭),那么后续请求都会受到影响。
  • 在HTTP1.0中,发送一次请求时,需要等待服务端响应了才可以继续发送请求。
  • 在HTTP1.1中,发送一次请求时,不需要等待服务端响应了就可以发送请求了,但是回送数据给客户端的时候,客户端还是需要按照响应的顺序来一一接收
  • 所以说,无论是HTTP1.0还是HTTP1.1提出了Pipelining理论,还是会出现阻塞的情况。从专业的名词上说这种情况,叫做线头阻塞(Head of line blocking)简称:HOLB

2.1.3HTTP1.1和HTTP2区别

HTTP2与HTTP1.1最重要的区别就是解决了线头阻塞的问题!其中最重要的改动是:多路复用 (Multiplexing)

  • 多路复用意味着线头阻塞将不在是一个问题,允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息,合并多个请求为一个的优化将不再适用。

    • (我们知道:HTTP1.1中的Pipelining是没有付诸于实际的),之前为了减少HTTP请求,有很多操作将多个请求合并,比如:Spriting(多个图片合成一个图片),内联Inlining(将图片的原始数据嵌入在CSS文件里面的URL里),拼接Concatenation(一个请求就将其下载完多个JS文件),分片Sharding(将请求分配到各个主机上)......

使用了HTTP2可能是这样子的:

HTTP2所有性能增强的核心在于新的二进制分帧层(不再以文本格式来传输了),它定义了如何封装http消息并在客户端与服务器之间传输。

看上去协议的格式和HTTP1.x完全不同了,实际上HTTP2并没有改变HTTP1.x的语义,只是把原来HTTP1.x的header和body部分用frame重新封装了一层而已

HTTP2连接上传输的每个帧都关联到一个“流”。流是一个独立的,双向的帧序列可以通过一个HTTP2的连接在服务端与客户端之间不断的交换数据。

实际上运输时:

HTTP2还有一些比较重要的改动:

  • 使用HPACK对HTTP/2头部压缩
  • 服务器推送

  • 流量控制

    • 针对传输中的进行控制(TCP默认的粒度是针对连接)
  • 流优先级(Stream Priority)它被用来告诉对端哪个流更重要

2.2HTTP2总结

HTTP1.1新改动:

  • 持久连接
  • 请求管道化
  • 增加缓存处理(新的字段如cache-control)
  • 增加Host字段、支持断点传输等

HTTP2新改动:

  • 二进制分帧
  • 多路复用
  • 头部压缩
  • 服务器推送

参考资料:

2.3HTTPS再次回顾

之前在面试的时候被问到了HTTPS,SSL这样的知识点,也没答上来,这里也简单整理一下。

首先还是来解释一下基础的东东:

  • 对称加密:

    • 加密和解密都是用同一个密钥
  • 非对称加密:

    • 加密用公开的密钥,解密用私钥
    • (私钥只有自己知道,公开的密钥大家都知道)
  • 数字签名:

    • 验证传输的内容是对方发送的数据
    • 发送的数据没有被篡改过
  • 数字证书 (Certificate)

    • 认证机构证明是真实的服务器发送的数据

3y的通讯之路:

  • 远古时代:3y和女朋友聊天传输数据之间没有任何的加密,直接传输

    • 内容被看得一清二楚,毫无隐私可言
  • 上古时期:使用对称加密的方式来保证传输的数据只有两个人知道

    • 此时有个问题:密钥不能通过网络传输(因为没有加密之前,都是不安全的),所以3y和女朋友先约见面一次,告诉对方密码是多少,再对话聊天。
  • 中古时期:3y不单单要跟女朋友聊天,还要跟爸妈聊天的哇(同样不想泄漏了自己的通讯信息)。那有那么多人,难道每一次都要约来见面一次吗?(说明维护多个对称密钥是麻烦的!)--->所以用到了非对称加密

    • 3y自己保留一份密码,独一无二的(私钥)。告诉3y女朋友,爸妈一份密码(这份密码是公开的,谁都可以拿--->公钥)。让他们给我发消息之前,先用那份我告诉他们的密码加密一下,再发送给我。我收到信息之后,用自己独一无二的私钥解密就可以了!
  • 近代:此时又出现一个问题:虽然别人不知道私钥是什么,拿不到你原始传输的数据,但是可以拿到加密后的数据,他们可以改掉某部分的数据再发送给服务器,这样服务器拿到的数据就不是完整的了。

    • 3y女朋友给3y发了一条信息”3y我喜欢你“,然后用3y给的公钥加密,发给3y了。此时不怀好意的人截取到这条加密的信息,他破解不了原信息。但是他可以修改加密后的数据再传给3y。可能3y拿到收到的数据就是”3y你今晚跪键盘吧“
  • 现代:拿到的数据可能被篡改了,我们可以使用数字签名来解决被篡改的问题。数字签名其实也可以看做是非对称加密的手段一种,具体是这样的:得到原信息hash值,用私钥对hash值加密,另一端公钥解密,最后比对hash值是否变了。如果变了就说明被篡改了。(一端用私钥加密,另一端用公钥解密,也确保了来源)
  • 目前现在:好像使用了数字签名就万无一失了,其实还有问题。我们使用非对称加密的时候,是使用公钥进行加密的。如果公钥被伪造了,后面的数字签名其实就毫无意义了。讲到底:还是可能会被中间人攻击~此时我们就有了CA认证机构来确认公钥的真实性

对于数字签名和CA认证还是不太了解参考一下


回到我们的HTTPS,HTTPS其实就是在HTTP协议下多加了一层SSL协议(ps:现在都用TLS协议了)

HTTPS采用的是混合方式加密

过程是这样子的:

  • 用户向web服务器发起一个安全连接的请求
  • 服务器返回经过CA认证的数字证书,证书里面包含了服务器的public key(公钥)
  • 用户拿到数字证书,用自己浏览器内置的CA证书解密得到服务器的public key
  • 用户用服务器的public key加密一个用于接下来的对称加密算法的密钥,传给web服务器

    • 因为只有服务器有private key可以解密,所以不用担心中间人拦截这个加密的密钥
  • 服务器拿到这个加密的密钥,解密获取密钥,再使用对称加密算法,和用户完成接下来的网络通信

所以相比HTTP,HTTPS 传输更加安全

  • (1) 所有信息都是加密传播,黑客无法窃听。
  • (2) 具有校验机制,一旦被篡改,通信双方会立刻发现。
  • (3) 配备身份证书,防止身份被冒充。

参考资料:

三、总结

我只是在学习的过程中,把自己遇到的问题写出来,整理出来,希望可以对大家有帮助。如果文章有错的地方,希望大家可以在评论区指正,一起学习交流~

参考资料:

  • 《图解HTTP》
如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以关注微信公众号:Java3y。为了大家方便,刚新建了一下qq群:742919422,大家也可以去交流交流。谢谢支持了!希望能多介绍给其他有需要的朋友

文章的目录导航

查看原文

codershamo 赞了文章 · 2018-04-18

小哥哥,小姐姐,我有一份tcp、http面试指南你要吗?

来来来

对,我就是骗你进来的。嘿嘿嘿... 既然来了就看看再走嘛

作为一个学通信出身的前端,说道http、tcp什么的,算是到了我的领域了。(我会告诉你我上课净睡觉了,啥也没学到吗?)这次给大家讲讲http,提高水平、丰富知识。(要不是为了面试,谁会管什么ttp呀,对我是就这么肤浅)

TCP

要说http就绕不开tcp,TCP协议对应于传输层,而HTTP协议对应于应用层,从本质上来说,二者没有可比性。但是,http是基于tcp协议的。

TCP/IP 协议分层模型

  • 物理层将二进制的0和1和电压高低,光的闪灭和电波的强弱信号进行转换
  • 链路层代表驱动
  • 网络层

    • 使用 IP 协议,IP 协议基于 IP 转发分包数据
    • IP 协议是个不可靠协议,不会重发
    • IP 协议发送失败会使用ICMP 协议通知失败
    • ARP 解析 IP 中的 MAC 地址,MAC 地址由网卡出厂提供

      • IP 还隐含链路层的功能,不管双方底层的链路层是啥,都能通信
  • 传输层

    • 通用的 TCP 和 UDP 协议

      • TCP 协议面向有连接,能正确处理丢包,传输顺序错乱的问题,但是为了建立与断开连接,需要至少7次的发包收包,资源浪费
      • UDP 面向无连接,不管对方有没有收到,如果要得到通知,需要通过应用层
    • 会话层以上分层

      • TCP/IP 分层中,会话层,表示层,应用层集中在一起
      • 网络管理通过 SNMP 协议

划重点了啊(面试最常问的啊)

TCP三次握手和四次挥手?

被问烂了的问题了,这里不详细讲了,三次握手:

  • 客户端–发送带有SYN标志的数据包–一次握手–服务端
  • 服务端–发送带有SYN/ACK标志的数据包–二次握手–客户端
  • 客户端–发送带有带有ACK标志的数据包–三次握手–服务端

四次挥手:

  • 客户端-发送一个FIN,用来关闭客户端到服务器的数据传送
  • 服务器-收到这个FIN,它发回一个ACK,确认序号为收到的序号加1 。和SYN一样,一个FIN将占用一个序号
  • 服务器-关闭与客户端的连接,发送一个FIN给客户端
  • 客户端-发回ACK报文确认,并将确认序号设置为收到序号加1

还不懂的童鞋,去找别人的文章好好看看!

TCP和UDP的区别?

仔细阅读上面传输层里写的内容,懂了吗?(不懂?不懂背下来啊,混蛋!)
举个例子

我们微信聊天时候经常会有这种情况。

是不是感同身受,这种情况就是对方用了TCP协议来聊天,要经过--在吗?--在--巴拉巴拉,才能成功的传递信息。
而如果对方使用UDP,则会有事直接说,不管我收没收到。(以后找我请用UDP协议,着急直接打电话!)

HTTP

Http协议是建立在TCP协议基础之上的,当浏览器需要从服务器获取网页数据的时候,会发出一次Http请求。Http会通过TCP建立起一个到服务器的连接通道,当本次请求需要的数据完毕后,Http会立即将TCP连接断开,这个过程是很短的。所以Http连接是一种短连接,是一种无状态的连接。

所谓的无状态,是指浏览器每次向服务器发起请求的时候,不是通过一个连接,而是每次都建立一个新的连接。如果是一个连接的话,服务器进程中就能保持住这个连接并且在内存中记住一些信息状态。而每次请求结束后,连接就关闭,相关的内容就释放了,所以记不住任何状态,成为无状态连接。

http传输流

无耻盗图

发送端在层与层间传输数据时,没经过一层都会被加上首部信息,接收端每经过一层都会删除一条首部

又来划重点了啊

HTTP的英文全称?

开玩笑的,这个显然不是重点,但是不排除有人会去问,还是要知道的:
超文本传输协议(HyperText Transfer Protocol)

状态码?

状态码就那些,常用的记住就行了:

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理
  • 204 No content,表示请求成功,但响应报文不含实体的主体部分
  • 206 Partial Content,进行范围请求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL
  • 303 see other,表示资源存在着另一个 URL,应使用 GET 方法丁香获取资源
  • 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
  • 307 temporary redirect,临时重定向,和302含义相同

4XX 客户端错误

  • 400 bad request,请求报文存在语法错误
  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
  • 403 forbidden,表示对请求资源的访问被服务器拒绝
  • 404 not found,表示在服务器上没有找到请求的资源

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执行请求时发生了错误
  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求

HTTP协议格式?

HTTP的请求和响应的消息协议是一样的,分为三个部分,起始行、消息头和消息体。这三个部分以CRLF作为分隔符。最后一个消息头有两个CRLF,用来表示消息头部的结束。

HTTP请求的起始行称为请求行,形如GET /index.html HTTP/1.1

HTTP响应的起始行称为状态行,形如200 ok

消息头部有很多键值对组成,多个键值对之间使用CRLF作为分隔符,也可以完全没有键值对。形如Content-Encoding: gzip
消息体是一个字符串,字符串的长度是由消息头部的Content-Length键指定的。如果没有Content-Length字段说明没有消息体,譬如GET请求就是没有消息体的,POST请求的消息体一般用来放置表单数据。GET请求的响应返回的页面内容也是放在消息体里面的。我们平时调用API返回的JSON内容都是放在消息体里面的。

HTTP的无状态性?

所谓HTTP协议的无状态性是指服务器的协议层无需为不同的请求之间建立任何相关关系,它特指的是协议层的无状态性。但是这并不代表建立在HTTP协议之上的应用程序就无法维持状态。应用层可以通过会话Session来跟踪用户请求之间的相关性,服务器会为每个会话对象绑定一个唯一的会话ID,浏览器可以将会话ID记录在本地缓存LocalStorage或者Cookie,在后续的请求都带上这个会话ID,服务器就可以为每个请求找到相应的会话状态。

输入url到页面加载都发生了什么事情?(最最常问的来了)

  • 输入地址
  • 浏览器查找域名的 IP 地址

这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存...

    • 浏览器跟踪重定向地址

      • 服务器处理请求
    • 服务器返回一个 HTTP 响应
    • 浏览器显示 HTML

      • 浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
      • . 浏览器发送异步请求

    查看原文

    赞 135 收藏 460 评论 7

    codershamo 关注了用户 · 2018-03-31

    ethannnli @ethannnli

    关注 184

    codershamo 赞了文章 · 2018-03-08

    一个两年Java的面试总结

    前言

    16年毕业到现在也近两年了,最近面试了阿里集团(菜鸟网络,蚂蚁金服),网易,滴滴,点我达,最终收到点我达,网易offer,蚂蚁金服二面挂掉,菜鸟网络一个月了还在流程中...
    最终有幸去了网易。
    但是要特别感谢点我达的领导及HR,真的非常非常好,很感谢他们一直的关照和指导。

    面试整体事项

    1. 简历要准备好,联系方式一定要正确清晰醒目,项目经历按照时间倒序阐述,注意描述自己在项目中承担的职责,简历的模板尽量选择简洁的,毕竟程序员大部分还是喜欢简单明了的。
    2. 推荐boss直聘,我觉得很好用(不是广告)。
    3. 一般的整体面试流程都是电面->现场面->HR面->等着。
    4. 不要觉得HR说让你回去等消息就是GG了,他们也要跟你之前的面试官讨论,再向领导汇报,如果说不急可能还要和其他候选人比较,所以HR让你回去等消息绝对不是说明你完蛋了。
    5. 面试前准备好自我介绍,1分钟左右就可以,可以写在纸上,电面可以照着念,等你到了现场面了基本也都快背下来你的自我介绍了。
    6. 准备好扎实的基础,这是一切的根源,没实力怎么都没用的。
    7. 面试中你可以把你的面试官往你会的知识上引导(我遇到过你会什么他不问什么的)。
    8. 遇到了设计类题目不要着急,面试官不是为了让你几分钟设计一个高并发高可用设计模式完美的架构,只是想看看你的思路,看看你应变的能力,然后给你些提示看看你能否迅速的调整。
    9. offer都会有的,不要着急,把面试当成一个交流的过程。

    需要准备的知识

    以下为在近期面试中比较有印象的问题,也就不分公司了,因为没什么意义,大致分类记录一下,目前只想起这么多,不过一定要知道这些问题只是冰山一角,就算都会了也不能怎么样,最最重要的,还是坚实的基础,清醒的头脑。

    Java基础

    1. HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。
    2. HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小。
    3. HashMap,HashTable,ConcurrentHashMap的区别。
    4. 极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
    5. HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。
    6. java中四种修饰符的限制范围。
    7. Object类中的方法。
    8. 接口和抽象类的区别,注意JDK8的接口可以有实现。
    9. 动态代理的两种方式,以及区别。
    10. Java序列化的方式。
    11. 传值和传引用的区别,Java是怎么样的,有没有传值引用。
    12. 一个ArrayList在循环过程中删除,会不会出问题,为什么。
    13. @transactional注解在什么情况下会失效,为什么。

    数据结构和算法

    1. B+树
    2. 快速排序,堆排序,插入排序(其实八大排序算法都应该了解
    3. 一致性Hash算法,一致性Hash算法的应用

    JVM

    1. JVM的内存结构。
    2. JVM方法栈的工作过程,方法栈和本地方法栈有什么区别。
    3. JVM的栈中引用如何和堆中的对象产生关联。
    4. 可以了解一下逃逸分析技术。
    5. GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
    6. 标记清除和标记整理算法的理解以及优缺点。
    7. eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。
    8. JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型。
    9. 强软弱虚引用的区别以及GC对他们执行怎样的操作。
    10. Java是否可以GC直接内存。
    11. Java类加载的过程。
    12. 双亲委派模型的过程以及优势。
    13. 常用的JVM调优参数。
    14. dump文件的分析。
    15. Java有没有主动触发GC的方式(没有)。

    多线程

    1. Java实现多线程有哪几种方式。
    2. Callable和Future的了解。
    3. 线程池的参数有哪些,在线程池创建一个线程的过程。
    4. volitile关键字的作用,原理。
    5. synchronized关键字的用法,优缺点。
    6. Lock接口有哪些实现类,使用场景是什么。
    7. 可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)。
    8. 悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决。
    9. ABC三个线程如何保证顺序执行。
    10. 线程的状态都有哪些。
    11. sleep和wait的区别。
    12. notify和notifyall的区别。
    13. ThreadLocal的了解,实现原理。

    数据库相关

    1. 常见的数据库优化手段
    2. 索引的优缺点,什么字段上建立索引
    3. 数据库连接池。
    4. durid的常用配置。

    计算机网络

    1. TCP,UDP区别。
    2. 三次握手,四次挥手,为什么要四次挥手。
    3. 长连接和短连接。
    4. 连接池适合长连接还是短连接。

    设计模式

    1. 观察者模式
    2. 代理模式
    3. 单例模式,有五种写法,可以参考文章单例模式的五种实现方式
    4. 可以考Spring中使用了哪些设计模式

    分布式相关

    1. 分布式事务的控制。
    2. 分布式锁如何设计。
    3. 分布式session如何设计。
    4. dubbo的组件有哪些,各有什么作用。
    5. zookeeper的负载均衡算法有哪些。
    6. dubbo是如何利用接口就可以通信的。

    缓存相关

    1. redis和memcached的区别。
    2. redis支持哪些数据结构。
    3. redis是单线程的么,所有的工作都是单线程么。
    4. redis如何存储一个String的。
    5. redis的部署方式,主从,集群。
    6. redis的哨兵模式,一个key值如何在redis集群中找到存储在哪里。
    7. redis持久化策略。

    框架相关

    1. SpringMVC的Controller是如何将参数和前端传来的数据一一对应的。
    2. Mybatis如何找到指定的Mapper的,如何完成查询的。
    3. Quartz是如何完成定时任务的。
    4. 自定义注解的实现。
    5. Spring使用了哪些设计模式。
    6. Spring的IOC有什么优势。
    7. Spring如何维护它拥有的bean。

    一些较新的东西

    1. JDK8的新特性,流的概念及优势,为什么有这种优势。
    2. 区块链了解
    3. 如何设计双11交易总额面板,要做到高并发高可用。

    一些小建议

    1. 可以去leetcode上刷题换换思路。
    2. 八大排序算法一定要手敲一遍(快排,堆排尤其重要)。
    3. 了解一些新兴的技术。
    4. 面试之后面试官都会问你有没有什么问题,千万不要没问题,也别傻乎乎的问一些敏感问题。
    5. 了解你要面试的公司的产品及竞争产品。

    几个链接

    1. 很多Java面试题
    2. 更多Java面试题
    3. 还是Java面试题

    总结

    无论是哪家公司,都很重视高并发高可用的技术,重视基础,重视JVM。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。
    如果这些文字能够帮到你,那就最好了,帮不到就当是我自己的一个记录。
    最后,希望大家都能找到适合自己的公司,开开心心的撸代码~

    查看原文

    赞 370 收藏 922 评论 84

    codershamo 关注了用户 · 2017-09-01

    codecraft @codecraft

    当一个代码的工匠回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧,这样,当他老的时候,可以很自豪告诉世人,我曾经将代码注入生命去打造互联网的浪潮之巅,那是个很疯狂的时代,我在一波波的浪潮上留下了或重如泰山或轻如鸿毛的几笔。

    关注 1121

    codershamo 发布了文章 · 2017-02-24

    浅谈Java并发编程系列(九)—— AQS结构及原理分析

    AQS介绍

    AQS,即AbstractQueuedSynchronizer, 队列同步器,它是Java并发用来构建锁和其他同步组件的基础框架。来看下同步组件对AQS的使用:
    图片描述

    AQS是一个抽象类,主是是以继承的方式使用。AQS本身是没有实现任何同步接口的,它仅仅只是定义了同步状态的获取和释放的方法来供自定义的同步组件的使用。从图中可以看出,在java的同步组件中,AQS的子类(Sync等)一般是同步组件的静态内部类,即通过组合的方式使用。

    AQS原理简介

    AQS的实现依赖内部的同步队列(FIFO双向队列),如果当前线程获取同步状态失败,AQS会将该线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。

    上面说的有点抽象,来具体看下,首先来看AQS最主要的三个成员变量:

        private transient volatile Node head;
        
        private transient volatile Node tail;
    
        private volatile int state;

    上面提到的同步状态就是这个int型的变量state. head和tail分别是同步队列的头结点和尾结点。假设state=0表示同步状态可用(如果用于锁,则表示锁可用),state=1表示同步状态已被占用(锁被占用)。

    下面举例说下获取和释放同步状态的过程:

    获取同步状态

    假设线程A要获取同步状态(这里想象成锁,方便理解),初始状态下state=0,所以线程A可以顺利获取锁,A获取锁后将state置为1。在A没有释放锁期间,线程B也来获取锁,此时因为state=1,表示锁被占用,所以将B的线程信息和等待状态等信息构成出一个Node节点对象,放入同步队列,head和tail分别指向队列的头部和尾部(此时队列中有一个空的Node节点作为头点,head指向这个空节点,空Node的后继节点是B对应的Node节点,tail指向它),同时阻塞线程B(这里的阻塞使用的是LockSupport.park()方法)。后续如果再有线程要获取锁,都会加入队列尾部并阻塞。

    释放同步状态

    当线程A释放锁时,即将state置为0,此时A会唤醒头节点的后继节点(所谓唤醒,其实是调用LockSupport.unpark(B)方法),即B线程从LockSupport.park()方法返回,此时B发现state已经为0,所以B线程可以顺利获取锁,B获取锁后B的Node节点随之出队。

    上面只是简单介绍了AQS获取和释放的大致过程,下面结合AQS和ReentrantLock源码来具体看下JDK是如何实现的,特别要注意JDK是如何保证同步和并发操作的。

    AQS源码分析

    接下来以ReentrantLock的源码入手来深入理解下AQS的实现。
    上面说过AQS一般是以继承的方式被使用,同步组件内部组合一个继承了AQS的子类。
    在ReentrantLock类中,有一个Sync成员变量,即是继承了AQS的子类,源码如下:

     public class ReentrantLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = 7373984872572414699L;
        /** Synchronizer providing all implementation mechanics */
        private final Sync sync;
    
        /**
         * Base of synchronization control for this lock. Subclassed
         * into fair and nonfair versions below. Uses AQS state to
         * represent the number of holds on the lock.
         */
        abstract static class Sync extends AbstractQueuedSynchronizer {
            ...
        }
    }

    这里的Sync也是一个抽象类,其实现类为FairSync和NonfairSync,分别对应公平锁和非公平锁。ReentrantLock的提供一个入参为boolean值的构造方法,来确定使用公平锁还是非公平锁:

         public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
         }

    获取锁

    这里以NonfairSync类为例,看下它的Lock()的实现:

         final void lock() {
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
         }

    lock方法先通过CAS尝试将同步状态(AQS的state属性)从0修改为1。若直接修改成功了,则将占用锁的线程设置为当前线程。看下compareAndSetState()和setExclusiveOwnerThread()实现:

         protected final boolean compareAndSetState(int expect, int update) {
            // See below for intrinsics setup to support this
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
         }

    可以看到compareAndSetState底层其实是调用的unsafe的CAS系列方法。

         protected final void setExclusiveOwnerThread(Thread thread) {
            exclusiveOwnerThread = thread;
        }

    exclusiveOwnerThread属性是AQS从父类AbstractOwnableSynchronizer中继承的属性,用来保存当前占用同步状态的线程。

    如果CAS操作未能成功,说明state已经不为0,此时继续acquire(1)操作,这个acquire()由AQS实现提供:

        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

    代码很短,不太好了理解,转换下写法(代码1):

        public final void acquire(int arg) {
            boolean hasAcquired = tryAcquire(arg);
            if (!hasAcquired) {
                Node currentThreadNode = addWaiter(Node.EXCLUSIVE);
                boolean interrupted = acquireQueued(currentThreadNode, arg);
                if (interrupted) {
                    selfInterrupt();
                }
            }
        }

    简单解释下:
    tryAcquire方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构适成一个Node节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。

    首先看tryAcquire(arg)在NonfairSync中的实现(这里arg=1):

            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
            
            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;
            }

    首先获取AQS的同步状态(state),在锁中就是锁的状态,如果状态为0,则尝试设置状态为arg(这里为1), 若设置成功则表示当前线程获取锁,返回true。这个操作外部方法lock()就做过一次,这里再做只是为了再尝试一次,尽量以最简单的方式获取锁。

    如果状态不为0,再判断当前线程是否是锁的owner(即当前线程在之前已经获取锁,这里又来获取),如果是owner, 则尝试将状态值增加acquires,如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true。这里可以看非公平锁的涵义,即获取锁并不会严格根据争用锁的先后顺序决定。这里的实现逻辑类似synchroized关键字的偏向锁的做法,即可重入而不用进一步进行锁的竞争,也解释了ReentrantLock中Reentrant的意义。

    如果状态不为0,且当前线程不是owner,则返回false。
    回到上面的代码1,tryAcquire返回false,接着执行addWaiter(Node.EXCLUSIVE),这个方法创建结点并入队,来看下源码:

    
        private Node addWaiter(Node mode) {
                Node node = new Node(Thread.currentThread(), mode);
                // Try the fast path of enq; backup to full enq on failure
                Node pred = tail;
                if (pred != null) {
                    node.prev = pred;
                    if (compareAndSetTail(pred, node)) {
                        pred.next = node;
                        return node;
                    }
                }
                enq(node);
                return node;
            }
    

    首先创建一个Node对象,Node中包含了当前线程和Node模式(这时是排他模式)。tail是AQS的中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法,从字面可以看出这是一个入队操作,来看下具体入队细节:

    
        private Node enq(final Node node) {
                for (;;) {
                    Node t = tail;
                    if (t == null) { // Must initialize
                        if (compareAndSetHead(new Node()))
                            tail = head;
                    } else {
                        node.prev = t;
                        if (compareAndSetTail(t, node)) {
                            t.next = node;
                            return t;
                        }
                    }
                }
            }
    

    方法体是一个死循环,本身没有锁,可以多个线程并发访问,假如某个线程进入方法,此时head, tail都为null, 进入if(t==null)区域,从方法名可以看出这里是用CAS的方式创建一个空的Node作为头结点,因为此时队列中只一个头结点,所以tail也指向它,第一次循环执行结束。注意这里使用CAS是防止多个线程并发执行到这儿时,只有一个线程能够执行成功,防止创建多个同步队列。

    进行第二次循环时(或者是其他线程enq时),tail不为null,进入else区域。将当前线程的Node结点(简称CNode)的prev指向tail,然后使用CAS将tail指向CNode。看下这里的实现:

    
        private final boolean compareAndSetTail(Node expect, Node update) {
                return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
            }
    

    expect为t, t此时指向tail,所以可以CAS成功,将tail重新指向CNode。此时t为更新前的tail的值,即指向空的头结点,t.next=node,就将头结点的后续结点指向CNode,返回头结点。经过上面的操作,头结点和CNode的关系如图:

    图片描述

    其他线程再插入节点以此类推,都是在追加到链表尾部,并且通过CAS操作保证线程安全。

    通过上面分析可知,AQS的写入是一种双向链表的插入操作,至此addWaiter分析完毕。

    addWaiter返回了插入的节点,作为acquireQueued方法的入参,看下源码:

        final boolean acquireQueued(final Node node, int arg) {
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    final Node p = node.predecessor();
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        failed = false;
                        return interrupted;
                    }
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }

    可以看到,acquireQueued方法也是一个死循环,直到进入 if (p == head && tryAcquire(arg))条件方法块。还是接着刚才的操作来分析。acquireQueued接收的参数是addWaiter方法的返回值,也就是刚才的CNode节点,arg=1。node.predecessor()返回CNode的前置节点,在这里也就是head节点,所以p==head成立,进而进行tryAcquire操作,即争用锁, 如果获取成功,则进入if方法体,看下接下来的操作:

    1) 将CNode设置为头节点。
    2) 将CNode的前置节点设置的next设置为null。

    此时队列如图:

    图片描述

    上面操作即完成了FIFO的出队操作。
    从上面的分析可以看出,只有队列的第二个节点可以有机会争用锁,如果成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点,if (p == head)条件不成立,首先进行shouldParkAfterFailedAcquire(p, node)操作(争用锁失败的第二个节点也如此), 来看下源码:

        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
            int ws = pred.waitStatus;
            if (ws == Node.SIGNAL)
                /*
                 * This node has already set status asking a release
                 * to signal it, so it can safely park.
                 */
                return true;
            if (ws > 0) {
                /*
                 * Predecessor was cancelled. Skip over predecessors and
                 * indicate retry.
                 */
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                /*
                 * waitStatus must be 0 or PROPAGATE.  Indicate that we
                 * need a signal, but don't park yet.  Caller will need to
                 * retry to make sure it cannot acquire before parking.
                 */
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            }
            return false;
        }

    shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,是说明此节点已经将状态设置如果锁释放,则应当通知它,所以它可以安全的阻塞了,返回true。

    如果前节点的状态大于0,即为CANCELLED状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。

    前节点状态小于0的情况是对应ReentrantLock的Condition条件等待的,这里不进行展开。

    如果shouldParkAfterFailedAcquire返回了true,则会执行:“parkAndCheckInterrupt()”方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。

    释放锁

    通过ReentrantLock的unlock方法来看下AQS的锁释放过程。来看下源码:

        public void unlock() {
            sync.release(1);
        }
    
        public final boolean release(int arg) {
            if (tryRelease(arg)) {
                Node h = head;
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    

    unlock调用AQS的release()来完成, AQS的如果tryRelease方法由具体子类实现。tryRelease返回true,则会将head传入到unparkSuccessor(Node)方法中并返回true,否则返回false。首先来看看Sync中tryRelease(int)方法实现,如下所示:

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
        setExclusiveOwnerThread(null);
        }
        setState(c);
        return free
    }

    这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。
    在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。

    在方法unparkSuccessor(Node)中,就意味着真正要释放锁了,它传入的是head节点(head节点是占用锁的节点),看下源码:

        private void unparkSuccessor(Node node) {
            /*
             * If status is negative (i.e., possibly needing signal) try
             * to clear in anticipation of signalling.  It is OK if this
             * fails or if status is changed by waiting thread.
             */
            int ws = node.waitStatus;
            if (ws < 0)
                compareAndSetWaitStatus(node, ws, 0);
    
            /*
             * Thread to unpark is held in successor, which is normally
             * just the next node.  But if cancelled or apparently null,
             * traverse backwards from tail to find the actual
             * non-cancelled successor.
             */
            Node s = node.next;
            if (s == null || s.waitStatus > 0) {
                s = null;
                for (Node t = tail; t != null && t != node; t = t.prev)
                    if (t.waitStatus <= 0)
                        s = t;
            }
            if (s != null)
                LockSupport.unpark(s.thread);
        }

    内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁。

    以上ReentrantLock的释放锁的过程就分析完毕了。
    结合对ReentrantLock的加解锁的过程的分析,本文对AQS的内部结构及原理进行了深入的分析,应看到Java通过一个AQS队列解决了许多问题,这个是Java层面的队列模型,其实我们也可以利用许多队列模型来解决自己的问题,甚至于可以改写模型模型来满足自己的需求。

    查看原文

    赞 22 收藏 29 评论 0

    认证与成就

    • 获得 55 次点赞
    • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2016-09-18
    个人主页被 1.7k 人浏览