祈祷二月(2020)

由于疫情原因这个假期格外得长

过完元宵我才回上海,在家又连续办公两周后才去的公司。

中国加油!武汉加油!

现在是两月底,从数据上来看疫情已经得到了有效的控制,疑似人数以及确诊人数都有比较大的降幅。当然,股市也是……

反而看着国外数据越来越大,外围开始恐慌,中国当然也不可能独善其身。

活在当下,生命至上!

愿所有人 无疾无病 无忧无怖 无愧于心。


刚才有同事问我,当疫情结束后有什么特别想见的人。

我沉默了,脑中却谁都想不起来,好像我的人生还并没有开始就已经迷茫了。


心累一月(2020)

心累累累累累累累累累累累累累累累累啊

又要自我否问了,又要成长了。


我一看 2019 的文件夹,加上总结总共才写了 15 篇,这一年我是有多水。说是当回首往事的时候,不因虚度年华而悔恨,也不因碌碌无为而羞耻。真的要羞耻了,怎么能这样呢!生命应是闪耀的,是红色有张力的,是滴入泥潭而不深陷在多遥远第一眼都能望得到的!大家都渴望着,都为之执着着,到了一种什么情境呢,就是那种这辈子我就算看一眼,远远得看一眼就能无比满足的死去。

人有血气那是极其好的,总会是成为人群中的那些目光聚焦点。其实我们本都是带着血气来到这世界的,只是血气本就珍贵,在岁月长河中慢慢就被那些个“主子”给抽了个精光,再打个三五折的折现了。

当我看到自己亲姐姐已是为人妻将是为人母,发现这个世界很多事还是很有意义的。小时候我和我姐常常争吵打架,由于男生嘛发育较晚,她又年长我几岁,奈何我怎能打得过,多是被指手去做事。小时候虽然生活条件没那么好,但是多的是值得回忆的地方,时代不同那些也终将过去难以再重现了。人会回忆,我觉得这个设定很是奇妙。

最近由于域名备案问题导致网站暂时无法访问,之前是在大学时候就备的案,用的自然也是当时的手机号,地址什么的都是合肥。现在那张手机卡早就不用了,上头呢也隔三差五查一次备案问题,本来之前就通知过需要更新信息。可是我这个情况就很麻烦,要么重新激活之前的手机号,去合肥再更改下信息,要么只能注销掉现在的备案号重新在上海备案。由于我当时还没有办上海的居住证也办不了备案就这样不了了之了,没想到时隔大半年,又开始查了,无奈啊,这次我决定注销掉重新在上海办理备案了,居住证要年后才拿到也就只能年后再办了,现在我先把之前的给注销了。不合情合理不够人性啊!

月底传出肺炎的消息,回家在虹桥火车站上看到都是戴着口罩的人,从刚开始的不以为意到后来的惊恐不已。

一整个春节都呆在家,看着电视追踪前线医疗士兵的消息,无话可说,唯有致敬!

2020 年的第一个月就这么坎坷,这到底是天灾呢还是人祸?


你好,贰零

此篇作为对 2019 整年的简单回顾


首先得接上上篇,嘉善半马 PB 比之前快了十分钟哈哈,214.

刚才在准备写这篇的时候,想想我该起个什么标题好,脑海中还在播放去年写年终篇《你好,依旧》时的画面。

延续风格,《你好,贰零》出炉!

今年春节来得早,过完元旦再过个两周就春节了,今年没什么输出,大致看了下去年的一些文章还感觉好像也就不久前写的,一下子过一年了。甚叹。

今年书看得少了,倒是看了不少电视剧,这算不算得上是精神上的一种疲呢。《权力的游戏》、《硅谷》、《绝命毒师》、《越狱》、《风骚律师》……美剧居多。《小欢喜》、《陈情令》以及最近比较火的《庆余年》这类电视,说实话确实是美剧看得更有意味点。

2019,公司旅游去了趟塞班岛,风景绝佳,消费颇高;

2019,又看了几场 live house,孤独者的狂欢;

2019,酒吧喝喝酒,听听音乐,还是没把到妹;

2019,生命不息,运动不停,跑步没拉下;

2019,长了十几斤肉,肥肉?不可能;

2019,两块半马完赛牌,PB 214;

2019,回家次数比去年要多;

2019,我姐终于嫁出去了;

2019,还是没搞到钱!

2019,昆明去了好几趟,把昆医体育馆这个项目给顺利弄完了,吃到了特色小吃去大学城走了走遗憾的是没有足够的时间去周边地区好好逛逛,听说泸沽湖相当美啊。云南的天确实很漂亮,气候也相当不错,只要不在阳光下直射。之前还跟大学室友讨论要不要搞个名宿,大搞一笔,可也只能是口嗨下罢了,一是资金,二来我们也没路子,若是保障每月都有稳定的人流量,那便不在话下,我就是卖血也去干啊。确实像云南四季都是旅游季,独特风景又数不胜数,适合不过了。突然想到之前去乌镇玩,那时候网上定的名宿,其实就是街边的房子一改造。那一条街两旁的房子不是用来住宿就是用来吃喝,真的带动了那一大片。有时我一想,要是真弄了个名宿这种的,刚好爸妈可以去帮忙运作而我继续上班,也不耽误,可这远了也不现实而已。千岛湖?不晓得。

今年科技热潮,5G 也带动了不少行业的热度,单从股价上就可以看出,不少都翻了好几番。老美的经济结构组成上可以看出科技是未来,科技是领头军,而中国经济结构大头还是地产和金融,科技现在也正慢慢上来,未来毋庸置疑,肯定是科技大头。可以看到中国改革还是有很大幅度的同时也藏着许多的机会。最近,小米发布了首款售价低于 2000 的 5G 手机,这一下,小米股价涨得那可不少,距离腾讯乘以苹果的市值又更进了一步!

今年还是在这家公司,事情是越做越杂了,有时候也深深感到内心的疲惫。一张一弛,又快一个年头,又是一个深冬。每每双脚踏出大厦,念及岁月蹉跎,明日生活何许,我又有何长进,苦矣。次次眼望灯火大厦,呆呆看那人来人往,何时何地我又能遇到何人,眼角不觉泛起一丝波动。何曾想,本就因该司基本不加班朝九晚六,可以偷得些许清闲时间为我所用,最近几天却基本弄到凌晨才昏沉回去,第二天又匆匆赶来。


↓↓ 附上年度总结 ↓↓

  时间一晃而过,转眼间到公司已经一年多了。这是我人生中弥足珍贵的一段经历。在这段时间里各级领导在工作上给予了我极大的帮助,在生活上给予了我极大的关心,让我充分感受到了领导们“海纳百川”的胸襟,感受到了同磊人“不经历风雨,怎能见彩虹”的豪气。在对公司各级领导肃然起敬的同时,也为我有机会成为公司的一份子而自豪。在这一年多的时间里,在领导和同事们的悉心关怀和指导下,通过自身的努力,各方面均取得了一定的进步,现将我的工作情况作如下汇报。

  一、通过培训学习和日常工作积累使我对同磊公司有了一定的新的认识

  在这近一年的时间里,对建筑行业和公司有了更深一步的了解。公司的理念在平时的工作以及项目开发时体现的淋漓尽致,本人对公司的理念非常认同。公司一直在技术更新中健步发展,令人敬佩。公司以人为本、尊重人才的思想在实际工作中贯彻,这是同磊公司能发展壮大的重要原因。现在公司在钢结构建筑监测以及张拉行业起到了举足轻重的地位,今后还将更加辉煌。

  二、遵守各项规章制度,认真工作,使自己素养不断得到提高

  爱岗敬业的职业道德素质是每一项工作顺利开展并最终取得成功的保障。在这一年多的时间里,我能遵守公司的各项规章制度,兢兢业业做好本职业工作,用满腔热情积极、认真地完成好每一项任务,认真履行岗位职责,平时生活中团结同事、不断提升自己的团队合作精神。近期也因公多次出差,在项目现场也需要与业主方等进行有效沟通,这也积极锻炼了我的沟通、交际能力,渴望有所突破的我,将会在以后的工作和生活中时时提醒自己,以便自己以后的人生道路越走越精彩。

  三、认真学习岗位职能,工作能力得到了一定的提高

  根据目前工作分工,我的主要工作任务是:

(1) 负责公司钢结构建筑监测系统的开发;

(2) 现场采集仪器的调试以及负责监测系统集成;

(3) 参与 BIM4D 项目的开发。

  四、不足和需改进方面

  虽然加入公司这个大家庭已经一年有余,但可能由于跨行业,对建筑方面的一些知识和了解还是很欠缺的。交际能力也是大多数程序员比较缺乏的当然包括我在内,我也希望能够在今后的工作中更加迅速的提升自己的业务能力以及技术能力。


2019,没发生什么改命的事,也没什么废命的事;

2019,还是在上海眼望辉煌,勉强度日;

2019,事无巨细,笑口常开;

你好,20!


剁手十一月(2019)

众所周知,这是一个神奇的自我高潮的“送钱”之月。

双手被下了诅咒,一时无法掌控,前方有个迷洞,迫不及待想要填补塞透。


由于上海进博会的举办,公司做了调休安排,我就趁那几天回了趟家,顺便带着老爸老妈去逛了逛商场,买了点衣物。

太久没碰车了,大早上从家出发去义乌随即又去浦江县城,到了下午直犯困。

虽然他们口中一直说不要乱花钱,破费,但是从他们眼中我还是看出了他们内心的开心。

他们开心我开心。为父母做的其实很少,他们却为了我们舍弃的太多。生命轮回。

我把这些当做是生活的记录,以前小时候写文本日记,可是那些都随着时光消逝渐渐不见了。每次想起都不免觉得可惜,都是逝去的青春啊,我也少了些给子孙吹嘘的资本。如今虽然每月记录一番,时至今日也有一年有半,有时我自己也会翻出往常写的内容,带着回忆走一遍,着实很有意味。人的一生都在不停地前行,不停地否定自己,不停地回想过去,不停地重摆手臂。年轻气盛,那时把与初恋之间来往的所有信件给丢失了,每每念及,捶胸哀气。这些都是我们逝去的青春,年轻的见证啊。人的成长在于勇于自嘲,别人没有栓住你,自己要会放过自己。

失魂落魄之时,大叹三声;

买定离手之时,大笑三声。

口袋空空之时,大唉三声;

包子入口之时,大赞三声。

爱存心间,忧怖伴边;

忧怖心间,爱在心尖。

愿我们都成为我们彼此心中我们的人。


周末打发时间,去看了场晓月老板的 livehouse,刚好大学机油也来了上海。

很幸运,这次又抽到了电影的超前点映。这次是《平原上的夏洛克》,随是小成本制作,全素人出镜,但全影中有笑点有感悟,个人觉得还是非常值得一看的。意外,还看到了肖央。

平原上的夏洛克 超前点映


十一月十七号,苏州(太湖)马拉松,半程 21.0975 公里。这是我参加的第一个马拉松赛事,成绩 224,虽然这成绩不算好,但是毕竟第一次参与,还是比较满意了(跑完就是胜利!)。

一回生,没考虑到的事情还不少,就不细细说了。说一下参赛体验吧,在赛场上我看到了年过古稀的老爷爷,刚上小学的小朋友,坐在轮椅上用双手奋力推轮的朋友……之前这些都是在网上看到某某报道上,谁谁谁今年多少岁跑了半马全马。现场看到的感觉真的很不一样,很有触动,这让我感觉马拉松这项活动赛事更加的有意义了,真的不虚此行,希望每一个人都能在其中体验一番,重在参与嘛。

场边市民的热情,志愿者的体贴服务,赛场上参赛者的激情,真的很带劲啊!

以后一定要带女朋友(老婆)甚至是子女都感受一下这全民健身的气氛。政府也是大力支持,光就大半天的封路操作就挺猛的,还是主大道高架。

再接再厉,争取下次半马进 215。(又报了个嘉善的,下月八号,Go Go Go)


降温咯,怕不穿羽绒服今年就没机会咯。

一晃眼,十一月就要过去了,就要迎来今年的最后一个月份同时也是 21 世纪 10 年代的最后一月,往后就是 21 世纪 20 年代咯。

年会时间定了,今年又是主持,希望能抽到个奖吧,也不晓得是不是内定了的。


祝好。P & L.


金秋十月(2019)

又是一年收获季 你有所收获吗?

已然桂花飘香。


国庆

庆祝中华人民共和国成立 70 周年!

不出意料,国庆回家被“批斗”了…然后天天食素养病,最后一日终吃上了老妈亲手做的红烧肉,真好吃。

回家第二天就跟着我姐他们去婚纱店试了结婚当天所要穿的礼服,还给我定制了套西装,破费破费了。

隔天还专门去了趟杭州买了双英伦风皮鞋,算是人生第一双小皮鞋了。

在上海的时候天天早上闹钟响起,我还不愿起,一回家还没等闹钟发力,就睁着眼再也睡不着了。一年中算是较长的一个假期了,满打满算现在一年在家的日子也不足一个月,唯一心愿就是两老能身体健康,每日心情舒坦,不要过多为我们操劳费心。当然,只有我们自己强大了有能力了,他们才会放心。

昆明

没错,又是昆明。第三次去了,这次是验收,希望一切顺利,完美结束吧。

网络调试没问题,设备也都能正常工作,可就还是不知道啥时候能验收,拖啊。

周四下午浦东飞过去,落地刚好昆明下过雨,微微凉。

第二天一大早开始干活,所幸还比较顺利,只是现在他们还没有给我们提供学校的 vpn。还好那边有网络,ssh 和 teamviewer 两条路都准备了。

这回算是终于吃到了,前几次都是匆匆忙忙得弄完活就赶紧走了,如图。

昆明炸虫子

太焦了有点,香是真挺香的。下次有机会去吃那种 爆浆的。

第一次去那边是寒假,上回去是暑假,这回是好啊,好久没看到那么多一群群的女人了。

这回去各大学校校区逛了下,还是云大的最厉害啊,里头各种古建筑和景点。旁边的翠湖海鸥还没飞来过冬,没能看到,着实可惜了些。

同事同学请吃烧烤,一直弄到凌晨两点才回到宾馆,草草躺床上睡了。我特么还要六点起来去坐高铁。你问我为什么有飞机不坐去坐高铁,我脑子是被驴给踢了,十一个半小时啊,一路上还都是山洞隧道,美景呢?沿途的风景和看风景的心情啥子都没有!

回到上海住处又是晚上十点多了,草草洗漱睡觉,第二天还得去上班呢。

卧槽,想到突然出差去结果早早买好的直火帮《第二个酒吧》演唱会没能去还是伤心啊!


这个十月加班有点多过得有点快。室友安利《绝命毒师》感觉还不错。


Netty4 实现数据传输中间层处理

Netty4 实现数据报文的接收/拆包/重组/转发


完整代码:netty4-datatrans


前言

由于项目中有对建筑的 GPS 定位模块,而 GPS 仪器作为客户端连接,传输的是标准的 GPGGA 语句,也就是多个客户端对一个服务端发送数据,节约端口资源故配置的是同一个端口,此时服务端接收到的 GPGGA 数据却并不能分辨出到底是哪一个客户端发送的,由此决定写一个数据中间层处理,给报文重组根据规则加上唯一标识符。

正题

根据实际需求我这写了服务端和客户端,即该脚本部署的机器同时作为 server 和 client。

可以进行对接收数据的拆包/逻辑重组/添加数据标识符等等 DIY 操作,再进行定向转发。

客户端处理

  • 添加了 Listener 启动时可监听判断 client 是否正常启动,即对应 server 端口是否启用监听
    • 若通道连通,正常连接进行数据传输
    • 若通道未连通,则调用 schedule 进行定时重连操作

GPSTransClientConnectionListener.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!future.isSuccess()) {
final EventLoop loop = future.channel().eventLoop();
loop.schedule(new Runnable() {
@Override
public void run() {
System.err.println("client reconnecting ...");
try {
client.connect(GPSTransConsts.REMOTE_IP, Integer.parseInt(GPSTransConsts.REMOTE_PORT));
} catch (NumberFormatException | InterruptedException e) {
System.out.println("restart err...");
e.printStackTrace();
}
}
}, 5L, TimeUnit.SECONDS);
} else {
System.out.println("client connected ...");
}
  • 同时若是启动成功但是运行一段时间后 server 端口关闭监听了,那也要进行重连处理,可以根据实际需求更改

GPSTransClientHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.err.println("server disconnect ...");
success = false;
// 使用过程中断线重连
final EventLoop eventLoop = ctx.channel().eventLoop();
eventLoop.schedule(new Runnable() {
@Override
public void run() {
try {
client.connect(GPSTransConsts.REMOTE_IP, Integer.parseInt(GPSTransConsts.REMOTE_PORT));
} catch (Exception e) {
System.out.println("restart err...");
e.printStackTrace();
}
}
}, 5L, TimeUnit.SECONDS);
super.channelInactive(ctx);
}

由于是不停的进行转发操作,所以需要循环处理。

定义了 private static volatile boolean success; 作为数据发送线程的循环标志符。

当连接成功时,success 置为 true,当连接断开时,success 置为 false。

volatile 修饰故保证了其可见性。

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
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive ...");
success = true;
System.out.println("send data to server ...");
// 必须另开线程处理,否则会在这个方法中出不去
new Thread() {
@Override
public void run() {
while (success) {
if (!GPSTransConsts.NAME_MESS.isEmpty()) {
StringBuilder sb = new StringBuilder();
GPSTransConsts.NAME_MESS.values().forEach(value -> {
sb.append(value);
});
ByteBuf resp = Unpooled.copiedBuffer(sb.toString(), CharsetUtil.UTF_8);
ctx.writeAndFlush(resp);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("client thread exit ...");
};
}.start();
super.channelActive(ctx);
}

服务端处理

接收多个客户端数据,根据其 IP 来定位设备,再进行报文拆包重组 DIY,存储到内存中便于 client 模块进行转发。

GPSTransServerHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

InetSocketAddress ipsocket = (InetSocketAddress) ctx.channel().remoteAddress();
// 获取客户端 IP
String clientIP = ipsocket.getAddress().getHostAddress();
int index = clientIP.lastIndexOf(".");
String ipNum = clientIP.substring(index + 1);
ByteBuf in = (ByteBuf) msg;
String message = in.toString(CharsetUtil.UTF_8);
if (message.startsWith("$")) {
message = message.replace("$", "#");
if (!GPSTransConsts.IP_NAME.containsKey(ipNum)) {
System.err.println(ipNum + "未配置!");
return;
}
String name = GPSTransConsts.IP_NAME.get(ipNum);
message = "#" + GPSTransConsts.IP_NAME.get(ipNum) + message + "\r";
GPSTransConsts.NAME_MESS.put(name, message);
}
// 释放
super.channelRead(ctx, msg);
}

因为我们没有进行 write 和 flush 操作,所以需要进行释放。

配置文件

为了方便配置的修改,可以把项目打成 jar 包,然后在同目录下新建一个 config 文件夹,把 gps.properties 丢进去,完事。


头晕九月(2019)

为什么现在这个头越来越疼了?

好难啊,好南啊。


最近因为好多电视剧都太优秀了,完全停不下来啊,一部接着一部,晚上回去好久都没看过书了……有几本买来都两年了,塑封都还在呢。一想我现在才25岁,正当青春活力年少,怎能沉迷于银幕中,看完这几部就决心告一段落了,一部有五六十集呢。

上次说的杨浦图书馆,好嘛,就那次去了一趟,环境很重要,在屋里虽也能看但心还是无法长静下来。这次中秋无所事事,无计可划,不如早些时辰去占个位儿,带上几个白面馍馍,看个一天大半天书的,也是个快乐的想法。

中秋

今年中秋不回家,国庆回吧,家离上海倒是不远,只是来回也得费点心力,在家歇一晚上,第二天又得忙活准备回来。到时候要给家里打个电话,家是温暖的港湾,是背后的倚靠。大中华中秋团圆夜,今年得独自倚窗赏月喝闷酒了。说到酒,我发现白酒喝多了我就会想吐。

不出意外,窝着呆了三天。

诸事不顺,病魔缠身。

感冒见好,又来低烧。

躺在床上,汗浸衣裳。

一觉醒来,面目荒凉。


祖国七十大寿!


Java 多线程程序的性能调校

本文摘抄自《Java 多线程编程实战指南》核心篇 第十二章小结

本章介绍了与 Java 多线程程序紧密相关的性能调校常用技术。


  Java 虚拟机自 Java 6 开始对内部锁进行了若干优化:锁消除、锁粗化、偏向锁以及适应性锁。除锁消除是 Java 7 开始引入的,其他优化均是在 Java 6 开始引入的,这些优化仅在 Java 虚拟机的 server 模式下起作用。这些优化默认都是开启的,且多数优化都可能依赖于 JIT 的内联优化,并且其本身也可能是通过 JIT 编译实现的。因此,这些优化都有其开销。锁消除优化能够彻底消除锁的开销,它依赖于逃逸分析技术。锁粗化优化能够减少线程申请/释放锁的频率,其代价是使临界区长度变大,从而可能导致线程在申请锁时的等待时间变长。偏向锁优化可以减小锁的申请/释放的开销,它不适用于争用程度较高的锁。适应性锁优化可以减小锁申请的开销,有利于减少上下文切换。

  锁的开销主要是由争用锁引起的。这些开销主要包括:上下文切换与线程调度开销、内存同步、编译器优化受限的开销以及限制可伸缩性。降低锁的开销可以从使用锁的替代品、降低锁的争用程度以及减少线程所需申请的锁的数量这几个方面入手。

  使用可参数化锁可以减少线程所需申请的锁的数量从而降低锁的开销,但是它在一定程度上破坏了封装性。

  减小临界区的长度可以减少锁的持有时间,从而降低锁的争用程度。减小临界区的长度有利于适用性锁优化发挥作用。在不影响线程安全的前提下,将临界区中的阻塞式 I/O 等阻塞操作以及较耗时的操作挪动到临界区之外可以减小临界区的长度。

  减小锁的粒度可以降低锁的申请频率从而降低锁的争用程度。减小锁的粒度常用技术包括锁拆分技术和锁分段技术。锁拆分技术在高争用情况下的效果可能并不明显;锁分段技术会使得对整个对象进行加锁比较困难乃至不可能。

  减少上下文切换可以从这几个方面入手:控制线程数量、避免在临界区中执行阻塞式 I/O 等阻塞操作、避免在临界区中执行比较耗时的操作和减少 Java 虚拟机垃圾回收。

  运用多线程设计模式也有助于提升多线程程序的性能,但是程序的复杂性也可能相应增加。

  伪共享产生的前提是多个线程访问被缓存到同一个缓存中的不同变量,它会导致大量的缓存未命中,从而增加内存访问操作的开销。了解 Java 对象的内存布局有助于分析与消除伪共享。Java 对象内存布局的规则包括:对象是以 8 字节为粒度进行对齐的、对象中的实例字段并非依照其源代码声明顺序排列以及继承自父类的实例字段不会与类本身定义的实例字段混杂在一起进行存储等。使用 jol 工具可以查看具体对象的内存布局情况。判断伪共享是否存在可以从分析多个线程是否存在共同的共享变量入手,并通过 jol 以及 Linux 内核工具 perf 来进一步分析与确认。伪共享可通过手工填充、自动填充以及降低共享变量的访问频率这几个方面来消除与规避。手工填充和自动填充可以在无须调整程序算法的前提下消除伪共享。手工填充的缺点比较多,使用该方法我们必须知道缓存行的宽度、Java 对象的具体内存布局,这使得该方法存在硬件、软件层面的可移植性问题,并对人员的要求比较高。并且,我们还需要避免手工填充的字段被 Java 虚拟机优化掉,自动填充依赖于 @Contented 注解,它避免了手工填充的缺点,但是其消耗的额外空间更多。Java 虚拟机对自动填充的支持需要通过 Java 虚拟机的开关 “-XX:-RestrictContended” 开启。虽然减少共享变量的访问频率所带来的效果可能比较明显,但是由于它可能涉及程序算法的调整,因此其适用范围比较有限。

本章知识结构图


多线程编程的硬件基础与 Java 内存模型

本文摘抄自《Java 多线程编程实战指南》核心篇 第十一章小结

本章介绍了多线程编程的硬件基础以及 Java 内存模型的基础知识。


  高速缓存是一个存取速率远比主内存大而容量远比主内存小的存储部件,其引入弥补了处理器与主内存处理能力之间的鸿沟。高速缓存相当于一个由硬件实现的散列表,其键为内存地址,其值为从内存读取或者准备写入内存的数据。高速缓存中的每个桶可包含若干缓存条目。缓存条目中的 Tag 部分包含了内存地址的高位部分比特;Flag 部分指示了缓存条目的有效性;缓存行用于存储从内存读取或者准备写入内存的数据,其容量在 16~256 字节之间不等,一个缓存行可用于存储多个变量。缓存命中意味着待读取或者写入内存的数据在高速缓存中存在相应的副本,这可以提升内存访问效率。缓存未命中包括读未命中和写未命中,它不利于性能,但是由于高速缓存容量的限制又往往是不可避免的。Linux 内核工具 perf 可用来查看缓存未命中情况。现代处理器多采用多级高速缓存,典型的高速缓存层级包括 L1 Cache/L2 Cache 和 L3 Cache。

  缓存一致性协议保障了多个处理器上高速缓存中的数据副本的数据一致性,避免了一个处理器读取到共享变量的旧值以及避免了一个处理器对共享变量所做的更新丢失。MESI 协议是一个广为使用的缓存一致性协议,在该协议下缓存条目的 Flag 可能值包括:M/E/S/I。内存读/写操作是通过处理器发送与接收相关消息并更新缓存条目的 Flag 实现的。这些消息包括:Read/Read Response、Invalidate/Invalidate Acknowledge、Read Invalidate、Writeback。

  写缓冲器与无效化队列的引入弥补了 MESI 协议的性能弱点。

  写缓冲器是处理器内部的一个容量比高速缓存还小的私有高速存储部件。其引入使得内存写操作的执行处理器无须等待其他处理器回复 Invalidate Acknowledge/Read Response 消息便可以执行其他指令,从而减小内存写操作的延迟。写缓冲器能导致写线程对共享变量所做的更新无法被其他处理器同步过去。存储转发技术使得一个处理器可以直接从写缓冲器中读取该处理器先前执行的写操作的结果,但是它也可能导致可见性问题。另外,写缓冲器还会导致 StoreLoad 重排序和 StoreStore 重排序。

  无效化队列的引入使得处理器在接收到 Invalidate 消息之后可以立即回复 Invalidate Acknowledge 消息,这减少了发送 Invalidate 消息的处理器的等待时间。无效化队列可能使写线程对共享变量所做的共享无法反映到读线程执行处理器的高速缓存中,即导致可见性问题。无效化队列可以导致 LoadLoad 重排序。

  从硬件的角度来看,可见性的保障是通过写线程和读线程配对使用存储屏障和加载屏障实现的。存储屏障能够冲刷写缓冲器使得写线程对共享变量所做的更新能够被其他处理器同步,加载屏障能够清空无效化队列,使得写线程对共享变量所做的更新能够反映在读线程执行处理器的高速缓存之中。

  获取屏障相当于 LoadLoad 屏障和 LoadStore 屏障的组合,释放屏障相当于 StoreStore 屏障和 StoreLoad 屏障的组合。LoadLoad 屏障相当于加载屏障;而 StoreLoad 屏障是“全能型”屏障,它既可以充当存储屏障,也可以充当加载屏障。

  Java 虚拟机(JIT 编译器)为了确保 final 关键字的语义,会在 final 字段初始化与构造器返回之前插入一个 StoreStore 屏障,这使得 final 字段初始化操作无法被重排序到构造器之外,从而确保了构造器返回之后相应对象的 final 字段总是初始化完毕的。有序性的保障是通过写线程与读线程配对执行释放屏障和获取屏障实现的,同样这些屏障也是 Java 虚拟机(JIT 编译器)替我们的应用程序插入的。Java 虚拟机(JIT 编译器)会在 volatile 变量写操作之后插入一个 StoreLoad 屏障,该屏障不仅充当了存储屏障以冲刷写缓冲器,它还充当了加载屏障以清空无效化队列从而消除了存储转发技术的副作用。Java 虚拟机(JIT 编译器)会在 volatile 变量读操作前插入一个 LoadLoad 屏障,该屏障充当了加载屏障,用于清空无效化队列。

  Java 内存模型从“什么”(What)的角度来回答线程安全有关问题,JSR 133 对 Java 内存模型进行了增强和修复。Java 内存模型规定,long/double 型变量以外的任何变量的读/写操作具有原子性;volatile 变量修饰的 long/double 型变量的读/写操作也具有原子性。long/double 型普通变量的读/写操作的原子性取决于具体的 Java 虚拟机。happens-before 从可见性的角度对有序性进行描述。happens-before 关系具有传递性和累积效果。Java 内存模型定义的 happens-before 规则包括:程序顺序规则/内部锁规则/volatile 变量规则/线程启动规则和线程终止规则。Java 标准库本身也定义了一些 happens-before 规则。从语言的层面来看,这些规则是通过使用 Java 的同步机制实现的;从底层的角度来看,这些规则是由 Java 虚拟机/编译器以及处理器一同协作来落实的,内存屏障则是 Java 虚拟机/编译器和处理器之间的“沟通”纽带。

本章知识结构图


Java 异步编程

本文摘抄自《Java 多线程编程实战指南》核心篇 第九章小结

本章介绍了同步计算与异步计算的概念,并介绍了 Java 平台对异步计算所提供的相关 API。


  从单个任务的角度来看,任务的执行方式可以是同步的,也可以是异步的。同步方式的优点是代码简单/直观,缺点是它往往意味着阻塞,因此不利于系统的吞吐率。异步方式的优点则是它往往意味着非阻塞,因此有利于系统的吞吐率,其代价是相对复杂的代码和额外的开销。阻塞/非阻塞是任务执行方式的属性,它们与任务执行方式没有必然的联系:同步任务既可能是阻塞的,也可能是非阻塞的;异步任务既可能是非阻塞的,也可能是阻塞的。对于同一个任务,我们既可以说它是同步任务也可以说它是异步任务,这取决于任务的执行方式以及我们的观察角度。

  Runnable/Callable 接口是对任务处理逻辑进行的抽象,而 Executor 接口是对任务的执行进行的抽象。Executor 接口使得我们能够对任务的提交与任务的具体执行细节进行解耦,这为更改任务的具体执行细节提供了灵活性与便利。ExecutorService 接口是对 Executor 接口的增强:它支持返回异步任务的处理结果/支持资源的管理接口/支持批量任务提交等。ThreadPoolExecutor 是 Executor/ExecutorService 接口的一个实现类。实用工具类 Executors 为线程池的创建提供了快捷方法。Completion Service 接口为异步任务的批量提交以及获取这些任务的处理结果提供了便利,其默认实现类为 ExecutorCompletionService。

  FutureTask 是 Java 标准库提供的 Future 接口实现类,它还实现了 Runnable 接口。因此,FutureTask 可直接用来获取异步任务的处理结果,它可以交给专门的工作者线程执行,也可以交给 Executor 实例执行,甚至由当前线程直接执行(同步)。一般来说,FutureTask 是一次性使用的,一个 FutureTask 实例代表的任务只能够被执行一次。如果需要多次执行同一个任务,那么可以考虑 AsyncTask 类。

  计划任务的执行方式包括延迟执行和周期性执行。ScheduledThreadPoolExecutor 是 ScheduledExecutorService 接口的默认实现类,它可以用于执行计划任务。ScheduledFuture 接口可用来获取延迟执行的计划任务的处理结果。如果要获取周期性执行的计划任务的处理结果,可以使用自定义的 AsyncTask 类。周期性执行的计划任务,其执行周期并不是固定的,而是受任务单次执行耗时的影响:提交给 scheAtFixedRate 方法执行的计划任务,其执行周期为 max(Execution Time,period);提交给 scheduleWithFixedDelay 方法执行的计划任务,其执行周期为 Execution Time + delay。计划任务在其执行过程中如果抛出未捕获的异常,那么该任务将不会再被执行。

本章知识结构图