-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
431 lines (431 loc) · 472 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[Redisson实现分布式可重入锁]]></title>
<url>%2F2019%2F07%2F25%2FRedisson%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E5%8F%AF%E9%87%8D%E5%85%A5%E9%94%81%2F</url>
<content type="text"><![CDATA[希望我是一个让你心动的人,而不是权衡取舍分析利弊后,觉得不错的人。 概述随着互联网技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一些线程安全的类等就可以做到。而在多机部署环境中,不同机器不同进程,就需要在多进程下保证线程的安全性了。因此,分布式锁应运而生。 在某些场景中,多个进程必须以互斥的方式独占共享资源,这时用分布式锁是最直接有效的。 实现分布式锁的三种选择 基于数据库实现分布式锁基于zookeeper实现分布式锁基于Redis缓存实现分布式锁 以上三种方式都可以实现分布式锁,其中,从健壮性考虑, 用 zookeeper 会比用 Redis 实现更好,但从性能角度考虑,基于 Redis 实现性能会更好,如何选择,还是取决于业务需求。 下面只介绍Rediss锁。 分布式锁需满足四个条件首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件: 互斥性。在任意时刻,只有一个客户端能持有锁。 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。 基于 Redis 实现分布式锁的两种方案 使用Redis实现分布式锁 使用 Redisson 实现分布式锁 使用Redis实现分布式锁通过 set key value px milliseconds nx 命令实现加锁, 通过Lua脚本实现解锁。核心实现命令如下: 123456789//获取锁(unique_value可以是UUID等)SET resource_name unique_value NX PX 30000//释放锁(lua脚本中,一定要比较value,防止误解锁)if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end 这种实现方式主要有以下几个要点: set 命令要用 set key value px milliseconds nx,替代 setnx + expire需要分两次执行命令的方式,保证了原子性value要具有唯一性,可以使用UUID.randomUUID().toString()方法生成,用来标识这把锁是属于哪个请求加的,在解锁的时候就可以有依据;释放锁时要验证 value 值,防止误解锁;通过 Lua 脚本来避免 Check And Set 模型的并发问题,因为在释放锁的时候因为涉及到多个Redis操作(利用了eval命令执行Lua脚本的原子性); 完整代码实现如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; private static final Long RELEASE_SUCCESS = 1L; /** * 获取分布式锁(加锁代码) * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean getDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } /** * 释放分布式锁(解锁代码) * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @return 是否释放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), C ollections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; }} 加锁:首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,用来标识这把锁是属于哪个请求加的,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。 解锁:将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。在执行的时候,首先会获取锁对应的value值,检查是否与requestId相等,如果相等则解锁(删除key)。 风险以上实现在 Redis 正常运行情况下是没问题的,但如果存储锁对应key的那个节点挂了的话,就可能存在丢失锁的风险,导致出现多个客户端持有锁的情况,这样就不能实现资源的独享了。 客户端A从master获取到锁在master将锁同步到slave之前,master宕掉了(Redis的主从同步通常是异步的)。主从切换,slave节点被晋级为master节点客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。导致存在同一时刻存不止一个线程获取到锁的情况。 所以在这种实现之下,不论Redis的部署架构是单机模式、主从模式、哨兵模式还是集群模式,都存在这种风险。因为Redis的主从同步是异步的。 Redisson分布式可重入锁主要思路Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。 设计初衷公司每个模块服务都有自己一套分布式锁的实现,为了实现各个模块的统一,减少跨模块开发的便捷性,便着手开发一套简单,易用,容易的分布式锁系统。这个任务就交给了我。调研阶段发现原生Redis写起来并不是那么容易,而且自己能力不一定能够写出能够适应生产环境的分布式可重入Redis锁。所以就使用分装好的Redisson来实现。 要求 简单易用,复杂度低,可重入 不侵占业务代码,业务代码无感知 能够实现多个key的同时加锁 为了显现上述要求,可用如下方式: 使用Redisson的tryLock方法获取锁,使用unlock解锁 通过注解的方式实现加锁的目的,利用Spring的AOP技术可以对业务无感知的实现加锁的功能,同时实现通用的工具类 使用Redisson联锁的功能 以下Redisson的单点模式示例:1234567891011121314151617181920212223/ 1.构造redisson实现分布式锁必要的ConfigConfig config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0);// 2.构造RedissonClientRedissonClient redissonClient = Redisson.create(config);// 3.获取锁对象实例(无法保证是按线程的顺序获取到)RLock rLock = redissonClient.getLock(lockKey);try { /** * 4.尝试获取锁 * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败 * leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完) */ boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS); if (res) { //成功获得锁,在这里处理业务 }} catch (Exception e) { throw new RuntimeException("aquire lock fail");}finally{ //无论如何, 最后都要解锁 rLock.unlock();} Redisson分布式重入锁实现分布式锁代码结构如下图所示: LockAspectAdvice类:Aop实现类,在方法前后加锁解锁 RedissonLockImpl类:redisson实现分布式锁实现类 tryLock(String key, Long waitTimeMillis, Long expirationMillis, TimeUnit unit):获取锁方法 key:锁key waitTimeMillis:获取锁等待时间,单位毫秒 expirationMillis:锁key的过期时间,单位毫秒 unit:时间单位 unlock(String key):解锁方法 key:锁key tryMultiLock(List keys, Long waitTime, Long expireTime, TimeUnit timeUnit) keys: 锁key集合 waitTime:等待时间 expireTime:过期时间 timeUnit:时间单位 LockKey注解:参数注解,注解的参数将作为锁key CustomReentrantLock注解:自定义方法注解,对方法加锁 long waitTimeMillis() default 1000: 等待获取锁时间,等待时间内一直尝试获取锁,单位毫秒ms,默认1秒 long expireMillis() default 15000:锁的过期时间,单位毫秒ms,默认15秒 Spring集群模式使用说明环境配置Spring与Redisson简单整合配置文件: 1234567891011121314151617181920<context:property-placeholder location="classpath:spring-config.properties" ignore-unresolvable="true" /> <!--单台redis机器配置--> <!--<redisson:client id="redissonClient">--> <!--<redisson:single-server address="192.168.6.21:6382" connection-pool-size="30" />--> <!--</redisson:client>--> <!-- redis集群配置 --> <redisson:client id="redissonClient"> <!--//scan-interval:集群状态扫描间隔时间,单位是毫秒 --> <redisson:cluster-servers scan-interval="10000" > <redisson:node-address value="redis://${redis.cluster1.hostname}:${redis.cluster1.port}"/> <redisson:node-address value="redis://${redis.cluster2.hostname}:${redis.cluster2.port}"/> <redisson:node-address value="redis://${redis.cluster3.hostname}:${redis.cluster3.port}"/> <redisson:node-address value="redis://${redis.cluster4.hostname}:${redis.cluster4.port}"/> <redisson:node-address value="redis://${redis.cluster5.hostname}:${redis.cluster5.port}"/> <redisson:node-address value="redis://${redis.cluster6.hostname}:${redis.cluster6.port}"/> </redisson:cluster-servers> </redisson:client> 这里没有设置复杂的参数,因为默认的方式已经足够实现平常业务需求,如果你自己需要更加优异的分布式锁,可以具体设置Redisson具体参数 具体的参数设置请参考:Redisson和Spring框架整合 比如如下示例,可以设置其中部分参数: 123456789101112131415161718<redisson:client id="cluster" codec-ref="stringCodec"> <redisson:cluster-servers slaveConnectionPoolSize="500" masterConnectionPoolSize="500" idle-connection-timeout="10000" connect-timeout="10000" timeout="3000" ping-timeout="1000" reconnection-timeout="3000" database="0"> <redisson:node-address value="redis://127.0.0.1:6379" /> <redisson:node-address value="redis://127.0.0.1:6380" /> <redisson:node-address value="redis://127.0.0.1:6381" /> <redisson:node-address value="redis://127.0.0.1:6382" /> <redisson:node-address value="redis://127.0.0.1:6383" /> <redisson:node-address value="redis://127.0.0.1:6384" /> </redisson:cluster-servers></redisson:client> 其中具体的参数含义请看:Redisson的基本配置 使用AOP对方法加锁AOP实现加锁配置文件,注入需要的类: 123456789101112131415161718<import resource="spring-redisson.xml"/> <!--注入redisson类对象属性值--> <bean id="redissonLockUtils" class="com.foutin.redisson.lock.cluster.impl.RedissonLockImpl"> <constructor-arg name="redissonClient" ref="redissonClient"/> </bean> <bean id="lockAspectAdvice" class="com.foutin.redisson.lock.cluster.annotation.LockAspectAdvice"> <constructor-arg name="distributedLock" ref="redissonLockUtils"/> </bean> <!--redisson锁--> <aop:config> <aop:pointcut id="pointcut" expression="@annotation(com.foutin.redisson.lock.cluster.annotation.CustomReentrantLock)"/> <aop:aspect ref="lockAspectAdvice"> <aop:around method="around" pointcut-ref="pointcut" /> </aop:aspect> </aop:config> 然后把spring-config-aop.xml导入spring-config.xml配置文件中即可。 使用示例方式一:使用注解通过方法注解CustomReentrantLock和参数注解LockKey一起使用。注解使用示例: 123456789101112131415@Servicepublic class DemoLockService { @CustomReentrantLock(expireMillis = 10000) public void demoDiffLock(String name, @LockKey Long id) { System.out.println("fanxingkai-demoLockService:" + id); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }} 方式二:使用加单个锁方法1234567891011121314151617@Autowiredprivate RedissonLockImpl redissonLock; public void demoUtils(String name) { Boolean locked = redissonLock.tryLock(name, 2L, 10000L, TimeUnit.MILLISECONDS); if (locked) { try { System.out.println("fanxingkai--llll:" + name); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { redissonLock.unlock(name); } }} 方式三:使用联锁12345678910public void demoMultiLock(List<String> name) { RedissonMultiLock multiLock = redissonLock.tryMultiLock(name, 2000L, 120000L); try { System.out.println("fanxingkai--llll:" + name); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } multiLock.unlock(); } 联锁使用场景:需要对多个字符串同时加锁。 注意项 redisson需要依赖netty包,不然会报错 使用注解的方式,注解的方法必须有参数,无参数无法使用LockKey注解,无法获取锁key。 源码地址Redisson实现分布式可重入锁 总结这次的实践让我对Redis分布式锁有了一定的了解。万变不离其宗,加锁:通过向Redis保存有指定时间的key-value值,每次在执行代码前都会查询指定key是否在Redis中存在,是否过期,不存在或者已经过期的值,可以重新保存,保存成功便相当于获取了Redis锁。解锁: 通过lua来实现解锁的整个过程,保证原子性操作,在解锁的时候必须判断加锁和解锁是否是同一把锁,也就是在Redis中是否是同一个key。这样便实现了Redis的锁的基本功能。如果要实现可重入的功能,每次获取相同的锁之前必须判断是否属于同一个线程的操作。同一个线程可以获取同一把锁,不为同一个线程设置其他的锁。每个线程可以通过记录线程和UUID值来组成一个唯一的值,并记录下来。同一把锁在重入的时候需要刷新这把锁的过期时间。以上便是我对Redis锁的理解。下次我将尝试zookeeper分布式锁的实现。]]></content>
<categories>
<category>Redis</category>
<category>Redisson</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpringIOC和AOP自实现]]></title>
<url>%2F2019%2F06%2F10%2FSpringIOC%E5%92%8CAOP%E8%87%AA%E5%AE%9E%E7%8E%B0%2F</url>
<content type="text"><![CDATA[长到这么大,我说不出来我最爱的一部电影,说不出来我最爱的一首歌,说不出来我最爱的一个人。时常觉得人生其实没那么有趣,偶尔也会质疑活着的意义,所有来自于书上和别人口中的意义都不曾说服过我。但今天突然觉得,大概人生最大的意义就是用余生去找到那些最爱吧。 Spring AOP常见概念AOP 是 Spring 独有的概念吗?不是,除了Spring AOP,AOP 常见的实现还有: Aspectj Guice AOP Jboss AOP AOP Alliance 是什么, 为什么 Spring AOP 需要 aopalliance.jar ? AOP Alliance 是AOP的接口标准,定义了 AOP 中的基础概念(Advice、CutPoint、Advisor等),目标是为各种AOP实现提供统一的接口,本身并不是一种 AOP 的实现。 Spring AOP, GUICE等都采用了AOP Alliance中定义的接口,因而这些lib都需要依赖 aopalliance.jar。 Spring 4.3之后内置了AOP Alliance接口,不再需要单独的aopalliance.jar。 Spring AOP 和 Aspectj 的区别? Spring AOP采用动态代理的方式,在运行期生成代理类来实现AOP,不修改原类的实现;Aspectj 使用编译期字节码织入(weave)的方式,在编译的时候,直接修改类的字节码,把所定义的切面代码逻辑插入到目标类中。 Spring AOP可以对其它模块正常编译出的代码起作用,Aspectj 需要对其它模块使用acj重新编译 由于动态代理机制,Spring AOP对于直接调用类内部的其它方法无效,无法对定义为final的类生效。Aspectj没有这些限制 Spring AOP使用XML配置文件的方式定义切入点(CutPoint),Aspectj使用注解方式 注: Aspectj 除了编译期静态织入的方式之外,也支持加载时动态织入修改类的字节码。 Spring AOP 如何生成代理类?Spring AOP使用JDK Proxy或者cglib实现代理类生成。对于有实现接口的类使用JDK Proxy,对于无接口的则是用cglib.通过 1<aop:aspectj-autoproxy proxy-target-class="true"/> 指定proxy-target-class为true可强制使用cglib. JDK Proxy 和 cglib 代理类生成什么区别? JDK Proxy只适用于类实现了接口的情况.生成的代理类实现了原类的接口,但和原类没有继承关系. cglib则是生成原来的子类,对于没有实现接口的情况也适用,cglib采用字节码生成的方式来在代理类中调用原类方法, JDK Proxy 则是使用反射调用,由于反射存在额外security check 的开销一集目前jvm jit对反射的内联支持不够好,JDK Proxy在性能上弱于cglib Spring-aspects 又是什么鬼因为Spring AOP XML配置文件定义的方式太繁琐遭到吐槽,所以spring从Aspectj中吸收了其定义AOP的方式,包括Aspectj Annotation和Aspectj-XML配置。然而其实现依然是动态代理的方式,与aspectj 字节码织入的方式不同。 为什么spring-aspects 还需要 aspectjweaver.jar才能工作Spring-aspects 实现XML配置解析和类似 Aspectj 注解方式的时候,借用了 aspectjweaver.jar 中定义的一些annotation 和 class,然而其并不使用 Aspectj 的字节码织入功能。 Spring-aspects不能把这些所需的类定义抄一份吗,这样就不需要aspectjweaver.jar了他们可以,但是他们偏不这样做。 Spring 3.1 之前 spring-aspects 对 aspectjweaver 的依赖还是 optional 的,需要自己再添加依赖;Sprint 3.2 之后 依赖取消了 optional 设置,可以不用自己添加了。]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[SpringIOC-Bean的循环依赖问题]]></title>
<url>%2F2019%2F05%2F31%2FSpringIOC-Bean%E7%9A%84%E5%BE%AA%E7%8E%AF%E4%BE%9D%E8%B5%96%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[我向来以为自己是个随和的人,只是性情有点孤僻,常闷闷不乐,甚至怀疑自己有忧郁症,并且觉得自己从出世就是个错,一言一行,时候回想总觉得不当。我什么都错。为什么要有我这个人呢? 概述在实际工作中,经常由于设计不佳或者各种因素,导致类之间相互依赖。这些类可能单独使用时不会出问题,但是在使用Spring进行管理的时候可能就会抛出BeanCurrentlyInCreationException等异常 。当抛出这种异常时表示Spring解决不了该循环依赖,本文将简要说明Spring对于循环依赖的解决方法和原理。 循环依赖产生循环依赖的产生可能有很多种情况,例如: A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象A的构造方法中依赖了B的实例对象,同时B的某个field或者setter需要A的实例对象,以及反之A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象,以及反之 当然,Spring对于循环依赖的解决不是无条件的,首先前提条件是针对scope单例并且没有显式指明不需要解决循环依赖的对象,而且要求该对象没有被代理过。同时Spring解决循环依赖,以上三种情况只能解决两种,第一种在构造方法中相互依赖的情况Spring是无法解决的。下面来看看Spring的解决方法,知道了解决方案就能明白为啥第一种情况无法解决了。 Spring的单例对象的初始化Spring的单例对象的初始化主要分为三步: createBeanInstance, 实例化,实际上就是调用对应的构造方法构造对象,此时只是调用了构造方法,spring xml中指定的property并没有进行填充populateBean,填充属性,这步对spring xml中指定的property进行填充initializeBean,调用spring xml中指定的init方法,或者AfterPropertiesSet方法 会发生循环依赖的步骤集中在第一步和第二步。 Spring Bean的实例化流程先看其他博主的bean实例化工程图: 这张图是一个简化后的流程图。开始流程图中只有一条执行路径,在条件 sharedInstance != null 这里出现了岔路,形成了绿色和红色两条路径。在上图中,读取/添加缓存的方法我用蓝色的框和☆标注了出来。至于虚线的箭头,和虚线框里的路径,这个下面会说到。 这个流程从 getBean 方法开始,getBean 是个空壳方法,所有逻辑都在 doGetBean 方法中。doGetBean 首先会调用 getSingleton(beanName) 方法获取 sharedInstance,sharedInstance 可能是完全实例化好的 bean,也可能是一个原始的 bean,当然也有可能是 null。如果不为 null,则走绿色的那条路径。再经 getObjectForBeanInstance 这一步处理后,绿色的这条执行路径就结束了。 我们再来看一下红色的那条执行路径,也就是 sharedInstance = null 的情况。在第一次获取某个 bean 的时候,缓存中是没有记录的,所以这个时候要走创建逻辑。上图中的 getSingleton(beanName,new ObjectFactory() {…}) 方法会创建一个 bean 实例,上图虚线路径指的是 getSingleton 方法内部调用的两个方法,其逻辑如下: 123456public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { // 省略部分代码 singletonObject = singletonFactory.getObject(); // ... addSingleton(beanName, singletonObject);} 如上所示,getSingleton 会在内部先调用 getObject 方法创建 singletonObject,然后再调用 addSingleton 将 singletonObject 放入缓存中。getObject 在内部代用了 createBean 方法,createBean 方法基本上也属于空壳方法,更多的逻辑是写在 doCreateBean 方法中的。doCreateBean 方法中的逻辑很多,其首先调用了 createBeanInstance 方法创建了一个原始的 bean 对象,随后调用 addSingletonFactory 方法向缓存中添加单例 bean 工厂,从该工厂可以获取原始对象的引用,也就是所谓的“早期引用”。再之后,继续调用 populateBean 方法向原始 bean 对象中填充属性,并解析依赖。getObject 执行完成后,会返回完全实例化好的 bean。紧接着再调用 addSingleton 把完全实例化好的 bean 对象放入缓存中。 Spring循环依赖的解决方式Spring循环依赖的理论依据其实是Java基于引用传递,当我们获取到对象的引用时,对象的field或者或属性是可以延后设置的。 那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。 首先我们看源码,三级缓存主要指: 12345678/** Cache of singleton objects: bean name --> bean instance */private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);/** Cache of singleton factories: bean name --> ObjectFactory */private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);/** Cache of early singleton objects: bean name --> bean instance */private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16); 这三级缓存分别指: singletonFactories : 单例对象工厂的cacheearlySingletonObjects:提前暴光的单例对象的CachesingletonObjects:单例对象的cache 以上三个cache构成了三级缓存,Spring就用这三级缓存巧妙的解决了循环依赖问题。 对于单例对象来说,在Spring的整个容器的生命周期内,有且只存在一个对象,很容易想到这个对象应该存在Cache中,Spring会尝试从缓存中获取,这个缓存就是指singletonObjects,主要调用的方法是: 123456789101112131415161718192021222324252627282930313233343536373839protected <T> T doGetBean( final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException { // ...... // 从缓存中获取 bean 实例 Object sharedInstance = getSingleton(beanName); // ......}public Object getSingleton(String beanName) { return getSingleton(beanName, true);}protected Object getSingleton(String beanName, boolean allowEarlyReference) { // 从 singletonObjects 获取实例,singletonObjects 中的实例都是准备好的 bean 实例,可以直接使用 Object singletonObject = this.singletonObjects.get(beanName); // 判断 beanName 对应的 bean 是否正在创建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // 从 earlySingletonObjects 中获取提前曝光的 bean singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null && allowEarlyReference) { // 获取相应的 bean 工厂 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 提前曝光 bean 实例(raw bean),用于解决循环依赖 singletonObject = singletonFactory.getObject(); // 将 singletonObject 放入缓存中,并将 singletonFactory 从缓存中移除 this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null);} 上面的源码中,doGetBean 所调用的方法 getSingleton(String) 是一个空壳方法,其主要逻辑在 getSingleton(String, boolean) 中。 getSingleton方法首先解释两个参数: isSingletonCurrentlyInCreation 判断对应的单例对象是否在创建中,当单例对象没有被初始化完全(例如A定义的构造函数依赖了B对象,得先去创建B对象,或者在populatebean过程中依赖了B对象,得先去创建B对象,此时A处于创建中)allowEarlyReference 是否允许从singletonFactories中通过getObject拿到对象 分析getSingleton()的整个过程,Spring首先从一级缓存singletonObjects中获取。如果获取不到,并且对象正在创建中,就再从二级缓存earlySingletonObjects中获取。如果还是获取不到且允许singletonFactories通过getObject()获取,就从三级缓存singletonFactory.getObject()(三级缓存)获取,如果获取到了则: 12this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName); 从singletonFactories中移除,并放入earlySingletonObjects中。其实也就是从三级缓存移动到了二级缓存。 从上面三级缓存的分析,我们可以知道,Spring解决循环依赖的诀窍就在于singletonFactories这个三级cache。这个cache的类型是ObjectFactory,定义如下: 123public interface ObjectFactory<T> { T getObject() throws BeansException;} 在bean创建过程中,有两处比较重要的匿名内部类实现了该接口。一处是: 12345678910new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } } 在上文已经提到,Spring利用其创建bean。 另一处就是: 12345addSingletonFactory(beanName, new ObjectFactory<Object>() { @Override public Object getObject() throws BeansException { return getEarlyBeanReference(beanName, mbd, bean); }}); 此处就是解决循环依赖的关键,这段代码发生在createBeanInstance之后,也就是说单例对象此时已经被创建出来的。这个对象已经被生产出来了,虽然还不完美(还没有进行初始化的第二步和第三步),但是已经能被人认出来了(根据对象引用能定位到堆中的对象),所以Spring此时将这个对象提前曝光出来让大家认识,让大家使用。 这样做有什么好处呢?让我们来分析一下“A的某个field或者setter依赖了B的实例对象,同时B的某个field或者setter依赖了A的实例对象”这种循环依赖的情况。A首先完成了初始化的第一步,并且将自己提前曝光到singletonFactories中,此时进行初始化的第二步,发现自己依赖对象B,此时就尝试去get(B),发现B还没有被create,所以走create流程,B在初始化第一步的时候发现自己依赖了对象A,于是尝试get(A),尝试一级缓存singletonObjects(肯定没有,因为A还没初始化完全),尝试二级缓存earlySingletonObjects(也没有),尝试三级缓存singletonFactories,由于A通过ObjectFactory将自己提前曝光了,所以B能够通过ObjectFactory.getObject拿到A对象(虽然A还没有初始化完全,但是总比没有好呀),B拿到A对象后顺利完成了初始化阶段1、2、3,完全初始化之后将自己放入到一级缓存singletonObjects中。此时返回A中,A此时能拿到B的对象顺利完成自己的初始化阶段2、3,最终A也完成了初始化,长大成人,进去了一级缓存singletonObjects中,而且更加幸运的是,由于B拿到了A的对象引用,所以B现在hold住的A对象也蜕变完美了!一切都是这么神奇!!知道了这个原理时候,肯定就知道为啥Spring不能解决“A的构造方法中依赖了B的实例对象,同时B的构造方法中依赖了A的实例对象”这类问题了!因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决。 总结Spring通过三级缓存加上“提前曝光”机制,配合Java的对象引用原理,比较完美地解决了某些情况下的循环依赖问题!上面是对Spring解决Bean循环依赖解决方案大致讲解,并没有很深入讲解代码的逻辑。博主能力有限,现在只能够理解到这个层次。 参考:Spring IOC 容器源码分析 - 循环依赖的解决办法]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[ThreadLocal]]></title>
<url>%2F2019%2F01%2F20%2FThreadLocal%2F</url>
<content type="text"><![CDATA[成熟成熟 概述ThreadLocal也是一种解决多线程并发无锁的方法。这里简单分析一下。以下内容仅仅是个人的记录。 ThreadLocal为什么用ThreadLocalThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同,而同一个线程在任何时候访问这个本地变量的结果都是一致的。当此线程结束生命周期时,所有的线程本地实例都会被 GC。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。ThreadLocal 通常定义为 private static 类型。 假如让我们来实现一个变量与线程相绑定的功能,我们可以很容易地想到用HashMap来实现,Thread作为key,变量作为value。事实上,JDK 中确实使用了类似 Map 的结构存储变量,但不是像我们想的那样。下面我们来探究OpenJDK 1.8中ThreadLocal的实现。 从线程Thread的角度来看,每个线程内部都会持有一个对ThreadLocalMap实例的引用,ThreadLocalMap实例相当于线程的局部变量空间,存储着线程各自的数据,具体如下图: ThreadLocal源码ThreadLocalThreadLocal 通过 threadLocalHashCode 来标识每一个 ThreadLocal 的唯一性。threadLocalHashCode 通过 CAS 操作进行更新,每次 hash 操作的增量为 0x61c88647(为什么用这个数,可以自行查)。 123456789101112131415161718192021222324252627282930313233public class ThreadLocal<T> { /** * ThreadLocals rely on per-thread linear-probe hash maps attached * to each thread (Thread.threadLocals and * inheritableThreadLocals). The ThreadLocal objects act as keys, * searched via threadLocalHashCode. This is a custom hash code * (useful only within ThreadLocalMaps) that eliminates collisions * in the common case where consecutively constructed ThreadLocals * are used by the same threads, while remaining well-behaved in * less common cases. */ private final int threadLocalHashCode = nextHashCode(); /** * The next hash code to be given out. Updated atomically. Starts at * zero. */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** * The difference between successively generated hash codes - turns * implicit sequential thread-local IDs into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } set 方法: 12345678public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 可以看到通过Thread.currentThread()方法获取了当前的线程引用,并传给了getMap(Thread)方法获取一个ThreadLocalMap的实例。我们继续跟进getMap(Thread)方法: 12345678910/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } 可以看到getMap(Thread)方法直接返回Thread实例的成员变量threadLocals。它的定义在Thread内部,访问级别为package级别: 123/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; 到了这里,我们可以看出,每个Thread里面都有一个ThreadLocal.ThreadLocalMap成员变量,也就是说每个线程通过ThreadLocal.ThreadLocalMap与ThreadLocal相绑定,这样可以确保每个线程访问到的thread-local variable都是本线程的。 我们往下继续分析。获取了ThreadLocalMap实例以后,如果它不为空则调用ThreadLocalMap.ThreadLocalMap#set方法设值;若为空则调用ThreadLocal#createMap方法new一个ThreadLocalMap实例并赋给Thread.threadLocals。 ThreadLocal#createMap方法的源码如下: 12345678910/** * Create the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the map */ void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ThreadLocalMapThreadLocalMap 是 ThreadLocal 的静态内部类,它的结构如下: 12345678910111213141516171819202122232425262728293031323334353637383940static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entries in the table. */ private int size = 0; /** * The next size value at which to resize. */ private int threshold; // Default to 0 可以看到ThreadLocalMap有一个常量和三个成员变量: 12345678910111213141516171819202122/** * The initial capacity -- MUST be a power of two. * Map的初始容量 */private static final int INITIAL_CAPACITY = 16;/** * The table, resized as necessary. * table.length MUST always be a power of two. * Entry类型的数组 */private Entry[] table;/** * The number of entries in the table. */private int size = 0;/** * The next size value at which to resize. */private int threshold; // Default to 0 其中 INITIAL_CAPACITY代表这个Map的初始容量;table 是一个Entry 类型的数组,用于存储数据;size 代表表中的存储数目; threshold 代表需要扩容时对应 size 的阈值。 Entry 类是 ThreadLocalMap 的静态内部类,用于存储数据。它的源码如下: Entry类继承了WeakReference<ThreadLocal<?>>,即每个Entry对象都有一个ThreadLocal的弱引用(作为key),这是为了防止内存泄露。一旦线程结束,key变为一个不可达的对象,这个Entry就可以被GC了。123456789static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; }} ThreadLocalMap类有两个构造函数,其中常用的是ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue): 123456789101112/** * Construct a new map initially containing (firstKey, firstValue). * ThreadLocalMaps are constructed lazily, so we only create * one when we have at least one entry to put in it. */ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY);} 构造函数的第一个参数就是本ThreadLocal实例(this),第二个参数就是要保存的线程本地变量。构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的哈希值,然后存储到table中,并设置size和threshold。 注意一个细节,计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现(和HashMap中的思路相同)。正是因为这种算法,我们要求size必须是 2的指数,因为这可以使得hash发生冲突的次数减小。 我们来看ThreadLocalMap#set方法的实现: 1234567891011121314151617181920212223242526272829303132333435363738/** * Set the value associated with key. * * @param key the thread local object * @param value the value to be set */ 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(); } 如果冲突了,就会通过nextIndex方法再次计算哈希值: 123456/** * Increment i modulo len. */ private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } 到这里,我们看到 ThreadLocalMap 解决冲突的方法是 线性探测法(不断加 1),而不是 HashMap 的 链地址法,这一点也能从 Entry 的结构上推断出来。 总结每个 Thread 里都含有一个 ThreadLocalMap 的成员变量,这种机制将 ThreadLocal 和线程巧妙地绑定在了一起,即可以保证无用的ThreadLocal被及时回收,不会造成内存泄露,又可以提升性能。假如我们把 ThreadLocalMap 做成一个Map<t extends Thread, ?> 类型的 Map,那么它存储的东西将会非常多(相当于一张全局线程本地变量表),这样的情况下用线性探测法解决哈希冲突的问题效率会非常差。而 JDK 里的这种利用 ThreadLocal 作为 key,再将ThreadLocalMap与线程相绑定的实现,完美地解决了这个问题。 总结一下什么时候无用的 Entry 会被清理: Thread 结束的时候 插入元素时,发现 staled entry,则会进行替换并清理 插入元素时,ThreadLocalMap 的 size 达到 threshold,并且没有任何 staled entries 的时候,会调用 rehash 方法清理并扩容 调用 ThreadLocalMap 的 remove 方法或set(null) 时 尽管不会造成内存泄露,但是可以看到无用的Entry只会在以上四种情况下才会被清理,这就可能导致一些 Entry 虽然无用但还占内存的情况。因此,我们在使用完 ThreadLocal 后一定要remove一下,保证及时回收掉无用的 Entry。 特别地,当应用线程池的时候,由于线程池的线程一般会复用,Thread 不结束,这时候用完更需要 remove 了。 总的来说,对于多线程资源共享的问题,同步机制采用了 以时间换空间 的方式,而 ThreadLocal 则采用了 以空间换时间 的方式。前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 哈希表-线性探测法和链地址法哈希表1、概念 哈希表(Hash Table)也叫散列表,是根据关键码值(Key Value)而直接进行访问的数据结构。它通过把关键码值映射到哈希表中的一个位置来访问记录,以加快查找的速度。这个映射函数就做散列函数,存放记录的数组叫做散列表。 2、散列存储的基本思路 以数据中每个元素的关键字K为自变量,通过散列函数H(k)计算出函数值,以该函数值作为一块连续存储空间的的单元地址,将该元素存储到函数值对应的单元中。 3、哈希表查找的时间复杂度 哈希表存储的是键值对,其查找的时间复杂度与元素数量多少无关,哈希表在查找元素时是通过计算哈希码值来定位元素的位置从而直接访问元素的,因此,哈希表查找的时间复杂度为O(1) 常用哈希函数1.直接寻址法 取关键字或者关键字的某个线性函数值作为哈希地址,即H(Key)=Key或者H(Key)=a*Key+b(a,b为整数),这种散列函数也叫做自身函数.如果H(Key)的哈希地址上已经有值了,那么就往下一个位置找,知道找到H(Key)的位置没有值了就把元素放进去. 2.数字分析法 分析一组数据,比如一组员工的出生年月,这时我们发现出生年月的前几位数字一般都相同,因此,出现冲突的概率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果利用后面的几位数字来构造散列地址,则冲突的几率则会明显降低.因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址. 3.平方取中法 取关键字平方后的中间几位作为散列地址.一个数的平方值的中间几位和数的每一位都有关。因此,有平方取中法得到的哈希地址同关键字的每一位都有关,是的哈希地址具有较好的分散性。该方法适用于关键字中的每一位取值都不够分散或者较分散的位数小于哈希地址所需要的位数的情况。 4.折叠法 折叠法即将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(注意:叠加和时去除进位)作为散列地址.数位叠加可以有移位叠加和间界叠加两种方法.移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加. 5.随机数法 选择一个随机数,去关键字的随机值作为散列地址,通常用于关键字长度不同的场合. 6.除留余数法 取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址.即H(Key)=Key MOD p,p<=m.不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选得不好,则很容易产生冲突。一般p取值为表的长度tableSize。 哈希冲突的处理方法1、开放定址法——线性探测 线性探测法的地址增量di = 1, 2, … , m-1,其中,i为探测次数。该方法一次探测下一个地址,知道有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。 线性探测容易产生“聚集”现象。当表中的第i、i+1、i+2的位置上已经存储某些关键字,则下一次哈希地址为i、i+1、i+2、i+3的关键字都将企图填入到i+3的位置上,这种多个哈希地址不同的关键字争夺同一个后继哈希地址的现象称为“聚集”。聚集对查找效率有很大影响。 2、开放地址法——二次探测 二次探测法的地址增量序列为 di =1^2, -1^2, 2^2, -2^2,… , q2, -q2(q <= m/2)。二次探测能有效避免“聚集”现象,但是不能够探测到哈希表上所有的存储单元,但是至少能够探测到一半。 3、链地址法(HashMap中用到的方法) 链地址法也成为拉链法。其基本思路是:将所有具有相同哈希地址的而不同关键字的数据元素连接到同一个单链表中。如果选定的哈希表长度为m,则可将哈希表定义为一个有m个头指针组成的指针数组T[0..m-1],凡是哈希地址为i的数据元素,均以节点的形式插入到T[i]为头指针的单链表中。并且新的元素插入到链表的前端,这不仅因为方便,还因为经常发生这样的事实:新近插入的元素最优可能不久又被访问。]]></content>
<categories>
<category>线程</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[来自2019的2018]]></title>
<url>%2F2019%2F01%2F01%2F2018%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[概述18年悄然已过,有点匆忙,有的不知所措。18年一直被时间催着走。 现在我是个很在意时间节点的人,好像非要做点什么才不辜负这个特殊的时间节点,也许是感觉时间过得有点快,也许是想留点值得回忆的事,也许是过去时间里得到的没有想象中的多,也许心中对过去的不舍。不过今年元旦我自己一个人在屋子里躺了三天,这样的时刻里独自度过倒也不错,趁这个机会总结一下过去的一年。 工作今年最大的遗憾就是工作了,可谓是有点惨。技术上没有太多的收获。其中的经历就不多说了,不过对我的教训也是一辈子的,忘不掉了,说实话不抱怨是不可能的,心里总有点不服,不甘,又有点委屈,公司的人可能好久都忘不掉。开玩笑了,主要的原因还是自身的原因: 首先,自身的技术不过硬,没有太多的项目经验,开发起来比较吃力,没有为公司的项目带来开发上的推进。 第二,中期态度上有很大的问题,一开始的时候,简单的工作还好,没有出现太多的comment,心态很可以正常维持,到后面comment实在太多,中间又遇到一个自己无法解决的问题,搞了好久,心态瞬间崩了,态度就变得十分不友好,不够坚定和自信。 第三,交流上一直是我的弊端,我不太擅长和不熟的人交流,我说话比较耿直,工作上的反馈也很少,像个傻子一样坐在那里。 这些教训是一次很大的收获,19年我会时刻提醒自己,学会在工作中生活。希望19年的我技术上达到一个更高的层次。工资更高点。 生活去年上半年因为还没有毕业,那生活过的真舒服。没有太多顾虑,一切都为毕业忙碌着,很累,很快乐。真正工作后,生活就过得很是单调,因为是一个人住,下了班就无所事事,看书实在看不进去。整个人变得十分的无聊,也是从那个时候开始,自己更不喜欢说话。 19年希望我要活得更自在。不工作的时候就出去转转,是时候和同学互动一下了,一个人也好,一群人也罢,都是生活,好好活着。19年想来一次健身,希望自己有一身的腱子肉。 性格自己的性格有时像个柔弱的女生,做事说话不想一个阳刚男生:矫情、腼腆、幼稚、爱抱怨、受不了委屈、说话太温柔、不自信、没用的想的太多… 也许是生活环境原因吧,我妈在我小时候天天唠叨我要是个女生就好了。 19年希望我能够继续改变自己的性格,遇到事情能够更加沉着稳重,更加现实一点,能够多方面的看待问题。自己更加成熟,更自信,更能抗住来自各方面的压力。做个更加健全的少年。 要学的还有很多学习不能够停止,就像吃饭一样。 这个社会知识更新的速度感觉要超过学习的速度。自己不懂的还有很多很多,每一个领域每一个层次都有着自己深奥之处。只有不断的学习才能够跟上世界的脚步。 希望19年自己能够养成看书的习惯,闲暇的时候,能够抱着书认真的看一看。把自己囤的基本技术书看完。 愿希望我在意的人能够健康幸福,我的存在能够给他们带来更多的快乐。愿你一切安好。 什么是生活? 想念某人了,便日行千里来相见,是生活。嘴馋了,穿行一座城找到熟悉的小摊,是生活。甚至疲倦无聊了,闷头睡上一整天,也是生活。 生活既是是风花雪月、悲欢离合,也是人间烟火、饮食男女。 既然活着当然要好好活着。我会好好活,你也要好好的。 一切都会变得好起来。]]></content>
<categories>
<category>总结</category>
</categories>
<tags>
<tag>所思所想</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Dubbo与Zookeeper]]></title>
<url>%2F2018%2F12%2F20%2FDubbo%E4%B8%8EZookeeper%2F</url>
<content type="text"><![CDATA[概述目前进入一家,做仓库存储的公司,公司使用的技术也都是现在使用比较多的。以下只是个人记录,不做任何商业用途。 Dubbo关于Dubbo,网上的资料很多,我也都看了很多,林林丛丛的都是对Dubbo技术的介绍,Dubbo用起来很简单,基本上就是配置配置xml文件。这里就不在介绍Dubbo怎么使用了。 我对Dubbo主要的疑虑主要有以下几点: RPC原理是什么? 为什么Dubbo可以达到调用远程服务的目的? Dubbo为什么能够实现软负载均衡,服务注册与发现的? 不过想要解决这些疑问,需要研究源码。因为对Dubbo涉及到的部分技术还有很多不是特别懂,暂时没有看源码。这部分技术分享暂时不做,等我看完源码再好好分享。 Dubbo官方文档 ZookeeperZookeeper的简介ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode。每个ZNode都可以通过其路径唯一标识,在每个ZNode上可存储少量数据(默认是1M, 可以通过配置修改,通常不建议在ZNode上存储大量的数据)。另外,每个ZNode上还存储了其Acl信息,这里需要注意,虽说ZNode的树形结构跟Unix文件系统很类似,但是其Acl与Unix文件系统是完全不同的,每个ZNode的Acl是独立的,子结点不会继承父结点的。 ZNode根据其本身的特性,可以分为下面两类: Regular ZNode: 常规型ZNode, 用户需要显式的创建、删除 Ephemeral ZNode: 临时型ZNode,用户创建它之后,可以显式的删除,也可以在创建它的Session结束后,由ZooKeeper Server自动删除 Zookeeper这种数据结构有如下这些特点: 1)每个子目录项如NameService都被称作为znode,这个znode是被它所在的路径唯一标识,如Server1这个znode的标识为/NameService/Server1。 2)znode可以有子节点目录,并且每个znode可以存储数据,注意EPHEMERAL(临时的)类型的目录节点不能有子节点目录。ZNode一个Sequential的特性,如果创建的时候指定的话,该ZNode的名字后面会自动Append一个不断增加的SequenceNo。 3)znode是有版本的(version),每个znode中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据,version号自动增加。 4)znode可以是临时节点(EPHEMERAL),可以是持久节点(PERSISTENT)。如果创建的是临时节点,一旦创建这个EPHEMERALznode的客户端与服务器失去联系,这个znode也将自动删除,Zookeeper的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为session,如果znode是临时节点,这个session失效,znode也就删除了。 5)znode的目录名可以自动编号,如App1已经存在,再创建的话,将会自动命名为App2。 6)znode可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是Zookeeper的核心特性,Zookeeper的很多功能都是基于这个特性实现的。Watcher ZooKeeper支持一种Watch操作,Client可以在某个ZNode上设置一个Watcher,来Watch该ZNode上的变化。如果该ZNode上有相应的变化,就会触发这个Watcher,把相应的事件通知给设置Watcher的Client。需要注意的是,ZooKeeper中的Watcher是一次性的,即触发一次就会被取消,如果想继续Watch的话,需要客户端重新设置Watcher。这个跟epoll里的oneshot模式有点类似。 7)ZXID:每次对Zookeeper的状态的改变都会产生一个zxid(ZooKeeper Transaction Id),zxid是全局有序的,如果zxid1小于zxid2,则zxid1在zxid2之前发生。 8)Session: Client与ZooKeeper之间的通信,需要创建一个Session,这个Session会有一个超时时间。因为ZooKeeper集群会把Client的Session信息持久化,所以在Session没超时之前,Client与ZooKeeper Server的连接可以在各个ZooKeeper Server之间透明地移动。在实际的应用中,如果Client与Server之间的通信足够频繁,Session的维护就不需要其它额外的消息了。否则,ZooKeeper Client会每t/3 ms发一次心跳给Server,如果Client 2t/3 ms没收到来自Server的心跳回应,就会换到一个新的ZooKeeper Server上。这里t是用户配置的Session的超时时间。 Zookeeper的结构如下图: client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。 ZooKeeper WatchZookeeper watch是一种监听通知机制。Zookeeper所有的读操作getData(), getChildren()和 exists()都可以设置监视(watch),监视事件可以理解为一次性的触发器,官方定义如下:a watch event is one-time trigger, sent to the client that set the watch, whichoccurs when the data for which the watch was set changes。 Watch的三个关键点: (一次性触发)One-time trigger当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了getData(“/znode1”, true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1 再一次发生了变化,除非客户端再次对/znode1 设置监视,否则客户端不会收到事件通知。 (发送至客户端)Sent to the clientZookeeper客户端和服务端是通过 socket 进行通信的,由于网络存在故障, 所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper 本身提供了顺序保证(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的znode发生了变化。网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。 (被设置 watch 的数据)The data for which the watch was set这意味着znode节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() 和exists()设置数据监视,getChildren()设置子节点监视。或者你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回znode节点的相关信息,而getChildren() 返回子节点列表。因此,setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。一次成功的 delete操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。 Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说这通常是透明的。只有一种情况会导致监视事件的丢失,即:通过exists()设置了某个znode节点的监视,但是如果某个客户端在此znode节点被创建和删除的时间间隔内与zookeeper服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知。 ZooKeeper的工作原理在zookeeper的集群中,各个节点共有下面3种角色和4种状态: 角色:leader,follower,observer 状态:leading,following,observing,looking Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议(ZooKeeper Atomic Broadcast protocol)。Zab协议有两种模式,它们分别是恢复模式(Recovery选主)和广播模式(Broadcast同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。 为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。 每个Server在工作过程中有4种状态: LOOKING:当前Server不知道leader是谁,正在搜寻。 LEADING:当前Server即为选举出来的leader。 FOLLOWING:leader已经选举出来,当前Server与之同步。 OBSERVING:observer的行为在大多数情况下与follower完全一致,但是他们不参加选举和投票,而仅仅接受(observing)选举和投票的结果。 Zookeeper的典型应用场景参考:阿里中间件博客 数据发布与订阅(配置中心)发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZK节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。 应用中用到的一些配置信息放到ZK上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的。 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZK的一些指定节点,供各个客户端订阅使用。 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在ZK上创建一个以应用名作为path的节点P,并将这个应用的所有机器ip,以子节点的形式注册到节点P上,这样一来就能够实现机器变动的时候,能够实时通知到收集器调整任务分配。 系统中有些信息需要动态获取,并且还会存在人工手动去修改这个信息的发问。通常是暴露出接口,例如JMX接口,来获取一些运行时的信息。引入ZK之后,就不用自己实现一套方案了,只要将这些信息存放到指定的ZK节点上即可。 ==注意:在上面提到的应用场景中,有个默认前提是:数据量很小,但是数据更新可能会比较快的场景。== 负载均衡这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。消息中间件中发布者和订阅者的负载均衡,linkedin开源的KafkaMQ和阿里开源的metaq都是通过zookeeper来做到生产者、消费者的负载均衡。这里以metaq为例如讲下: 生产者负载均衡:metaq发送消息的时候,生产者在发送消息的时候必须选择一台broker上的一个分区来发送消息,因此metaq在运行过程中,会把所有broker和对应的分区信息全部注册到ZK指定节点上,默认的策略是一个依次轮询的过程,生产者在通过ZK获取分区列表之后,会按照brokerId和partition的顺序排列组织成一个有序的分区列表,发送的时候按照从头到尾循环往复的方式选择一个分区来发送消息。 消费负载均衡: 在消费过程中,一个消费者会消费一个或多个分区中的消息,但是一个分区只会由一个消费者来消费。MetaQ的消费策略是: 每个分区针对同一个group只挂载一个消费者。 如果同一个group的消费者数目大于分区数目,则多出来的消费者将不参与消费。 如果同一个group的消费者数目小于分区数目,则有部分消费者需要额外承担消费任务。 在某个消费者故障或者重启等情况下,其他消费者会感知到这一变化(通过 zookeeper watch消费者列表),然后重新进行负载均衡,保证所有的分区都有消费者进行消费。 命名服务(Naming Service)命名服务也是分布式系统中比较常见的一类场景。在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等——这些我们都可以统称他们为名字(Name)。其中较为常见的就是一些分布式服务框架中的服务地址列表。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。 阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表,点击这里查看Dubbo开源项目。在Dubbo实现中: 服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。 服务消费者启动的时候,订阅/dubbo/${serviceName}/providers目录下的提供者URL地址, 并向/dubbo/${serviceName} /consumers目录下写入自己的URL地址。 ==注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。== 另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目录下所有提供者和消费者的信息。 分布式通知/协调ZooKeeper中特有watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。使用方法通常是不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理 另一种心跳检测机制:检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,大大减少系统耦合。 另一种系统调度模式:某系统有控制台和推送系统两部分组成,控制台的职责是控制推送系统进行相应的推送工作。管理人员在控制台作的一些操作,实际上是修改了ZK上某些节点的状态,而ZK就把这些变化通知给他们注册Watcher的客户端,即推送系统,于是,作出相应的推送任务。 另一种工作汇报模式:一些类似于任务分发系统,子任务启动后,到zk来注册一个临时节点,并且定时将自己的进度进行汇报(将进度写回这个临时节点),这样任务管理者就能够实时知道任务进度。 总之,使用zookeeper来进行分布式通知和协调能够大大降低系统之间的耦合 集群管理与Master选举集群机器监控:这通常用于那种对集群中机器状态,机器在线率有较高要求的场景,能够快速对集群中机器变化作出响应。这样的场景中,往往有一个监控系统,实时检测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器,或者每个机器自己定时向监控系统汇报“我还活着”。 这种做法可行,但是存在两个比较明显的问题: 集群中机器有变动的时候,牵连修改的东西比较多。 有一定的延时。 利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统: 客户端在节点 x 上注册一个Watcher,那么如果 x?的子节点变化了,会通知该客户端。 创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,那么该节点就会消失。 例如,监控系统在 /clusterServers 节点上注册一个Watcher,以后每动态加机器,那么就往 /clusterServers 下创建一个 EPHEMERAL类型的节点:/clusterServers/{hostname}. 这样,监控系统就能够实时知道机器的增减情况,至于后续处理就是监控系统的业务了。 Master选举则是zookeeper中最为经典的应用场景了。 在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。 利用ZooKeeper的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建 /currentMaster 节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。 另外,这种场景演化一下,就是动态Master选举。这就要用到?EPHEMERAL_SEQUENTIAL类型节点的特性了。 上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,就是允许所有请求都能够创建成功,但是得有个创建顺序,于是所有的请求最终在ZK上创建结果的一种可能情况是这样: /currentMaster/{sessionId}-1 ,?/currentMaster/{sessionId}-2 ,?/currentMaster/{sessionId}-3 ….. 每次选取序列号最小的那个机器作为Master,如果这个机器挂了,由于他创建的节点会马上小时,那么之后最小的那个机器就是Master了。 在搜索系统中,如果集群中每个机器都生成一份全量索引,不仅耗时,而且不能保证彼此之间索引数据一致。因此让集群中的Master来进行全量索引的生成,然后同步到集群中其它机器。另外,Master选举的容灾措施是,可以随时进行手动指定master,就是说应用在zk在无法获取master信息时,可以通过比如http方式,向一个地方获取master。 在Hbase中,也是使用ZooKeeper来实现动态HMaster的选举。在Hbase实现中,会在ZK上存储一些ROOT表的地址和HMaster的地址,HRegionServer也会把自己以临时节点(Ephemeral)的方式注册到Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的存活状态,同时,一旦HMaster出现问题,会重新选举出一个HMaster来运行,从而避免了HMaster的单点问题 分布式锁分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。 所谓保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。 控制时序,就是所有视图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock 已经预先存在,客户端在它下面创建临时有序节点(这个可以通过节点的属性控制:CreateMode.EPHEMERAL_SEQUENTIAL来指定)。Zk的父节点(/distribute_lock)维持一份sequence,保证子节点创建的时序性,从而也形成了每个客户端的全局时序。 分布式队列队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。 第二种队列其实是在FIFO队列的基础上作了一个增强。通常可以在 /queue 这个znode下预先建立一个/queue/num 节点,并且赋值为n(或者直接给/queue赋值n),表示队列大小,之后每次有队列成员加入后,就判断下是否已经到达队列大小,决定是否可以开始执行了。这种用法的典型场景是,分布式环境中,一个大任务Task A,需要在很多子任务完成(或条件就绪)情况下才能进行。这个时候,凡是其中一个子任务完成(就绪),那么就去 /taskList 下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /taskList 发现自己下面的子节点满足指定个数,就可以进行下一步按序进行处理了。 Zookeeper应用近期公司给了我一个简单的任务,就是利用zk 实现当外界的某一个条件改变的时候,通知客户端或者服务端重新获取新的取值。技术上使用zk的分布式通知/协调来实现。 Zookeeper客户端Curator的使用Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。 Curator包含了几个包: curator-framework:对zookeeper的底层api的一些封装。 curator-client:提供一些客户端的操作,例如重试策略等。 curator-recipes:封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等。 Maven依赖(使用curator的版本:2.12.0,对应Zookeeper的版本为:3.4.x,如果跨版本会有兼容性问题,很有可能导致节点操作失败): 12345678910<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version></dependency><dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version></dependency> Curator的基本Api创建会话1.使用静态工程方法创建客户端 123456RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);CuratorFramework client = CuratorFrameworkFactory.newClient( connectionInfo, 5000, 3000, retryPolicy); newClient静态工厂方法包含四个主要参数: 参数名 说明 connectionString 服务器列表,格式host1:port1,host2:port2,… retryPolicy 重试策略,内建有四种重试策略,也可以自行实现RetryPolicy接口 sessionTimeoutMs 会话超时时间,单位毫秒,默认60000ms connectionTimeoutMs 连接创建超时时间,单位毫秒,默认60000ms 2.使用Fluent风格的Api创建会话 核心参数变为流式设置,一个列子如下: 12345678RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);CuratorFramework client =CuratorFrameworkFactory.builder() .connectString(connectionInfo) .sessionTimeoutMs(5000) .connectionTimeoutMs(5000) .retryPolicy(retryPolicy) .build(); 3.创建包含隔离命名空间的会话 为了实现不同的Zookeeper业务之间的隔离,需要为每个业务分配一个独立的命名空间(NameSpace),即指定一个Zookeeper的根路径(官方术语:为Zookeeper添加“Chroot”特性)。例如(下面的例子)当客户端指定了独立命名空间为“/base”,那么该客户端对Zookeeper上的数据节点的操作都是基于该目录进行的。通过设置Chroot可以将客户端应用与Zookeeper服务端的一棵子树相对应,在多个应用共用一个Zookeeper集群的场景下,这对于实现不同应用之间的相互隔离十分有意义。 123456789RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectionInfo) .sessionTimeoutMs(5000) .connectionTimeoutMs(5000) .retryPolicy(retryPolicy) .namespace("base") //命名空间 .build(); 启动客户端当创建会话成功,得到client的实例然后可以直接调用其 start( ) 方法: 1client.start(); CuratorFramework的方法 方法名 描述 create() 开始创建操作, 可以调用额外的方法(比如方式mode 或者后台执行background) 并在最后调用forPath()指定要操作的ZNode delete() 开始删除操作. 可以调用额外的方法(版本或者后台处理version or background)并在最后调用forPath()指定要操作的ZNode checkExists() 开始检查ZNode是否存在的操作. 可以调用额外的方法(监控或者后台处理)并在最后调用forPath()指定要操作的ZNode getData() 开始获得ZNode节点数据的操作. 可以调用额外的方法(监控、后台处理或者获取状态watch, background or get stat) 并在最后调用forPath()指定要操作的ZNode setData() 开始设置ZNode节点数据的操作. 可以调用额外的方法(版本或者后台处理) 并在最后调用forPath()指定要操作的ZNode getChildren() 开始获得ZNode的子节点列表。 以调用额外的方法(监控、后台处理或者获取状态watch, background or get stat) 并在最后调用forPath()指定要操作的ZNode inTransaction() 开始是原子ZooKeeper事务. 可以复合create, setData, check, and/or delete 等操作然后调用commit()作为一个原子操作提交 数据节点操作创建数据节点Zookeeper的节点属性: PERSISTENT:持久化 PERSISTENT_SEQUENTIAL:持久化并且带序列号 EPHEMERAL:临时 EPHEMERAL_SEQUENTIAL:临时并且带序列号 注意:如果没有设置节点属性,节点创建模式默认为持久化节点,内容默认为空 a.创建一个节点,初始内容为空: 1client.create().forPath("path"); b.创建一个节点,附带初始化内容 1client.create().forPath("path","init".getBytes()); c.创建一个节点,指定创建模式(临时节点),内容为空 1client.create().withMode(CreateMode.EPHEMERAL).forPath("path"); d.创建一个节点,指定创建模式(临时节点),附带初始化内容 1client.create().withMode(CreateMode.EPHEMERAL).forPath("path","init".getBytes()); e.创建一个节点,指定创建模式(临时节点),附带初始化内容,并且自动递归创建父节点 1234client.create() .creatingParentContainersIfNeeded() .withMode(CreateMode.EPHEMERAL) .forPath("path","init".getBytes()); 这个creatingParentContainersIfNeeded()接口非常有用,因为一般情况开发人员在创建一个子节点必须判断它的父节点是否存在,如果不存在直接创建会抛出NoNodeException,使用creatingParentContainersIfNeeded()之后Curator能够自动递归创建所有所需的父节点。 删除数据节点1234567891011121314//1.删除一个节点,此方法只能删除叶子节点,否则会抛出异常。client.delete().forPath("path");//2.删除一个节点,并且递归删除其所有的子节点client.delete().deletingChildrenIfNeeded().forPath("path");//3.删除一个节点,强制指定版本进行删除client.delete().withVersion(10086).forPath("path");//4.删除一个节点,强制保证删除,guaranteed()接口是一个保障措施,只要客户端会话有效,那么Curator会在后台持续进行删除操作,直到删除节点成功。client.delete().guaranteed().forPath("path");//5.上面的多个流式接口是可以自由组合,例如:client.delete().guaranteed().deletingChildrenIfNeeded().withVersion(10086).forPath("path"); 读取数据节点数据123456//1.读取一个节点的数据内容,注意,此方法返的返回值是byte[ ];client.getData().forPath("path");//2.读取一个节点的数据内容,同时获取到该节点的statStat stat = new Stat();client.getData().storingStatIn(stat).forPath("path"); 更新数据节点数据12345//1.更新一个节点的数据内容,该接口会返回一个Stat实例client.setData().forPath("path","data".getBytes());//2.更新一个节点的数据内容,强制指定版本进行更新client.setData().withVersion(10086).forPath("path","data".getBytes()); 检查节点是否存在123//该方法返回一个Stat实例,用于检查ZNode是否存在的操作.//可以调用额外的方法(监控或者后台处理)并在最后调用forPath()指定要操作的ZNodeclient.checkExists().forPath("path"); 获取某个节点的所有子节点路径1234//该方法的返回值为List,获得ZNode的子节点Path列表。//可以调用额外的方法(监控、后台处理或者获取状态watch, background or get stat)//并在最后调用forPath()指定要操作的父ZNodeclient.getChildren().forPath("path"); 事务CuratorFramework的实例包含inTransaction( )接口方法,调用此方法开启一个ZooKeeper事务. 可以复合create, setData, check, and/or delete 等操作然后调用commit()作为一个原子操作提交。 1234567client.inTransaction().check().forPath("path") .and() .create().withMode(CreateMode.EPHEMERAL).forPath("path","data".getBytes()) .and() .setData().withVersion(10086).forPath("path","data2".getBytes()) .and() .commit(); 异步接口上面提到的创建、删除、更新、读取等方法都是同步的,Curator提供异步接口,引入了BackgroundCallback接口用于处理异步接口调用之后服务端返回的结果信息。BackgroundCallback接口中一个重要的回调值为CuratorEvent,里面包含事件类型、响应吗和节点的详细信息。 CuratorEventType 事件类型 对应CuratorFramework实例的方法 CREATE create() DELETE delete() EXISTS checkExists() GET_DATA getData() SET_DATA setData() CHILDREN getChildren() SYNC sync(String,Object) GET_ACL getACL() SET_ACL setACL() WATCHED Watcher(Watcher) CLOSING close() 响应码(#getResultCode()) 响应码 意义 0 OK,即调用成功 -4 ConnectionLoss,即客户端与服务端断开连接 -110 NodeExists,即节点已经存在 -112 SessionExpired,即会话过期 异步创建节点的示例如下: 1234567Executor executor = Executors.newFixedThreadPool(2);client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .inBackground((curatorFramework, curatorEvent) -> { System.out.println(String.format("eventType:%s,resultCode:%s",curatorEvent.getType(),curatorEvent.getResultCode())); },executor).forPath("path"); 注意:如果#inBackground()方法不指定executor,那么会默认使用Curator的EventThread去进行异步处理。 Curator高级特性以上都是Curator简单的使用,在实际应用中会使用更高级的方式来管理zk节点。 提醒:首先你必须添加curator-recipes依赖 12345<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version></dependency> CacheZookeeper原生支持通过注册Watcher来进行事件监听,但是开发者需要反复注册(Watcher只能单次注册单次使用)。Cache是Curator中对事件监听的包装,可以看作是对事件监听的本地缓存视图,能够自动为开发者处理反复注册监听。Curator提供了三种 Watcher(Cache) 来监听结点的变化。 Path CachePath Cache用来监控一个ZNode的子节点. 当一个子节点增加, 更新,删除时, Path Cache会改变它的状态, 会包含最新的子节点, 子节点的数据和状态,而状态的更变将通过PathChildrenCacheListener通知。 实际使用时会涉及到四个类: PathChildrenCache PathChildrenCacheEvent PathChildrenCacheListener ChildData 通过下面的构造函数创建Path Cache: 1public PathChildrenCache(CuratorFramework client, String path, boolean cacheData) 想使用cache,必须调用它的start方法,使用完后调用close方法。 可以设置StartMode来实现启动的模式, StartMode有下面几种: NORMAL:正常初始化。 BUILD_INITIAL_CACHE:在调用start()之前会调用rebuild()。 POST_INITIALIZED_EVENT:当Cache初始化数据后发送一个PathChildrenCacheEvent.Type#INITIALIZED事件 ++public void addListener(PathChildrenCacheListener listener)++ 可以增加listener监听缓存的变化。 ++getCurrentData()++ 方法返回一个List 对象,可以遍历所有的子节点。 设置/更新、移除其实是使用client (CuratorFramework)来操作, 不通过PathChildrenCache操作:项目示例: 123456789101112131415161718192021222324252627282930313233343536373839/** * 监听子节点 * 子节点新增,就往里面写数据 * 子节点更新, * 子节点删除, */ public void start(){ final String path = ZKContant.ROOT_PATH + "/" + strategy; //client是通过 CuratorFrameworkFactory.newClient创建的 final PathChildrenCache cache = new PathChildrenCache(client, path,true); ExecutorService pool = Executors.newCachedThreadPool(); cache.getListenable().addListener(new PathChildrenCacheListener(){ @Override public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception { ChildData data = pathChildrenCacheEvent.getData(); switch (pathChildrenCacheEvent.getType()) { case CHILD_ADDED: //向子节点写入信息 并更新信息 childrenWatch.childrenAdd(data); break; case CHILD_UPDATED: childrenWatch.childrenUpdate(data); break; case CHILD_REMOVED: childrenWatch.childrenRemove(data); break; default: break; } } }, pool); try { cache.start(); } catch (Exception e) { logger.error(e.toString()); throw new RuntimeException(e); } } 注意:如果new PathChildrenCache(client, PATH, true)中的参数cacheData值设置为false,则示例中的pathChildrenCacheEvent.getData().getData()、data.getData()将返回null,cache将不会缓存节点数据。 Node CacheNode Cache与Path Cache类似,Node Cache只是监听某一个特定的节点。它涉及到下面的三个类: NodeCache - Node Cache实现类 NodeCacheListener - 节点监听器 ChildData - 节点数据 注意:使用cache,依然要调用它的start()方法,使用完后调用close()方法。 getCurrentData()将得到节点当前的状态,通过它的状态可以得到当前的值。 12345678910111213141516171819202122public void start() { final String path = ZKContant.ROOT_PATH + "/" + strategy; final NodeCache cache = new NodeCache(client, path, true); ExecutorService pool = Executors.newCachedThreadPool(); cache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception { ChildData currentData = cache.getCurrentData(); if (currentData != null){ Properties props = ObjectUtil.toObject(currentData.getData()); nodeWatch.handle(props, path); } } }, pool); try { cache.start(); } catch (Exception e) { logger.error(e.toString()); throw new RuntimeException(e); } } 注意:NodeCache只能监听一个节点的状态变化。 Tree CacheTree Cache可以监控整个树上的所有节点,类似于PathCache和NodeCache的组合,主要涉及到下面四个类: TreeCache - Tree Cache实现类 TreeCacheListener - 监听器类 TreeCacheEvent - 触发的事件类 ChildData - 节点数据 1234567891011121314151617181920212223242526public class TreeCacheDemo { private static final String PATH = "/example/cache"; public static void main(String[] args) throws Exception { TestingServer server = new TestingServer(); CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new ExponentialBackoffRetry(1000, 3)); client.start(); client.create().creatingParentsIfNeeded().forPath(PATH); TreeCache cache = new TreeCache(client, PATH); TreeCacheListener listener = (client1, event) -> System.out.println("事件类型:" + event.getType() + " | 路径:" + (null != event.getData() ? event.getData().getPath() : null)); cache.getListenable().addListener(listener); cache.start(); client.setData().forPath(PATH, "01".getBytes()); Thread.sleep(100); client.setData().forPath(PATH, "02".getBytes()); Thread.sleep(100); client.delete().deletingChildrenIfNeeded().forPath(PATH); Thread.sleep(1000 * 2); cache.close(); client.close(); System.out.println("OK!"); }} 注意:TreeCache在初始化(调用start()方法)的时候会回调TreeCacheListener实例一个事TreeCacheEvent,而回调的TreeCacheEvent对象的Type为INITIALIZED,ChildData为null,此时event.getData().getPath()很有可能导致空指针异常,这里应该主动处理并避免这种情况。 工作中主要使用了Path Cache 和Node Cache,一个监控子节点,一个监控节点。系统中会有很多种策略,每种策略都会在根节点上创建一个ZKNode,当另一个系统启动的时候,也会检测这个策略节点,当这个策略节点信息改变的时候,监控客户端和服务端就回收到改变的通知。然后做出相应的处理。 大致如流程图所示: end参考:]]></content>
<categories>
<category>分布式,Dubbo,Zookeeper</category>
</categories>
<tags>
<tag>Dubbo,Zookeeper,分布式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈MQ]]></title>
<url>%2F2018%2F11%2F28%2F%E6%B5%85%E8%B0%88MQ%2F</url>
<content type="text"><![CDATA[达到沟通目的才算有效沟通 概述在上家公司工作的时候,ZStack架构的设计中用到RabbitMQ,让一个服务混乱的项目,变得简洁而清晰。所以对MQ又进一步进行研究。 什么是消息队列我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。因为队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。 为什么要用消息队列使用消息队列主要有两点好处:1.通过异步处理提高系统性能(削峰、减少响应所需时间); 2.降低系统耦合性。 (1) 通过异步处理提高系统性能(削峰、减少响应所需时间) 如上图,在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。 通过以上分析我们可以得出消息队列具有很好的削峰作用的功能——即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示: 因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合,比如用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功,以免交易纠纷。这就类似我们平时手机订火车票和电影票。 (2) 降低系统耦合性我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。 我们最常见的事件驱动架构类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示: 消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。 从上图可以看到消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计。 消息接受者对消息进行过滤、处理、包装后,构造成一个新的消息类型,将消息继续发送出去,等待其他消息接受者订阅该消息。因此基于事件(消息对象)驱动的业务架构可以是一系列流程。 另外为了避免消息队列服务器宕机造成消息丢失,会将成功发送到消息队列的消息存储在消息生产者服务器上,等消息真正被消费者服务器处理后才删除消息。在消息队列服务器宕机后,生产者服务器会选择分布式消息队列服务器集群中的其他服务器发布消息。 备注: 在解耦这个特定业务环境下是使用发布-订阅模式的。除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。 另外,这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 消息队列带来的问题 系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了! 系统复杂性提高: 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题! 一致性问题:我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! JMS 和AMQP比较JMSJMS(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范,允许应用程序组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 jms是消息队列中提供的一组API 接口,是提供的服务API。 JMS和JDBC担任差不多的角色,用户都是根据相应的接口可以和实现了JMS的服务进行通信,进行相关的操作。ActiveMQ 就是基于 JMS 规范实现的。 JMS两种消息模型①点到点(P2P)模型 使用队列(Queue)作为消息通信载体;满足生产者与消费者模式,一条消息只能被一个消费者使用,未被消费的消息在队列中保留直到被消费或超时。比如:我们生产者发送100条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。) ② 发布/订阅(Pub/Sub)模型 发布订阅模型(Pub/Sub) 使用主题(Topic)作为消息通信载体,类似于广播模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。 JMS 五种不同的消息正文格式JMS定义了五种不同的消息正文格式,以及调用的消息类型,允许你发送并接收以一些不同形式的数据,提供现有消息格式的一些级别的兼容性。 StreamMessage –Java原始值的数据流 MapMessage–一套名称-值对 TextMessage–一个字符串对象 ObjectMessage–一个序列化的Java对象 BytesMessage–一个字节的数据流 AMQPAMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 高级消息队列协议(二进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。RabbitMQ 就是基于 AMQP 协议实现的。 AMQP模型 AMPQ的消息模型示意图如下: 运作过程 左边的客户端向右边的客户发送消息,流程如下: 获取Connection(客户端到MQ服务器的TCP链路) 获取Channel(逻辑层的链路,基于Conncetion) 定义交换器、队列 使用一个RoutingKey将队列绑定到一个交换器 通过指定一个交换器和一个RoutingKey来消息发送到对应的队列上 接收方在接受时也是获取Connection,接着获取Channel,然后指定一个队列直接到它关心的队列上取消息,它对交换器、RoutingKey及如何绑定都不关心,到对应的对列上取消息就行了 名词解释在该模型中,三个主要功能模块连接成一个处理链完成预期的功能: exchange(交换器):接收发布应用程序发送的消息,并根据一定的规则将这些消息路由到“消息队列”。 message queue(消息队列):存储消息,直到这些消息被消费者安全处理完为止。 binding(绑定器):定义了exchange和message queue之间的关联,提供路由规则。 Exchange本身不保持消息,只是起到路由的作用,Exchange接收消息生产者(MessageProducer)发送的消息根据不同的路由算法将消息发送往MessageQueue。MessageQueue会在消息不能被正常消费时缓存这些消息,具体的缓存策略由实现者决定,当MessageQueue与消息消费者(Messageconsumer)之间的连接通畅时,MessageQueue会将消息转发到consumer。 AMQP架构图如下: 该图(VirtualHost)用来指Exchange和MessageQueue组成的集合。它是一个虚拟概念,一个虚拟主机可以是一台服务器,还可以是由多台服务器组成的集群,还可以是一些虚拟机组成的集群,上面运行一些Exchange和MessageQueue。 JMS vs AMQP 总结 AMQP 为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。在 Java 体系中,多个client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平台、跨语言特性。 JMS 支持TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可序列化后发送)。 由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队列 和 主题/订阅 方式两种。 常见消息队列 MQ在实际项目中的使用openStack中的MQOpenStack遵循这样的设计原则:项目之间通过RESTful API进行通信;项目内部,不同服务进程之间的通信,则必须要通过消息总线。这种设计思想保证了各个项目对外提供服务的接口可以被不同类型的客户端高效支持,同时也保证了项目内部通信接口的可扩展性和可靠性,以支持大规模的部署。 软件从最初的面向过程,面向对象,再到面向服务(SOA),要求我们去考虑各个服务之间如何传递消息。借鉴硬件总线的概念,消息总线的模式被引入,顾名思义,一些服务向总线发送消息,其他服务从总线上获取消息。 OpenStack oslo.messageing库实现了以下两种方式来完成项目内部各服务进程之间的通信: 远程过程调用(RPC,Remote Procedure Call) 通过远程过程调用,一个服务进程可以调用其他远程服务进程方法,并且有两种调用方式:call和cast。call 则是同步执行的,调用者会被阻塞直到结果返回;cast 则是异步执行,结果不会立刻被返回,调用者也不会被阻塞,但是调用者需要利用其他方式查询这次远程调用的结果。 事件通知(Event Notification) 某个服务进程可以把事件通知发送到消息总线上,该消息总线上所有对此类事件感兴趣的服务进程,都可以获得此事件通知并进行一步的处理,处理的结果并不会返回给事件发送者。这种通信方式,不但可以在同一个项目内部的各个服务进程之间发送通知,也可以实现跨项目之间的通知发送。Ceilometer就通过这种方式大量获取其他OpenStack项目的事件通知,从而进行计量和监控。 事件通知的方式在ZStack中也有体现:如下图所示:MQ用作消息总线 OpenStack中的通信方式OpenStack中所支持的消息总线类型中,大部分都是基于AMQP的。前面已经提到过了。 基于AMPQ实现RPC 这里引用别人画好的图:如下图 客户端发送一个请求消息给Exchange,指定routing key为”op_queue”,同时指明一个消息队列名用来获取响应,图中为”res_queue”,同时指明一个消息队列名用来获取响应。在图中为”res_queue” Exchange把此消息转发到消息队列op_queue 消息队列op_queue把消息推送给服务端,服务端执行此RPC调用对应的任务。执行结束后,服务端把相应结果发送给消息队列,指明routing key为”res_queue” Exchange 把此消息转发到消息队列res_queue 客户端从消息队列res_queue中获取响应。]]></content>
<categories>
<category>MQ</category>
</categories>
<tags>
<tag>MQ,Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[此刻小记]]></title>
<url>%2F2018%2F11%2F24%2F%E6%AD%A4%E5%88%BB%E5%B0%8F%E8%AE%B0%2F</url>
<content type="text"><![CDATA[吴军所写的《态度》一书真的不错 工作自从上家公司离职,最近一直在找工作,失业已经两周的时间了,这种没有事情做的感觉真的挺难受,就像随风飘摇的蒲公英一样,什么事都缺少趣味。除了学习看书,基本上没有事情可以干。不过这段时间也给我很多思考的时间,review之前工作中暴露的问题,当时是怎么做的,为什么这么做,有没有更好的办法。挺后悔前一两个月没有把态度摆正,以为自己是个应届毕业生,就可以不那么着急开始自己的工作,如果摆正自己学到的应该更多,但是我真的是特别珍惜上家公司的工作。离开就离开了,自己不要丢掉信心就好。 说实话这是我这一年来学到最多,感觉视野一下子宽了很多。从学生到一个社会人,职场人,中间遇到过很多自己以前不愿意做的事情。当你见识到比自己厉害的人后,你就会发现自己的弱小和无知 。那种想要超越别人的欲望愈发的强烈。 懒惰已经成为前进的阻碍。现在我才觉得自己真的挺懒的,规划的事情总是不能按当初想的完成。有一次一天给自己定好几个闹钟,提醒自己该做什么了。这段时间也正在努力让自己勤快一点。不想让自己失望。 工作这段时间挺感谢一个人的,那就是我师傅。第一个让我感觉强大的人,前几个月可以说是手把手教我敲代码。他的负责任的态度和他渊博的知识,帮助我很多。也同时感谢QBackup产品线的各位,让我有了一段难忘的经历。感谢大家的帮助。 迷茫的时候你会做什么?这是我在熊总即将离开公司的时候问他的一个问题。 熊总是我们产品部门的老大,也是我招我进公司的一个重要的人。他因为自己对技术的追求,离开自己亲手创办的公司,现在好像在美团工作。 听说他要离开公司,真的很诧异,有点惊呆了。他是一个幽默又特别好相处的一个人。我真的特别喜欢他,也特别感谢他让我有机会进入公司。知道他要离开的消息后,我就写了一封邮件,一来表达我内心的感谢,二来解答自己内心的疑惑。 当时这个问题还在公司的群里面谈论了一番,感觉自己提出了一个许多人都遇到过的问题,挺开心的。从他的回复和群中讨论大概结果如下: 这个时代的焦虑更多的来自于对自身不足的不满,解决问题的最好办法就是更加的努力,花时间去想看不见摸不着的东西,不如指定一个更加长期的,艰苦的学习计划,提高能力的同时也磨炼自己的心性。 每个人对自己的鄙视都是对自己不足的不满。 迷茫的时候想的再多都是毫无作用,空想只会让你更加的迷茫,不如更加艰苦的学习(不是疯狂加班),我相信通过这种方式给予自己信心,也就慢慢的找到前进的方向。 总结一个正确的态度是非常有必要的。工作中要把态度摆正,无论这个功能多简单或者多难都要尽力做到出人意料的精彩。 一个人要有表达自己观点的勇气,新人也好,菜鸡也罢,都要有勇气面对他人,表达自己。 学会沟通。 项目的进度或者困难都要给自己的上级进行及时的答复,不要每次都等到别人来问你工作的进度,你遇到什么困难等。 自信。 自信对待代码,自信对待公司的人,自信对待自己。 态度与自信无聊的时候我追了一部剧《将夜》,虽然有人说一般般,但是我感觉还挺好。 我最记忆犹新的一段是,作为一个 直通十窍,书院第十三个进入书院二层楼的人,又是世上最强符道之人的唯一徒弟,修炼速度就是个废柴的主人公宁缺。一度自信全无。 可是人生哪有什么顺顺利利,挫折总会克服,又总会到来。在在意他的人的帮助下,慢慢的把失去的信心找了回来,画出了人生的第一道符。 态度和自信 人生路上必不可少。带着他们 我在未来的某一刻也会画出自己人生的第一道符。 文采拙劣,敬请原谅]]></content>
<categories>
<category>所思所想</category>
</categories>
<tags>
<tag>所思所想</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java8新特性]]></title>
<url>%2F2018%2F11%2F19%2FJava8%2F</url>
<content type="text"><![CDATA[没有工作的生活感觉真的无聊 概述自从毕业进入上家公司,感受到自己的无知和技术更新迭代速度是真的快。Java8中许多新特性都在ZStack中有所使用。故这里对Java8新特性进行总结一下。 近几年Java的发展的确挺快的,修改了许多令开发人员诟病的特性。正在逐渐吸收着其他语言的优点。我相信Java仍是许多项目中不可或缺的语言。前景仍然看好。 Lambda表达式官网表述:Lambda表达式是Java 8中最大和最令人期待的语言改变。它允许我们将函数当成参数传递给某个方法,或者把代码本身当作数据处理:函数式开发者非常熟悉这些概念。很多JVM平台上的语言(Groovy、Scala等)从诞生之日就支持Lambda表达式,但是早期Java开发者没有选择,只能使用匿名内部类代替Lambda表达式。 最简单的Lambda表达式可由逗号分隔的参数列表、->符号和语句块组成,例如: 1Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) ); 在上面这个代码中的参数e的类型是由编译器推理得出的,你也可以显式指定该参数的类型,例如: 1Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) ); 如果Lambda表达式需要更复杂的语句块,则可以使用花括号将该语句块括起来,类似于Java中的函数体,例如: 1234Arrays.asList( "a", "b", "d" ).forEach( e -> { System.out.print( e ); System.out.print( e );} ); Lambda表达式可以引用类成员和局部变量(会将这些变量隐式得转换成final的),例如下列两个代码块的效果完全相同: 12345678String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) ); final String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) ); Lambda表达式有返回值,返回值的类型也由编译器推理得出。如果Lambda表达式中的语句块只有一行,则可以不用使用return语句,下列两个代码片段效果相同: 1234567Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> { int result = e1.compareTo( e2 ); return result;} ); Java8实现Lambda表达式其实就是让行为参数化,就是把一个行为当做方法的参数,以此达到和以前版本进行兼容的目的。 注意:Java 8的Lambda和匿名类和闭包是有一些不同的地方。用科学的说法来说,闭包就是一个函数的实例,且它可以无限制地访问那个函数的非本地变量。例如,闭包可以作为参数传递给另一个函数。它也可以访问和修改其作用域之外的变量。Java 8的Lambda和匿名类可以做类似于闭包的事情:它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但有一个限制:它们不能修改定义Lambda的方法的局部变量的内容。这些变量必须是隐式最终的。可以认为Lambda是对值封闭,而不是对变量封闭。这种限制存在的原因在于局部变量保存在栈上,并且隐式表示它们仅限于其所在线程。如果允许捕获可改变的局部变量,就会引发造成线程不安全的新的可能性,而这是我们不想看到的。 函数接口Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了),举个简单的函数式接口的定义: 1234@FunctionalInterfacepublic interface Functional { void method();} 如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods found in interface Foo”,表明存在多个抽象方法。看下面的例子:1234567891011121314151617//接口public interface Cumputer { String start(String name); String communication(int e);}//主函数public class LambdaDemo { static void demoInterface(Cumputer cumputer){ } public static void main(String [] args){ demoInterface(name -> { name += "a"; return name; }); }} 这个时候 name += “a”这句就会报错,Lambda表达式无法推断出name的参数类型。如果指定name的类型。如下: 123456public static void main(String [] args){ demoInterface((String name)-> { name += "a"; return name; }); } 编译器会提示:Multiple non-overriding abstract methods found in interface Foo错误。函数接口只能允许有一个方法。所以@FunctionalInterface就可以提示开发者在修改接口的时候,这个接口是否允许多个方法行为。 请注意,@FunctionalInterface不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。并且默认方法和静态方法不会破坏函数式接口的定义,因此如下的代码是合法的。 1234567@FunctionalInterfacepublic interface FunctionalDefaultMethods { void method(); default void defaultMethod() { } } 接口默认方法默认方法使得接口有点类似traits,不过要实现的目标不一样。默认方法使得开发者可以在 不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。 默认方法和抽象方法之间的区别在于抽象方法需要实现,而默认方法不需要。接口提供的默认方法会被接口的实现类继承或者覆写,例子代码如下: 1234567private interface Defaulable { // Interfaces now allow default methods, the implementer may or // may not implement (override) them. default String notRequired() { return "Default implementation"; } } 由于JVM上的默认方法的实现在字节码层面提供了支持,因此效率非常高。默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给java.util.Collection接口添加新方法,如stream()、parallelStream()、forEach()和removeIf()等等。 方法引用方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。方法引用可以使用::。定义了4个方法的Car这个类作为例子,区分Java中支持的4种不同的方法引用。 1234567891011121314151617public static class Car { public static Car create( final Supplier< Car > supplier ) { return supplier.get(); } public static void collide( final Car car ) { System.out.println( "Collided " + car.toString() ); } public void follow( final Car another ) { System.out.println( "Following the " + another.toString() ); } public void repair() { System.out.println( "Repaired " + this.toString() ); }} 第一种方法引用是构造器引用,它的语法是Class::new,或者更一般的Class< T >::new。请注意构造器没有参数。 12final Car car = Car.create( Car::new );final List< Car > cars = Arrays.asList( car ); 第二种方法引用是静态方法引用,它的语法是Class::static_method。请注意这个方法接受一个Car类型的参数。 1cars.forEach( Car::collide ); 第三种方法引用是特定类的任意对象的方法引用,它的语法是Class::method。请注意,这个方法没有参数。 1cars.forEach( Car::repair ); 最后,第四种方法引用是特定对象的方法引用,它的语法是instance::method。请注意,这个方法接受一个Car类型的参数 12final Car police = Car.create( Car::new );cars.forEach( police::follow ); 流Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。 什么是流Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。 流的构成当我们使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。 流管道 (Stream Pipeline) 的构成 流的操作类型分为三种: Intermediate(中间):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。 Terminal(终点):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果。 Short-Circuiting(短循环):1.对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。2.对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。3.当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。 在对于一个 Stream 进行多次转换操作 (Intermediate 操作),每次都对 Stream 的每个元素进行转换,而且是执行多次,这样时间复杂度就是 N(转换次数)个 for 循环里把所有操作都做掉的总和吗?其实不是这样的,转换操作都是 lazy 的,多个转换操作只会在 Terminal 操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream 里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在 Terminal 操作的时候循环 Stream 对应的集合,然后对每个元素执行所有的函数。 示例: 1234int sum = widgets.stream().filter(w -> w.getColor() == RED) .mapToInt(w -> w.getWeight()) .sum(); stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。 流操作有两个重要的特点: 流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。这让我们下一章中的一些优化成为可能,如延迟和短路。流水线的操作可以看作对数据源进行数据库式查询。 内部迭代——与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。 流的操作接下来,当把一个数据结构包装成 Stream 后,就要开始对里面的元素进行各类操作了。常见的操作可以归类如下。Intermediate: map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、skip、 parallel、 sequential、 unordered Terminal: forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator Short-circuiting: anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit 操作 类型 返回值 说明 map 中间 Stream<R> map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。 flatMap 中间 Stream<R> 一对多, flatMap 把 input Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,看下面示例。 filter 中间 Stream<T> filter 对原始 Stream 进行某项过滤,通过过滤的元素被留下来生成一个新 Stream。 limit 中间 Stream<T> limit 返回 Stream 的前面 n 个元素; skip 中间 Stream<T> skip 则是扔掉前 n 个元素 distinct 中间 Stream<T> 找出不重复的单词 sorted 中间 Stream<T> 对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、filter、limit、skip 甚至 distinct 来减少元素数量后,再排序 min/max 中间 Stream<T> min 和 max 的功能也可以通过对 Stream 元素先排序,再 findFirst 来实现,但前者的性能会更好,为 O(n),而 sorted 的成本是 O(n log n) forEach 终端 void forEach 方法接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。 findFirst 终端 Optional<T> 返回 Stream 的第一个元素,或者空 reduce 终端 Optional<T> 主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。 findAny 终端 Optional<T> 查找到任何一个就返回 collect 终端 R 收集结果 count 终端 long 计算个数 allMatch 终端 boolean Stream 中全部元素符合传入的断言( predicate),返回 true anyMatch 终端 boolean Stream 中只要有一个元素符合传入的断言predicate,返回 true noneMatch 终端 boolean Stream 中没有一个元素符合传入的 断言predicate,返回 true flatMap示例: 1234567Stream<List<Integer>> inputStream = Stream.of( Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6) );Stream<Integer> outputStream = inputStream.flatMap((childList) -> childList.stream()); reduce 示例: 123456789101112// 字符串连接,concat = "ABCD"String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat); // 求最小值,minValue = -3.0double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); // 求和,sumValue = 10, 有起始值int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);// 求和,sumValue = 10, 无起始值sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();// 过滤,字符串连接,concat = "ace"concat = Stream.of("a", "B", "c", "D", "e", "F"). filter(x -> x.compareTo("Z") > 0). reduce("", String::concat); 收集器即Collectors类提供的工厂方法(例如groupingBy)创建的收集器。它们主要提供了三大功能: 将流元素归约和汇总为一个值 元素分组 元素分区 Collectors类的静态工厂方法: 工厂方法 返回类型 说明 示例 toList List<T> 把流中所有项目收集到一个List List<Dish> dishes = menuStream.collect(toList()); toSet Set<T> 把流中所有项目收集到一个Set,删除重复项 Set<Dish> dishes = menuStream.collect(toSet()); toCollection Collection<T> 把流中所有项目收集到给定的供应源创建的集合 Collection<Dish> dishes = menuStream.collect(toCollection(),ArrayList::new); counting Long 计算流中元素的个数 long howManyDishes = menuStream.collect(counting()); summingInt Integer 对流中项目的一个整数属性求和 int totalCalories = menuStream.collect(summingInt(Dish::getCalories)); averagingInt Double 计算流中项目Integer属性的平均值 double avgCalories =menuStream.collect(averagingInt(Dish::getCalories)); summarizingInt IntSummaryStatistics 收集关于流中项目Integer属性的统计值,例如最大、最小、总和与平均值 IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories)); joining Sring 连接对流中每个项目调用toString方法所生成的字符串 String shortMenu = menuStream.map(Dish::getName).collect(joining(", ")); groupingBy Map<K, List<T>> 根据项目的一个属性的值对流中的项目作问组,并将属性值作为结果Map的键 Map<Dish.Type,List<Dish>> dishesByType = menuStream.collect(groupingBy(Dish::getType)); partitioningBy Map<Boolean,List<T>> 根据对流中每个项目应用谓词的结果来对项目进行分区 Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian)); 在Java 7之前,并行处理数据集合非常麻烦。第一,你得明确地把包含数据的数据结构分成若干子部分。第二,你要给每个子部分分配一个独立的线程。第三,你需要在恰当的时候对它们进行同步来避免不希望出现的竞争条件,等待所有线程完成,最后把这些部分结果合并起来。Java 7引入了一个叫作分支/合并的框架,让这些操作更稳定、更不易出错。我们简要地提到了Stream接口可以让你非常方便地处理它的元素:可以通过对收集源调用parallelStream方法来把集合转换为并行流。并行流就是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让它们都忙起来。 OptionalJava应用中最常见的bug就是空值异常。在Java 8之前,Google Guava引入了Optionals类来解决NullPointerException,从而避免源码被各种null检查污染,以便开发者写出更加整洁的代码。Java 8也将Optional加入了官方库。你现在可以通过一种数据类型表示显式缺失的值——使用空指针的问题在于你无法确切了解出现空指针的原因,它是预期的情况,还是说由于之前的某一次计算出错导致的一个偶然性的空值,有了Optional之后你就不需要再使用之前容易出错的空指针来表示缺失的值了。 方法 方法名 描述 empty 返回一个空的Optional实例 filter 如果值存在并且满足提供的谓词,就返回包含该值的Optional对象;否则返回一个空的Optional对象 flatMap 如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值,否则就返回一个空的Optional对象 get 如果该值存在,将该值用Optional封装返回,否则抛出一个NoSuchElementException异常 ifPresent 如果值存在,就执行使用该值的方法调用,否则什么也不做 isPresent 如果值存在就返回true,否则返回false map 如果值存在,就对该值执行提供的mapping函数调用 of 将指定值用Optional封装之后返回,如果该值为null,则抛出一个NullPointerException异常 ofNullable 将指定值用Optional封装之后返回,如果该值为null,则返回一个空的Optional对象 orElse 如果有值则将其返回,否则返回一个默认值 orElseGet 如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值 orElseThrow 如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常 示例: 1234Optional< String > fullName = Optional.ofNullable( null );System.out.println( "Full Name is set? " + fullName.isPresent() ); System.out.println( "Full Name: " + fullName.orElseGet( () -> "[none]" ) ); System.out.println( fullName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) 结果: 123Full Name is set? falseFull Name: [none]Hey Stranger! 日期和时间Java 8中引入全新的日期和时间API:日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。 LocalDate LocalTime LocalDateTime Instant Duration Period 在Java8之前的版本中,日期时间API存在很多的问题,比如: 线程安全问题:java.util.Date是非线程安全的,所有的日期类都是可变的; 设计很差:在java.util和java.sql的包中都有日期类,此外,用于格式化和解析的类在java.text包中也有定义。而每个包将其合并在一起,也是不合理的; 时区处理麻烦:日期类不提供国际化,没有时区支持,因此Java中引入了java.util.Calendar和Java.util.TimeZone类; Java8重新设计了日期时间相关的API,Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。在java.util.time包中常用的几个类有: 它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()Instant:一个instant对象表示时间轴上的一个时间点,Instant.now()方法会返回当前的瞬时点(格林威治时间);Duration:用于表示两个瞬时点相差的时间量;LocalDate:一个带有年份,月份和天数的日期,可以使用静态方法now或者of方法进行创建;LocalTime:表示一天中的某个时间,同样可以使用now和of进行创建; LocalDateTime:兼有日期和时间;ZonedDateTime:通过设置时间的id来创建一个带时区的时间;DateTimeFormatter:日期格式化类,提供了多种预定义的标准格式; 示例: 12345678910111213141516171819public class TimeTest { public static void main(String[] args) { Clock clock = Clock.systemUTC(); Instant instant = clock.instant(); System.out.println(instant.toString()); LocalDate localDate = LocalDate.now(); System.out.println(localDate.toString()); LocalTime localTime = LocalTime.now(); System.out.println(localTime.toString()); LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime.toString()); ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); System.out.println(zonedDateTime.toString()); }} CompletableFutureFuture是Java 5添加的类,用来描述一个异步计算的结果,但是获取一个结果时方法较少,要么通过轮询isDone,确认完成后,调用get()获取值,要么调用get()设置一个超时时间。但是这个get()方法会阻塞住调用线程,这种阻塞的方式显然和我们的异步编程的初衷相违背。为了解决这个问题,JDK吸收了guava的设计思想,加入了Future的诸多扩展功能形成了CompletableFuture。 请看: CompletableFuture 详解 总结Java8 的新特性给开发带来了很大的便利性,ZStack中用到了很多Java8的新特性。以前看Java8的新特性都是一知半解,因为没有在项目中应用到,当使用过以后,你会发现理解起来也是挺容易的。 实践才是最快的学习方式]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[zstack的自动化测试]]></title>
<url>%2F2018%2F11%2F01%2Fzstack%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8C%96%E6%B5%8B%E8%AF%95%2F</url>
<content type="text"><![CDATA[把简单的事情做得出人意料的精彩 概述zstack是什么?zstack是一个IaaS软件,可以通过API灵活的管理存储,网络,路由,计算,KVM等资源。什么是自动化测试?我们可以简单的理解为前期通过人工编码完成框架,后期来解放人力并自动完成规定的测试。在测试人员测试之前,开发人员已经完成很大部分的功能测试,测试人员只需要调用后者运行代码就可以看到测试结果。具体实现是:在自动化测试框架下,开发人员对自己负责的模块要达到基本覆盖测试代码。 zstack的Integration Test框架作为产品型的IaaS项目,ZStack非常重视测试,我们要求每个功能、用户场景都有对应的测试用例覆盖。ZStack的测试有多种维度,本文介绍后端Java开发人员使用的基于模拟器的Integration Test框架。 ZStack的运行过程中,实际上是管理节点进程(Java编写)通过HTTP PRC调用控制部署在数据中心各物理设备上的Agent(Python或Golang编写),如下图:在Integreation Test中,我们用模拟器(通过内嵌的Jetty Server)实现所有Agent HTTP RPC接口,每个用例的JVM进程就是一个自包含的ZStack环境,如图: 实例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159class OneVmBasicLifeCycleCase extends SubCase { EnvSpec env def DOC = """test a VM's start/stop/reboot/destroy/recover operations """ @Override void setup() { spring { sftpBackupStorage() localStorage() virtualRouter() securityGroup() kvm() } } @Override void environment() { env = OneVmBasicEnv.env() } @Override void test() { env.create { testStopVm() testStartVm() testRebootVm() testDestroyVm() testRecoverVm() } } void testRecoverVm() { VmSpec spec = env.specByName("vm") VmInstanceInventory inv = recoverVmInstance { uuid = spec.inventory.uuid } assert inv.state == VmInstanceState.Stopped.toString() // confirm the vm can start after being recovered testStartVm() } void testDestroyVm() { VmSpec spec = env.specByName("vm") KVMAgentCommands.DestroyVmCmd cmd = null env.afterSimulator(KVMConstant.KVM_DESTROY_VM_PATH) { rsp, HttpEntity<String> e -> cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.DestroyVmCmd.class) return rsp } destroyVmInstance { uuid = spec.inventory.uuid } assert cmd != null assert cmd.uuid == spec.inventory.uuid VmInstanceVO vmvo = dbFindByUuid(cmd.uuid, VmInstanceVO.class) assert vmvo.state == VmInstanceState.Destroyed } void testRebootVm() { // reboot = stop + start VmSpec spec = env.specByName("vm") KVMAgentCommands.StartVmCmd startCmd = null KVMAgentCommands.StopVmCmd stopCmd = null env.afterSimulator(KVMConstant.KVM_STOP_VM_PATH) { rsp, HttpEntity<String> e -> stopCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StopVmCmd.class) return rsp } env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity<String> e -> startCmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) return rsp } VmInstanceInventory inv = rebootVmInstance { uuid = spec.inventory.uuid } assert startCmd != null assert startCmd.vmInstanceUuid == spec.inventory.uuid assert stopCmd != null assert stopCmd.uuid == spec.inventory.uuid assert inv.state == VmInstanceState.Running.toString() } void testStartVm() { VmSpec spec = env.specByName("vm") KVMAgentCommands.StartVmCmd cmd = null env.afterSimulator(KVMConstant.KVM_START_VM_PATH) { rsp, HttpEntity<String> e -> cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StartVmCmd.class) return rsp } VmInstanceInventory inv = startVmInstance { uuid = spec.inventory.uuid } assert cmd != null assert cmd.vmInstanceUuid == spec.inventory.uuid assert inv.state == VmInstanceState.Running.toString() VmInstanceVO vmvo = dbFindByUuid(cmd.vmInstanceUuid, VmInstanceVO.class) assert vmvo.state == VmInstanceState.Running assert cmd.vmInternalId == vmvo.internalId assert cmd.vmName == vmvo.name assert cmd.memory == vmvo.memorySize assert cmd.cpuNum == vmvo.cpuNum //TODO: test socketNum, cpuOnSocket assert cmd.rootVolume.installPath == vmvo.rootVolumes.installPath assert cmd.useVirtio vmvo.vmNics.each { nic -> KVMAgentCommands.NicTO to = cmd.nics.find { nic.mac == it.mac } assert to != null: "unable to find the nic[mac:${nic.mac}]" assert to.deviceId == nic.deviceId assert to.useVirtio assert to.nicInternalName == nic.internalName } } void testStopVm() { VmSpec spec = env.specByName("vm") KVMAgentCommands.StopVmCmd cmd = null env.afterSimulator(KVMConstant.KVM_STOP_VM_PATH) { rsp, HttpEntity<String> e -> cmd = JSONObjectUtil.toObject(e.body, KVMAgentCommands.StopVmCmd.class) return rsp } VmInstanceInventory inv = stopVmInstance { uuid = spec.inventory.uuid } assert inv.state == VmInstanceState.Stopped.toString() assert cmd != null assert cmd.uuid == spec.inventory.uuid def vmvo = dbFindByUuid(cmd.uuid, VmInstanceVO.class) assert vmvo.state == VmInstanceState.Stopped } @Override void clean() { env.delete() }} ZStack的Integreation Test使用groovy编写,通过JUnit运行。运行如下命令可以执行该case: cd /root/zstack/testmvn test -Dtest=OneVmBasicLifeCycleCase 构成从代码中可以看到所有Integration Test都继承SubCase类,例如: 1class OneVmBasicLifeCycleCase extends SubCase { 并实现4个抽象函数: 1.setup:配置用例,主要用于加载运行用例需要用到的ZStack服务和组件2.environment: 构造测试环境,例如创建zone、cluster,添加host等操作3.test:执行具体测试代码4.clean:清理环境 (仅当该case在test suite中运行时执行,后文详述 测试用例运行时,上述4个函数依次执行,任何一个环节出现错误则测试终止退出(case在test suite中运行时例外)。 一般在setup中,会将依赖的Bean按需加载进来。这在前面提到过;而environment则会构建出一个环境。Grovvy对DSL支持较好,所以整个环境的构建代码可读性极强,本质上每个DSL都对应了一个Spec,而Sepc对应了一个ZStack的SDK创建调用——即XXXAction。而XXXAction则通过HTTP调用ZStack的API接口。 在平时测试中大家可能直接Build一个环境对数据库进行操作,但是这在ZStack中并不是很好的方案。一个Iaas中的资源依赖及状态变动的关系是错综复杂的,因此调用外部的API来创建资源是一个明智的选择。同时也可以测试SDK和API的行为是否是期待的。 模拟agent行为-灵活测试ZStack Integreation Test最核心功能是通过基于Jetty的模拟器模拟真实环境下物理设备上安装的agent,例如模拟物理机上安装的KVM agent。当测试的场景涉及到后端agent调用时,我们需要捕获这些HTTP请求并进行验证,也可以伪造agent返回测试API逻辑。 如果看过ZStack的Case,可以看到很多类似的方法: env.afterSimulator env.simulator env.message 这几个方法用来hook Message和HTTP Request。由于在ZStack中各个组件的通信都由Message来完成,对于Agent的请求则是统一通过HTTP来完成。这样在TestCase就可以任意模拟任何组件及agent的状态,让Case有极强的实用性——也保证了ManagentMent Node的逻辑健壮。 具体的使用方式可以参考官网zstack测试部分的讲解 与Java Web应用中MockMVC对比ZStack的SDK本质上是包装了一层HTTP Path,利用通用的协议便于开发者进行开发或测试。而在传统的Java WEB应用中,一般会通过MockMvc进行测试。其本质也是通过调用每个API的Path传参来进行测试。如下VMAgent测试代码: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697@RunWith(SpringJUnit4ClassRunner.class)@SpringBootTest@Transactionalpublic class MySqlIntegrationTests { private MockMvc mvc; @Autowired private WebApplicationContext context; @Autowired private TestCloudBus cloudBus; @Autowired private Environment env; @Autowired private Platform platform; private final String UTF8 = "UTF-8"; @Before public void setUp() { mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); } @Test public void testModifyConfig() throws Exception { final String MY_CONF = "my.conf"; final String APP_YML_FILE = "application.yml"; URL url = MyConfFileUtils.class.getClassLoader().getResource(APP_YML_FILE); Assert.assertNotNull(url); Path dirPath = Paths.get(url.toURI()).getParent(); Path currentPath = Paths.get(dirPath.toString(), MY_CONF); String v = "testInsertAndDelete-str"; LinkedHashMap<String, String> map = new LinkedHashMap<>(); map.put(MyConfConst.LOG_ERROR.getStringValue(), v); ModifyMyConfDTO dto = new ModifyMyConfDTO(); dto.setParams(map); dto.setPath(currentPath.toString()); String param = JsonUtils.objectToJson(dto); MvcResult result = mvc.perform(MockMvcRequestBuilders.put("/mysql/conf") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .content(param)) .andExpect(status().isOk()) .andReturn(); String content = result.getResponse().getContentAsString(); Result response = JsonUtils.jsonToResult(content); Assert.assertTrue(response.isSuccess()); Assert.assertNull(response.getData()); byte[] fileContent = MyConfFileUtils.getContent(); Assert.assertTrue(new String(fileContent, UTF8).contains(MyConfConst.LOG_ERROR.getStringValue())); Assert.assertTrue(new String(fileContent, UTF8).contains(v)); } @Test public void testStartMySQL() throws Exception { AtomicBoolean isIntercepterMessage = new AtomicBoolean(false); AtomicBoolean ishandleMessage = new AtomicBoolean(false); cloudBus.installIntercepter((FrontMessageIntercepter) msg -> { if (msg instanceof StartMySqlMsg) { isIntercepterMessage.set(true); } }); cloudBus.hookMessage(StartMySqlMsg.class, msg -> { StartMySqlMsg smsg = (StartMySqlMsg) msg; ishandleMessage.set(true); MsgReply reply = new MsgReply(smsg); reply.setResult(Result.createBySuccess()); cloudBus.reply(reply); return reply; }); MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/mysql/start") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); String content = result.getResponse().getContentAsString(); Result response = JsonUtils.jsonToResult(content); Assert.assertTrue(response.isSuccess()); Assert.assertTrue(isIntercepterMessage.get()); Assert.assertTrue(ishandleMessage.get()); } ................ 从代码中可以看到MockMvc,发送请求到指定路径。 12345MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/mysql/start") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); 后端接收到消息,可以hook其中的Msg,返回假的值。 为什么使用自动化测试自动化测试好处很明显: 保证软件质量,重复的活交给机器来做,避免繁琐重复的手动测试,节省人力;为重构打下良好的基础:软件内部无论如何重构,对外部请求所返回的结果不应该有所变化;保证核心类库的逻辑不遭受破坏,同时也可以作为使用的“样本”,由于没有业务逻辑的耦合,代码显得更加清楚,便于阅读;…..]]></content>
<categories>
<category>zstack</category>
</categories>
<tags>
<tag>zstack,自动化测试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Facade设计模式和六大设计原则]]></title>
<url>%2F2018%2F10%2F24%2Ffacade%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E5%92%8C%E5%85%AD%E5%A4%A7%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99%2F</url>
<content type="text"><![CDATA[生活是具体的 概述本周我在开发的时候,在补mysql主备搭建测试的时候,发现部分的代码兼容性不好,agent端代码使用的是springboot来管理的,在实际开发中是跑在VM上的,而测试的代码不能在本地运行,因为有些shell命名需要通过ssh发送到远程虚拟机上执行,而ssh连接这部分代码不太适合实际运行环境和开发环境都能测试这个要求,所以动手重构sshUtils代码。 Facade设计模式 Facade设计模式:定义了一个高层、统一的接口,外部与通过这个统一的接口对子系统中的一群接口进行访问。通过创建一个统一的类,用来包装子系统中一个或多个复杂的类,客户端可以通过调用外观类的方法来调用内部子系统中所有方法。 使用Facade设计模式原因1.实现客户类与子系统类的松耦合2.降低原有系统的复杂度3.提高了客户端使用的便捷性,使得客户端无须关心子系统的工作细节,通过外观角色即可调用相关功能。 引入外观角色之后,用户只需要与外观角色交互; 用户与子系统之间的复杂逻辑关系由外观角色来实现 第一次重构首先想到的不是通过设计模式来解决,而是直接在sshUtils中修改部分代码,把sshUtils中没有的功能添加进去,并创建一个类,ip地址默认是127.0.0.1,测试的代码就可以动态的修改这个ip,而线上环境就使用默认ip。这样就可以完美的解决ip不同的情况。 可是我后面遇到了两个问题:第一:sshUtils中放入了业务代码第二:如果是oracle也需要使用这个Utils,怎么办,无法复用。 工具类和管理类的区别工具类:作为系统中通用的工具,不会牵扯到业务代码,也就是说工具类是无状态的管理类:作用某一个场景下管理工具,里面会包含很多的业务代码,也就是说管理类是有状态的 Facade模式的使用通过facade设计模式来解决上述问题,因为都是ssh操作,统一写一个外观模式,可以很好的统筹连接ssh的操作。牵扯到业务的功能代码,可以设计针对某一大业务的Impl。 facadeSsh最终代码如下: 12345678910111213141516171819202122232425262728293031323334353637public interface SshFacade { /** * 执行shell命令 * * @param cmd shell cmd * @return ShellResult * @throws IOException * @throws JSchException */ Result execShellCmd(String cmd) throws IOException, JSchException; /** * 执行RemoteHostDTO ssh * * @param vo vo * @param cmd cmd * @return Result * @throws IOException * @throws JSchException */ Result execCmdOnRemote(RemoteHostDTO vo, String cmd) throws IOException, JSchException; /** * 读取文件内容 * @param remotePath path * @return String * @throws JSchException * @throws SftpException * @throws IOException */ String readFile(String remotePath) throws JSchException, SftpException, IOException; /** * 得到userInfo * @return UserInfo */ 针对mysql业务的代码,首先想到的是继承SshFacade的实现类来设计,最后通过同事的指点,发现如果使用继承便会违反依赖倒置原则。如下代码所示: 1234567891011121314151617181920212223242526272829@Componentpublic class MySqlSshFacadeImpl extends SshFacadeImpl { private final String SPEACE = "\t"; @Autowired private SshInfo info; /** * 生成文件 * * @param remotePath path 比如:/opt/exec/my.cnf * @param map map * @throws JSchException * @throws SftpException * @throws IOException */ public void coverConfigFile(String remotePath, LinkedHashMap<String, String> map) { Session session; ChannelSftp sftp; InputStream stream; try { session = JSCH.getSession(info.getSshUser(), info.getSshIp(), info.getSshPort()); session.setPassword(info.getSshPwd()); UserInfo ui = getUserInfo(); session.setUserInfo(ui); session.connect(30000); sftp = (ChannelSftp) session.openChannel("sftp"); sftp.connect(); .................... .................... 这里直接继承SshFacadeImpl,这样会导致高层次模块依赖了低层次模块,高层模块就是调用端,低层模块就是具体实现类。通过继承的方式MySqlSshFacadeImpl便无法继承其他的功能模块,而且当SshFacade接口发生改变的时候,对MySQLSshFacade不在适用的时候,便要修改MySQL这部分的代码。如果通过接口的方式,则不会这些这部分的问题。 六大设计原则 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。 接口隔离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。 依赖反转(Dependency Inversion),或者称依赖倒置实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。 迪米特法则(Law of Demeter),类间解耦,一个类对自己依赖的类知道的越少越好。自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。无论是面向过程编程还是面向对象编程,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。 一句话概括: 单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。 注意:现代语言发展很快,很多时候并不是完全准守前面的原则。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式,Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[IO与NIO]]></title>
<url>%2F2018%2F10%2F13%2FIO%E4%B8%8ENIO%2F</url>
<content type="text"><![CDATA[成功才是成功之母 概述Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。 首先,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。 java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。 第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。 第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续工作。 java NIO组成通道 和 缓冲区 是 NIO 中的核心对象,几乎在每一个 I/O 操作中都要使用它们。通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。 什么是缓冲区Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 缓冲区类型最常用的缓冲区类型是 ByteBuffer。一个 ByteBuffer 可以在其底层字节数组上进行 get/set 操作(即字节的获取和设置)。ByteBuffer 不是 NIO 中唯一的缓冲区类型。事实上,对于每一种基本 Java 类型都有一种缓冲区类型: ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer 什么是通道Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流。正如前面提到的,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。 通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。 NIO中的读写读和写是 I/O 的基本过程。从一个通道中读取很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。如下示例:我们以读取一个文件内容为示例,从文件中读文件需要三步: (1) 从 FileInputStream 获取 Channel(2) 创建 Buffer (3) 将数据从 Channel 读到Buffer 中。 代码: 1234567//从 FileInputStream 获取通道FileInputStream fin = new FileInputStream( "fan.txt" );FileChannel fc = fin.getChannel();//创建缓冲区ByteBuffer buffer = ByteBuffer.allocate( 1024 );//将数据从通道读到缓冲区fc.read( buffer ); 写入文件过程类似:12345678//从 FileOutputStream 获取通道FileOutputStream fout = new FileOutputStream( "kai.txt" );FileChannel fc = fout.getChannel();//创建缓冲区ByteBuffer buffer = ByteBuffer.allocate( 1024 );buffer.flip();//将数据从通道读到缓冲区fc.write( buffer ); 上面两个例子是读与写进行分开的,下面介绍读写结合:三个基本操作:首先创建一个 Buffer,然后从源文件中将数据读到这个缓冲区中,然后将缓冲区写入目标文件。这个程序不断重复 ― 读、写、读、写 ― 直到源文件结束。读写结合操作会使用到使用clear() 和 flip() 方法重设缓冲区,使它可以接受读入的数据。flip()方法让缓冲区可以将新读入的数据写入另一个通道。在从输入通道读入缓冲区之前,我们调用clear() 方法。同样,在将缓冲区写入输出通道之前,我们调用flip() 方法 12345678910111213141516FileInputStream fin = new FileInputStream( "fan.txt" );FileChannel fcin = fin.getChannel();FileOutputStream fout = new FileOutputStream( "kai.txt" );FileChannel fcout = fout.getChannel();ByteBuffer buffer = ByteBuffer.allocate( 1024 );while (true) { buffer.clear(); int r = fcin.read( buffer ); if (r==-1) { break; } buffer.flip(); fcout.write( buffer );} 缓冲区详解本节将介绍 NIO 中两个重要的缓冲区组件:状态变量和访问方法 (accessor)。状态变量是前一节中提到的”内部统计机制”的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。在从通道读取数据时,数据被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要查看数据。这是使用 访问方法 get() 来完成的。同样,如果要将原始数据放入缓冲区中,就要使用访问方法 put()。 状态变量三个值指定缓冲区在任意时刻的状态: position limit capacity Position缓冲区实际上就是美化了的数组。在从通道读取时,您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。 Limitlimit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。position 总是小于或者等于 limit。 Capacity缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。limit 决不能大于 capacity。 状态变量的变化出于本例子的需要,我们假设这个缓冲区的 总容量 为8个字节。那么一开始的时候,limit和capacity应该在同一位置,都只想数组的尾部,如图:position 设置为0。如果我们读一些数据到缓冲区中,那么下一个读取的数据就进入 slot 0 。如果我们从缓冲区写一些数据,从缓冲区读取的下一个字节就来自 slot 0 。 position 设置如下所示:第一次读取:现在我们可以开始在新创建的缓冲区上进行读/写操作。首先从输入通道中读一些数据到缓冲区中。第一次读取得到三个字节。它们被放到数组中从 position 开始的位置,这时 position 被设置为 0。读完之后,position 就增加到 3,如下所示:第二次读取:在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。这两个字节储存在由 position 所指定的位置上, position 因而增加 2: 将数据写到输出通道中:在这之前,我们必须调用 flip() 方法。这个方法做两件非常重要的事: 1.它将 limit 设置为当前 position。2.它将 position 设置为 0。 下面是在 flip 之后的缓冲区:我们现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设置为原来的 position。 写入:limit在我们调用 flip() 时被设置为 5,并且 position 不能超过 limit。一次读取五个字节时,这使得 position 增加到 5,并保持 limit 不变,如下所示: clear:最后一步是调用缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。 Clear 做两种非常重要的事情: 1.它将 limit 设置为与 capacity 相同。2.它设置 position 为 0。 下图显示了在调用 clear() 后缓冲区的状态: 访问方法到目前为止,我们只是使用缓冲区将数据从一个通道转移到另一个通道。然而,程序经常需要直接处理数据。例如,您可能需要将用户数据保存到磁盘。在这种情况下,您必须将这些数据直接放入缓冲区,然后用通道将缓冲区写入磁盘。或者,您可能想要从磁盘读取用户数据。在这种情况下,您要将数据从通道读到缓冲区中,然后检查缓冲区中的数据。在本节的最后,我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的数据。 get()方法ByteBuffer 类中有四个 get() 方法: 1.byte get();2.ByteBuffer get( byte dst[] );3.ByteBuffer get( byte dst[], int offset, int length );4.byte get( int index ); 第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this 值。 put()方法ByteBuffer 类中有五个 put() 方法: 1.ByteBuffer put( byte b );2.ByteBuffer put( byte src[] );3.ByteBuffer put( byte src[], int offset, int length );4.ByteBuffer put( ByteBuffer src );5.ByteBuffer put( int index, byte b ); 第一个方法 写入(put) 单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将数据从一个给定的源 ByteBuffer 写入这个 ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置 。那些返回 ByteBuffer 的方法只是返回调用它们的缓冲区的 this值。 关于缓存区的更多内容这里就不多介绍了,可以网上搜一些关于缓冲区的内容。 使用NIO进行网络连接以前实现socket服务器方式为: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051public class DemoServer extends Thread { private ServerSocket serverSocket; public int getPort() { return serverSocket.getLocalPort(); } public void run() { try { serverSocket = new ServerSocket(0); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); requestHandler.start(); } } catch (IOException e) { e.printStackTrace(); } finally { if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } ; } } } public static void main(String[] args) throws IOException { DemoServer server = new DemoServer(); server.start(); try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println(s)); } } }// 简化实现,不做读取,直接发送字符串class RequestHandler extends Thread { private Socket socket; RequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try (PrintWriter out = new PrintWriter(socket.getOutputStream());) { out.println("Hello world!"); out.flush(); } catch (Exception e) { e.printStackTrace(); } } } 在接收到客户端请求之前,服务器端都是阻塞的,如果是单线程的,每次只能处理单个请求,虽然可以通过加入线程池的方式解决,但是连接数不是很多时,这种方式可以工作的很好,但是如果连接数量急剧上升,这种方式就无法很好的工作了。因为线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。 为什么使用NIO连网关于这点不得不说,NIO的特性之一:多路复用机制。首先我们先了解下面几个概念: SelectorsNIO中连网核心对象名为Selector,Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。Selector是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。我们需要做的第一件事就是创建一个 Selector: Selector selector = Selector.open(); 然后,我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。 ServerSocketChannel为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示: 1234ServerSocketChannel ssc = ServerSocketChannel.open();ssc.configureBlocking( false );InetSocketAddress address = new InetSocketAddress( ports[i] );ss.bind( address ); 第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法。 选择键下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示: SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT ); register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。 请注意对 register() 的调用的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。 下面将进入主循环。使用 Selectors 的几乎每个程序都像下面这样使用内部循环: 12345678int num = selector.select();Set selectedKeys = selector.selectedKeys();Iterator it = selectedKeys.iterator();while (it.hasNext()) { SelectionKey key = (SelectionKey)it.next(); // ... deal with I/O event ...} 首先,我们调用 Selector 的select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。 完整示例123456789101112131415161718192021222324252627282930313233343536public class NIOServer extends Thread { public void run() { try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建 Selector 和 Channel serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888)); serverSocket.configureBlocking(false); // 注册到 Selector,并说明关注点 serverSocket.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select();// 阻塞等待就绪的 Channel,这是关键点之一 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); // 生产系统中一般会额外进行就绪状态检查 sayHelloWorld((ServerSocketChannel) key.channel()); iter.remove(); } } } catch (IOException e) { e.printStackTrace(); } } private void sayHelloWorld(ServerSocketChannel server) throws IOException { try (SocketChannel client = server.accept();) { client.write(Charset.defaultCharset().encode("Hello world!")); } } public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.start(); try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println(s)); } }} 步骤总结: 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出IllegalBlockingModeException 异常。Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。 NIO与IO相比有什么优点先看张图: NIO利用单线程轮询事件的机制,通过高效地定位就绪的Channel,来决定做什么,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁切换带来的问题,应用的扩展能力有了非常大的提高。 总结在 Java 编程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对象。NIO 与原来的 I/O 有同样的作用和目的,但是它使用不同的方式 块 I/O。正如您将在本教程中学到的,块 I/O 的效率可以比流 I/O 高许多。 NIO还有很多新的功能,比如文件的拷贝,零拷贝的特性等。这些功能放在下篇介绍。 补充-文件拷贝Java有多种比较典型的文件拷贝实现方式,比如: 利用java.io类库,比如:FileInputStream读取 利用java.nio类库提供的transferTo或transferFrom方法实现(零拷贝:zero-copy) java标准类库本身已经提供了几种Files.copy的实现 对于Copy的效率,这个其实与操作系统和配置等情况相关,总体上来说,NIO transferTo/From的方式更快,因为它更能利用现在操作系统底层机制,避免不必要拷贝和上下文切换。 拷贝实现机制分析先来理解一下,前面实现的不同拷贝方法,本质上有什么明显的区别。 首先,你需要理解用户态空间(User Space)和内核态空间(Kernel Space),这是操作系统层面的基本概念,操作系统内核,硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。 当我们使用输入输出流进行读写时,实际上是进行了多少上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。写入操作也是类似,仅仅是步骤相反,你可以参考下面这张图:所以,这种方式会带来一定的额外开销,可能会降低IO效率。 而基于NIO transferTo的实现方式,在Linux和Unix上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行Socket发送,同样可以享受这种机制带来的性能和扩展性提高。 transferTo 的传输过程是: 示例代码如下: 12345678910111213141516public static void copyFileByChannel(File source, File dest) throws IOException { try (FileChannel sourceChannel = new FileInputStream(source) .getChannel(); FileChannel targetChannel = new FileOutputStream(dest).getChannel ();){ for (long count = sourceChannel.size() ;count>0 ;) { long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel); sourceChannel.position(sourceChannel.position() + transferred); count -= transferred; } } }]]></content>
<categories>
<category>NIO</category>
</categories>
<tags>
<tag>NIO,IO</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java枚举]]></title>
<url>%2F2018%2F09%2F15%2Fjava%E6%9E%9A%E4%B8%BE%2F</url>
<content type="text"><![CDATA[概述 所有的无能好像都来自于自我否定的借口 枚举在开发中用到的地方颇多,今天就来总结一下。 Enum定义Enum是java 1.5引入的,枚举的引入给开发带来很大的便利性。在没有Enum之前,我们定义一些常量时会用如下的代码: 1234567public interface Constant { String CHECK_CODE = "check"; String DELETE_CODE = "delete"; String FORCE_DELETE_CODE = "forceDelete"; String CLEANUP_CODE = "cleanup"; .....} 这样的代码我们一般称之为常量接口(constant interface)——这种接口不包含任何方法,它只包含静态的final域,每个域都导出一个常量。接口的定义,是为了通过实现,来表名某个类需要具体实现具体功能的细节,表明客户端对这个类的实例可以实现某些动作,为了其他的目的去定义接口不是接口设计的初衷。常量接口是对接口的一种不良使用。 还有偶尔会在项目中看到如下风格的代码:1234567891011public class DayDemo { public static final int MONDAY =1; public static final int TUESDAY=2; public static final int WEDNESDAY=3; public static final int THURSDAY=4; public static final int FRIDAY=5; public static final int SATURDAY=6; public static final int SUNDAY=7;} 这样的代码一般被叫做int枚举模式。 类在内部使用某些常量,纯粹是实现细节,实现常量接口,会导致把这样的实现细节泄露到该类的导出API中,因为接口中所有的域都是及方法public的。类实现常量接口,这对于这个类的用户来讲并没有实际的价值。那既然不适合存在全部都是导出常量的常量接口,那么如果需要导出常量,它们应该放在哪里呢?如果这些常量与某些现有的类或者接口紧密相关,就应该把这些常量添加到这个类或者接口中,注意,这里说添加到接口中并不是指的常量接口。在Java平台类库中所有的数值包装类都导出MIN_VALUE和MAX_VALUE常量。如果这些常量最好被看作是枚举类型成员,那就应该用枚举类型来导出。否则,应该使用不可实例化的工具类来导出这些常量。 使用enum定义常量,如下定义周一到周日的常量:12345//枚举类型,使用关键字enumenum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY} 枚举实现原理实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。看看反编译Day.class文件: 12345678910111213141516171819202122232425262728293031323334353637383940414243//反编译Day.classfinal class Day extends Enum{ //编译器为我们添加的静态的values()方法 public static Day[] values() { return (Day[])$VALUES.clone(); } //编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法 public static Day valueOf(String s) { return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s); } //私有构造函数 private Day(String s, int i) { super(s, i); } //前面定义的7种枚举实例 public static final Day MONDAY; public static final Day TUESDAY; public static final Day WEDNESDAY; public static final Day THURSDAY; public static final Day FRIDAY; public static final Day SATURDAY; public static final Day SUNDAY; private static final Day $VALUES[]; static { //实例化枚举实例 MONDAY = new Day("MONDAY", 0); TUESDAY = new Day("TUESDAY", 1); WEDNESDAY = new Day("WEDNESDAY", 2); THURSDAY = new Day("THURSDAY", 3); FRIDAY = new Day("FRIDAY", 4); SATURDAY = new Day("SATURDAY", 5); SUNDAY = new Day("SUNDAY", 6); $VALUES = (new Day[] { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }); }} 从反编译的代码可以看出编译器确实帮助我们生成了一个Day类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类(稍后我们会分析该类中的主要方法),除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期,这也充分说明了我们前面使用关键字enum定义的Day类型中的每种日期枚举常量也是实实在在的Day实例对象,只不过代表的内容不一样而已。注意编译器还为我们生成了两个静态方法,分别是values()和 valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。 枚举的进阶用法重新定义一个日期枚举类,带有desc成员变量描述该日期的对于中文描述,同时定义一个getDesc方法,返回中文描述内容,自定义私有构造函数,在声明枚举实例时传入对应的中文描述,代码如下: 1234567891011121314151617181920212223242526public enum Day2 { MONDAY("星期一"), TUESDAY("星期二"), WEDNESDAY("星期三"), THURSDAY("星期四"), FRIDAY("星期五"), SATURDAY("星期六"), SUNDAY("星期日");//记住要用分号结束 private String desc;//中文描述 /** * 私有构造,防止被外部调用 * @param desc */ private Day2(String desc){ this.desc=desc; } /** * 定义方法,返回描述,跟常规类的定义没区别 * @return */ public String getDesc(){ return desc; } 从上述代码可知,在enum类中确实可以像定义常规类一样声明变量或者成员方法。 关于StringValue的较佳实践1234567891011121314151617181920212223242526272829303132public enum SourceDiskType { SYSTEM("system"), DATA("data"),; private String stringValue; SourceDiskType(String stringValue) { setStringValue(stringValue); } public String getStringValue() { return stringValue; } public void setStringValue(String stringValue) { this.stringValue = stringValue; } public static SourceDiskType getEnum(String stringValue) { if (null == stringValue) { return null; } for (SourceDiskType sourceDiskType : SourceDiskType.values()) { if (sourceDiskType.getStringValue().equals(stringValue)) { return sourceDiskType; } } return null; }} 这是阿里云早期版本SDK中的一段代码。]]></content>
<tags>
<tag>Java,Enum</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Spring小记]]></title>
<url>%2F2018%2F09%2F09%2FSpring%E5%B0%8F%E8%AE%B0%2F</url>
<content type="text"><![CDATA[人生而迷茫吗? 概述学了Spring框架那么久,也没有很好总结过,很长时间不用有马上都忘了,今天来总结一下。无论什么框架,一切皆源于Java最底层的设计。 为什么使用SpringSpring是最近最火的框架,是快速开发项目最不可或缺的。Spring主要两个有功能为我们的业务对象管理提供了非常便捷的方法: DI(Dependency Injection,依赖注入) AOP(Aspect Oriented Programming,面向切面编程) Java Bean每一个类实现了Bean的规范才可以由Spring来接管,那么Bean的规范是什么呢? 必须是个公有(public)类 有无参构造函数 用公共方法暴露内部成员属性(getter,setter) 实现这样规范的类,被称为Java Bean。即是一种可重用的组件。 依赖注入(DI)简单来说,一个系统中可能会有成千上万个对象。如果要手工维护它们之间的关系,这是不可想象的。我们可以在Spring的XML文件描述它们之间的关系,由Spring自动来注入它们——比如A类的实例需要B类的实例作为参数set进去。 以前这种管理对象的方式称为:Inversion of Control, 简称IoC。 但是IoC这个词不能让人更加直观和清晰的理解背后所代表的含义, 于是Martin Flower先生就创造了一个新词 : 依赖注入 (Dependency Injection,简称DI)。 通过Spring容器管理的对象默认是单例的,基本过程是: 解析xml或通过注解, 获取各种元素 通过Java反射把各个bean 的实例创建起来。 通过Java反射调用类的两个方法:setter或通过构造器把实例注入进来 其实Spring的处理方式和上面说的非常类似, 当然Spring 处理了更多的细节,例如不仅仅是setter方法注入, 还可以构造函数注入,init 方法, destroy方法等等, 基本思想是一致的。 既然对象的创建过程和装配过程都是Spring做的, 那Spring 在这个过程中就可以玩很多把戏了, 比如对你的业务类做点字节码级别的增强, 搞点AOP什么的, 这都不在话下了。 面向切面编程(AOP)在分布式开发中,把不同的模块进行解耦分离,但是分解以后就会发现有些很有趣的东西, 这些东西是通用的,或者是跨越多个模块的: 日志: 对特定的操作输出日志来记录安全:在执行操作之前进行操作检查性能:要统计每个方法的执行时间事务:方法开始之前要开始事务,结束后要提交或者回滚事务等等…. 这些可以称为是非功能需求, 但他们是多个业务模块都需要的, 是跨越模块的, 把他们放到什么地方呢?就以日志系统为例。在执行某个操作前后都需要输出日志,如果手工加代码,那简直太可怕了。而且等代码庞大起来,也是非常难维护的一种情况。最简单的办法就是把这些通用模块的接口写好, 让程序员在实现业务模块的时候去调用就可以了。 也许你会想到通过以下两种设计模式可以暂时解决: 模版方法 装饰者模式 模版方法用设计模式在某些情况下可以部分解决上面的问题,如模版方法伪代码如下:以订单管理和支付管理为例: 12345678910111213141516171819202122232425262728293031323334public abstract class BaseCommand { public void execute(){ Logger logger = Logger.getLog(xxx); //记录日志 logger.debug("xxx"); //权限检查 PerformanceUtil.startTimer(xxx); //开始事务 beginTransaction(); //这是一个需要子类实现的抽象方法 doBusiness(); commitTransaction(); PerformanceUtil.endTimer(); } public abstract void doBusiness();}class PlaceOrderCommand extends BaseCommand{ @Override public void doBusiness(){ //执行订单逻辑 }}class PaymentCommand extends BaseCommand{ @Override public void doBusiness(){ //执行支付逻辑 }} 在父类(BaseCommand)中已经把那些“乱七八糟“的非功能代码都写好了, 只是留了一个口子(抽象方法doBusiness())让子类去实现。 子类变的清爽, 只需要关注业务逻辑就可以了。调用也很简单,例如:BaseCommand cmd = … 获得PlaceOrderCommand的实例…cmd.execute(); 缺点: 这样方式的巨大缺陷就是父类会定义一切: 要执行哪些非功能代码, 以什么顺序执行等等 子类只能无条件接受,完全没有反抗余地。 如果有个子类, 根本不需要事务, 但是它也没有办法把事务代码去掉。 装饰者模式如果利用装饰者模式, 针对上面的问题,可以带来更大的灵活性:如下代码所示: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950public interface Command { void execute();}public class LoggerDecorate implements Command { Command cmd; public LoggerDecorate(Command cmd) { this.cmd = cmd; } @Override public void execute() { //记录日志 logger.debug(xxxx); this.cmd.execute(); }}public class PerformanceDecorator implements Command { Command cmd; public PerformanceDecorator(Command cmd) { this.cmd = cmd; } @Override public void execute() { PerformanceUtil.startTimer(xxx); this.cmd.execute(); PerformanceUtil.endTimer(xxx); }}class PlaceOrderCommand extends BaseCommand{ @Override public void doBusiness(){ //执行订单逻辑 }}class PaymentCommand extends BaseCommand{ @Override public void doBusiness(){ //执行支付逻辑 }} 缺点: 如果仔细思考一下就会发现装饰者模式的不爽之处:(1) 一个处理日志/性能/事务 的类为什么要实现 业务接口(Command)呢?(2) 如果别的业务模块,没有实现Command接口,但是也想利用日志/性能/事务等功能,该怎么办呢? 最好把日志/安全/事务这样的代码和业务代码完全隔离开来,因为他们的关注点和业务代码的关注点完全不同 ,他们之间应该是正交的,他们之间的关系应该是这样的: 如果把这个业务功能看成一层层面包的话, 这些日志/安全/事务 像不像一个个“切面”(Aspect) ?如果我们能让这些“切面“能和业务独立, 并且能够非常灵活的“织入”到业务方法中, 那就实现了面向切面编程(AOP)! 实现AOP在用代码使用AOP之前,我们先了解其中的一些名词概念: 通知(Advice)通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只在方法抛出异常时调用?Spring切面可以应用5种类型的通知: 前置通知(Before):在目标方法被调用之前调用通知功能; 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么; 返回通知(After-returning):在目标方法成功执行之后调用通知; 异常通知(After-throwing):在目标方法抛出异常后调用通知; 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。 注解 通知 @After 通知方法会在目标方法返回或抛出异常后调用 @Before 通知方法会在目标方法调用之前执行 @AfterReturning 通知方法会在目标方法返回后调用 @AfterThrowing 通知方法会在目标方法抛出异常后调用 @Around 通知方法会将目标方法封装起来 连接点(Join point)连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。(被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器) 切点(Pointcut)如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处” 。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。(对连接点进行拦截的定义)。 切面(Aspect)通知+切点=切面 引入(Introduction)引入允许我们向现有的类添加新方法或属性 织入(Weaving)织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入: 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。 Spring对AOP的支持: 1.基于代理的经典Spring AOP;2.纯POJO切面(4.x版本需要XML配置);3.@AspectJ注解驱动的切面;4.注入式AspectJ切面(适用于Spring各版本)。 前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。也就是说,AspectJ才是王道。 XML中声明切面 AOP配置元素 用途 <aop:advisor> 定义AOP通知器 <aop:after> 定义AOP后置通知(不管被通知的方法是否执行成功) <aop:after-returning> 定义AOP返回通知 <aop:after-throwing> 定义AOP异常通知 <aop:around> 定义AOP环绕通知 <aop:aspect> 定义一个切面 <aop:aspectj-autoproxy> 启用@AspectJ注解驱动的切面 <aop:before> 定义一个AOP前置通知 <aop:config> 顶层的AOP配置元素。大多数的<aop:*>元素必须包含在<aop:config>元素内 <aop:declare-parents> 以透明的方式为被通知的对象引入额外的接口 <aop:pointcut> 定义一个切点 示例: 123public interface Performance(){ public void perform();} 现在来写一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用。 123456execution(* concert.Performance.perform(..))//execution:在方法执行时触发//*:返回任意类型//concert.Performance:方法所属类//perform:方法名//(..):使用任意参数 1234567891011121314public class Audience{ public void silenceCellPhones(){ System.out.println("Silencing cell phones"); } public void taskSeats(){ System.out.println("Talking seats"); } public void applause(){ System.out.println("CLAP CLAP CLAP!!!"); } public void demandRefund(){ System.out.println("Demanding a refund"); }} 通过XML将无注解的Audience声明为切面: 12345678910111213141516<aop:config> <aop:aspect ref="audience"> <aop:before pointcut ="execution(** concert.Performance.perform(..))" method="sillenceCellPhones"/> <aop:before pointcut ="execution(** concert.Performance.perform(..))" method="taskSeats"/> <aop:after-returning pointcut ="execution(** concert.Performance.perform(..))" method="applause"/> <aop:After-throwing pointcut ="execution(** concert.Performance.perform(..))" method="demanRefund"/> </aop:aspect></aop:config> 关于Spring BeanSpring的ioc容器功能非常强大,负责Spring的Bean的创建和管理等功能。而Spring 的bean是整个Spring应用中很重要的一部分,了解Spring Bean的生命周期对我们了解整个spring框架会有很大的帮助。BeanFactory和ApplicationContext是Spring两种很重要的容器,前者提供了最基本的依赖注入的支持,而后者在继承前者的基础进行了功能的拓展,例如增加了事件传播,资源访问和国际化的消息访问等功能。 Bean的生命周期首先看下生命周期图: 再谈生命周期之前有一点需要先明确: Spring 只帮我们管理单例模式 Bean 的完整生命周期,对于 prototype 的 bean ,Spring 在创建好交给使用者之后则不会再管理后续的生命周期。 如你所见,在bean准备就绪之前,bean工厂执行了若干启动步骤。我们对图进行详细描述: 实例化 bean 对象,类似于 new XXObject() 将配置文件中配置的属性填充到刚刚创建的 bean 对象中。 检查 bean 对象是否实现了 Aware 一类的接口,如果实现了则把相应的依赖设置到 bean 对象中。比如如果 bean 实现了 BeanFactoryAware 接口,Spring 容器在实例化bean的过程中,会将 BeanFactory 容器注入到 bean 中。 调用 BeanPostProcessor 前置处理方法,即 postProcessBeforeInitialization(Object bean, String beanName)。 检查 bean 对象是否实现了 InitializingBean 接口,如果实现,则调用 afterPropertiesSet 方法。或者检查配置文件中是否配置了 init-method 属性,如果配置了,则去调用 init-method 属性配置的方法。 调用 BeanPostProcessor 后置处理方法,即 postProcessAfterInitialization(Object bean, String beanName)。我们所熟知的 AOP 就是在这里将 Adivce 逻辑织入到 bean 中的。 注册 Destruction 相关回调方法。 bean 对象处于就绪状态,可以使用了。 应用上下文被销毁,调用注册的 Destruction 相关方法。如果 bean 实现了 DispostbleBean 接口,Spring 容器会调用 destroy 方法。如果在配置文件中配置了 destroy 属性,Spring 容器则会调用 destroy 属性对应的方法。 上述流程从宏观上对 Spring 中 singleton 类型 bean 的生命周期进行了描述,接下来说说所上面流程中的一些细节问题。先看流程中的第二步 – 设置对象属性,在这一步中,对于普通类型的属性,例如 String,Integer等,比较容易处理,直接设置即可。但是如果某个 bean 对象依赖另一个 bean 对象,此时就不能直接设置了。Spring 容器首先要先去实例化 bean 依赖的对象,实例化好后才能设置到当前 bean 中。大致流程如下: 上面图片描述的依赖比较简单,就是 BeanA 依赖 BeanB。现在考虑这样一种情况,BeanA 依赖 BeanB,BeanB 依赖 BeanC,BeanC 又依赖 BeanA。三者形成了循环依赖,如下所示: 对于这样的循环依赖,根据依赖注入方式的不同,Spring 处理方式也不同。如果依赖靠构造器方式注入,则无法处理,Spring 直接会报循环依赖异常。这个理解起来也不复杂,构造 BeanA 时需要 BeanB 作为构造器参数,此时 Spring 容器会先实例化 BeanB。构造 BeanB 时,BeanB 又需要 BeanC 作为构造器参数,Spring 容器又不得不先去构造 BeanC。最后构造 BeanC 时,BeanC 又依赖 BeanA 才能完成构造。此时,BeanA 还没构造完成,BeanA 要等 BeanB 实例化好才能完成构造,BeanB 又要等 BeanC,BeanC 等 BeanA。这样就形成了死循环,所以对于以构造器注入方式的循环依赖是无解的,Spring 容器会直接报异常。对于 setter 类型注入的循环依赖则可以顺利完成实例化并依次注入。为什么对于setter类型注入的循环依赖可以顺利完成实例化注入呢? 请看:SpringIOC-Bean的初始化-循环依赖问题。 循环依赖问题说完,接下来 bean 实例化流程中的第7步 – 调用 BeanPostProcessor 后置处理方法。先介绍一下 BeanPostProcessor 接口,BeanPostProcessor 接口中包含了两个方法,其定义如下: 123456public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws Exception; Object postProcessAfterInitialization(Object bean, String beanName) throws Exception;} BeanPostProcessor 是一个很有用的接口,通过实现接口我们就可以插手 bean 的实例化过程,为拓展提供了可能。我们所熟知的 AOP 就是在这里进行织如入,具体点说是在 postProcessAfterInitialization(Object bean, String beanName) 执行织入逻辑的。下面就来说说 Spring AOP 织入的流程,以及 AOP 是怎样和 IOC 整合的。先说 Spring AOP 织入流程,大致如下: 查找实现了 PointcutAdvisor 类型的切面类,切面类包含了 Pointcut 和 Advice 实现类对象。 检查 Pointcut 中的表达式是否能匹配当前 bean 对象。 如果匹配到了,表明应该对此对象织入 Advice。 将 bean,bean class对象,bean实现的interface的数组,Advice对象传给代理工厂 ProxyFactory。代理工厂创建出 AopProxy 实现类,最后由 AopProxy 实现类创建 bean 的代理类,并将这个代理类返回。此时从 postProcessAfterInitialization(Object bean, String beanName) 返回的 bean 此时就不是原来的 bean 了,而是 bean 的代理类。原 bean 就这样被无感的替换掉了,是不是有点偷天换柱的感觉。 这就是AOP作用在IOC上的原因,那么AOP是怎么和IOC整合起来并协同工作的呢? AOP和IOC协同工作Spring AOP 生成代理类的逻辑是在 AbstractAutoProxyCreator 相关子类中实现的,比如 DefaultAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator 等。上面说了 BeanPostProcessor 为拓展留下了可能,这里 AbstractAutoProxyCreator 就将可能变为了现实。AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口,这样 AbstractAutoProxyCreator 可以在 bean 初始化时做一些事情。光继承这个接口还不够,继承这个接口只能获取 bean,要想让 AOP 生效,还需要拿到切面对象(包含 Pointcut 和 Advice)才行。所以 AbstractAutoProxyCreator 同时继承了 BeanFactoryAware 接口,通过实现该接口,AbstractAutoProxyCreator 子类就可拿到 BeanFactory,有了 BeanFactory,就可以获取 BeanFactory 中所有的切面对象了。有了目标对象 bean,所有的切面类,此时就可以为 bean 生成代理对象了。 引用其他博主一张继承图片: 这里简单的展示了类之间的继承关系,实际Spring的实现比这个更复杂。这里的继承做了简化。 Bean的作用域Spring定义了多种Bean作用域,可以基于这些作用域创建bean,包括: 单例(Singleton):在整个应用中,只创建bean的一个实例。 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。 会话(Session):在Web应用中,为每个会话创建一个bean实例。 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。 在默认情况下,Spring应用上下文中所有bean都是作为以单例(singleton)的形式创建的。也就是说,不管给定的一个bean被注入到其他bean多少次,每次所注入的都是同一个实例。 在大多数情况下,单例bean是很理想的方案。初始化和垃圾回收对象实例所带来的成本只留给一些小规模任务,在这些任务中,让对象保持无状态并且在应用中反复重用这些对象可能并不合理。 有时候,可能会发现,你所使用的类是易变的(mutable),它们会保持一些状态,因此重用是不安全的。在这种情况下,将class声明为单例的bean就不是什么好主意了,因为对象会被污染,稍后重用的时候会出现意想不到的问题。 声明Bean以下是声明Bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用 @Repository 在数据访问层使用 @Controller 在展现层使用(MVC -> Spring MVC)使用 在这里,可以指定bean的id名:Component("yourBeanName")同时,Spring支持将@Named作为@Component注解的替代方案。两者之间有一些细微的差异,但是在大多数场景中,它们是可以互相替换的。 补充Spring AOP之Around增强处理@Around注解用于修饰Around增强处理,Around增强处理是功能比较强大的增强处理,它近似于Before增强处理和AfterReturing增强处理的总结,Around增强处理既可在执行目标方法之前增强动作,也可在执行目标方法之后织入增强的执行。与Before增强处理、AfterReturning增强处理不同的是,Around增强处理可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标方法的执行。 当定义一个Around增强处理方法时,该方法的 ==第一个形参必须是ProceedJoinPoint类型==(至少含有一个形参),在增强处理方法体内,调用ProceedingJoinPoint参数的procedd()方法才会执行目标方法——这就是Around增强处理可以完全控制方法的执行时机、如何执行的关键;如果程序没有调用ProceedingJoinPoint参数的proceed()方法,则目标方法不会被执行。下面定义一个Around增强处理。 AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息。我们先来了解一下这两个接口的主要方法: 1)JoinPoint java.lang.Object[] getArgs():获取连接点方法运行时的入参列表; Signature getSignature() :获取连接点的方法签名对象; java.lang.Object getTarget() :获取连接点所在的目标对象; java.lang.Object getThis() :获取代理对象本身; 2)ProceedingJoinPoint ProceedingJoinPoint继承JoinPoint子接口,它新增了两个用于执行连接点方法的方法: java.lang.Object proceed() throws java.lang.Throwable:通过反射执行目标对象的连接点处的方法; java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参。 SpringAOP的概念: 切面(Aspect):切面用于组织多个Advice,Advice放在切面中定义。 连接点(Joinpoint):程序执行过程中明确的点,如方法的调用,或者异常的抛出。在Spring AOP中,连接点总是方法的调用。 增强处理(Advice):AOP框架在特定的切入点执行的增强处理。处理有“around”、“before”和“after”等类型。 切入点(Pointcut):可以插入增强处理的连接点。 自定义注解123456789101112@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface MethodLog { /** * 这个方法是什么意思 * @return */ String module() default ""; int type() default 0;} 1)@Target({ElementType.METHOD,ElementType.TYPE}) : 用于描述注解的使用范围(即:被描述的注解可以用在什么地方),其取值有: CONSTRUCTOR: 用于描述构造器。 FIELD: 用于描述域。 LOCAL_VARIABLE: 用于描述局部变量。 METHOD: 用于描述方法。 PACKAGE: 用于描述包。 PARAMETER: 用于描述参数。 TYPE: 用于描述类或接口(甚至 enum )。 2)@Retention(RetentionPolicy.RUNTIME): 用于描述注解的生命周期(即:被描述的注解在什么范围内有效),其取值有: SOURCE: 在源文件中有效(即源文件保留)。 CLASS: 在 class 文件中有效(即 class 保留)。 RUNTIME: 在运行时有效(即运行时保留)。 3)@Documented 在默认的情况下javadoc命令不会将我们的annotation生成再doc中去的,所以使用该标记就是告诉jdk让它也将annotation生成到doc中去 Sring AOP 失效,自我调用在使用AOP进行拦截的时候发现有些方法有时候能输出拦截的日志有时候不输出拦截的日志。发现在单独调用这些方法的时候是有日志输出,在被同一个类中的方法调用的时候没有日志输出。 这里先说一下AOP拦截不到自我调用方法的原因:假设我们有一个类是ServiceA,这个类中有一个A方法,A方法中又调用了B方法。当我们使用AOP进行拦截的时候,首先会创建一个ServiceA的代理类,其实在我们的系统中是存在两个ServiceA的对象的,一个是目标ServiceA对象,一个是生成的代理ServiceA对象,如果在代理类的A方法中调用代理类的B方法,这个时候AOP拦截是可以生效的,但是如果在代理类的A方法中调用目标类的B方法,这个时候AOP拦截是不生效的,大多数情况下我们都是在代理类的A方法中直接调用目标类的B方法。 示例如下:自定义注解1234567891011@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface MethodLog { /** * @return */ String module() default ""; int type() default 0;} Bean类 1234567891011121314151617181920212223public class LogAspectModel extends BaseEntity { //逻辑标识id,用来标志同一个链,id一样的为同一次任务的不同方法 private String flagId; //表示这个记录是做什么子目标,二级子目标,三级子目标,等等 private String module; //状态0正常,1不正常 private String status; //备注 private String remark; //异常原因 private String exception; //耗时 private String consumeTime; //类名 private String className; //入参 private String inParameters; //出参 private String outParameters; //方法名 private String methodName; //时间 private String time; 使用Spring AOP技术,定义切点和切面,使用@Around环绕通知: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128@Component@Aspectpublic class LogAspectAdvice { private static Logger log = LoggerFactory.getLogger(LogAspectAdvice.class); private NamedThreadLocal<String> flagId = new NamedThreadLocal<>("flag"); private AtomicInteger atomicInteger = new AtomicInteger(); @Pointcut("execution(* com.wwwarehouse.ccp..*(..))&&@annotation(MethodLog)") private void pointcut() { } @Around("pointcut()") public Object around(ProceedingJoinPoint point) throws Throwable { LogAspectModel logAspectModel = new LogAspectModel(); //开始时间 long beginTime = System.currentTimeMillis(); if (ObjectUtil.hasNull(flagId.get())) { //线程绑定变量(该数据只有当前请求的线程可见) flagId.set(beginTime + UUID.randomUUID().toString().replaceAll("-", "")); } atomicInteger.getAndIncrement(); Map<String, String> map = getMethodLog(point); //入参 logAspectModel.setInParameters(map.get("param")); logAspectModel.setModule(map.get("module")); //方法名 logAspectModel.setMethodName(map.get("methodName")); //类名 logAspectModel.setClassName(point.getTarget().getClass().toString()); Object object = null; try { object = point.proceed(); logAspectModel.setStatus("1"); logAspectModel.setRemark("成功"); } catch (Exception e) { logAspectModel.setStatus("0"); logAspectModel.setRemark("失败"); logAspectModel.setException(getExceptionTrace(e)); } //结束时间 long endTime = System.currentTimeMillis(); long time = endTime - beginTime; logAspectModel.setConsumeTime(String.valueOf(time)); logAspectModel.setTime(DateUtil.toDateTimeString(new Date())); logAspectModel.setFlagId(flagId.get()); if (!ObjectUtil.isEmpty(object)) { String simpleName = object.getClass().getSimpleName(); logAspectModel.setOutParameters("[" + simpleName + " : " + JSON.toJSONString(object) + "]"); } //日志打印 generateLog(logAspectModel); if (atomicInteger.decrementAndGet() <= 0) { flagId.remove(); } return object; } private void generateLog(LogAspectModel logAspectModel) { if (!Objects.equals(logAspectModel.getStatus(), "0")) { log.info("AOP日志:{}", logAspectModel.toString()); } else { log.error("AOP日志:{}", logAspectModel.toString()); } } /** * 获取日志注解标注信息 * * @param joinPoint * @return * @throws Exception */ public static Map<String, String> getMethodLog(ProceedingJoinPoint joinPoint) throws Exception { Map<String, String> map = new HashMap<>(); Class targetClass = joinPoint.getTarget().getClass(); String methodName = joinPoint.getSignature().getName(); map.put("methodName", methodName); Method[] method = targetClass.getMethods(); //获取参数值 Object[] args = joinPoint.getArgs(); StringBuilder sb = new StringBuilder("["); for (Method m : method) { if (!m.getName().equals(methodName)) { continue; } if (args != null) { int i = 0; for (Object arg : args) { sb.append(arg.getClass().getSimpleName()); sb.append(":"); sb.append(JSON.toJSONString(arg)); if (++i < args.length) { sb.append(","); } } } sb.append("]"); map.put("param", sb.toString()); MethodLog methodCache = m.getAnnotation(MethodLog.class); if (methodCache != null) { map.put("module", methodCache.module()); } break; } return map; } /** * 获取异常信息 * * @param e * @return */ public static String getExceptionTrace(Throwable e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); pw.flush(); pw.close(); return sw.toString(); } 测试Demo代码: 123456789101112131415161718192021222324252627282930@Servicepublic class DemoAOPImpl implements DemoAOP { private static Logger logger = LoggerFactory.getLogger(DemoAOPImpl.class); @Resource DemoAOPImpl demoAOP; public String flagId = "test"; @MethodLog(module = "ll") @Override public Integer handle(List<String> list, String s) {// ((DemoAOPImpl)AopContext.currentProxy()).demo(list.get(0),"ffff"); demo(list.get(0), "ffff"); //直接调用的时候,这个方法AOP失效 logger.info("======================"+list.get(0)); return 2; } @Override @MethodLog public Integer demo(String b, String ff) { System.out.println("b===" + b); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return 4; }} 测试:123456789101112131415public class DemoTest extends BaseTest { @Resource private DemoAOP demoAOP; @Test public void handle() { List<String> list = new ArrayList<>(); list.add("hahha"); demoAOP.handle(list,"fan"); }} 结果: 测试发现只生成了handle方法的日志demo方法AOP失效。 12handle方法日志结果:com.wwwarehouse.ccp.aop.LogAspectAdvice.generateLog(LogAspectAdvice.java:90) - AOP日志:LogAspectModel{ flagId='154726241777110862bb3174049af8b320c818b635a07', status='1', remark='成功', exception='null', className='class com.wwwarehouse.ccp.aop.DemoAOPImpl', methodName='handle', inParameter='[ArrayList:["hahha"],String:"fan"]', outParameter='[Integer : 2]', consumeTime(ms)='1426', module='ll', finishTime='2019-01-12 11:06:59'} 测试中发现无论demo方法是否加注解@MethodLog,demo方法都不能生成AOP日志,而handle方法却没有影响。 总结就是:自调用的第二个方法AOP总是失效的。 原因上面例子中,在handle()方法里面直接调用demo(……)方法,这里还隐含一个关键字,那就是this,实际上这里调用是这样的:this.demo(),this是当前对象。而调用handle()是的对象是被代理的,在代理对象中执行增强后,通过invoke,用实际DemoAOPImpl对象来调用handle()方法执行业务逻辑。在业务逻辑内又调用了demo(……)方法,调用的对象是当前对象,当前对象是DemoAOPImpl,问题就出在这里,因为要想用执行demo()方法的增强,必须用代理对象执行,但是此时却直接用DemoAOPImpl对象调用,绕过了代理对象增强的部分,也就是说代理增强部分失效。在同一个类中使用@Transaction,@Async并不能实现事务和异步,道理就是这样的。 总结在使用Spring框架中,对于源码的分析还是过少,在以后的工作中要学习Spring的代码设计。增强自己对架构的理解。]]></content>
<categories>
<category>Spring</category>
</categories>
<tags>
<tag>Spring</tag>
</tags>
</entry>
<entry>
<title><![CDATA[怕]]></title>
<url>%2F2018%2F08%2F31%2F%E6%80%95%2F</url>
<content type="text"><![CDATA[抬起头,又底下 当我遇到自己的理想和行为不一致的时候,我就会审视自己的内心,拷问自己的灵魂,你是否还记的那些理想,是否还记得曾经的誓言。 当我胆小怕事的时候,我总是问自己,你到底在害怕什么? 你和我一样背负着与众不同的秘密在活着。你的眼睛告诉我,仇恨和恐惧,那是什么样的经历造就的!而后再用杀人来掩盖这种感受吗!你敢不敢问自己,到底要去哪里!背负着恐惧寻找的终点,非要是末路吗!你能听到吗!你还能听到吗!你还有勇气直面你的恐惧吗! 每个人的发展都是首先从单维度开始的,如果一开始对单维度的技能没有达到极致,就暂时不要进行多维度的发展,不但不能起到快速进步的地步,还会影响到原来单维度的发展。 环境的改变自己才会在这个环境中学的更好。可以说环境是最影响进步的一个助力,在符合的环境中学习才会让你学有所用所得。 每个人都会鄙视自己,因为鄙视自己,所以才会想要进步,想要努力。不满足源于内心无尽的鄙视。]]></content>
<categories>
<category>所思</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JMM和volatile与synchronized]]></title>
<url>%2F2018%2F08%2F26%2FJMM%E5%92%8Cvolatile%E4%B8%8Esynchronized%2F</url>
<content type="text"><![CDATA[好的习惯可以影响人一生 概述在一开始的学习java的时候,一直认为JMM(Java Memory Model)和Java内存区域是一个东西。后面深入学习后,Java内存区域是JVM在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机。JMM则是本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。 JMM(Java Memory Model)Java内存区域就不在过多的介绍,有兴趣可以看我以前写的关于深入理解JVM的内容。在执行程序时JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的。这也是我们在并发操作,或者使用多线程执行的时候围绕这三个原则来保证数据的同步。 对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存, 总结来说一句话:方法中的局部变量是基本类型就直接操作个线程的栈中,引用变量值存在主内存中。而对象成员变量都存放在主内存中。 为什么需要JMM,它试图解决什么问题Java 是最早尝试提供内存模型的语言,这是简化多线程编程、保证程序可移植性的一个飞跃。早期类似 C、C++ 等语言,并不存在内存模型的概念(C++ 11 中也引入了标准内存模型),其行为依赖于处理器本身的内存一致性模型,但不同的处理器可能差异很大,所以一段 C++ 程序在处理器 A 上运行正常,并不能保证其在处理器 B 上也是一致的。 即使如此,最初的 Java 语言规范仍然是存在着缺陷的,当时的目标是,希望 Java 程序可以充分利用现代硬件的计算能力,同时保持“书写一次,到处执行”的能力。 但是,显然问题的复杂度被低估了,随着 Java 被运行在越来越多的平台上,人们发现,过于泛泛的内存模型定义,存在很多模棱两可之处,对 synchronized 或 volatile 等,类似指令重排序时的行为,并没有提供清晰规范。这里说的指令重排序,既可以是编译器优化行为,也可能是源自于现代处理器的乱序执行等。 换句话说: 既不能保证一些多线程程序的正确性,例如最著名的就是双检锁(Double-Checked Locking,DCL)的失效问题,双检锁可能导致未完整初始化的对象被访问,理论上这叫并发编程中的安全发布(Safe Publication)失败。 也不能保证同一段程序在不同的处理器架构上表现一致,例如有的处理器支持缓存一致性,有的不支持,各自都有自己的内存排序模型。 所以,Java 迫切需要一个完善的 JMM,能够让普通 Java 开发者和编译器、JVM 工程师,能够清晰地达成共识。换句话说,可以相对简单并准确地判断出,多线程程序什么样的执行序列是符合规范的。 volatile和synchronizedvolatile在并发编程中很常见,但也容易被滥用,现在我们就进一步分析volatile关键字的语义。volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。 禁止指令重排序优化。 volatile和synchronized特点首先需要理解线程安全的两个方面:执行控制和内存可见。执行控制的目的是控制代码执行(顺序)及是否可以并发执行。内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。 synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。 volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。在Java 5提供了原子数据类型atomic wrapper classes,对它们的increase之类的操作都是原子操作,不需要使用sychronized关键字。对于volatile关键字,当且仅当满足以下所有条件时可使用: 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。 该变量没有包含在具有其他变量的不变式中。 volatile和synchronized的区别 volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化 volatile和Atomicvolatile锁(lock)的代价锁是用来做并发最简单的方式,当然其代价也是最高的。内核态的锁的时候需要操作系统进行一次上下文切换,加锁、释放锁会导致比较多的上下文切换和调度延时,等待锁的线程会被挂起直至锁释放。在上下文切换的时候,cpu之前缓存的指令和数据都将失效,对性能有很大的损失。 操作系统对多线程的锁进行判断就像两姐妹在为一个玩具在争吵,然后操作系统就是能决定他们谁能拿到玩具的父母,这是很慢的。用户态的锁虽然避免了这些问题,但是其实它们只是在没有真实的竞争时才有效。 Java在JDK1.5之前都是靠synchronized关键字保证同步的,这种通过使用一致的锁定协议来协调对共享状态的访问,可以确保无论哪个线程持有守护变量的锁,都采用独占的方式来访问这些变量,如果出现多个线程同时访问锁,那第一些线线程将被挂起,当线程恢复执行时,必须等待其它线程执行完他们的时间片以后才能被调度执行,在挂起和恢复执行过程中存在着很大的开销。锁还存在着其它一些缺点,当一个线程正在等待锁时,它不能做任何事。如果一个线程在持有锁的情况下被延迟执行,那么所有需要这个锁的线程都无法执行下去。 如果被阻塞的线程优先级高,而持有锁的线程优先级低,将会导致优先级反转(Priority Inversion)。 与锁相比,volatile变量是一和更轻量级的同步机制,因为在使用这些变量时不会发生上下文切换和线程调度等操作,但是volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。 volatile使用准则volatile的使用是非常容易出错的,在介绍volatile之前先说两个准则,只要我们的程序能遵循它,咱就可以放心使用volatile变量来实现线程安全。 对变量的写操作不依赖于当前值。 该变量没有包含在具有其他变量的不变式中。 是不是有些头大,其实就是volatile修饰的变量独立于程序的任何状态,包括自己当前的状态。只有在其状态完全独立于程序其他状态时才可使用volatile变量。 注意:重申一下使用volatile变量的正确条件 –volatile变量必须真正独立于其他变量和其以前的值。还有并发专家也同时告诫我们:尽量远离volatile变量,除非你真正的理解其涵义和使用场景。 不要将volatile用在getAndOperate场合,仅仅set或者get的场景是适合volatile的 例如你让一个volatile的integer自增(i++),其实要分成3步:1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见。这3步的jvm指令为: 1234mov 0xc(%r10),%r8d ; Loadinc %r8d ; Incrementmov %r8d,0xc(%r10) ; Storelock addl $0x0,(%rsp) ; StoreLoad Barrier 注意最后一步是内存屏障。 什么是内存屏障(Memory Barrier)?内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。 编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。 内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道: 1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。 volatile为什么没有原子性?明白了内存屏障(memory barrier)这个CPU指令,回到前面的JVM指令:从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。所以volatile无法保证在内存屏障前的操作,有可能会发生修改。下面的测试代码可以实际测试voaltile的自增没有原子性: 12345678910111213141516171819202122232425262728293031323334private static volatile long _longVal = 0; private static class LoopVolatile implements Runnable { public void run() { long val = 0; while (val < 10000000L) { _longVal++; val++; } } } private static class LoopVolatile2 implements Runnable { public void run() { long val = 0; while (val < 10000000L) { _longVal++; val++; } } } private void testVolatile(){ Thread t1 = new Thread(new LoopVolatile()); t1.start(); Thread t2 = new Thread(new LoopVolatile2()); t2.start(); while (t1.isAlive() || t2.isAlive()) { } System.out.println("final val is: " + _longVal); } 为什么AtomicXXX具有原子性和可见性?那就不得不提CAS(比较并交换)算法。 其实AtomicLong的源码里也用到了volatile,但只是用来读取或写入,见源码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748public class AtomicInteger extends Number implements java.io.Serializable { private static final long serialVersionUID = 6214790243416807050L; // setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) { value = initialValue; } /** * Creates a new AtomicInteger with initial value {@code 0}. */ public AtomicInteger() { } /** * Gets the current value. * * @return the current value */ public final int get() { return value; } /** * Sets to the given value. * * @param newValue the new value */ public final void set(int newValue) { value = newValue; } 其CAS虚拟机指令为: 1234mov 0xc(%r11),%eax ; Loadmov %eax,%r8d inc %r8d ; Incrementlock cmpxchg %r8d,0xc(%r11) ; Compare and exchange 因为CAS是基于乐观锁的,也就是说当写入的时候,如果寄存器旧值已经不等于现值,说明有其他CPU在修改,那就继续尝试。所以这就保证了操作的原子性。 那CAS是什么?为什么能够保证操作的原子性?为什么比volatile更好? CAS无锁算法要实现无锁(lock-free)的非阻塞算法有多种实现方法,其中CAS(比较与交换,Compare and swap)是一种有名的无锁算法。CAS, CPU指令,在大多数处理器架构,包括IA32、Space中采用的都是CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。CAS无锁算法的C实现如下: 123456789int compare_and_swap (int* reg, int oldval, int newval) { ATOMIC(); int old_reg_val = *reg; if (old_reg_val == oldval) *reg = newval; END_ATOMIC(); return old_reg_val;} CAS(乐观锁算法)的假设前提CAS比较与交换的伪代码可以表示为: do{ 备份旧数据; 基于旧数据构造新数据;}while(!CAS( 内存地址,备份的旧数据,新数据 )) (上图的解释:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。) 就是指当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的 commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。 CAS的问题CAS并不是完全没有缺陷的,其中一个就是著名的ABA问题,这是通常只在 lock-free 算法下暴露的问题。 CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差类会导致数据的变化。 比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。 比如如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。这允许一对变化的元素进行原子操作。 在运用CAS做Lock-Free操作中有一个经典的ABA问题: 线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题,例如下面的例子: 现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B: head.compareAndSet(A,B); 在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态: 此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为: 其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。 以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题. 高并发环境下优化锁或无锁(lock-free)的设计思路服务端编程的3大性能杀手:1、大量线程导致的线程切换开销。2、锁(锁的代价:内核态上下文切换,大量缓存的数据会失效,挂起和恢复线程消耗大量资源,阻塞线程,CPU利用率不高)。3、非必要的内存拷贝。在高并发下,对于纯内存操作来说,单线程是要比多线程快的, 可以比较一下多线程程序在压力测试下cpu的sy和ni百分比。 高并发环境下要实现高吞吐量和线程安全,两个思路:一个是用优化的锁实现,一个是lock-free的无锁结构。但非阻塞算法要比基于锁的算法复杂得多。开发非阻塞算法是相当专业的训练,而且要证明算法的正确也极为困难,不仅和具体的目标机器平台和编译器相关,而且需要复杂的技巧和严格的测试。虽然Lock-Free编程非常困难,但是它通常可以带来比基于锁编程更高的吞吐量。所以Lock-Free编程是大有前途的技术。它在线程中止、优先级倒置以及信号安全等方面都有着良好的表现。 优化锁实现的例子:Java中的ConcurrentHashMap,设计巧妙,用桶粒度的锁和锁分离机制,避免了put和get中对整个map的锁定,尤其在get中,只对一个HashEntry做锁定操作,性能提升是显而易见的(详细分析见:《探索 ConcurrentHashMap 高并发性的实现机制》 )。 Lock-free无锁的例子:CAS(CPU的Compare-And-Swap指令)的利用和LMAX的disruptor无锁消息队列数据结构等。 结语volatile无法保证原则性。可以使用并发包提供的Atomic* 。]]></content>
<categories>
<category>线程</category>
</categories>
<tags>
<tag>JMM,volatile,synchronized</tag>
</tags>
</entry>
<entry>
<title><![CDATA[线程池]]></title>
<url>%2F2018%2F08%2F19%2F%E7%BA%BF%E7%A8%8B%E6%B1%A0%2F</url>
<content type="text"><![CDATA[最好是更好的敌人 概述多线程的软件设计可以最大限度地发挥现代多核处理器的计算能力,提高生产系列的吞吐量和性能。但是,线程过多的话,不但不能达到提高性能的目的,还会是性能严重下降,因为过多的线程是CPU忙于进行线程之间的切换,而没有时间执行其他的任务。 为了避免系统频繁地创建和销毁线程,我们可以让创建的线程进行复用。如果有同学有过数据库开发的经验,对数据库连接池这个概念应该不会陌生。为了避免每次数据库查询都重新建立和销毁数据库连接,我们可以使用数据库连接池维护一些数据库连接,使其长期保持在一个激活的状态。当系统需要使用数据库时,并不是创建一个新的连接,而是从连接池中获得一个可用的连接即可。反之,当需要关闭连接时,并不真的把连接关闭,而是将这个连接“还”给连接池即可。 为了方便我们使用线程池,jdk提供了一套Executor框架,如下图所示: 以上成员均在java.util.concurrent包中,是JDK并发包的核心类。其中ThreadPoolExecutor表示一个线程池。Executors类则扮演线程池工厂角色,通过Executors可以取得一个具有特定功能的线程池。从UML图中亦可知,ThreadPoolExecutor实现了Executor接口,因此通过这个接口,任何Runnable对象都可以被ThreadPoolExecutor线程池调度。 Java提供了ExecutorService的两种实现: ThraedPoolExecutor:标准线程池 ScheduledThreadPoolExecutor:支持延时任务的线程池 ForkJoinPool:类似于ThraedPoolExecutor,但是使用work-stealing模式,其会为线程池中的每个线程创建一个队列,从而使用work-stealing(任务窃取)算法使得线程可以从其他线程队列里窃取任务来执行。即如果自己的任务处理完成了,则可以去忙碌的工作线程那里去窃取任务执行。 Executor框架提供了各种类型的线程池,主要有以下工厂方法。 12345public static ExecutorService newFixedThreadPool(int nThreads)public static ExecutorService newSingleThreadExecutor()public static ExecutorService newCachedThreadPool()public static ScheduledExecutorService newSingleThreadScheduledExecutor()public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) Executors 目前提供了 5 种不同的线程池创建配置: newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列。 newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。 newSingleThreadExecutor(),它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。 newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。 newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。 线程池的内部实现无论是newFixedThreadPool()、newSingleThreadExecutor()还是newCacheThreadPool方法,虽然看起来创建的线程具有完全不同的功能特点,但其内部均使用了ThreadPoolExecutor实现。 12345678910public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()); }public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>()); } 由以上线程池的实现可以看到,它们都只是ThreadPoolExecutor类的封装。我们看下ThreadPoolExecutor最重要的构造函数: 123456789101112131415161718192021public ThreadPoolExecutor( //指定了线程池中的线程数量 int corePoolSize, //指定了线程池中的最大线程数量 int maximumPoolSize, //当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多次时间内会被销毁。 long keepAliveTime, //keepAliveTime的单位 TimeUnit unit, //任务队列,被提交但尚未被执行的任务。 BlockingQueue<Runnable> workQueue, //线程工厂,用于创建线程,一般用默认的即可 ThreadFactory threadFactory, //拒绝策略,当任务太多来不及处理,如何拒绝任务。 RejectedExecutionHandler handler) WorkQueueworkQueue指提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象,根据队列功能分类,在ThreadPoolExecutor的构造函数中可使用以下几种BlockingQueue。 直接提交的队列: 该功能由synchronousQueue对象提供,synchronousQueue对象是一个特殊的BlockingQueue。synchronousQueue没有容量,每一个插入操作都要等待一个响应的删除操作,反之每一个删除操作都要等待对应的插入操作。如果使用synchronousQueue,提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲线程,则尝试创建线程,如果线程数量已经达到了最大值,则执行拒绝策略,因此,使用synchronousQueue队列,通常要设置很大的maximumPoolSize值,否则很容易执行拒绝策略。 有界的任务队列: 有界任务队列可以使用ArrayBlockingQueue实现。ArrayBlockingQueue构造函数必须带有一个容量参数,表示队列的最大容量。当使用有界任务队列时,若有新任务需要执行时,如果线程池的实际线程数量小于corePoolSize,则会优先创建线程。若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。可见有界队列仅当在任务队列装满后,才可能将线程数量提升到corePoolSize以上,换言之,除非系统非常繁忙,否则确保核心线程数维持在corePoolSize。 无界的任务队列: 无界队列可以通过LinkedBlockingQueue类实现。与有界队列相比,除非系统资源耗尽,无界队列的任务队列不存在任务入队失败的情况。若有新任务需要执行时,如果线程池的实际线程数量小于corePoolSize,则会优先创建线程执行。但当系统的线程数量达到corePoolSize后就不再创建了,这里和有界任务队列是有明显区别的。若后续还有新任务加入,而又没有空闲线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,知道耗尽系统内存。 优先任务队列: 带有优先级别的队列,它通过PriorityBlokingQueue实现,可以控制任务执行的优先顺序。它是一个特殊的无界队列。无论是ArrayBlockingQueue还是LinkedBlockingQueue实现的队列,都是按照先进先出的算法处理任务,而PriorityBlokingQueue根据任务自身优先级顺序先后执行,在确保系统性能同时,也能很好的质量保证(总是确保高优先级的任务优先执行)。 线程池及增长策略和拒绝策略ThreadPoolExecutor类实现了ExecutorService接口和Executor接口,可以设置线程池corePoolSize,最大线程池大小,AliveTime,拒绝策略等。常用构造方法: 1234ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) corePoolSize: 线程池维护线程的最少数量 maximumPoolSize:线程池维护线程的最大数量 keepAliveTime: 线程池维护线程所允许的空闲时间 unit: 线程池维护线程所允许的空闲时间的单位 workQueue: 线程池所使用的缓冲队列 handler: 线程池对拒绝任务的处理策略 当一个任务通过execute(Runnable)方法欲添加到线程池时: 1 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。 2 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。 3 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。 4 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过handler所指定的策略来处理此任务。 也就是: ==处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。==当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。 handler有四个选择: 策略1:直接抛异常:ThreadPoolExecutor.AbortPolicy() 抛出java.util.concurrent.RejectedExecutionException异常 策略2:ThreadPoolExecutor.CallerRunsPolicy,用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。如下:RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy(); 这个策略不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身来执行。 策略3:RejectedExecutionHandler handler =new ThreadPoolExecutor.DiscardOldestPolicy();如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序。 策略4:ThreadPoolExecutor.DiscardPolicy,用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。不能执行的任务将被丢弃。 这四种策略是独立无关的,是对任务拒绝处理的四中表现形式。最简单的方式就是直接丢弃任务。但是却有两种方式,到底是该丢弃哪一个任务,比如 ==可以丢弃当前将要加入队列的任务本身(DiscardPolicy)== 或者 ==丢弃任务队列中最旧任务(DiscardOldestPolicy)。== 丢弃最旧任务也不是简单的丢弃最旧的任务,而是有一些额外的处理。除了丢弃任务还可以直接 ==抛出一个异常(RejectedExecutionException),== 这是比较简单的方式。抛出异常的方式(AbortPolicy)尽管实现方式比较简单,但是由于抛出一个RuntimeException,因此会中断调用者的处理过程。除了抛出异常以外还可以 ==不进入线程池执行,在这种方式(CallerRunsPolicy)== 中任务将有调用者线程去执行。 自定义线程创建:ThreadFactoryThreadFactory是一个接口,它只有一个方法,用来创建线程 1Thread newThread(Runnable r); 当线程池需要新建线程时,就会调用这个方法。 自定义线程池可以帮我们做不少事情。我们可以跟踪线程池在何时创建了多少线程,也可以自定义线程的名称、组以及优先级等信息,甚至可以任性地将所有的线程设置为守护线程。总之,使用自定义线程可以让我们更加自由地设置池中所有的线程的状态。下面的案例使用自定义ThreadFactory,一方面记录了线程的创建,另一方面将所有的线程都设置为守护线程,这样,当主线程退出后,将会强制销毁线程池。 1234567891011121314151617181920212223242526272829303132333435public class ThreadFactoryExample { public static class MyTask implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + " coming..."); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] a ) throws InterruptedException { MyTask myTask = new MyTask(); ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10) , new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("T " + t.getId() + "_" +System.currentTimeMillis()); t.setDaemon(true); System.out.println("Create a Thread Name is : "+t.getName()); return t; } }); for (int i=0;i<10;i++){ es.submit(myTask); } Thread.sleep(2000); }} 扩展线程池ThreadPoolExecutor是可扩展的,它提供了几个“钩子”方法可以在子类化中改写:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。 在执行任务的线程中将调用beforeExecute和afterExecute等方法,在这些方法中还可以添加日志、计时、监视或统计信息收集的功能。无论任务是从run中正常返回,还是抛出一个异常而返回,afterExecute都会被调用。 结语这里比较粗糙的研究了一下线程池的一些基本的概念和功能。并发的路上还有很多路要走。]]></content>
<categories>
<category>线程</category>
</categories>
<tags>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[多线程]]></title>
<url>%2F2018%2F08%2F13%2F%E7%BA%BF%E7%A8%8B%2F</url>
<content type="text"><![CDATA[概述 深感自己的能力的不足,此处仅仅是对知识的记录和总结,许多地方都是借鉴他人的语句 线程与进程的区别想要了解线程,必须首先知道进程,并知道他与线程的区别是什么? 进程通常,我们把一个程序的执行称为一个进程。反过来讲,进程用于描述程序的执行过程。因此,程序和进程是一对概念,它们分別描述了一个程序的静态和动态特征:除此之外,进程还操作系统进行资源分配的一个基本单位。 进程的衍生进程使用fork()系统调用来创建。父进程调用fork创建子进程。每个子进程都是源自它的父进程的一个副本,它会获得父进程的数据段、堆和栈的副本,并与父进程共享代码段。每一份副本都是独立的,子进程对属于它的副本的修改对其父进程和兄弟进程(同父进程)都是不可见的,反之亦然。全盘复制父进程的数据是一种相当低效的做法。 Linux操作系统内核使用写时复制(Copy on Write,常简称为COW,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程)等技术来提高进程创建的效率。当然,刚创建的子进程也可以通过系统调用exec把一个新的程序加载到自己的内存中,而原先在内存中的数据段、堆、栈以及代码段就会被替换掉,在这之后,子进程执行的就会是那个刚刚加载进来的新程序(意思就是当传入的参数或者变量不同的时候,子进程就相当于在做自己的事情)。 父进程被如果优先于子进程结束,那么子进程就会被原来父进程的父进程“收养”(也就是子进程的爷爷)。 为了管理进程,内核必须对每个进程的数据和行为进行详细的记录,包括进程的优先级、状态、虚拟地址范围以及各种访问权限等等。更具体地说,这些信息都会被记在每个进程的进程描述符中。进程描述符并不是一个简单的符号,而是一个非常复杂的数据结构。保存在进程描述符中的进程ID (常称为PID )是进程在操作系统中的唯一标识,其中进程ID为1的进程就是之前提到的内核启动进程。进程id是一个非负整数且总是顺序的编号,新创建的进程ID总是前一个进程ID递增的结果。此外,进程ID也可以重复使用。当进程ID达到其最大限值时,内核会从头开始查找闲置的进程ID并使用M先找到的那一个作为新进程的ID。另外,进程描述符中还会包含当前进程的父进程的ID (常称为PPID )。 进程通信进程通信叫做IPC(Inter-Process Communication),Linux中通信的方式大致可分成三种: 基于通信的IPC 基于信号的IPC 基于同步的IPC 通信IPC 以数据为传送手段的IPC 管道(pipe):用于传输字节流消息队列(message queue):用来传输结构化的对象 以共享内存为手段的IPC 共享内存区(share memory):最快的IPC方法 信号IPC 操作系统的信号(signal)机制:唯一一种异步IPC方法。通过kill -l查看。 同步IPC 信号量(semaphore) 线程线程可以视为进程中的控制流。一个进程至少包含一个线程,因为其他至少会有一个控制流持续运行。因而,一个进程的第一个线程会随着这个进程的启动而创建,这个线程被称为该进程的主线程。当然,一个进程可以包含多个线程。这些线程都是由当前线程中已经存在的线程创建出来的,创建的方法就是系统调用(pthread_create)。拥有多个线程的进程可以并发执行多个任务,并且即时某个或某些任务被阻塞,也不会影响其他任务执行,这可以大大改善程序的响应时间和吞吐量。另一方面,线程不可能独立于进程存在。它的生命周期不可能逾越所属进程的生命周期。 一个进程中的所有线程都拥有自己线程栈,并以此存储自己的私有数据。这些线程的线程栈都包含在其所属进程的虚拟内存地址中。不过要注意,一个进程中的很多资源都会被其中的所有线程共享,这些被线程共享的资源包含当前进程所持有文件描述符,等等。正因为如此,同一个进程的多个线程运行的一定是同一个程序,只不过具体的控制流程的执行函数可能有所不同。在同一个进程的多个线程之间共享数据也是一件非常轻松和自然的事情。另外,创建一个新线程,也不会像创建一个新进程那样耗时费力,因为在其所属进程的虚拟内存地址中存储的代码、数据和资源都不需要被复制。 和进程一样,每个线程都有自己的ID(由内核分配),叫做线程ID或者TID。但是在操作系统范围内不唯一,在所属进程的范围内唯一。 多线程的三大特性原子性原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个人操作一旦开始,就不会被其他的线程干扰。 可见性(Visibility)可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。显然,对于串行程序来说,可见性问题是不存在的。因为你在任何一个操作步骤中修改了某个变量,那么在后续的步骤中,读取这个变量的值,一定是修改后的新值。 有序性(Ordering)有序性问题是三个问题中最难理解的。对于一个线程的执行代码而言,我们总是习惯地认为代码的执行是从先往后,依次执行。这么理解也不是说完全错误,因为就一个线程内而言,确实会表现成这样。但是,在并发时,程序的执行可能就会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。然而有序性的问题的原因因为是程序在执行时,可能会进行指令重排,重排后的指令与原指令的顺序未必一致。 哪些指令不能重排——Happen-Before规则 1.程序顺序原则:一个线程内保证语义的串行性2.volatile规则:volatile变量的写,先发生与读,这保证了volatile变量的可见性。一般用volatile修饰的都是经常修改的对象。3.锁规则:解锁(unlock)必然发生在随后的加锁(lock)前4.传递性:A先于B,B先于C,那么A必然先于C5.线程的start()方法先于它的每一个动作6.线程的所有操作先于线程的终结(Thread.join())7.线程的中断(interrupt())先于被中断线程的代码8.对象的构造函数执行、结束先于finalize()方法 线程的生命周期五种状态在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。尤其是当线程启动以后,它不可能一直”霸占”着CPU独自运行,所以CPU需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换 新建状态,当程序使用new关键字创建了一个线程之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值 就绪状态,当线程对象调用了start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行 运行状态,如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态 阻塞状态,当处于运行状态的线程失去所占用资源之后,便进入阻塞状态 死亡状态,线程终止 终止线程线程可以通过多种方式来终结同一个进程中的其他线程。其他一种方式就是调用系统调用pthread_cancel,其作用是取消掉给定线程ID代表的那个线程。更确切地讲,它会向目标线程发送一个请求,要求它立刻终止执行。但是该函数只是发送请求并即可返回。但是,该函数只是发送请求并立刻返回,而不会等待目标线程对该请求做出响应。至于目标线程什么时候对此做出线程、怎么样的响应,则取决与另外的因素(比如线程目标的取消状态及类型)。在默认情况下,目标线程总是会接受线程取消请求,不过等到时机成熟(执行到某个取消点)的时候,目标线程才会响应线程的取消请求。 连接已终止的线程此操作由系统调用pthread_join来执行,该函数会一直等待与给定的线程ID对应的那个线程终止,并把线程执行的pthread_create函数的返回值告知调用线程。如果目标线程已经处于终止状态,那么该函数会立即返回。这就像是把调用线程放置在了目标线程的后面,当目标线程把线程控制权交出时,调用线程会接过流程控制权并继续执行pthread_join函数调用之后的代码。这也把这一操作称为连接的缘由之一。实际上,如果一个线程可被连接,那么在它终止之前就必须连接,否则就会变成一个僵尸线程。僵尸线程不但会导致系统资源浪费,还会无意义减少其进程的可创建线程数量。 分离线程将一个线程分离后那么它将变得不可连接。而在默认情况下,一个线程总是可以被连接的。分离操作的另一个作用是让操作系统内核在目标线程终止时自行进行清理和销毁工作。注意,分离操作是不可逆的。也就是说,我们无法使一个不可连接的线程变回可连接的状态。不过,对于一个已处于分离状态的线程,执行终止操作仍然会起作用。分离操作由系统调用pthread_detach来执行,它接受一个代表了线程ID的参数值。 一个线程对自身也可以进行两种控制:终止和分离。线程终止自身的方式有很多种。在线程执行的start函数中执行return语句,会使该线程随着start函数的结束而终止。需要注意的是,如果在主线程中执行了return语句,那么当前进程中的所有线程都会终止。另外,在任意线程中调用系统调用exit也会达到这种效果。还有一种终止自身的方式就是显示调用pthread_exit。而分离pthread_detach函数则是传入自己的TID。 创建线程主线程在其所属进程启动时创建。其他线程可以通过别的线程用pthread_create来创建——要传入新线程将要执行的函数以及传入该函数的参数值。在创建成功的时候,该函数会返回线程的TID。 创建线程的方式线程创建的方式有三种,在我至今使用的经历中从来没有使用到过第三种方法:所以今天只介绍两种,第一种是继承Thread类,第二种是实现Runnable接口。两种方法的优缺点就和继承和接口优缺点有关了,其实底层都是一样的。具体的代码这里不再进行详述,基本上都是见过的。 线程同步的方式在多个线程之间交换线程是非常简单和自然的事,而在多个进程之间只能通过一些额外的手段(比如管道、消息队列、信号量和共享内存区)传递数据。显然,使用这些额外手段会增加开发成本。不过,线程间交换数据虽然简单但却由于可能发生竞态条件而不得不使用一些同步工具(比如互斥量和条件变量)加以保护。这些与业务逻辑无关的代码会增加程序的复杂度,尤其在使用不当的情况下还会引起灾难。 互斥量可以理解为我们常见的锁。而条件变量所做的就是保证线程间共享的数据状态改变时通知到其他因此而被阻塞的线程。条件变量总是与互斥量组合使用。当线程成功锁定互斥量并访问到共享数据时,共享数据的状态并不一定满足它的要求。 锁synchronized使用synchronize的可以加载方法,代码块,类上,以此实现多线同步。 同步代码块:即有synchronized关键字修饰的语句块。 被该关键字修饰的语句块会自动被加上内置锁,从而实现同步 123456代码如: synchronized(object){ }注:同步是一种高开销的操作,因此应该尽量减少同步的内容。 通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。 同步方法即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 12代码如: public synchronized void save(){} 注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类 volatile使用volatile修饰共享变量可以实现线程同步。其中的原理与synchronized区别会在JMM文章中讲到。a.volatile关键字为域变量的访问提供了一种免锁机制,b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新,c.因此每次使用该域就要重新计算,而不是使用寄存器中的值d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 重入锁什么是重入呢?它是表示当一个线程试图获取一个它已经获取的锁时,这个获取动作就自动成功,这是对锁获取粒度的一个概念,也就是锁持有的是以线程为单位而不是基于调用次数。再入锁可以设置公平性(fairness),我们可以创建重入锁时选择是否是公平的。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReentrantLock相比synchronize,因为可以像普通对象一样使用,所以可以利用其提供的各种便利方法,进行精细的同步操作。 123456789101112131415161718class Bank { private int account = 100; //需要声明这个锁 private Lock lock = new ReentrantLock(); public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { lock.lock(); try{ account += money; }finally{ lock.unlock(); } } } ReenreantLock类的常用方法有: ReentrantLock() : 创建一个ReentrantLock实例 lock() : 获得锁 unlock() : 释放锁 从性能的角度,synchronize早起的实现比较低效,对比ReentrantLock大多数场景性能都相差较大,但是在java6 中对其进行了非常多的改进,在高竞争情况下ReentrantLock仍然有一定优势。 条件变量与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。条件变量分为两部分:条件和变量。条件本身是由互斥量保护的。线程在改变条件状态前先要锁住互斥量。条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使”条件成立”(给出条件成立信号)。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。 局部变量如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。ThreadLocal 类的常用方法 ThreadLocal() : 创建一个线程本地变量 get() : 返回此线程局部变量的当前线程副本中的值initialValue() : 返回此线程局部变量的当前线程的”初始值”set(T value) :将此线程局部变量的当前线程副本中的值设置为value 123456789101112131415public class Bank{ //使用ThreadLocal类管理共享变量account private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){ @Override protected Integer initialValue(){ return 100; } }; public void save(int money){ account.set(account.get()+money); } public int getAccount(){ return account.get(); } } ThreadLocal下面单独讲 ThreadLocal一句话概括:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。所以ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。 数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。 具体看这篇博客:https://blog.csdn.net/lufeng20/article/details/24314381 基本概念并发(Concurrency)和并行(Parallelism) 并发和并行往往被人所混淆。它们都可以表示两个或多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务有可能还是串行。而并行则是真正意义上的“同时执行”。 严格来说,并行的多个任务是真实的同时执行,而对并发来说,这个过程这是交替的,一会儿运行任务A一会儿执行任务B,系统会不停地在两者间切换。但对于外部观察者来说,即使多个任务之间是串行并发的,也会造成多任务间是并行执行的错觉。 临界区 临界区(criticalsection)用来表示一种公共资源或者共享数据,可以被多个线程使用。但是每一次,只有一个线程可以使它,一旦临界区资源被占用,其他线程要想使用资源,就必须等待,即串行化访问或执行。 死锁(DeadLock)、饥饿(Starvation)和活锁(Livelock) 死锁、饥饿和活锁都属于多线程的活跃性问题,如果发生上述情况,那么相关线程可能就不再活跃,也就是说它可能很难继续往下执行了。死锁应该是最糟糕的一种情况了,虽然别的情况也没有好到哪儿去。 死锁:多个线程互相等待多方释放资源而一直没有执行。 饥饿:一个或多个线程因为种种原因无法获取所得的需要资源,导致一直无法执行。导致的原因往往是当前线程优先级不高导致没有资源,或某线程一直占着关键资源不放。 活锁:多个线程都释放资源给别的线程使用,导致没有线程拿到资源而正常执行。 阻塞和非阻塞 描述的是用户线程调用内核 I/O 操作的方式: 阻塞(Blocking)是指 I/O 操作需要彻底完成后才返回到用户空间; 非阻塞(Non-Blocking)是指 I/O 操作被调用后立即返回给用户一个状态值,无需等到 I/O 操作彻底完成。 一个 I/O 操作其实分成了两个步骤: 发起 I/O 请求 实际的 I/O 操作。 阻塞 I/O 和非阻塞 I/O 的区别在于第一步,发起 I/O 请求是否会被阻塞。如果阻塞直到完成那么就是传统的阻塞 I/O,如果不阻塞,那么就是非阻塞 I/O 。 同步 I/O 和异步 I/O 的区别就在于第二个步骤是否阻塞,如果实际的 I/O 读写阻塞请求进程,那么就是同步 I/O 。]]></content>
<categories>
<category>线程</category>
</categories>
<tags>
<tag>多线程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[数据库事务--原子性]]></title>
<url>%2F2018%2F08%2F12%2F%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1-%E5%8E%9F%E5%AD%90%E6%80%A7%2F</url>
<content type="text"><![CDATA[数据库看似简单,却是个十分复杂的东西 概述ACID: 指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。 如果没有没有正确执行便会出现以下问题:1,脏读(dirtyRead)脏读是指的一个事物正在访问数据,并且对数据进行了修改,而且这种修改还没有提交到数据库中,这时,另一个事物也访问这个数据,然后使用了这个数据。因为这个数据还没有提交数据,那么另外一个事物读到的这个数据就是脏数据。 2,不可重复读不可重复读,就是在同一事务中,两次读取同一数据(同一主键),得到内容不同。不可重复读和脏读的区别是,不可重复读读取到的都是已经提交的数据,而不是脏数据。 3,虚读(幻读)幻读指的是,同一事务中,用同样的操作读取两次,得到的记录数不相同。幻读和不可重复读都是读取到了另一条已经提交的事物,这一点和脏读不同。看似幻读和不可重复读都是一样的,但是区别在于不可重复读针对的是同一个主键的数据。而幻读针对的是一批数据两次读取中,有新增或者减少。 原子性了解了数据库的四大特性,那么今天就来讲讲数据库特性之一 —–原子性首先:原子性一说大家都明白,那你能说说在执行的操作过程中,如果还没做完系统就崩溃了,或者断电了,你怎么办啊? 你怎么保证原子性?如果我还没做完,系统就崩溃了,那系统重启以后我就得做恢复操作? 下面我们就来讲讲这个问题的答案。首先以旺财有200块钱, 小强有50 块钱,现在旺财要给小强转账,假设转100块为例。如果按照事务完整进行则会有一下四步:(1) 开始事务 T1 (假设T1是个事务的内部编号)(2) 旺财余额 = 旺财余额 -100(3) 小强余额 = 小强余额 + 100(4) 提交事务 T1我们知道所有的计算操作都是在内存中进行的,这个时候被计算的数据首先放入缓冲区中:如下图因为硬盘的速度太慢,所以不会经常性的操作硬盘,而是把数据放入到缓冲区中,然后一次性保存到硬盘中。 Undo 日志这里先假定数据缓冲区能和硬盘的数据文件同步。问题:旺财在给小强转账, 第二步执行完了,旺财的余额变成了100块 (200-100), 假设已经写入了硬盘文件, 现在断电了, 小强的余额有没有加上,系统的钱白白消失了100块, 数据已经不一致了, 你怎么办?而这个叫做Undo的日志文件,就是为了解决这个问题的。分析:按照上面的情况,会在日志文件中记录下事务开始之前他俩账号余额: [事务T1, 旺财原有余额 , 200] [事务T1, 小强原有余额, 50 ] 如果事务执行到一半,就断电了,那数据库重启以后我就根据undo的日志文件来恢复。 问题:恢复数据的时候, 那你怎么才能知道一个事务没有完成呢? [开始事务 T1] [事务T1, 旺财原有余额,200] [事务T1, 小强原有余额,50] [提交事务 T1] Undo日志文件中不仅仅只有余额, 事务的开始和结束也会记录,如果我在日志文件中看到了提交事务 T1, 或者 回滚事务 T1, 我就知道这个事务已经结束,不用再去理会它了, 更不用去恢复。 如果我只看到 开始事务 T1, 而找不到提交或回滚,那我就得恢复。比如下面这个: [开始事务 T1] [事务T1, 旺财原有余额,200] [事务T1, 小强原有余额,50] 如果已经恢复了,就在日志文件中加上一行 回滚事务 T1 , 这样下一次恢复我就不用再考虑T1这个事务了。 问题:那我们应该什么时候记录Undo日志呢?什么时候把Undo日志写入文件呢? 把日志记录也放到了内存的Undo日志缓冲区,伺机写入硬盘。 我们来看看下面的分析: 如果系统在第4步和第5步之间崩溃,旺财的余额写入了硬盘,但是小强的还没写入, 那Undo日志看起来是这样的: [开始事务 T1] [事务T1, 旺财原有余额,200] 由于找不到事务结束的日志, 你会进行恢复操作, 把旺财的原有余额给恢复了。 如果是在第7步和第8步之间系统崩溃,旺财和小强的最新余额都写入了硬盘,但是没有提交事务, 那Undo日志看起来是这样的: [开始事务 T1] [事务T1, 旺财原有余额,200] [事务T1, 小强原有余额,50] 由于没有事务结束的日志,你也需要进行恢复,把旺财和小强的原有余额恢复成200和50 ” 如果是在第8步和第9步之间系统崩溃, 旺财和小强的最新余额都写入了硬盘, 也提交了事务, 但是提交事务的操作没有写入Undo 日志, 所以Undo日志还是这样: [开始事务 T1] [事务T1, 旺财原有余额,200] [事务T1, 小强原有余额,50] 由于没有事务结束的日志,你还得需要进行恢复,把旺财和小强的原有余额恢复成200和50。 总结其实这里面是有规律的,如果实现了这个规律便可以解决所有的问题: 在你把最新余额写入硬盘之前, 一定要先把相关的Undo日志记录写入硬盘。 例如 [事务T1, 旺财原有余额,200] 一定要在旺财的新余额=100写入硬盘之前写入。 [提交事务 T1] 这样的Undo日志记录一定要在所有的新余额写入硬盘之后再写入。 有了这两条的保证,我就可以高枕无忧了!, 比如说,换个操作次序也没有问题: end参考:https://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665514001&idx=1&sn=17b72c3e69db6c4277e3045c699b7b6b&chksm=80d67c52b7a1f5446020826841869221873f4578524181384592839d19c4810dc68807117e13&mpshare=1&scene=1&srcid=0311ddHdl0tOCk3XkUy6l8lA#rd 我所向往的不过是如此简单而已]]></content>
<categories>
<category>数据库</category>
</categories>
<tags>
<tag>数据库,事务</tag>
</tags>
</entry>
<entry>
<title><![CDATA[策略模式和观察者模式]]></title>
<url>%2F2018%2F07%2F31%2F%E7%AD%96%E7%95%A5%E6%A8%A1%E5%BC%8F%E5%92%8C%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[概述 哪里会有人喜欢孤独,不过是不喜欢失望罢了。 在学习Zstack时候看到了系统中多处使用策略模式和观察者模式,学习发现这两种模式在开发中经常用到,故在此记录下来。 策略模式定义策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。 使用场景 完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。 在软件开发中也常常遇到类似的情况,实现某一个功能有多个途径,此时可以使用一种设计模式来使得系统可以灵活地选择解决途径,也能够方便地增加新的解决途径。 在软件系统中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。 类图对照类图可以看到,策略模式与模版方法模式的区别仅仅是多了一个单独的封装类Context,它与模版方法模式的区别在于:在模版方法模式中,调用算法的主体在抽象的父类中,而在策略模式中,调用算法的主体则是封装到了封装类Context中,抽象策略Strategy一般是一个接口,目的只是为了定义规范,里面一般不包含逻辑。其实,这只是通用实现,而在实际编程中,因为各个具体策略实现类之间难免存在一些相同的逻辑,为了避免重复的代码,我们常常使用抽象类来担任Strategy的角色,在里面封装公共的代码。 代码演示 创建一个接口 Strategy.java123public interface Strategy { public int doOperation(int num1, int num2);} 创建实现接口的实体类 OperationAdd.java123456public class OperationAdd implements Strategy{ @Override public int doOperation(int num1, int num2) { return num1 + num2; }} 创建 Context 类 Context.java 1234567891011public class Context { private Strategy strategy; public Context(Strategy strategy){ this.strategy = strategy; } public int executeStrategy(int num1, int num2){ return strategy.doOperation(num1, num2); }} 从上面代码可以看出:创建Context时,传入不同的Strategy的子类会执行不同的功能方法。使用到java父类的引用指向子类对象。 优缺点和注意事项优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。 Zstack中的体现Zstack中充斥着大量的回调函数,如上图所示。CloudBusCallBack是一个接口,通过在方法中实现匿名类重写接口中定义的方法,方法中的功能可以根据上层类要实现的功能去灵活的定义。 这种方式就和传入一个子类对象一样,不过这个子类对象没有名字。因为Zstack所有功能的模块都是通过异步通信的,对策略模式了解可以有效的加深对整个Zstack的了解。 观察者模式定义意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。 优缺点优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。 使用场景 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。 一个对象必须通知其他对象,而并不知道这些对象是谁。需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。 注意事项: 1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。 类图创建 Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类。 代码演示观察者模式,我理解的就是观察者订阅被观察者的状态,当被观察者状态改变的时候会通知所有订阅的观察者的过程。 创建观察者接口 123public abstract class Observer { public abstract void update(String msg);} 观察者的实现类 第一个观察者12345public class F_Observer extends Observer { public void update(String msg) { System.out.println(F_Observer.class.getName() + " : " + msg); }} 第二个观察者 12345public class S_Observer extends Observer { public void update(String msg) { System.out.println(S_Observer.class.getName() + " : " + msg); }} 第三个观察者 12345public class T_Observer extends Observer { public void update(String msg) { System.out.println(T_Observer.class.getName() + " : " + msg); }} 被观察者 12345678910111213141516public class Subject { private List<Observer> observers = new ArrayList<>(); //状态改变 public void setMsg(String msg) { notifyAll(msg); } //订阅 public void addAttach(Observer observer) { observers.add(observer); } //通知所有订阅的观察者 private void notifyAll(String msg) { for (Observer observer : observers) { observer.update(msg); } }} 运行 123456789101112public class Main { public static void main(String[] args) { F_Observer fObserver = new F_Observer(); S_Observer sObserver = new S_Observer(); T_Observer tObserver = new T_Observer(); Subject subject = new Subject(); subject.addAttach(fObserver); subject.addAttach(sObserver); subject.addAttach(tObserver); subject.setMsg("msg change"); }} 总结:从上面的代码中可以看出,当被观察者的行为改变的时候就可以通知观察者,观察者可以依据不同的变现做出不同的反应,我猜想:MQ有一种模式是消息订阅模式,其中必用到观察者模式,不过MQ使用的必然是异步的方式。 Zstack中的体现观察者模式在Zstack中的体现就要和Zstack的三驾马车(后面会讲到)联系到一起了。三驾马车分别对应着三层:应用层,业务层和领域层。应用层(可以被调用的API):就是界面定义的一些功能。业务层(一个Impl):一个服务的入口,对功能的分发,不会处理底层很具体功能。领域层(base):这层主要是一些行为的逻辑,对某一个功能具体的操作。 我们知道各个层之间是需要通信的,那么层与层之间只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段。而如果下层元素需要与上层元素通信,则需要采用另一种通信机制——比如回调或者Observers模式(在ZStack中即是ExtensionPoint)。 这里以PrimaryStorageBase为例:在PrimaryStorageBase中,其中handle APIAttachPrimaryStorageToClusterMsg的地方会做事件发送: 1extpEmitter.preAttach(self, msg.getClusterUuid()); 其会发送向:在抽象等级上,PrimaryStorageBase是比图中的这些Base高的。而这类具象Base可以使Message返回Success或者Fail使高层Base做出不同的决策。这里是通过回调函数的形式来对上层的Base进行通知的。具体对象的Base执行失败或者成功会回调上层的fail或者success,这样上层就知道下面的方法是否执行成功。因为系统中都是通过异步的方式来实现的。]]></content>
<categories>
<category>设计模式</category>
</categories>
<tags>
<tag>设计模式,Java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[1688开放平台对接]]></title>
<url>%2F2018%2F06%2F19%2F1688%E5%AF%B9%E6%8E%A5%2F</url>
<content type="text"><![CDATA[概述什么是1688开放平台( https://open.1688.com )? 依托B2B海量用户资源以及强大的平台优势,是为阿里巴巴商家提供基础服务的重要开放途径,帮助商家提升经营能力、拓宽生意渠道、提高办公效率。从今年开始,阿里巴巴开放平台将向合作伙伴和广大第三方开发者逐步开放会员、公司库、类目、产品、交易、咨讯等一系列接口。为合作伙伴提供快捷的提交通路,多入口最优展现的同时,共享商机,互利双赢。 对接过程以下演示是以对接1688采购订单为例。 1 注册开发者如果想使用1688开放平台,必须首先要注册成为开发者,需要同时具备以下两个条件: 具备一个阿里巴巴中国站帐号; 必须绑定了通过个人实名(公司企业)认证的支付宝帐号;个人开发者必须绑定通过个人实名认证的支付宝账号,企业开发者必须绑定通过商家认证的支付宝账号; 开发者身份绑定的支付宝账户用于产品分成结算的收款账户。因此为了确认您的身份和安全考虑,必须通过支付宝认证。 2 获取证书一、什么是应用证书:证书指的是开发者在阿里巴巴开放平台创建应用是默认给开发者的应用开发证书。想要调用1688开放平台上的API必须申请证书。证书包含四个内容: 证书编号:App Key 证书密钥:App Secret 接口权限:开发者可以调用的API权限,包含基础开放与增值包 证书流量:应用可以调用API的流量限制 也就是“权限+流量+appkey+app secret=证书”,每个应用都有对应的应用开发证书,在应用创建时开发者获得证书。二、什么是App Key? App Key是应用的唯一标识,阿里巴巴开放平台通过App Key来鉴别应用的身份。三、什么是App Secret? AppSecret是阿里巴巴开放平台给应用分配的密钥,开发者需要妥善保存这个密钥,这个密钥用来保证应用来源的可靠性,防止被伪造。 获取证书过程 https://open.1688.com/developer/serviceType.htm?spm=a260s.8209130.sidebar.2.4fbb55edhIdo07#/?_k=l6b00o 1.创建应用:根据自己公司的业务需求可以选择不同的应用类型,如下图所示: 选择采购对接企业对接面向的是从1688网站上采购的数据信息拉取到公司自己的管理软件中进行统一的管理。点击采购对接后填写必要的一些信息提交申请就可以了。在填写信息的时候,应用类目选择企业采购,大企业采购一般是通过不了的;授权用户数可以选择单用户授权也可以选择多用户授权,他们其中的区别放到下面讲。 提交成功后便可在应用列表中看到申请到的对接应用。可以点击查看,就会看到应用详情,里面便包括了App Key和App Secret。如下图: 2.申请解决方案:即针对你申请到的账户需要申请调用对应API的权限 申请解决方案一般在48小时之内会反馈方案是否给予通过。所以开发的时候如果急于上线,需要提前申请。通过的解决方案便可以使用。 3.测试账号 当你在解决方案列表看到已经通过的账号,下一步就需要在线测试账号的可用性。在API文档中点击API TOOLS 在线测试工具,输入App Key和App Secret便可以测试账号是否可用。 4.下载jar包进行开发 5.编程 具体代码请看我的github上的项目,里面有完整的授权流程代码,地址:多用户授权下调用1688接口API授权流程。 3 关于单用户授权和多用户授权的说明我们首先看一下官方给的不同授权方式的定义: 单用户授权和多用户授权的区别是:1.单用户授权在调用API的时候不需要传access_token,而多用户授权需要传access_token ,不然的话无法调用1688的API。2.单用户授权在申请上线的时候一个公司只能有一个账户,而多用户授权在上线的时候可以有多个账户。3.多用户授权可以关联其他的账户,通过一个账户拉去多个账户的信息。 虽然多用户授权有很多的好处,不过它开发起来要比单用户授权麻烦很多,主要是处理access_token 的问题。首先我们先看看多用户授权下access_token 的授权流程,也就是获取access_token 的流程(官网授权解释地址): 首先通过code即临时令牌得到access_token,第一次授权后会得到refresh_token,等下次access_token过期的时候就可以通过refresh_token来得到。授权的流程图如下所示: 具体代码请看我的github,地址:多用户授权下调用1688接口API授权流程。 END]]></content>
<tags>
<tag>1688,1688开放平台,阿里巴巴</tag>
</tags>
</entry>
<entry>
<title><![CDATA[初次提交代码总结]]></title>
<url>%2F2018%2F05%2F26%2F%E5%88%9D%E6%AC%A1%E6%8F%90%E4%BA%A4%E4%BB%A3%E7%A0%81%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[概述 自己空白的还有很多,记录一下本周所感所想 本周算是经历了第一次的merge request,虽然是在我师傅写好的代码下填补一些小功能,不过感觉学到的东西还是挺多的。无论是对开发中的细节,还是良好习惯的培养都让我受益颇多。 从前前面的一个月基本上都是在学习zstack的基本知识,架构还有里面的实现原理。这次也是我真正去读别人的代码,跟着别人代码的思想去理解整个结构,去添加一些需求。可以说这周的开发过程也是对我一次深刻的提醒。意识到其实在以前学习zstack的时候许多重要的点并没有深刻的去理解,以前的心态就是能看懂就行,不需要问原因和原理,所以遇到一些不是特别清晰的并没有过于的深刻探究其中主要原理。像Inner Msg在zstack中处理的过程,CloudBus怎么处理去拦截和收集这些消息的,FlowChain的调用过程,回调实现的方式,一些类的继承实现有的时候居然不知道为什么。以前在学校的时候就接触过回调函数这方面的知识点,也从未认真看过。这次要不是我师傅的指点,我恐怕陷得很深。 现在本周主要是对审计模块一些简单功能的添加,实现用户自定义operation name,并且添加一些基本的测试。这两个功能前前后后花了一周的时间。接到需求的第一天,我大概浏览了各个问题的要求,然后开始添加自己的代码,在做的过程中,我发现我面临许多的问题,我对问题并不是很理解。比如添加用户自定义operation name,我理解的就是把某一个属性写到配置文件中,加载新来而已。还有添加inner msg的case,我以为Inner Msg实现审计模块会有人帮我写好。一切都源于对问题的不清晰然后就开始干,第一天就踩了不少的坑。辛亏我师父救场,问我有哪些不懂的问题,不然我还在按照自己的思路在写。还有后面看代码十分的不认真,有一个问题是set集合的泛型问题,在创建HashSet集合的时候,泛型放在前面和后面有着不同的意义。其实开始的我以为这部分功能最多三天就搞完了,最终还是在我师父帮助下,五天勉勉强强的完成。我总结这次主要原因如下: zstack部分功能理解不深入 没有搞清楚需求是怎么样的就急于入手 不善于提问,总认为自己可以解决,思维逻辑混乱 过于浮躁,不认真 以后踩坑真的是能让人进步的过程,如果不真的接触代码,真的很少能够意识到那么多的问题。现阶段我不足的部分: 回调方面的部分,我总是感觉有些地方不是特别理解,但是又说不上来。 FlowChain部分也是有点模糊 … 等待发现 也许这几部分也要通过写代码的方式来锻炼,来理解。后面的工作我会尽力去做。不能解决的问题,我会尽量的问我的师傅,不会自己再闷头苦想(虽然自己是应届生,不过遇到弱智的问题,感觉还是挺丢人的)。 后面再写一段代码,我会把我学到的zstack知识尽量做一个总结,写几篇博客,把zstack的原理自己总结一下。 总结跟着QB团队学到不少的东西,有着中国第一的PM孙总,zstack的高级开发者和精通Java的我师傅,python和java都精通的伟嘉,打得过PM的高级开发工程师强哥,受益匪浅。比在上海那三个月的实习学到的多得多。不过感觉赶上他们的步伐还是好难。这段时间也听到了很多陌生的词,像比特币,区域块,股票方面的知识还有他们对中国经济和政府投资的分析。我有的时候真的明天为什么有些人可以赚那么多钱了。很感谢我师傅的指导,很有耐心,知道我缺什么,然后认真提出方法来解决。还有我师傅认真起来真的很吓人。最后: 不要把不会当成一个负担,不会就问。]]></content>
<categories>
<category>所思</category>
</categories>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[jdk动态代理]]></title>
<url>%2F2018%2F01%2F20%2Fjdk%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%2F</url>
<content type="text"><![CDATA[动态代理什么是代理代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。 动态代理类Java动态代理类位于java.lang.reflect包下,一般主要涉及到以下两个类:(1) Interface InvocationHandler:该接口中仅定义了一个方法 publicobject invoke(Object obj,Method method, Object[] args) 在实际使用时,第一个参数obj一般是指代理类,method是被代理的方法,args为该方法的参数数组,这个抽象方法在代理类中动态实现。 (2) Proxy:该类即为动态代理类,其中主要包含以下内容: protected Proxy(InvocationHandler h):构造函数,用于给内部的h赋值。 static InvocationHandler getInvocationHandler(Object proxy) :返回指定代理实例的调用处理程序 static Class<?> getProxyClass (ClassLoaderloader, Class[] interfaces):返回代理类的 java.lang.Class 对象,其中loader是类装载器,interfaces是真实类所拥有的全部接口的数组。 static boolean isProxyClass(Class<?> cl) //当且仅当指定的类通过 getProxyClass 方法或 newProxyInstance 方法动态生成为代理类时,返回 true。 static Object newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h):/返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。 所谓DynamicProxy是在运行时生成的class,在生成它时你必须提供一组interface给它,然后该class就宣称它实现了这些interface。你当然可以把该class的实例当作这些interface中的任何一个来用。当然,这个DynamicProxy其实就是一个Proxy,它不会替你作实质性的工作,在生成它的实例时你必须提供一个handler,由它接管实际的工作。在使用动态代理类时,我们必须实现InvocationHandler接口,通过这种方式,被代理的对象(RealPerson)可以在运行时动态改变,需要控制的接口(Person接口)可以在运行时改变,控制的方式(DynamicSubject类)也可以动态改变,从而实现了非常灵活的动态代理关系。 动态代理的步骤1.创建一个实现接口InvocationHandler的类,它必须实现invoke方法2.创建被代理的类以及接口3.通过Proxy的静态方法newProxyInstance(ClassLoaderloader, Class[] interfaces, InvocationHandler h)创建一个代理4.通过代理调用方法 动态代理的实现1:创建动态代理的接口 1234567package com.fan.Proxy_one;//动态代理接口public interface Person { String SayHello(String name); String SayGoodBye();} 2:创建需要代理的对象 12345678910111213141516package com.fan.Proxy_one;//实际对象public class RealPerson implements Person { @Override public String SayHello(String name) { return "Hello "+name; } @Override public String SayGoodBye() { return "Bye"; }} 3:实现InvocationHandler类,重写invoke方法 123456789101112131415161718192021222324252627282930313233package com.fan.Proxy_one;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;//调用处理类实现类//每次生成动态代理类对象时都需要指定一个实现了该接口的调用处理器对象public class InvocationHandlerImpl implements InvocationHandler { //我们要代理的真是对象 private Person person; public InvocationHandlerImpl(Person person) { this.person = person; } /** * 该方法负责集中处理动态代理类上的所有方法调用 * 调用处理器根据这三个参数进行预处理或分派到委托类实例上反射执行 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在代理真实对象方法前可以添加自己的操作 System.out.println("在调用方法之前,我正在走路!"); System.out.println("Method:"+method); /*当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler *对象的invoke方法进行调用 */ Object returnValue = method.invoke(person, args); //调用真实的代理对象的方法后,我们可以添加自己的方法 System.out.println("调用之后,我继续向前走!"); return returnValue; }} 4:测试jdk动态代理 1234567891011121314151617181920212223242526package com.fan.Proxy_one;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;public class DynamicProxyTest { public static void main(String[] args) { //代理的真实对象 Person realperson = new RealPerson(); /** * InvocationHandlerImpl 实现了 InvocationHandler 接口,并能 * 实现方法调用从代理类到委托类的分派转发 * 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用. * 即:要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法 */ InvocationHandler ih = new InvocationHandlerImpl(realperson); Person proxy = (Person) Proxy.newProxyInstance(realperson.getClass().getClassLoader(), realperson.getClass().getInterfaces(), ih); System.out.println("动态代理对象的类型:"+proxy.getClass().getName()); String hello = proxy.SayHello("樊兴凯"); System.out.println(hello); String bye = proxy.SayGoodBye(); System.out.println(bye); }} 5:输出结果 动态代理对象的类型:com.sun.proxy.$Proxy0在调用方法之前,我正在走路!Method:public abstract java.lang.String com.fan.Proxy_one.Person.SayHello(java.lang.String)调用之后,我继续向前走!Hello 张三在调用方法之前,我正在走路!Method:public abstract java.lang.String com.fan.Proxy_one.Person.SayGoodBye()调用之后,我继续向前走!Bye 改进使用匿名内部类的方式来实现 InvocationHandlerImpl 1234567891011121314151617181920212223242526272829package com.fan.Proxy_two;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class DynamicProxyTest2 { public static void main(String[] args) { Person person = new RealPerson(); Person proxy = (Person)Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new InvocationHandler() { //使用匿名内部类的方式 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在方法调用前,添加自己的动作 System.out.println("打招呼之前-"); System.out.println("Method:"+method); Object obj = method.invoke(person, args); System.out.println("打招呼之后----"); return obj; } }); System.out.println("动态代理对象的类型:"+proxy.getClass().getName()); String hello = proxy.SayHello("张三"); System.out.println(hello); String goodbye = proxy.SayGoodBye(); System.out.println(goodbye); }} 结果: 动态代理对象的类型:com.sun.proxy.$Proxy0打招呼之前-Method:public abstract java.lang.String com.fan.Proxy_two.Person.SayHello(java.lang.String)打招呼之后—-Hello 张三打招呼之前-Method:public abstract java.lang.String com.fan.Proxy_two.Person.SayGoodBye()打招呼之后—-Bye 总结一个典型的动态代理创建对象过程可分为以下四个步骤:1、通过实现InvocationHandler接口创建自己的调用处理器 IvocationHandler handler = new InvocationHandlerImpl(…);2、通过为Proxy类指定ClassLoader对象和一组interface创建动态代理类Class clazz = Proxy.getProxyClass(classLoader,new Class[]{…});3、通过反射机制获取动态代理类的构造函数,其参数类型是调用处理器接口类型Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});4、通过构造函数创建代理类实例,此时需将调用处理器对象作为参数被传入Interface Proxy = (Interface)constructor.newInstance(new Object[] (handler));为了简化对象创建过程,Proxy类中的newInstance方法封装了2~4,只需两步即可完成代理对象的创建。生成的RealPerson继承Proxy类实现Person接口,实现的Person的方法实际调用处理器的invoke方法,而invoke方法利用反射调用的是被代理对象的的方法(Object result=method.invoke(proxied,args)) 缺点Proxy已经设计得非常优美,但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。Java的继承机制注定了这些动态代理类们无法实现对class的动态代理,原因是多继承在Java中本质上就行不通。有很多条理由,人们可以否定对 class代理的必要性,但是同样有一些理由,相信支持class动态代理会更美好。接口和类的划分,本就不是很明显,只是到了Java中才变得如此的细化。如果只从方法的声明以及是否被定义来考量,有一种两者的混合体,它的名字叫抽象类。实现对抽象类的动态代理,相信也有其内在的价值。此外,还有一些历史遗留的类,它们将因为没有实现任何接口而从此与动态代理永世无缘。如此种种,不得不说是一个小小的遗憾。但是,不完美并不等于不伟大,伟大是一种本质,Java动态代理就是佐例。jdk给目标类提供动态要求目标类必须实现接口,当一个目标类不实现接口时,jdk是无法为其提供动态代理的。cglib 却能给这样的类提供动态代理。cglib在接下来的时间里将会介绍。]]></content>
<tags>
<tag>java,动态代理</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Eclipse中git的使用]]></title>
<url>%2F2018%2F01%2F16%2FEgit%E7%9A%84%E4%BD%BF%E7%94%A8%2F</url>
<content type="text"><![CDATA[概述git在eclipse中的配置和远程仓库中关联,这里不再累述,如果有问题可以参考下面博客:这里有详细的说明。 http://blog.csdn.net/hhhccckkk/article/details/10458159 问题下面主要是我在使用中遇到的问题:rejected - non-fast-forward我在网上查找原因的时候,并没有一个很完整的文档,总结的都是挺乱的。故我对使用方式给予详细总结,以帮助更多的人。 原因出现此问题的原因主要是: 我们平时使用eclipse开发的时候,主要是用git的commit和push,如果你的github库没有初始化,第一次push是可以正确提交的,但是当你已经push过一次后,会遇到无法提交到master分支(因为master分支是在服务器已经初始化好了,在服务器上已经存在了一个master分支,你在本地初始化的master分支和服务器的有冲突,并不是一个分支,就会提示rejected - non-fast-forward),如果你此时非要想提交到主分支,必须先要把服务器的初始化的git主分支pull到本地。只有这样才能正确的提交。 解决方案1,点window—preference–team—git–configuration————-Repository Settings,Repository选择你的项目的本地仓库,然后点右边的open,视图如下: open后会看到如下的config信息 2,然后在上面的基础上填上如下信息 123456789...[branch "master"] remote = origin merge = refs/heads/master[remote "origin"] url = [email protected]:xxxx/xx ----->你自己的仓库ssh的url fetch = +refs/heads/*:refs/remotes/origin/* push = refs/heads/master:refs/heads/master 3,配置好后选择项目Team–> pull ,会看到项目会变成如下效果: 查看本地仓库目录: 4,push本地项目到远程仓库 过程和第一次push的过程相同,填写信息然后push就可以了。]]></content>
<tags>
<tag>eclipse,git,github</tag>
</tags>
</entry>
<entry>
<title><![CDATA[装饰者模式]]></title>
<url>%2F2017%2F12%2F30%2F%E8%A3%85%E9%A5%B0%E8%80%85%E6%A8%A1%E5%BC%8F%2F</url>
<content type="text"><![CDATA[设置模式之装饰者模式装饰者模式(Decorator)是一种结构式模式。动态地给一个对象添加一些额外的职责。就增加功能来说,装饰者模式相比生成子类更为灵活。同时还可以让这些装饰类互相装饰。 1: 装饰者设计模式的步骤:a.在装饰类的内部维护一个被装饰类的引用。b.让装饰类有一个共同的父类或者是父接口 2:具体过程:Component : 定义一个对象接口,可以给这些对象动态地添加职责。 12345package com.fan.DecoratorPattern;public interface Component { void operation();} ConcreteComponent:实现Component定义的接口 1234567891011package com.fan.DecoratorPattern;//实现Component定义的接口public class ConcreteComponent implements Component { @Override public void operation() { System.out.println("初始化"); }} Decorator : 装饰抽象类,继承了 Component, 从外类来扩展 Component 类的功能,但对于 Component 来说,是无需知道 Decorator 的存在的。 1234567891011121314package com.fan.DecoratorPattern;public class Decorator implements Component { //维护一个Component对象,和Component形成聚合关系 private Component component; public Decorator(Component component) { this.component = component; } //调用要修饰对象的原方法 @Override public void operation() { component.operation(); }} ConcreteDecoratorA : 具体的装饰对象,起到给 Component 添加职责A的功能。 1234567891011121314package com.fan.DecoratorPattern;public class ConcreteDecoretorA extends Decorator { public ConcreteDecoretorA(Component component) { super(component); } @Override public void operation(){ super.operation(); System.out.println("添加属性:性属性1"); }} ConcreteDecoratorB : 具体的装饰对象,起到给 Component 添加职责B的功能。 1234567891011121314package com.fan.DecoratorPattern;public class ConcreteDecoratorB extends Decorator { public ConcreteDecoratorB(Component component) { super(component); } @Override public void operation() { super.operation(); System.out.println("添加行为"); }} 测试代码 12345678910111213141516package com.fan.DecoratorPattern;public class DecoratorPattern { public static void main(String[] args) { Component component = new ConcreteComponent(); component.operation(); System.out.println("++++++++++++++++++++++"); Decorator decoratorA = new ConcreteDecoretorA(component); decoratorA.operation(); //通过super向上级层层调用 System.out.println("++++++++++++++++++++++"); Decorator decoratorB = new ConcreteDecoratorB(decoratorA); decoratorB.operation();//B调用A的 --A调用父类的,父类调用接口的实现类operation方法 }} 运行结果: 初始化++++++++++++++++++初始化添加属性:性属性1++++++++++++++++++初始化添加属性:性属性1添加行为 3:装饰者模式的应用场景a.需要动态的、透明的为一个对象添加职责,即不影响其他对象。b.需要动态的给一个对象添加功能,这些功能可以再动态的撤销。c.需要增加由一些基本功能的排列组合而产生的非常大量的功能,从而使继承关系变的不现实。d.当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。 4:要点: 装饰者和被装饰对象有相同的超类型。 可以用一个或多个装饰者包装一个对象。 装饰者可以在所委托被装饰者的行为之前或之后,加上自己的行为,以达到特定的目的。 对象可以在任何时候被装饰,所以可以在运行时动态的,不限量的用你喜欢的装饰者来装饰对象。 装饰模式中使用继承的关键是想达到装饰者和被装饰对象的类型匹配,而不是获得其行为。 装饰者一般对组件的客户是透明的,除非客户程序依赖于组件的具体类型。在实际项目中可以根据需要为装饰者添加新的行为,做到“半透明”装饰者。 5:继承实现的增强类和修饰模式实现的增强类有和区别? 继承实现的增强类: 优点:代码结构清晰,而且实现简单.缺点:对于每一个的需要增强的类都要创建具体的子类来帮助其增强,这样会导致继承体系过于庞大。 装饰者设计模式实现的增强类: 优点:内部可以通过多态技术对多个需要增强的类进行增强,可以使这些装饰类达到互相装饰的效果,使用比较灵活。缺点:需要内部通过多态技术维护需要被增强的类的实例。进而使得代码稍微复杂。]]></content>
<tags>
<tag>设计模式,java</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ubuntu安装Vundle管理vim插件]]></title>
<url>%2F2017%2F11%2F20%2FUbuntu%E5%AE%89%E8%A3%85Vundle%2F</url>
<content type="text"><![CDATA[概述我在学习Linux这段时间里,使用vim编程,为了提高自己的逼格,也为了提高自己的效率,就尝试着安装了2个插件。感觉逼格高了很多。 安装安装其他插件之前,我们首先需要安装一个管理插件的插件,它就是Vundle,Vundle可以帮助我们管理和安装其他的插件,非常好用。 安装Vundle插件Vundle可以在GitHub上找到,地址:https://github.com/VundleVim/Vundle.vim 1、 如果在你的Linux或者Ubuntu中没有暗转git,首先请先安装git sudo apt-get install git 2、 使用git安装Vundle ,可以安装到~/.vim/bundle/Vundle.vim下 git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim 3、 添加官方文档提供的配置信息到 ~/.vimrc 中(.vimrc 如果不存在就创建一个【vi ~/.vimrc】): 1234567891011121314151617181920212223242526272829303132333435363738394041424344set nocompatible " be iMproved, requiredfiletype off " required" set the runtime path to include Vundle and initializeset rtp+=~/.vim/bundle/Vundle.vimcall vundle#begin()" alternatively, pass a path where Vundle should install plugins"call vundle#begin('~/some/path/here')" let Vundle manage Vundle, requiredPlugin 'VundleVim/Vundle.vim'" The following are examples of different formats supported." Keep Plugin commands between vundle#begin/end." plugin on GitHub repoPlugin 'tpope/vim-fugitive'" plugin from http://vim-scripts.org/vim/scripts.html" Plugin 'L9'" Git plugin not hosted on GitHubPlugin 'git://git.wincent.com/command-t.git'" git repos on your local machine (i.e. when working on your own plugin)Plugin 'file:///home/gmarik/path/to/plugin'" The sparkup vim script is in a subdirectory of this repo called vim." Pass the path to set the runtimepath properly.Plugin 'rstacruz/sparkup', {'rtp': 'vim/'}" Install L9 and avoid a Naming conflict if you've already installed a" different version somewhere else." Plugin 'ascenator/L9', {'name': 'newL9'}" All of your Plugins must be added before the following linecall vundle#end() " requiredfiletype plugin indent on " required" To ignore plugin indent changes, instead use:"filetype plugin on"" Brief help" :PluginList - lists configured plugins" :PluginInstall - installs plugins; append `!` to update or just :PluginUpdate" :PluginSearch foo - searches for foo; append `!` to refresh local cache" :PluginClean - confirms removal of unused plugins; append `!` to auto-approve removal"" see :h vundle for more details or wiki for FAQ" Put your non-Plugin stuff after this line 注意:如果按这个文件配置,我在进行安装的时候回报错: 提示说 Plugin ‘file:///home/gmarik/path/to/plugin’ 找不到,文档中说:git repos on your local machine (i.e. when working on your own plugin) 意思是这个安装本地插件的一个目录,我这没有本地插件,所以我就把这句注释掉了。 4、 打开vim ,安装默认插件: 只在终端键入 vim,后面什么都不加 sudo vim –sudo临时提高权限,如果不加sudo,可能会遇到权限不够。 然后键入下面的命令 :PluginInstall 之后等待安装完成,[ :q ] 来退出即可 安装成功如下图所示: 安装2个常用插件1、 tagbar 这个插件可以浏览当前文件的标签,如果想更深的了解,GitHub地址为: https://github.com/majutsushi/tagbar效果图如下: 该插件安装之前需要先安装 ctags sudo apt-get install ctags 添加插件和其他配置信息到 ~/.vimrc 中 12345678910111213141516进入 ~/.vimrc# 添加以下 tagbar 插件Plugin 'majutsushi/tagbar'# 配置 tagbar 插件let g:tagbar_ctags_bin='ctags' "ctags 程序的路径let g:tagbar_width=30 "窗口宽度设置为 30let g:tagbar_left=1 "设置在 vim 左边显示let g:tagbar_map_openfold = "zv" "按 zv 组合键打开标签,默认 zc 关闭标签"如果是 C 语言的程序的话,tagbar 自动开启autocmd BufReadPost *.cpp,*.c,*.h,*.hpp,*.cc,*.cxx call tagbar#autoopen() "我设置 F2 为打开或者关闭的快捷键,根据你的习惯更改nnoremap <silent> <F2> :TagbarToggle<CR> 然后通过命令进行安装,语句和安装Vundle 第4步相同 2、 安装 vim-airline 插件 这个插件没有很大的实用性,但能增加逼格,增加vim的有趣性。 第一步,我们先把下面的需要配置的文件添加到 ~/.vimrc 中 123456789101112131415161718192021222324252627282930313233343536373839" ------------------------安装 vim-airline------------------set laststatus=2 " 永远显示状态栏set t_Co=256 " 在windows中用xshell连接打开vim可以显示色彩"Vim 在与屏幕/键盘交互时使用的编码(取决于实际的终端的设定) :set encoding=utf-8:set langmenu=zh_CN.UTF-8:set fileencodings=utf-8:set fileencoding=utf-8:set termencoding=utf-8Plugin 'vim-airline' let g:airline_theme="molokai""这个是安装字体后 必须设置此项" let g:airline_powerline_fonts = 1 "打开tabline功能,方便查看Buffer和切换,省去了minibufexpl插件 let g:airline#extensions#tabline#enabled = 1 let g:airline#extensions#tabline#buffer_nr_show = 1"设置切换Buffer快捷键"nnoremap <F4> :bn<CR>" 关闭状态显示空白符号计数 let g:airline#extensions#whitespace#enabled = 0 let g:airline#extensions#whitespace#symbol = '!' " 设置consolas字体"前面已经设置过 "set guifont=Consolas\ for\ Powerline\ FixedD:h11 if !exists('g:airline_symbols') let g:airline_symbols = {} endif " old vim-powerline symbols let g:airline_left_sep = '⮀' let g:airline_left_alt_sep = '⮁' let g:airline_right_sep = '⮂' let g:airline_right_alt_sep = '⮃' let g:airline_symbols.branch = '⭠' let g:airline_symbols.readonly = '⭤' 第二步:要安装字体,如果没有安装字体的话,vim-airline的效果就没法正确的显示 字体安装GitHub地址:https://github.com/powerline/fonts 在终端上一步步输入下面的内容即可: 12345678# clonegit clone https://github.com/powerline/fonts.git --depth=1# installcd fonts./install.sh# clean-up a bitcd ..rm -rf fonts 第三步:继续执行安装Vundle的第四步即可 效果图如下: 注意:在安装的时候,如果遇到权限不够,使用sudo vim 临时提高权限 使用vim时遇到的问题: 如何用vim命令把编辑文件的几行内容拷贝到一个新文件 — 如把58行到79行拷贝到~/test.txt文件可以使用下面的命令>> :58,79w!~/.test.txt]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux,vim,Vundle</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】总结]]></title>
<url>%2F2017%2F10%2F20%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3JVM%E6%80%BB%E7%BB%93%2F</url>
<content type="text"><![CDATA[总结经过三周的时间我把周志明的【深入理解java虚拟机】看完了,对我而言我感觉只是看到一些皮毛,以上的文章都是基于这本书来写的,基本上能理解的都写了上去,我也参考了许多大V的博客,下面我把主要参考博客链接发下下面,如果觉得我写的一般般,可以参考他们的,谢谢! 兰亭风雨:http://my.csdn.net/mmc_maodun ChangWen的博客: http://blog.csdn.net/oChangWen 生命壹号:http://www.cnblogs.com/smyhvae/category/587723.html 生命壹号对于JVM的总结:http://www.cnblogs.com/smyhvae/p/4810168.html 以下是我的总结:通过XMind见到总结了一下,希望对你有用: 一个菜鸟到一个大神,需要经历许多许多,但是只要梦想在,就要继续前进,因为有很多人需要你照顾。]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,总结</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之七:Javac编译与JIT编译]]></title>
<url>%2F2017%2F10%2F15%2FJavac%E7%BC%96%E8%AF%91%E4%B8%8EJIT%E7%BC%96%E8%AF%91%2F</url>
<content type="text"><![CDATA[概述 本文章参考周志明的【深入理解Java虚拟机】代码编译的结果从本地机器码转变成字节码,是存储格式发展的一小步,却是编译语言发展的一大步。 编译过程 java虚拟机的执行引擎在执行java代码的时候都有解释执行(通过解释器执行)和编译执行(通过编译器产生本地代码执行)这两种选择。 不论是解释还是编译,也不论是物理机还是虚拟机,大部分的程序代码从开始编译到最终转化成物理机的目标代码或虚拟机能执行的指令集之前,都会按照如下图所示的各个步骤进行: 其中绿色的模块可以选择性实现。很容易看出,上图中间的那条分支是解释执行的过程(即一条字节码一条字节码地解释执行,如JavaScript),而下面的那条分支就是传统编译原理中从源代码到目标机器代码的生成过程。 如今,基于物理机、虚拟机等的语言,大多都遵循这种基于现代经典编译原理的思路,在执行前先对程序源码进行词法解析和语法解析处理,把源码转化为抽象语法树。对于一门具体语言的实现来说,词法和语法分析乃至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是C/C++语言。也可以把抽象语法树或指令流之前的步骤实现一个半独立的编译器,这类代表是Java语言。又或者可以把这些步骤和执行引擎全部集中在一起实现,如大多数的JavaScript执行器。 Javac编译 在Java中提到“编译”,自然很容易想到Javac编译器将.java文件编译成为.class文件的过程,这里的Javac编译器称为前端编译器,其他的前端编译器还有诸如Eclipse JDT中的增量式编译器ECJ等。相对应的还有后端编译器,它在程序运行期间将字节码转变成机器码(现在的Java程序在运行时基本都是解释执行加编译执行),如HotSpot虚拟机自带的JIT(Just In Time Compiler)编译器(分Client端和Server端)。另外,有时候还有可能会碰到静态提前编译器(AOT,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码,如GCJ、Excelsior JET等,这类编译器我们应该比较少遇到。下面简要说下Javac编译(前端编译)的过程: 词法、语法分析 词法分析是将源代码的字符流转变为标记(Token)集合。单个字符是程序编写过程中的的最小元素,而标记则是编译过程的最小元素,关键字、变量名、字面量、运算符等都可以成为标记,比如整型标志int由三个字符构成,但是它只是一个标记,不可拆分。 语法分析是根据Token序列来构造抽象语法树的过程。抽象语法树是一种用来描述程序代码语法结构的树形表示方式,语法树的每一个节点都代表着程序代码中的一个语法结构,如bao、类型、修饰符、运算符等。经过这个步骤后,编译器就基本不会再对源码文件进行操作了,后续的操作都建立在抽象语法树之上。 填充符号表 完成了语法分析和词法分析之后,下一步就是填充符号表的过程。符号表是由一组符号地址和符号信息构成的表格。符号表中所登记的信息在编译的不同阶段都要用到,在语义分析(后面的步骤)中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,党对符号名进行地址分配时,符号表是地址分配的依据。 语义分析 语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是读结构上正确的源程序进行上下文有关性质的审查。语义分析过程分为标注检查和数据及控制流分析两个步骤: 标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量和赋值之间的数据类型是否匹配等。 数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题 字节码生成 字节码生成是Javac编译过程的最后一个阶段。字节码生成阶段不仅仅是把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。 实例构造器()方法和类构造器()方法就是在这个阶段添加到语法树之中的(这里的实例构造器并不是指默认的构造函数,而是指我们自己重载的构造函数,如果用户代码中没有提供任何构造函数,那编译器会自动添加一个没有参数、访问权限与当前类一致的默认构造函数,这个工作在填充符号表阶段就已经完成了)。 JIT编译 Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。 现在主流的商用虚拟机(如Sun HotSpot、IBM J9)中几乎都同时包含解释器和编译器(三大商用虚拟机之一的JRockit是个例外,它内部没有解释器,因此会有启动相应时间长之类的缺点,但它主要是面向服务端的应用,这类应用一般不会重点关注启动时间)。 二者各有优势: 当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行;当程序运行后,随着时间的推移,编译器逐渐会返回作用,把越来越多的代码编译成本地代码后,可以获取更高的执行效率。解释执行可以节约内存,而编译执行可以提升效率。 同时解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,当激进优化的假设不成立,如加载了新类后类型继承结构出现变化,出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行(部分没有解释器的虚拟机中也会采用不进行激进优化的C1编译器-(在虚拟机中习惯将Client compiler称为C1,将Server Compiler称为C2)担任“逃生门”的角色),因此,在整个虚拟机执行机构中,解释器与编译器经常配合工作,如下图: HotSpot虚拟机中内置了两个JIT编译器:Client Complier和Server Complier,分别用在客户端和服务端,目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。无论采用的编译器hiClient Compiler还是Server Compiler,解释器与编译器搭配使用的方式在虚拟机中称为“混合模式”(Mixed Mode)。 为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启用分层编译的策略(1.7默认开启),分层编译根据编译器编译,优化的规模与耗时,划分出不同的编译层次,其中包括: 第0层,程序解释执行,解释器不开启性能监控功能,可触发第1层编译 第1层,也成C1编译,将字节码编译为本地代码,进行简单,可靠的优化,如有必要将加入性能监控的逻辑。 第2层(或2层以上),也成C2编译,也是将字节码编译成本地代码,但是会启用一些耗时较长的优化甚至会根据性能监控信息进行一些不可靠的激进优化。 编译对象与触发条件运行过程中会被即时编译器编译的“热点代码”有两类: 被多次调用的方法。 被多次调用的循环体。 两种情况,编译器都是以整个方法作为编译对象,这种编译也是虚拟机中标准的编译方式。要知道一段代码或方法是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。目前主要的热点 判定方式有以下两种: 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系,缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。 在HotSpot虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。 方法调用计数器用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。。当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,按这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。 回边计数器用于统计一个方法中循环体代码执行的次数(准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令就称为“回边”在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。 触发了JIT编译后,在默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成为止(编译工作在后台线程中进行)。当编译工作完成后,下一次调用该方法或代码时,就会使用已编译的版本。 方法调用计数器触发即时编译的流程如下图: 回边计数器触发即时编译的流程和方法调用计数器流程基本相同,这里不再累述。 这个世界不会亏欠每一个在孤独中重生的人。]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,类加载</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之六:虚拟机字节码执行引擎]]></title>
<url>%2F2017%2F10%2F10%2F%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E%2F</url>
<content type="text"><![CDATA[概述 本文章参考周志明的【深入理解Java虚拟机】代码编译的结果从本地机器码转变成字节码,是存储格式发展的一小步,却是编译语言发展的一大步。 在Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,这个概念模型成为各种虚拟机执行引擎的统一外观(Facade)。在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎,但从外观上看起来,所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行过程,下面将从概念模型的角度来讲解虚拟机的方法调用和字节码执行。 运行时栈帧结构栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息等信息。每一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个当前栈帧关联的方法称为当前方法。执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。下图是栈帧结构的概念模型: 局部变量变局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位。虚拟机规范中并没有明确指明一个Slot应占的内存空间的大小,只是说到每个Slot都应该存放一个boolean,byte,char,short,int,float,reference和returnAddress这8种类型数据。第七种reference类型表示对一个对象实例的引用,虚拟机规范即没有说明它的长度,也没有明确指定这种应用应有怎样的结构。但一般来说,虚拟机实现至少都应该能通过这个引用做到两点:一是从此引用中直接或间接的查找到对象在java堆中的数据存放的起始地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息,否则无法实现java语言规范中定义的语法约束的约束。对于64位的数据类型,虚拟机会高位对齐的方式为其分配两个连续的Slot空间,java语言规定的64位数据类型只有long和double两种(reference类型可能是32位也可能是64位)。 虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始至局部变量表最大的Slot数量。如果访问的是32位数据类型的变量,索引n就代表了使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。对于两个相邻的共同存放一个64位数据的两个Slot,不允许采用任何方式单独访问其中的某一个,Java虚拟机规范中明确要求了如果遇到进行这种操作的字节码序列,虚拟机应该在类加载的校验阶段抛出异常。 在方法执行时,虚拟机是使用局部变量表完成参数变量列表的传递过程,如果是实例方法(非static方法),那么局部变量表中的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数,其余参数则按照参数列表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域来分配其余的Slot。 局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法,如果当前字节码PC计算器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其它变量使用。 注意:虽然Slot重用可以节省栈帧空间,但是重用栈帧会伴随一些额外的副作用。如,某些情况下,Slot的复用会影响垃圾收集行为。 影响之一:1234public static void main(String[] args) { byte[] placeholder = new byte[64*1024*1024]; System.gc(); } 运行结果: 12[GC (System.gc()) 68198K->66120K(125952K), 0.0027128 secs][Full GC (System.gc()) 66120K->66063K(125952K), 0.0080045 secs] 从结果中可以看出,在运行System.gc()后并没有回收64M的内存。主要原因是在执行GC时,变量placeholder还处于作用域之内,虚拟机无法回收。 修改代码如下: 123456public static void main(String[] args) { { byte[] placeholder = new byte[64*1024*1024]; } System.gc(); } 运行结果如下: 12[GC (System.gc()) 67532K->66176K(125952K), 0.0013205 secs][Full GC (System.gc()) 66176K->66060K(125952K), 0.0067795 secs] 加了花括号之后,placeholder的作用域被限制在花括号之内,从逻辑代码上讲,在执行System.gc()时,placeholder已经不可能被访问了,但结果发现,还是有64M的内存无法别回收。这又是为什么呢??? 下面我们继续修改代码: 1234567public static void main(String[] args) { { byte[] placeholder = new byte[64*1024*1024]; } int a =0; System.gc(); } 运行结果: 12[GC (System.gc()) 67532K->66176K(125952K), 0.0008704 secs][Full GC (System.gc()) 66176K->524K(125952K), 0.0063974 secs] 结果中我们发现,64M内存被回收了,这中间发生了什么?根本原因: placeholder被收回要看局部变量表中的slot是否还存有关于placeholder数组对象的引用,第一次修改中,代码虽然已经离开了placeholder的作用域,但在此以后,没有任何对局部变量表的读写操作,placeholder原本所占用的slot没有被其他变量复用,局部变量表仍保持着对它的关联。这种关联没有及时被打断,在绝大部分情况下影响都是很轻微的,但如果遇到一个方法,其后的代码有一些耗时很长的操作,而前面有定义了占用大量内存,实际上已经不会再使用的变量,手动将其设置为null(用来代替a=0,把变量对应的局部变量表Slot清空)便不见得是一个绝对无意义的操作。但是我们不应该对赋null值的操作有过多的依赖,原因是:从编码角度将,以恰当的变量作用域来控制变量回收时间才是最优雅的解决办法。 局部变量不像前面介绍的类变量那样存在“准备阶段”。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值。另外一次在初始化阶段,赋予程序员定义的值。因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔变量默认为false等这样的默认值。 操作数栈操作数栈也常被称为操作栈,它是一个后入先出栈。同局部变量表一样,操作数栈的最大深度也是编译的时候被写入到方法表的Code属性的max_stacks数据项中。 当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类检验阶段的数据流分析中还要再次验证这一点。以iadd指令为例,这个指令用于整型数加法,这在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况 另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了,如下图所示: Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。 下面我们来讲解一下 许多java虚拟机的执行引擎在执行java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择—-关于编译执行(JIT),在以后的文章中将会讲解。解释执行即对字节码逐条解释执行。 基于栈的解释器执行过程本节准备一段java代码,看看虚拟机总实际是如何执行的: 123456public int calc(){ int a=100; int b=200; int c =300; return (a+b)*c; } 使用javap命令看看它的字节码指令: javap 提示这段代码需要深度位2的操作数栈和4个Slot的局部变量空间。我们通过图的方式来观察javap字节码指令执行中的代码,操作数栈和局部变量表的变化:偏移地址为0的指令的情况: 偏移地址为1的指令的情况: 偏移地址为11的指令的情况: 偏移地址为12的指令的情况: 偏移地址为13的指令的情况: 偏移地址为14的指令的情况: 偏移地址为16的指令的情况: 上面的执行过程仅仅是一种概念模型,虚拟机最终会对执行过程做一些优化来提高性能,实际的运作过程不一定完全符合概念模型的描述…….更准确的说,实际情况会和上面描述的概念模型差距非常大,这种差距产生的原因是虚拟机中解析器和即时编译器都会对输入的字节码进行优化。 动态链接 每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(DynamicLinking)。通过前面类文件结构,我们知道在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接。 方法返回值地址当一个方法被执行后,有两种方式退出这个方法。 第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。 无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说, 方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。 而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有: ①.恢复上层方法的局部变量表和操作数栈,②.把返回值(如果有的话)压入调用都栈帧的操作数栈中,③.调用PC计数器的值以指向方法调用指令后面的一条指令等。 附加信息虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。所以可以说栈帧分为三部分:局部变量区、操作数栈和栈帧信息。 方法调用 方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期才能确定目标方法的直接引用。 解析 所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用。 在类加载的解析阶段会将一部分符号引用转化为直接引用,这种解析能成功的前提是:方法在程序真正运行之前就有一个课确定的调用版本,并且这个方法的调用版本在运行期是不可变的。也就是说,调用目标在程序代码写好,编译器进行编译时就确定下来,这类方法的调用称为解析(Resolution)。 在java语言中满足“编译期确定,运行期不变”的方法有静态方法和私有方法两大类。前者与类型直接关联,后者在外部不可被访问,这两种方式各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类加载阶段进行解析。 与之相对应的是 ,在Java虚拟机里提供了5条方法调用字节码指令(实际上在JVM jdk 1.6层面只有前面四种方法调用的指令),分别如下: 1).invokestatic:调用静态方法 2).invokespecial:调用类实例的构造器< init>方法、私有方法和父类方法 3).invokevirtual:调用所有的虚方法 4).invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。 5).invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 通过以上描述,我们可以得出下面两种概念:非虚方法:只要能被invokestatic和invokespecial指令调用的方法,都可以在类加载的时候把符号引用解析为该方法的直接引用。这里主要是指,私有方法,静态方法,实例构造器,父类方法.(Java中明确说明了final方法是一种非虚方法,虽然被invokevirtual调用,但它无法被覆盖,没有其它版本)虚方法(除去final方法),被invokevirtual和invokeinterface调用的则为虚方法,因为在编译期间并不能确定要调用的真正方法,所以称为虚方法。示例如下: 12345678public class StaticResolution { public static void sayHello(){ System.out.println("hello world"); } public static void main(String[] args) { StaticResolution.sayHello(); }} 使用javap命令查看字节码如下: 我们发现的确是通过invokestatic命令来调用sayHello()方法的 对于被final修饰的方法,虽然final方法是使用invokevirtual指令来调用,但是由于它无法被覆盖,没有其他的版本,所以也无须对方法接受者进行多态选择,又或者说多态选择的结果肯定是唯一的。在java语言规范中明确说明了final方法是一种非虚方法。 解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把符号引用转为直接引用,不会延迟到运行期再去完成。而分派(Dispatch)调用则可能是静态的也可能是动态的,根据分派依据的宗量数(本文后面有讲)可分为单分派和多分派。这两类分派方式的两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况。 分派众所周知java面向对象的三个重要特性,封装、继承、多态。而在jvm层面多态的实现由分派完成。分派有静态分派、动态分派。 静态分派–代表重载在讲解之前,我们先看下面的代码: 12345678910111213141516171819202122232425262728public class StaticDispatch { static abstract class Human {} static class Man extends Human {} static class Woman extends Human {} public void sayHello(Human human) { System.out.println("hello human"); } public void sayHello(Man man) { System.out.println("hello man"); } public void sayHello(Woman woman) { System.out.println("hello woman"); } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.sayHello(man); sr.sayHello(woman); }} 运行结果: 12hello ,guy!hello ,guy! 为什么会出现上面的结果呢???我们来看看反编译的结果: 解决以上 问题之前,我们先解释两个重要的概念:Human man = new Man();代码里的Human称为静态类型(或者叫外观类型):其变化仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的。而Man称为实际类型:其变化的结果在运行期才可确定,编译器不编译程序时并不知道一个对象的实际类型是什么。 main()方法的两次sayHello方法的调用,在方法接收者已经确定是对象sr的前提下,使用哪个重载版本,完全取决于传入参数的数量和数据类型,代码中刻意定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确的说是编译器)在重载时通过参数的静态类型,而不是实际类型作为判定依据。 而且通过以上的反编译结果我们可以发现,man和woman在编译的时候被强转成了Human类型,所以导致上面的结果 所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。 静态分派典型的应用是方法重载(虚拟机准确的说是编译器,在重载时是通过参数的静态类型而不是实际类型作为判定依据的)静态分派发生在编译阶段(也就是说在编译期是可知的),因此确定静态分派的动作实际上不是由虚拟机来执行的对于方法参数的匹配也是根据变量的静态类型来确定,在很多情况下根据参数的类型并不能找到”唯一的”方法调用,这个时候的处理方式是找到一个”最合适’的方法。 如下代码:123456789101112131415161718192021222324252627public class OverLoad { public static void sayHello(char arg) { System.out.println("hello char"); } public static void sayHello(int arg) { System.out.println("hello int"); } public static void sayHello(long arg) { System.out.println("hello long"); } public static void sayHello(Character arg) { System.out.println("hello Character"); } public static void sayHello(Serializable arg) { System.out.println("hello Serializable"); } public static void sayHello(Object arg) { System.out.println("hello object"); } public static void sayHello(char ...arg) { System.out.println("hello arg..."); } public static void main(String[] args) { sayHello('a'); }} 从头注解方法,结果会按顺序输出。注意:1、基本类型是重载按char->int->long->float->double顺序匹配的。 2、可变参数的重载优先级是最低的。 动态分派–代表重写想了解动态分派,必须先了解多态另一个体现—重写。如下示例: 1234567891011121314151617181920212223242526272829public class DynamicDispatch { static abstract class Human { protected abstract void sayHello(); } static class Man extends Human { @Override protected void sayHello() { System.out.println("man say hello"); } } static class Woman extends Human { @Override protected void sayHello() { System.out.println("woman say hello"); } } public static void main(String[] args) { Human man = new Man(); Human woman = new Woman(); man.sayHello(); woman.sayHello(); man = new Woman(); man.sayHello(); }} 运行结果: 123man say hello woman say hello woman say hello 显然这里不可能再根据静态类型来决定,因为静态类型同样都是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量在两次调用中执行了不同的方法。导致这个现象的原因:是这两个变量的实际类型不同。由于invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以两次调用的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。这种在运行期间根据实际类型确定方法执行版本的过程称为动态分派。动态分派的一个重要体现就是方法的重写,虽然父类引用可以指向子类对象,但是动态分派的方法调用是在运行时根据对象的实际类型去确认的。 单分派与多分派方法的接收者与方法的参数称为方法的宗量。 单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。 示例如下: 1234567891011121314151617181920212223242526272829public class Dispatch { static class QQ{} static class _360{} public static class Father{ public void hardChoice(QQ arg){ System.out.println("father choose 11"); } public void hardChoice(_360 arg){ System.out.println("father choose 360"); } } public static class Son extends Father{ //重写父类的方法 @Override public void hardChoice(QQ arg){ System.out.println("son choose qq"); } @Override public void hardChoice(_360 arg){ System.out.println("son choose 360"); } } public static void main(String[] args) { Father father = new Father(); Father son = new Son(); father.hardChoice(new _360()); son.hardChoice(new QQ()); }} 运行结果: 12father choose 360 son choose qq 看编译阶段编译器的选择过程,也就是静态分派的过程。这时选择目标方法的依据有两点:一是静态类型是Father还是Son,二是方法参数是QQ还是360。因为是根据两个宗量进行选择,所以Java语言的静态分派属于多分派类型。再看运行阶段虚拟机的选择,也就是动态分派的过程。在执行“son.hardChoose(new QQ())”这句代码时(准确的说是在执行这句代码所对应的invokevirtual指令),由于编译期已经决定目标方法的签名必须为hardChoice(QQ),虚拟机不会关心传递过来的参数”QQ”是什么,因为这时参数的静态类型、实际类型都对方的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。 Java语言是一门静态多分派,动态单分派的语言。 虚拟机动态分派的实现虚拟机在实际实现动态分派是基于性能考虑的。jvm在实现层面提供了一个叫做虚方法表的索引来代替元数据查找以提高性能,下面是书中的一张虚方法表结构图: 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果重写了这个方法,子类方法表中的地址将会替换指向子类实现版本的入口地址。Father是父类son是子类,并且子类重写了父类的连个方法,hardChoice(QQ),hardChoice(_360),因此子类中的这两个方法指向了Son的类型数据,而这两个类都继承自Object且没重写它的任何方法,因此都指向了Object的类型数据。方法表一般在类加载的链接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。 你敢不敢问自己到底要去哪里,背负着恐惧寻找的终点,非要是末路吗,你能听到吗,你还能听到吗,你还有勇气直面你的恐惧吗?]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,类加载</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之五:虚拟机的类加载机制]]></title>
<url>%2F2017%2F10%2F04%2F%E8%99%9A%E6%8B%9F%E6%9C%BA%E7%9A%84%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[概述 本文章参考周志明的【深入理解Java虚拟机】代码编译的结果从本地机器码转变成字节码,是存储格式发展的一小步,却是编译语言发展的一大步。 虚拟机吧描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。在java语言中,类型的加载,连接和初始化过程都是在程序运行期间完成的。 注意: 以下所讲的Class文件并非特指某个存在于具体磁盘中的文件,应当是一串二进制的字节流。 类加载的时机从类被加载到虚拟机内存中开始,到卸载出内存为止,类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段 。如下图: 其中验证、准备和解析三部分称为连接,在Java语言中,类型的加载和连接过程都是在程序运行期间完成的(Java可以动态扩展的语言特性就是依赖运行期动态加载、动态连接这个特点实现的),这样会在类加载时稍微增加一些性能开销,但是却为Java应用程序提供高度的灵活性 。 加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的(即:加载阶段必须在验证阶段开始之前开始,验证阶段必须在准备阶段开始之前开始等。这些阶段都是互相交叉地混合式进行的,通常会在一个阶段的执行过程中调用或激活另一个阶段),解析阶段则不一定,在某些情况下,解析阶段有可能在初始化阶段结束后开始,以支持Java的动态绑定。 什么情况下需要开始类加载过程的加载阶段? –Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始): 遇到new(使用new关键字实例化对象)、getstatic(获取一个类的静态字段,final修饰符修饰的静态字段除外)、putstatic(设置一个类的静态字段,final修饰符修饰的静态字段除外)和invokestatic(调用一个类的静态方法)这4条字节码指令时,如果类还没有初始化,则必须首先对其初始化 使用java.lang.reflect包中的方法对类进行反射调用时,如果类还没有初始化,则必须首先对其初始化 当初始化一个类时,如果其父类还没有初始化,则必须首先初始化其父类 当虚拟机启动时,需要指定一个主类(main方法所在的类),虚拟机会首选初始化这个主类 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化 对于这5中会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5中场景中的行为称为对一个类进行主动引用。除此之外的方式都不会触发初始化,称为被动引用。被动引用的示例:1.通过子类引用父类的静态字段,不会导致子类初始化。 123456789101112131415161718public class SuperClasses { public static int value = 123; static { System.out.println("SuperClasses init!"); }}class SubClass extends SuperClasses { static { System.out.println("SubClass init!"); }}class NotInitialization { public static void main(String[] args) { System.out.println(SubClass.value); }} 结果:SuperClasses init!123对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只有父类会被初始化,子类不会被初始化(子类的调用方式不符合5种直接引用中的任何一种)。至于是否会触发子类的加载和验证,虚拟机规范没有明确规定,视虚拟机具体实现而定,对Hotspot虚拟机,可通过-XX:+TraceClassLoading参数看到此操作会导致子类的加载从图中我们可以看出,子类被加载,却没有初始化。2.通过数组定义的引用类,不会触发此类的初始化 12345class NotInitialization { public static void main(String[] args){ SuperClasses[] temp = new SuperClasses[10]; }} 输出结果为空3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,所以不会触发定义常量的类的初始化 12345678910111213141516public class ConstClass { public static final int A = 1; static { System.out.println("ConstClass init!"); }}/** * 非主动使用类字段演示 */class Test { public static void main(String[] args){ System.out.println(ConstClass.A); }} 输出结果:1 在编译节阶段已将常量的值1存储到Test类的常量池中,对常量的Constant.A的引用实际上被转化为Test类对自身常量池的引用,这2个类在编译为Class文件后就没有任何关系了 注意:接口的加载过程与类加载过程不些不同,接口也有初始化的过程,虽然接口中不能使用静态代码块,但编译器仍然会为接口生成“()”类构造器,用于初始化接口中所定义的成员变量。接口与类真正的区别是前面讲得5种“有且仅有”需要开始初始化中的第3中: 当一个类初始化的时候,要求父类全部都已经初始化过了,但是一个接口在初始化时,不要求父类接口全部都完成初始化,只有在真正使用到接口(如使用到父类中定义的常量)时才会初始化。 类加载的过程详解类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作 加载1.通过“类全名”来获取定义此类的二进制字节流虚拟机规范对于“通过“类全名”来获取定义此类的二进制字节流”并没有指明二进制流必须要从一个本地class文件中获取,准确地说是根本没有指明要从哪里获取及怎样获取。例如: ①.从Zip包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。 ②.从网络获取,常见应用Applet。 ③.运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass来为特定接口生成$Prxoy的代理类的二进制字节流。 ④.由其他格式文件生成,典型场景:JSP应用 ⑤.从数据库中读取,这种场景相对少见,有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发 …………………….2.将字节流所代表的静态存储结构转换为方法区的运行时数据结构虚拟机规范并未规定方法区存储数据的具体数据结构,数据存储格式由虚拟机实现自行定义。3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较 特殊,它虽然是个对象,但在存放在方法区里!!!),这个对象作为程序访问方法区中的这些类型数据的外部接口。加载阶段与链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。 类加载器说到加载,不得不提到类加载器,下面就具体讲述下类加载器。虚拟机设计者把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块成为“类加载器”。 类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。 双亲委派模型站在Java虚拟机的角度来讲,只存在两种不同的类加载器: 启动类加载器:它使用C++实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),是虚拟机自身的一部分。 所有其他的类加载器:这些类加载器都由Java语言实现,独立于虚拟机之外,并且全部继承自抽象类java.lang.ClassLoader,这些类加载器需要由启动类加载器加载到内存中之后才能去加载其他的类。 站在Java开发人员的角度来看,类加载器可以大致划分为以下三类: 启动类加载器:Bootstrap ClassLoader,跟上面相同。它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。这些类加载器之间的关系一般如下图所示:上图的这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。 验证验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。 件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。 准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: 1、这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。 2、这里所说的初始值“通常情况”下是数据类型的零值。假设一个类变量定义为: public static int value = 12; 那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器< clinit>()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。 这里还需要注意如下几点: 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。下图列出了所有基本数据类型的零值: 数据类型 零值 int 0 long 0L short (short)0 char ‘\u0000’ byte (byte)0 boolean false float 0.0f double 0.0d reference null 3、初始值“通常情况”下是零值,但在“特殊情况”下:如果类字段的字段属性表中包含ConstantValue属性,那在准备阶段变量就会被初始化为ConstantValue属性所指定的值,即如果a变量定义变为public final static int a = 1;,编译时javac会为a生成ConstantValue属性,准备阶段虚拟机就会根据ConstantValue的设置将a的值置为123。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。 解析解析阶段是虚拟机将常量池内的符号引用特换成直接应用的过程。a、符号引用(Symbolic References):符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因此符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。b、直接引用(Direct References):直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。问:到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)?答:虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast等16个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断 对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符七类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info七种常量类型。但是下面我们只讨论前四种。 1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。 2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程如下图所示 最后需要注意:理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译。如下代码:123456789101112131415161718192021public class StaticTest { public static void main(String[] args) { System.out.println(Child.m); }}interface Super { public static int m = 11;}class Father { public static int m = 33; static { System.out.println("执行了父类静态语句块"); }}class Child extends Father implements Super { static { System.out.println("执行了子类静态语句块"); }} 结果编译不通过,会报如下错误: 3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。 4、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。 初始化类初始化阶段是“类加载过程”中最后一步,在之前的阶段,除了加载阶段用户应用程序可以通过自定义类加载器参与,其它阶段完全由虚拟机主导和控制,直到初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)在准备阶段,变量已经赋过一次初始值,在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其它资源,简单说,初始化阶段即虚拟机执行类构造器< clinit>()方法的过程,下面详细介绍下< clinit>方法:①.< clinit>由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定,特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。示例如下: 1234567public class JavaTest{ static { i = 2; //赋值而已编译通过 System.out.println(i); // 这句编译时会报“非法向前引用” } static int i = 1;} ②.类构造器< clinit>()方法与类的构造函数(实例构造函数< init>()方法)不同,它不需要显式调用父类构造,虚拟机会保证在子类< clinit>()方法执行之前,父类的< clinit>()方法已经执行完毕(这也意味着父类中定义的静态语句块要优于子类的变量赋值操作)。因此在虚拟机中的第一个执行的< clinit>()方法的类肯定是java.lang.Object。③.< clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句,也没有变量赋值的操作,那么编译器可以不为这个类生成< clinit>()方法。④.接口中不能使用静态语句块,,但是可以有类变量的赋值操作,故编译器也会对接口生成< clinit>()方法。但接口与类不同的是,执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。⑤.虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕。如果一个类的< clinit>()方法中有耗时很长的操作,那就可能造成多个进程阻塞。 1234567891011121314151617181920212223public class DeadLoopClass { static{ if (true) { System.out.println(Thread.currentThread() + " init DeadLoopClass"); while (true){} } } public static void main(String[] args) throws InterruptedException { Runnable script = new Runnable() { public void run() { System.out.println(Thread.currentThread() + " start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + " run over"); } }; Thread t1 = new Thread(script); Thread t2 = new Thread(script); t1.start(); t2.start(); }} 运行结果:Thread[Thread-0,5,main]startThread[Thread-0,5,main]startThread[Thread-0,5,main]init DeadLoopClass一条线程在死循环以模拟长时间操作,另外的线程在阻塞等待。 你敢不敢问自己到底要去哪里,背负着恐惧寻找的终点,非要是末路吗,你能听到吗,你还能听到吗,你还有勇气直面你的恐惧吗?]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,类加载</tag>
</tags>
</entry>
<entry>
<title><![CDATA[java的浅复制问题]]></title>
<url>%2F2017%2F10%2F03%2Fjava%E6%B5%85%E5%A4%8D%E5%88%B6%E9%97%AE%E9%A2%98%2F</url>
<content type="text"><![CDATA[概述 昨天逛知乎看到一个java浅复制的问题,看到很多的解释我并不是很理解,所以今天特意写出来,讨论一下。 问题链接地址:(看一下链接的内容,然后下面的内容都是依据链接内容进行的分析)https://www.zhihu.com/question/66099841/answer/238247431 浅复制 什么是浅复制呢? 浅复制(shallow copy)只是复制了对象的引用,而不是对象本身的拷贝。 如果你看到上面链接的内容,你应该对 “胖胖” 这位大V的回答有了一定的了解,对他回答的内容我表示基本赞同,但是我却又一些疑问。 Integer定义的时候,数值在小于127的时候,里面的值都是从数组缓存中取出的(如果对着有疑问请看 Integer= =比较时127相等128不相等的原因 ),也就是说同一个数应该是同一个对象。假如我们定义两个数数组a1,和a2,进行数组值的替换,代码如下: 12345678Integer[] a1 = new Integer[]{12,23,34,45}; Integer[] a2 = new Integer[4]; System.arraycopy(a1, 0, a2, 0, a1.length); //使用System静态方法进行复制(各参数含义请查API) System.out.println("a1="+Arrays.toString(a1)); System.out.println("a2="+Arrays.toString(a2)); a2[1] = 3; System.out.println("a1="+Arrays.toString(a1)); System.out.println("a2="+Arrays.toString(a2)); 结果为: 12345a1=[12, 23, 34, 45]a2=[12, 23, 34, 45]a1=[12, 23, 34, 45]a2=[12, 3, 34, 45] 从结果可以看出,在复制之后,a1,a2里的数据的值,不在是同一个对象,因为如果是浅复制的话,那么a1的值应该也会改变。 通过以上的疑问,我产生了一下的怀疑:1、是不是复制以后,两个数组里的值不再是同一个对象,然后改变一个数组的值后,另一个不在改变。但是我看到 “胖胖” 大V最后的回答,我给予了否定,我们来看看他的代码: 12345678910111213141516//定义一个Node对象public class Node { private int node; public Node(int node) { this.node = node; } public int getNode() { return node; } public void setNode(int node) { this.node = node; } public String toString() { return String.valueOf(node); }} 测试结果: 123456789Node[] arr1 = new Node[10]; Node[] arr2 = new Node[10]; Arrays.fill(arr1, new Node(1));//将指定的 Node值分配给指定 Node 型数组的每个元素 System.arraycopy(arr1, 0, arr2, 0, arr1.length); System.out.println("arr1="+Arrays.toString(arr1)); System.out.println("arr2="+Arrays.toString(arr2)); arr1[1].setNode(2); System.out.println("arr1="+Arrays.toString(arr1)); System.out.println("arr2="+Arrays.toString(arr2)); 结果为: 1234arr1=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]arr2=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]arr1=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2]arr2=[2, 2, 2, 2, 2, 2, 2, 2, 2, 2] 通过上面的测试例子,我们可以说Node在arr1和arr2里的值指向了同一个引用。修改其中的一个我们可以改变另一个数组的值。所以我的以上的怀疑并不正确。我也看过两个数组的 hashcode,虽然并不相等,但是这里不相等是因为,定义数组是两个数组对象,根据数组在堆中的存储,并且数组的值可以指向其他的引用,所以并不能说明任何问题。 那到底是什么问题呢,其实我也不知道,如果有朋友知道这个问题的答案可以帮我解答一下吗?下面附上我的邮箱: Gmail邮箱:[email protected]邮箱:[email protected]如果你看到这篇文章,并且知道答案,如果你愿意把你的答案分享出去,请发送邮件或者加我微信,给与我解答,非常感谢! 刚刚写完文章就收到了”胖胖”大V的回复,他给我一个链接,我看了以后懵懵懂懂,链接如下: java到底是值传递还是引用传递?看了链接,我觉得问题应该出在=赋值操作上,我也没法解释了,有点迷茫。如果谁有更好的解释,可以回复我,感激不尽。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>JavaSE,shallow copy</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何理解 String 类型值的不可变?]]></title>
<url>%2F2017%2F09%2F27%2F%E5%A6%82%E4%BD%95%E7%90%86%E8%A7%A3%20String%20%E7%B1%BB%E5%9E%8B%E5%80%BC%E7%9A%84%E4%B8%8D%E5%8F%AF%E5%8F%98%2F</url>
<content type="text"><![CDATA[概要我们都知道String类的对象是一个典型的不可变对象,我们调用它的subString(),replace(),concat()这些方法都不会影响它原来的值,只会返回一个新构建的对象。如下图,给一个已有字符串”abc”第二次赋值成”abcde”,不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。 String为什么不可变?翻开源码如下:首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[ ]数组,而且是用final修饰的。final修饰的字段创建以后就不可改变。因为虽然value是不可变,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。数组的本体结构在heap堆。String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。如下示例: 123final int[] value={1,2,3}int[] another={4,5,6};value=another; //编译器报错,final不可变 value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果我直接对数组元素动手,情况就会发生变化。 12final int[] value={1,2,3};value[2]=100; //这时候数组里已经是{1,2,100} 所以为了保证String不可变,String类中把char类型的数组设置成private的,外界不允许访问,而且设计者把整个String类设计成final禁止继承,避免了String被改变。所以String是不可变的关键都在底层的实现,而不是一个final。 String 不可变的好处1、不可变的好处,就要牵扯到字符串常量池了,只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多heap空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现(译者注:String interning是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串。),因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。 12String s1 = "aaddcc";String s2 = "aaddcc"; 这样在大量使用字符串的情况下,可以节省内存空间,提高效率。但之所以能实现这个特性,String的不可变性是最基本的一个必要条件。要是内存里字符串内容能改来改去,这么做就完全没有意义了。 2、因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。3、因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。4、如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。 除了上面提到的String之外,常用的还有枚举类,以及java.lang.Number的部分子类,如Long和Double等数值包装类,BigInteger和BigDecimal等大数据类型。 StringBuilder和StringBufferString 覆盖了 equals 方法和 hashCode 方法,而 StringBuffer和StringBuilder没有覆盖 equals 方法和 hashCode 方法,所以,将 StringBuffer对象存储进 Java集合类中时会出现问题。 StringBuffer(线程安全) 和 StringBuilder都是继承自AbstractStringBuilder,底层都是数组,java9之前使用char,之后是byte,这个内部数组应该创建多大呢?目前的实现时,构建时初始字符串长度16。 延伸String的特性》1、String类是final的,不可被继承。2、String类是的本质是字符数组char[], 并且其值不可改变。3、String类对象有个特殊的创建的方式,就是直接指定比如String x = “abc”,”abc”就表示一个字符串对象。而x是”abc”对象的地址,也叫做”abc”对象的引用。4、String对象可以通过“+”串联。串联后会生成新的字符串。5、Java运行时会维护一个String Pool(String池),JavaDoc翻译很模糊“字符串缓冲区”。String池用来存放运行时中产生的各种字符串,并且池中的字符串的内容不重复。而一般对象不存在这个缓冲池,并且创建的对象仅仅存在于方法的堆栈区。6、创建字符串的方式很多,归纳起来有三类:其一,使用new关键字创建字符串,比如String s1 = new String(“abc”);其二,直接指定。比如String s2 = “abc”;其三,使用串联生成新的字符串。比如String s3 = “ab” + “c”; 《String对象的创建》String对象的创建也有很多门道,关键是要明白其原理。 原理1:当使用任何方式来创建一个字符串对象s=X时,Java运行时(运行中JVM)会拿着这个X在String池中找是否存在内容相同的字符串对象,如果不存在,则在池中创建一个字符串s,否则,不在池中添加。原理2:Java中,只要使用new关键字来创建对象,则一定会(在堆区或栈区)创建一个新的对象。原理3:使用直接指定或者使用纯字符串串联来创建String对象,则仅仅会检查维护String池中的字符串,池中没有就在池中创建一个,有则罢了!但绝不会在堆栈区再去创建该String对象。原理4:使用包含变量的表达式来创建String对象,则不仅会检查维护String池,而且还会在堆栈区创建一个String对象。 《不可变类》JAVA为了提高效率,对String类型进行了特别的处理---为string类型提供了串池定义一个string类型的变量有两种方式:12string name= "tom ";(String name="t"+"o"+"m"的效果和此处是相同的)string name =new string( "tom ") 如果你使用了第一种方式,那么当你在声明一个内容也是 “tom “的string时,它将使用串池里原来的那个内存,而不会重新分配内存,也就是说,string saname= “tom “,将会指向同一块内存。而如果用第二种方式,不管串池里有没有”tom”,它都会在堆中重新分配一块内存,定义一个新的对象。另外关于string类型是不可改变的问题: string类型是不可改变的,也就是说,当你想改变一个string对象的时候,比如name= “madding “ 那么虚拟机不会改变原来的对象,而是生成一个新的string对象,然后让name去指向它,如果原来的那个 “tom “没有任何对象去引用它,虚拟机的垃圾回收机制将接收它。最后,关于定义String的堆栈问题:String s =new String()分析堆与栈,是先定义S,还是先new string()??? 112String str1 = "abc";System.out.println(str1 == "abc"); 步骤:1) 栈中开辟一块空间存放引用str1;2) String池中开辟一块空间,存放String常量”abc”;3) 引用str1指向池中String常量”abc”;4) str1所指代的地址即常量”abc”所在地址,输出为true; 212String str2 = new String("abc");System.out.println(str2 == "abc"); 步骤:1) 栈中开辟一块空间存放引用str2;2) 堆中开辟一块空间存放一个新建的String对象”abc”;3) 引用str2指向堆中的新建的String对象”abc”;4) str2所指代的对象地址为堆中地址,而常量”abc”地址在池中,输出为false; 312String str3 = new String("abc");System.out.println(str3 == str2); 步骤:1) 栈中开辟一块空间存放引用str3;2) 堆中开辟一块新空间存放另外一个(不同于str2所指)新建的String对象;3) 引用str3指向另外新建的那个String对象 ;4) str3和str2指向堆中不同的String对象,地址也不相同,输出为false; 412String str4 = "a" + "b";System.out.println(str4 == "ab"); 步骤:1) 栈中开辟一块空间存放引用str4;2) 根据编译器合并已知量的优化功能,池中开辟一块空间,存放合并后的String常量”ab”;3) 引用str4指向池中常量”ab”;4) str4所指即池中常量”ab”,输出为true; 5123final String s = "a"; //注意:这里s用final修饰,相当于一个常量String str5 = s + "b";System.out.println(str5 == "ab"); 步骤:同四 61234String s1 = "a";String s2 = "b";String str6 = s1 + s2;System.out.println(str6 == "ab"); 步骤:1) 栈中开辟一块中间存放引用s1,s1指向池中String常量”a”,2) 栈中开辟一块中间存放引用s2,s2指向池中String常量”b”,3) 栈中开辟一块中间存放引用str5,4) s1 + s2通过StringBuilder的最后一步toString()方法还原一个新的String对象”ab”,因此堆中开辟一块空间存放此对象,5) 引用str6指向堆中(s1 + s2)所还原的新String对象,6) str6指向的对象在堆中,而常量”ab”在池中,输出为false]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java,String</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Integer用==比较时127相等128不相等的原因]]></title>
<url>%2F2017%2F09%2F27%2FInteger%E7%94%A8%3D%3D%E6%AF%94%E8%BE%83%E6%97%B6127%E7%9B%B8%E7%AD%89128%E4%B8%8D%E7%9B%B8%E7%AD%89%E7%9A%84%E5%8E%9F%E5%9B%A0%2F</url>
<content type="text"><![CDATA[问题关于以上问题,我们首先先看如下代码: 1234567891011121314public static void main(String[] args) { Integer a =1; Integer b =2; Integer c =3; Integer d =3; Integer e =321; Integer f =321; Long g = 3L; System.out.println(c==d); //true System.out.println(e == f); System.out.println(c == (a+b)); //true System.out.println(c.equals(a+b)); //true System.out.println(g==(a+b)); //true System.out.println(g.equals(a+b)); //false 运行的结果为: 123456truefalsetruetruetruefalse 咱们来看看反编译的代码: 1234567891011121314public static void main(String[] args) { Integer a = Integer.valueOf(1); Integer b = Integer.valueOf(2); Integer c = Integer.valueOf(3); Integer d = Integer.valueOf(3); Integer e = Integer.valueOf(321); Integer f = Integer.valueOf(321); Long g = Long.valueOf(3L); System.out.println(c == d); System.out.println(e == f); System.out.println(c.intValue() == a.intValue() + b.intValue()); System.out.println(c.equals(Integer.valueOf(a.intValue() + b.intValue()))); System.out.println(g.longValue() == (long) (a.intValue() + b.intValue())); System.out.println(g.equals(Integer.valueOf(a.intValue() + b.intValue()))); 第一个输出语句是true,第二个输出语句出现false主要是加入自动拆装箱而出现的问题,而==比较的是两个对象是不是同一个对象,如果上面的结果推算,也就是e和f经过自动拆装箱之后生成的对象不再是一个,而c和d仍是统一个对象。为什么会出现这种情况呢?我们来看看cdef打印的地址: 123//打印cdef的内存地址 System.out.println(c+"-"+d+":"+System.identityHashCode(c)+"-"+System.identityHashCode(d)); System.out.println(e+"-"+f+":"+System.identityHashCode(e)+"-"+System.identityHashCode(f)); 结果如下图所示: 可以看出与我们推测的一样,cd经过自动装箱后认为同一个对象,而ef经过自动差装箱后不再为通过各对象,这主要是自动拆装箱带来的后果。查看源码:“从0到127不同时候自动装箱得到的是同一个对象”就只能有一种解释:自动装箱并不一定new出新的对象。查看Integer.valueOf()源码如下:其注释里就直接说明了-128到127之间的值都是直接从缓存中取出的。看看是怎么实现的:如果int型参数i在IntegerCache.low和IntegerCache.high范围内,则直接由IntegerCache返回;否则new一个新的对象返回。似乎IntegerCache.low就是-128,IntegerCache.high就是127了。看看IntegerCache的源码: 1234567891011121314151617181920212223242526272829private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } 果然在其static块中就一次性生成了-128到127直接的Integer类型变量存储在cache[]中,对于-128到127之间的int类型,返回的都是同一个Integer类型对象。这下真相大白了,整个工作过程就是:Integer.class在装载(Java虚拟机启动)时,其内部类型IntegerCache的static块即开始执行,实例化并暂存数值在-128到127之间的Integer类型对象。当自动装箱int型值在-128到127之间时,即直接返回IntegerCache中暂存的Integer类型对象。为什么Java这么设计?我想是出于效率考虑,因为自动装箱经常遇到,尤其是小数值的自动装箱;而如果每次自动装箱都触发new,在堆中分配内存,就显得太慢了;所以不如预先将那些常用的值提前生成好,自动装箱时直接拿出来返回。哪些值是常用的?就是-128到127了。我们继续看第5和第6个输出语句,发现两种比较的方式,输出的结果却不同,为什么会这样呢?通过反编译代码我们发现,第5个输出语句使用了强制类型转换,而第6个却没有,这又是为什么呢?答:主要原因是包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,,equals方法不处理数据转换的关系,所以第6句没有强制类型转换。注意,Integer、Short、Byte、Character、Long这几个类的valueOf方法的实现是类似的,Double、Float的valueOf方法的实现是类似的。 我们来试验Double类型,如下代码: 1234567System.out.println("-------Double---------");Double i1 = 100.0;Double i2 = 100.0;Double i3 = 200.0;Double i4 = 200.0;System.out.println(i1==i2);System.out.println(i3==i4); 结果:falsefalse我们来看Double的valueof方法:然后我们观察Boolean类型,代码如下: 1234567System.out.println("-------Boolean--------"); Boolean b1 = false; Boolean b2 = false; Boolean b3 = true; Boolean b4 = true; System.out.println(b1==b2); System.out.println(b3==b4); 结果:truetrue我们来看看Boolean的valueof方法 基本类型 == equals 字符串变量 对象的内存地址 内容 非字符串变量 地址 内容 基本类型 值 不可以(没有强制类型转换,不能使用) 包装类 地址(不同的包装类处理方式不同,Integer大小在127前在缓存中取,地址相同) 比较的是两边的包装类是否为同一个对象 Integer包装类的equals方法:我们看到包装类的equals方法主要是比较两边是不是同一个对像。 延伸int和Integer的区别1、Integer是int的包装类,int则是java的一种基本数据类型2、Integer变量必须实例化后才能使用,而int变量不需要3、Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值4、Integer的默认值是null,int的默认值是0 关于Integer和int的比较1、由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。 123Integer i = new Integer(100);Integer j = new Integer(100);System.out.print(i == j); //false 2、Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较) 123Integer i = new Integer(100);int j = 100;System.out.print(i == j); //true 3、非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同) 123Integer i = new Integer(100);Integer j = 100;System.out.print(i == j); //false 4、对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false 1234567Integer i = 100;Integer j = 100;System.out.print(i == j); //trueInteger i = 128;Integer j = 128;System.out.print(i == j); //false]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Java,自动拆装箱</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之四:字节码指令简介]]></title>
<url>%2F2017%2F09%2F26%2F%E5%AD%97%E8%8A%82%E7%A0%81%E6%8C%87%E4%BB%A4%E7%AE%80%E4%BB%8B%2F</url>
<content type="text"><![CDATA[简介java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。由于java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。 字节码与数据类型在java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息,例如,iload指令用于从局部变量表中加载int型的数据到操作数栈,而fload指令加载的则是float类型的数据。对于大部分与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为那种数据类型服务:i代表int类型,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确的指明操作符类型的字母,如arraylengh指令。大部分的指令都没有支持整数类型byte,char和short,甚至没有任何指令支持boolean类型,编译器会在编译期或者运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据。与之相似,在处理boolean,byte,short和char类型数组时,也会转换成为使用对应的int类型的字节码指令来处理。 加载和存储指令加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,指令包括: 将一个局部变量加载到操作数栈:iload,iload_,lload,lload_,fload,float_,dload,dload_,aload,aload_。 将一个数值从操作数栈存储到局部变量表:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_. 将一个常量加载到操作数栈:bipush,sipush , ldc,ldc_w , ldc2_w, aconst_null, iconst_ml, iconst_,lconst_, fconst_, dconst_. 扩充局部变量表的访问索引的指令:wide 运算指令运算或算术指令用于对两个操作数栈上的值惊醒某种特定运算,并把结果重新存入到操作数栈。 加法指令:iadd,ladd,fadd,dadd 减法指令:isub,lsub,fsub,dsub 乘法指令:imul,lmul,fmul,dmul 除法指令:idiv,ldiv,fdiv,ddiv 求余指令:irem,lrem,frem,drem 取反指令:ineg,lneg,fneg,dneg 位移指令:ishl,ishr,iushr,lshl,lshr,lushr 按位或指令:ior,lor 按位与指令:iand,land 按位异或指令:ixor,lxor 局部变量自增指令:iinc 比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp 类型转换指令 你敢不敢问自己到底要去哪里,背负着恐惧寻找的终点,非要是末路吗,你能听到吗,你还能听到吗,你还有勇气直面你的恐惧吗?]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,Class类</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之三:Class类文件结构]]></title>
<url>%2F2017%2F09%2F22%2FClass%E7%B1%BB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%2F</url>
<content type="text"><![CDATA[概述 本文章参考周志明的【深入理解Java虚拟机】代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。提示:如果想系统的了解java虚拟机,我建议买一本周志明写的【深入理解Java虚拟机】来看看,通俗易懂,一本圣经。 Class类文件结构Class文件是一组以8为字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。根据Java虚拟机规范规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据的,这种微结构中只有两种数据类型:无符号数和表。1.无符号数无符号数属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。2.表 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info” 结尾。表是用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表,它是由图1所示的数据项构成。 在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型数据的集合。 魔数与Class文件的版本每个Class文件的头4个字节成为魔数(Magic Number),它唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。很多文件存储标准都使用魔数进行身份的识别,如图片格式 gif或jpeg等在文件头都有魔数。使用魔数而不是扩展名进行身份识别主要基于安全考虑,因为扩展名可以所以的改动。文件格式的制定者可以自由选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混乱即可。Class文件的魔数是0xCAFEBABE。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。示例代码: 123456789package com.fan.JVMDemo;public class JavaTest { public static void main(String[] args) { byte[] b = null; for (int i = 0; i < 10; i++) b = new byte[1 * 1024 * 1024]; }} 下图显示的是使用十六进制编辑器WinHex打开的Class文件的结果。可以看到开头的4个字节的16进制表示是0xCAFEBABE,代表此版本号的是第5和第6个字节值为0x0000,主版本号是0x0034,也即是10进制的52。 这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。 0X0034(对应十进制的52):JDK1.8 0X0033(对应十进制的51):JDK1.7 0X0032(对应十进制的50):JDK1.6 0X0031(对应十进制的49):JDK1.5 0X0030(对应十进制的48):JDK1.4 0X002F(对应十进制的47):JDK1.3 0X002E(对应十进制的46):JDK1.2 常量池紧接着魔数与版本号之后的是常量池入口,常量池简单理解为class文件的资源从库,它是Class文件结构中与其它项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在文件中第一个出现的表类型数据项目。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。这个容量计数器从1开始计数。如下图中,常量池容量(十六进制0x001B),即使十进制的27,,这就代表常量池中有26项常量,索引值范围为1-26.这样做的目的是有特殊考虑的, 第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的意思,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数器是从1开始的,对与其它集合类型,包括接口索引集合,字段表集合,方法表集合等容量计数都是从0开始。使用Javap命令输出常量表,如下:从表中可以发现总共有26个,也就是说常量池容量为26。常量池之中主要存放两大类常量: 1).字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等 2).符号引用: 属于编译原理方面的概念,包括了下面三类常量: ①.类和接口的全限定名 ②.字段的名称和描述符 ③.方法的名称和描述符 Java代码在进行Java编译的时候,并不像C和C++那样有”连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。 常量池中每一项常量都是一个表,在jdk1.7之前共有11中结构各不相同的表结构数据,之后又增加了3中。 这14中都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前这个属性属于那种类型常量。具体如下图所示: 符号引用与直接引用的关联 符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。 直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。 访问标志在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。具体的标志位以及标志的含义见下表: 标志名称 标志值 含义 ACC_PUBLIC 0x0001 是否为public类型 ACC_FINAL 0x0010 是否被声明为final,只有类可设置 ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志必须为真 ACC_INTERFACE 0x0200 标识这是一个接口 ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口和抽象类,此标志为真,其它类为假 ACC_SYNTHETIC 0x1000 标识别这个类并非由用户代码产生 ACC_ANNOTATION 0x2000 标识这是一个注解 ACC_ENUM 0x4000 标识这是一个枚举 access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。仍以开始代码为例,类JavaTest为一个普通的类,不是借口,枚举或者注解,被public修饰但没有声明final和abstract,用的是JDK1.2之后的编译器进行编译的。所以ACC_PUBLIC,ACC_SUPER标志应当为真,其他为假。所以access_flags的值为:0x0001|0x0020 = 0x0021。结果如下图: 类索引、父类索引与接口索引集合类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而借口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。 类索引(this_class),用于确定这个类的全限定名,占2字节,父类索引(super_class),用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中。 this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。 类索引,父类索引与接口索引的内容如下图所示: 测试类对应的this_class的值为0x0001,即常量池中第1个常量,super_class的值为0x0003,即常量池中的第3个常量,interfaces_counts的值为0x0000,故接口索引集合大小为0. 字段表集合字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的变量。 在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示,字段表格式如下表所示: 类型 名称 数量 u2 access_flags 1 u2 name_index 1 u2 descriptor_index 1 u2 attributes_count 1 attribute_info attributes attributes_count 字段表里的字段修饰符放在access_flags中,占2个字节,与类中的访问标志(access_flags)十分相似,都是u2的数据类型,其中可以设置的标志位和含义见表: 标志名称 标志值 含义 ACC_PUBLIC 0x0001 字段是否为public ACC_PRIVATE 0x0002 字段是否为private ACC_PROTECTED 0x0004 字段是否为protected ACC_STATIC 0x0008 字段是否为static ACC_FINAL 0x0010 字段是否为final ACC_VOLATILE 0x0040 字段是否为volatile ACC_TRANSIENT 0x0080 字段是否为transient ACC_SYNTHETIC 0x1000 字段是否为编译器自动产生 ACC_ENUM 0x4000 字段是否为enum 当然实际上,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED这3个标志只能选择一个,接口中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,Class文件对此并无规定,这些都是java语言所要求的。跟随access_flags标志的是两项索引值:name_index和descriptor_index。他们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。简单名称,描述符的解释:简单名称:是指没有类型和参数修饰的方法或者字段名称,这一个类的main()方法的简单名称时main。描述符:作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值。根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象的权限定名来表示,如下表: 标识字符 含义 B 基本类型byte C 基本类型char D 基本类型double F 基本类型float I 基本类型int J 基本类型long S 基本类型short Z 基本类型boolean V 基本类型void L 对象类型,如Ljava/lang/Object 对于数组类型,每一维度将使用一个前置的“[” 字符来描述,如一个定义为“java.lang.String[ ][ ]”类型的二维数组,将被记录为:”[[Ljava/lang/String” ,一个整形数组“int[]“将被记录为”[I“。用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内,如方法void inc() 的描述符是“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String“。开始的代码案例不能有效说明字段集合,所以下面我们以如下的代码来说明:代码二: 12345678package com.fan.JVMDemo;public class TestClass { private int m; public int inc(){ return m+1; }} 字段表结构实例如下图: 从图中可以看到,fields_count的值为0x0001,说明这个类只有一个字段表数据,接下来是access_flags表示,值为0x0002,代表private修饰符,代表字段名称的name_index的值为0x0005,从代码清单第五项常量是m,代表字段描述符的值为0 x0006,指向常量池的字符串“I”,根据这些信息我们可以推算字段为“private int m” 字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段. Java语言中字段是不能重载的,2个字段无论数据类型、修饰符是否相同,都不能使用相同的名称;但是对于字节码,只要字段描述符不同,字段重名就是合法的. 方法表集合方法表的结构如同字段表一样,依次包括了访问标志(access_flags),名称索引(name_index),描述符索引(descriptor_index),属性表集合(attributes)几项;如下表所示: 类型 名称 数量 u2 access_flags 1 u2 name_index 1 u2 descriptor_index 1 u2 attributes_count 1 attribute_info attributes attributes_count 由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加ACC_SYNCHRONIZED标志、ACC_NATIVE标志、ACC_STRICTFP标志和ACC_ABSTRACT标志。 access_flags为0x0001,即public;name_index为0x0007,即常量池中第7个常量;descriptor_index为0x0008,即常量池中第8个常量, 接下来2个字节为属性计数器,其值为0x0001,说明这个方法的属性表集合中有一个属性(详细说明见后面“八、属性表集合”), 属性名称为接下来2位0x0009,指向常量池中第9个常量:Code。 在Java语言中,重载一个方法除了要求和原方法拥有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,由于特征签名不包含返回值,故Java语言中不能仅仅依靠返回值的不同对一个已有的方法重载;但是在Class文件格式中,特征签名即为方法描述符,只要是描述符不完全相同的2个方法也可以合法共存,即2个除了返回值不同之外完全相同的方法在Class文件中也可以合法共存 属性表集合属性表在Class文件,字段表,方法表都可以携带自己的表集合,以用来描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序,长度和内容不同,属性表集合的限制稍微宽松了一些,不在要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以想属性表中写入自己定义的信息,java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项),如下表: 属性名称 使用位置 含义 Code 方法表 Java代码编译成的字节码指令 ConstantValue 字段表 final关键字定义的常量值 Deprecated 类文件、字段表、方法表 被声明为deprecated的方法和字段 Exceptions 方法表 方法抛出的异常 InnerClasses 类文件 内部类列表 LineNumberTale Code属性 Java源码的行号与字节码指令的对应关系 LocalVariableTable Code属性 方法的局部变量描述 SourceFile 类文件 源文件名称 Synthetic 类文件、方法表、字段表 标识方法或字段是由编译器自动生成的 关于属性表集合这里不再详细介绍,如果想深入理解,请参考周志明的【深入理解java虚拟机】]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,Class类</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之二:垃圾收集器与常用JVM配置参数]]></title>
<url>%2F2017%2F09%2F20%2Fjava%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8%E4%B8%8E%E5%B8%B8%E7%94%A8JVM%E9%85%8D%E7%BD%AE%E5%8F%82%E6%95%B0%2F</url>
<content type="text"><![CDATA[垃圾收集器概述 本文章参考周志明的【深入理解Java虚拟机】java与C++支架有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 GC的概述1.GC:Garbage Collection 垃圾收集。这里所谓的垃圾指的是在系统运行过程当中所产生的一些无用的对象,这些对象占据着一定的内存空间,如果长期不被释放,可能导致OOM。在C/C++里是由程序猿自己去申请、管理和释放内存空间,因此没有GC的概念。而在Java中,后台专门有一个专门用于垃圾回收的线程来进行监控、扫描,自动将一些无用的内存进行释放,这就是垃圾收集的一个基本思想,目的在于防止由程序猿引入的人为的内存泄露。2.内存区域中的程序计数器、虚拟机栈、本地方法栈这3个区域随着线程而生,线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈的操作,每个栈帧中分配多少内存基本是在类结构确定下来时就已知的。在这几个区域不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟着回收了。而Java堆和方法区则不同,一个接口中的多个实现类需要的内存可能不同,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,GC关注的也是这部分内存,后面的文章中如果涉及到“内存”分配与回收也仅指着一部分内存。 对象存活算法堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象).1.引用计数算法 很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。 引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。 例子: 1234567891011121314151617181920212223public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /** * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过 */ private byte[] bigSize = new byte[2 * _1MB]; public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; //假设在这行发生了GC,objA和ojbB是否被回收 System.gc(); } } 运行结果: 从结果中可以看出虚拟机启动GC后,日志中包含“6758K -> 592K”(红色的框),意味着虚拟机并没有因为这两个对象相互引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数算法来判断对象是否存活的。2.可达性分析算法 在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。 在Java语言里,可作为GC Roots对象的包括如下几种: a.虚拟机栈(栈桢中的本地变量表)中的引用的对象 b.方法区中的类静态属性引用的对象 c.方法区中的常量引用的对象 d.本地方法栈中JNI的引用的对象 引用Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例。谈到Java堆中的垃圾回收,自然要谈到引用。无论通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。在JDK1.2之前,Java中的引用定义很很纯粹:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。但在JDK1.2之后,Java对引用的概念进行了扩充,将其分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,引用强度依次减弱。a.强引用:如“Object obj = new Object()”,这类引用是Java程序中最普遍的。只要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。b.软引用:它用来描述一些可能还有用,但并非必须的对象。在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。JDK1.2之后提供了SoftReference类来实现软引用。c.弱引用:它也是用来描述非需对象的,但它的强度比软引用更弱些,被弱引用关联的对象只能生存岛下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2之后,提供了WeakReference类来实现弱引用。d.虚引用:最弱的一种引用关系,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。JDK1.2之后提供了PhantomReference类来实现虚引用。 回收方法区很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,java规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,在方法区进行垃圾收集的性价比很低,在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%-95%的空间,而永生代垃圾收集率远低于此。永生代的垃圾收集主要回收两个部分内容:废弃常量和无用的类判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用类”的条件相对苛刻,类需满足下面3个条件:1.该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例2.加载该类的ClassLoader已经被收回3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。 垃圾收集算法1.标记-清除算法 标记—清除算法是最基础的收集算法,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。如图: 缺点: ①.效率问题:标记清除过程效率都不高。 ②.空间问题:标记清除之后会产生大量的不连续的内存碎片(空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续的内存空间而不得不提前触发另一次垃圾收集动作。)2.复制算法 复制算法是针对标记—清除算法的缺点,在其基础上进行改进而得到的,它讲课用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次清理掉。复制算法有如下优点:每次只对一块内存进行回收,运行高效。只需移动栈顶指针,按顺序分配内存即可,实现简单。内存回收时不用考虑内存碎片的出现。它的缺点是:可一次性分配的最大内存缩小了一半。3.标记-整理算法复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代(后面会讲到)一般不能直接选用这种算法。根据老年代的特点,有人提出“标记-整理(Mark-Compact)”算法,标记过程仍与“标记-清除”一样,但后续不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外 的内存。如图:4.分带收集算法当前商业虚拟机的垃圾收集 都采用分代收集,它根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代。在新生代中,每次垃圾收集时都会发现有大量对象死去,只有少量存活,因此可选用复制算法来完成收集,而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清除算法或标记—整理算法来进行回收。 垃圾收集器HotSpot虚拟机实现的垃圾收集器可以参考下面的博客,内容很详细 Chang_Wen_Liu博客: http://blog.csdn.net/ochangwen/article/details/51412595 我在学习java虚拟机的时候看到一篇对本节的知识点介绍比较好理解的文章,如果大家对以上的概念有不理的地方,我想下面的这篇博客可以给你启发(图比较多,更有助于理解,无言表达比较容易懂): 生命壹号:http://www.cnblogs.com/smyhvae/p/4744233.html 常用JVM配置参数Trace跟踪参数1.打开GC开关 -verbose:gc-XX:+printGC这两个是一样的,可以粗糙的认为其中一个相当于另一个的别名。 在IDE的后台打印GC日志:本实例以eclipse为例既然学习JVM,阅读GC日志是处理Java虚拟机内存问题的基础技能,它只是一些人为确定的规则,没有太多技术含量。结果:虽然可以打印出一部分GC内容,但是这还是远远不够的,一下接收的和参数的配置方式和图中 的一样。2.打印GC详细信息 -XX:+PrintGCDetails 打印的GC结果3.打印CG发生的时间戳 -XX:+PrintGCTimeStamps 4.指定GC log的位置: -Xloggc:log/gc.log 5.每一次GC前和GC后,都打印堆信息 -XX:+PrintHeapAtGC 6.监控类的加载 -XX:+TraceClassLoading 理解GC日志的内容:每一种收集器的日志形式都是有它们自身的实现所决定的,换言之,每个收集器的日志格式都可以不一样,但虚拟机设计者为了方便用户阅读,讲个收集器的日志都维持一定的共性。例如:GC日志开头的“[GC”和”[Full GC” 是表示这次垃圾收集的停顿时间,而不是用来区分新生代GC还是老生代GC的,如果有Full,说明这次的GC是发生了Stop-The-Word(下面介绍)的。接下来“[PSYoungGen”,“[ParOldGen”表示GC发生的区域,这里显示的区域名称与使用的GC收集器密切是相关的,例如使用Serial收集器,则新生代名为“Default New Generation” 所以会显示“[DefNew”,如果是ParNew收集器,新生代会显示“[ParNew”,如果采用Parallel Scavenge收集器,那么新生代称为“PaYoungGen”,老年代和永久代同理,名称也是由收集器决定的。后面的2662K->696K(38400K) 含义是“GC前该内存区域已使用的容量–> GC后该内存区域使用的容量(该内存区域总容量)”,而方括号之外的“2662K->704K(125952K)”表示“GC前java对已使用容量 —> GC后java堆已使用容量(java堆总容量)”,在后面“0.0061965 secs”表示该内存区域GC所占用的时间,单位是秒。-XX:+PrintGCDetails打印结果:heap区又分为: Eden Space(伊甸园)、 Survivor Space(幸存者区)、 Old Gen(老年代)。Eden Space字面意思是伊甸园,对象被创建的时候首先放到这个区域,进行垃圾回收后,不能被回收的对象被放入到空的survivor区域。Survivor Space幸存者区,用于保存在eden space内存区域中经过垃圾回收后没有被回收的对象。Survivor有两个,分别为To Survivor、 From Survivor,这个两个区域的空间大小是一样的。执行垃圾回收的时候Eden区域不能被回收的对象被放入到空的survivor(也就是To Survivor,同时Eden区域的内存会在垃圾回收的过程中全部释放),另一个survivor(即From Survivor)里不能被回收的对象也会被放入这个survivor(即To Survivor),然后To Survivor 和 From Survivor的标记会互换,始终保证一个survivor是空的。图中的from space对应的就是From Survivor,to space 对应的是ToServivor。Eden Space和Survivor Space都属于新生代,新生代中执行的垃圾回收被称之为Minor GC(因为是对新生代进行垃圾回收,所以又被称为Young GC),每一次Young GC后留下来的对象age加1。Old Gen老年代,用于存放新生代中经过多次垃圾回收仍然存活的对象,也有可能是新生代分配不了内存的大对象会直接进入老年代。经过多次垃圾回收都没有被回收的对象,这些对象的年代已经足够old了,就会放入到老年代。当老年代被放满的之后,虚拟机会进行垃圾回收,称之为Major GC(不是Minor GC)。由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC。MinorGC和Full GC区别 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕死的特性,所以Minor GC非常的频繁,一般回收速度也比较快。 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(并非绝对,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般比Minor GC 慢10倍以上。上图中,我们先看一下“[0x00000000d5e00000, 0x00000000d8880000, 0x0000000100000000)”(PSYoungGen后方括号里的内容)的含义,它表示新生代在内存当中的位置:第一个参数是申请到的起始位置,第二个参数是申请到的终点位置,第三个参数表示最多能申请到的位置。上图中的例子表示新生代申请到了43M的控件,而这个43M是等于:(eden space的33280K)+(from space的5120K)+(to space的5120K)。疑问:分配到的新生代有43M,但是可用的只有38400K,为什么会有这个差异呢?这里还是因为Survivor Space幸存者区,这个区有两个分别为To Survivor、 From Survivor,其中的一个区被要求用来放不能被回收的对象,所有能放新对象的区域只有 Eden space和Survivor Space中的一个。 Stop-The-Word:1、Stop-The-World概念:Java中一种全局暂停的现象。全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互,多半情况下是由于GC引起。 少数情况下由其他情况下引起,如:Dump线程、死锁检查、堆Dump。2、GC时为什么会有全局停顿?(1)避免无法彻底清理干净打个比方:类比在聚会,突然GC要过来打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。 况且,如果没有全局停顿,会给GC线程造成很大的负担,GC算法的难度也会增加,GC很难去判断哪些是垃圾。(2)GC的工作必须在一个能确保一致性的快照中进行。这里的一致性的意思是:在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。这点是导致GC进行时必须停顿所有Java执行线程的其中一个重要原因。 堆的分配参数1.-Xmx –Xms:指定最大堆和最小堆案例:-Xmx20m -Xms5m指定最大堆内存20M,最小堆内存5M。2.设置新生代大小 -Xmn 3.新生代和老年代的比值 -XX:NewRatio比如:-XX:NewRatio=4 表示新生代:老年代=1:4,即新生代占整个堆的1/5 4.设置两个Survivor区和Eden的比值 -XX:SurvivorRatio例如:值为8时,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10 先运行一下代码:12345678package com.fan.JVMDemo;public class JavaTest { public static void main(String[] args) { byte[] b = null; for (int i = 0; i < 10; i++) b = new byte[1 * 1024 * 1024]; }} 然后我们通过设置不同的GC参数来看看输出结果有什么不同: -Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails (设置新生代大小为1M)结果进行了一次GC,Allocation Failure – 引起垃圾回收的原因. 本次GC是因为年轻代中没有任何合适的区域能够存放需要分配的数据结构而触发的. -Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails(新生代15M,足够大)结果没有进行GC,老年代也没有使用到。所有都分配到Eden space。 -Xmx20m -Xms20m –Xmn7m -XX:+PrintGCDetails(新生代大小7M)结果进行了两次GC,老年代中使用了2152K,survivor太小需要老年代担保。 -Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails(增加幸存带的大小)进行了三次GC。在新生代相同的情况下,增加了幸存带,老年代的使用减小了。结论:通过对GC参数的调整可以调成程序的运行速度,根据GC的值可以对程序进行优化。 5.OOM时导出堆信息到文件,根据这个文件,我们可以看到系统dump时发生了什么。 -XX:+HeapDumpOnOutOfMemoryError 6.导出OOM的路径 -XX:+HeapDumpPath例如:-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump上方意思是说,现在给堆内存最多分配20M的空间。如果发生了OOM异常,那就把dump信息导出到d:/a.dump文件中。 栈的分配参数 -Xss设置栈空间的大小。通常只有几百K决定了函数调用的深度每个线程都有独立的栈空间局部变量、参数 分配在栈上 代码示例: 123456789101112131415161718package com.fan.JVMDemo;public class TestStackDeep { private static int count = 0; public static void recursion(long a, long b, long c) { long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10; count++; recursion(a, b, c); } public static void main(String args[]) { try { recursion(0L, 0L, 0L); } catch (Throwable e) { System.out.println("deep of calling = " + count); e.printStackTrace(); } }} 如果设置栈大小为128k:-Xss128k如果你去掉-Xss,改成默认你会发现会结果发生变化。 永久区分配参数 -XX:PermSize-XX:MaxPermSize 参考: http://www.cnblogs.com/smyhvae/p/4736162.html]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>垃圾收集器,参数配置,JVM</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【深入理解JVM】之一: java内存区域与内存溢出异常]]></title>
<url>%2F2017%2F09%2F15%2Fjava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E4%B8%8E%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E5%BC%82%E5%B8%B8%2F</url>
<content type="text"><![CDATA[前言本文是基于周志明的《深入理解Java虚拟机》java与C++支架有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。 运行时数据区域java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,ava虚拟机规范将JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。 如下图所示:《深入理解java虚拟机》中的描述: 程序计数器程序计数器是一块较小的空间,他可以看做是当前线程所执行的字节码的行号指示器,在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。 由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此, 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储 ,我们称这类内存区域为“线程私有”的内存。 一个处理器都只会执行一条线程中的指令,所以,每个线程都需要一个独立的程序计数器,各线程之间计数器互不影响,独立存储。如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器值则为空(Undefined)。 注意:此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。 java虚拟机栈(Java Virtual Machine Stacks)与程序计数器一样,java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的声明周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部标量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就读应这一个栈帧在虚拟机栈中入栈到出栈的过程。 局部变量表 局部变量表存放了编译期可知的各种基本数据类型(8中基本数据类型),对象引用(reference类型)和returnAddress类型(指向了一条字节码指令的地址) 其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。局部变量表所需的内存大小在编译期就完成了分配,也就是说当进入一个方法时,此方法需要在栈帧中分配多大的局部变量表空间时完全确定的,运行期不会改变 。如下代码:1234567891011121314package test03;/** * Created by smyhvae on 2015/8/15. */public class StackDemo { //静态方法 public static int runStatic(int i, long l, float f, Object o, byte b) { return 0; } //实例方法 public int runInstance(char c, short s, boolean b) { return 0; }} 上方代码中,静态方法有5个形参,实例方法有3个形参。其对应的局部变量表如下:上方表格中,静态方法和实例方法对应的局部变量表基本类似。但有以下区别:实例方法的表中,第一个位置存放的是当前对象的引用。(如果对这张图看不同不要紧,通过下节对Class类文件结构的讲解,也许你就能理解其中的含义) 在Java虚拟机规范中,对这个区域规定了两种异常情况: (1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。 (2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。 这两种情况存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。在单线程的操作中,无论是由于栈帧太大,还是虚拟机栈空间太小,当栈空间无法分配时,虚拟机抛出的都是StackOverflowError异常,而不会得到OutOfMemoryError异常。而在多线程环境下,则会抛出OutOfMemoryError异常。 操作数栈操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了。32位数据类型所占的栈容量为1,64为数据类型所占的栈容量为 2。当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。因此我们也称Java虚拟机是基于栈的,这点不同于Android虚拟机,Android虚拟机是基于寄存器的。基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。 动态链接每个栈帧都包含一个指向运行时常量池(1.7之前在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如final、static域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。(接下的文章将会讲解什么是直接引用,什么是间接引用) 方法返回地址 当一个方法被执行后,有两种方式退出该方法:执行引擎遇到了任意一个方法返回的字节码指令或遇到了异常,并且该异常没有在方法体内得到处理。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出站,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令。 本地方法栈(Native Method Stacks) 作用和虚拟机栈非常相似,区别: 虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务 JVM规范没有强制规定本地方法栈中的方法使用的语言、使用方式、数据结构,所以具体JVM不同实现。 有的虚拟机如HotSpot虚拟机直接把虚拟机栈和本地方法栈合二为一了。 java堆(Java Heap)Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间 、中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。 方法区(Method Area) 方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区域又被称为“永久代”,但这仅仅对于Sun HotSpot来讲,JRockit和IBM J9虚拟机中并不存在永久代的概念。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,而且它和Java Heap一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。该区域的内存回收目标主要针是对废弃常量的和无用类的回收。运行时常量池是方法区的一部分(1.7以后的版本把它放在了堆内存中),Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Class文件常量池),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中的常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的是String类的intern()方法。 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。 常量池之中主要存放两大类常量: 1).字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等 2).符号引用: 属于编译原理方面的概念,包括了下面三类常量: ①.类和接口的全限定名 ②.字段的名称和描述符 ③.方法的名称和描述符 直接内存(Direct Memory)直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,它直接从操作系统中分配,因此不受Java堆大小的限制,但是会受到本机总内存的大小及处理器寻址空间的限制,因此它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式,可以直接从操作系统中分配直接内存,即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据。 对象实例化分析 对内存分配情况分析最常见的示例便是对象实例化: Object obj = new Object(); 这段代码的执行会涉及java栈、Java堆、方法区三个最重要的内存区域。假设该语句出现在方法体中,及时对JVM虚拟机不了解的Java使用这,应该也知道obj会作为引用类型(reference)的数据保存在Java栈的本地变量表中,而会在Java堆中保存该引用的实例化对象,但可能并不知道,Java堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。 另外,由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同。 主流的访问方式有两种:使用句柄池和直接使用指针。 a. 通过句柄池访问对象 如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图1 b.直接指针访问 如果使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图所示 这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本。从上一部分讲解的对象内存布局可以看出,就虚拟机HotSpot而言,它是使用第二种方式进行对象访问,但在整个软件开发的范围来看,各种语言、框架中使用句柄来访问的情况也十分常见。 虚拟机对象的创建 Java是一门面向对象的编程语言,Java程序运行过程中无时无刻都有对象被创建出来。在语言层面上,创建对象通常(例外:克隆、反序列化)仅仅是一个new关键字而已,而在虚拟机中,对象(本文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样一个过程呢? 虚拟机遇到一条new指令时, 1).首先检验: a.检查这个指令的参数是否能在常量池中定位到一个类的符号引用 b.并且检查这个符号引用代表的类是否已被加载、解析和初始化过的。 如果没有,那必须先执行相应的类加载过程。 2).在类加载查通过后,接下来虚拟机将为新生对象分配内存。 准备阶段 对象所需内存的大小在类加载完成后便可完全确定(如何确定在下一节对象内存布局时再详细讲解),为对象分配空间的任务具体便等同于一块确定大小的内存从Java堆中划分出来,怎么划呢? a.假设Java堆中内存是绝对规整的,所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。 b.如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 因此在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时,就通常采用空闲列表。 3).考虑并发情况下线程安全问题 除如何划分可用空间之外,还有另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。 解决这个问题有两个方案, a.一种是对分配内存空间的动作进行同步——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性; b.另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲,(TLAB ,Thread Local Allocation Buffer)。 哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完,分配新的TLAB时才需要同步锁定。虚拟机是否使用TLAB,可以通过 -XX:+/-UseTLAB 参数来设定。 4).内存空间初始化为0 对应类加载的初始化 内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB的话,这一个工作也可以提前至TLAB分配时进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 5).对象头的设置 接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。关于对象头的具体内容,在下一节再详细介绍。 6).执行init()方法 在上面工作都完成之后,在虚拟机的视角来看,一个新的对象已经产生了。但是在Java程序的视角看来,对象创建才刚刚开始——< init>方法还没有执行,所有的字段都为零呢。所以一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行< init>方法,把对象按照程序员的意愿进行初始化。 这样一个真正可用的对象才算完全产生出来。 内存泄露和内存溢出的区别: 内存泄露是值分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费,java中一般不会产生内存泄露,以为有垃圾回收器自动回收垃圾,但是这也不绝对,当我们new对象,并保存了其引用,但是后面一直没有用它,而垃圾回收器又不会去回收它,这便造成内存泄露。 内存溢出是值程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。 参考博客 兰亭风雨的专栏: http://blog.csdn.net/ns_code/article/details/17565503]]></content>
<categories>
<category>Java虚拟机</category>
</categories>
<tags>
<tag>JVM,内存区域,内存溢出异常</tag>
</tags>
</entry>
<entry>
<title><![CDATA[关于 java.lang.NoSuchMethodError:antlr.collections.AST.getLine()I 问题解决]]></title>
<url>%2F2017%2F03%2F21%2FBugProblem%2F</url>
<content type="text"><![CDATA[今天在MyEclipse中运行Tomcat时,访问主页时,出现以下错误: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485java.lang.NoSuchMethodError:antlr.collections.AST.getLine()I org.hibernate.hql.ast.HqlSqlWalker.generatePositionalParameter(HqlSqlWalker.java:896) org.hibernate.hql.antlr.HqlSqlBaseWalker.parameter(HqlSqlBaseWalker.java:4819) org.hibernate.hql.antlr.HqlSqlBaseWalker.expr(HqlSqlBaseWalker.java:1373) org.hibernate.hql.antlr.HqlSqlBaseWalker.exprOrSubquery(HqlSqlBaseWalker.java:4243) org.hibernate.hql.antlr.HqlSqlBaseWalker.comparisonExpr(HqlSqlBaseWalker.java:3725) org.hibernate.hql.antlr.HqlSqlBaseWalker.logicalExpr(HqlSqlBaseWalker.java:1864) org.hibernate.hql.antlr.HqlSqlBaseWalker.logicalExpr(HqlSqlBaseWalker.java:1789) org.hibernate.hql.antlr.HqlSqlBaseWalker.whereClause(HqlSqlBaseWalker.java:818) org.hibernate.hql.antlr.HqlSqlBaseWalker.query(HqlSqlBaseWalker.java:604) org.hibernate.hql.antlr.HqlSqlBaseWalker.selectStatement(HqlSqlBaseWalker.java:288) org.hibernate.hql.antlr.HqlSqlBaseWalker.statement(HqlSqlBaseWalker.java:231) org.hibernate.hql.ast.QueryTranslatorImpl.analyze(QueryTranslatorImpl.java:254) org.hibernate.hql.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:185) org.hibernate.hql.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:136) org.hibernate.engine.query.HQLQueryPlan.<init>(HQLQueryPlan.java:101) org.hibernate.engine.query.HQLQueryPlan.<init>(HQLQueryPlan.java:80) org.hibernate.engine.query.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:94) org.hibernate.impl.AbstractSessionImpl.getHQLQueryPlan(AbstractSessionImpl.java:156) org.hibernate.impl.AbstractSessionImpl.createQuery(AbstractSessionImpl.java:135) org.hibernate.impl.SessionImpl.createQuery(SessionImpl.java:1651) org.springframework.orm.hibernate3.HibernateTemplate$30.doInHibernate(HibernateTemplate.java:914) org.springframework.orm.hibernate3.HibernateTemplate$30.doInHibernate(HibernateTemplate.java:1) org.springframework.orm.hibernate3.HibernateTemplate.doExecute(HibernateTemplate.java:406) org.springframework.orm.hibernate3.HibernateTemplate.executeWithNativeSession(HibernateTemplate.java:374) org.springframework.orm.hibernate3.HibernateTemplate.find(HibernateTemplate.java:912) DAO.baseDAO.findByHql(baseDAO.java:118) Service.WenJianViewService.chaXunLeiBieByFuID(WenJianViewService.java:31) Action.WenJianViewAction.getMainUIData(WenJianViewAction.java:414) sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) java.lang.reflect.Method.invoke(Method.java:606) com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:452) com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:291) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:254) com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor.doIntercept(DefaultWorkflowInterceptor.java:176) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.validator.ValidationInterceptor.doIntercept(ValidationInterceptor.java:263) org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor.doIntercept(AnnotationValidationInterceptor.java:68) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor.intercept(ConversionErrorInterceptor.java:133) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:207) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:207) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:190) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) org.apache.struts2.interceptor.MultiselectInterceptor.intercept(MultiselectInterceptor.java:75) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:94) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:243) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:100) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:141) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) org.apache.struts2.interceptor.debugging.DebuggingInterceptor.intercept(DebuggingInterceptor.java:267) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:142) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:166) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:176) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:164) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:190) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:187) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:248) org.apache.struts2.impl.StrutsActionProxy.execute(StrutsActionProxy.java:52) org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:485) org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77) org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:91)note The full stack trace of the root cause is available in the Apache Tomcat/7.0.69 logs.Apache Tomcat/7.0.69 原因 包版本冲突问题,问题出在struts包里面有也有个antlr_xxx.jar,与hibernate包里面的冲突了,hibernate的版本高,Struts自带的antlr-2.7.2.jar,比Hibernate3.3自带的antlr-2.7.6.jar的版本要低,antlr这个语法解析包出错 。SSH组合完成后,执行hibernate的HQL查询时,报错:java.lang.NoSuchMethodError: antlr.collections.AST.getLine()I只要删除前一个低版本的,struts1.3 和2.1都带有antlr-2.7.2.jar,要把版本低的清除掉就可以了。 但由于myeclipse 添加的struts性能 不是放在工程lib下的,而是myeclipse自带的,所以删除比较麻烦。 具体方案1.在MyEclipse下,windows–>preferences–>在文本框中搜索struts2–>选择antlr2.7.2–>remove2.在tomcat该应用的目录WEB-INF\lib 删除 antlr2.7.2.jar重启Tomcat就可以了。]]></content>
<categories>
<category>SSH</category>
</categories>
<tags>
<tag>JavaEE</tag>
<tag>Tomcat</tag>
</tags>
</entry>
<entry>
<title><![CDATA[myFirstBlog]]></title>
<url>%2F2017%2F03%2F21%2FmyNewPost%2F</url>
<content type="text"><![CDATA[欢迎来到樊兴凯的个人博客,这里是一个开发者的天堂,在这里你们可以领略代码的美丽与奇妙。在往后的时间里,我将教大家怎么装逼,怎么编程,怎么在纷乱复杂的世界里生存下来。这里我会不定期更新一些内容,其中不仅仅是代码,技能还有诗和方向。 经过长久的等待,我的域名已经可以备案好,可以使用了域名地址:fxkoutlook.cn 给你们推荐几片自我感觉不错的文章,看懂看不懂就看你的理解力了: http://mp.weixin.qq.com/s/LygoCcBqRW7Aqp1ulfyjfg这篇主要讲的是:咪蒙式的成功 http://mp.weixin.qq.com/s/3foshLbcGpmlp6NX2txoAg这篇主要讲: 技术是打破阶层固化最有效的手段 ==遵从你内心的想法,喜欢那条路就努力去做,也许成功一切都是那么的随意。==]]></content>
<categories>
<category>自我介绍</category>
</categories>
<tags>
<tag>介绍</tag>
<tag>博客</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HexoNext主题配置教程]]></title>
<url>%2F2017%2F03%2F14%2FHexoNext%2F</url>
<content type="text"><![CDATA[前言我的博客是利用GitHub+hexo搭建的。建立博客可以参考一下链接: 这个博客主要主要介绍Hexo+coding搭建博客,但是里面的前几章可以参考。http://windliang.cc 这个主要介绍hexo+GitHub。http://svend.cc/posts/16820/ 今天我欣赏了别人的博客,许多主题都很好看,于是今天花费了一天的时间在整Next(花费时间比较长,毕竟不熟悉嘛)。我就稍微介绍一下Next主题配置的问题,也算是记录一下修改的过程。下面介绍的主要是上面两个作者没有介绍的主题配置。 设置社交链接侧栏社交链接的修改包含两个部分,第一是链接,第二是链接图标。 两者配置均在主题配置文件中。 1.链接放置在 social 字段下,一行一个链接。其键值格式是 显示文本: 链接地址。 12345678#Socialsocial: GitHub: https://github.com/your-user-name Twitter: https://twitter.com/your-user-name 微博: http://weibo.com/your-user-name 豆瓣: http://douban.com/people/your-user-name 知乎: http://www.zhihu.com/people/your-user-name #等等 2.设定链接的图标,对应的字段是 social_icons。其键值格式是 匹配键: Font Awesome 图标名称, 匹配键 与上一步所配置的链接的 显示文本 相同(大小写严格匹配),图标名称 是 Font Awesome 图标的名字(不必带 fa- 前缀)。 enable 选项用于控制是否显示图标,你可以设置成 false 来去掉图标。 123456social_icons: enable: true #Icon Mappings GitHub: github Twitter: twitter 微博: weibo 文章打赏功能越来越多的平台(微信公众平台,新浪微博,简书,百度打赏等)支持打赏功能,付费阅读时代越来越近,Next特此增加了打赏功能,支持微信打赏和支付宝打赏。 只需要复制下面的代码添加到主题配置文件中即可开启该功能。 reward_comment: 坚持原创技术分享,您的支持将鼓励我继续创作!wechatpay: 图片链接alipay: 图片链接 设置友情链接在主题文件开启links_title和links即可 #titlelinks_title: 前端工具箱 #links_layout: block #links_layout: inlinelinks: 代码压缩: http://tool.oschina.net/jscompress CSS整理: http://example.com/ 修改底部logo首先,找到 \themes\next\layout_partials\下面的footer.swig文件,打开会发现,如下图的语句: 第一个框 是下面侧栏的“日期❤ XXX”如果想像我一样加东西,一定要在双大括号外面写。如:xxx,当然你要是想改彻底可以变量都删掉,看个人意愿。第二个,是图一当中 “由Hexo驱动” 的Hexo链接,先给删掉防止跳转,如果想跳转当然也可以自己写地址,至于中文一会处理。注意删除的时候格式不能错,只把…标签这部分删除即可,留着两个单引号”,否则会出错哦。第三个框也是最后一个了,这个就是更改图一后半部分“主题-Next.XX”,这个比较爽直接将..都删掉,同样中文“主题”一会处理,删掉之后在上一行 ‘-’后面可以随意加上你想显示的东西,不要显示敏感信息哟,请自重。接下来,处理剩余的中文信息。找到这个地方\themes\next\languages\ 下面的语言文件zh-Hans.yml(这里以中文为例,有的习惯用英文的配置文件,道理一样,找对应位置即可) 如果有其他疑问可以结合NextT的使用文档http://theme-next.iissnan.com/getting-started.html]]></content>
<categories>
<category>Next主题配置教程</category>
</categories>
<tags>
<tag>Next主题</tag>
</tags>
</entry>
</search>