码农戏码

新生代农民工的自我修养

0%

没有一身好内功,招式再多都是空;算法绝对是防身必备,面试时更是不可或缺;跟着算法渣一起从零学算法

定义

希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一

算法

直接插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次

是否能够减少这样的移位呢?不希望是一步一步的移动,而是大步大步的移动

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止

初始时,有一个大小为 10 的无序序列

在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组

接下来,按照直接插入排序的方法对每个组进行排序

在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组

按照直接插入排序的方法对每个组进行排序

在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组

按照直接插入排序的方法对每个组进行排序。此时,排序已经结束

演示

  • 1.增量N/2=4分组

{35, 14}, {33, 19}, {42, 27} , {10, 44}

排序完:

  • 2.增量N/2/2=2分组

{14, 27, 35, 42}, {19, 10, 33, 44}

排序完:

  • 3.增量N/2/2/2=1分组排序

也可以通过动画更清晰了解整个排序过程

动画:https://www.zhuxingsheng.com/tools/sort/sort.html

分组排序后,他们的索引还是保留在原始索引中,来两个更加直观的动画演示

image

image

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void shellSort(int []array) {
//以length/2为增量
for (int gap=array.length/2;gap>0;gap= gap/2) {
//以gap分组,进行排序
for(int i = gap;i<array.length;i++){
int tmp = array[i];
int j = i - gap;
//相对直接插入,步长从1变成了gap
while ( j>=0 && tmp<array[j] ) {
array[j+gap] = array[j];
j=j-gap;
}
array[j+gap] = tmp;
}
}
}

复杂度

希尔排序的时间复杂度与增量(即,步长gap)的选取有关

{1,2,4,8,…}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是O(n²)

Hibbard提出了另一个增量序列{1,3,7,…,2^k-1},这种序列的时间复杂度(最坏情形)为O(n^1.5)

Sedgewick提出了几种增量序列,其最坏情形运行时间为O(n^1.3),其中最好的一个序列是{1,5,19,41,109,…}

1
9×4^i−9×2i+1  和 4^i−3×2^i+1  这两个算式

其中, i=0,1,2,⋯ 时,第一个式子计算出的值分别为1,19,109,……; i=2,3,⋯ 时,第二个式子算出的值分别为5,41,……

Best| Average| Worst|Memory|Stable
— | —| —|——| :–: |—
n log(n) |depends on gap sequenc| n² | 1 | No

前言

其实不只在微服务中,在平常网络请求,或者与第三方系统进行交互都需要设置超时时间

为什么需要超时与重试? 总体上讲,肯定是为了增加系统可靠性,具体表现在两个方面

  1. 系统自我保护:
    快速失败,在业务最大允许等待时间内未收到返回数据,主动放弃等待,释放占用资源,避免请求不断累积带来的客户端雪崩效应
  2. 成功率:服务处理超时原因有很多,但常见的超时都是短暂的,主要是GC,或者有网络抖动等。这些短时间影响服务端状态的情况而造成请求成功率下降,需要补救措施。简单的补救有超时重试操作:当前请求超时后,将会重试到非当前服务器,降低重试超时的机率

这一篇将由浅入深探索timeout机制,以及在微服务下的实践

超时

经常被提起的两种超时:connection timeout、socket timeout

通过最底层的Socket,ServerSocket演示一下这两种超时的表现,nio框架都会有对应的配置选项

connectionTimeout

建立连接超时时间

客户端,随便写个IP,设置一个timeout

1
2
Socket socket = new Socket();
socket.connect(new InetSocketAddress("10.0.0.1",8080),20000);

在timeout时间到时,就会抛出connect timed out异常

1
2
3
4
5
6
7
8
9
Exception in thread "main" java.net.SocketTimeoutException: connect timed out
at java.net.DualStackPlainSocketImpl.waitForConnect(Native Method)
at java.net.DualStackPlainSocketImpl.socketConnect(DualStackPlainSocketImpl.java:85)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.PlainSocketImpl.connect(PlainSocketImpl.java:172)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)

socketTimeout

Enable/disable SO_TIMEOUT with the specified timeout, in milliseconds. With this option set to a non-zero timeout, a read() call on the InputStream associated with this Socket will block for only this amount of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the Socket is still valid. The option must be enabled prior to entering the blocking operation to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite timeout.

服务端,只要让客户端能连接上就行,不发送数据

1
2
3
4
5
ServerSocket serverSocket = new ServerSocket(8080);
while ( true) {
Socket socket = serverSocket.accept();
new Thread(new P(socket)).start();
}

客户端,进行读数据

1
2
3
Socket socket = new Socket();
socket.connect(new InetSocketAddress("localhost",8080),20000);
socket.setSoTimeout(3000);

3s后,就抛出Read timed out

1
2
3
4
5
6
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:170)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:223)

nio

对NIO,看网上一些示例基本没有关注到这一点,所以值得思考,难道是nio不需要关注timeout?

客户端对服务器的连接:

1
2
3
4
Selector selector = Selector.open();
InetSocketAddress isa = new InetSocketAddress(host, port);
// 调用open静态方法创建连接到指定主机的SocketChannel
SocketChannel sc = SocketChannel.open(isa);

在调用SocketChannel.open()方法时,如果连接不上服务器,那么此方法就会长时间阻塞,为了解决这个问题,我们可以在调用open()方法前,启动一个定时器,这个定时器会在指定的时间内检查是否已连接成功,这个指定的时间也就是我们希望设置的连接超时时间,当检查已连接上服务器时,提示用户已连接成功;若没有连接上,可在代码中抛出SocketTimeoutException,并提示用户连接超时

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
public void connect(){
try{
selector = Selector.open();
InetSocketAddress isa = new InetSocketAddress(host, port);
//10秒连接超时
new Timer().schedule(tt, 10000);
// 调用open静态方法创建连接到指定主机的SocketChannel
sc = SocketChannel.open(isa);
// 设置该sc以非阻塞方式工作
sc.configureBlocking(false);
// 将Socketchannel对象注册到指定Selector
sc.register(selector, SelectionKey.OP_READ);
Message msg = new Message();
msg.what = 0;
msg.obj = sc;
handler.sendMessage(msg); // 连接成功
new Thread(new NIOReceiveThread(selector, handler)).start();
}catch (IOException e) {
e.printStackTrace();
handler.sendEmptyMessage(-1); // IO异常
}
}
TimerTask tt = new TimerTask(){
@Override
public void run(){
if (sc == null || !sc.isConnected()){
try{
throw new SocketTimeoutException("连接超时");
}catch (SocketTimeoutException e){
e.printStackTrace();
handler.sendEmptyMessage(-6); // 连接超时
}
}
}
};

在stackoverflow上有人回答了Read timeout for an NIO SocketChannel?

  1. You are using a Selector, in which case you have a select timeout which you can play with, and if it goes off (select(timeout) returns zero) you close all the registered channels, or
  2. You are using blocking mode, in which case you might think you should be able to call Socket.setSoTimeout() on the underlying socket (SocketChannel.socket()), and trap the SocketTimeoutException that is thrown when the timeout expires during read(), but you can’t, because it isn’t supported for sockets originating as channels, or
  3. You are using non-blocking mode without a Selector, in which case you need to change to case (1).

netty

netty业界公认的高成熟nio产品,也是大多数微服务底层使用的通信框架,内部细节值得挖一挖处理方式,篇幅有限,另开篇深挖

先看在微服务产品中的使用

connectionTimeout

这种场景很简单,在使用netty时,对应的配置选项

1
2
Bootstrap b = new Bootstrap();
b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)

socketTimeout

这种场景各个框架在处理里,手法不尽相同,各有特色

motan

Motan是一套高性能、易于使用的分布式远程服务调用(RPC)框架,新浪微博开源。

之前解读过motan源码,《motan客户端解析》,《motan服务端解析》

NettyChannel.request

  1. 获取此method的配置timeout
  2. 在write时,awaitUninterruptibly
  3. 超时就抛出timeoutException
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
public Response request(Request request) throws TransportException {
int timeout = nettyClient.getUrl().getMethodParameter(request.getMethodName(), request.getParamtersDesc(),
URLParamType.requestTimeout.getName(), URLParamType.requestTimeout.getIntValue());
if (timeout <= 0) {
throw new MotanFrameworkException("NettyClient init Error: timeout(" + timeout + ") <= 0 is forbid.",
MotanErrorMsgConstant.FRAMEWORK_INIT_ERROR);
}
NettyResponseFuture response = new NettyResponseFuture(request, timeout, this.nettyClient);
this.nettyClient.registerCallback(request.getRequestId(), response);

ChannelFuture writeFuture = this.channel.write(request);

boolean result = writeFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS);

if (result && writeFuture.isSuccess()) {
response.addListener(new FutureListener() {
@Override
public void operationComplete(Future future) throws Exception {
if (future.isSuccess() || (future.isDone() && ExceptionUtil.isBizException(future.getException()))) {
// 成功的调用
nettyClient.resetErrorCount();
} else {
// 失败的调用
nettyClient.incrErrorCount();
}
}
});
return response;
}

writeFuture.cancel();
response = this.nettyClient.removeCallback(request.getRequestId());

if (response != null) {
response.cancel();
}

// 失败的调用
nettyClient.incrErrorCount();

if (writeFuture.getCause() != null) {
throw new MotanServiceException("NettyChannel send request to server Error: url="
+ nettyClient.getUrl().getUri() + " local=" + localAddress + " "
+ MotanFrameworkUtil.toString(request), writeFuture.getCause());
} else {
throw new MotanServiceException("NettyChannel send request to server Timeout: url="
+ nettyClient.getUrl().getUri() + " local=" + localAddress + " "
+ MotanFrameworkUtil.toString(request));
}
}

注意到其中的NettyResponseFuture,看名字就明白是个Future模式,在《代码小析 - 异步回调》中有分析过

接收到服务端返回

1
2
3
4
5
6
7
8
9
10
11
12
13
NettyResponseFuture responseFuture = NettyClient.this.removeCallback(response.getRequestId());
if (responseFuture == null) {
LoggerUtil.warn(
"NettyClient has response from server, but resonseFuture not exist, requestId={}",
response.getRequestId());
return null;
}

if (response.getException() != null) {
responseFuture.onFailure(response);
} else {
responseFuture.onSuccess(response);
}

获取真实结果NettyResponseFuture.getValue()

通过long waitTime = timeout - (System.currentTimeMillis() - createTime);计算一下真正的wait时间

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
public Object getValue() {
synchronized (lock) {
if (!isDoing()) {
return getValueOrThrowable();
}

if (timeout <= 0) {
try {
lock.wait();
} catch (Exception e) {
cancel(new MotanServiceException("NettyResponseFuture getValue InterruptedException : "
+ MotanFrameworkUtil.toString(request) + " cost="
+ (System.currentTimeMillis() - createTime), e));
}

// don't need to notifylisteners, because onSuccess or
// onFailure or cancel method already call notifylisteners
return getValueOrThrowable();
} else {
long waitTime = timeout - (System.currentTimeMillis() - createTime);

if (waitTime > 0) {
for (;;) {
try {
lock.wait(waitTime);
} catch (InterruptedException e) {
}

if (!isDoing()) {
break;
} else {
waitTime = timeout - (System.currentTimeMillis() - createTime);
if (waitTime <= 0) {
break;
}
}
}
}

if (isDoing()) {
timeoutSoCancel();
}
}
return getValueOrThrowable();
}
}

totalTimeout

有了timeout基本已经满足需求,但这儿再提一个totalTimeout,为什么需要一个总超时时间

比如客户端希望服务端在60ms内返回,由于成功率要求必须加一次重试,但是设置超时时间30ms重试一次会很浪费(绝大部分重试很快,但预留了30ms则压缩了初次调用的时间)。设置40ms重试一次就可能出现整体耗时大于60ms。所以希望可以配置整体超时时间为60ms,单次40ms加重试一次,这样既充分利用看重试机会也不会导致整体超过60ms

一次服务调用的正常请求的最长时间为:timeout * failovertimes + success_invocation_time

比如某个服务的timeout为100ms,failovertimes为1,正常调用的平均耗时为10ms,那么它的上游在重试情况下的耗时就是:乐观估计100 * 1+10=110ms;悲观估计100 * 1+100=200ms

为了保证总体超时时间,只能把单次超时时间压缩,使得某些情况下可能不需求重试的场景也进行了重试

对比一下,设置totalTimeout与不设置的情况:

  1. 某服务通常能在20ms返回,但是因为某些意外(比如gc),连续两次都要40ms
  • 只能设置单次timeout的情况下,timeout=30ms,failovertimes=1
    因此对于client来说,它看到的调用耗时就是:30ms(超时) + 30ms(超时) = 60ms
  • 分开设置首次超时和总体超时的情况下,timeout=40ms,total_timeout=60ms,failovertimes=1
    因此对于client来说,它看到的调用耗时就是:40ms(超时)+ (60ms-40ms)(超时) = 60ms
  1. 某服务通常能在20ms返回,但是因为某些意外(比如gc),这次需要35ms才能返回。
  • 只能设置单次timeout的情况下,timeout=30ms,failovertimes=1。
    因此对于client来说,它看到的调用耗时就是:30ms(超时) + 20ms = 50ms
  • 分开设置首次超时和总体超时的情况下,timeout=40ms,total_timeout=60ms,failovertimes=1。
    因此对于client来说,它看到的调用耗时就是:35ms(正常返回) = 35ms

重试

因某个服务实例短暂状态不佳而造成的超时,使用重试处理可以让请求转向其他服务实例的做法可以很好改善非集中式问题的成功率。

但如果超时重试只做简单的重试策略:有超时便重试,这样可能会导致服务端的崩溃。

例如:当前基础组件(如db)压力过大而造成超时,如果一律重试的话,会导致服务端集群实际接受请求量翻倍,这会使得基础组件压力无减反增,可能会导致其最终崩溃

实现

  • 思路简单,配置重试次数,出现非业务异常就重试,达到次数上限或者中途成功结束
  • 重试限流,重试造成雪崩的可能性,所以重试需要控制流量

motan

之前解读过motan源码,《motan客户端解析》,motan的failover就是这么处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 先使用method的配置
int tryCount =
refUrl.getMethodParameter(request.getMethodName(), request.getParamtersDesc(), URLParamType.retries.getName(),
URLParamType.retries.getIntValue());
// 如果有问题,则设置为不重试
if (tryCount < 0) {
tryCount = 0;
}

for (int i = 0; i <= tryCount; i++) {
Referer<T> refer = referers.get(i % referers.size());
try {
request.setRetries(i);
return refer.call(request);
} catch (RuntimeException e) {
// 对于业务异常,直接抛出
if (ExceptionUtil.isBizException(e)) {
throw e;
} else if (i >= tryCount) {
throw e;
}
LoggerUtil.warn(String.format("FailoverHaStrategy Call false for request:%s error=%s", request, e.getMessage()));
}
}

超时重试

但像我司框架就没有这样处理,只关注超时重试,因为超时重试主要是解决因偶尔短暂状态不佳而对成功率造成的影响,所以把重点放在处理短暂处于超时状态超时请求,对于长时间处于较大量的超时状态时,将选择不进行重试fail fast

限流

主要是防止重试次数过多,引起系统雪崩,有必要进行一下限流

《微服务-熔断机制》中提到过限流算法,细节可参考《计数器算法》

参考资料

Read timeout for an NIO SocketChannel?

没有一身好内功,招式再多都是空;算法绝对是防身必备,面试时更是不可或缺;跟着算法渣一起从零学算法

定义

插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序

它的基本思想是:每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止

联想打扑克牌时理顺手中牌的时候,你从左往右依次检查你的牌,并将其和前面的牌进行比较,然后将其插入正确的位置

理解起来,比《快速排序》容易多了

算法

  1. 设置监视哨r[0],将待插入记录的值赋值给r[0];
  2. 设置开始查找的位置j;
  3. 在数组中进行搜索,搜索中将第j个记录后移,直至r[0].key≥r[j].key为止;
  4. 将r[0]插入r[j+1]的位置上。

演示

插入排序

也可以通过动画更清晰了解整个排序过程

动画:https://www.zhuxingsheng.com/tools/sort/sort.html

实现

插入排序包括:直接插入排序,二分插入排序(又称折半插入排序),链表插入排序,希尔排序(又称缩小增量排序)

1
2
3
4
5
6
7
8
9
10
11
12
static void insertSort1(int []sorts) {
for(int i = 1;i<sorts.length;i++) {
int tmp = sorts[i];//哨兵
int j= i-1;
while(j >=0 && tmp < sorts[j]) {
sorts[j+1] = sorts[j];//比tmp大的全部往右移动
j--;
}
//别的移完位置,把哨兵放到正确的位置
sorts[++j] = tmp;
}
}

复杂度

当问题规模为n时

最好情况(原本就是有序的)

比较次数:Cmin=n-1

移动次数:Mmin=0

最差情况(逆序)

比较次数:Cmax=2+3+4+……+n=(n+2)n/2

移动次数:Mmax=1+2+3+……+n-1=n*n/2

Best| Average| Worst|Memory|Stable
— | —| —|——| :–: |—
n |n^2| n^2 | 1 | Yes

没有一身好内功,招式再多都是空;算法绝对是防身必备,面试时更是不可或缺;跟着算法渣一起从零学算法

定义

快速排序(Quicksort)是对冒泡排序的一种改进

快速排序由C. A. R. Hoare在1962年提出。

它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

算法

设要排序的数组是A[0]……A[N-1],首先任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序。值得注意的是,快速排序不是一种稳定的排序算法,也就是说,多个相同的值的相对位置也许会在算法结束时产生变动

一趟快速排序的算法是:

  1. 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
  2. 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
  3. 从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
  4. 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
  5. 重复第3、4步,直到i=j; (3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

演示

上面的定义,算法都是很学术的表达,看得有点晕了,看一个示例:

对数组[2,10,8,22,34,5,12,28,21,11]进行快速排序

快速排序

也可以通过动画更清晰了解整个排序过程

动画:https://www.zhuxingsheng.com/tools/sort/sort.html

实现

快速排序有好些实现手法,左右指针法、挖坑法、前后指针(快慢指针)法

左右指针法

算法过程与上面的演示图差不多

  1. 先从数列中取出一个数作为中轴基数。
  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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 分治法,从小到大排序
* @param array
*/
public static int partition(int []array,int start,int end) {

//取一位做为基数
int pivot = array[end];//这儿取最后一位作为基数了

int left = start;
int right = end-1;//从倒数第二位开始比较

//从两头向中间搜索,从小到大排序
while (left < right) {
//从left端开始搜索
while(left < right && array[left] <= pivot) {
left ++;
}
//从right端开始搜索
while (left < right && array[right] >= pivot) {
right -- ;
}
//两数交换,大的放到右边,小的放到左边
if(left < right) {
swap(array,left,right);
left++;
right--;
}
}
//
if(left != end && array[left] > array[end] ) {
swap(array,left,end);
return left;
}
//right == left
return left+1;
}

/**
* 分治法,从小到大排序
* @param array
* @param start
* @param end
*/
private static void quicSort(int []array,int start,int end){
if( start < end ){
int partition = partition(array,start,end);
print(array);
quicSort(array,start,partition-1);
quicSort(array,partition+1,end);
}
}

挖坑法

挖坑填坑,也可以叫拆东墙补西墙

先演示一番,对[22,12,28,21,14]进行排序

index: 0 1 2 3 4
data: 22 12 28 21 14

第一步:挖最右边的数为坑

坑: pit=array[right]=14;左索引: left=0,右索引: right=3,

index: 0 1 2 3 4
data: 22 12 28 21 X

从left开始搜索,寻找比坑大的数,发现array[left=0]>14,那就用left=0 填 right=4的坑

array[right=4]=array[left=0];left++;

此时数组:

index: 0 1 2 3 4
data: X 12 28 21 22

left=1,right=3

第二步: 从right端开始找比pit小的数,发现array[1] < 14,那就用right=1 填 left=0的坑

array[left=0]= array[right=1];

此时数组:

index: 0 1 2 3 4
data: 12 X 28 21 22

left=1,right=0; left>right,那此回合结束,用pit填回坑array[left=1]

结束时:

index: 0 1 2 3 4
data: 12 14 28 21 22

发现14左都是小于它的数,右边都是大于它的数

以14为分界,两边数组进行递归下去,就行了

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
int partion(int arr[], int begin, int end)
{
int pit = arr[end];//挖最右数,留一个坑
int left = begin;
int right = end;

while (left < right)
{
//从最左开始搜索比坑大的数
while (left < right && arr[left] <= pit)
left++;
if (left<right) {
arr[right] = arr[left];//拿left去填坑,left成为新坑
}
while (left < right && arr[right] >= pit)
right--;
if (left < right) {
arr[left] = arr[right];//right去填left坑,left成为新坑
}
}

arr[left] = pit;//用key填坑
return left;
}

void QuickSort(int arr[], int left, int right)
{
if (left < right)
{
int mid = partion(arr, left, right);
print(arr);
QuickSort(arr, left, mid - 1);
QuickSort(arr, mid + 1, right);
}
}

前后指针法

大体思路:

  1. 定义两个指针,一前一后,cur(前)指向起始位置,prev(后)指向cur的前一个位置;选择数组最后一个元素作为key(right)
  2. cur找比key小的数,prev在cur没有找到的情况下,一直不动(即保证prev一直指向比key大的数);直到cur找到比key小的数(+ +prev && prev != cur时)然后++cur,prev仍然不动
  3. 直到cur走到right前一个位置,终止循环。最后++prev,交换prev和right的值。返回中间位置prev。最后再继续递归

前后指针法

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
int partion(int arr[], int left, int right)
{
int key = arr[right];//取最后一位为key

int cur = left;//当前指针
int prev = left - 1;//前一个指针

while (cur < right)
{
if(arr[cur] < key && ++prev != cur){//发现比key小的数
swap(arr,cur,prev);
}
cur++;
}
//最后++prev交换
swap(arr,++prev,cur);
return prev;
}

void QuickSort(int arr[], int left, int right)
{
if (left < right)
{
int mid = partion(arr, left, right);
print(arr);
QuickSort(arr, left, mid - 1);
QuickSort(arr, mid + 1, right);
}
}

分治

以上几个方法,不管怎样的思路,都是寻找一个标准数,让它成为一个分界,大于所有左边的数,小于所有右边的数,进行分而治之

复杂度

最好情况:最在最好的情况下,每次的划分都会恰好从中间将序列划分开来,那么只需要lgn次划分即可划分完成,是一个标准的分治算法Cn=2Cn/2+N,每一次划分都需要比较N次。时间复杂度O(nlogn)

最坏情况:在最坏的情况下,即序列已经排好序的情况下,每次划分都恰好把数组划分成了0,n两部分,那么需要n次划分,但是比较的次数则变成了n, n-1, n-2,….1, 所以整个比较次数约为n(n-1)/2~n2/2

比较和移动次数最少时间复杂度表示为O(nlogn);

比较和移动次数最多的时间复杂度表示为O(n^2);

使用的辅助存储空间最少为logn,最多为n^2;是不稳定的排序;

Best| Average| Worst|Memory|Stable
— | —| —|——| :–: |—
nlog(n) |nlog(n)| n^2 | log(n) | No

没有一身好内功,招式再多都是空;算法绝对是防身必备,面试时更是不可或缺;跟着算法渣一起从零学算法

定义

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。

这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”

算法

它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素已经排序完成。

演示

对数组[5,6,3,1,8,7,2,4]进行冒泡排序

BubbleSort

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 传统写法
* @param sorts
*/
private static void bubbleSort(int [] sorts){
for (int i=0;i<sorts.length-1;i++) {
for (int j= 0;j<sorts.length-1-i;j++) {
if(sorts[j] < sorts[j+1]) {
swap(sorts,j,j+1);
}
}
}
}

private static void swap(int []sorts,int i,int j){
int tmp = sorts[i];
sorts[i] = sorts[j];
sorts[j] = tmp;
}

改进

有序

如果一个数组本身就是有序的,或者部分已经有序

比如:[8,7,6,5,4,3,1,2]

假如现在有一个长度10000的数组,前1000无序,后9000有序并且大于前1000数据。用上面这种方法,数据交换次数在1000之内,但是剩下9000的数据虽然没有交换,但是每次循环都会进行比较。剩下9000数据已经有序了,我们不要对它们去进行判断浪费不必要的时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 如果部分已经排序,设置一个flag,有序就跳出循环
* @param sorts
*/
private static void bubbleSort1(int [] sorts){
for (int i=0;i<sorts.length-1;i++) {
boolean flag = false;
for (int j= 0;j<sorts.length-1-i;j++) {
if(sorts[j] < sorts[j+1]) {
swap(sorts,j,j+1);
flag = true;
}
}
if(!flag) {
break;
}
}
}

鸡尾酒排序

鸡尾酒排序等于是冒泡排序的轻微变形。不同的地方在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较序列里的每个元素。他可以得到比冒泡排序稍微好一点的效能,原因是冒泡排序只从一个方向进行比对(由低到高),每次循环只移动一个项目。

算法

排序过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 鸡尾酒排序,定向冒泡排序
* @param sorts
*/
private static void bubbleSort2(int [] sorts){
int i = 0;
int j = sorts.length-1;
while (i <= j) {
for(int s = i;s<j;s++) {//从左往右
if(sorts[s] < sorts[s+1]) {
swap(sorts, s, s + 1);
}
}

System.out.println(JSON.toJSONString(sorts));
for(int e = j;e>i;e--) {//从右往左
if(sorts[e] > sorts[e-1]) {
swap(sorts, e, e - 1);
}
}
j--;i++;
System.out.println(JSON.toJSONString(sorts));
}
}

总体上,鸡尾酒排序可以获得比冒泡排序稍好的性能。但是完全逆序时,鸡尾酒排序与冒泡排序的效率都非常差

时间复杂度

  1. 如果我们的数据正序,只需要走一趟即可完成排序。所需的比较次数C和记录移动次数M均达到最小值,即:Cmin=n-1;Mmin=0;所以,冒泡排序最好的时间复杂度为O(n)。

  2. 如果很不幸我们的数据是反序的,则需要进行n-1趟排序。每趟排序要进行n-i次比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:

Best| Average| Worst|Memory|Stable
— | —| —|——| :–: |—
n | n^2 |n^2| 1 | Stable

天下皆知美之为美,斯恶已;此专栏本想取名代码之美,但有傍名之嫌,也给别误解,所以就叫代码小析吧,看到一段好代码,思路清奇,奇巧淫技,拿出来鉴赏一番

之前是计划one week one alogrithm,结果算法是个短板,不仅要理解,还得再写出代码,特别烧脑,所以中间穿插一下,换换脑子

之前有类似一篇《仅且仅创建一次对象》

最近看到一个段子:

老板有毛病吧,写完排序就叫我走人,我还嫌你这9K工资低了呢

感觉能想到这思路的也算清奇,哈哈!

回调

if you call me, i will call back

回调分类:同步回调,异步回调

场景

建立TCP连接是很耗时的,所以在创建Socket Channel时,可以通过异步回调方式解决

代码

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
/**
* 异步取得channel
* @param index
* @param callback
*/
public void asynGetChannel(int index,final Callback callback) {
// 1. 随机获取一条channel
final int pos = ThreadLocalRandom.current().nextInt(MAX_CONNECTIONS);
Channel target = channels[pos];

// 2. 如果获取到了连接,直接返回
if (target != null && target.isActive()) {
logger.info("direct success "+index);
callback.onSuccess(target);
return;
}

synchronized (locks[pos]) {
target = channels[pos];
// 2. 如果获取到了连接,直接返回
if (target != null && target.isActive()) {
callback.onSuccess(target);
return;
}

// 3.如果连接正在创建中,则加入queue
if (target instanceof EmptyChannel) {
boolean result = jobs.offer(callback);
if (result) {
return;
} else {
throw new RuntimeException("Can't connet to target server and the waiting queue is full");
}
}

// 4. 连接尚未创建
channels[pos] = new EmptyChannel();

Connector.connect(host, port, new Callback() {
@Override
public void onSuccess(Channel channel) {
logger.info(index + " ------------connect success---------"+pos + " channel:" +channels[pos].getClass().getName());
List<Callback> tmpJobs;//建立一个tempJobs,快速释放锁
synchronized (locks[pos]) {
// 设置channels,拷贝jobs队列,释放锁
channels[pos] = channel;
tmpJobs = drainJobs();
}
for(Callback pendingCallback : tmpJobs) {
try {
if(pendingCallback != callback) {
pendingCallback.onSuccess(channel);
}
} catch (Exception e) {
logger.error("call connectionCallback fail", e);
}
}
}

@Override
public void onError(Throwable e) {
List<Callback> tmpJobs;//建立一个tempJobs,快速释放锁
synchronized (locks[pos]) {
// 设置channels,拷贝jobs队列,释放锁
channels[pos] = null;
tmpJobs = drainJobs();
}
for(Callback pendingCallback : tmpJobs) {
try {
if(pendingCallback != callback) {
pendingCallback.onError(e);
}
} catch (Exception x) {
logger.error("call connectionCallback fail", x);
}
}
}
});
}
}

完整的代码:https://github.com/zhuxingsheng/javastudy

亮点

思路很简单,亮点就在于job队列,连接在没有建立成功时,会先建立一个EmptyChannel,有些类似lazy load中的影子对象放到队列中,不造成阻塞,当channel建立完成后,回调

VS Future模式

异步回调的套路与Future模式特别类似

1
2
3
4
Future future = doTask1();
doTask2();
doTask3();
Result result = future.get();

Future 模式中,一个任务的启动和获取结果分成了两部分,启动执行是异步的,调用后立马返回,调用者可以继续做其他的任务,而等到其他任务做完,再获取Future的结果,此时调用 get 时是同步的,也就是说如果 doTask1 如果还没有做完,等它做完。

看出最大区别,异步回调不需要返回值,准确说调用者不用太关心返回值,甚至不需要关心真正执行情况,而future模式就不一样了,调用者是一定要拿到返回值的

参考

同步调用,异步回调和 Future 模式

算法虐我千万遍,我待算法如初恋;IT人永远逃脱不了的算法

概念

算法是特定问题求解步骤的描述,在计算机中表现为指令的有限序列

算法是独立存在的一种解决问题的方法和思想

对于算法而言,实现的语言并不重要,重要的是思想

特性

  1. 输入: 算法具有0个或多个输入
  2. 输出: 算法至少有1个或多个输出
  3. 有穷性: 算法在有限的步骤之后会自动结束而不会无限循环,并且每一个步骤可以在可接受的时间内完成
  4. 确定性:算法中的每一步都有确定的含义,不会出现二义性
  5. 可行性:算法的每一步都是可行的,也就是说每一步都能够执行有限的次数完成

复杂度

一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量

时间复杂度

定义:如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n),它是n的某一函数T(n)称为这一算法的“时间复杂性”

1
T(n) = O(f(n))

当输入量n逐渐加大时,时间复杂性的极限情形称为算法的“渐近时间复杂性”。

比如排序算法:可用算法执行中的数据比较次数与数据移动次数来衡量

空间复杂度

对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。

比如直接插入排序,需一个监视哨兵,空间复杂度是O(1) ,而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。

大O表示法

大O表示法被用来描述一个算法的性能或复杂度。大O表示法可以用来描述一个算法的最差情况,或者一个算法执行的耗时或占用空间(例如内存或磁盘占用)

假设一个算法的时间复杂度是 O(n),n在这里代表的意思就是数据的个数。

举个例子,如果你的代码用一个循环遍历 100 个元素,那么这个算法就是 O(n),n 为 100,所以这里的算法在执行时就要做 100 次工作

大O表示法就是将算法的所有步骤转换为代数项,然后排除不会对问题的整体复杂度产生较大影响的较低阶常数和系数,只关心复杂度最重要的部分

1
2
3
4
5
6
7
规律       Big-O

2 O(1) --> 就是一个常数

2n + 10 O(n) --> n 对整体结果会产生最大影响

5n^2 O(n^2) --> n^2 具有最大影响

O(log n),即对数复杂度(logarithmic complexity)。对数可以是ln(底数为e),log10,log2 或者以其它为底数,这无关紧要,它仍然是O(log n),正如O(2n^2) 和 O(100n^2) 都记为 O(n^2)。

示例

O(1)

O(1)表示该算法的执行时间(或执行时占用空间)总是为一个常量,不论输入的数据集是大是小

1
2
3
4
bool IsFirstElementNull(IList<string> elements)
{
return elements[0] == null;
}

O(N)

O(N)表示一个算法的性能会随着输入数据的大小变化而线性变化。下面的例子同时也表明了大O表示法其实是用来描述一个算法的最差情况的:在for循环中,一旦程序找到了输入数据中与第二个传入的string匹配时,程序就会提前退出,然而大O表示法却总是假定程序会运行到最差情况(在这个例子中,意味着大O会表示程序全部循环完成时的性能)

1
2
3
4
5
6
7
8
9
bool ContainsValue(IList<string> elements, string value)
{
foreach (var element in elements)
{
if (element == value) return true;
}

return false;
}

O(n²)

for循环嵌套的复杂度就是二次方的,因为你在一个线性操作里执行另外一个线性操作(或者说: n*n =n² )

如果嵌套层级不断深入的话,算法的性能将会变为O(N^3),O(N^4),以此类推

1
2
3
4
5
6
7
8
9
10
for (var outer = 0; outer < elements.Count; outer++)
{
for (var inner = 0; inner < elements.Count; inner++)
{
// Don't compare with self
if (outer == inner) continue;

if (elements[outer] == elements[inner]) return true;
}
}

O(2^N)

O(2^N)表示一个算法的性能将会随着输入数据的每次增加而增大两倍。O(2^N)的增长曲线是一条爆炸式增长曲线——开始时较为平滑,但数据增长后曲线增长非常陡峭。一个典型的O(2^N)方法就是裴波那契数列的递归计算实现

1
2
3
4
5
6
int Fibonacci(int number)
{
if (number <= 1) return number;

return Fibonacci(number - 2) + Fibonacci(number - 1);
}

(logn)

1
2
3
i=1;       
while (i<=n)
i=i*2;

比较

1
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)

Big-O Complexity

引申阅读

算法渣-排序-冒泡

算法渣-排序-快速排序

算法渣-排序-插入

算法渣-排序-希尔

1、为什么需要自定义类加载器

  1. 《类加载器》中讲的,默认类加载器只能加载固定路径下的class,如果有特定路径下的class,需要自定义
  2. 安全性:系统自身需要一些jar,class,如果业务类代码中也有相同的class,破坏系统,类似双亲委托安全性

可以看看tomcat自定义类加载器的原因,别的就大同小异了

1
2
3
4
5
6
7
8
a)、要保证部署在tomcat上的每个应用依赖的类库相互独立,不受影响。
b)、由于tomcat是采用java语言编写的,它自身也有类库依赖,为了安全考虑,tomcat使用的类库要与部署的应用的类库相互独立。
c)、有些类库tomcat与部署的应用可以共享,比如说servlet-api,使用maven编写web程序时,servlet-api的范围是provided,
表示打包时不打包这个依赖,因为我们都知道服务器已经有这个依赖了。
d)、部署的应用之间的类库可以共享。这听起来好像与第一点相互矛盾,但其实这很合理,类被类加载器加载到虚拟机后,
会生成代表该类的class对象存放在永久代区域,这时候如果有大量的应用使用spring来管理,如果spring类库不能共享,
那每个应用的spring类库都会被加载一次,将会是很大的资源浪费。

2、自定义加载器

这儿主要说下我司的自定义类加载器;更复杂点的可以看看tomcat的类加载机制

为什么需要自定义类加载器?这可以参考章节1的答案

主要在于应用与基础平台的隔离,相对应用:可以有更大技术选型自由度,不用考虑基础平台的jar包版本、相对平台:更可靠安全,不被应用class影响

类加载器结构

虽然JAVA使用了类加载的委派机制,但并没严格要求开发者必须遵守该机制,我们可以打破这种”双亲委派”机制

目录结构

目录 说明
/servicesdir 业务实现jar包
/thirddir 业务依赖jar包
/platformdir 平台依赖jar包

类加载器

  • 1.PlatformClassLoader平台加载器
    • 1.1.加载/platformdir下的jar包
    • 1.2.在加载时,采用了默认的“双亲委派”
  • 2.AppClassLoader应用加载器
    • 2.1.加载/servicesdir,/thirddir下的jar
    • 2.2.该类加载器一定程度上打破了默认的“双亲委派”
      • 2.2.0.loadClass方法中,如果本加载器没有load到对应的类,则会检查该类是否处于平台类加载器白名单中:
      • 2.2.1.如果处于白名单中,则委派PlatformClassLoader加载
      • 2.2.2.否则,通过super.loadClass(String,boolean)走默认的双亲委派

此处白名单类:平台核心类,不能被同名业务类干扰

预加载

《类加载器》中说过,程序启动后,并不会加载所有类,在运行中实现到时,才会去加载。这儿就有性能损耗。

按类加载规则,一个类只加载一次

可以测试一下,加载需要的损耗

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
/**
* 类加载时间性能测试
*
* 看一下类加载需要消耗的时间
* Created by Jack on 2018/10/8.
*/
public class ClassLoaderTest1 {
public static void main(String[] args) throws SQLException {
long s = System.nanoTime();

LoaderClass loaderClass = new LoaderClass();
long e = System.nanoTime();
//第一次时间
System.out.println(e - s);
e = System.nanoTime();
//第二次实例,但已经加载过,不再需要加载
LoaderClass loaderClass1 = new LoaderClass();
long e1 = System.nanoTime();
//第二次时间
System.out.println(e1 - e);
}
}
//输出
2409737
396

可以从输出看到性能损耗是不小的,这部分损耗可以通过预加载来消除

随着程序运行时间越久,被触发的业务越多,那加载到的业务类越多。

预加载类的逻辑

ClassWarmUp

  • 1.在classloader中loadClass时,把className加入到LinkedBlockingDeque中
  • 2.为了性能,异步把deque中的class写入到文件中,需要起一个后台线程
    • 2.1 后台线程,从deque中取出class,写入到文件中
  • 3.下次从文件中预先加载class

打包

对于/servicesdir 与 /thirddir 都好处理,但对于platformdir是怎么打包的呢?毕竟在开发时,只是引入一个平台基础jar就行

使用

有了自定义类加载器,在应用主函数中,就不能直接new了,不然就会使用AppClassLoader

所以需要使用反射机制

1
2
3
Class<?> loadClass = platformClassLoader.loadClass("com.jack.Start");
Method startMethod = loadClass.getMethod("startUp");
startMethod.invoke(loadClass);

这样,通过Start加载的类也会通过platformClassLoader去加载

创建springcontext也一样,这儿还需使用到Thread.currentThread().getContextClassLoader()【下面有详解】

1
2
3
4
5
6
7
8
9
10
11
12
ClassLoader currentThreadLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(appClassLoader);

Class<?> contextClass = appClassLoader
.loadClass("org.springframework.context.support.FileSystemXmlApplicationContext");
Class<?>[] parameterTypes = new Class[] { String[].class };
Constructor<?> constructor = contextClass.getConstructor(parameterTypes);
return constructor.newInstance(new Object[] { xmlPaths.toArray(new String[0]) });


// switch back the thread context classloader
Thread.currentThread().setContextClassLoader(currentThreadLoader);

3、反常

“双亲委派”模型有优点,也有力不从心的地方

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。
而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

场景:

  1. 当高层提供了统一的接口让低层去实现,同时又要在高层加载(或者实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管

解决方案:

从jdk1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader c1),分别用来获取和设置类加载器

一般使用模式:获取-使用-还原

1
2
3
4
5
6
7
8
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); 
try{
Thread.currentThread().setContextClassLoader(targetTccl);
excute();
} finally {
Thread.currentThread().setContextClassLoader(classLoader);
}

jdbc

以jdbc看下场景1的情况

1
2
3
4
Class.forName("com.mysql.jdbc.Driver")
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
  • 1.Class.forName(“com.mysql.jdbc.Driver”); 在com.mysql.jdbc.Driver中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}

通过Class.forName(),主要就是执行初始化static代码块,也就是向DriverManager注册Driver

此时:应用类、Driver是由AppClassLoader加载,但由于双亲委派java.sql.DriverManager是由BootstrapClassLoader加载

  • 2.java.sql.DriverManager.getConnection 获取连接
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
private static Connection getConnection(
String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
java.util.Vector drivers = null;

synchronized(DriverManager.class) {
if(callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

if (!initialized) {
initialize();
}

synchronized (DriverManager.class){
drivers = readDrivers;
}

SQLException reason = null;
for (int i = 0; i < drivers.size(); i++) {
DriverInfo di = (DriverInfo)drivers.elementAt(i);

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
Connection result = di.driver.connect(url, info);
if (result != null) {
// Success!
println("getConnection returning " + di);
return (result);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}

if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}


private static Class getCallerClass(ClassLoader callerClassLoader,
String driverClassName) {
Class callerC = null;

try {
callerC = Class.forName(driverClassName, true, callerClassLoader);
}
catch (Exception ex) {
callerC = null; // being very careful
}

return callerC;
}

这其中有两行代码:

1
2
3
4
callerCL = Thread.currentThread().getContextClassLoader();

callerC = Class.forName(driverClassName, true, callerClassLoader);

这儿是取线程上下文中的classloader,也就是AppClassLoader;如果不取此classloader,那么Class.forName(driverClassName)就是使用DriverManager的BootstrapClassLoader加载,那必然是加载不到,这也就是父层类加载器加载不了低层类。

还有个问题,为什么在应用程序中已经加载过Driver,到了getConnection()又要再加载,还得通过Thread.currentThread().getContextClassLoader()?

其实在getConnection()中,只是对比class是否是同一个,像tomcat那样,各个应用都有自己的mysql-driver的jar包,就只能通过classloader来区分,因为class是不是相同需要classname+classloader组合鉴别

spring

对于场景2的问题

如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean

org.springframework.web.context.ContextLoader类

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
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already a root application context present - " +
"check whether you have multiple ContextLoader* definitions in your web.xml!");
}

Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();

try {
// Determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);

// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
this.context = createWebApplicationContext(servletContext, parent);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}

if (logger.isDebugEnabled()) {
logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
}

return this.context;
}
catch (RuntimeException ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
catch (Error err) {
logger.error("Context initialization failed", err);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
throw err;
}
}

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取线程上下文类加载器,默认为WebAppClassLoader
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
// 如果spring的jar包放在每个webapp自己的目录中
// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
// 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
// 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
currentContextPerThread.put(ccl, this.context);
}

这样做的目的在于当通过ConetxtLoader的静态方法获取context的时候,能保证获取的是当前web application的context.实际上就是对于tomcat下面的任何一个线程,我们都能很方便的找出这个线程对应的webapplicationContext.于是在一些不能方便获取servletContext的场合,我们可以通过当前线程获取webapplicationContext.

1
2
3
4
5
6
7
8
9
10
public static WebApplicationContext getCurrentWebApplicationContext() {
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl != null) {
WebApplicationContext ccpt = currentContextPerThread.get(ccl);
if (ccpt != null) {
return ccpt;
}
}
return currentContext;
}

总结

简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作

4、参考资料

以jdbc为例搞清contextClassLoader

java执行过程

先回顾一下要执行java程序,需要经过哪些步骤

执行java程序

  1. 编写java代码
  2. 通过javac把源代码编译成class
  3. 把class载入JVM

1、2两步是需要开发人员参与的,而第3步是JVM的行为,对开发人员透明

JVM类加载

详细看下第三点,class载入JVM过程

从内存空间视角,会分配到各个空间:

内存结构

每个内存空间详情可参考:《GC及JVM参数》

从类生命周期角度,分阶段:

类生命周期

其中类加载的过程包括了加载验证准备解析初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

1.加载

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

在加载阶段,虚拟机需要完成以下3件事情:

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

加载.class文件的方式

  • 从本地系统中直接加载
  • 通过网络下载.class文件
  • 从zip,jar等归档文件中加载.class文件
  • 从专有数据库中提取.class文件
  • 将Java源文件动态编译为.class文件

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载

加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,
而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据

加载时机

  • 当应用程序启动的时候,所有的类不会被一次性加载,因为如果一次性加载,内存资源有限,可能会影响应用程序的正常运行。例如,A a=new A(),
    一个类真正被加载的时机是在创建对象的时候,才会去执行以上过程,加载类。当我们测试的时候,最先加载拥有main方法的主线程所在类

  • Java虚拟机有预加载功能。类加载器并不需要等到某个类被”首次主动使用”时再加载它,JVM规范规定JVM可以预测加载某一个类,如果这个类出错,但是应用程序没有调用这个类, JVM也不会报错;如果调用这个类的话,JVM才会报错,(LinkAgeError错误)

加载方式

隐式加载
  1. 创建类对象
  2. 使用类的静态域
  3. 创建子类对象
  4. 使用子类的静态域
  5. 在JVM启动时,BootStrapLoader会加载一些JVM自身运行所需的class
  6. 在JVM启动时,ExtClassLoader会加载指定目录下一些特殊的class
  7. 在JVM启动时,AppClassLoader会加载classpath路径下的class,以及main函数所在的类的class文件
显式加载
  1. ClassLoader.loadClass(className),不会进行初始化
  2. Class.forName(String name, boolean initialize,ClassLoader loader); 借助当前调用者的class的ClassLoader完成class的加载,加载class的同时根据initialize是否初始化

2.连接

2.1.验证:确保被加载的类的正确性

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

符号引用验证:确保解析动作能正确执行。

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用**-Xverify:none**参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

2.2. 准备:为类的静态变量分配内存,并将其初始化为默认值

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  • 2.2.1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

  • 2.2.2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
    假设一个类变量的定义为:public static int value = 3;
    那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行

  • 2.2.3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

    假设上面的类变量value被定义为: public static final int value = 3;

    编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3

2.3. 解析:虚拟机将常量池中的符号引用替换为直接引用(内存地址)的过程

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

常量池
  1. 字面量:比较接近Java语言层面,如String字符串,声明final的常量等
  2. 符号引用:属于编译原理方面的概念:1、包括类和接口的全限定名 2、字段的名称和描述符3.方法的名称和描述符
常量项结构

常量项结构

这些内容,需要再去分析class文件详细结构,后续再学习了

3.初始化,为类的静态变量赋予正确的初始值

类加载的最后一个阶段,除了加载阶段我们可以通过自定义类加载器参与之外,其余完全又JVM主导。到了初始化阶段,才真正开始执行程序,也就是由java转换成的class

JVM负责对类进行初始化,主要对类变量进行初始化。

在Java中对类变量进行初始值设定有两种方式:

  1. 声明类变量是指定初始值

  2. 使用静态代码块为类变量指定初始值

JVM初始化规则

类初始化时机:只有当对类的主动使用的时候才会导致类的初始化

Java程序对类的使用方式可以分为两种:

  1. 主动使用:会执行加载、连接、初始化静态域
  2. 被动使用:只执行加载、连接,不执行类的初始化静态域
类的主动使用包括以下六种:
  • 创建类的实例,如(1)new (2)反射newInstance (3)序列化生成obj;遇到new、getstatic、putstatic、invokestatic这四条字节码指令
  • 访问某个类或接口的静态变量,或者对该静态变量赋值 (注意static 与static final的区别)
  • 调用类的静态方法
  • 反射(如Class.forName(“Test”))
  • 初始化某个类的子类,则其父类也会被初始化;接口初始化不会导致父接口的初始化(这其实也是static final的原因);对于静态字段,
    • 只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
  • Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
被动使用,不在主动使用的六种以内都是被动的
  • 1.如通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化
  • 2.通过数组定义类引用类,为类的被动使用,不会触发此类的初始化
    • 2.1 原因:其实数组已经不是E类型了,E的数组jvm在运行期,会动态生成一个新的类型,新类型为:
      如果是一维数组,则为:[L+元素的类全名;二维数组,则为[[L+元素的类全名
      如果是基础类型(int/float等),则为[I(int类型)、[F(float类型)等
  • 3.常量在编译阶段会存入调用方法所在的类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 主动 被动使用问题测试
* Created by Jack on 2018/9/28.
*/
public class ClassInitTest3 {
public static void main(String[] args) {
String x = F.s;
}
}

class F {
//因为UUID.randomUUID().toString()这个方法,是运行期确认的,所以,这不是被动使用
static final String s = UUID.randomUUID().toString();

static {
//这儿会被输出
System.out.println("Initialize class F");
}
}

clinit 与 init

在编译生成class文件时,编译器会产生两个方法加于class文件中,一个是类的初始化方法clinit, 另一个是实例的初始化方法init

clinit:

clinit指的是类构造器,这个构造器是jvm自动合并生成的,在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行

它合并static变量的赋值操作

  1. 注意是赋值操作,**(仅声明,或者final static)**不会触发,毕竟前面准备阶段已经默认赋过值为0了
  2. static{}语句块生成,且虚拟机保证执行前,父类的已经执行完毕,所以说父类如果定义static块的话,一定比子类先执行
  3. 如果一个类或接口中没有static变量的赋值操作和static{}语句块,那么不会被JVM生成
  4. static变量的赋值操作和static{}语句块合并的顺序是由语句在源文件中出现的顺序所决定的。
init:

在实例创建出来的时候调用,也就是构造函数,包括:

  1. new操作符
  2. 普通代码块
  3. 调用Class或java.lang.reflect.Constructor对象的newInstance()方法;
  4. 调用任何现有对象的clone()方法;
  5. 通过java.io.ObjectInputStream类的getObject()方法反序列化。
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
/**
* <clinit> 与 <init> 区别
*/
public class ClassInitTest2 {
static {
System.out.println("cinit");

i = 3;//可以赋值
//System.out.println(i);//但不能使用,语法错误
}

private static int i = 1;

{
System.out.println("init");//实例化构造器,
}

public static void main(String [] args) {
new ClassInitTest2();
new ClassInitTest2();
String str = "str";
System.out.println(str);
}
}

// 输出
cinit
init
init
str

static 与 static final 对初始化的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* static 与 static final 对初始化的区别
*/
public class ClassInitFinalTest {
public static int age = 20;

static {
//如果age定义为static final,这儿就不会执行
System.out.println("静态初始化!");
}

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

不会执行类初始化的几种情况

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化
  • 类A引用类B的static final常量不会导致类B初始化 (看上面的ClassInitFinalTest)
  • 通过类名获取Class对象,不会触发类的初始化。如
  • System.out.println(Person.class);
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作

结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致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
/**
* 测试类加载及初始化顺序问题
* Created by jack01.zhu on 2018/9/28.
*/
public class ClassInit {
private static ClassInit singleton = new ClassInit();
public static int counter1;
public static int counter2 = 0;
private ClassInit() {
counter1++;
counter2++;
}
public static ClassInit getSingleton() {
return singleton;
}
}

/**
* 通过输出结果,推测类加载过程
* Created by jack01.zhu on 2018/9/28.
*/
public class ClassInitTestMain {

public static void main(String []args) {
ClassInit classInitTest = ClassInit.getSingleton();
System.out.println("counter1="+classInitTest.counter1);
System.out.println("counter2="+classInitTest.counter2);
}
}

这段代码输出的结果是什么?

1
2
counter1=1
counter2=0
  1. 入口肯定是ClassInitTestMain.main(),从这儿开始加载,初始化
  2. ClassInit.getSingleton(),首次使用化,所以从加载部分开始执行,执行到准备阶段所有static变量都被设置为初始值。此时
1
2
3
public static int counter1 = 0;
public static int counter2 = 0;
private static ClassInit singleton = null;
  1. ClassInit执行到初始化阶段,生成类构造器,类构造器会合并 static变量的赋值操作和 static语句块。合并后执行
1
2
3
4
5
6
public static int counter1 ; // 由于 counter1没被赋值,所以不会被合并进去

public void clinit() {// 伪代码:<clinit>方法体内容
ClassInit singleton = new ClassInit();//(1)
int counter2 = 0;// (2)
}
  1. 初始化阶段 执行clinit内代码,执行到(1)处,此时counter1和counter2都变为1。
  2. 初始化阶段 执行clinit内代码,执行到(2)处,counter2又被设置为0。
  3. 初始化结束 ,回到Main方法的ClassInit.getSingleton();继续执行main方法,最后输出结束。

以上,就是一个类的生命周期,这篇重点就是加载部分,如上面所说,加载阶段相对别的阶段,对开发人员而言有更强的可控性;下面学习一下类加载器相关知识

类加载器

类加载器

  1. BootstrapClassLoader:加载路径: System.getProperty(“java.class.path”) 或直接通过 -Xbootclasspath 指定

特性: 用C语言写的

手动获取加载路径: sun.misc.Launcher.getBootstrapClassPath().getURLs()

  1. ExtClassLoader:加载路径: System.getProperty(“java.ext.dirs”) 或直接通过 -Djava.ext.dirs 指定

特性: 继承 URLClassLoader

手动获取加载路径:((URLClassLoader)App.class.getClassLoader().getParent()).getURLs()

  1. AppClassLoader:加载路径: System.getProperty(“sun.boot.class.path”) 或直接通过 -cp, -classpath 指定

特性: 继承 URLClassLoader

手动获取加载路径: ((URLClassLoader)App.class.getClassLoader()).getURLs()
通过 ClassLoader.getSystemClassLoader() 就可以获取 AppClassLoader, 自己写的程序中写的 ClassLoader(继承 URLClassLoader), 若不指定 parent, 默认的parent就是 AppClassLoader

同一个class

在JVM中,如何确定一个类型实例:

同一个Class = 相同的 ClassName + PackageName + ClassLoader

在JVM中,类型被定义在一个叫SystemDictionary 的数据结构中,该数据结构接受类加载器和全类名作为参数,返回类型实例。

SystemDictionary 如图所示:

SystemDictionary

加载机制

  1. 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
  2. 双亲委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
  3. 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

双亲委托模型

双亲委托的工作过程:如果一个类加载器收到了一个类加载请求,它首先不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成加载请求(它管理的范围之中没有这个类)时,子加载器才会尝试着自己去加载

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。

  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。

  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;

  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

1
javac –verbose查看运行类是加载了jar文件 
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
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{

// 首先检查,jvm中是否已经加载了对应名称的类,findLoadedClass(String )方法实际上是findLoadedClass0方法的wrapped方法,做了检查类名的工
//作,而findLoadedClass0则是一个native方法,通过底层来查看jvm中的对象。
Class c = findLoadedClass(name);
if (c == null) {//类还未加载
try {
if (parent != null) {
//在类还未加载的情况下,我们首先应该将加载工作交由父classloader来处理。
c = parent.loadClass(name, false);
} else {
//返回一个由bootstrap class loader加载的类,如果不存在就返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found

// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);//这里是我们的入手点,也就是指定我们自己的类加载实现
}
}
if (resolve) {
resolveClass(c);//用来做类链接操作
}
return c;
}

从上面的方法也看出我们在实现自己的加载器的时候,不要覆盖locaClass方法,而是重写findClass(),这样能保证双亲委派模型,同时也实现了自己的方法

为什么要使用双亲委托这种模型呢?

  1. 节约系统资源: 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次
  2. 保证Java核心库的类型安全: 我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

自定义加载器

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader

定义自已的类加载器分为两步:

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

示例

很简单的两个类,方法中打印出各自的类加载器

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

public void loader(){
System.out.println("LoaderClass:"+this.getClass().getClassLoader());
LoaderClass1 class1 = new LoaderClass1();
class1.loader();
}
}

public class LoaderClass1 {

public void loader() {
System.out.println(this.getClass().getName() + " loader:"+this.getClass().getClassLoader());

}
}

自定义加载器

  1. 重写findClass方法,从class文件加载
  2. 通过defineClass从bytes构建class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyClassLoader extends ClassLoader {

protected Class<?> findClass(String name) throws ClassNotFoundException {

String root = "d:/";

byte[] bytes = null;
try {
//路径改到根目录下
String file = root + name.substring(name.lastIndexOf(".")+1) + ".class";
InputStream ins = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
bytes = baos.toByteArray();

ins.close();
} catch (Exception e) {
e.printStackTrace();
}
return defineClass(name, bytes, 0, bytes.length);
}
}

测试类

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
public class ClassLoaderTest {

public static void main(String[]args) throws Exception {
ClassLoaderTest test = new ClassLoaderTest();

System.out.println(test.getClass().getClassLoader());//输出sun.misc.Launcher$AppClassLoader

System.out.println(test.getClass().getClassLoader().getParent());//输出sun.misc.Launcher$ExtClassLoader

System.out.println(test.getClass().getClassLoader().getParent().getParent());//输出null

//=====测试重复加载,类路径中LoaderClass.class存在=================
//======虽然指定了classloader,但依然输出的是LoaderClass:sun.misc.Launcher$AppClassLoader
//==删除类路径下的LoaderClass.class,才会输出LoaderClass:com.jack.classloader.MyClassLoader
//并且loaderclass中创建的对象类加载器也是MyClassLoader
MyClassLoader classLoader = new MyClassLoader();
Class<?> loadClass = Class.forName("com.jack.classloader.LoaderClass", true, classLoader);
Method startMethod = loadClass.getMethod("loader");
startMethod.invoke(loadClass.newInstance());

//===当类加载器不一样时,两个class不相等
MyClassLoader classLoader1 = new MyClassLoader();
Class<?> loadClass1 = Class.forName("com.jack.classloader.LoaderClass", true, classLoader1);
System.out.println(loadClass.equals(loadClass1));//输出false
}
}

参考资料

class加载时机及两种显示加载的区别

JVM类加载机制—类加载的过程

<init>和<clinit>

类加载原理分析&动态加载Jar/Dex

java类的主动使用/被动使用

前言

之前《TreadLocal解析》说过Threadlocal的结构:

ThreadLocal结构

但netty却重新搞了一个fastthreadlocal,从各方面对比一下两者的区别。也不得不说一下netty真不愧是款优秀框架,里面中有很多优秀类和方法值得细品

VS ThreadLocal

1、性能

第一点,从性能开始,为什么要重造轮子,可能就是之前的轮子达不到性能要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class FastThreadLocalTest {


public static void main(String[] args) {
testFast(100);
testSlow(100);
}

private static void testFast(int threadLocalCount) {
final FastThreadLocal<String>[] caches = new FastThreadLocal[threadLocalCount];
final Thread mainThread = Thread.currentThread();
for (int i = 0; i < threadLocalCount; i++) {
caches[i] = new FastThreadLocal();
}
Thread t = new FastThreadLocalThread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < threadLocalCount; i++) {
caches[i].set("float.lu");
}
long start = System.nanoTime();
for (int i = 0; i < threadLocalCount; i++) {
for (int j = 0; j < 1000000; j++) {
caches[i].get();
}
}
long end = System.nanoTime();
System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
"]ms");
LockSupport.unpark(mainThread);
}

});
t.start();
LockSupport.park(mainThread);
}

private static void testSlow(int threadLocalCount) {
final ThreadLocal<String>[] caches = new ThreadLocal[threadLocalCount];
final Thread mainThread = Thread.currentThread();
for (int i=0;i<threadLocalCount;i++) {
caches[i] = new ThreadLocal();
}
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<threadLocalCount;i++) {
caches[i].set("float.lu");
}
long start = System.nanoTime();
for (int i=0;i<threadLocalCount;i++) {
for (int j=0;j<1000000;j++) {
caches[i].get();
}
}
long end = System.nanoTime();
System.out.println("take[" + TimeUnit.NANOSECONDS.toMillis(end - start) +
"]ms");
LockSupport.unpark(mainThread);
}

});
t.start();
LockSupport.park(mainThread);
}
}

//输出
fast[15]ms
slow[302]ms

从输出可见性能提升很大

2、数据结构

两者的数据结构大体相似,都是thread带上map属性,threadlocal实例为key;但在细节算法处理时,不一样

get()

整体思路:通过thread取到map,再从map中取value

ThreadLocal.get()

1
2
3
4
5
6
7
8
9
10
11
12
13
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

从map中取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

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

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

如果key值相等,直接返回value

如果key不相等,使用循环线性探测,一直找到最后一个元素

FastThreadLocal.get()

1
2
3
4
5
6
7
8
9
10
11
12
13
public final V get(InternalThreadLocalMap threadLocalMap) {
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}

return initialize(threadLocalMap);
}

public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}

这个明显就快些,有index,直接数组拿值,不需要再去处理循环

set()

主要在于向map中放值

ThreadLocal.set()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

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

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

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

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
  1. 通过取模,得到index
  2. key相等,直接赋值value
  3. key不相等,那就线性探测存放

FastThreadLocal.set()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
if (setKnownNotUnset(threadLocalMap, value)) {
registerCleaner(threadLocalMap);
}
} else {
remove();
}
}

public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
expandIndexedVariableTableAndSet(index, value);
return true;
}
}

这类似就是放入到数组中

总结

到此可以看出二者的区别

区别 ThreadLocal FastThreadLocal
map ThreadLocalMap InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap
线程 Thread FastThreadLocalThread extends Thread

主要还是在内部map的处理逻辑上,两者都没有使用hashmap,但是自定义了map结构与行为,在《hashmap源码解析》中指出map结构的两种处理方式:拉链法线性探测法;在hasmap中使用的是拉链法,而threadlocal中使用的是线性探测法

线性探查(Linear Probing)方式虽然简单,但是有一些问题,它会导致同类哈希的聚集。在存入的时候存在冲突,在查找的时候冲突依然存在

冲突也就造成了性能损耗,而FastTreadLocal就更简单,直接使用数组

1
2
3
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}

UnpaddedInternalThreadLocalMap

1
2
3
4
5
6
7
8
9
10
11
Object[] indexedVariables;


public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}

整个map就是一个数组结构,在每个thread中,每一个FastThreadLocal在创建时就指定了index,value就是数组元素