Shared posts

27 Oct 09:41

Java线程池介绍

by 小编辑

Java线程池介绍

The post Java线程池介绍 appeared first on 头条 - 伯乐在线.

27 Oct 09:39

世间万象都可以用两只猪来解释

by 新用户365758

Image title

这一阵的合并案接二连三,美团抱团大众点评,去哪儿携程携手同行。其中我们也听闻了一些企业间的博弈。也许可以从有名的两只猪来解释这些情形。

智猪博弈(the Boxed Pigs Game)是博弈论中的一局。一头大猪和一头小猪养在一个箱子里,箱子的一头是食槽,另一头是杠杆,按下杠杆就有食物落进食槽里。如果由小猪去按下杠杆,等它赶到食槽的时候,会发现大猪已经全吃干净了。如果由大猪去按下杠杆,赶到食槽的时候,会发现尽管小猪已经吃了许多,还有残剩的食物。那么这个箱子里会发生什么呢?

对小猪来说,按杠杆,一无所获;等着大猪按杠杆,倒有可能吃到许多食物。所以小猪的策略一定是守株待兔。而大猪只要相信小猪是理智的(猪可是智慧的动物),就料得到小猪一定不会去按杠杆,所以为了能有东西吃,大猪只能自己去按杠杆,并且忍受小猪占去它劳动换来的大半食物。在这个例子里,光脚的不怕穿鞋的,体量弱小的玩家反而挟制了体量强大的玩家。

智猪博弈的经典实例,是科威特和沙特阿拉伯这对欢喜冤家。科威特虽然弹丸小国,但从政治到经济都绑架着隔壁的大土豪。90年代海湾战争,科威特遭伊拉克侵略,战争打响后的12个小时内,科威特皇室就丢下整个国家逃走了,沙特顾及自身国土安全,只能出兵和伊拉克交手,帮科威特驱逐伊军。而在 OPEC 组织里,以沙特为首的各大石油输出国试图形成一个垄断联盟,控制国际油价,科威特这个猪队友却作了弊,把石油以较低价格出售。结果是,沙特为了保持高油价,只能减产少卖,而科威特作为市场上难得的低价供应商,则借机大卖特卖。

在中国的商界里也有案例。比如哈尔滨理工大学的研究发现,在哈尔滨的牛奶产业里,本地的小奶场需要承担生奶资源相关的投资(例如修缮牧场),而资金更充裕的外来牛奶企业(国企和外企)则搭了免费的顺风车。原因是外来的牛奶企业可以出于品质把控,随时选择不收购哈尔滨当地的牛奶。而当代的奶场主除了把牛奶卖给这些收购企业,没有别的出路,因此只能不断根据要求做出投资,改进奶源品质。

在美团和大众点评的合并案里,大小猪的角色非常明确。美团公布的2014年全年交易额在460亿元,至2015年6月底,上半年交易额又就被刷新到了470亿元。大众点评没有交出直接的成绩单,只称2014年单月成交额超20亿,业内人士估计全年不超过300亿元,远远不敌美团。两家合并时,作为大猪的美团提高了小猪大众点评的估值。同时在阿里口碑和百度糯米仍然强敌压境的情况下,美团为了减少内部管理运转的摩擦,很可能会高额补贴大众点评的管理层以进行重构,获得对大众点评的控制权,否则这些小猪管理层满可以继续维持原状,在王兴-张涛双总裁、北京-上海双总部的模式里模糊权力焦点,拖垮这家大企业的行政效率。

同样的情形也发生在昨天刚刚宣布合并的大猪携程和小猪去哪儿。百度出售旗下绝大部分的去哪儿股份,以在体量更大的携程换取25%的投票权,携程则拥有去哪儿45%的投票权。管理层架构上,携程原有的六名董事可能保持不变,只是新加入百度的李彦宏和叶卓。而去哪儿的董事会上,百度提名的4名董事置换成携程提名的4名董事。另有可靠消息称,去哪儿的CEO庄辰超也将离职。

另一只小猪糯米,大概可以毫无忧虑地度过这个资本寒冬,因为大猪百度为了保证双方还能在外卖领域占住一席之地,最有可能是勒紧裤带给糯米输送资金,奋力一搏。也或许正是外卖领域这头的资源配给绷紧,才让百度在旅游行业松了手,丢下了去哪儿这只小猪去和携程博弈协作。

图片来自网络

27 Oct 06:53

王兴戴上了婚戒,却改变了去哪儿的命运

by guest

编者按:本文来自开八(微信订阅号 hlkaiba),授权36氪转载

话说,八姐昨天真的要给跪了啊,180度orz了,尼玛,什么?居然,真的,携程和去哪儿合体了。凌乱啊。

八姐昨天问了一圈多少知点情的小盆友们,几乎所有人的判断是,突然,然后没有想到会这么快合并。

综合分析了各种小心思,八姐得出的结论是,百度是受了美团点评合并的刺激,才快速地做出了将去哪儿股权换给携程的决定的,而去哪儿特别是庄辰超无疑是郁闷地、受伤了,风中凌乱的。。

所以,可以这么理解,王兴做了个决定,戴上婚戒迎娶张涛的同时,却改变了去哪儿的命运——本来有希望和携程一拼高下的去哪儿,却因为百度的焦虑,让携程成了自己的大股东。哎呀,好狗血!

为毛这么说呢?来,听我给你八。

首先,八姐觉得,在今年10月份之前,百度其实是不着急将去哪儿和携程合并的。

1,去哪儿自己搞到钱了。百度想卖去哪儿有一段时间了,但是今年5月的时候,因为庄辰超的剧烈反对,百度的算盘落空了。这其中其实隐藏的逻辑其实是,百度还可以忍一段时间,不着急。

6月2日,庄辰超又搞到了一笔钱,5亿美刀,这笔钱至少可以烧一年,既然去哪儿已经搞到钱了,百度估计也暂时歇了卖去哪儿股份的念想。那么正常来说,时间只过了4个多月,百度还是可以再忍忍的嘛,万一去哪儿拼到了比携程更好的位置呢?所以正常来说,百度不应该太着急现在出手。

2,去哪儿在百度的O2O盘子里充当着刷销售量的重要位置,所以,八姐觉得,单从布局O2O的角度、从数字的角度,百度估计也没想着急着卖去哪儿。

请翻看今年7月底百度发的二季度财报,百度第一次公布O2O的交易额(GMV)数据,百度糯米、百度外卖和去哪儿合共交易额达405亿元人民币,这一数字甚至比美团的交易额还要高。而八姐也分析过,尽管在百度的措辞中,去哪儿排名靠后,但Q2去哪儿交易额约为350亿元,这才是O2O的交易额老大好不好。

3,就在9月,百度还把糯米的总经理曾良等三人拉入去哪儿董事会,同时将百度的董事会席位扩充到5个。八姐觉得彼时的百度尚在想着加强对去哪儿的控制力度以及保证去哪儿与百度O2O的协调性,估计还没想着卖呢。

来,再上证据。除了董事会席位扩容外,李彦宏还曾在新闻稿中盛赞去哪儿管理层,并说要加强百度与去哪儿的战略合作。

Image title

同时,庄辰超神马的高管似乎干得还很来劲,还签了一份4年的工作协议。

Image title

所以,我敢说,当时的百度绝没想着将去哪儿股权易手携程,庄辰超也绝没想到会从了携程,要不然,以他嫉恨携程的态度,又怎会签个4年的合同呢?

4,同样,又在9月初,去哪儿还宣布了组织架构调整,建事业群,奖励优秀员工,搞内部孵化,blablabla。这明显是撸起袖子大干一场,将携程踩于脚下的节奏啊。这明显是和百度干爹商量好的,一起努力加油,和糯米一起成为中国O2O老大,一起happy ending的感觉嘛。

所以八姐判断,直至9月份,百度还不想卖去哪儿呢。

那么,为毛仅仅一个多月之后,一切都变化了,携程和去哪儿这么快就合并了呢?八姐觉得,原因无他,美团和大众点评合并了啊。

10月7日,国庆还没结束,美团和大众点评就宣布合并了。俺觉得,估计听到这一消息的同时,百度就觉得,尼玛,坏菜了,天都快塌了。

美团和点评合并了,鬼都知道,百度糯米就受到了巨大的压力。百度想着慢慢地将糯米、去哪儿捆绑到一起的想法落空了。一切必须要快快快!!!

八姐甚至估计,百度的高管们、顾问啊、负责投资的大脑们连夜开会想对策。最后想出的局只能是,割去哪儿,减亏止损,引入携程,做更大的局,盘活O2O概念。

所以,八姐就听说,在点评和美团合并之后没几天,去哪儿的高管们就在和香港媒体接触的时候,暗示,庄辰超不再反对和携程合并,一切皆有可能。

你说,去哪儿的管理层们,庄辰超会是情愿的吗?会是甘心将控制权拱手让与携程的吗?不可能。我认为,必然是,百度不留余地地、压迫式的推进了和携程的交易。没办法,庄辰超股份只有7%,怎么敌得过大粗腿百度呢?

说去哪儿管理层是不情愿合并的,原因如下:

1,读了庄辰超的内部邮件,我的个人感觉是,有点心不甘情不愿,比如还专门强调了携程的非控股权,比如,还强调去哪儿是独立的。(反正我是这么感觉的,一千个人心中有一千个哈姆雷特哈。)

2,在稍晚时候的分析师问答环节,据说在提及这一交易时,去哪儿的高管说是百度推动的,言外之意是,去哪儿高管只是被动接受。

而且,以庄辰超今年初的言论和今年6月在财报里否认了将于与程合并的态度来看,他是绝不愿接受这种情形的。

Image title

anyway,当然,也有盆友说,人是会变的,嘿嘿。可能吧。

好了,再来说百度。将去哪儿的股份置换成携程的了,百度在旅游OTA上的布局暂时看起来是完美的了:甩掉了去哪儿烧钱的包袱,起码财报会好看些;成为了携程最大的机构股东,在股权分散的携程中,百度便宜尽占;携程和去哪儿再无竞争,烧钱停止,再加上百度的流量,这会快速提高百度在旅游O2O中的销售额;百度过两天发财报了,受这个交易的影响,即使财报不好看,也会令股价上扬,有高市值,才能干更多的事情。

而且大胆做一个预估,在去哪儿携程案之后,百度还将在做几笔大买卖,或卖,或买或合,等着看吧。且看资本高手百度如何突出重围吧。

只是,八姐实在不喜欢现在这种互联网公司态势——最终都是资本和BAT的天下,颠覆者又在哪里?不好玩。

27 Oct 01:01

百度一下携程去哪儿,是不是OTA时代落幕的开始?

by 尧异

百度、携程、去哪儿这一出戏演完,至少说明了一个问题,如果你有耐心把一只羊羔从小养大,还是能换回一把镰刀的。

现在看来,百度换回的是一把相当锋利的镰刀,认准在线旅游的赛道,想要大干一场。这时候想起“千年老二”这个说法,还真是有种宿命感。

*     *     *

36氪在昨晚发布的两篇文章中(文章1文章2)大概简述了“合并”的来龙去脉,我更倾向于用“重构”来定义这件事。综合新闻通稿和两位CEO的内部邮件,我们先说说这笔交易中的几个潜台词:

1、去哪儿1股ADS置换携程0.725股ADS:截至昨晚10点30分(后来涨幅有所收窄),开盘即大涨的携程市值大约在132亿美元,去哪儿是62亿美元左右,前者的股价大约为92美元左右,后者股价则在46美元左右,按照市值和股价来估算,去哪儿存在一定幅度的溢价。

2、百度出售去哪儿的绝大多数股权,拥有携程25%的总投票权,成为携程第一大股东,携程拥有去哪儿45%的总投票权:重构之前,携程董事会有6名成员,而李彦宏和叶卓加入之后百度的投票权还只有25%,这可能意味着携程的董事会只是扩容到8人,之前6名董事维持不变,原有的携程团队依然保持了很高的话语权,从而规避了被百度控制的可能。去哪儿则有所不同,之前就已经扩容的董事会共有9名成员,其中百度占了4席(包括李彦宏、糯米总经理曾良等人),本次携程4名高管是替换这4名董事加入的董事会,掌握了45%的投票权。

3、去哪儿维持独立运营,并维持与百度的商业合作,携程和百度将展开商业合作。李彦宏说“这将会为我们的投资人创造新的价值”,梁建章说“这将有助于中国旅游业的健康发展”,庄辰超说“感谢全球投资人对我们的支持”。

*     *     *

最后的最后,携程没能成为BAT之后的X,百度也知道自己必须在O2O领域有所作为,去哪儿作为千年老二,也就成了这个旅游行业大事件的注脚。

如我们在之前报道所说,百度在搜索引擎的单一营收被华尔街视为不可持续增长,而在可能成为增长引擎的O2O领域还缺乏利器,导致股价难看,之前给糯米的200亿投资许诺也没能换来什么。于是百度打出了手中仅剩的大牌——去哪儿,换来了行业龙头、有增速还有盈利能力的携程。

昨天开盘之后百度股价上涨,虽然涨幅不能和携程去哪儿的两位数比,但也一扫近日颓势。也许我们能从中看出华尔街的纠结:这是一笔好生意(股权有溢价,几乎零成本拿下OTA第一玩家),但成效还得假以时日来观察。我的同事孝羽说:总有种百度在甩包袱的感觉。

好生意的背后,是百度靠去哪儿打到了携程的软肋——其实不能算是软肋,去哪儿对携程更像是抄后路的威胁。无论是去哪儿在机票业务上的强势,还是直签低端酒店数量的快速增长,都让携程老大的位置坐的不那么稳,更不用说被去哪儿拖下水打价格战,一度还陷入季度业绩净亏损。一贯不乐意跟BAT站队的携程,这次更多的是在站队和规避与去哪儿直面冲突的两害相权中取了其轻。

去哪儿是一家令人钦佩的公司(虽然我同样钦佩携程),不甘做老二的他们一直在用近乎自绝后路的激进打法,但最终还是选择了妥协。虽然这更像是一种被迫的妥协,当公司经营持续出现营业收入和净亏损一个数量级、且竞争对手同样表现出色的时候,一个“用烧钱换时间”的故事可能也不那么动听了。

而以去哪儿和携程这么大体量的上市公司,都选择了妥协,是否意味着,资本的寒冬比我们想象的更严酷一些?因为毕竟外部正在壮大的诸如阿里旅行·去啊和美团,都还不至于构成如此大的威胁。

*     *     *

然后我们回头看看国内在线旅游战场,我更倾向于判断,这一笔重构意味着OTA时代进入后鼎盛时期——不太可能再有玩家以OTA的形态挑战“携程-去哪儿联盟”。我的理解是,如果再有玩家想切入在线旅游市场,OTA形态已经不是一种选择了(不是好坏的问题,是行不行的问题,答案是不行)。

华泰证券行业分析师刘洋的观点是,旅游行业需要寻找新的在线机会,无论是UGC、PGC还是其他什么模式,都有待探索。他还预测,可能会有一些玩家重新回到线下去寻求机会。这也是我们之前讨论过的,当线上获客成本由于玩家互相烧钱而炒得很高的时候,线下获客成本反而显得没那么高了。

如果我的判断正确,OTA已经进入后鼎盛时期,那么行业格局稳定,活力也会随之减少,再考虑到用户消费习惯的改变(比如不再以OTA作为旅游第一接触点),这都意味着OTA时代的大幕可能正在缓缓落下。

所以在旅游行业创业,面向PC端的产品售卖平台可能越来越不是一个好的选择了。而对于同程、途牛(昨日开盘后也有所上涨)、驴妈妈等平台而言,这次重构难言好事,尽管旅游行业的在线渗透率只有一成,但很难说这些平台能在在线渗透率不断提升的过程中,能吃到多少市场份额。说不定,下一次合并,就出现在这几家之间?

那么,和传统OTA增长路径有所不同的美团和阿里旅行·去啊,可能受到的影响会小一些。旅游行业最美好的一点就是,如果不追求当老大,不和巨头去做同质化竞争,在一个需求长尾化且现金流还不错的行业中,依然有不少的机会(至少可以被巨头收购)。

回到开头,去哪儿这家在中国在线旅游行业中有过浓重一笔的公司,可能再难画出同样的色彩了。本来我还期待前总裁孙含晖离职之后的架构调整,能重新激发去哪儿与携程战斗的活力。现在看来,目的地、旅游SaaS平台和高星酒店和海外业务三个事业群倒是补齐了携程的几块短板,还能强化在海外休闲度假业务上的资源整合能力,而随后进行的董事会扩容,更像是一场阳谋。

同样令人抱憾的是,cc.Zhuang这位斗士的出走问题又被提上了台面,毕竟他在内部邮件中已经像安排后事一样保住了去哪儿员工的股权激励福利。有业内人感叹,比艺龙被收购时候的内部邮件有人情味多了。

想起今天有人打趣,明天可以到去哪儿门口举牌子招地推、产品和技术了。

感谢本文合作者孝羽

27 Oct 00:51

携程去哪儿网合并:背后的技术力量回顾

by 郭蕾

2015年10月26日,携程宣布与百度达成股权置换交易,通过股权交换的方式来完成去哪儿网与携程的合并。交易完成后,百度将拥有携程普通股可代表约25%的携程总投票权,携程将拥有约45%的去哪儿总投票权。

携程和去哪儿网都是中国领先的在线旅游平台,合并后合计市值约达156亿美元。携程创立于1999年,总部在上海,2003年在纳斯达克上市。相对于携程,去哪儿网则比较年轻,创立于2005年,总部在北京,2013年在纳斯达克上市。去哪儿网是国内的Java使用大户,目前线上有上千个Java系统,而携程则使用的是.NET。对于携程为什么选择.NET,网上有很多的讨论,比较合理的解释是携程创立之初选择使用了ASP,而后随着技术的发展从ASP升级到了ASP.NET。

在过去的几年中,InfoQ中国曾对去哪儿网、携程旅行网进行过详细的跟踪报道。现从架构、开发语言、搜索引擎、云平台、无线等多个维度盘点二者的技术发展历程。

搜索

去哪儿网成立之初是一家纯旅游搜索公司,它将各大小OTA销售的机票、酒店信息汇集到网站上直接销售,可想而知搜索对它的重要性。而携程作为一家专业而全面的OTA(在线旅游)网站,拥有非常多的产品,如何帮助用户快速定位产品是他们的重中之重,搜索引擎又扮演了一个非常重要的角色。像去哪儿网和携程这类的旅游行业的垂直搜索,挑战非常多,比如产品种类繁多,如何帮用户挑选出最具性价比的产品,产品价格和日期、地点强相关,数据量更新大等。在2014年10月的QCon上海软件开发大会上,携程搜索产品研发部总监分享了介绍了携程的搜索系统架构:

搜索系统的架构大概分为两个部分,分别是在线检索系统和离线的索引系统。在线检索系统主要负责处理用户的输入,并返回查询结果,这其中有两个比较重要的模块,一个是Demand Service,负责用户查询前的引导,一个是QRW Service,负责分词、纠错、语义解析和查询重写。离线的索引系统主要负责把数据以索引的形式组织起来,这其中又分为两块,一个是全量索引系统,定时执行,主要作用是建立所有产品的索引,并对数据进行优化和压缩。一个是实时索引系统,它负责把最新的产品数据快速推送给用户。

在2012年的QCon北京的演讲中,去哪儿网的朱翔分享了去哪儿搜索引擎QSearch设计与实现。由于时间比较早,所以并不确认目前去哪儿网是否还在使用QSearch。根据演讲介绍,QSearch其实是基于Lucene和Solr,它有丰富的存储类型,可以定制规则排序算法。框架整体上分为两部分,一个是Searcher,一个是Indexer,分别负责搜索和索引。考虑到数据量比较大,会将Indexer分片,每个Searcher负责一个Shard。同样为了保证高可用,系统又有多个索引的拷贝。在Indexer的上层又有Dispatcher来负责结果的合并和请求分发。

私有云

随着业务的不断扩展,各大公司都已经开始着手构建自己的私有云平台。在2014年,携程的吴毅挺分享了题为《基于OpenStack打造携程私有云》的演讲。携程目前在南通和上海的数据中心都已经大规模部署了自己的私有云平台,平台完全基于开源的OpenStack平台构建。从计算的角度来看,携程是将不同的虚拟化技术混合在一起,包括KVM、VMware、Docker。网络这块,使用的是OpenVSwitch和VLan,VMware使用的是Nova-VMware-Driver。具体读者可以浏览演讲视频。

对于去哪儿网的私有云平台,目前未找到相关的技术资料。不过,据ZDNet的报道,去哪儿网从2012年就开始使用了OpenStack,可以说是中国第一批用户。所以从这个信息推断,去哪儿网目前的私有云平台也是基于OpenStack。

开发语言

去哪儿网是国内的一个Java使用大户,目前有上千个系统在线上运行,公司内有大批国内优秀的Java工程师。在过去的几年里,他们创造了大批的工具和系统来解决开发过程中遇到的问题,内部有非常完善的Java开发生态。根据高级系统架构师孙立在2014年的介绍,去哪儿网基于Java的生态平台有自动化发布系统、可靠消息系统QMQ、与测试相关的Mock平台、自动化测试Qunit、代码Review系统、任务调度系统以及监控报警平台。

携程主要使用的语言是.NET,.NET相关的实践并没有做过太多分享。不过,在今年Java 20岁生日InfoQ发布的迷你书中,CTO叶亚明这样评价Java:

尽管当下仍不断有新语言出现,但毫无疑问,未来二十年,Java仍将会是最受欢迎的编程语言。如大家所知,Java不仅仅只是一种主流编程语言,它同时也代表着一整个活跃的生态系统。Java开发者们将自己的聪明才智投入到这个平台上,而平台则回报给他们工作岗位与相应薪酬。要打理好现有的Java解决方案,我们需要Java。而为了顺利推动未来的业务发展,我们必将打造出更多Java应用程序。

无线

去哪儿网从2010年开始投入无线领域,随着HTML5标准的成熟,他们开始探索使用HTML方案在性能及体验间寻求平衡的解决方案。无线技术高级总监蔡欢分享了去哪儿的SPA HTML应用架构。他提到NativeApp有很多用户体验方面的优势,但也有很多的局限,比如分平台开发维护成本高,部署成本高。目前WebApp的解决方案有传统的page2page、pjax(pushState Ajax)、SPA(SinglePageApplication)。相比其它两种方案,SPA的优势是前后端分离,灵活度高,贴近于Native应用的交互体验。去哪儿网的SPA设计思路主要包括模块化开发、视图切换、URL路由、模板前端渲染、响应式、浏览器及App内做功能扩充和体验差异、开发环境及构建⼯具。

截止到2014年年底,携程50%的交易量都已经来自于手机端,为了迎接移动方面的挑战,携程在2014年做了非常多的努力,其中包括组织架构调整,拆散无线团队,分到各个业务团队中。关于当时面临的调整,叶亚明这样描述:无线是单独的事业部,所有无线需要开发的功能到那儿排队,这就导致无线的开发永远滞后于Web。对于解决方案,他做了几个总结,一是调整组织架构,让每个业务开发管好他自己的无线产品。二是推动工程师文化,提高大家的学习能力和业务能力。

同样,携程在无线端也尝试了HTML的解决方案。在2014年的QCon上海中,携程高级架构师刘普功分享了携程Mobile架构演化的演讲的演讲,介绍了Mobile 2.0下客户端H5/Hybrid/Native和服务端(H5 Service & Moblie Service)的架构调整和技术变迁。

另外,还有部分角度由于资料不全,所以无法做归类,现将相关的内容列举如下:

  1. Qunar酒店交易系统架构实践
  2. 携程App的网络性能优化实践
  3. 系统架构去哪儿了
  4. 深入解析和反思携程宕机事件
  5. 基于PXC的MySQL高可用架构探索
  6. 携程App for Apple Watch探索
  7. 携程首席架构师谈DevOps:找到合适的人最重要
26 Oct 01:43

淘宝内部分享:怎么跳出 MySQL 的10个大坑

by changqi

编者按:淘宝自从2010开始规模使用MySQL,替换了之前商品、交易、用户等原基于IOE方案的核心数据库,目前已部署数千台规模。同时和Oracle, Percona, Mariadb等上游厂商有良好合作,共向上游提交20多个Patch。目前淘宝核心系统研发部数据库组,根据淘宝的业务需求,改进数据库和提升性能,提供高性能、可扩展的、稳定可靠的数据库(存储)解决方案。 目前有以下几个方向:单机,提升单机数据库的性能,增加我们所需特性;集群,提供性能扩展,可靠性,可能涉及分布式事务处理;IO存储体系,跟踪IO设备变化潮流, 研究软硬件结合,输出高性能存储解决方案。本文是来自淘宝内部数据库内容分享。

MySQL · 性能优化· Group Commit优化

背景

关于Group Commit网上的资料其实已经足够多了,我这里只简单的介绍一下。

众所周知,在MySQL5.6之前的版本,由于引入了Binlog/InnoDB的XA,Binlog的写入和InnoDB commit完全串行化执行,大概的执行序列如下:

InnoDB prepare (持有prepare_commit_mutex);
write/sync Binlog;
InnoDB commit (写入COMMIT标记后释放prepare_commit_mutex)。

当sync_binlog=1时,很明显上述的第二步会成为瓶颈,而且还是持有全局大锁,这也是为什么性能会急剧下降。

很快Mariadb就提出了一个Binlog Group Commit方案,即在准备写入Binlog时,维持一个队列,最早进入队列的是leader,后来的是follower,leader为搜集到的队列中的线程依次写Binlog文件, 并commit事务。Percona 的Group Commit实现也是Port自Mariadb。不过仍在使用Percona Server5.5的朋友需要注意,该Group Commit实现可能破坏掉Semisync的行为,感兴趣的点击 bug#1254571

Oracle MySQL 在5.6版本开始也支持Binlog Group Commit,使用了和Mariadb类似的思路,但将Group Commit的过程拆分成了三个阶段:flush stage 将各个线程的binlog从cache写到文件中; sync stage 对binlog做fsync操作(如果需要的话);commit stage 为各个线程做引擎层的事务commit。每个stage同时只有一个线程在操作。

Tips:当引入Group Commit后,sync_binlog的含义就变了,假定设为1000,表示的不是1000个事务后做一次fsync,而是1000个事务组。

Oracle MySQL的实现的优势在于三个阶段可以并发执行,从而提升效率。

XA Recover

在Binlog打开的情况下,MySQL默认使用MySQL_BIN_LOG来做XA协调者,大致流程为:

1.扫描最后一个Binlog文件,提取其中的xid;

2.InnoDB维持了状态为Prepare的事务链表,将这些事务的xid和Binlog中记录的xid做比较,如果在Binlog中存在,则提交,否则回滚事务。

通过这种方式,可以让InnoDB和Binlog中的事务状态保持一致。显然只要事务在InnoDB层完成了Prepare,并且写入了Binlog,就可以从崩溃中恢复事务,这意味着我们无需在InnoDB commit时显式的write/fsync redo log。

Tips:MySQL为何只需要扫描最后一个Binlog文件呢 ? 原因是每次在rotate到新的Binlog文件时,总是保证没有正在提交的事务,然后fsync一次InnoDB的redo log。这样就可以保证老的Binlog文件中的事务在InnoDB总是提交的。

问题

其实问题很简单:每个事务都要保证其Prepare的事务被write/fsync到redo log文件。尽管某个事务可能会帮助其他事务完成redo 写入,但这种行为是随机的,并且依然会产生明显的log_sys->mutex开销。

优化

从XA恢复的逻辑我们可以知道,只要保证InnoDB Prepare的redo日志在写Binlog前完成write/sync即可。因此我们对Group Commit的第一个stage的逻辑做了些许修改,大概描述如下:

Step1. InnoDB Prepare,记录当前的LSN到thd中;
Step2. 进入Group Commit的flush stage;Leader搜集队列,同时算出队列中最大的LSN。
Step3. 将InnoDB的redo log write/fsync到指定的LSN
Step4. 写Binlog并进行随后的工作(sync Binlog, InnoDB commit , etc)

通过延迟写redo log的方式,显式的为redo log做了一次组写入,并减少了log_sys->mutex的竞争。

目前官方MySQL已经根据我们report的bug#73202锁提供的思路,对5.7.6的代码进行了优化,对应的Release Note如下:

When using InnoDB with binary logging enabled, concurrent transactions written in the InnoDB redo log are now grouped together before synchronizing to disk when innodb_flush_log_at_trx_commit is set to 1, which reduces the amount of synchronization operations. This can lead to improved performance.

性能数据

简单测试了下,使用sysbench, update_non_index.lua, 100张表,每张10w行记录,innodb_flush_log_at_trx_commit=2, sync_binlog=1000,关闭Gtid

 并发线程        原生                  修改后
 32             25600                27000
 64             30000                35000
 128            33000                39000
 256            29800                38000

MySQL · 新增特性· DDL fast fail

背景

项目的快速迭代开发和在线业务需要保持持续可用的要求,导致MySQL的ddl变成了DBA很头疼的事情,而且经常导致故障发生。本篇介绍RDS分支上做的一个功能改进,DDL fast fail。主要解决:DDL操作因为无法获取MDL排它锁,进入等待队列的时候,阻塞了应用所有的读写请求问题。

MDL锁机制介绍

首先介绍一下MDL(METADATA LOCK)锁机制,MySQL为了保证表结构的完整性和一致性,对表的所有访问都需要获得相应级别的MDL锁,比如以下场景:

session 1: start transaction; select * from test.t1;
session 2: alter table test.t1 add extra int;
session 3: select * from test.t1;

  • session 1对t1表做查询,首先需要获取t1表的MDL_SHARED_READ级别MDL锁。锁一直持续到commit结束,然后释放。
  • session 2对t1表做DDL,需要获取t1表的MDL_EXCLUSIVE级别MDL锁,因为MDL_SHARED_READ与MDL_EXCLUSIVE不相容,所以session 2被session 1阻塞,然后进入等待队列。
  • session 3对t1表做查询,因为等待队列中有MDL_EXCLUSIVE级别MDL锁请求,所以session3也被阻塞,进入等待队列。

这种场景就是目前因为MDL锁导致的很经典的阻塞问题,如果session1长时间未提交,或者查询持续过长时间,那么后续对t1表的所有读写操作,都被阻塞。 对于在线的业务来说,很容易导致业务中断。

aliyun RDS分支改进

DDL fast fail并没有解决真正DDL过程中的阻塞问题,但避免了因为DDL操作没有获取锁,进而导致业务其他查询/更新语句阻塞的问题。

其实现方式如下:

alter table test.t1 no_wait/wait 1 add extra int;
在ddl语句中,增加了no_wait/wait 1语法支持。

其处理逻辑如下:

首先尝试获取t1表的MDL_EXCLUSIVE级别的MDL锁:

  • 当语句指定的是no_wait,如果获取失败,客户端将得到报错信息:ERROR : Lock wait timeout exceeded; try restarting transaction。
  • 当语句指定的是wait 1,如果获取失败,最多等待1s,然后得到报错信息:ERROR : Lock wait timeout exceeded; try restarting transaction。

另外,除了alter语句以外,还支持rename,truncate,drop,optimize,create index等ddl操作。

与Oracle的比较

在Oracle 10g的时候,DDL操作经常会遇到这样的错误信息:

ora-00054:resource busy and acquire with nowait specified 即DDL操作无法获取表上面的排它锁,而fast fail。

其实DDL获取排他锁的设计,需要考虑的就是两个问题:

  1. 雪崩,如果你采用排队阻塞的机制,那么DDL如果长时间无法获取锁,就会导致应用的雪崩效应,对于高并发的业务,也是灾难。
  2. 饿死,如果你采用强制式的机制,那么要防止DDL一直无法获取锁的情况,在业务高峰期,可能DDL永远无法成功。

在Oracle 11g的时候,引入了DDL_LOCK_TIMEOUT参数,如果你设置了这个参数,那么DDL操作将使用排队阻塞模式,可以在session和global级别设置, 给了用户更多选择。
MySQL · 性能优化· 启用GTID场景的性能问题及优化

背景

MySQL从5.6版本开始支持GTID特性,也就是所谓全局事务ID,在整个复制拓扑结构内,每个事务拥有自己全局唯一标识。GTID包含两个部分,一部分是实例的UUID,另一部分是实例内递增的整数。

GTID的分配包含两种方式,一种是自动分配,另外一种是显式设置session.gtid_next,下面简单介绍下这两种方式:

自动分配

如果没有设置session级别的变量gtid_next,所有事务都走自动分配逻辑。分配GTID发生在GROUP COMMIT的第一个阶段,也就是flush stage,大概可以描述为:

  • Step 1:事务过程中,碰到第一条DML语句需要记录Binlog时,分配一段Gtid事件的cache,但不分配实际的GTID
  • Step 2:事务完成后,进入commit阶段,分配一个GTID并写入Step1预留的Gtid事件中,该GTID必须保证不在gtid_owned集合和gtid_executed集合中。 分配的GTID随后被加入到gtid_owned集合中。
  • Step 3:将Binlog 从线程cache中刷到Binlog文件中。
  • Step 4:将GTID加入到gtid_executed集合中。
  • Step 5:在完成sync stage 和commit stage后,各个会话将其使用的GTID从gtid_owned中移除。

显式设置

用户通过设置session级别变量gtid_next可以显式指定一个GTID,流程如下:

  • Step 1:设置变量gtid_next,指定的GTID被加入到gtid_owned集合中。
  • Step 2:执行任意事务SQL,在将binlog从线程cache刷到binlog文件后,将GTID加入到gtid_executed集合中。
  • Step 3:在完成事务COMMIT后,从gtid_owned中移除。

备库SQL线程使用的就是第二种方式,因为备库在apply主库的日志时,要保证GTID是一致的,SQL线程读取到GTID事件后,就根据其中记录的GTID来设置其gtid_next变量。

问题

由于在实例内,GTID需要保证唯一性,因此不管是操作gtid_executed集合和gtid_owned集合,还是分配GTID,都需要加上一个大锁。我们的优化主要集中在第一种GTID分配方式。

对于GTID的分配,由于处于Group Commit的第一个阶段,由该阶段的leader线程为其follower线程分配GTID及刷Binlog,因此不会产生竞争。

而在Step 5,各个线程在完成事务提交后,各自去从gtid_owned集合中删除其使用的gtid。这时候每个线程都需要获取互斥锁,很显然,并发越高,这种竞争就越明显,我们很容易从pt-pmp输出中看到如下类似的trace:

ha_commit_trans—>MySQL_BIN_LOG::commit—>MySQL_BIN_LOG::ordered_commit—>MySQL_BIN_LOG::finish_commit—>Gtid_state::update_owned_gtids_impl—>lock_sidno
这同时也会影响到GTID的分配阶段,导致TPS在高并发场景下的急剧下降。

解决

实际上对于自动分配GTID的场景,并没有必要维护gtid_owned集合。我们的修改也非常简单,在自动分配一个GTID后,直接加入到gtid_executed集合中,避免维护gtid_owned,这样事务提交时就无需去清理gtid_owned集合了,从而可以完全避免锁竞争。

当然为了保证一致性,如果分配GTID后,写入Binlog文件失败,也需要从gtid_executed集合中删除。不过这种场景非常罕见。

性能数据

使用sysbench,100张表,每张10w行记录,update_non_index.lua,纯内存操作,innodb_flush_log_at_trx_commit = 2,sync_binlog = 1000

 并发线程       原生               修改后
 32           24500              25000
 64           27900              29000
 128          30800              31500
 256          29700              32000
 512          29300              31700
 1024         27000              31000

从测试结果可以看到,优化前随着并发上升,性能出现下降,而优化后则能保持TPS稳定。
MySQL · 捉虫动态· InnoDB自增列重复值问题

问题重现

先从问题入手,重现下这个 bug

use test;
drop table if exists t1;
create table t1(id int auto_increment, a int, primary key (id)) engine=innodb;
insert into t1 values (1,2);
insert into t1 values (null,2);
insert into t1 values (null,2);
select * from t1;
+----+------+
| id | a |
+----+------+
| 1 | 2 |
| 2 | 2 |
| 3 | 2 |
+----+------+
delete from t1 where id=2;
delete from t1 where id=3;
select * from t1;
+----+------+
| id | a |
+----+------+
| 1 | 2 |
+----+------+

这里我们关闭MySQL,再启动MySQL,然后再插入一条数据

insert into t1 values (null,2);
select * FROM T1;
+----+------+
| id | a |
+----+------+
| 1 | 2 |
+----+------+
| 2 | 2 |
+----+------+

我们看到插入了(2,2),而如果我没有重启,插入同样数据我们得到的应该是(4,2)。 上面的测试反映了MySQLd重启后,InnoDB存储引擎的表自增id可能出现重复利用的情况。

自增id重复利用在某些场景下会出现问题。依然用上面的例子,假设t1有个历史表t1_history用来存t1表的历史数据,那么MySQLd重启前,ti_history中可能已经有了(2,2)这条数据,而重启后我们又插入了(2,2),当新插入的(2,2)迁移到历史表时,会违反主键约束。

原因分析

InnoDB 自增列出现重复值的原因:

MySQL> show create table t1\G;
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=innodb AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

建表时可以指定 AUTO_INCREMENT值,不指定时默认为1,这个值表示当前自增列的起始值大小,如果新插入的数据没有指定自增列的值,那么自增列的值即为这个起始值。对于InnoDB表,这个值没有持久到文件中。而是存在内存中(dict_table_struct.autoinc)。那么又问,既然这个值没有持久下来,为什么我们每次插入新的值后, show create table t1看到AUTO_INCREMENT值是跟随变化的。其实show create table t1是直接从dict_table_struct.autoinc取得的(ha_innobase::update_create_info)。

知道了AUTO_INCREMENT是实时存储内存中的。那么,MySQLd 重启后,从哪里得到AUTO_INCREMENT呢? 内存值肯定是丢失了。实际上MySQL采用执行类似select max(id)+1 from t1;方法来得到AUTO_INCREMENT。而这种方法就是造成自增id重复的原因。

MyISAM自增值

MyISAM也有这个问题吗?MyISAM是没有这个问题的。myisam会将这个值实时存储在.MYI文件中(mi_state_info_write)。MySQLd重起后会从.MYI中读取AUTO_INCREMENT值(mi_state_info_read)。因此,MyISAM表重启是不会出现自增id重复的问题。

问题修复

MyISAM选择将AUTO_INCREMENT实时存储在.MYI文件头部中。实际上.MYI头部还会实时存其他信息,也就是说写AUTO_INCREMENT只是个顺带的操作,其性能损耗可以忽略。InnoDB 表如果要解决这个问题,有两种方法。

1)将AUTO_INCREMENT最大值持久到frm文件中。
2)将 AUTO_INCREMENT最大值持久到聚集索引根页trx_id所在的位置。

第一种方法直接写文件性能消耗较大,这是一额外的操作,而不是一个顺带的操作。我们采用第二种方案。为什么选择存储在聚集索引根页页头trx_id,页头中存储trx_id,只对二级索引页和insert buf 页头有效(MVCC)。而聚集索引根页页头trx_id这个值是没有使用的,始终保持初始值0。正好这个位置8个字节可存放自增值的值。我们每次更新AUTO_INCREMENT值时,同时将这个值修改到聚集索引根页页头trx_id的位置。 这个写操作跟真正的数据写操作一样,遵守write-ahead log原则,只不过这里只需要redo log ,而不需要undo log。因为我们不需要回滚AUTO_INCREMENT的变化(即回滚后自增列值会保留,即使insert 回滚了,AUTO_INCREMENT值不会回滚)。

因此,AUTO_INCREMENT值存储在聚集索引根页trx_id所在的位置,实际上是对内存根页的修改和多了一条redo log(量很小),而这个redo log 的写入也是异步的,可以说是原有事务log的一个顺带操作。因此AUTO_INCREMENT值存储在聚集索引根页这个性能损耗是极小的。

修复后的性能对比,我们新增了全局参数innodb_autoinc_persistent 取值on/off; on 表示将AUTO_INCREMENT值实时存储在聚集索引根页。off则采用原有方式只存储在内存。

./bin/sysbench --test=sysbench/tests/db/insert.lua --MySQL-port=4001 --MySQL-user=root \--MySQL-table-engine=innodb --MySQL-db=sbtest --oltp-table-size=0 --oltp-tables-count=1 \--num-threads=100 --MySQL-socket=/u01/zy/sysbench/build5/run/MySQL.sock  --max-time=7200 --max-requests run
set global innodb_autoinc_persistent=off;
tps: 22199 rt:2.25ms
set global innodb_autoinc_persistent=on;
tps: 22003 rt:2.27ms

可以看出性能损耗在%1以下。

改进

新增参数innodb_autoinc_persistent_interval 用于控制持久化AUTO_INCREMENT值的频率。例如:innodb_autoinc_persistent_interval=100,auto_incrememt_increment=1时,即每100次insert会控制持久化一次AUTO_INCREMENT值。每次持久的值为:当前值+innodb_autoinc_persistent_interval。

测试结论

innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=1时性能损耗在%1以下。
innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=100时性能损耗可以忽略。

限制

  • innodb_autoinc_persistent=on, innodb_autoinc_persistent_interval=N>1时,自增N次后持久化到聚集索引根页,每次持久的值为当前AUTO_INCREMENT+(N-1)*innodb_autoextend_increment。重启后读取持久化的AUTO_INCREMENT值会偏大,造成一些浪费但不会重复。innodb_autoinc_persistent_interval=1 每次都持久化没有这个问题。
  • 如果innodb_autoinc_persistent=on,频繁设置auto_increment_increment的可能会导致持久化到聚集索引根页的值不准确。因为innodb_autoinc_persistent_interval计算没有考虑auto_increment_increment变化的情况,参看dict_table_autoinc_update_if_greater。而设置auto_increment_increment的情况极少,可以忽略。

注意:如果我们使用需要开启innodb_autoinc_persistent,应该在参数文件中指定

innodb_autoinc_persistent= on

如果这样指定set global innodb_autoinc_persistent=on;重启后将不会从聚集索引根页读取AUTO_INCREMENT最大值。

疑问:对于InnoDB表,重启通过select max(id)+1 from t1得到AUTO_INCREMENT值,如果id上有索引那么这个语句使用索引查找就很快。那么,这个可以解释MySQL 为什么要求自增列必须包含在索引中的原因。 如果没有指定索引,则报如下错误,

ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key 而myisam表竟然也有这个要求,感觉是多余的。
MySQL · 优化改进· 复制性能改进过程

前言

与oracle 不同,MySQL 的主库与备库的同步是通过 binlog 实现的,而redo日志只做为MySQL 实例的crash recovery使用。MySQL在4.x 的时候放弃redo 的同步策略而引入 binlog的同步,一个重要原因是为了兼容其它非事务存储引擎,否则主备同步是没有办法进行的。

redo 日志同步属于物理同步方法,简单直接,将修改的物理部分传送到备库执行,主备共用一致的 LSN,只要保证 LSN 相同即可,同一时刻,只能主库或备库一方接受写请求; binlog的同步方法属于逻辑复制,分为statement 或 row 模式,其中statement记录的是SQL语句,Row 模式记录的是修改之前的记录与修改之后的记录,即前镜像与后镜像;备库通过binlog dump 协议拉取binlog,然后在备库执行。如果拉取的binlog是SQL语句,备库会走和主库相同的逻辑,如果是row 格式,则会调用存储引擎来执行相应的修改。

本文简单说明5.5到5.7的主备复制性能改进过程。

replication improvement (from 5.5 to 5.7)

(1) 5.5 中,binlog的同步是由两个线程执行的

io_thread: 根据binlog dump协议从主库拉取binlog, 并将binlog转存到本地的relaylog;

sql_thread: 读取relaylog,根据位点的先后顺序执行binlog event,进而将主库的修改同步到备库,达到主备一致的效果; 由于在主库的更新是由多个客户端执行的,所以当压力达到一定的程度时,备库单线程执行主库的binlog跟不上主库执行的速度,进而会产生延迟造成备库不可用,这也是分库的原因之一,其SQL线程的执行堆栈如下:

sql_thread:
exec_relay_log_event
    apply_event_and_update_pos
         apply_event
             rows_log_event::apply_event
                 storage_engine operation
         update_pos

(2) 5.6 中,引入了多线程模式,在多线程模式下,其线程结构如下

io_thread: 同5.5

Coordinator_thread: 负责读取 relay log,将读取的binlog event以事务为单位分发到各个 worker thread 进行执行,并在必要时执行binlog event(Description_format_log_event, Rotate_log_event 等)。

worker_thread: 执行分配到的binlog event,各个线程之间互不影响;

多线程原理

sql_thread 的分发原理是依据当前事务所操作的数据库名称来进行分发,如果事务是跨数据库行为的,则需要等待已分配的该数据库的事务全部执行完毕,才会继续分发,其分配行为的伪码可以简单的描述如下:

get_slave_worker
  if (contains_partition_info(log_event))
     db_name= get_db_name(log_event);
     entry {db_name, worker_thread, usage} = map_db_to_worker(db_name);
     while (entry->usage > 0)
        wait();
    return worker;
  else if (last_assigned_worker)
    return last_assigned_worker;
  else
    push into buffer_array and deliver them until come across a event that have partition info

需要注意的细节

  • 内存的分配与释放。relay thread 每读取一个log_event, 则需要 malloc 一定的内存,在work线程执行完后,则需要free掉;
  • 数据库名 与 worker 线程的绑定信息在一个hash表中进行维护,hash表以entry为单位,entry中记录当前entry所代表的数据库名,有多少个事务相关的已被分发,执行这些事务的worker thread等信息;
  • 维护一个绑定信息的array , 在分发事务的时候,更新绑定信息,增加相应 entry->usage, 在执行完一个事务的时候,则需要减少相应的entry->usage;
  • slave worker 信息的维护,即每个 worker thread执行了哪些事务,执行到的位点是在哪,延迟是如何计算的,如果执行出错,mts_recovery_group 又是如何恢复的;
  • 分配线程是以数据库名进行分发的,当一个实例中只有一个数据库的时候,不会对性能有提高,相反,由于增加额外的操作,性能还会有一点回退;
  • 临时表的处理,临时表是和entry绑定在一起的,在执行的时候将entry的临时表挂在执行线程thd下面,但没有固化,如果在临时表操作期间,备库crash,则重启后备库会有错误;

总体上说,5.6 的并行复制打破了5.5 单线程的复制的行为,只是在单库下用处不大,并且5.6的并行复制的改动引入了一些重量级的bug

  • MySQL slave sql thread memory leak (http://bugs.MySQL.com/bug.php?id=71197)
  • Relay log without xid_log_event may case parallel replication hang (http://bugs.MySQL.com/bug.php?id=72794)
  • Transaction lost when relay_log_info_repository=FILE and crashed (http://bugs.MySQL.com/bug.php?id=73482)

(3) 5.7中,并行复制的实现添加了另外一种并行的方式,即主库在 ordered_commit中的第二阶段的时候,将同一批commit的 binlog 打上一个相同的seqno标签,同一时间戳的事务在备库是可以同时执行的,因此大大简化了并行复制的逻辑,并打破了相同 DB 不能并行执行的限制。备库在执行时,具有同一seqno的事务在备库可以并行的执行,互不干扰,也不需要绑定信息,后一批seqno的事务需要等待前一批相同seqno的事务执行完后才可以执行。

详细实现可参考: http://bazaar.launchpad.net/~MySQL/MySQL-server/5.7/revision/6256 。

reference: http://geek.rohitkalhans.com/2013/09/enhancedMTS-deepdive.html
MySQL · 谈古论今· key分区算法演变分析

本文说明一个物理升级导致的 “数据丢失”。

现象

在MySQL 5.1下新建key分表,可以正确查询数据。

drop table t1;

create table t1 (c1 int , c2 int) 
PARTITION BY KEY (c2) partitions 5; 
insert into t1  values(1,1785089517),(2,null); 
MySQL> select * from t1 where c2=1785089517;
+------+------------+
| c1   | c2         |
+------+------------+
|    1 | 1785089517 |
+------+------------+
1 row in set (0.00 sec)
MySQL> select * from t1 where c2 is null;
+------+------+
| c1   | c2   |
+------+------+
|    2 | NULL |
+------+------+
1 row in set (0.00 sec)

而直接用MySQL5.5或MySQL5.6启动上面的5.1实例,发现(1,1785089517)这行数据不能正确查询出来。

alter table t1 PARTITION BY KEY ALGORITHM = 1 (c2)  partitions 5;
MySQL> select * from t1 where c2 is null;
+------+------+
| c1   | c2   |
+------+------+
|    2 | NULL |
+------+------+
1 row in set (0.00 sec)
MySQL> select * from t1 where c2=1785089517;
Empty set (0.00 sec)

原因分析

跟踪代码发现,5.1 与5.5,5.6 key hash算法是有区别的。

5.1 对于非空值的处理算法如下

void my_hash_sort_bin(const CHARSET_INFO *cs __attribute__((unused)),
                     const uchar *key, size_t len,ulong *nr1, ulong *nr2)
{
  const uchar *pos = key; 
                         
  key+= len;
 
  for (; pos < (uchar*) key ; pos++)
  {
    nr1[0]^=(ulong) ((((uint) nr1[0] & 63)+nr2[0]) * 
             ((uint)*pos)) + (nr1[0] << 8);
    nr2[0]+=3;
  }
}

通过此算法算出数据(1,1785089517)在第3个分区

5.5和5.6非空值的处理算法如下

void my_hash_sort_simple(const CHARSET_INFO *cs,
                         const uchar *key, size_t len,
                         ulong *nr1, ulong *nr2)
{
  register uchar *sort_order=cs->sort_order;
  const uchar *end;
 
  /* 
    Remove end space. We have to do this to be able to compare
    'A ' and 'A' as identical
  */        
  end= skip_trailing_space(key, len);
 
  for (; key < (uchar*) end ; key++)
  {
    nr1[0]^=(ulong) ((((uint) nr1[0] & 63)+nr2[0]) * 
            ((uint) sort_order[(uint) *key])) + (nr1[0] << 8);
    nr2[0]+=3;
  }
}

通过此算法算出数据(1,1785089517)在第5个分区,因此,5.5,5.6查询不能查询出此行数据。

5.1,5.5,5.6对于空值的算法还是一致的,如下

if (field->is_null())
{
  nr1^= (nr1 << 1) | 1;
  continue;
}

都能正确算出数据(2, null)在第3个分区。因此,空值可以正确查询出来。

那么是什么导致非空值的hash算法走了不同路径呢?在5.1下,计算字段key hash固定字符集就是my_charset_bin,对应的hash 函数就是前面的my_hash_sort_simple。而在5.5,5.6下,计算字段key hash的字符集是随字段变化的,字段c2类型为int对应my_charset_numeric,与之对应的hash函数为my_hash_sort_simple。具体可以参考函数Field::hash

那么问题又来了,5.5后为什么算法会变化呢?原因在于官方关于字符集策略的调整,详见 WL#2649 。

兼容处理

前面讲到,由于hash 算法变化,用5.5,5.6启动5.1的实例,导致不能正确查询数据。那么5.1升级5.5,5.6就必须兼容这个问题.MySQL 5.5.31以后,提供了专门的语法 ALTER TABLE … PARTITION BY ALGORITHM=1 [LINEAR] KEY … 用于兼容此问题。对于上面的例子,用5.5或5.6启动5.1的实例后执行

MySQL> alter table t1 PARTITION BY KEY ALGORITHM = 1 (c2) partitions 5;
Query OK, 2 rows affected (0.02 sec)
Records: 2  Duplicates: 0  Warnings: 0

MySQL> select * from t1 where c2=1785089517;
+------+------------+
| c1   | c2         |
+------+------------+
|    1 | 1785089517 |
+------+------------+
1 row in set (0.00 sec)

数据可以正确查询出来了。

而实际上5.5,5.6的MySQL_upgrade升级程序已经提供了兼容方法。MySQL_upgrade 执行check table xxx for upgrade 会检查key分区表是否用了老的算法。如果使用了老的算法,会返回

MySQL> CHECK TABLE t1  FOR UPGRADE\G
*************************** 1. row ***************************
   Table: test.t1
      Op: check
Msg_type: error
Msg_text: KEY () partitioning changed, please run:
ALTER TABLE `test`.`t1` PARTITION BY KEY /*!50611 ALGORITHM = 1 */ (c2)
PARTITIONS 5
*************************** 2. row ***************************
   Table: test.t1
      Op: check
Msg_type: status
Msg_text: Operation failed
2 rows in set (0.00 sec)

检查到错误信息后会自动执行以下语句进行兼容。

ALTER TABLE `test`.`t1` PARTITION BY KEY /*!50611 ALGORITHM = 1 */ (c2) PARTITIONS 5。

MySQL · 捉虫动态· MySQL client crash一例

背景

客户使用MySQLdump导出一张表,然后使用MySQL -e ‘source test.dmp’的过程中client进程crash,爆出内存的segment fault错误,导致无法导入数据。

问题定位

test.dmp文件大概50G左右,查看了一下文件的前几行内容,发现:

A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database If you don't want to restore GTIDs pass set-gtid-purged=OFF. To make a complete dump, pass...
 -- MySQL dump 10.13  Distrib 5.6.16, for Linux (x86_64)
 --
 -- Host: 127.0.0.1    Database: carpath
 -- ------------------------------------------------------
 -- Server version       5.6.16-log
 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
 /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;

问题定位到第一行出现了不正常warning的信息,是由于客户使用MySQLdump命令的时候,重定向了stderr。即:

MySQLdump …>/test.dmp 2>&1

导致error或者warning信息都重定向到了test.dmp, 最终导致失败。

问题引申

问题虽然定位到了,但却有几个问题没有弄清楚:

问题1. 不正常的sql,执行失败,报错出来就可以了,为什么会导致crash?

MySQL.cc::add_line函数中,在读第一行的时候,读取到了don’t,发现有一个单引号,所以程序死命的去找匹配的另外一个单引号,导致不断的读取文件,分配内存,直到crash。
假设没有这个单引号,MySQL读到第六行,发现;号,就会执行sql,并正常的报错退出。

问题2. 那代码中对于大小的边界到底是多少?比如insert语句支持batch insert时,语句的长度多少,又比如遇到clob字段呢?

  • 首先clob字段的长度限制。clob家族类型的column长度受限于max_allowed_packet的大小,MySQL 5.5中,对于max_allowd_packet的大小限制在(1024, 1024*1024*1024)之间。
  • MySQLdump导出insert语句的时候,如何分割insert语句?MySQLdump时候支持insert t1 value(),(),();这样的batch insert语句。 MySQLdump其实是根据opt_net_buffer_length来进行分割,当一个insert语句超过这个大小,就强制分割到下一个insert语句中,这样更多的是在做网络层的优化。又如果遇到大的clob字段怎么办? 如果一行就超过了opt_net_buffer_length,那就强制每一行都分割。
  • MySQL client端读取dump文件的时候, 到底能分配多大的内存?MySQL.cc中定义了:#define MAX_BATCH_BUFFER_SIZE (1024L * 1024L * 1024L)。 也就是MySQL在执行语句的时候,最多只能分配1G大小的缓存。

所以,正常情况下,max_allowed_packet现在的最大字段长度和MAX_BATCH_BUFFER_SIZE限制的最大insert语句,是匹配的。

RDS问题修复原则

从问题的定位上来看,这一例crash属于客户错误使用MySQLdump导致的问题,Aliyun RDS分支对内存导致的crash问题,都会定位并反馈给用户。 但此例不做修复,而是引导用户正确的使用MySQLdump工具。

MySQL · 捉虫动态· 设置 gtid_purged 破坏AUTO_POSITION复制协议

bug描述

Oracle 最新发布的版本 5.6.22 中有这样一个关于GTID的bugfix,在主备场景下,如果我们在主库上 SET GLOBAL GTID_PURGED = “some_gtid_set”,并且 some_gtid_set 中包含了备库还没复制的事务,这个时候如果备库接上主库的话,预期结果是主库返回错误,IO线程挂掉的,但是实际上,在这种场景下主库并不报错,只是默默的把自己 binlog 中包含的gtid事务发给备库。这个bug的造成的结果是看起来复制正常,没有错误,但实际上备库已经丢事务了,主备很可能就不一致了。

背景知识

  • binlog GTID事件

binlog 中记录的和GTID相关的事件主要有2种,Previous_gtids_log_event 和 Gtid_log_event,前者表示之前的binlog中包含的gtid的集合,后者就是一个gtid,对应一个事务。一个 binlog 文件中只有一个 Previous_gtids_log_event,放在开头,有多个 Gtid_log_event,如下面所示

Previous_gtids_log_event   // 此 binlog 之前的所有binlog文件包含的gtid集合

Gtid_log_event // 单个gtid event
Transaction
Gtid_log_event
Transaction
.
.
.
Gtid_log_event
Transaction
  • 备库发送GTID集合给主库

我们知道备库的复制线程是分IO线程和SQL线程2种的,IO线程通过GTID协议或者文件位置协议拉取主库的binlog,然后记录在自己的relay log中;SQL线程通过执行realy log中的事件,把其中的操作都自己做一遍,记入本地binlog。在GTID协议下,备库向主库发送拉取请求的时候,会告知主库自己已经有的所有的GTID的集合,Retrieved_Gtid_Set + Executed_Gtid_Set,前者对应 realy log 中所有的gtid集合,表示已经拉取过的,后者对应binlog中记录有的,表示已经执行过的;主库在收到这2个总集合后,会扫描自己的binlog,找到合适的binlog然后开始发送。

  • 主库如何找到要发送给备库的第一个binlog

主库将备库发送过来的总合集记为 slave_gtid_executed,然后调用 find_first_log_not_in_gtid_set(slave_gtid_executed),这个函数的目的是从最新到最老扫描binlog文件,找到第一个含有不存在 slave_gtid_executed 这个集合的gtid的binlog。在这个扫描过程中并不需要从头到尾读binlog中所有的gtid,只需要读出 Previous_gtids_log_event ,如果Previous_gtids_log_event 不是 slave_gtid_executed的子集,就继续向前找binlog,直到找到为止。

这个查找过程总会停止的,停止条件如下:

  1. 找到了这样的binlog,其Previous_gtids_log_event 是slave_gtid_executed子集
  2. 在往前读binlog的时候,发现没有binlog文件了(如被purge了),但是还没找到满足条件的Previous_gtids_log_event,这个时候主库报错
  3. 一直往前找,发现Previous_gtids_log_event 是空集

在条件2下,报错信息是这样的

Got fatal error 1236 from master when reading data from binary log: ‘The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires.

其实上面的条件3是条件1的特殊情况,这个bugfix针对的场景就是条件3这种,但并不是所有的符合条件3的场景都会触发这个bug,下面就分析下什么情况下才会触发bug。

bug 分析

假设有这样的场景,我们要用已经有MySQL实例的备份重新做一对主备实例,不管是用 xtrabackup 这种物理备份工具或者MySQLdump这种逻辑备份工具,都会有2步操作,

  1. 导入数据
  2. SET GLOBAL GTID_PURGED =”xxxx”

步骤2是为了保证GTID的完备性,因为新实例已经导入了数据,就需要把生成这些数据的事务对应的GTID集合也设置进来。

正常的操作是主备都要做这2步的,如果我们只在主库上做了这2步,备库什么也不做,然后就直接用 GTID 协议把备库连上来,按照我们的预期这个时候是应该出错的,主备不一致,并且主库的binlog中没东西,应该报之前停止条件2报的错。但是令人大跌眼镜的是主库不报错,复制看起来是完全正常的。

为啥会这样呢,SET GLOBAL GTID_PURGED 操作会调用 MySQL_bin_log.rotate_and_purge切换到一个新的binlog,并把这个GTID_PURGED 集合记入新生成的binlog的Previous_gtids_log_event,假设原有的binlog为A,新生成的为B,主库刚启动,所以A就是主库的第一个binlog,它之前啥也没有,A的Previous_gtids_log_event就是空集,并且A中也不包含任何GTID事件,否则SET GLOBAL GTID_PURGED是做不了的。按照之前的扫描逻辑,扫到A是肯定会停下来的,并且不报错。

bug 修复

官方的修复就是在主库扫描查找binlog之前,判断一下 gtid_purged 集合不是不比slave_gtid_executed大,如果是就报错,错误信息和条件2一样 Got fatal error 1236 from master when reading data from binary log: ‘The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires。

MySQL · 捉虫动态· replicate filter 和 GTID 一起使用的问题

问题描述

当单个 MySQL 实例的数据增长到很多的时候,就会考虑通过库或者表级别的拆分,把当前实例的数据分散到多个实例上去,假设原实例为A,想把其中的5个库(db1/db2/db3/db4/db5)拆分到5个实例(B1/B2/B3/B4/B5)上去。

拆分过程一般会这样做,先把A的相应库的数据导出,然后导入到对应的B实例上,但是在这个导出导入过程中,A库的数据还是在持续更新的,所以还需在导入完后,在所有的B实例和A实例间建立复制关系,拉取缺失的数据,在业务不繁忙的时候将业务切换到各个B实例。

在复制搭建时,每个B实例只需要复制A实例上的一个库,所以只需要重放对应库的binlog即可,这个通过 replicate-do-db 来设置过滤条件。如果我们用备库上执行 show slave status\G 会看到Executed_Gtid_Set是断断续续的,间断非常多,导致这一列很长很长,看到的直接效果就是被刷屏了。

为啥会这样呢,因为设了replicate-do-db,就只会执行对应db对应的event,其它db的都不执行。主库的执行是不分db的,对各个db的操作互相间隔,记录在binlog中,所以备库做了过滤后,就出现这种断断的现象。

除了这个看着不舒服外,还会导致其它问题么?

假设我们拿B1实例的备份做了一个新实例,然后接到A上,如果主库A又定期purge了老的binlog,那么新实例的IO线程就会出错,因为需要的binlog在主库上找不到了;即使主库没有purge 老的binlog,新实例还要把主库的binlog都从头重新拉过来,然后执行的时候又都过滤掉,不如不拉取。

有没有好的办法解决这个问题呢?SQL线程在执行的时候,发现是该被过滤掉的event,在不执行的同时,记一个空事务就好了,把原事务对应的GTID位置占住,记入binlog,这样备库的Executed_Gtid_Set就是连续的了。

bug 修复

对这个问题,官方有一个相应的bugfix,参见 revno: 5860 ,有了这个patch后,备库B1的 SQL 线程在遇到和 db2-db5 相关的SQL语句时,在binlog中把对应的GTID记下,同时对应记一个空事务。

这个 patch 只是针对Query_log_event,即 statement 格式的 binlog event,那么row格式的呢? row格式原来就已经是这种行为,通过check_table_map 函数来过滤库或者表,然后生成一个空事务。

另外这个patch还专门处理了下 CREATE/DROP TEMPORARY TABLE 这2种语句,我们知道row格式下,对临时表的操作是不会记入binlog的。如果主库的binlog格式是 statement,备库用的是 row,CREATE/DROP TEMPORARY TABLE 对应的事务传到备库后,就会消失掉,Executed_Gtid_Set集合看起来是不连续的,但是主库的binlog记的gtid是连续的,这个 patch 让这种情况下的CREATE/DROP TEMPORARY TABLE在备库同样记为一个空事务。

TokuDB·特性分析· Optimize Table

来自一个TokuDB用户的“投诉”:

https://mariadb.atlassian.net/browse/MDEV-6207

现象大概是:

用户有一个MyISAM的表test_table:

 CREATE TABLE IF NOT EXISTS `test_table` (
   `id` int(10) unsigned NOT NULL,
   `pub_key` varchar(80) NOT NULL,
   PRIMARY KEY (`id`),
   KEY `pub_key` (`pub_key`)
 ) ENGINE=MyISAM DEFAULT CHARSET=latin1;

转成TokuDB引擎后表大小为92M左右:

47M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb
 45M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb

执行”OPTIMIZE TABLE test_table”:

63M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb
 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb

再次执行”OPTIMIZE TABLE test_table”:

79M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb
 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb

继续执行:

79M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb
 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb

基本稳定在这个大小。

主索引从47M–>63M–>79M,执行”OPTIMIZE TABLE”后为什么会越来越大?

这得从TokuDB的索引文件分配方式说起,当内存中的脏页需要写到磁盘时,TokuDB优先在文件末尾分配空间并写入,而不是“覆写”原块,原来的块暂时成了“碎片”。

这样问题就来了,索引文件岂不是越来越大?No, TokuDB会把这些“碎片”在checkpoint时加入到回收列表,以供后面的写操作使用,看似79M的文件其实还可以装不少数据呢!

嗯,这个现象解释通了,但还有2个问题:

  1. 在执行这个语句的时候,TokuDB到底在做什么呢? 在做toku_ft_flush_some_child,把内节点的缓冲区(message buffer)数据刷到最底层的叶节点。

  2. 在TokuDB里,OPTIMIZE TABLE有用吗? 作用非常小,不建议使用,TokuDB是一个”No Fragmentation”的引擎。

淘宝内部分享:怎么跳出 MySQL 的10个大坑,首发于博客 - 伯乐在线

22 Oct 00:37

详解设计模式六大原则

by promumu

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

借用并改编一下鲁迅老师《故乡》中的一句话,一句话概括设计模式: 希望本无所谓有,无所谓无.这正如coding的设计模式,其实coding本没有设计模式,用的人多了,也便成了设计模式

六大原则

设计模式(面向对象)有六大原则:

  • 开闭原则(Open Closed Principle,OCP)
  • 里氏代换原则(Liskov Substitution Principle,LSP)
  • 依赖倒转原则(Dependency Inversion Principle,DIP)
  • 接口隔离原则(Interface Segregation Principle,ISP)
  • 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
  • 最小知识原则(Principle of Least Knowledge,PLK,也叫迪米特法则)

开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。 设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。

 

C# 开闭原则

1.概念:

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原(是“原”,指原来的代码)代码的情况下进行扩展。

2.模拟场景:

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

3.Solution:

当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

4.注意事项:

  • 通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法
  • 参数类型、引用对象尽量使用接口或者抽象类,而不是实现类
  • 抽象层尽量保持稳定,一旦确定即不允许修改

5.开闭原则的优点:

  • 可复用性
  • 可维护性

6.开闭原则图解:

 

C# 里氏代换原则

1.概述: 派生类(子类)对象能够替换其基类(父类)对象被调用

2.概念:

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。(源自百度百科)

3.子类为什么可以替换父类的位置?:

当满足继承的时候,父类肯定存在非私有成员,子类肯定是得到了父类的这些非私有成员(假设,父类的的成员全部是私有的,那么子类没办法从父类继承任何成员,也就不存在继承的概念了)。既然子类继承了父类的这些非私有成员,那么父类对象也就可以在子类对象中调用这些非私有成员。所以,子类对象可以替换父类对象的位置。

4.C# 里氏代换原则优点:

需求变化时,只须继承,而别的东西不会改变。由于里氏代换原则才使得开放封闭成为可能。这样使得子类在父类无需修改的话就可以扩展。

5.C# 里氏代换原则Demo:

代码正文:

namespace TestApp
{
    using System;

    class Program
    {
        static void Main(string[] args)
        {
            Transportation transportation = new Transportation();
            transportation.Say();
            Transportation sedan = new Sedan();
            sedan.Say();
            Console.ReadKey();
        }
    }

    class Transportation
    {
        public Transportation()
        {
            Console.WriteLine("Transportation?");
        }

        public virtual void Say()
        {
            Console.WriteLine("121");
        }
    }

    class Sedan:Transportation
    {
        public Sedan()
        {
            Console.WriteLine("Transportation:Sedan");
        }

        public override void Say()
        {
            Console.WriteLine("Sedan");
        }
    }

    class Bicycles : Transportation
    {
        public Bicycles()
        {
            Console.WriteLine("Transportation:Bicycles");
        }

        public override void Say()
        {
            Console.WriteLine("Bicycles");
        }
    }
}

 

代码效果:

6.里氏代换原则图解:

 

C# 依赖倒转原则

1.概念:

依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

2.C# 依赖倒转原则用处:

有些时候为了代码复用,一般会把常用的代码写成函数或类库。这样开发新项目时,直接用就行了。比如做项目时大多要访问数据库,所以我们就把访问数据库的代码写成了函数。每次做项目去调用这些函数。那么我们的问题来了。我们要做新项目时,发现业务逻辑的高层模块都是一样的,但客户却希望使用不同的数据库或存储住处方式,这时就出现麻烦了。我们希望能再次利用这些高层模块,但高层模块都是与低层的访问数据库绑定在一起,没办法复用这些高层模块。所以不管是高层模块和低层模块都应该依赖于抽象,具体一点就是接口或抽象类,只要接口是稳定的,那么任何一个更改都不用担心了。

3.注意事项:

  • 高层模块不应该依赖低层模块。两个都应该依赖抽象。
  • 抽象不应该依赖结节。细节应该依赖抽象。

4.模拟场景:

场景:

假设现在需要一个Monitor工具,去运行一些已有的APP,自动化来完成我们的工作。Monitor工具需要启动这些已有的APP,并且写下Log。

代码实现1:

namespace TestLibrary.ExtensionsClass
{
    using System;

    public class AppOne
    {
        public bool Start()
        {
            Console.WriteLine("1号APP开始启动");
            return true;
        }

        public bool ExportLog()
        {
            Console.WriteLine("1号APP输出日志");
            return true;
        }
    }

    public class AppTwo
    {
        public bool Start()
        {
            Console.WriteLine("2号APP开始启动");
            return true;
        }

        public bool ExportLog()
        {
            Console.WriteLine("2号APP输出日志");
            return true;
        }
    }

    public class Monitor
    {
        public enum AppNumber
        {
            AppOne=1,
            AppTwo=2
        }

        private AppOne appOne = new AppOne();
        private AppTwo appTwo = new AppTwo();
        private AppNumber number;
        public Monitor(AppNumber number)
        {
            this.number = number;
        }

        public bool StartApp()
        {
            return number == AppNumber.AppOne ? appOne.Start() : appTwo.Start();
        }

        public bool ExportAppLog()
        {
            return number == AppNumber.AppOne ? appOne.ExportLog() : appTwo.ExportLog();
        }
    }
}

代码解析1:

在代码实现1中我们已经轻松实现了Monitor去运行已有APP并且写下LOG的需求。并且代码已经上线了.

春…夏…秋…冬…

春…夏…秋…冬…

春…夏…秋…冬…

就这样,三年过去了。

一天客户找上门了,公司业务扩展了,现在需要新加3个APP用Monitor自动化。这样我们就必须得改Monitor。

代码实现2:

namespace TestLibrary.ExtensionsClass
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    public class Monitor
    {
        public enum AppNumber
        {
            AppOne = 1,
            AppTwo = 2,
            AppThree = 3,
            AppFour = 4,
            AppFive = 5
        }

        private AppOne appOne = new AppOne();
        private AppTwo appTwo = new AppTwo();
        private AppThree appThree = new AppThree();
        private AppFour appFour = new AppFour();
        private AppFive appFive = new AppFive();
        private AppNumber number;
        public Monitor(AppNumber number)
        {
            this.number = number;
        }

        public bool StartApp()
        {
            bool result = false;
            if (number == AppNumber.AppOne)
            {
                result = appOne.Start();
            }
            else if (number == AppNumber.AppTwo)
            {
                result = appTwo.Start();
            }
            else if (number == AppNumber.AppThree)
            {
                result = appThree.Start();
            }
            else if (number == AppNumber.AppFour)
            {
                result = appFour.Start();
            }
            else if (number == AppNumber.AppFive)
            {
                result = appFive.Start();
            }

            return result;
        }

        public bool ExportAppLog()
        {
            bool result = false;
            if (number == AppNumber.AppOne)
            {
                result = appOne.ExportLog();
            }
            else if (number == AppNumber.AppTwo)
            {
                result = appTwo.ExportLog();
            }
            else if (number == AppNumber.AppThree)
            {
                result = appThree.ExportLog();
            }
            else if (number == AppNumber.AppFour)
            {
                result = appFour.ExportLog();
            }
            else if (number == AppNumber.AppFive)
            {
                result = appFive.ExportLog();
            }

            return result;
        }
    }
}

代码解析2:

这样会给系统添加新的相互依赖。并且随着时间和需求的推移,会有更多的APP需要用Monitor来监测,这个Monitor工具也会被越来越对的if…else撑爆炸,而且代码随着APP越多,越难维护。最终会导致Monitor走向灭亡(下线)。

介于这种情况,可以用Monitor这个模块来生成其它的程序,使得系统能够用在需要的APP上。OOD给我们提供了一种机制来实现这种“依赖倒置”。

代码实现3:

namespace TestLibrary.ExtensionsClass
{
    using System;

    public interface IApp
    {
        bool Start();
        bool ExportLog();
    }

    public class AppOne : IApp
    {
        public bool Start()
        {
            Console.WriteLine("1号APP开始启动");
            return true;
        }

        public bool ExportLog()
        {
            Console.WriteLine("1号APP输出日志");
            return true;
        }
    }

    public class AppTwo : IApp
    {
        public bool Start()
        {
            Console.WriteLine("2号APP开始启动");
            return true;
        }

        public bool ExportLog()
        {
            Console.WriteLine("2号APP输出日志");
            return true;
        }
    }

    public class Monitor
    {
        private IApp iapp;
        public Monitor(IApp iapp)
        {
            this.iapp = iapp;
        }

        public bool StartApp()
        {
            return iapp.Start();
        }

        public bool ExportAppLog()
        {
            return iapp.ExportLog();
        }
    }
}

代码解析3:

现在Monitor依赖于IApp这个接口,而与具体实现的APP类没有关系,所以无论再怎么添加APP都不会影响到Monitor本身,只需要去添加一个实现IApp接口的APP类就可以了。

 

C# 接口隔离原则

1.概念:

客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上

2.含义:

接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则。

3.模拟场景:

一个OA系统,外部只负责提交和撤回工作流,内部负责审核和驳回工作流。

4.代码演示:

namespace TestLibrary.ExtensionsClass
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    public interface IReview
    {
        void ReviewWorkFlow();

        void RejectWorkFlow();
    }

    public class Review : IReview
    {
        public void ReviewWorkFlow()
        {
            Console.WriteLine("开始审核工作流");
        }

        public void RejectWorkFlow()
        {
            Console.WriteLine("已经驳回工作流");
        }
    }

    public interface ISubmit
    {
        void SubmitWorkFlow();

        void CancelWorkFlow();
    }

    public class Submit : ISubmit
    {
        public void SubmitWorkFlow()
        {
            Console.WriteLine("开始提交工作流");
        }

        public void CancelWorkFlow()
        {
            Console.WriteLine("已经撤销工作流");
        }
    }
}

5.代码解析:

其实接口隔离原则很好理解,在上面的例子里可以看出来,如果把OA的外部和内部都定义一个接口的话,那这个接口会很大,而且实现接口的类也会变得臃肿。

 

C# 合成/聚合复用原则

1.概念:

合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)经常又叫做合成复用原则。合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。

2.合成/聚合解析:

聚合概念:

聚合用来表示“拥有”关系或者整体与部分的关系。代表部分的对象有可能会被多个代表整体的对象所共享,而且不一定会随着某个代表整体的对象被销毁或破坏而被销毁或破坏,部分的生命周期可以超越整体。例如,Iphone5和IOS,当Iphone5删除后,IOS还能存在,IOS可以被Iphone6引用。

聚合关系UML类图:

C# 合成/聚合复用原则

代码演示:

namespace TestLibrary.ExtensionsClass
{
    class IOS
    { 
    }

    class Iphone5
    {
        private IOS ios;
        public Iphone5(IOS ios)
        {
            this.ios = ios;
        }
    }
}

合成概念:

合成用来表示一种强得多的“拥有”关系。在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。使用程序语言的术语来说,合成而成的新对象对组成部分的内存分配、内存释放有绝对的责任。一个合成关系中的成分对象是不能与另一个合成关系共享的。一个成分对象在同一个时间内只能属于一个合成关系。如果一个合成关系湮灭了,那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍)要么就得将这一责任交给别人(较为罕见)。例如:水和鱼的关系,当水没了,鱼也不可能独立存在。

合成关系UML类图:

代码演示:

namespace TestLibrary.ExtensionsClass
{
    using System;

    class Fish
    {
        public Fish CreateFish()
        {
            Console.WriteLine("一条小鱼儿");
            return new Fish();
        }
    }

    class Water
    {
        private Fish fish;
        public Water()
        {
            fish = new Fish();
        }

        public void CreateWater()
        {
            // 当创建了一个水的地方,那这个地方也得放点鱼进去
            fish.CreateFish();
        }
    }
}

3.模拟场景:

比如说我们先摇到号(这个比较困难)了,需要为自己买一辆车,如果4S店里的车默认的配置都是一样的。那么我们只要买车就会有这些配置,这时使用了继承关系:

不可能所有汽车的配置都是一样的,所以就有SUV和小轿车两种(只列举两种比较热门的车型),并且使用机动车对它们进行聚合使用。这时采用了合成/聚合的原则:

 

C# 迪米特法则

1.概念:

一个软件实体应当尽可能少的与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类来转达。因此,应用迪米特法则有可能造成的一个后果就是:系统中存在大量的中介类,这些类之所以存在完全是为了传递类之间的相互调用关系——这在一定程度上增加了系统的复杂度。

2.模拟场景:

场景:公司财务总监发出指令,让财务部门的人去统计公司已发公司的人数。

一个常态的编程:(肯定是不符LoD的反例)

UML类图:

代码演示:

namespace TestLibrary.ExtensionsClass
{
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// 财务总监
    /// </summary>
    public class CFO
    {
        /// <summary>
        /// 财务总监发出指令,让财务部门统计已发工资人数
        /// </summary>
        public void Directive(Finance finance)
        {
            List<Employee> employeeList = new List<Employee>();
            // 初始化已发工资人数
            for (int i = 0; i < 500; i++)
            {
                employeeList.Add(new Employee());
            }

            // 转告财务部门开始统计已结算公司的员工
            finance.SettlementSalary(employeeList);
        }
    }

    /// <summary>
    /// 财务部
    /// </summary>
    public class Finance
    {
        /// <summary>
        /// 统计已结算公司的员工
        /// </summary>
        public void SettlementSalary(List<Employee> employeeList) 
        {
            Console.WriteLine(string.Format("已结算工资人数:{0}", employeeList.Count));
        }
    }

    /// <summary>
    /// 员工
    /// </summary>
    public class Employee
    {

    }

    /// <summary>
    /// 主程序
    /// </summary>
    public class Runner
    {
        public static void main(String[] args)
        {
            CFO cfo = new CFO();
            // 财务总监发出指令
            cfo.Directive(new Finance());
        }
    }
}

根据模拟的场景:财务总监让财务部门总结已发工资的人数。 财务总监和员工是陌生关系(即总监不需要对员工执行任何操作)。根据上述UML图和代码解决办法显然可以看出,上述做法违背了LoD法则。

依据LoD法则解耦:(符合LoD的例子)

UML类图:

代码演示:

namespace TestLibrary.ExtensionsClass
{
    using System;
    using System.Collections.Generic;

    /// <summary>
    /// 财务总监
    /// </summary>
    public class CFO
    {
        /// <summary>
        /// 财务总监发出指令,让财务部门统计已发工资人数
        /// </summary>
        public void Directive(Finance finance)
        {
            // 通知财务部门开始统计已结算公司的员工
            finance.SettlementSalary();
        }
    }

    /// <summary>
    /// 财务部
    /// </summary>
    public class Finance
    {
        private List<Employee> employeeList;  

        //传递公司已工资的人
        public Finance(List<Employee> _employeeList)
        {
            this.employeeList = _employeeList;  
    }  

        /// <summary>
        /// 统计已结算公司的员工
        /// </summary>
        public void SettlementSalary() 
        {
            Console.WriteLine(string.Format("已结算工资人数:{0}", employeeList.Count));
        }
    }

    /// <summary>
    /// 员工
    /// </summary>
    public class Employee
    {

    }

    /// <summary>
    /// 主程序
    /// </summary>
    public class Runner
    {
        public static void main(String[] args)
        {
            List<Employee> employeeList = new List<Employee>();

            // 初始化已发工资人数
            for (int i = 0; i < 500; i++)
            {
                employeeList.Add(new Employee());
            }

            CFO cfo = new CFO();

            // 财务总监发出指令
            cfo.Directive(new Finance(employeeList));
        }
    }
}

根据LoD原则我们需要让财务总监和员工之间没有之间的联系。这样才是遵守了迪米特法则。

 

博客总结

想搞懂设计模式,必须先知道设计模式遵循的六大原则,无论是哪种设计模式都会遵循一种或者多种原则。这是面向对象不变的法则。本文针对的是设计模式(面向对象)主要的六大原则展开的讲解,并尽量做到结合实例和UML类图,帮助大家理解。在后续的博文中还会跟进一些设计模式的实例。

详解设计模式六大原则,首发于博客 - 伯乐在线

13 Oct 11:21

如何配置一个高效的 Mac 工作环境

by 伯乐

原标题:强迫症的 Mac 设置指南

一直想写这么一篇文章,把我从同事那里学到的经验分享出来。市面上有很多类似的文章,写得都非常好,让我受益匪浅。不过我还是有一些自己总结出来的经验想要分享。

在工作中,我一般会在 1 到 10 人的团队中,经常会结对编程,即两个人共用一台 Mac 工作,因此也经常会把 Mac 外接一个大显示器、鼠标和键盘。我的常用开发平台有 Java、Ruby、Node.js、Web 等,使用 JetBrains 的开发工具,比如 IntelliJ IDEA、RubyMine、WebStorm 等。

(伯乐在线转载配图)

我认为“一个高效的 Mac 工作环境”有以下几个特点:

  • 自动化
    举个例子。手动安装一个应用,需要1)打开浏览器,2)搜索应用的名字,3)打开应用网站,4)寻找下载链接和安装方法,5)下载并等待下载完成,6)安装下载文件,7)可能还有后续的安装步骤。而自动化安装一个应用,只需要1)打开终端工具,2)敲入安装命令,3)等待完成这几个步骤。自动化可以大大简化操作,提高效率。
  • 统一
    我经常结对编程,偶尔会遇到快捷键不一样,命令不同等问题。我强烈建议,至少在一个团队中,大家尽量使用相同的快捷键、命令等环境。(我记得有个实践就是这个,可是我一直没找到该实践的名字和出处,求告诉)
  • 够用
    够用就好,如果系统本身已经满足了我的需求,我不会再使用第三方工具。
  • 效率
    效率,一切都是为了效率。

本文对于第三方应用如何安装和使用只有最简单的介绍,具体还请参考官方网站和相关文档。

有些章节标题标注了[OCD],意思是这些章节带有我强烈的个人色彩,如果你跟我臭味相投,欢迎借鉴,如果你并不认同,请忽略掉好了。

1. OS X

本节介绍操作系统本身的一些设置。

功能键

默认情况下,F1-F12 都是特殊功能,比如调节屏幕亮度。而当你需要键入 F1-F12 时(比如在使用 IntelliJ IDEA 的快捷键时),需要同时按住 Fn。这对于开发人员来说是非常不方便的。

把 F1-F12 改成标准功能键:选择System Preferences > Keyboard,在Keyboard标签页中选中Use all F1, F2, etc. keys as standard function keys

全键盘控制

当你在 Sublime Text 里关闭文件时,可能会遇到这样的对话框:

dialog-box-without-all-controls

注意这个Save按钮跟其他两个按钮不太一样,它的底色是蓝的。这种按钮被称为默认按钮,除了用鼠标点击触发外,还可以通过回车键触发。

那么问题来了,如果你不想保存,想点击Don't Save,是不是只能用鼠标点击了呢?

并不是这样:选择System Preferences > Keyboard,在Shortcuts标签页中选择All controls;或者使用快捷键⌃F7。之后这个对话框会变成这样:

dialog-box-with-all-controls

这个Don't Save按钮有了一圈蓝边,这个意味着你可以通过空格键触发。不仅如此,你还可以用Tab键把蓝边转移到其他按钮,来实现全键盘控制。

除了All controls这个方法,你还可以用⌘⌫在包含“删除”或“不存储”按钮的对话框中选择“删除”或“不存储”。

在这个对话框上,你可以用Esc来执行Cancel操作。

Spotlight 快捷键

中文版 OS X 的 Spotlight 的快捷键是⌃Space。这个快捷键有一些问题:

  • JetBrains 的 IDE,比如 IntelliJ IDEA、WebStorm 等都使用⌃Space作为自动完成这个最常用功能的快捷键。我不建议更改 IDE 的快捷键,而建议更改 Spotlight 的快捷键。
  • 对于没有添加中文输入法的 Mac 来说,Spotlight 的快捷键是⌘Space。英语国家的人都是这样的。所以我建议把 Spotlight 的快捷键设置为⌘Space,跟他们一致。

输入法快捷键

一般来说切换输入法的快捷键是⌘Space。由于我建议把 Spotlight 的快捷键设置为⌘Space,所以我建议把切换输入法的快捷键设置为⌥Space

其他快捷键

让双手尽量多的键盘和快捷键,少使用鼠标和触摸板,可以大大提高效率。

设置 Trackpad 轻点来点按

默认情况下按下触摸板才是点按(click)。我喜欢设置成用轻点作为点按:

选择System Preferences > Trackpad,在Point & Click标签页中选中Tap to click

语音

OS X 自带了语音功能,可以用say命令让 Mac 开口说话:

say hello

可以和&&或者;配合使用来提示你某任务已经完成:

brew update && brew upgrade && brew cleanup ; say mission complete

通过命令行来听取发音还是有点麻烦。其实我们几乎可以在任何地方选中单词,然后使用快捷键⌥+ESC发音。仅仅需要这样设置一下:选择System Preferences > Dictation & Speech,在Text to Speech标签页中选中Speak selected text when the key is pressed

词典

OS X 自带了词典(Dictionary)。你几乎可以在任何应用中通过三指轻拍触摸板来现实对应单词的释义。

也可以打开 Dictionary 应用来查找单词。

可以在 Dictionary 应用中添加英汉汉英词典。

Dock Position

默认 Dock 在屏幕下方。我们的屏幕一般都是 16:10,Dock 在屏幕下方的话会占据本来就不大的垂直空间。建议把 Dock 放到左边或者右边。

Remove all Dock icons[OCD]

本条目对于强迫症适用。

默认情况下 Dock 被一堆系统自带的应用占据着,而其中大部分我都很少使用,当我打开几个常用应用后,Dock 上会有很多图标,每个图标都会被挤得很小。所以我会把所有 Dock 上固定的图标都删掉,这样一来 Dock 上只有我打开的应用。

PS:Finder 图标是删不掉的。

重置 Launchpad 上图标位置[OCD]

本条目对于强迫症适用。

新的应用被安装后,经常会跑到 Launchpad 的第一屏,所以它们的位置跟安装的顺序有关系,而我更希望它们可以按照某种更加稳定的顺序排列,比如按照系统默认的顺序:

defaults write com.apple.dock ResetLaunchPad -bool true; killall Dock

在默认顺序中,Launchpad 第一屏只有 Apple 自家应用。

2. 常用工具

本节介绍一些常用的,跟开发没有直接关系的第三方应用及其设置。

Homebrew

包管理工具,官方称之为The missing package manager for OS X

安装步骤:先打开 Terminal 应用,输入:

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

有了 brew 以后,要下载工具,比如 MySQL、Gradle、Maven、Node.js 等工具,就不需要去网上下载了,只要一行命令就能搞定:

brew install mysql gradle maven node

PS:安装 brew 的时候会自动下载和安装 Apple 的 Command Line Tools。

brew 的替代品有 MacPorts,现在基本没人用它。

Homebrew Cask

brew-cask 允许你使用命令行安装 OS X 应用。比如你可以这样安装 Chrome:brew cask install google-chrome。还有 Evernote、Skype、Sublime Text、VirtualBox 等都可以用 brew-cask 安装。

brew-cask 是社区驱动的,如果你发现 brew-cask 上的应用不是最新版本,或者缺少你某个应用,你可以自己提交 pull request。

安装:

brew install caskroom/cask/brew-cask

应用也可以通过 App Store 安装,而且有些应用只能通过 App Store 安装,比如 Xcode 等一些 Apple 的应用。App Store 没有对应的命令行工具,还需要 Apple ID。倒是更新起来很方便。

几乎所有常用的应用都可以通过 brew-cask 安装,而且是从应用的官网上下载,所以你要安装新的应用时,建议用 brew-cask 安装。如果你不知道应用在 brew-cask 中的 ID,可以先用brew cask search命令搜索。

iTerm2

iTerm2 是最常用的终端应用,是 Terminal 应用的替代品。提供了诸如Split Panes一群实用特性。它默认的黑色背景让我毫不犹豫的抛弃了 Terminal。

安装:

brew cask install iterm2

感谢 brew-cask,我们可以通过命令行自动安装 iTerm2 了。

在终端里,除了可以用⌃E等快捷键(详见其他快捷键)之外,还可以使用⌥B⌥F等快捷键(具体可以参考这里)。前提是这样设置一下:

选择Iterm菜单 > Preferences > Profiles,选择你在使用的 Profile(默认是Default),在Keys标签页中把Left option (⌥) key acts asRight option (⌥) key acts as都设置成+ESC

在打开新的窗口/标签页的时候,默认情况下新窗口总是 HOME 目录,还需要我每次敲命令才能进入工作目录。如果想要这个新窗口在打开的时候就自动进入工作目录,需要如下设置:

选择Iterm菜单 > Preferences > Profiles,选择你在使用的 Profile(默认是Default),在General标签页中的Working Directory部分中选择Reuse previous seesion's directory

至此,Terminal 应用已经出色的完成了其历史使命。后面就交给 iTerm2 啦。

Oh My Zsh

默认的 Bash 是黑白的,没有色彩。而 Oh My Zsh 可以带你进入彩色时代。Oh My Zsh 同时提供一套插件和工具,可以简化命令行操作。后面我们会看到很多介绍,你会看到我爱死这家伙了。

安装:

sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

目前我使用的插件有:git z sublime history rbenv bundler rake

Oh My Zsh 使用了 Z shell(zsh),一个和 Bash 相似的 Shell,而非 Bash。

在 Z shell 中,~/.zshrc是最重要的配置文件。Oh My Zsh 在安装的时候会把当前环境的$PATH写入~/.zshrc中。这并不是我期望的行为,因为使用了 brew,我们基本不再需要去定制$PATH,而 Oh My Zsh 提供的默认$PATH$HOME/bin:/usr/local/bin:$PATH是非常合适的一个值,它把$HOME/bin加入了$PATH,可以让我们把自己用的脚本放到$HOME/bin下。

所以建议把~/.zshrc重置:

cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

Oh My Zsh 还有很多有价值的插件

替代品有 Oh My Fish,使用了 Fishshell 作为基础。

Git 常用别名

几乎每个人都会使用一些方法比如 Git 别名来提高效率,几乎所有人都会把使用git st来代替git status。然而这需要手动设置,每个人也都不完全一样。

Oh My Zsh 提供了一套系统别名(alias),来达到相同的功能。比如gst作为git status的别名。而且 Git 插件是 Oh My Zsh 默认启用的,相当于你使用了 Oh My Zsh,你就拥有了一套高效率的别名,而且还是全球通用的。是不是棒棒哒?下面是一些我常用的别名:

Alias Command
gapa git add --patch
gc! git commit -v --amend
gcl git clone --recursive
gclean git reset --hard && git clean -dfx
gcm git checkout master
gcmsg git commit -m
gco git checkout
gd git diff
gdca git diff --cached
glola git log --graph --pretty = format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --all
gp git push
grbc git rebase --continue
gst git status
gup git pull --rebase
gwip git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit -m "--wip--"

完整列表请参考:https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git

Scroll Reverser

当你在浏览一个很长的网页时,你看完了当前显示的内容,想要看后续的内容,你可以在 Trackpad 上双指上滑,或者鼠标滚轮向上滚动。这是被称作“自然”的滚动方向。

然而在 Windows 里鼠标滚动的行为是相反的:鼠标滚轮向下滚动才会让浏览器显示后续的内容,向上滚动会达到页面的顶部。你可以在 OS X 的系统偏好设置里修改(选择System Preferences >Trackpad,在Scroll & Zoom标签页中不选中Scroll direction: natural),但是这样会同时改变鼠标滚轮的方向和 Trackpad 的方向。

要想只改变鼠标滚轮的方向,而保持 Trackpad 依旧是“自然”的,我们需要 Scroll Reverser:

brew cask install scroll-reverser

PS:这货会让三指点击失效

ShiftIt

原生 OS X 下只能手动调整窗口大小,所以我们需要窗口管理工具。我用过很多窗口管理工具,可惜大部分工具都存在快捷键冲突的问题(对我来说主要是 IntelliJ IDEA)。ShiftIt 是少见的没有冲突的窗口管理工具:

brew cask install shiftit

PS:ShiftIt的旧版本需要安装 X11,最新版本已经修正了这个问题。

替代者有 SizeUp,主要快捷键和 ShiftIt 相同。

当然如果喜欢 hacking,Slate 是个不错的 hackable 的窗口管理工具。配置可以参照http://thume.ca/howto/2012/11/19/using-slate/

Sublime Text 2

安装:

brew cask install sublime-text

在命令行中指定使用 Sublime Text 打开某文件,是一个非常常用的功能,一般我们会按照 OS X Command Line 中所说执行 ln -s "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl" ~/bin/subl 来增加subl链接。但是如果你用 brew-cask 安装的话,恭喜你,你不需要运行这个命令,因为 brew-cask 自动帮你做了这件事情。而且你卸载 Sublime Text 的时候 brew-cask 会自动删掉这个链接。

同时 Oh My Zsh 也提供了 Sublime Text 插件,叫做sublime。参考:https://github.com/robbyrussell/oh-my-zsh/tree/master/plugins/sublime,这个插件和通过 brew-cask 安装的 Sublime Text 完美兼容。

替代品有 TextMate,Sublime Text 3 等。

MacDown

MacDown 是 Markdown 编辑器。由于 Mou 一直不支持代码高亮,我就转向了 MacDown。完美支持GFM

我特别喜欢 Markdown,我用 Makdown 来写文章(包括本文),写幻灯片(reveal.js)。Markdown 可以让我专注于内容本身,而无需花精力在排版和样式上。

安装:

brew cask install macdown

z

在打开终端后,你是怎么进入项目的工作目录?是cd xxx⌃R还是用别名?

z 工具可以帮你快速进入目录。比如在我的 Mac 上运行z cask就会进入/usr/local/Library/Taps/caskroom/homebrew-cask/Casks目录。

这货的安装非常方便,甚至都不需要下载任何东西,因为它已经整合在了 Oh My Zsh 中。编辑~/.zshrc文件,在plugins=(git)这行中加上z变成plugins=(git z),然后运行source ~/.zshrc重新加载配置文件,就可以使用 z 了。

替代品有 autojump。autojump 需要使用 brew 安装。

Vimium

Vimium 是一个 Google Chrome 扩展,让你可以纯键盘操作 Chrome,把你的 Chrome 变成“黑客的浏览器”。

安装方法请参考官方网站。

其他浏览器也有类似的工具,比如 FireFox 的 KeySnail

LastPass

LastPass 是管理密码的工具,支持二次验证,提供所有浏览器插件以及 Mac 桌面版本。

最重要的是,它提供 命令行 的版本,可以直接通过 brew 安装

brew install lastpass-cli --with-pinentry

之后,只需要登陆:

lpass login you@email.com

就可以拷贝密码或者集成到其他命令中了:

lpass show --password gmail.com -c

SourceTree

SourceTree 是 Atlassian 公司出品的一款优秀的 Git 图形化客户端。如果你发现命令行无法满足你的要求,可以试试 SourceTree。

安装:

brew cask install sourcetree

用 brew-cask 安装会自动增加命令行工具stree$PATH里。在命令行中输入stree可以快速用 SourceTree 打开当前 Git 仓库。详细用法请参见stree --help

3. 开发工具

Java

现在 OS X 都不会自带 JDK 了,所以进行 Java 开发的话,需要下载 JDK。在 brew-cask 之前,我们需要从 https://developer.apple.com/downloads/ 或者 Oracle 网站上下载。还有更麻烦的--卸载 JDK 和升级 JDK。

JDK 安装文件是 pkg 格式,卸载和.app不一样,且没有自动卸载方式。

而 brew-cask 提供了自动安装和卸载功能,能够自动从官网上下载并安装 JDK 8。

brew cask install java

如果你需要安装 JDK 7 或者 JDK 6,可以使用homebrew-cask-versions

brew tap caskroom/versions
brew cask install java6

在 OS X 上,你可以同时安装多个版本的 JDK。你可以通过命令/usr/libexec/java_home -V来查看安装了哪几个 JDK。

那问题来了,当你运行java或者 Java 程序时使用的是哪个 JDK 呢?在 OS X 下,java也就是/usr/bin/java在默认情况下指向的是已经安装的最新版本。但是你可以设置环境变量JAVA_HOME来更改其指向:

$ java -version
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
$ JAVA_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home java -version
java version "1.6.0_65"
Java(TM) SE Runtime Environment (build 1.6.0_65-b14-466.1-11M4716)
Java HotSpot(TM) 64-Bit Server VM (build 20.65-b04-466.1, mixed mode)

其中JAVA_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home可以用JAVA_HOME=`/usr/libexec/java_home -v 1.6`这种更加通用的方式代替。

jEnv

也可以使用 jEnv 来管理不同版本的 JDK,这个工具跟 rbenv 类似,通过当前目录下的.java-version来决定使用哪个 JDK。jEnv 也可以用 brew 安装。不过要使用 jEnv 要有几个问题:

  • 需要手动把eval "$(jenv init -)"加入 profile,没有 Oh My Zsh 插件。这点是我非常反感的。可以把eval "$(jenv init -)"加入~/.zlogin,这样可以避免修改~/.zshrc
  • 需要手动添加 JDK,不会自动采集系统 JDK。跟 Ruby 不同,OS X 已经提供/usr/libexec/java_home工具来管理安装的 JDK。
  • 需要 jenv rehash。这个是跟 rbenv 学的。

所以我建议不要使用 jEnv。

Java[OCD]

作为一个强迫症患者,每当我看到 Java 的错误写法就想纠正过来。

当指编程语言时,Java 的正确写法是首字母大写,其余小写。其他写法比如JAVAjava都是不对的。

在其他一些地方会使用小写的java

  • java命令
  • 原文件Main.java
  • 包名java.lang

只有在全大写的标题里使用JAVA或者环境变量JAVA_HOME

IntelliJ IDEA

Java 开发必备工具 IntelliJ IDEA。可以安装 Ultimate Edition:

brew cask install intellij-idea

也可以安装开源免费的 Community Edition:

brew cask install intellij-idea-ce

IntelliJ IDEA 有几套内建的快捷键方案(Keymap)。其中适用于 OS X 的有Mac OS XMac OS X 10.5+两种。区别是:

  • Mac OS X方案和其他平台上的快捷键类似,
  • Mac OS X 10.5+更加符合 OS X 常用的快捷键。

一个团队使用不同的快捷键会严重影响效率。可以用View | Quick Switch Scheme⌃ Back Quote)快速切换 Keymap。

如果可以选择的话,我建议使用Mac OS X方案。因为我经常遇到使用 Windows 的客户,而 Windows 平台上的快捷键和Mac OS X方案类似。

rbenv

人人都需要一个 Ruby 版本管理工具。rbenv 就是这样一个轻量级工具,它可以通过 brew 安装。

安装:

brew install rbenv ruby-build

然后在~/.zshrc中加上rbenv插件。否则你需要手动添加eval "$(rbenv init -)"~/zshrc或者~/.zprofile文件里。

有时候项目会依赖一些奇怪的版本号,比如ruby-2.1.0,这个时候你需要 rbenv-aliases 帮忙:

brew install rbenv-aliases

替代品有 RVM、chruby。因为 RVM 不能通过 brew 安装,并且安装的时候会没有节操的修改一堆文件,所以被我早早的弃用了。chruby 也是一个轻量级工具,而且可以完美的和 Oh My Zsh 集成在一起,我看到有些生产环境在用它。

Ruby 常用别名

几乎所有 Ruby 开发人员都会把bi作为bundle install的别名。Oh My Zsh 提供builder插件,这个插件提供了一套别名,比如bibe。同时还能让你在运行一些常用 gem 的时候直接输入rspec,不需要be rspec这样了。具体包括哪些命令请参考这里

Z shell 对于[]符号有特殊的处理,所以在运行rake task[parameter]的时候会报错,你需要改成 noglob rake task[parameter]。然而 Oh My Zsh 已经看穿这一切,自带的 rake 插件已经解决了这个问题:brake task[parameter]

添加插件的时候注意把rake放到bundler后面,例如这样:

plugins=(git z sublime history rbenv bundler rake)

参考资料

如何配置一个高效的 Mac 工作环境,首发于博客 - 伯乐在线

12 Oct 15:23

【大公司晚报】阿里投资“58 到家”;百度看上“中粮我买”;EMC 670 亿美元卖身戴尔

by 饭遥

Image title

58 到家获阿里、KKR 和平安联合参与 3 亿美元 A 轮融资

根据 58 到家发布的信息,公司目前已经签署有约束力的 A 轮融资协议,资方包含阿里巴巴集团、KKR 和平安创投。据称,融资完成后,58 同城保留 58 到家多数股权,前者估值超过 10 亿美元。58 到家将把资金投入到研发和市场推广中,同时在上门服务行业寻求投资机会。

戴尔将 670 亿美元收购 EMC

戴尔正式宣布将联合银湖资本(Silver Lake)以 670 亿美元价格收购 EMC。根据纽约时报的报道,正式公告将于当地时间周一发出,每股收购价为 33.15 美元。消息发出后,EMC 盘前价格上涨 8%。

百度、泰康人寿等等将向中粮我买网投资 2 亿以上美元

根据腾讯财经的消息,这是中粮我买网获得第三融资,也是百度第一次涉足生鲜电商。我买网曾于 2014 年获得 IDG 领投、赛富基金跟投的 B 轮 1 亿美元投资,C 轮具体协议则尚未公布。据介绍,此前投资者 IDG 和赛富不会在本轮退出,但不确定中粮集团是否维持股东地位,以及各股东持股比例变化。本轮资金将用于自建冷链物流和配送服务。目前为止,我买网在北上广设有视频仓储中心,配送服务覆盖 142 个城市,声称最迟 48 小时送达。此外,我买网计划在 2016 至 2017 年间上市。

网商银行推出“口碑贷”,线下小微商户最高获100万无抵押信用贷款

口碑贷将优先覆盖口碑平台上的小微餐饮商户,根据商户的信用资质等情况来决定授信额度。每户的授信额度最高可以达到100万元,最长用款期限为12个月。口碑商户可登陆支付宝PC版的“商家中心”或者的口碑商户版APP在线申请贷款,系统自动审批通过之后,商户便可立即拿到贷款。

神州专车2.1亿美元收购神州租车约5.28%股份

神州租车今日向港交所提交公告披露,该公司主要股东Grand Union Investment Fund, L.P与优车科技有限公司签立购股协议,向优车转让1.254亿神州租车股票。根据公告,本次转让股份的价格为每股股份13.50港元,较神州租车前30 天的平均每日收市价溢价约8%,比前六个月的平均每日收市价折让约15%。

盛大游戏举报德力股份收购9377涉嫌侵权

盛大游戏表示已向证监会、安徽省证监局及深交所等提交了举报材料,举报德力股份发布的有关公告存在虚假记载、误导性陈述、重大遗漏。主要原因是 9377 称其自主研发的《烈焰》、《雷霆之怒》以及《赤月传说》侵犯盛大《热血传奇》著作权。

途牛宣布陈福炜升任公司CMO

途牛旅游网正式宣布,公司副总裁陈福炜先生升任首席市场官(CMO),全面负责途牛市场营销、品牌推广、公共关系等工作,并继续分管无线中心、机票事业部及酒店BU。陈福炜先后领导途牛网技术研发、网站无线、市场营销等团队,此前担任公司副总裁。

12 Oct 13:59

知名公司的Java面试题

by liken

查看不同公司新鲜真实的Java面试题,摘自Glassdoor.com

巴克莱投资:

  • 假设有一个 getNextparson() 方法返回 Person 对象,Person 类实现了 comparable 接口,现在从文件中读取记录并排序,然后给出前 1000 条记录,纸上作答
  • 答案见Glassdoor
  • 写一个函数,传入 2 个有序的整数数组,返回一个有序的整数数组。
  • 答案见Glassdoor 

骑士资本

全球医疗交流:

高盛投资:

Google:

  • 给定 2 个包含单词列表(每行一个)的文件,编程列出交集。
  • 答案见Glassdoor

Nextlabs:

树(二叉或其他)形成许多普通数据结构的基础。请描述一些这样的数据结构以及何时可以使用它们。


Delphix:

亚马逊:

  • 给定如下整数列表,如何最有效的进行排序同时剔除重复的数字?
  • 答案见Glassdoor

埃森哲: 

  • 解释 Java 和 C++ 内存管理的不同。
  • 答案见Glassdoor
  • 给定一个 C++ 或 Java 类型/表达式,给出等价的表达(如果存在)。例如:C++ 有常量,Java 有…?
  • 答案见Glassdoor
  • 给一个例子, 关于何时创建一个接口?
  • 答案见Glassdoor

Citi:

微软:

  • 写一个程序找出所有字符串的组合,并检查它们是否是回文串?
  • 答案见Glassdoor

RedMane技术:

技术问题:1. OOP的三个支柱,并解释它们 2. Java 的 final 关键字 3. 抽象类与接口的不同 4. JSP 与 Servlet 的不同 5.  Java 中的不变性 6. 多线程 – 休眠与让步? 7. 什么是设计模式,说一些。解释单例设计模式? 8. 访问修饰符中的 protected 关键字 9. continue 与 break 表达式 10. 描述 MVC 模式 11. StringBuffer 与StringBuilder 12. 一个逻辑/算法问题:向有序数组中插入一个元素到合适的位置。他们将关注非常基础的逻辑设计,如循环,函数和它们的参数。

答案见Glassdoor

OPNET:

BlackBerry:

EverBank Financial:

TRUSTe:

Airline Tariff Publishing Company:

  • 在 Spring 中使用单例实例,你只能得到唯一一个实例。如果应用想得到多个实例,你是怎么重写或克服这种情况的?
  • 答案见Glassdoor

Clearwire:

UC Davis:

  • 两个 JSP 页面见如何传递变量,那么在一个控制权 servlet 和 JSP 页面间呢?
  • 答案见Glassdoor

Deutsche Bank:

  • ArrayList 和 LinkedList 的区别,例如什么时候用 ArrayList?
  • 答案见Glassdoor

Mindteck: 

1. 什么是泛型? 2. arraylist 和 set 的区别? 3. 解释 finally

Antra:

什么 Java 原型不是线程安全的;final 和 finalize 的区别;能否在运行时向 static final 类型的赋值;抽象类和接口的区别?

相关文章

10 Oct 09:57

Spark Streaming 新手指南

by Leo

随着大数据技术的不断发展,人们对于大数据的实时性处理要求也在不断提高,传统的 MapReduce 等批处理框架在某些特定领域,例如实时用户推荐、用户行为分析这些应用场景上逐渐不能满足人们对实时性的需求,因此诞生了一批如 S3、Samza、Storm 这样的流式分析、实时计算框架。Spark 由于其内部优秀的调度机制、快速的分布式计算能力,所以能够以极快的速度进行迭代计算。正是由于具有这样的优势,Spark 能够在某些程度上进行实时处理,Spark Streaming 正是构建在此之上的流式框架。

流式大数据处理框架介绍

Samza

Samza 是一个分布式的流式数据处理框架(streaming processing),Linkedin 开源的产品, 它是基于 Kafka 消息队列来实现类实时的流式数据处理的。更为准确的说法是,Samza 是通过模块化的形式来使用 Apache Kafka 的,因此可以构架在其他消息队列框架上,但出发点和默认实现是基于 Apache Kafka。

本质上说,Samza 是在消息队列系统上的更高层的抽象,是一种应用流式处理框架在消息队列系统上的一种应用模式的实现。

总的来说,Samza 与 Storm 相比,传输上完全基于 Apache Kafka,集群管理基于 Hadoop YARN,即 Samza 只负责处理这一块具体业务,再加上基于 RocksDB 的状态管理。由于受限于 Kafka 和 YARN,所以它的拓扑结构不够灵活。

Storm

Storm 是一个开源的、大数据处理系统,与其他系统不同,它旨在用于分布式实时处理且与语言无关。Storm 不仅仅是一个传统的大数据分析系统,它可以被用于构建复杂事件处理 (CEP) 系统。CEP 系统从功能上来说,通常被分类为计算和面向检测两类,两者都可通过用户定义的算法在 Storm 中实现。举例而言,CEP 可用于识别事件洪流中有意义的事件,然后实时地处理这些事件。

Storm 框架与其他大数据解决方案的不同之处,在于它的处理方式。Apcahe Hadoop 本质上来说是一个批处理系统,即目标应用模式是针对离线分析为主。数据被引入 Hadoop 的分布式文件系统 (HDFS),并被均匀地分发到各个节点进行处理,HDFS 的数据平衡规则可以参照本文作者发表于 IBM 的文章《HDFS 数据平衡规则及实验介绍》,进行深入了解。当处理完成时,结果数据返回到 HDFS,然后可以供处理发起者使用。Storm 则支持创建拓扑结构来转换没有终点的数据流。不同于 Hadoop 作业,这些转换从不会自动停止,它们会持续处理到达的数据,即 Storm 的流式实时处理方式。

Spark Streaming

Spark Streaming 类似于 Apache Storm,用于流式数据的处理。根据其官方文档介绍,Spark Streaming 有高吞吐量和容错能力强这两个特点。Spark Streaming 支持的数据输入源很多,例如:Kafka、Flume、Twitter、ZeroMQ 和简单的 TCP 套接字等等。数据输入后可以用 Spark 的高度抽象原语如:map、reduce、join、window 等进行运算。而结果也能保存在很多地方,如 HDFS,数据库等。另外 Spark Streaming 也能和 MLlib(机器学习)以及 Graphx 完美融合。

在 Spark Streaming 中,处理数据的单位是一批而不是单条,而数据采集却是逐条进行的,因此 Spark Streaming 系统需要设置间隔使得数据汇总到一定的量后再一并操作,这个间隔就是批处理间隔。批处理间隔是 Spark Streaming 的核心概念和关键参数,它决定了 Spark Streaming 提交作业的频率和数据处理的延迟,同时也影响着数据处理的吞吐量和性能。

我们可以通过如下命令启动 WordCount 程序,如清单 1 所示。

Spark Streaming 示例

清单 1. 运行 WordCount 程序
./bin/run-example org.apache.spark.examples.streaming.JavaRecoverableNetworkWordCount 
                                                localhost 9999 wordcountdata wordcountdata

作为构建于 Spark 之上的应用框架,Spark Streaming 承袭了 Spark 的编程风格。

清单 2. WordCount 示例源代码
JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {
 @Override
 public JavaStreamingContext create() {
 return createContext(ip, port, checkpointDirectory, outputPath);
 }
};

SparkConf sparkConf = new SparkConf().setAppName("JavaRecoverableNetworkWordCount");
 // Create the context with a 1 second batch size
 //首先通过 JavaStreamingContextFactory 创建 Spark Streaming 过程。
 JavaStreamingContext ssc = new JavaStreamingContext(sparkConf, Durations.seconds(1));
 ssc.checkpoint(checkpointDirectory);

 // Create a socket stream on target ip:port and count the
 // words in input stream of \n delimited text (eg. generated by 'nc')
 JavaReceiverInputDStream<String> lines = ssc.socketTextStream(ip, port);
 JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
 @Override
 public Iterable<String> call(String x) {
 return Lists.newArrayList(SPACE.split(x));
 }
 });
 JavaPairDStream<String, Integer> wordCounts = words.mapToPair(
 new PairFunction<String, String, Integer>() {
 @Override
 public Tuple2<String, Integer> call(String s) {
 return new Tuple2<String, Integer>(s, 1);
 }
 }).reduceByKey(new Function2<Integer, Integer, Integer>() {
 @Override
 public Integer call(Integer i1, Integer i2) {
 return i1 + i2;
 }
 });

 wordCounts.foreachRDD(new Function2<JavaPairRDD<String, Integer>, Time, Void>() {
 @Override
 public Void call(JavaPairRDD<String, Integer> rdd, Time time) throws IOException {
 String counts = "Counts at time " + time + " " + rdd.collect();
 System.out.println(counts);
 System.out.println("Appending to " + outputFile.getAbsolutePath());
 Files.append(counts + "\n", outputFile, Charset.defaultCharset());
 return null;
 }
});

JavaStreamingContextFactory factory = new JavaStreamingContextFactory() {
 @Override
 public JavaStreamingContext create() {
 return createContext(ip, port, checkpointDirectory, outputPath);
 }
 };
 JavaStreamingContext ssc = JavaStreamingContext.getOrCreate(checkpointDirectory, factory);
 ssc.start();
ssc.awaitTermination();

如清单 2 所示,构建一个 Spark Streaming 应用程序一般来说需要 4 个步骤。

  1. 构建 Streaming Context 对象与 Spark 初始需要创建 SparkContext 对象一样,使用 Spark Streaming 就需要创建 StreamingContext 对象。创建 StreamingContext 对象所需的参数与 SparkContext 基本一致,包括指明 master、设定名称等。需要注意的是参数 Second(1),Spark Streaming 需要制定处理数据的时间间隔,如 1s,那么 Spark Streaming 会以 1s 为时间窗口进行数据处理。此参数需要根据用户的需求和集群的处理能力进行适当的设置,它的生命周期会伴随整个 StreamingContext 的生命周期且无法重新设置。因此,用户需要从需求和集群处理能力出发,设置一个合理的时间间隔。
  2. 创建 InputDStream如同 Strom 的 Spout 一样,Spark Streaming 需要指明数据源。例如 socketTextStream,Spark Streaming 将以套接字连接作为数据源读取数据。当然,Spark Streaming 支持多种不同的数据源,包括 kafkaStream、flumeStream、fileStream、networkStream 等。
  3. 操作 DStream对于从数据源得到的 DStream,用户可以在其基础上进行各种操作,如 WordCount 的操作就是一个典型的单词计数执行流程,即对当前时间窗口内从数据源得到的数据进行分词,然后利用 MapReduce 算法映射和计算,最后使用 print() 输出结果。
  4. 启动 Spark Streaming之前的所有步骤只创建了执行流程,程序没有有真正连接上数据源,也没有对数据进行任何操作,只是设定好了所有的执行计算,当 ssc.start() 启动后,程序才真正进行所有预期的操作。

上面第一步提到了时间窗口,Spark Streaming 有特定的窗口操作,窗口操作涉及两个参数:一个是滑动窗口的宽度(Window Duration);另一个是窗口滑动的频率(Slide Duration),这两个参数必须是 batch size 的倍数。例如以过去 5 秒钟为一个输入窗口,每 1 秒统计一下 WordCount,那么我们会将过去 5 秒钟的每一秒钟的 WordCount 都进行统计,然后进行叠加,得出这个窗口中的单词统计。

从上面的步骤可以看出,一个 Spark Streaming 应用程序与 Spark 应用程序非常相似,用户构建执行逻辑,内部主驱动程序来调用用户实现的逻辑,持续不断地以并行的方式对输入的流式数据进行处理。Spark Streaming 抽象了离散数据流 (Discretized Stream,即 DStream) 这个概念,它包含了一组连续的 RDD,这一组连续的 RDD 代表了连续的流式数据。DStream 可以通过实时的输入数据,例如从套接字接口或者 Kafka 消息队列中得到的数据创建,也可以通过现有的 DStream 转换得到,这些转换操作包括 map、reduce、window 等。

离散数据流 (DStream) 作为 Spark Streaming 中的一个基本抽象,代表了一个数据流,这个数据流既可以从外部输入源获得,也可以通过对输入流的转换获得。在其内部,DStream 是通过一组时间序列上连续的 RDD 来表示的,每一个 RDD 都包含了特定时间间隔内的数据流。

在 DStream 内部维护了一组离散的以时间轴为键的 RDD 序列,这些 RDD 序列分别代表着不同时间段内的数据集,而我们对于 DStream 的各种操作最终都会映射到内部的 RDD 上。

如清单 3 所示代码是将基于行的数据流按照预先设置好的规则 (SPACE 关键字),本示例是空格,清单 1 可以看到具体的设置方式,切分为基于词的数据流,即通过 flatMap 将一个 DStream 转换成另一个 DStream。对于 DStream 的转换操作,最终会被映射到内部基于 RDD 的操作,操作结束后我们将得到一个新的 DStream,我们可以再次 DStream 上继续进行操作。

清单 3. 切分数据流
JavaDStream<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
 @Override
 public Iterable<String> call(String x) {
 return Lists.newArrayList(SPACE.split(x));
 }
});

这些内部的 RDD 序列最终会提交到 Spark 上进行处理。DStream 操作提升了抽象程度,隐藏了具体的实现细节,使得用户能够专注在 DStream 上进行操作而无须关心内部实现的细节。

清单 2 所示程序里面使用到了几个函数,这里做一一解释。

map(func) 方法返回一个新 DStream,其中的每一个元素都是通过将原 DStream 的每个元素作用于函数 func 得到的。

flatMap(func) 方法与 map 相似,不同之处在于每一个元素通过函数 func 可以产生出 0 个或多个新元素。

reduceByKey(func,numTasks) 方法将 DStream[(K,V)] 中的值 V 按键 K 使用聚合函数 func 聚合。默认情况下,将采用 Spark 的默认任务并行的提交任务 (本地环境下是 2,集群环境下是 8),可以通过配置 numTasks 设置不同的任务数量。

foreachRDD(func) 方法是基本的输出操作,将 DStream 中的每个 RDD 作用于函数 func 上,如输出每个 RDD 内的元素、将 RDD 保存到外部文件中。

Spark Streaming DStream

在内部实现上,DStream 由连续的序列化 RDD 来表示。每个 RDD 含有一段时间间隔内的数据,如图 1 所示。

图 1. 序列化 RDD

对数据的操作也是按照 RDD 为单位来进行的,如图 2 所示。

图 2. 以 RDD 为单位处理数据

图 2 下方的 RDD 都是通过 Spark 高级原语转换而来,计算过程由 Spark Engine 来完成。

DStream 上的原语与 RDD 的类似,分为 Transformations(转换)和 Output Operations(输出)两种,此外转换操作中还有一些比较特殊的原语,如:updateStateByKey()、transform() 以及各种 Window 相关的原语。

UpdateStateByKey 原语用于记录历史记录,上文中 Word Count 示例中就用到了该特性。若不用 UpdateStateByKey 来更新状态,那么每次数据进来后分析完成,结果输出后将不再保存。如,若将上文清单 2 中的第 15 行替换为:

JavaPairDStream<String, Integer> counts = pairs.reduceByKey((i1, i2) -> (i1 + i2));

那么输入:hellow world,结果则为:(hello,1)(world,1),然后输入 hello spark,结果则为 (hello,1)(spark,1)。也就是不会保留上一次数据处理的结果。

使用 UpdateStateByKey 原语用于需要记录的 State,可以为任意类型,如上例中即为 Optional<Intege>类型。

Transform() 原语允许 DStream 上执行任意的 RDD-to-RDD 函数,通过该函数可以方便的扩展 Spark API。

Spark Streaming 优缺点

与传统流式框架相比,Spark Streaming 最大的不同点在于它对待数据是粗粒度的处理方式,即一次处理一小批数据,而其他框架往往采用细粒度的处理模式,即依次处理一条数据。Spark Streaming 这样的设计实现既为其带来了显而易见的优点,又引入了不可避免的缺点。

优点

1. Spark Streaming 内部的实现和调度方式高度依赖 Spark 的 DAG 调度器和 RDD,这就决定了 Spark Streaming 的设计初衷必须是粗粒度方式的,同时,由于 Spark 内部调度器足够快速和高效,可以快速地处理小批量数据,这就获得准实时的特性。

2. Spark Streaming 的粗粒度执行方式使其确保“处理且仅处理一次”的特性,同时也可以更方便地实现容错恢复机制。

3. 由于 Spark Streaming 的 DStream 本质是 RDD 在流式数据上的抽象,因此基于 RDD 的各种操作也有相应的基于 DStream 的版本,这样就大大降低了用户对于新框架的学习成本,在了解 Spark 的情况下用户将很容易使用 Spark Streaming。

4. 由于 DStream 是在 RDD 上的抽象,那么也就更容易与 RDD 进行交互操作,在需要将流式数据和批处理数据结合进行分析的情况下,将会变得非常方便。

缺点

1. Spark Streaming 的粗粒度处理方式也造成了不可避免的延迟。在细粒度处理方式下,理想情况下每一条记录都会被实时处理,而在 Spark Streaming 中,数据需要汇总到一定的量后再一次性处理,这就增加了数据处理的延迟,这种延迟是由框架的设计引入的,并不是由网络或其他情况造成的。

2. Spark Streaming 当前版本稳定性不是很好。

总而言之,Spark Streaming 为我们提供了一种崭新的流式处理框架,相信未来随着 Spark Streaming 会在易用性、稳定性以及其他方面有很大的提升。

结束语

通过本文的学习,读者可以大致了解 Spark Streaming 程序的运行方式、如何编写 Spark Streaming 程序、Spark Streaming 的优缺点。目前市面上发布的 Spark 中文书籍对于初学者来说大多较为难读懂,作者力求推出一系列 Spark 文章,让读者能够从实际入手的角度来了解 Spark。后续除了应用之外的文章,还会致力于基于 Spark 的系统架构、源代码解释等方面的文章发布。

Spark Streaming 新手指南,首发于博客 - 伯乐在线

10 Oct 04:29

Java线程池架构原理和源码解析(ThreadPoolExecutor)

by importnewzz

在前面介绍JUC的文章中,提到了关于线程池Execotors的创建介绍,在文章:《java之JUC系列-外部Tools》中第一部分有详细的说明,请参阅;

文章中其实说明了外部的使用方式,但是没有说内部是如何实现的,为了加深对实现的理解,在使用中可以放心,我们这里将做源码解析以及反馈到原理上,Executors工具可以创建普通的线程池以及schedule调度任务的调度池,其实两者实现上还是有一些区别,但是理解了ThreadPoolExecutor,在看ScheduledThreadPoolExecutor就非常轻松了,后面的文章中也会专门介绍这块,但是需要先看这篇文章。

使用Executors最常用的莫过于是使用:Executors.newFixedThreadPool(int)这个方法,因为它既可以限制数量,而且线程用完后不会一直被cache住;那么就通过它来看看源码,回过头来再看其他构造方法的区别:

在《java之JUC系列-外部Tools》文章中提到了构造方法,为了和本文对接,再贴下代码:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

其实你可以自己new一个ThreadPoolExecutor,来达到自己的参数可控的程度,例如,可以将LinkedBlockingQueue换成其它的(如:SynchronousQueue),只是可读性会降低,这里只是使用了一种设计模式

我们现在来看看ThreadPoolExecutor的源码是怎么样的,也许你刚开始看他的源码会很痛苦,因为你不知道作者为什么是这样设计的,所以本文就我看到的思想会给你做一个介绍,此时也许你通过知道了一些作者的思想,你也许就知道应该该如何去操作了。

这里来看下构造方法中对那些属性做了赋值:

源码段1:

   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

这里你可以看到最终赋值的过程,可以先大概知道下参数的意思:

corePoolSize:核心运行的poolSize,也就是当超过这个范围的时候,就需要将新的Thread放入到等待队列中了;

maximumPoolSize:一般你用不到,当大于了这个值就会将Thread由一个丢弃处理机制来处理,但是当你发生:newFixedThreadPool的时候,corePoolSize和maximumPoolSize是一样的,而corePoolSize是先执行的,所以他会先被放入等待队列,而不会执行到下面的丢弃处理中,看了后面的代码你就知道了。

workQueue:等待队列,当达到corePoolSize的时候,就向该等待队列放入线程信息(默认为一个LinkedBlockingQueue),运行中的队列属性为:workers,为一个HashSet;内部被包装了一层,后面会看到这部分代码。

keepAliveTime:默认都是0,当线程没有任务处理后,保持多长时间,cachedPoolSize是默认60s,不推荐使用。

threadFactory:是构造Thread的方法,你可以自己去包装和传递,主要实现newThread方法即可;

handler:也就是参数maximumPoolSize达到后丢弃处理的方法,java提供了5种丢弃处理的方法,当然你也可以自己弄,主要是要实现接口:RejectedExecutionHandler中的方法:

public void rejectedExecution(Runnabler, ThreadPoolExecutor e)

java默认的是使用:AbortPolicy,他的作用是当出现这中情况的时候会抛出一个异常;其余的还包含:

1、CallerRunsPolicy:如果发现线程池还在运行,就直接运行这个线程

2、DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放进去。

3、DiscardPolicy:什么也不做

4、AbortPolicy:java默认,抛出一个异常:RejectedExecutionException。

通常你得到线程池后,会调用其中的:submit方法或execute方法去操作;其实你会发现,submit方法最终会调用execute方法来进行操作,只是他提供了一个Future来托管返回值的处理而已,当你调用需要有返回值的信息时,你用它来处理是比较好的;这个Future会包装对Callable信息,并定义一个Sync对象(),当你发生读取返回值的操作的时候,会通过Sync对象进入锁,直到有返回值的数据通知,具体细节先不要看太多,继续向下:

来看看execute最为核心的方法吧:

源码段2:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
            if (runState == RUNNING && workQueue.offer(command)) {
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

这段代码看似简单,其实有点难懂,很多人也是这里没看懂,没事,我一个if一个if说:

首先第一个判定空操作就不用说了,下面判定的poolSize >= corePoolSize成立时候会进入if的区域,当然它不成立也有可能会进入,他会判定addIfUnderCorePoolSize是否返回false,如果返回false就会进去;

我们先来看下addIfUnderCorePoolSize方法的源码是什么:

源码段3:

    private boolean addIfUnderCorePoolSize(Runnable firstTask) {
        Thread t = null;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (poolSize < corePoolSize && runState == RUNNING)
                t = addThread(firstTask);
        } finally {
            mainLock.unlock();
        }
        if (t == null)
            return false;
        t.start();
        return true;
    }

可以发现,这段源码是如果发现小雨corePoolSize就会创建一个新的线程,并且调用线程的start()方法将线程运行起来:这个addThread()方法,我们先不考虑细节,因为我们还要先看到前面是怎么进去的,这里可以发信啊,只有没有创建成功Thread才会返回false,也就是当当前的poolSize > corePoolSize的时候,或线程池已经不是在running状态的时候才会出现;

注意:这里在外部判定一次poolSize和corePoolSize只是初步判定,内部是加锁后判定的,以得到更为准确的结果,而外部初步判定如果是大于了,就没有必要进入这段有锁的代码了。

此时我们知道了,当前线程数量大于corePoolSize的时候,就会进入【代码段2】的第一个if语句中,回到【源码段2】,继续看if语句中的内容:

这里标记为

源码段4

if (runState == RUNNING && workQueue.offer(command)) {
   if (runState != RUNNING || poolSize == 0)
       ensureQueuedTaskHandled(command);
   }
   else if (!addIfUnderMaximumPoolSize(command))
       reject(command); // is shutdown or saturated

第一个if,也就是当当前状态为running的时候,就会去执行workQueue.offer(command),这个workQueue其实就是一个BlockingQueue,offer()操作就是在队列的尾部写入一个对象,此时写入的对象为线程的对象而已;所以你可以认为只有线程池在RUNNING状态,才会在队列尾部插入数据,否则就执行else if,其实else if可以看出是要做一个是否大于MaximumPoolSize的判定,如果大于这个值,就会做reject的操作,关于reject的说明,我们在【源码段1】的解释中已经非常明确的说明,这里可以简单看下源码,以应征结果:

源码段5:

    private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
        Thread t = null;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (poolSize < maximumPoolSize && runState == RUNNING)
                //在corePoolSize = maximumPoolSize下,该代码几乎不可能运行
                t = addThread(firstTask); 
        } finally {
            mainLock.unlock();
        }
        if (t == null)
            return false;
        t.start();
        return true;
}
void reject(Runnable command) {
        handler.rejectedExecution(command, this);
    }

也就是如果线程池满了,而且线程池调用了shutdown后,还在调用execute方法时,就会抛出上面说明的异常:RejectedExecutionException

再回头来看下【代码段4】中进入到等待队列后的操作:

if (runState != RUNNING || poolSize == 0)

                   ensureQueuedTaskHandled(command);

这段代码是要在线程池运行状态不是RUNNING或poolSize == 0才会调用,他是干啥呢?

他为什么会不等于RUNNING呢?外面那一层不是判定了他== RUNNING了么,其实有时间差就是了,如果是poolSize == 0也会执行这段代码,但是里面的判定条件是如果不是RUNNING,就做reject操作,在第一个线程进去的时候,会将第一个线程直接启动起来;很多人也是看这段代码很绕,因为不断的循环判定类似的判定条件,你主要记住他们之间有时间差,要取最新的就好了。

此时貌似代码看完了?咦,此时有问题了:

1、  等待中的线程在后来是如何跑起来的呢?线程池是不是有类似Timer一样的守护进程不断扫描线程队列和等待队列?还是利用某种锁机制,实现类似wait和notify实现的?

2、  线程池的运行队列和等待队列是如何管理的呢?这里还没看出影子呢!

NO,NO,NO!

Java在实现这部分的时候,使用了怪异的手段,神马手段呢,还要再看一部分代码才晓得。

在前面【源码段3】中,我们看到了一个方法叫:addThread(),也许很少有人会想到关键在这里,其实关键就是在这里:

我们看看addThread()方法到底做了什么。

源码段6:

    private Thread addThread(Runnable firstTask) {
        Worker w = new Worker(firstTask);
        Thread t = threadFactory.newThread(w);
        if (t != null) {
            w.thread = t;
            workers.add(w);
            int nt = ++poolSize;
            if (nt > largestPoolSize)
                largestPoolSize = nt;
        }
        return t;
    }

这里创建了一个Work,其余的操作,就是讲poolSize叠加,然后将将其放入workers的运行队列等操作;

我们主要关心Worker是干什么的,因为这个threadFactory对我们用途不大,只是做了Thread的命名处理;而Worker你会发现它的定义也是一个Runnable,外部开始在代码段中发现了调用哪个这个Worker的start()方法,也就是线程的启动方法,其实也就是调用了Worker的run()方法,那么我们重点要关心run方法是如何处理的

源码段7:

       public void run() {
            try {
                Runnable task = firstTask;
                firstTask = null;
                while (task != null || (task = getTask()) != null) {
                    runTask(task);
                    task = null;
                }
            } finally {
                workerDone(this);
            }
        }

FirstTask其实就是开始在创建work的时候,由外部传入的Runnable对象,也就是你自己的Thread,你会发现它如果发现task为空,就会调用getTask()方法再判定,直到两者为空,并且是一个while循环体。

那么看看getTask()方法的实现为:

源码段8:

     Runnable getTask() {
        for (;;) {
            try {
                int state = runState;
                if (state > SHUTDOWN)
                    return null;
                Runnable r;
                if (state == SHUTDOWN)  // Help drain queue
                    r = workQueue.poll();
                else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
                    r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
                else
                    r = workQueue.take();
                if (r != null)
                    return r;
                if (workerCanExit()) {
                    if (runState >= SHUTDOWN) // Wake up others
                        interruptIdleWorkers();
                    return null;
                }
                // Else retry
            } catch (InterruptedException ie) {
                // On interruption, re-check runState
            }
        }
    }

你会发现它是从workQueue队列中,也就是等待队列中获取一个元素出来并返回!

回过头来根据代码段6理解下:

当前线程运行完后,在到workQueue中去获取一个task出来,继续运行,这样就保证了线程池中有一定的线程一直在运行;此时若跳出了while循环,只有workQueue队列为空才会出现或出现了类似于shutdown的操作,自然运行队列会减少1,当再有新的线程进来的时候,就又开始向worker里面放数据了,这样以此类推,实现了线程池的功能。

这里可以看下run方法的finally中调用的workerDone方法为:

源码段9:

    void workerDone(Worker w) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            completedTaskCount += w.completedTasks;
            workers.remove(w);
            if (--poolSize == 0)
                tryTerminate();
        } finally {
            mainLock.unlock();
        }
    }

注意这里将workers.remove(w)掉,并且调用了—poolSize来做操作。

至于tryTerminate是做了更多关于回收方面的操作。

最后我们还要看一段代码就是在【源码段6】中出现的代码调用为:runTask(task);这个方法也是运行的关键。

源码段10:

     private void runTask(Runnable task) {
            final ReentrantLock runLock = this.runLock;
            runLock.lock();
            try {
                if (runState < STOP &&
                    Thread.interrupted() &&
                    runState >= STOP)
                    thread.interrupt();

                boolean ran = false;
                beforeExecute(thread, task);
                try {
                    task.run();
                    ran = true;
                    afterExecute(task, null);
                    ++completedTasks;
                } catch (RuntimeException ex) {
                    if (!ran)
                        afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                runLock.unlock();
            }
        }

你可以看到,这里面的task为传入的task信息,调用的不是start方法,而是run方法,因为run方法直接调用不会启动新的线程,也是因为这样,导致了你无法获取到你自己的线程的状态,因为线程池是直接调用的run方法,而不是start方法来运行。

这里有个beforeExecuteafterExecute方法,分别代表在执行前和执行后,你可以做一段操作,在这个类中,这两个方法都是【空body】的,因为普通线程池无需做更多的操作。

如果你要实现类似暂停等待通知的或其他的操作,可以自己extends后进行重写构造;

本文没有介绍关于ScheduledThreadPoolExecutor调用的细节,下一篇文章会详细说明,因为大部分代码和本文一致,区别在于一些细节,在介绍:ScheduledThreadPoolExecutor的时候,会明确的介绍它与TimerTimerTask的巨大区别,区别不在于使用,而是在于本身内在的处理细节。

相关文章

10 Oct 03:45

Rust 1.3提升了API的稳定性

by James Chesters

近日,Rust核心团队发布了Rust 1.3稳定版,该版本提升了Rust语言的性能及API的稳定性。

在Rust官方博客文章"Rust 1.3发布"中,团队介绍说, 该版本的发布使得Rust语言的稳定性有了大幅提升, 这其中"包含了新的Duration API函数以及对Error和Hash/Hahser的改进", 未来对std::time模块的改进有望在1.5版本中实现。

负责Duration稳定性方面工作(commit 26818)的Rust语言开发人员Steven Fackler说,std::time模块和Duration类型的稳定性都得到了加强。Fackler强调说,Duration::span仍然不稳定,Duration的Display实现方法被删除了,原因是"它还在反复修改中并且所有关于稳定类型的功能实现事实上还算是稳定的"。

Fackler指出,这个提交会影响到任何使用Duration的Display实现的开发者。

继今年五月Rust 1.0的发布,Rust迎来了一个快速编译时代以及对于DST(dynamically-sized types)的全面支持。1.3版本的发行说明重点强调说"新对象的默认生命周期开始于在对新对象生命周期变化的一个警告周期之后"。这是一个有可能影响其他功能的变化,例如将&'a Box<Trait>&'a Box<Trait+'a>解释为&'a Box<Trait+'static>

关于这个变化,开发者Aaron TuronRFC1156文档中说道:

“当我们开始着手建立默认的对象边界时,[RFC599](https://github.com/rust-lang/rfcs/blob/master/text/0599-default-object-bound.md)文档规定`&'x Box<Trait>`(和`&'x mut Box<Trait>`)应该扩展为`&'x Box<Trait+'x>`(和`&'x mut Box<Trait+'x>`)。相对于那种出现在引用之外的Box类型,这种类型默认使用`static (Box<Trait+'static>`。做出这个决定的原因是,这么做意味着按照此类格式书写的函数可以接收更多的对象。”

Rust 1.3稳定版同时还提供了一些性能方面的改进,包括使用双路算法(two way)提升子字符串的搜索速度并将此做成固定的API,性能远超之前的实现方法。

其他值得关注的改进还包括“对于提升Vec::resize和 Read::readtoend零字节填充速度的改进。”

提到bug 25483(使用StrSearcher完成原始字符串搜索), Rust开发者bluss说"双路搜索算法的常量空间开销非常小,不需要动态分配空间。我们的实现方法速度很快,尤其是当算法需要使用额外的的字节空间时,通常这些空间用来为许多不匹配情况(no-match cases)提高搜索速度"

Rust 1.3同时还提供了对Windows XPlint capping的支持。更多详细内容请参考发行说明

查看英文原文:Rust 1.3 Brings Stabilisation for APIs


感谢张龙对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

09 Oct 02:26

数据库范式那些事

by promumu

简介

数据库范式在数据库设计中的地位一直很暧昧,教科书中对于数据库范式倒是都给出了学术性的定义,但实际应用中范式的应用却不甚乐观,这篇文章会用简单的语言和一个简单的数据库DEMO将一个不符合范式的数据库一步步从第一范式实现到第四范式。

 

范式的目标

应用数据库范式可以带来许多好处,但是最重要的好处归结为三点:

1.减少数据冗余(这是最主要的好处,其他好处都是由此而附带的)

2.消除异常(插入异常,更新异常,删除异常)

3.让数据组织的更加和谐…

 

但剑是双刃的,应用数据库范式同样也会带来弊端,这会在文章后面说到。

 

什么是范式

简单的说,范式是为了消除重复数据减少冗余数据,从而让数据库内的数据更好的组织,让磁盘空间得到更有效利用的一种标准化标准,满足高等级的范式的先决条件是满足低等级范式。(比如满足2nf一定满足1nf)

 

DEMO

让我们先从一个未经范式化的表看起,表如下:

先对表做一个简单说明,employeeId是员工id,departmentName是部门名称,job代表岗位,jobDescription是岗位说明,skill是员工技能,departmentDescription是部门说明,address是员工住址

对表进行第一范式(1NF)

如果一个关系模式R的所有属性都是不可分的基本数据项,则R∈1NF。

简单的说,第一范式就是每一个属性都不可再分。不符合第一范式则不能称为关系数据库。对于上表,不难看出Address是可以再分的,比如”北京市XX路XX小区XX号”,着显然不符合第一范式,对其应用第一范式则需要将此属性分解到另一个表,如下:

对表进行第二范式(2NF)

若关系模式R∈1NF,并且每一个非主属性都完全函数依赖于R的码,则R∈2NF

简单的说,是表中的属性必须完全依赖于全部主键,而不是部分主键.所以只有一个主键的表如果符合第一范式,那一定是第二范式。这样做的目的是进一步减少插入异常和更新异常。在上表中,departmentDescription是由主键DepartmentName所决定,但却不是由主键EmployeeID决定,所以departmentDescription只依赖于两个主键中的一个,故要departmentDescription对主键是部分依赖,对其应用第二范式如下表:

对表进行第三范式(3NF)

关系模式R<U,F> 中若不存在这样的码X、属性组Y及非主属性Z(Z  Y), 使得X→Y,Y→Z,成立,则称R<U,F> ∈ 3NF。

简单的说,第三范式是为了消除数据库中关键字之间的依赖关系,在上面经过第二范式化的表中,可以看出jobDescription(岗位职责)是由job(岗位)所决定,则jobDescription依赖于job,可以看出这不符合第三范式,对表进行第三范式后的关系图为:

上表中,已经不存在数据库属性互相依赖的问题,所以符合第三范式

对表进行BC范式(BCNF)

设关系模式R<U,F>∈1NF,如果对于R的每个函数依赖X→Y,若Y不属于X,则X必含有候选码,那么R∈BCNF。

简单的说,bc范式是在第三范式的基础上的一种特殊情况,既每个表中只有一个候选键(在一个数据库中每行的值都不相同,则可称为候选键),在上面第三范式的noNf表中可以看出,每一个员工的email都是唯一的(难道两个人用同一个email??)则,此表不符合bc范式,对其进行bc范式化后的关系图为:

对表进行第四范式(4NF)

关系模式R<U,F>∈1NF,如果对于R的每个非平凡多值依赖X→→Y(Y  X),X都含有候选码,则R∈4NF。

简单的说,第四范式是消除表中的多值依赖,也就是说可以减少维护数据一致性的工作。对于上面bc范式化的表中,对于员工的skill,两个可能的值是”C#,sql,javascript”和“C#,UML,Ruby”,可以看出,这个数据库属性存在多个值,这就可能造成数据库内容不一致的问题,比如第一个值写的是”C#”,而第二个值写的是”C#.net”,解决办法是将多值属性放入一个新表,则第四范式化后的关系图如下:

而对于skill表则可能的值为:

 

总结

上面对于数据库范式进行分解的过程中不难看出,应用的范式登记越高,则表越多。表多会带来很多问题:

1 查询时要连接多个表,增加了查询的复杂度

2 查询时需要连接多个表,降低了数据库查询性能

而现在的情况,磁盘空间成本基本可以忽略不计,所以数据冗余所造成的问题也并不是应用数据库范式的理由。

因此,并不是应用的范式越高越好,要看实际情况而定。第三范式已经很大程度上减少了数据冗余,并且减少了造成插入异常,更新异常,和删除异常了。我个人观点认为,大多数情况应用到第三范式已经足够,在一定情况下第二范式也是可以的。

由于本人对数据库研究还处于初级阶段,所以上述如有不当之处,还望高手不吝指教…

数据库范式那些事,首发于博客 - 伯乐在线

10 Sep 12:02

我理解的 KMP 算法

by LynnShaw

最近一段时间,我一直在看 KMP 字符串模式匹配算法的各种不同解释。因为各种原因,没有找到一种我觉得好的解释。当我读到“……的前缀的后缀的前缀”时,我会不停地拍自己的脑袋。

最后,花了大约30分钟将《算法导论》里相同的部分反反复复读了以后,我决定坐下来做一些例子和图解。现在,我已经搞清楚了这个算法并能对它解释。对于那些和我有一样想法的人,下面是我自己的理解。一方面,我不打算解释为什么它比朴素的字符串匹配效率更高;这些在很多地方都已经解释得非常好了。我要说明的是,它究竟是如何工作的。

部分匹配表

毫无疑问,KMP算法的精髓是部分匹配表。我理解KMP算法时,最大的障碍就在于是否充分明白部分匹配表里的值所代表的意义。下面我会尽可能简单地来解释这些。

下面这个是“abababca”这个模板的部分匹配表:

char:   | a | b | a | b | a | b | c | a |

index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |

value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

如果我有一个8个字符的模板(这里我们就用“abababca”来举例子),我的部分匹配表将会有8格。如果此时此刻我正匹配模板的第8格即最后一格,那意味着我匹配了整个模板(“abababca”);如果我正匹配模板的第7格,则意味着当前仅匹配了整个模板的前7位(“abababc”),此时第8位(“a”)是无关的,不用去管它;如果我此时此刻正匹配模板的第6格,那意味着……看到这里你应该已经明白我的意思了。目前我还没有提到部分匹配表每格数据的含义,在这里仅仅是交代了大概。

现在,为了说明刚刚提到的每格数据的含义,我们首先要明白什么是最优前缀什么是最优后缀

最优前缀:一个字符串中,去除一个或多个尾部的字符所得的新字符串就是最优前缀。例如 “S”、 “Sn”、 “Sna”、 “Snap”都是“Snape”的最优前缀。

最优后缀:一个字符串中,去除一个或多个首部的字符所得的新字符串就是最优后缀。例如“agrid”、 “grid”、“rid”、 “id”、“d”都是 “Hagrid”的最优后缀。

有了两个概念,我现在可以用一句话来概括部分匹配表里每列数据的含义了:

模板(子模板)中,既是最优前缀也是最优后缀的最长字符串的长度。

下面我举例说明一下这句话。我们来看部分匹配表的第3格数据,如果你还记得我在前面提到的,这意味着我们目前仅仅关心前3个字母(“aba”)。在“aba”这个子模板中,有两个最优前缀(“a”和“ab”)和两个最优后缀(“a”和“ba”)。其中,最优前缀“ab”并不是最优后缀。因此,最优前缀与最优后缀中,相同的只有“a”。那么,此时此刻既是最优前缀也是最优后缀的最长字符串的长度就是1了。

我们再来试试第4格,我们应该是关注于前4个字母(“abab”)。可以看出,有3个最优前缀(“a”、“ab”、 “aba”)和3个最优后缀(“b”、“ab”、“bab”)。这一次 “ab” 既是最优前缀也是最优后缀,并且长度为2,因此,部分匹配表的第4格值为2。

这是很有趣的例子,我们再看看第5格的情况,也就是考虑“ababa”。我们有4个最优前缀(“a”、 “ab”、“aba”,和“abab”)和4个最优后缀(“a”、 “ba”、“aba”,和“baba”)。现在,有两个匹配“a”和“aba” 既是最优前缀也是最优后缀,而“aba”比“a”要长,所以部分匹配表的第5格值为3。

跳过中间的直接来看第7格,此时只考虑字母“abababc”。即使不一一枚举出所有的最优前缀与最优后缀也不难看出,这两个集合之间不会有任何的交集。因为,所有最优后缀都以“c”结尾,但没有任何最优前缀是以“c”结尾的,所以没有相匹配的,因此第7格值为0。

最后,让我们看看第8格,也就是考虑整个模板(abababca)。它的最优前缀与最有后缀都以“a”开头以“a”结尾,所以第8列的值至少是1。然而1就是最终结果了,所有长度大于等于2的最优后缀都包含“c”,但只有“abababc”这一个最优前缀包含“c”,这个7位的最优后缀“bababca”并不匹配,所以第8列最终赋值为1。

如何使用部分匹配表

当我们找到了部分匹配的字符串时,可以用部分匹配表里的值来跳过前面一些字符(而不是重复进行没有必要的比较)。具体是这样工作的:

如果已经匹配到的部分字符串的长度为partial_match_length且 table[partial_match_length] > 1,那么我们可以跳过partial_match_length- table[partial_match_length - 1]个字符。

比如,我们拿“abababca”来这个模板来匹配文本“ bacbababaabcbab”的话,我们的部分匹配表应该是这样的:

char:  | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

第一次匹配的时候是在这里

bacbababaabcbab
 |
 abababca

partial_match_length值为1,对应的table[partial_match_length - 1] (即table[0])值为0。所以,这种情况下我们不能跳过任何字符。下一次的匹配是这里:

bacbababaabcbab
    |||||
    abababca

partial_match_length值为5,对应的 table[partial_match_length - 1] (即 table[4])值为3。这意味着我们可以跳过 partial_match_length- table[partial_match_length - 1] (即 5 – table[4] 或5 – 3 亦即 2)个字符:

// x 表示一个跳过

bacbababaabcbab
    xx|||
      abababca

partial_match_length值为3,对应的 table[partial_match_length - 1] (即 table[2])值为1,这意味着我们可以跳过 partial_match_length- table[partial_match_length - 1] (即 3- table[2] 或3 – 1亦即 2)个字符:

// x 表示一个跳过

bacbababaabcbab
      xx|
        abababca

现在,模板长度大于所剩余的目标字符串长度,所以我们知道不会再有匹配了。

结语

那么你应该搞明白了吧。就像我一开始说的,这篇文章没有关于KMP多余的解释或者或枯燥的证明;而是我自己的理解,以及我发现的容易让人感到迷惑部分的详尽解释。如果你有任何疑问或者发现我这篇文章哪里写错了,请给我留言;也许我们都会有所收获。

我理解的 KMP 算法,首发于博客 - 伯乐在线

10 Sep 06:09

史上最全的MSSQL复习笔记

by changqi

1.什么是SQL语句

sql语言:结构化的查询语言。(Structured Query Language),是关系数据库管理系统的标准语言。

它是一种解释语言:写一句执行一句,不需要整体编译执行。
语法特点:
1.没有“ ”,字符串使用‘ ’包含
2.没有逻辑相等,赋值和逻辑相等都是=
3.类型不再是最严格的。任何数据都可以包含在‘ ’以内
4.没有bool值的概念,但是在视图中可以输入true/false
5.它也有关系运算符:> < >= <= = <> != ,它返回一个bool值
6.它也有逻辑运算符: !(not) &&(and) ||(or)
7.它不区别大小写

2.使用sql语句创建数据库和表

语法:
create database 数据库名称
on primary –默认在主文件组上
(
name=’逻辑名称_data’ , –当你发现它不是一句完整的sql语句,而仅仅是一个处理结构中的某一句的时候,就需要添加 ,
size=初始大小,–数值不包含在‘’以内
filegrowth=文件增长 ,
maxsize=最大容量,
filename=’物理路径’
)
log on
(
name=’逻辑名称_log’ , –当你发现它不是一句完整的sql语句,而仅仅是一个处理结构中的某一句的时候,就需要添加 ,
size=初始大小,–数值不包含在‘’以内
filegrowth=文件增长 ,
maxsize=最大容量, –一般来说日志文件不限制最大容量
filename=’物理路径’
)

--判断数据库文件是否已经存在 :数据库的记录都存储在master库中的sysdatabases表中
--自动切换当前数据库
--使用代码开启外围应该配置器
exec sp_configure 'show advanced options' ,1
RECONFIGURE
exec sp_configure 'xp_cmdshell',1
RECONFIGURE
--自定义目录  xp_cmdshell可以创建出目录   'mkdir f:\project':指定创建目录
exec xp_cmdshell 'mkdir f:\project'

use master
--exists 函数判断()中的查询语句是否返回结果集,如果返回了结果集则得到true,否则得到false
if exists( select * from sysdatabases where name='School')
    drop database School --删除当前指定名称的数据库
create database School
on primary
(
 name='School_data',--逻辑名称.说明最多能够存储100mb数据,如果没有限制就可以将硬盘存储满
 size=3mb,--初始大小
 maxsize=100mb,--最大容量
 filegrowth=10%,--文件增长一次增长10%
 filename='f:\project\School_data.mdf'    
),
--创建文件组
filegroup mygroup
(
 name='School_data1',--逻辑名称.说明最多能够存储100mb数据,如果没有限制就可以将硬盘存储满
 size=3mb,--初始大小
 maxsize=100mb,--最大容量
 filegrowth=10%,--文件增长一次增长10%
 filename='F:\qiyi\School_data1.ndf'    
)
log on
(
 name='School_log',--逻辑名称
 size=3mb,--初始大小
 --maxsize=100mb,--最大容量
 filegrowth=10%,--文件增长一次增长10%
 filename='f:\project\School_log.ldf'    
),
(
 name='School_log1',--逻辑名称
 size=3mb,--初始大小
 --maxsize=100mb,--最大容量
 filegrowth=10%,--文件增长一次增长10%
 filename='F:\qiyi\School_log1.ldf'    
)

3.创建数据表

语法:
create table 表名
(
字段名称 字段类型 字段特征(是否为null,默认值 标识列 主键 唯一键 外键 check约束),
字段名称 字段类型 字段特征(是否为null,默认值 标识列 主键 唯一键 外键 check约束)
)
创建老师表Teacher :Id、Name、Gender、Age、Salary、Birthday

use School
if exists(select * from sysobjects where name='Classes')
  drop table Classes
create table Classes
(
 Classid int identity(1,1),
 ClassName nvarchar(50) not null 
)
if exists(select * from sysobjects where name='teacher')
  drop table teacher
create table Teacher
(
 Id int identity(1,1),--可以同时创建多个特征,用空格 分隔开。 identity是标识列,第一个参数是种子,第二个是增量
Name nvarchar(50)  not null,-- not null标记它的值不能为null--不能不填写
ClassId int not null,
 Gender bit not null,
Age int   ,
Salary money, --如果不标记为 not null.那么就相当于标记了null
Birthday datetime  
)

4.数据完整性约束

实体完整性:实体就是指一条记录。这种完整性就是为了保证每一条记录不是重复记录。是有意义的
– 主键:非空和唯一.一个表只有一个主键,但是一个主键可以是由多个字段组成的 组合键
– 标识列:系统自动生成,永远不重复
– 唯一键:唯一,但是可以为null,只能null一次

域完整性:域就是指字段,它是为了保证字段的值是准和有效,合理值
– 类型 是否null,默认值,check约束,关系

自定义完整性:
– check约束 , 存储过程 触发器

引用完整性:一个表的某个字段的值是引用自另外一个表的某个字段的值。引用的表就是外键表,被引用的表就是主键表
– 1.建立引用的字段类型必须一致
– 2.建立引用的字段的意义一样
– 3.建立主外键关系的时候选择 外键表 去建立主外键关系
– 4.建立主外键关系的字段在主表中必须是主键或者唯一键
– 5.对于操作的影响 :
– 1.在添加数据时,先添加主键表再添加外键表数据
– 2.在删除的时候先外键表数据再删除主键表数据
– 级联的操作:不建议使用:会破坏数据完整性
– 不执行任何操作:该报错就报错,该删除就删除
– 级联:删除主表记录,从表引用该值的记录也被删除
– 设置null:删除主表记录,从表对应的字段值设置为null,前提是可以为null
– 设置为default:删除主表记录,从表对应的字段值设置为default,前提是可以为default

主键约束(PK Primary key) 唯一键约束(UQ unique) 外键约束(FK foreign key) 默认值约束(DF default) check约束(CK check)

语法:
alter table 表名
add constraint 前缀_约束名称 约束类型 约束说明(字段 关系表达式 值)

use School
if exists(select * from sysobjects where name='PK_Classes_Classid')
 alter table classes  drop constraint PK_Classes_Classid
alter table classes 
add constraint PK_Classes_Classid primary key(classid)
--为id添加主键
alter table teacher 
add constraint PK_teacher_id primary key(id)
--为name添加唯一键
alter table teacher
add constraint UQ_Teacher_Name unique(Name)
--同时创建salary的默认约束和age的check约束
alter table teacher
add constraint DF_Teacher_Salary default(5000) for salary,
constraint CK_Teacher_Age check(age>0 and age<=100)
--为teacher表的classid字段创建主外键
if exists(select * from sysobjects where name='FK_Teacher_Classes_Classid')
 alter table teacher  drop constraint FK_Teacher_Classes_Classid
alter table teacher
with nocheck --不检查现有数据
add constraint FK_Teacher_Classes_Classid foreign key(classid) references classes(classid)
--on delete set default  级联操作
--不执行任何操作:该报错就报错,该删除就删除  --no action --默认选择
--级联:删除主表记录,从表引用该值的记录也被删除 --cascade
--设置null:删除主表记录,从表对应的字段值设置为null,前提是可以为null   --set null
--设置为default:删除主表记录,从表对应的字段值设置为default,前提是可以为default  --set default

5.四中基本字符类型说明

--len(参数) --获取指定参数内容的字符个数
select LEN('abcd') 【4】运行结果
select LEN('中华人民共和国') 【7】

--DataLength(参数):获取指定内占据的字节数--空间大小
select DataLength('abcd') 【4】
select DataLength('中华人民共和国') 【14】

--char类型:当空间分配后,不会因为存储的内容比分配的空间小就回收分配的空间。但是如果存储的内容超出了指定的空间大小,就会报错,当你存储的内容的长度变化区间不大的时候可以考虑使用char
select LEN(char) from CharTest 【2】
select DataLength(char) from CharTest 【10】

--varchar  var--变化的:当你存储的内容小于分配的空间的时候,多余的空间会自动收缩。但是如果存储的内容超出了指定的空间大小,就会报错 当存储的内容波动区间比较大时候使用varchar
select LEN(varchar) from CharTest 【2】
select DataLength(varchar) from CharTest 【2】

--nchar--  n代表它是一个unicode字符。规定不管什么样的字符都占据两个字节。  char:空间是固定的
select LEN(nchar) from CharTest 【10】
select DataLength(nchar) from CharTest 【20】

--nvarchar  n  var  char 
select LEN(nvarchar) from CharTest 【2】
select DataLength(nvarchar) from CharTest 【4】

6.SQL基本语句

数据插入
调用方法 一 一对应原则:类型对应,数量对应,顺序对应

语法: 形参 实参
insert into 表名([字段列表]) values(值列表) –数据必须要符合数据完整性
插入操作是单个表的操作
插入操作insert一次只能插入一条记录

use School
--插入teacher所有字段的数据.如果在表后没有指定需要插入的字段名称,那么就默认为所有字段添加值
--但是一定需要注意的是:标识列永远不能自定义值--不能人为插入值
--仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'Teacher'中的标识列指定显式值。
insert into Teacher values('张三',5,1,30,4000,'1984-9-11')
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values('张三',5,1,30,4000,'1984-9-11')
--不为可以为null的字段插入值  :可以null的字段可以不赋值 
--列名或所提供值的数目与表定义不匹配
insert into Teacher(Name,ClassId,Gender,Age,Salary) values('李四',5,1,30,4000)
--非空字段一定需要赋值 :不能将值 NULL 插入列 'Gender',表 'School.dbo.Teacher';列不允许有 Null 值。INSERT 失败
insert into Teacher(Name,ClassId,Age,Salary) values('李四',5,30,4000)
--为有默认值的字段插入值:
--1.不写这一列让系统自动赋值
insert into Teacher(Name,ClassId,Gender,Age) values('王五',5,1,30)
--指定 null或者default
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values('赵六',5,1,30,default,null)
--数据必须完全符合表的完整性约束
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values('赵六1',5,1,300,default,null)

--任意类型的数据都可以包含在''以内,     不包括关键字
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values('马鹏飞','5','0','15',default,null)
--但是字符串值如果没有包含在''以内.会报错   列名 '兰鹏' 无效。
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values('兰鹏','5','0','15',default,null)
--但是数值组成的字符串可以不使用''包含
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values(123,'5','0','15',default,null)
--日期值必须包含在’‘以内,否则就是默认值
insert into Teacher(Name,ClassId,Gender,Age,Salary,Birthday) values('邹元标2','5','0','15',default,'1991-9-11')

数据删除
语法:
delete [from] 表名 where 条件

delete from Teacher where Age<20
--特点:
--1.删除是一条一条进行删除的
--2.每一条记录的删除都需要将操作写入到日志文件中
--3.标识列不会从种子值重新计算,以从上次最后一条标识列值往下计算
--4.这种删除可以触发delete触发器

--truncate table 表名 --没有条件,它是一次性删除所有数据
--特点:
--1.一次性删除所有数据,没有条件,那么日志文件只以最小化的数据写入
--2.它可以使用标识列从种子值重新计算
--3.它不能触发delete触发器
truncate table teacher

数据更新(数据修改):一定需要考虑是否有条件

语法:
update 表名 set 字段=值,字段=值 。。where 条件

update Teacher set Gender='true'
--修改时添加条件
update Teacher set Gender=0 where Id=20
--多字段修改
update Teacher set ClassId=4,Age+=5,Salary=5000 where Id=22
--修改班级id=4,同时年龄》20岁的人员工资+500
update Teacher set Salary=Salary+500 where ClassId=4 and Age>20

数据检索–查询

语法: *代表所有字段
select */字段名称列表 from 表列表

select StudentNo,StudentName,Sex,[Address] from Student
--可以为标题设置  别名,别名可以是中文别名
select StudentNo as 学号,StudentName 姓名,性别=Sex,[Address] from Student
--添加常量列
select StudentNo as 学号,StudentName 姓名,性别=Sex,[Address] ,国籍='中华人民共和国' from Student
--select的作用
--1.查询
--2.输出
select 1+1
--+是运算符,系统会自动为你做类型转换
select 1+'1'
select '1'+1
--如果+两边都是字符串,那么它就是一字符串连接符
select '1'+'1'
select 'a'+1
--可以输出多列值
select 1,2,34,3,545,67,567,6,7
--Top、Distinct
select * from Student
--top可以获取指定的记录数,值可以大于总记录数.但是不能是负值
select top 100 * from Student
--百分比是取ceiling()
select top 10 percent * from Student

--重复记录与原始的数据表数据无关,只与你查询的结果集有关系 distinct可以去除结果集中的重复记录--结果集中每一列的值都一样
select distinct LoginPwd,Sex,Email from Student
select distinct Sex from Student
--聚合函数:
--1.对null过滤
--2.都需要有一个参数
--3.都是返回一个数值
--sum():求和:只能对数值而言,对字符串和日期无效
--avg():求平均值
--count():计数:得到满足条件的记录数
--max():求最大值:可以对任意类型的数据进行聚合,如果是字符串就比较拼音字母进行排序
--min():求最小值
--获取学员总人数
select COUNT(*) from Student
--查询最大年龄值
select  MIN(BornDate) from Student
select  max(BornDate) from Student

--查询总分
select SUM(StudentResult) from Result where StudentNo=2
--平均分
select avg(StudentResult) from Result where SubjectId=1
--注意细节:
select  SUM(StudentName) from Student
select  SUM(BornDate) from Student

select  min(StudentName) from Student
select  max(StudentName) from Student

--查询学号,姓名,性别,年龄,电话,地址 ---查询女生
select StudentNo,StudentName,Sex,BornDate,Address from Student where Sex='女' and BornDate >'1990-1-1' and Address='广州传智播客'
--指定区间范围
select StudentNo,StudentName,Sex,BornDate,Address from Student where  BornDate >='1990-1-1' and BornDate<='1993-1-1'
--between...and  >=  <=
select StudentNo,StudentName,Sex,BornDate,Address from Student where BornDate  between '1990-1-1' and '1993-1-1'
--查询班级id  1  3 5  7的学员信息
select * from Student where ClassId=1 or ClassId=3 or ClassId=5 or ClassId=7
--指定具体的取值范围--可以是任意类型的范围.值的类型需要一致--可以相互转换
select * from Student where ClassId in(1,3,'5',7)
select * from Student where ClassId not in(1,3,'5',7)
--带条件的查询-模糊查询-- 只针对字符串而言

--查询  姓 林 的女生信息
--=是一种精确查询,需要完全匹配
select * from Student where Sex='女' and StudentName='林'
--通配符--元字符
--%:任意个任意字段  window:*  正则表达式 :.*
--_:任意的单个字符
--[]:代表一个指定的范围,范围可以是连续也可以是间断的。与正则表达式完全一样[0-9a-zA-Z].可以从这个范围中取一个字符
--[^]:取反值
select * from Student where Sex='女' and StudentName='林%'
--通配符必须在模糊查询关键的中才可以做为通配符使用,否则就是普通字符
--like   像 。。。。一样
select * from Student where Sex='女' and StudentName  like '林%'
select * from Student where Sex='女' and StudentName  like '林_'
--[]的使用  学号在11~15之间的学员信息
select * from Student where StudentNo like '[13579]'

---处理null值
--null:不是地址没有分配,而是不知道你需要存储什么值  所以null是指   不知道。但是=只能匹配具体的值,而null根本就不是一个值
select COUNT(email) from Student where Email !=null
select COUNT(email) from Student where Email  is null
select count(email) from Student where Email  is not null
--将null值替换为指定的字符串值
select StudentName,ISNULL(Email,'没有填写电子邮箱') from Student where ClassId=2
--当你看到  每一个,,各自,不同,,分别  需要考虑分组
--查询每一个班级的男生人数
--与聚合函数一起出现在查询中的列,要么也被聚合,要么被分组
select classid,Sex,COUNT(*) from Student where Sex='男' group by ClassId,sex
--查询每一个班级的总人数,显示人数>=2的信息
--1.聚合不应出现在 WHERE 子句中--语法错误
select ClassId ,COUNT(*) as num from Student where Email is not null   GROUP by ClassId having COUNT(*)>=2 order by num desc
--完整的sql查询家庭
 --5                            1                      2                                 3                                     4                                           6                                                 
--select 字段列表 from 表列表  where 数据源做筛选 group by 分组字段列表 having 分组结果集做筛选 Order by  对结果集做记录重排

select ClassId ,COUNT(*) as num from Student where Email is not null   GROUP by ClassId order by ClassId desc

--关于top的执行顺序 排序之后再取top值
select top 1 ClassId ,COUNT(*) as num from Student  GROUP by ClassId  order by num desc

7.类型转换函数

--select :输出为结果集--虚拟表
--print:以文本形式输出  只能输出一个字符串值.

print 1+'a'
select 1,2

select * from Student

--类型转换
--Convert(目标类型,源数据,[格式]) --日期有格式
print '我的成绩是:'+convert(char(3),100)

print '今天是个大日子:'+convert(varchar(30),getdate(),120)
select getdate()
select len(getdate())

--cast(源数据  as  目标类型)  它没有格式
print '我的成绩是:'+cast(100 as char(3))

8.日期函数

--getdate():获取当前服务器日期
select GETDATE()
--可以在源日期值是追加指定时间间隔的日期数
select DATEADD(dd,-90,GETDATE())
--dateDiff:找到两个日期之间指定格式的差异值
select StudentName,DATEDIFF(yyyy,getdate(),BornDate) as age from Student order by  age
--DATENAME:可以获取日期的指定格式的字符串表现形式
select DATENAME(dw,getdate())
--DATEPART:可以获取指定的日期部分
select cast(DATEPART(yyyy,getdate()) as CHAR(4))+'-' +cast(DATEPART(mm,getdate()) as CHAR(2))+'-' +cast(DATEPART(dd,getdate()) as CHAR(2))

9.数学函数

--rand:随机数:返回0到1之间的数,理论上说可以返回0但是不能返回1
select RAND()
--abs:absolute:取绝对值
select ABS(-100)
--ceiling:获取比当前数大的最小整数
select CEILING(1.00)
--floor:获取比当前数小的最大整数
select floor(1.99999)
power:
select POWER(3,4)
--round():四舍五入.只关注指定位数后一位
select ROUND(1.549,1)
--sign:正数==1  负数 ==-1  0=0
select SIGN(-100)

select ceiling(17*1.0/5)

10.字符串函数

--1.CHARINDEX --IndexOf():能够返回一个字符串在源字符串的起始位置。找不到就返回0,如果可以找到就返回从1开始的索引--没有数组的概念
--第一个参数是指需要查询的字符串,第二个是源字符串,第三个参数是指从源字符的那个索引位置开始查找
select CHARINDEX('人民','中华人民共和国人民',4)
--LEN():可以返回指定字符串的字符个数
select LEN('中华人民共和国')
--UPPER():小写字母转换为大写字母  LOWER():大写转小写
select LOWER(UPPER('sadfasdfa'))
--LTRIM:去除左空格  RTIRM:去除右空格
select lTRIM(RTRIM('                   sdfsd             '))+'a'
--RIGHT:可以从字符串右边开始截取指定位数的字符串  如果数值走出范围,不会报错,只会返回所有字符串值,但是不能是负值
select RIGHT('中华人民共和国',40)
select LEFT('中华人民共和国',2)
--SUBSTRING()
select SUBSTRING('中华人民共和国',3,2)
--REPLACE 第一个参数是源字符串,第二个参数是需要替换的字符串,第三个参数是需要替换为什么
select REPLACE('中华人民共和国','人民','居民')
select REPLACE('中        华      人民       共        和       国',' ','')
--STUFF:将源字符串中从第几个开始,一共几个字符串替换为指定的字符串
select STUFF('中华人民共和国',3,2,'你懂的')

--sudyfsagfyas@12fasdf6.fsadfdsaf

declare <a href="http://www.jobbole.com/members/Email">@email</a> varchar(50)='sudyfsagfyas@12fasdf6.fsadfdsaf'
select CHARINDEX('@',@email)
select LEFT(@email,CHARINDEX('@',@email)-1)

--使用right
select right(@email,len(@email)-CHARINDEX('@',@email))
--使用substring
select SUBSTRING(@email,CHARINDEX('@',@email)+1,LEN(@email))
--使用stuff
select STUFF(@email,1,CHARINDEX('@',@email),'')

11.联合结果集union

--联合结果集union
select * from Student where Sex='男'
--union
select * from Student where Sex='女'

--联合的前提是:
--1.列的数量需要一致:使用 UNION、INTERSECT 或 EXCEPT 运算符合并的所有查询必须在其目标列表中有相同数目的表达式
--2.列的类型需要可以相互转换
select StudentName,Sex from Student --在字符串排序的时候,空格是最小的,排列在最前面
union
select cast(ClassId as CHAR(3)),classname from grade

--union和union all的区别
--union是去除重复记录的
--union all不去除重复 :效率更高,因为不需要判断记录是否重复,也没有必须在结果庥是执行去除重复记录的操作。但是可以需要消耗更多的内存存储空间
select * from Student where ClassId=2
union all
select * from Student where ClassId=2

--查询office这科目的全体学员的成绩,同时在最后显示它的平均分,最高分,最低分
select ' '+cast(StudentNo as CHAR(3)),cast(SubjectId as CHAR(2)),StudentResult from Result where SubjectId=1
union
select '1','平均分',AVG(StudentResult) from Result where SubjectId=1
union
select '1','最高分',max(StudentResult) from Result where SubjectId=1
union
select '1','最低分',min(StudentResult) from Result where SubjectId=1

--一次性插入多条数据
--1.先将数据复制到另外一个新表中,删除源数据表,再将新表的数据插入到源数据表中
--1.select */字段  into 新表 from 源表
--1.新表是系统自动生成的,不能人为创建,如果新表名称已经存在就报错
--2.新表的表结构与查询语句所获取的列一致,但是列的属性消失,只保留非空和标识列。其它全部消失,如主键,唯一键,关系,约束,默认值
select * into newGrade from grade

truncate table grade
select *  from newGrade
--select * into grade from newGrade
--2.insert into  目标表  select 字段列表/* from  数据源表
--1、目标表必须先存在,如果没有就报错
--2.查询的数据必须符合目标表的数据完整性
--3.查询的数据列的数量和类型必须的目标的列的数量和对象完全对应
insert into grade select classname from newGrade
delete from admin
--使用union一次性插入多条记录
--insert into 表(字段列表)
--select 值。。。。 用户自定义数据
--union
--select 值 。。。。
insert into Admin
select 'a','a'
union all
select 'a','a'
union all
select 'a','a'
union all
select 'a',null

12.CASE函数用法

相当于switch case—c#中的switch…case只能做等值判断
这可以对字段值或者表达式进行判断,返回一个用户自定义的值,它会生成一个新列
2.要求then后面数据的类型一致
1.第一种做等值判断的case..end
case 字段或者表达式
when .值..then .自定义值
when .值..then .自定义值
…..
else 如果不满足上面所有的when就满足这个else
end

--显示具体班级的名称
select StudentNo,StudentName,
case ClassId  --如果case后面接有表达式或者字段,那么这种结构就只能做等值判断,真的相当于switch..case
  when 1 then '一班'
  when 2 then '2班' 
  when 3 then '3班' 
  when null  then 'aa' --不能判断null值
  else  '搞不清白'
end,
sex
 from Student
--2.做范围判断,相当于if..else,它可以做null值判断
--case  --如果没有表达式或者字段就可实现范围判断
-- when  表达式  then 值   --不要求表达式对同一字段进行判断
-- when  表达式  then 值  
-- .....
--else  其它情况  
--end
select StudentNo,StudentName,
case
 when BornDate>'2000-1-1' then '小屁孩'
 when BornDate>'1990-1-1' then '小青年' 
 when BornDate>'1980-1-1' then '青年'  
 --when Sex='女'  then '是女的'
 when BornDate is null then '出生不详'
 else  '中年'
end
 from Student

--百分制转换为素质教育  90 -A   80--B  70 --C  60 --D  <60 E  NULL--没有参加考试
select StudentNo,SubjectId,
case
    when StudentResult>=90 then 'A'
    when StudentResult>=80 then 'B'
    when StudentResult>=70 then 'C'
    when StudentResult>=60 then 'D'
    when StudentResult is null then '没有参加考试'
    else 'E'
end 成绩,
ExamDate
 from Result

13.IF ELSE语法

1,.没有{},使用begin..end.如果后面只有一句,可以不使用begin..end包含
2.没有bool值,只能使用关系运算符表达式
3.也可以嵌套和多重
4.if后面的()可以省略

declare @subjectname nvarchar(50)='office' --科目名称
declare @subjectId int=(select Subjectid from Subject where SubjectName=@subjectname) --科目ID
declare @avg int --平均分
set @avg=(select AVG(StudentResult) from Result where SubjectId=@subjectId and StudentResult is not null) --获取平均分
print @avg
if @avg>=60
 begin
   print '成绩不错,输出前三名:' 
   select top 3 * from Result where SubjectId=@subjectId order by StudentResult desc 
 end 
else
  begin
    print '成绩不好,输出后三名:' 
    select top 3 * from Result where SubjectId=@subjectId order by StudentResult  
  end

14.WHILE循环语法

没有{},使用begin..end
没有bool值,需要使用条件表达式
可以嵌套
也可以使用break,continue

go
declare @subjectName nvarchar(50)='office' --科目名称
declare @subjectId int--科目ID
declare @classid int =(select classid from Subject where SubjectName=@subjectName) --查询当前科目属于那一个班级
set @subjectId=(select SubjectId from Subject where SubjectName=@subjectName) --获取科目ID
declare @totalCount int --总人数 :那一个班级需要考试这一科目 
set @totalCount=(select COUNT(*) from Student where ClassId=@classid)
print @totalcount  --14
declare @unpassNum int --不及格人数
set @unpassNum=(select COUNT(distinct Studentno) from Result where SubjectId=@subjectId and StudentNo in(select StudentNo from Student where ClassId=@classid) and StudentResult<60)
while(@unpassNum>@totalCount/2)
begin
 --执行循环加分
 update Result set StudentResult+=2 where SubjectId=@subjectId and StudentNo in(select StudentNo from Student where ClassId=@classid) and StudentResult<=98
  --重新计算不及格人数
  set @unpassNum=(select COUNT(distinct Studentno) from Result where SubjectId=@subjectId and StudentNo in(select StudentNo from   Student where ClassId=@classid) and StudentResult<60)
end

go
declare @subjectName nvarchar(50)='office' --科目名称
declare @subjectId int--科目ID
declare @classid int =(select classid from Subject where SubjectName=@subjectName) --查询当前科目属于那一个班级
set @subjectId=(select SubjectId from Subject where SubjectName=@subjectName) --获取科目ID
declare @totalCount int --总人数
set @totalCount=(select COUNT(*) from Student where ClassId=@classid)
print @totalcount  --14
declare @unpassNum int --不及格人数
while(1=1)
 begin
     set @unpassNum=(select COUNT(distinct Studentno) from Result where SubjectId=@subjectId and StudentNo in(select StudentNo   from   Student where ClassId=@classid) and StudentResult<60)
    if(@unpassNum>@totalCount/2)     
        update Result set StudentResult+=2 where SubjectId=@subjectId and StudentNo in(select StudentNo from Student where ClassId=@classid) and StudentResult<=98
    else
         break
 end

15.子查询

子查询–一个查询中包含另外一个查询。被包含的查询就称为子查询,。包含它的查询就称父查询
1.子查询的使用方式:使用()包含子查询
2.子查询分类:

1.独立子查询:子查询可以直接独立运行
查询比“王八”年龄大的学员信息
select * from Student where BornDate<(select BornDate from Student where StudentName=’王八’)
2.相关子查询:子查询使用了父查询中的结果

--子查询的三种使用方式
--1.子查询做为条件,子查询接在关系运算符后面  >  < >= <= = <> !=,如果是接这关系运算符后面,必须保证 子查询只返回一个值
--查询六期班的学员信息
select * from Student where ClassId=(select ClassId from grade where classname='八期班')
--子查询返回的值不止一个。当子查询跟随在 =、!=、<、<=、>、>= 之后,或子查询用作表达式时,这种情况是不允许的。
select * from Student where ClassId=(select ClassId from grade)
--查询八期班以外的学员信息
--当子查询返回多个值(多行一列),可以使用in来指定这个范围
select * from Student where ClassId in(select ClassId from grade where classname<>'八期班')
--当没有用 EXISTS 引入子查询时,在选择列表中只能指定一个表达式。如果是多行多列或者一行多列就需要使用exists
--使用 EXISTS 关键字引入子查询后,子查询的作用就相当于进行存在测试。外部查询的 WHERE 子句测试子查询返回的行是否存在
select * from Student where  EXISTS(select * from grade)
select * from Student where  ClassId in(select * from grade)

--2.子查询做为结果集--
select top 5 * from Student --前五条
--使用top分页
select top 5 * from Student where StudentNo not in(select top 5 studentno from Student)
--使用函数分页  ROW_NUMBER() over(order by studentno),可以生成行号,排序的原因是因为不同的排序方式获取的记录顺序不一样
select ROW_NUMBER() over(order by studentno),* from Student
--查询拥有新生成行号的结果集  注意:1.子查询必须的别名  2.必须为子查询中所有字段命名,也就意味着需要为新生成的行号列命名
select * from (select ROW_NUMBER() over(order by studentno) id,* from Student) temp where temp.id>0 and temp.id<=5
select * from (select ROW_NUMBER() over(order by studentno) id,* from Student) temp where temp.id>5 and temp.id<=10
select * from (select ROW_NUMBER() over(order by studentno) id,* from Student) temp where temp.id>10 and temp.id<=15

--3.子查询还可以做为列的值
select (select studentname from student where studentno=result.studentno),(select subjectname from subject where subjectid=result.SubjectId), StudentResult from Result

--使用Row_number over()实现分页
--1.先写出有行号的结果集
select ROW_NUMBER() over(order by studentno),* from Student
--2.查询有行号的结果集 子查询做为结果集必须添加别名,子查询的列必须都有名称
select * from (select ROW_NUMBER() over(order by studentno) id,* from Student) temp where id>0 and id<=5
--查询年龄比“廖杨”大的学员,显示这些学员的信息
select * from Student where BornDate<(select BornDate from Student where StudentName='廖杨')
--查询二期班开设的课程
select * from Subject where ClassId=(select ClassId from grade where classname='二期班')
--查询参加最近一次“office”考试成绩最高分和最低分
--1查询出科目 ID
select subjectid from Subject where SubjectName='office'
--2.查询出这一科目的考试日期
select MAX(ExamDate) from Result where SubjectId=(select subjectid from Subject where SubjectName='office')
--3,写出查询的框架
select MAX(StudentResult),MIN(StudentResult) from Result where SubjectId=() and ExamDate=()
--4.使用子查询做为条件
select MAX(StudentResult),MIN(StudentResult) from Result where SubjectId=(
    select subjectid from Subject where SubjectName='office'
        ) and ExamDate=(
                select MAX(ExamDate) from Result where SubjectId=(
                        select subjectid from Subject where SubjectName='office'
                        )
                    )

16.表连接Join

--1.inner join :能够找到两个表中建立连接字段值相等的记录
--查询学员信息显示班级名称
select Student.StudentNo,Student.StudentName,grade.classname
from Student
inner join grade on Student.ClassId=grade.ClassId
--左连接: 关键字前面的表是左表,后面的表是右表
--左连接可以得到左表所有数据,如果建立关联的字段值在右表中不存在,那么右表的数据就以null值替换
select PhoneNum.*,PhoneType.*
from   PhoneNum  
left join  PhoneType on PhoneNum.pTypeId=PhoneType.ptId
--右连接: 关键字前面的表是左表,后面的表是右表
--右连接可以得到右表所有数据,如果建立关联的字段值在右左表中不存在,那么左表的数据就以null值替换
select PhoneNum.*,PhoneType.*
from   PhoneNum  
right join  PhoneType on PhoneNum.pTypeId=PhoneType.ptId
--full join :可以得到左右连接的综合结果--去重复
select PhoneNum.*,PhoneType.*
from   PhoneNum  
full join  PhoneType on PhoneNum.pTypeId=PhoneType.ptId

17.事务

一种处理机制。以事务处理的操作,要么都能成功执行,要么都不执行

事务的四个特点 ACID:
A:原子性:事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。它是一个整体,不能再拆分
C:一致性:事务在完成时,必须使所有的数据都保持一致状态。。某种程度的一致
I:隔离性:事务中隔离,每一个事务是单独的请求将单独的处理,与其它事务没有关系,互不影响
D:持久性:如果事务一旦提交,就对数据的修改永久保留

使用事务:
将你需要操作的sql命令包含在事务中
1.在事务的开启和事务的提交之间
2.在事务的开启和事务的回滚之间

三个关键语句:
开启事务:begin transaction
提交事务:commit transaction
回滚事务:rollback transaction

declare @num int =0 --记录操作过程中可能出现的错误号
begin transaction
  update bank set cmoney=cmoney-500 where name='aa'
  set @num=@num+@@ERROR
  --说明这一句的执行有错误  但是不能在语句执行的过程中进行提交或者回滚
  --语句块是一个整体,如果其中一句进行了提交或者回滚,那么后面的语句就不再属于当前事务,
  --事务不能控制后面的语句的执行
  update bank set cmoney=cmoney+500 where name='bb'
  set @num=@num+@@ERROR
  select * from bank
   if(@num<>0 )  --这个@@ERROR只能得到最近一一条sql语句的错误号
     begin 
     print '操作过程中有错误,操作将回滚' 
     rollback transaction
    end 
   else 
     begin  
     print '操作成功' 
     commit transaction  
    end 

    --事务一旦开启,就必须提交或者回滚
    --事务如果有提交或者回滚,必须保证它已经开启

18.视图

视图就是一张虚拟表,可以像使用子查询做为结果集一样使用视图
select * from vw_getinfo
使用代码创建视图

语法:
create view vw_自定义名称
as
查询命令
go

--查询所有学员信息
if exists(select * from sysobjects where name='vw_getAllStuInfo')
 drop view vw_getAllStuInfo
go --上一个批处理结果的标记
create view vw_getAllStuInfo
as
--可以通过聚合函数获取所以记录数
 select top (select COUNT(*) from Student) Student.StudentNo,Student.StudentName,grade.ClassId,grade.classname from Student
inner join grade on Student.ClassId=grade.ClassId  order by StudentName --视图中不能使用order by
--select * from grade --只能创建一个查询语句
--delete from grade where ClassId>100 --在视图中不能包含增加删除修改
go

--使用视图。。就像使用表一样
select * from vw_getAllStuInfo 
--对视图进行增加删除和修改操作--可以对视图进行增加删除和修改操作,只是建议不要这么做:所发可以看到:如果操作针对单个表就可以成功,但是如果 多张的数据就会报错:不可更新,因为修改会影响多个基表。
update vw_getAllStuInfo set classname='asdas' ,studentname='aa' where studentno=1

19.触发器

触发器:执行一个可以改变表数据的操作(增加删除和修改),会自动触发另外一系列(类似于存储过程中的模块)的操作。

语法:
create trigger tr_表名_操作名称
on 表名 after|instead of 操作名称
as
go

if exists(select * from sysobjects where name='tr_grade_insert')
 drop trigger tr_grade_insert
go
create trigger tr_grade_insert
on grade for  insert  ---为grade表创建名称为tr_grade_insert的触发器,在执行insert操作之后触发
as
declare @cnt int 
set @cnt = (select count(*) from student)
 select * ,@cnt from student
select * from grade 
go
--触发器不是被调用的,而是被某一个操作触 发的,意味着执行某一个操作就会自动触发 触发器
insert into grade values('fasdfdssa')
---替换触 发器:本来需要执行某一个操作,结果不做了,使用触 发器中的代码语句块进行替代

if exists(select * from sysobjects where name='tr_grade_insert')
 drop trigger tr_grade_insert
go
create trigger tr_grade_insert
on grade instead of insert  ---为grade表创建名称为tr_grade_insert的触发器,在执行insert操作之后触发
as
declare @cnt int 
set @cnt = (select count(*) from student)
 select * ,@cnt from student
select * from grade 
go

insert into grade values('aaaaaaaaaaaa')
go

---触 发器的两个临时表:
--inserted: 操作之后的新表:所有新表与原始的物理表没有关系,只与当前操作的数据有关
--deleted:操作之前的旧表:所有新表与原始的物理表没有关系,只与当前操作的数据有关

if exists(select * from sysobjects where name='tr_grade_insert')
 drop trigger tr_grade_insert
go
create trigger tr_grade_insert
on grade after insert 
as
 print '操作之前的表:操作之前,这一条记录还没有插入,所以没有数据'
 select * from deleted 
 print '操作之后的表:已经成功插入一条记录,所有新表中有一条记录'
 select * from inserted  
go
--测试:
insert into grade values('aaaaa')

if exists(select * from sysobjects where name='tr_grade_update')
 drop trigger tr_grade_update
go
create trigger tr_grade_update
on grade after update 
as
 print '操作之前的表:存储与这个修改操作相关的没有被修改之前的记录'
 select * from deleted 
 print '操作之后的表:存储这个操作相关的被修改之后 记录'
 select * from inserted  
go
--测试
update grade set classname=classname+'aa' where  ClassId>15

if exists(select * from sysobjects where name='tr_grade_delete')
 drop trigger tr_grade_delete
go
create trigger tr_grade_delete
on grade after delete 
as
 print '操作之前的表:存储与这个修改操作相关的没有被删除之前的记录'
 select * from deleted 
 print '操作之后的表:存储这个操作相关的被删除之后 记录--没有记录'
 select * from inserted  
go

--测试
delete from grade where ClassId>15

20.存储过程

存储过程就相当于c#中的方法
参数,返回值,参数默认值,参数:值的方式调用
在调用的时候有三个对应:类型对应,数量对应,顺序对应

创建语法:
create proc usp_用户自定义名称
对应方法的形参 –(int age, out string name)
as
对应方法体:创建变量,逻辑语句,增加删除修改和查询..return返回值
go

调用语法:
exec 存储过程名称 实参,实参,实参 …

--获取所有学员信息
if exists(select * from sysobjects where name='usp_getAllStuInfo')
 drop proc usp_getAllStuInfo 
go 
create procedure usp_getAllStuInfo
as
 select * from Student
go 
--调用存储过程,获取的有学员信息
execute usp_getAllStuInfo

--exec sp_executesql  'select * from Student'

--查询指定性别的学员信息
go
if exists(select * from sysobjects where name='usp_getAllStuInfoBySex')
 drop proc usp_getAllStuInfoBySex 
go 
create procedure usp_getAllStuInfoBySex
 @sex nchar(1) --性别  参数不需要declare
as
 select * from Student where Sex=@sex
go
--调用存储过程,获取指定性别的学员信息
Exec usp_getAllStuInfoBySex '女'

--创建存储过程获取指定班级和性别的学员信息
go
if exists(select * from sysobjects where name='usp_getAllStuInfoBySexandClassName')
 drop proc usp_getAllStuInfoBySexandClassName 
go 
create procedure usp_getAllStuInfoBySexandClassName
 @classname nvarchar(50), --班级名称 
 @sex nchar(1)='男'--性别   有默认的参数建议写在参数列表的最后
as
 declare  @classid int ---班级ID
set @classid=(select classid from grade where classname=@classname) --通过参数班级名称获取对应的班级ID 
 select * from Student where Sex=@sex and ClassId=@classid
go
--执行存储过程获取指定班级和性别的学员信息
--exec usp_getAllStuInfoBySexandClassName '八期班'
exec usp_getAllStuInfoBySexandClassName default, '八期班'  --有默认值的参数可以传递default
exec usp_getAllStuInfoBySexandClassName @classname='八期班'    --也可以通过参数=值的方式调用
exec usp_getAllStuInfoBySexandClassName @classname='八期班'  ,@sex='女'
exec usp_getAllStuInfoBySexandClassName @classname='八期班',@sex='女'

--创建存储过程,获取指定性别的学员人数及总人数
go
if exists(select * from sysobjects where name='usp_getCountBySexandClassName')
 drop proc usp_getCountBySexandClassName 
go 
create procedure usp_getCountBySexandClassName
@cnt int=100 output, --output标记说明它是一个输出参数。output意味着你向服务器请求这个参数的值,那么在执行的时候,服务器发现这个参数标记了output,就会将这个参数的值返回输出
@totalnum int =200output, --总人数
@className nvarchar(50), --输入参数没有默认值,在调用的时候必须传入值
@sex nchar(1)='男'--输入参数有默认值,用户可以选择是否传入值
as
 declare  @classid int ---班级ID
 set @classid=(select classid from grade where classname=@classname) --通过参数班级名称获取对应的班级ID 
 select * from Student where Sex=@sex and ClassId=@classid
set @cnt= (select COUNT(*) from Student where Sex=@sex and ClassId=@classid) --获取指定班级和性别的总人数
set @totalnum=(select COUNT(*) from Student) ----获取总人数
go
--调用存储过程,获取指定性别的学员人数及总人数
declare @num int,@tnum int
exec usp_getCountBySexandClassName @cnt=@num output ,@totalnum=@tnum output , @className='八期班'
print @num
print @tnum
print '做完了'
---获取指定班级的人数
if exists(select * from sysobjects where name='usp_getCount')
 drop proc usp_getCount 
go 
create procedure usp_getCount
 @className nvarchar(50)='八期班'
as
declare @classid int=(select classid from grade where classname=@className)
 declare @cnt int
set @cnt =(select COUNT(*) from Student where ClassId=@classid) 
--return 只能返回int整数值
--return '总人数是'+cast(@cnt as varchar(2))
return @cnt 
go

--调用存储过程,接收存储过程的返回值
declare @count int
--set @count=(exec usp_getCount)
exec @count=usp_getCount '八期班'
print @count

if exists(select * from sysobjects where name='usp_getClassList')
 drop proc usp_getClassList 
go 
create procedure usp_getClassList
as
 select classid,classname from grade
go

21.分页存储过程

if exists(select * from sysobjects where name='usp_getPageData')
 drop proc usp_getPageData 
go 
create procedure usp_getPageData
@totalPage int output,--总页数
@pageIndex int =1 ,--当前页码,默认是第一页
@pageCount int =5 --每一页显示的记录数
as
select * from (select ROW_NUMBER() over(order by studentno) id,* from Student) temp where temp.id>(@pageindex-1)*@pagecount and temp.id<=(@pageindex*@pagecount)
set @totalPage=CEILING((select COUNT(*) from Student)*1.0/@pageCount)
go

22.索引

select * from sysindexes

--create  index IX_Student_studentName
--on 表名(字段名)

--clustered index:聚集索引  nonclustered index--非聚集索引
if exists(select * from sysindexes where name='IX_Student_studentName')
 drop index student.IX_Student_studentName
go 
create clustered index IX_Student_studentName
on student(studentname)

--如果是先创建主键再创建聚集索引就不可以,因为主键默认就是聚集索引
--但是如果先创建聚集索引,那么还可以再创建主键,因为主键不一定需要是聚集的

23.临时表

--创建局部临时表
create table #newGrade
(
 classid int ,
 classname nvarchar(50) 
)
---局部临时表只有在当前创建它的会话中使用,离开这个会话临时表就失效.如果关闭创建它的会话,那么临时表就会消失
insert into #newGrade select * from  grade 
select * from #newGrade
select * into #newnewnew from grade
select * into newGrade from #newgrade

--创建全局临时表:只要不关闭当前会话,全局临时表都可以使用,但是关闭当前会话,全局临时表也会消失
create table ##newGrade
(
 classid int ,
 classname nvarchar(50) 
)
drop table ##newGrade
select * into ##newGrade from grade
select * from ##newGrade

--创建表变量
declare @tb table(cid int,cname nvarchar(50))
insert into @tb select * from grade
select * from @tb

史上最全的MSSQL复习笔记,首发于博客 - 伯乐在线

10 Sep 04:31

Apache Spark 1.5发布,新特性一览

by 梁堰波

Apache Spark是一个围绕速度、易用性和复杂分析构建的大数据处理框架。最初在2009年由加州大学伯克利分校的AMPLab开发,并于2010年成为Apache的开源项目之一。Apache Spark社区刚刚发布了1.5版本,明略数据高级工程师梁堰波解析了该版本中的众多新特性,同时梁堰波也是QCon上海《基于大数据的机器学习技术》专题的讲师,他将分享《基于机器学习的银行卡消费数据预测与推荐》的专题演讲。

DataFrame执行后端优化(Tungsten第一阶段)

DataFrame可以说是整个Spark项目最核心的部分,在Spark 1.5这个开发周期内最大的变化就是Tungsten项目的第一阶段已经完成。主要的变化是由Spark自己来管理内存而不是使用JVM,这样可以避免JVM GC带来的性能损失。内存中的Java对象被存储成Spark自己的二进制格式,计算直接发生在二进制格式上,省去了序列化和反序列化时间。同时这种格式也更加紧凑,节省内存空间,而且能更好的估计数据量大小和内存使用情况。如果大家对这部分的代码感兴趣,可以在源代码里面搜索那些Unsafe开头的类即可。在1.4版本只提供UnsafeShuffleManager等少数功能,剩下的大部分都是1.5版本新加入的功能。

其它优化还包括默认使用code generation,cache-aware算法对join、aggregation、shuffle、sorting的增强,window function性能的提高等。

那么性能到底能提升多少呢?可以参考DataBricks给出的这个例子。这是一个16 million行的记录,有1 million的组合键的aggregation查询分别使用Spark 1.4和1.5版本的性能对比,在这个测试中都是使用的默认配置。

那么如果我们想自己测试下Tungsten第一阶段的性能改如何测试呢?Spark 1.4以前的版本中spark.sql.codegen, spark.sql.unsafe.enabled等几个参数在1.5版本里面合并成spark.sql.tungsten.enabled并默认为true,只需要修改这一个参数就可以配置是否开启tungsten优化(默认是开启的)。

DataFrame/SQL/Hive

在DataFrame API方面,实现了新的聚合函数接口AggregateFunction2以及7个相应的build-in的聚合函数,同时基于新接口实现了相应的UDAF接口。新的聚合函数接口把一个聚合函数拆解为三个动作:initialize、update、merge,然后用户只需要定义其中的逻辑既可以实现不同的聚合函数功能。Spark的这个新的聚合函数实现方法和Impala里面非常类似。

Spark内置的expression function得到了很大的增强,实现了100多个这样的常用函数,例如string、math、unix_timestamp、from_unixtime、to_date等。同时在处理NaN值的一些特性也在增强,例如 NaN = Nan 返回true;NaN大于任何其他值等约定都越来越符合SQL界的规则了。
用户可以在执行join操作的时候指定把左边的表或者右边的表broadcast出去,因为基于cardinality的估计并不是每次都是很准的,如果用户对数据了解可以直接指定哪个表更小从而被broadcast出去。
Hive模块最大的变化是支持连接Hive 1.2版本的metastore,同时支持metastore partition pruning(通过spark.sql.hive.metastorePartitionPruning=true开启,默认为false)。因为很多公司的Hive集群都升级到了1.2以上,那么这个改进对于需要访问Hive元数据的Spark集群来说非常重要。Spark 1.5支持可以连接Hive 0.13, 0.14, 1.0/0.14.1, 1.1, 1.2的metastore。

在External Data Source方面,Parquet的支持有了很大的加强。Parquet的版本升级到1.7;更快的metadata discovery和schema merging;同时能够读取其他工具或者库生成的非标准合法的parquet文件;以及更快更鲁棒的动态分区插入。

由于Parquet升级到1.7,原来的一个重要bug被修复,所以Spark SQL的Filter Pushdown默认改为开启状态(spark.sql.parquet.filterPushdown=true),能够帮助查询过滤掉不必要的IO。

Spark 1.5可以通过指定spark.sql.parquet.output.committer.class参数选择不同的output committer类,默认是org.apache.parquet.hadoop.ParquetOutputCommitter,用户可以继承这个类实现自己的output committer。由于HDFS和S3这两种文件存储系统的区别,如果需要向S3里面写入数据,可以使用DirectParquetOutputCommitter,能够有效提高写效率,从而加快Job执行速度。

另外还有一些改动,包括:StructType支持排序功能,TimestampType的精度减小到1us,Spark现在的checkpoint是基于HDFS的,从1.5版本开始支持基于memory和local disk的checkpoint。这种类型的checkpoint性能更快,虽然不如基于HDFS的可靠,但是对于迭代型机器学习运算还是很有帮助的。

机器学习MLlib

MLlib最大的变化就是从一个机器学习的library开始转向构建一个机器学习工作流的系统,这些变化发生在ML包里面。MLlib模块下现在有两个包:MLlib和ML。ML把整个机器学习的过程抽象成Pipeline,一个Pipeline是由多个Stage组成,每个Stage是Transformer或者Estimator。

以前机器学习工程师要花费大量时间在training model之前的feature的抽取、转换等准备工作。ML提供了多个Transformer,极大提高了这些工作的效率。在1.5版本之后,已经有了25+个feature transformer,其中CountVectorizer, Discrete Cosine Transformation, MinMaxScaler, NGram, PCA, RFormula, StopWordsRemover, and VectorSlicer这些feature transformer都是1.5版本新添加的,做机器学习的朋友可以看看哪些满足你的需求。

这里面的一个亮点就是RFormula的支持,目标是使用户可以把原来用R写的机器学习程序(目前只支持GLM算法)不用修改直接搬到Spark平台上来执行。不过目前只支持集中简单的R公式(包括’.’,’~’,’+’和 ‘-‘),社区在接下来的版本中会增强这项功能。

另外越来越多的算法也作为Estimator搬到了ML下面,在1.5版本中新搬过来的有Naive Bayes、K-means、Isotonic Regression等。大家不要以为只是简单的在ML下面提供一个调用相应算法的API,这里面变换还是挺多的。例如Naive Bayes原来的模型分别用Array[Double]和Array[Array[Double]]来存储pi和theta,而在ML下面新的API里面使用的是Vector和Matrix来存储。从这也可以看出,新的ML框架下所有的数据源都是基于DataFrame,所有的模型也尽量都基于Spark的数据类型表示。在ML里面的public API下基本上看不到对RDD的直接操作了,这也与Tungsten项目的设计目标是一致的。

除了这些既有的算法在ML API下的实现,ML里面也增加了几个新算法:

  • MultilayerPerceptronClassifier(MLPC)这是一个基于前馈神经网络的分类器,它是一种在输入层与输出层之间含有一层或多层隐含结点的具有正向传播机制的神经网络模型,中间的节点使用sigmoid (logistic)函数,输出层的节点使用softmax函数。输出层的节点的数目表示分类器有几类。MLPC学习过程中使用BP算法,优化问题抽象成logistic loss function并使用L-BFGS进行优化。

  • MLlib包里面增加了一个频繁项挖掘算法PrefixSpan,AssociationRules能够把FreqItemset生成关联式规则

  • 在MLlib的统计包里面实现了Kolmogorov–Smirnov检验,用以检验两个经验分布是否不同或一个经验分布与另一个理想分布是否不同。

另外还有一些现有算法的增强:LDA算法、决策树和ensemble算法,GMM算法。

  • ML里面的多个分类模型现在都支持预测结果的概率而不像过去只支持预测结果,像LogisticRegressionModel、NaiveBayesModel、DecisionTreeClassificationModel、RandomForestClassificationModel、GBTClassificationModel等,分别使用predictRaw、predictProbability、predict分别可以得到原始预测、概率预测和最后的分类预测。同时这些分类模型也支持通过设置thresholds指定各个类的阈值。

  • RandomForestClassificationModel和RandomForestRegressionModel模型都支持输出feature importance

  • GMM EM算法实现了当feature维度或者cluster数目比较大的时候的分布式矩阵求逆计算。实验表明当feature维度>30,cluster数目>10的时候,这个优化性能提升明显。

  • 对于LinearRegressionModel和LogisticRegressionModel实现了LinearRegressionTrainingSummary和LogisticRegressionTrainingSummary用来记录模型训练过程中的一些统计指标。

Spark 1.5版本的Python API也在不断加强,越来越多的算法和功能的Python API基本上与Scala API对等了。此外在tuning和evaluator上也有增强。

其他

从Spark 1.5开始,Standalone、YARN和Mesos三种部署方式全部支持了动态资源分配。SparkR支持运行在YARN集群上,同时DataFrame的函数也提供了一些R风格的别名,可以降低熟悉R的用户的迁移成本。
在Streaming和Graphx方面也有非常大的改进,在这里不在一一赘述,详细可以参考发布说明

感谢郭蕾对本文的审校。

05 Sep 00:55

[转]高性能IO模型浅析

by DLevin

高性能IO模型浅析

转自:http://www.cnblogs.com/fanzhidongyzby/p/4098546.html

服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:

(1)同步阻塞IO(Blocking IO):即传统的IO模型。

(2)同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的NIO并非Java的NIO(New IO)库。

(3)IO多路复用(IO Multiplexing):即经典的Reactor设计模式,有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。

(4)异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

同步和异步的概念描述的是用户线程与内核的交互方式:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

另外,Richard Stevens 在《Unix 网络编程》卷1中提到的基于信号驱动的IO(Signal Driven IO)模型,由于该模型并不常用,本文不作涉及。接下来,我们详细分析四种常见的IO模型的实现原理。为了方便描述,我们统一使用IO的读操作作为示例。

一、同步阻塞IO

同步阻塞IO模型是最简单的IO模型,用户线程在内核进行IO操作时被阻塞。

图1 同步阻塞IO

如图1所示,用户线程通过系统调用read发起IO读操作,由用户空间转到内核空间。内核等到数据包到达后,然后将接收的数据拷贝到用户空间,完成read操作。

用户线程使用同步阻塞IO模型的伪代码描述为:

{
    read(socket, buffer);
    process(buffer);
}

即用户需要等待read将socket中的数据读取到buffer后,才继续处理接收的数据。整个IO请求的过程中,用户线程是被阻塞的,这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

二、同步非阻塞IO

同步非阻塞IO是在同步阻塞IO的基础上,将socket设置为NONBLOCK。这样做用户线程可以在发起IO请求后可以立即返回。

 

图2 同步非阻塞IO

如图2所示,由于socket是非阻塞的方式,因此用户线程发起IO请求时立即返回。但并未读取到任何数据,用户线程需要不断地发起IO请求,直到数据到达后,才真正读取到数据,继续执行。

用户线程使用同步非阻塞IO模型的伪代码描述为:

{
    while(read(socket, buffer) != SUCCESS) { }
    process(buffer);
}

即 用户需要不断地调用read,尝试读取socket中的数据,直到读取成功后,才继续处理接收的数据。整个IO请求的过程中,虽然用户线程每次发起IO请 求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。一般很少直接使用这种模型,而是在其他IO模型中使用非阻 塞IO这一特性。

三、IO多路复用

IO多路复用模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题。

图3 多路分离函数select

如图3所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

从 流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效 率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调 用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

用户线程使用select函数的伪代码描述为:

{
    select(socket);
    while(1) {
        sockets = select();
        for(socket in sockets) {
            if(can_read(socket)) {
                read(socket, buffer);
                process(buffer);
            }
        }
    }
}

其中while循环前将socket添加到select监视中,然后在while内一直调用select获取被激活的socket,一旦socket可读,便调用read函数将socket中的数据读取出来。

然 而,使用select函数的优点并不仅限于此。虽然上述方式允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻 塞),平均时间甚至比同步阻塞IO模型还要长。如果用户线程只注册自己感兴趣的socket或者IO请求,然后去做自己的事情,等到数据到来时再进行处 理,则可以提高CPU的利用率。

IO多路复用模型使用了Reactor设计模式实现了这一机制。

图4 Reactor设计模式

如 图4所示,EventHandler抽象类表示IO事件处理器,它拥有IO文件句柄Handle(通过get_handle获取),以及对Handle的 操作handle_event(读/写等)。继承于EventHandler的子类可以对事件处理器的行为进行定制。Reactor类用于管理 EventHandler(注册、删除等),并使用handle_events实现事件循环,不断调用同步事件多路分离器(一般是内核)的多路分离函数 select,只要某个文件句柄被激活(可读/写等),select就返回(阻塞),handle_events就会调用与文件句柄关联的事件处理器的 handle_event进行相关操作。

图5 IO多路复用

如 图5所示,通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理 器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时,则通知 相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。由于select函数是阻塞的,因此多路IO复用 模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。一般在使用IO多路复用模型 时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。

用户线程使用IO多路复用模型的伪代码描述为:

void UserEventHandler::handle_event() {
    if(can_read(socket)) {
        read(socket, buffer);
        process(buffer);
    }
}

{
    Reactor.register(new UserEventHandler(socket));
}

用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作,用户线程只需要将自己的EventHandler注册到Reactor即可。Reactor中handle_events事件循环的伪代码大致如下。

Reactor::handle_events() {
    while(1) {
       sockets = select();
       for(socket in sockets) {
            get_event_handler(socket).handle_event();
       }
    }
}

事件循环不断地调用select获取被激活的socket,然后根据获取socket对应的EventHandler,执行器handle_event函数即可。

IO多路复用是最常使用的IO模型,但是其异步程度还不够“彻底”,因为它使用了会阻塞线程的select系统调用。因此IO多路复用只能称为异步阻塞IO,而非真正的异步IO。

四、异步IO

“真 正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程,由用户线程自行读取数据、处理数据。而在异 步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

异步IO模型使用了Proactor设计模式实现了这一机制。

图6 Proactor设计模式

如 图6,Proactor模式和Reactor模式在结构上比较相似,不过在用户(Client)使用方式上差别较大。Reactor模式中,用户线程通过 向Reactor对象注册感兴趣的事件监听,然后事件触发时调用事件处理函数。而Proactor模式中,用户线程将 AsynchronousOperation(读/写等)、Proactor以及操作完成时的CompletionHandler注册到 AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提 供了一组异步操作API(读/写等)供用户使用,当用户线程调用异步API后,便继续执行自己的任务。 AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步。当异步IO操作完成 时,AsynchronousOperationProcessor将用户线程与AsynchronousOperation一起注册的Proactor 和CompletionHandler取出,然后将CompletionHandler与IO操作的结果数据一起转发给 Proactor,Proactor负责回调每一个异步操作的事件完成处理函数handle_event。虽然Proactor模式中每个异步操作都可以 绑定一个Proactor对象,但是一般在操作系统中,Proactor被实现为Singleton模式,以便于集中化分发操作完成事件。

图7 异步IO

如 图7所示,异步IO模型中,用户线程直接使用内核提供的异步IO API发起read请求,且发起后立即返回,继续执行用户线程代码。不过此时用户线程已 经将调用的AsynchronousOperation和CompletionHandler注册到内核,然后操作系统开启独立的内核线程去处理IO操 作。当read请求的数据到达时,由内核负责读取socket中的数据,并写入用户指定的缓冲区中。最后内核将read的数据和用户线程注册的 CompletionHandler分发给内部Proactor,Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件 处理函数),完成异步IO。

用户线程使用异步IO模型的伪代码描述为:


void UserCompletionHandler::handle_event(buffer) {
    process(buffer);
}

{
    aio_read(socket, new UserCompletionHandler);
}

用户需要重写CompletionHandler的handle_event函数进行处理数据的工作,参数buffer表示Proactor已经准备好的数据,用户线程直接调用内核提供的异步IO API,并将重写的CompletionHandler注册即可。

相 比于IO多路复用模型,异步IO并不十分常用,不少高性能并发服务程序使用IO多路复用模型+多线程任务处理的架构基本可以满足需求。况且目前操作系统对 异步IO的支持并非特别完善,更多的是采用IO多路复用模型模拟异步IO的方式(IO事件触发时不直接通知用户线程,而是将数据读写完毕后放到用户指定的 缓冲区中)。Java7之后已经支持了异步IO,感兴趣的读者可以尝试使用。

本文从基本概念、工作流程和代码示 例三个层次简要描述了常见的四种高性能IO模型的结构和原理,理清了同步、异步、阻塞、非阻塞这些容易混淆的概念。通过对高性能IO模型的理解,可以在服 务端程序的开发中选择更符合实际业务特点的IO模型,提高服务质量。希望本文对你有所帮助。


相似的:
http://www.cnblogs.com/nufangrensheng/p/3588690.html
http://www.ibm.com/developerworks/cn/linux/l-async/



DLevin 2015-09-04 15:16 发表评论
03 Sep 13:05

Java HashMap工作原理

by Wing

大部分Java开发者都在使用Map,特别是HashMap。HashMap是一种简单但强大的方式去存储和获取数据。但有多少开发者知道HashMap内部如何工作呢?几天前,我阅读了java.util.HashMap的大量源代码(包括Java 7 和Java 8),来深入理解这个基础的数据结构。在这篇文章中,我会解释java.util.HashMap的实现,描述Java 8实现中添加的新特性,并讨论性能、内存以及使用HashMap时的一些已知问题。

内部存储

Java HashMap类实现了Map<K, V>接口。这个接口中的主要方法包括:

  • V put(K key, V value)
  • V get(Object key)
  • V remove(Object key)
  • Boolean containsKey(Object key)

HashMap使用了一个内部类Entry<K, V>来存储数据。这个内部类是一个简单的键值对,并带有额外两个数据:

  • 一个指向其他入口(译者注:引用对象)的引用,这样HashMap可以存储类似链接列表这样的对象。
  • 一个用来代表键的哈希值,存储这个值可以避免HashMap在每次需要时都重新生成键所对应的哈希值。

下面是Entry<K, V>在Java 7下的一部分代码:

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
…
}

HashMap将数据存储到多个单向Entry链表中(有时也被称为桶bucket或者容器orbins)。所有的列表都被注册到一个Entry数组中(Entry<K, V>[]数组),这个内部数组的默认长度是16。

下面这幅图描述了一个HashMap实例的内部存储,它包含一个nullable对象组成的数组。每个对象都连接到另外一个对象,这样就构成了一个链表。

所有具有相同哈希值的键都会被放到同一个链表(桶)中。具有不同哈希值的键最终可能会在相同的桶中。

当用户调用 put(K key, V value) 或者 get(Object key) 时,程序会计算对象应该在的桶的索引。然后,程序会迭代遍历对应的列表,来寻找具有相同键的Entry对象(使用键的equals()方法)。

对于调用get()的情况,程序会返回值所对应的Entry对象(如果Entry对象存在)。

对于调用put(K key, V value)的情况,如果Entry对象已经存在,那么程序会将值替换为新值,否则,程序会在单向链表的表头创建一个新的Entry(从参数中的键和值)。

桶(链表)的索引,是通过map的3个步骤生成的:

  • 首先获取键的散列码
  • 程序重复散列码,来阻止针对键的糟糕的哈希函数,因为这有可能会将所有的数据都放到内部数组的相同的索引(桶)上。
  • 程序拿到重复后的散列码,并对其使用数组长度(最小是1)的位掩码(bit-mask)。这个操作可以保证索引不会大于数组的大小。你可以将其看做是一个经过计算的优化取模函数。

下面是生成索引的源代码:

// the "rehash" function in JAVA 7 that takes the hashcode of the key
static int hash(int h) {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
// the "rehash" function in JAVA 8 that directly takes the key
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
// the function that returns the index from the rehashed hash
static int indexFor(int h, int length) {
    return h & (length-1);
}

为了更有效地工作,内部数组的大小必须是2的幂值。让我们看一下为什么:

假设数组的长度是17,那么掩码的值就是16(数组长度-1)。16的二进制表示是0…010000,这样对于任何值H来说,“H & 16”的结果就是16或者0。这意味着长度为17的数组只能应用到两个桶上:一个是0,另外一个是16,这样不是很有效率。但是如果你将数组的长度设置为2的幂值,例如16,那么按位索引的工作变成“H & 15”。15的二进制表示是0…001111,索引公式输出的值可以从0到15,这样长度为16的数组就可以被充分使用了。例如:

  • 如果H = 952,它的二进制表示是0..01110111000,对应的索引是0…01000 = 8
  • 如果H = 1576,它的二进制表示是0..011000101000,对应的索引是0…01000 = 8
  • 如果H = 12356146,它的二进制表示是0..0101111001000101000110010,对应的索引是0…00010 = 2
  • 如果H = 59843,它的二进制表示是0..01110100111000011,它对应的索引是0…00011 = 3

这种机制对于开发者来说是透明的:如果他选择一个长度为37的HashMap,Map会自动选择下一个大于37的2的幂值(64)作为内部数组的长度。

自动调整大小

在获取索引后,get()、put()或者remove()方法会访问对应的链表,来查看针对指定键的Entry对象是否已经存在。在不做修改的情况下,这个机制可能会导致性能问题,因为这个方法需要迭代整个列表来查看Entry对象是否存在。假设内部数组的长度采用默认值16,而你需要存储2,000,000条记录。在最好的情况下,每个链表会有125,000个Entry对象(2,000,000/16)。get()、remove()和put()方法在每一次执行时,都需要进行125,000次迭代。为了避免这种情况,HashMap可以增加内部数组的长度,从而保证链表中只保留很少的Entry对象。

当你创建一个HashMap时,你可以通过以下构造函数指定一个初始长度,以及一个loadFactor:

</pre>
public HashMap(int initialCapacity, float loadFactor)
<pre>

如果你不指定参数,那么默认的initialCapacity的值是16, loadFactor的默认值是0.75。initialCapacity代表内部数组的链表的长度。

当你每次使用put(…)方法向Map中添加一个新的键值对时,该方法会检查是否需要增加内部数组的长度。为了实现这一点,Map存储了2个数据:

  • Map的大小:它代表HashMap中记录的条数。我们在向HashMap中插入或者删除值时更新它。
  • 阀值:它等于内部数组的长度*loadFactor,在每次调整内部数组的长度时,该阀值也会同时更新。

在添加新的Entry对象之前,put(…)方法会检查当前Map的大小是否大于阀值。如果大于阀值,它会创建一个新的数组,数组长度是当前内部数组的两倍。因为新数组的大小已经发生改变,所以索引函数(就是返回“键的哈希值 & (数组长度-1)”的位运算结果)也随之改变。调整数组的大小会创建两个新的桶(链表),并且将所有现存Entry对象重新分配到桶上。调整数组大小的目标在于降低链表的大小,从而降低put()、remove()和get()方法的执行时间。对于具有相同哈希值的键所对应的所有Entry对象来说,它们会在调整大小后分配到相同的桶中。但是,如果两个Entry对象的键的哈希值不一样,但它们之前在同一个桶上,那么在调整以后,并不能保证它们依然在同一个桶上。

这幅图片描述了调整前和调整后的内部数组的情况。在调整数组长度之前,为了得到Entry对象E,Map需要迭代遍历一个包含5个元素的链表。在调整数组长度之后,同样的get()方法则只需要遍历一个包含2个元素的链表,这样get()方法在调整数组长度后的运行速度提高了2倍。

线程安全

如果你已经非常熟悉HashMap,那么你肯定知道它不是线程安全的,但是为什么呢?例如假设你有一个Writer线程,它只会向Map中插入已经存在的数据,一个Reader线程,它会从Map中读取数据,那么它为什么不工作呢?

因为在自动调整大小的机制下,如果线程试着去添加或者获取一个对象,Map可能会使用旧的索引值,这样就不会找到Entry对象所在的新桶。

在最糟糕的情况下,当2个线程同时插入数据,而2次put()调用会同时出发数组自动调整大小。既然两个线程在同时修改链表,那么Map有可能在一个链表的内部循环中退出。如果你试着去获取一个带有内部循环的列表中的数据,那么get()方法永远不会结束。

HashTable提供了一个线程安全的实现,可以阻止上述情况发生。但是,既然所有的同步的CRUD操作都非常慢。例如,如果线程1调用get(key1),然后线程2调用get(key2),线程2调用get(key3),那么在指定时间,只能有1个线程可以得到它的值,但是3个线程都可以同时访问这些数据。

从Java 5开始,我们就拥有一个更好的、保证线程安全的HashMap实现:ConcurrentHashMap。对于ConcurrentMap来说,只有桶是同步的,这样如果多个线程不使用同一个桶或者调整内部数组的大小,它们可以同时调用get()、remove()或者put()方法。在一个多线程应用程序中,这种方式是更好的选择

键的不变性

为什么将字符串和整数作为HashMap的键是一种很好的实现?主要是因为它们是不可变的!如果你选择自己创建一个类作为键,但不能保证这个类是不可变的,那么你可能会在HashMap内部丢失数据。

我们来看下面的用例:

  • 你有一个键,它的内部值是“1”。
  • 你向HashMap中插入一个对象,它的键就是“1”。
  • HashMap从键(即“1”)的散列码中生成哈希值。
  • Map在新创建的记录中存储这个哈希值。
  • 你改动键的内部值,将其变为“2”。
  • 键的哈希值发生了改变,但是HashMap并不知道这一点(因为存储的是旧的哈希值)。
  • 你试着通过修改后的键获取相应的对象。
  • Map会计算新的键(即“2”)的哈希值,从而找到Entry对象所在的链表(桶)。
  • 情况1: 既然你已经修改了键,Map会试着在错误的桶中寻找Entry对象,没有找到。
  • 情况2: 你很幸运,修改后的键生成的桶和旧键生成的桶是同一个。Map这时会在链表中进行遍历,已找到具有相同键的Entry对象。但是为了寻找键,Map首先会通过调用equals()方法来比较键的哈希值。因为修改后的键会生成不同的哈希值(旧的哈希值被存储在记录中),那么Map没有办法在链表中找到对应的Entry对象。

下面是一个Java示例,我们向Map中插入两个键值对,然后我修改第一个键,并试着去获取这两个对象。你会发现从Map中返回的只有第二个对象,第一个对象已经“丢失”在HashMap中:

public class MutableKeyTest {

	public static void main(String[] args) {

		class MyKey {
			Integer i;

			public void setI(Integer i) {
				this.i = i;
			}

			public MyKey(Integer i) {
				this.i = i;
			}

			@Override
			public int hashCode() {
				return i;
			}

			@Override
			public boolean equals(Object obj) {
				if (obj instanceof MyKey) {
					return i.equals(((MyKey) obj).i);
				} else
					return false;
			}

		}

		Map<MyKey, String> myMap = new HashMap<>();
		MyKey key1 = new MyKey(1);
		MyKey key2 = new MyKey(2);

		myMap.put(key1, "test " + 1);
		myMap.put(key2, "test " + 2);

		// modifying key1
		key1.setI(3);

		String test1 = myMap.get(key1);
		String test2 = myMap.get(key2);

		System.out.println("test1= " + test1 + " test2=" + test2);

	}

}

上述代码的输出是“test1=null test2=test 2”。如我们期望的那样,Map没有能力获取经过修改的键 1所对应的字符串1。

Java 8 中的改进

在Java 8中,HashMap中的内部实现进行了很多修改。的确如此,Java 7使用了1000行代码来实现,而Java 8中使用了2000行代码。我在前面描述的大部分内容在Java 8中依然是对的,除了使用链表来保存Entry对象。在Java 8中,我们仍然使用数组,但它会被保存在Node中,Node中包含了和之前Entry对象一样的信息,并且也会使用链表:

下面是在Java 8中Node实现的一部分代码:

   static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

那么和Java 7相比,到底有什么大的区别呢?好吧,Node可以被扩展成TreeNode。TreeNode是一个红黑树的数据结构,它可以存储更多的信息,这样我们可以在O(log(n))的复杂度下添加、删除或者获取一个元素。下面的示例描述了TreeNode保存的所有信息:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
	final int hash; // inherited from Node<K,V>
	final K key; // inherited from Node<K,V>
	V value; // inherited from Node<K,V>
	Node<K,V> next; // inherited from Node<K,V>
	Entry<K,V> before, after;// inherited from LinkedHashMap.Entry<K,V>
	TreeNode<K,V> parent;
	TreeNode<K,V> left;
	TreeNode<K,V> right;
	TreeNode<K,V> prev;
	boolean red;

红黑树是自平衡的二叉搜索树。它的内部机制可以保证它的长度总是log(n),不管我们是添加还是删除节点。使用这种类型的树,最主要的好处是针对内部表中许多数据都具有相同索引(桶)的情况,这时对树进行搜索的复杂度是O(log(n)),而对于链表来说,执行相同的操作,复杂度是O(n)。

如你所见,我们在树中确实存储了比链表更多的数据。根据继承原则,内部表中可以包含Node(链表)或者TreeNode(红黑树)。Oracle决定根据下面的规则来使用这两种数据结构:

- 对于内部表中的指定索引(桶),如果node的数目多于8个,那么链表就会被转换成红黑树。

- 对于内部表中的指定索引(桶),如果node的数目小于6个,那么红黑树就会被转换成链表。

这张图片描述了在Java 8 HashMap中的内部数组,它既包含树(桶0),也包含链表(桶1,2和3)。桶0是一个树结构是因为它包含的节点大于8个。

内存开销

JAVA 7

使用HashMap会消耗一些内存。在Java 7中,HashMap将键值对封装成Entry对象,一个Entry对象包含以下信息:

  • 指向下一个记录的引用
  • 一个预先计算的哈希值(整数)
  • 一个指向键的引用
  • 一个指向值的引用

此外,Java 7中的HashMap使用了Entry对象的内部数组。假设一个Java 7 HashMap包含N个元素,它的内部数组的容量是CAPACITY,那么额外的内存消耗大约是:

sizeOf(integer)* N + sizeOf(reference)* (3*N+C)

其中:

  • 整数的大小是4个字节
  • 引用的大小依赖于JVM、操作系统以及处理器,但通常都是4个字节。

这就意味着内存总开销通常是16 * N + 4 * CAPACITY字节。

注意:在Map自动调整大小后,CAPACITY的值是下一个大于N的最小的2的幂值。

注意:从Java 7开始,HashMap采用了延迟加载的机制。这意味着即使你为HashMap指定了大小,在我们第一次使用put()方法之前,记录使用的内部数组(耗费4*CAPACITY字节)也不会在内存中分配空间。

JAVA 8

在Java 8实现中,计算内存使用情况变得复杂一些,因为Node可能会和Entry存储相同的数据,或者在此基础上再增加6个引用和一个Boolean属性(指定是否是TreeNode)。

如果所有的节点都只是Node,那么Java 8 HashMap消耗的内存和Java 7 HashMap消耗的内存是一样的。

如果所有的节点都是TreeNode,那么Java 8 HashMap消耗的内存就变成:

N * sizeOf(integer) + N * sizeOf(boolean) + sizeOf(reference)* (9*N+CAPACITY )

在大部分标准JVM中,上述公式的结果是44 * N + 4 * CAPACITY 字节。

性能问题

非对称HashMap vs 均衡HashMap

在最好的情况下,get()和put()方法都只有O(1)的复杂度。但是,如果你不去关心键的哈希函数,那么你的put()和get()方法可能会执行非常慢。put()和get()方法的高效执行,取决于数据被分配到内部数组(桶)的不同的索引上。如果键的哈希函数设计不合理,你会得到一个非对称的分区(不管内部数据的是多大)。所有的put()和get()方法会使用最大的链表,这样就会执行很慢,因为它需要迭代链表中的全部记录。在最坏的情况下(如果大部分数据都在同一个桶上),那么你的时间复杂度就会变为O(n)。

下面是一个可视化的示例。第一张图描述了一个非对称HashMap,第二张图描述了一个均衡HashMap。

在这个非对称HashMap中,在桶0上运行get()和put()方法会很花费时间。获取记录K需要花费6次迭代。

在这个均衡HashMap中,获取记录K只需要花费3次迭代。这两个HashMap存储了相同数量的数据,并且内部数组的大小一样。唯一的区别是键的哈希函数,这个函数用来将记录分布到不同的桶上。

下面是一个使用Java编写的极端示例,在这个示例中,我使用哈希函数将所有的数据放到相同的链表(桶),然后我添加了2,000,000条数据。

public class Test {

	public static void main(String[] args) {

		class MyKey {
			Integer i;
			public MyKey(Integer i){
				this.i =i;
			}

			@Override
			public int hashCode() {
				return 1;
			}

			@Override
			public boolean equals(Object obj) {
			…
			}

		}
		Date begin = new Date();
		Map <MyKey,String> myMap= new HashMap<>(2_500_000,1);
		for (int i=0;i<2_000_000;i++){
			myMap.put( new MyKey(i), "test "+i);
		}

		Date end = new Date();
		System.out.println("Duration (ms) "+ (end.getTime()-begin.getTime()));
	}
}

我的机器配置是core i5-2500k @ 3.6G,在java 8u40下需要花费超过45分钟的时间来运行(我在45分钟后停止了进程)。如果我运行同样的代码, 但是我使用如下的hash函数:

	@Override
	public int hashCode() {
		int key = 2097152-1;
		return key+2097152*i;
}

运行它需要花费46秒,和之前比,这种方式好很多了!新的hash函数比旧的hash函数在处理哈希分区时更合理,因此调用put()方法会更快一些。如果你现在运行相同的代码,但是使用下面的hash函数,它提供了更好的哈希分区:

 @Override
 public int hashCode() {
 return i;
 }

现在只需要花费2秒

我希望你能够意识到哈希函数有多重要。如果在Java 7上面运行同样的测试,第一个和第二个的情况会更糟(因为Java 7中的put()方法复杂度是O(n),而Java 8中的复杂度是O(log(n))。

在使用HashMap时,你需要针对键找到一种哈希函数,可以将键扩散到最可能的桶上。为此,你需要避免哈希冲突。String对象是一个非常好的键,因为它有很好的哈希函数。Integer也很好,因为它的哈希值就是它自身的值。

调整大小的开销

如果你需要存储大量数据,你应该在创建HashMap时指定一个初始的容量,这个容量应该接近你期望的大小。

如果你不这样做,Map会使用默认的大小,即16,factorLoad的值是0.75。前11次调用put()方法会非常快,但是第12次(16*0.75)调用时会创建一个新的长度为32的内部数组(以及对应的链表/树),第13次到第22次调用put()方法会很快,但是第23次(32*0.75)调用时会重新创建(再一次)一个新的内部数组,数组的长度翻倍。然后内部调整大小的操作会在第48次、96次、192次…..调用put()方法时触发。如果数据量不大,重建内部数组的操作会很快,但是数据量很大时,花费的时间可能会从秒级到分钟级。通过初始化时指定Map期望的大小,你可以避免调整大小操作带来的消耗

但这里也有一个缺点:如果你将数组设置的非常大,例如2^28,但你只是用了数组中的2^26个桶,那么你将会浪费大量的内存(在这个示例中大约是2^30字节)。

结论

对于简单的用例,你没有必要知道HashMap是如何工作的,因为你不会看到O(1)、O(n)以及O(log(n))之间的区别。但是如果能够理解这一经常使用的数据结构背后的机制,总是有好处的。另外,对于Java开发者职位来说,这是一道典型的面试问题。

对于大数据量的情况,了解HashMap如何工作以及理解键的哈希函数的重要性就变得非常重要。

我希望这篇文章可以帮助你对HashMap的实现有一个深入的理解。

相关文章

03 Sep 11:52

Java集合框架综述

最近被陆陆续续问了几遍HashMap的实现,回答的不好,打算复习复习JDK中的集合框架,并尝试分析其源码,这么做一方面是这些类非常实用,掌握其实现能更好的优化我们的程序;另一方面是学习借鉴JDK是如何实现了这么一套优雅高效的类库,提升编程能力。

在介绍具体适合类之前,本篇文章对Java中的集合框架做一个大致描述,从一个高的角度俯视这个框架,了解了这个框架的一些理念与约定,会大大帮助后面分析某个具体类,让我们开始吧。

集合框架(collections framework)

首先要明确,集合代表了一组对象(和数组一样,但数组长度不能变,而集合能)。Java中的集合框架定义了一套规范,用来表示、操作集合,使具体操作与实现细节解耦。

其实说白了,可以把一个集合看成一个微型数据库,操作不外乎“增删改查”四种操作,我们在学习使用一个具体的集合类时,需要把这四个操作的时空复杂度弄清楚了,基本上就可以说掌握这个类了。

设计理念

主要理念用一句话概括就是:提供一套“小而美”的API。API需要对程序员友好,增加新功能时能让程序员们快速上手。
为了保证核心接口足够小,最顶层的接口(也就是Collection与Map接口)并不会区分该集合是否可变(mutability),是否可更改(modifiability),是否可改变大小(resizability)这些细微的差别。相反,一些操作是可选的,在实现时抛出UnsupportedOperationException即可表示集合不支持该操作。集合的实现者必须在文档中声明那些操作是不支持的。

为了保证最顶层的核心接口足够小,它们只能包含下面情况下的方法:

  1. 基本操作,像之前说的“增删改查”
  2. There is a compelling performance reason why an important implementation would want to override it.

此外,所有的集合类都必须能提供友好的交互操作,这包括没有继承Collection类的数组对象。因此,框架提供一套方法,让集合类与数组可以相互转化,并且可以把Map看作成集合。

两大基类Collection与Map

在集合框架的类继承体系中,最顶层有两个接口:

  • Collection表示一组纯数据
  • Map表示一组key-value对

一般继承自CollectionMap的集合类,会提供两个“标准”的构造函数:

  • 没有参数的构造函数,创建一个空的集合类
  • 有一个类型与基类(CollectionMap)相同的构造函数,创建一个与给定参数具有相同元素的新集合类

因为接口中不能包含构造函数,所以上面这两个构造函数的约定并不是强制性的,但是在目前的集合框架中,所有继承自CollectionMap的子类都遵循这一约定。

Collection

java-collection-hierarchy

如上图所示,Collection类主要有三个接口:

  • Set表示不允许有重复元素的集合(A collection that contains no duplicate elements)
  • List表示允许有重复元素的集合(An ordered collection (also known as a sequence))
  • Queue JDK1.5新增,与上面两个集合类主要是的区分在于Queue主要用于存储数据,而不是处理数据。(A collection designed for holding elements prior to processing.)

Map

MapClassHierarchy

Map并不是一个真正意义上的集合(are not true collections),但是这个接口提供了三种“集合视角”(collection views ),使得可以像操作集合一样操作它们,具体如下:

  • 把map的内容看作key的集合(map’s contents to be viewed as a set of keys)
  • 把map的内容看作value的集合(map’s contents to be viewed as a collection of values)
  • 把map的内容看作key-value映射的集合(map’s contents to be viewed as a set of key-value mappings)

集合的实现(Collection Implementations)

实现集合接口的类一般遵循<实现方式>+<接口>的命名方式,通用的集合实现类如下表:

Interface Hash Table Resizable Array Balanced Tree Linked List Hash Table + Linked List
Set HashSet   TreeSet   LinkedHashSet
List   ArrayList   LinkedList  
Deque   ArrayDeque   LinkedList  
Map HashMap   TreeMap   LinkedHashMap

总结

今天先开个头,后面会陆陆续续来一系列干货,Stay Tuned。

需要说明一点,今后所有源码分析都将基于Oracle JDK 1.7.0_71,请知悉。

$ java -versionjava version "1.7.0_71"Java(TM) SE Runtime Environment (build 1.7.0_71-b14)Java HotSpot(TM) 64-Bit Server VM (build 24.71-b01, mixed mode)

参考

21 Aug 07:27

程序员招聘那些事

by 新用户279165

 又到了一年一度的招聘季节,在校大学生们又要找工作了。让我们看看程序员招聘都有哪些事吧。

在线编程

程序员最重要的技能就是编程啦。在线编程网站能够帮助程序员练习编程技巧,帮助公司筛选编程能力好的程序员。因此在线编程网站分为两种:2B和2C。

著名的2B在线编程网站 HackerRank 主要为企业提供服务,当互联网企业收到程序猿的简历时,可以将 HackerRank 的挑战链接直接用邮件回复给程序猿,让程序猿直接点击链接来接受笔试挑战,从而可以测试出该程序猿的技术能力是否达标。国内 oxcoder 也是采用这种模式。2014年12月,oxcoder 获得华创资本200万人民币的种子轮投资。

Image title

2C在线编程网站的用户是程序员,帮组程序员提高编程能力。国外的Codefights 把编程设计成PK游戏。程序员和程序员之间可以随机或者指定配对对手进行PK,PK的内容是又快又好地完成编程任务。

Image title

针对国内程序员,牛客网 推出了在线试题练习模块。牛客网的不少在线试题是 BAT 历年真题。

Image title

算法题

今年早些时候,著名软件Homebrew作者Max howell 因为没有在白板上写出二叉树翻转的程序,而没有通过Google面试。他的面试官有90%的可能性在使用Homebrew。并且Howell是面试 Google的 iOS工程师。很多算法很好的工程师都表示在实际工作中,用到算法的可能性很少。一种观点是,程序员有两种技能树,追求极致的Hacker Style技能树和追求协调的Engineer Style技能树。算法是在Hacker Style技能树上,而实际工作中更需要的是 Engineer Style技能树。

也有认可算法题的观点。大公司并不需要招聘上来的工程师能立马干活,而是需要工程师足够聪明,能够快速地学习。算法题和逻辑题能很好地筛选出聪明的人。我曾经问过阿里巴巴的面试官,“算法题和逻辑题是很有争议的,您出这些题的原因是什么呢?”。他的回答是,其实他不是很关心结果,更关心碰到一个很难的题目时,应聘者的心理反应以及沟通能力。

不管怎么样,算法题是在校学生面试的一道坎,因此就有针对这个需求的创业项目。LeetCode 是为美国程序员面试FLAG(Facebook,Linkedin,  Apple 和 Google)提供的在线算法训练平台。很多准备肉身翻墙的中国在校学生也使用 LeetCode 训练。LeetCode 的盈利模式是出售解题思路的电子书。

Image title

Lintcode则针对准备肉身翻墙的中国在校学生,其盈利模式是线上开班给应聘者讲授算法题解题思路和面试技巧。

算法题练习平台本质上也是2C在线编程平台。但算法题本身比较难,在大公司招聘中考察的权重大。因此算法题练习平台一般都专注算法题,而没有扩展到其他类型的题目。

内推

内推最开始的形态是,一位应聘者和要面试的部门一位工程师熟悉,然后让工程师把简历发给工程师部门的上级。上级看到是自己下属的推荐,就会直接推动 HR 走流程。这种内推效果很明显。

在程序员紧缺的情况下,内推发展出一套新的形态。很多公司有一套内推系统。工程师可以把不是面试自己部门但很优秀的应聘者的简历,发送到内推系统中。内推系统会把简历发送到相应的部门。这种内推的效率也是很高的。

到现在,内推还有一种形态,就是公司 HR 部门和不同的机构合作,给机构一些内推的指标和筛选条件。这种内推本质上是为了收到更多的简历。对于一些能力不错但人际关系不是很广的在校学生,这种内推可能有一些效果。

目前国内有一家内推网就是这种模式。互联网公司 HR 或者其他部门可以在网站上发布职位,应聘者可以直接发送简历。但这种模式的招聘属性强,内推属性弱。

内推项目也是很多相关公司提供的一项服务。牛客网就和一些互联网企业的 HR 部门合作,开展了内推服务。开源中国也有招聘频道。

程序员招聘网站

程序员招聘除了一些通用的招聘网站,比如智联招聘、前程无忧和58同城招聘之外,还有一些专注互联网领域的招聘网站。拉勾网是目前最有名的互联网招聘网站。2014年8月,拉勾网获得启明创投和贝塔斯曼亚洲投资基金 2500 万美元的B轮投资。

Image title

针对数据分析人才急缺的情况,我们甚至看到了专门针对数据分析人才的招聘网站。NLPJOB就是这样一个网站,专门针对数据分析相关领域的招聘,比如自然语言处理、机器学习、数据挖掘等领域。

Image title

如果你熟悉程序员招聘的相关项目,欢迎来信至 lili at 36kr.com 交流。

可爱的创业者,如果你或你的朋友的项目希望被 36 氪报道的话,请狠戳这里

 

20 Aug 04:34

展望2016年的Rust语言

by 张天雷

2006年,编程语言工程师Graydon Hoare利用业余时间启动了Rust语言项目。该项目充分借鉴了C/C++/Java/Python等语言的经验,试图在保持良好性能的同时,克服以往编程语言所存在的问题。其最大的特点在于保持较高的运行效率、深入的底层控制和广泛应用范围的同时,解决了传统C语言和C++语言中的内存安全问题。2009年,Mozilla接手Rust项目,创建了以Graydon为首的专业全职开发团队,并且开放了该项目的源代码。2012年1月,第一个面向公众的预览版本——v0.1 发布。经历了大刀阔斧的10年发展,Rust在2015年5月份正式发布1.0版本。z之后,Rust开始遵守 SemVer 2.0 规范,进入稳步发展的阶段。那么,作为一个正在崛起的语言,Rust在2016年将会向何处发展呢?

目前,Rust以其无虚拟机、无垃圾收集器、无运行时、无空指针/野指针/内存越界/缓冲区溢出/段错误、无数据竞争等特点已经吸引了广大开发人员的广泛关注。但是,作为一门新兴的语言,Rust仍然有很多地方需要完善。据Rust核心开发团队透露,Rust在2016年的发展主要包括加大在框架上的投入、完善关键特性和扩展应用领域等三个方向。

首先,在加大在框架上的投入方向,Rust团队准备在Crater工具、增量编译和IDE集成三个方面着手开始。作为测试编译器的工具,Crater目前已经成为Rust社区不可或缺的工具。它能够有效发现编译器中存在的问题。此外,Rust开发团队还经常使用Crater来比较稳定版与开发版的不同以及评估不同改变所带来的影响。对于如此重要的工具,Rust团队未来将会扩展其对Linux外其他平台的覆盖度,使得Crater更简单易用,并包含除crates.io以外其他源的代码。而且,该团队还计划制作一个适用于库作者的版本(使得库的变化对下游代码的影响可以很容易被观察到)。在增量编译方面,Rust才刚刚起步。之前,Rust编译器会把所有的代码作为输入,经过类型检查后发送给LLVM进行优化。这种方式在带来深度优化的同时,也使得每次编译都非常耗时,加大了代码调试的难度。未来,Rust团队将在支持增量编译方面努力。而且,增量编译工作还包括了重新构造编译器,来引进一种新的中间层表示——MIR。MIR是一种更加简单和底层的Rust代码形式,能够使得Rust编译器更加简单。最后,Rust团队还试图扩展Rust编译器,使其可以与IDE或者其他工具更深入的集成。

完善关键特性方向包括了标准化、改善借用检查器(Borrow Checker)以及完善插件稳定性三个方面。在Rust语言设计之初,其试图试图实现的目标就包括用户不为不使用的东西付费和用户所使用的东西肯定是最好的这两个方面。目前,Rust 1.0已经实现了第一个目标。但第二个目标还未实现。为此,Rust团队准备在标准化方面进行努力。标准化就是允许用户在有需要时提供多个、相互重叠的trait实现,从而使得每个实现都有更加专业的应用范围。此外,标准化还能改善代码的重用性。作为某种意义上Rust的核心,借用检查器通过抓取use-after-free类似的问题保证了编译器在没有垃圾收集器情况下的内存安全。但是,目前的借用检查器偶尔还存在误检测的情况。Rust团队计划通过重构借用检查器,使其能够以细粒度范围(移动到MIR所移动的一步)查看代码,从而解决该问题。最后,目前Rust的很多crate都使用了高度不稳定的编译器插件,非常容器引起编译器出现问题。Rust团队计划提出一个新的插件设计框架,使其更加鲁棒,并能够提供内置纯净的宏扩展支持。

最后,扩展Rust的应用领域方向包括交叉编译、安装Cargo以及追踪钩(tracing
hook)三个方面。尽管目前的Rust能够支持交叉编译,但该过程需要大量的人工参与。Rust团队正试图自动化交叉编译的流程,使得用户只需要下载一个对应版本的预编译libstd库,然后执行编译/安装即可。此外,Cargo缺乏安装可执行文件的方法。Rust团队希望能够使用cargo install这样的命令,来实现Linux中make install的功能。在追踪钩方面,Rust团队深谋远虑。使用Rust一个最高级的方法就是把Rust代码嵌入到用Ruby或Python等高级语言编写的系统中。这种嵌入法一般通过为Rust代码提供一个C语言的API来完成。这种方法在目标平台运行传统GC等这种C语言友好的内存管理机制时十分高效。然而,与一个使用更高级GC的环境进行集成将会十分困难。与这些引擎进行集成需要非常小心进行代码编写工作。否则,非常小的错误都可能导致系统崩溃。为了把Rust引入到更高级GC的环境中,Rust团队计划扩展编译器的能力,使其能够产生追踪钩。这些钩子就可以被GC用来搜索堆栈和识别root,大大简化与高级VM集成代码的编写工作。

从以上分析可以看出,Rust在2016年仍将会有长足的发展。而且,从Rust语言的首届会议RustCamp 2015来看,Rust社区未来也会更加活跃。目前,Rust已经应用到OpenDNS和Skylight等生产环境,以及浏览器引擎Servo和Rust编译器等项目。Rust1.0版本代码贡献者庄晓立在一次访谈中表示,Rust未来一定会继续沿着“确保内存安全、无运行开销、高效实用”的既定方向持续发展。


感谢徐川对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

18 Aug 00:42

《 Java并发编程从入门到精通》第5章 多线程之间交互:线程阀

by 张振华_jack

javaC

作者:张振华    购买链接:天猫商城

(投入多少,收获多少。参与多深,领悟多深,京东,亚马逊,当当均有销售。)

 


5.1 线程安全的阻塞队列BlockingQueue

(1)先理解一下Queue、Deque、BlockingQueue的概念:

Queue(队列) :用于保存一组元素,不过在存取元素的时候必须遵循先进先出原则。队列是一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。在队列这种数据结构中,最先插入的元素将是最先被删除的元素;反之最后插入的元素将是最后被删除的元素,因此队列又称为“先进先出”(FIFO—first in first out)的线性表。

Deque(双端队列): 两端都可以进出的队列。当我们约束从队列的一端进出队时,就形成了另外一种存取模式,它遵循先进后出原则,这就是栈结构。双端队列主要是用于栈操作。使用站结构让操作有可追溯性(如windows窗口地址栏内的路径前进栈、后退栈)。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

阻塞队列提供了四种处理方法:

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用
  • 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
  • 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

(2)Java里的阻塞队列最新JDK中提供了7个阻塞队列。分别是:

 

BlockingQueue常用的方法有,更多方法请查询API:

1)add(anObject):把anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则招聘异常

2)offer(anObject):表示如果可能的话,将anObject加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回true,否则返回false.

3)put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断直到BlockingQueue里面有空间再继续.

4)poll(time):取走BlockingQueue里排在首位的对象,若不能立即取出,则可以等time参数规定的时间,取不到时返回null

5)take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到Blocking有新的对象被加入为止

其中:BlockingQueue 不接受null 元素。试图add、put 或offer 一个null 元素时,某些实现会抛出NullPointerException。null 被用作指示poll 操作失败的警戒值。


5.2 ArrayBlockingQueue


    ArrayBlockingQueue一个由数组支持的有界的阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。

这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

此类支持对等待的生产者线程和使用者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。然而,通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。

先看一下ArrayBlockingQueue的部分源码:理解一下ArrayBlockingQueue的实现原理和机制

public class ArrayBlockingQueue <E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
 
    //数组的储存结构
    final Object[] items;    
   //锁采用的机制
    final ReentrantLock lock;
    public ArrayBlockingQueue( int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        //通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程
        lock = new ReentrantLock(fair);
        notEmpty = lock .newCondition();
        notFull =  lock .newCondition();
    }
    public boolean offer(E e) {
        checkNotNull(e);
        //使用ReentrantLock 锁机制
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            if (count == items.length)
                return false ;
            else {
                enqueue(e);
                return true ;
            }
        } finally {
            lock.unlock();//释放锁
        }
    }
    private void enqueue(E x) {
        final Object[] items = this.items;
        items[ putIndex] = x;//通过数组进行储存
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        notEmpty.signal();
    }
…….
}
使用实例是:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/*
* 现有的程序代码模拟产生了16个日志对象,并且需要运行16秒才能打印完这些日志,
* 请在程序中增加4个线程去调用parseLog()方法来分头打印这16个日志对象,
* 程序只需要运行4秒即可打印完这些日志对象。
*/
public class BlockingQueueTest {
       public static void main(String[] args) throws Exception {
             // 新建一个等待队列
             final BlockingQueue<String> bq = new ArrayBlockingQueue<String>(16);
             // 四个线程
             for (int i = 0; i < 4; i++) {
                   new Thread(new Runnable() {
                         @Override
                         public void run() {
                               while (true ) {
                                     try {
                                          String log = (String) bq.take();
                                           parseLog(log);
                                    } catch (Exception e) {
                                    }
                              }
                        }
                  }).start();
            }
             for (int i = 0; i < 16; i++) {
                  String log = (i + 1) + ” –>  “;
                  bq.put(log); // 将数据存到队列里!
            }
      }
       // parseLog方法内部的代码不能改动
       public static void parseLog(String log) {
            System. out.println(log + System.currentTimeMillis());
             try {
                  Thread. sleep(1000);
            } catch (InterruptedException e) {
                  e.printStackTrace();
            }
      }
}

5.3 LinkedBlockingQueue
      LinkedBlockingQueue : 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列 中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时 (LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反 之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别 采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大 小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于 消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

先看一下LinkedBlockingDeque的部分源码:理解一下ArrayBlockingQueue的实现原理和机制
public class LinkedBlockingDeque <E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {
    final ReentrantLock lock = new ReentrantLock();//线程安全
    /**
     * @throws NullPointerException {@inheritDoc}
     */
    public boolean offerLast(E e) {
        if (e == nullthrow new NullPointerException();
        Node<E> node = new Node<E>(e);//每次插入后都将动态地创建链接节点
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return linkLast(node);
        } finally {
            lock.unlock();
        }
    }
    public boolean offer(E e) {
        return offerLast(e);
    }
    public boolean add(E e) {
        addLast(e);
        return true ;
    }
    public void addLast(E e) {
        if (!offerLast(e))
            throw new IllegalStateException(“Deque full”);
    }
    public E removeFirst() {
        E x = pollFirst();
        if (x == nullthrow new NoSuchElementException();
        return x;
    }
    public E pollFirst() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return unlinkFirst();
        } finally {
            lock.unlock();
        }
    }
 
……
}
使用实例是:
将ArrayBlockingQueue的例子换成LinkedBlockingQueue即可:
 // 新建一个等待队列
 final BlockingQueue<String> bq = new ArrayBlockingQueue<String>(16);
换成:
final BlockingQueue<String> bq = new LinkedBlockingQueue<String>(16);

5.4 PriorityBlockingQueue 


PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。

先看一下PriorityBlockingQueue的部分源码,理解一下PriorityBlockingQueue的实现原理和机制:

public class PriorityBlockingQueue <E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {
    private final ReentrantLock lock ;//说明本类使用一个lock来同步读写等操作
    private transient Comparator<? super E> comparator;
     // 使用指定的初始容量创建一个 PriorityBlockingQueue,并根据指定的比较器对其元素进行排序。
    public PriorityBlockingQueue( int initialCapacity,
                                 Comparator<? super E> comparator) {
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.lock = new ReentrantLock();
        this.notEmpty = lock.newCondition();
        this.comparator = comparator;
        this.queue = new Object[initialCapacity];
    }
     public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
……
}
 

5.5 DelayQueue


     DelayQueue:是一个支持延时获取元素的使用优先级队列的实现的无界阻塞队列。队列中的元素必须实现Delayed接口和Comparable接口,也就是说DelayQueue里面的元素必须有public int compareTo( T o)和long getDelay(TimeUnit unit)方法存在,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:
  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  • 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
我们来看一下DelayQueue的源码来理解一下:
//可以看出来E元素必须继承Delayed和而Delayed又继承Comparable;
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
 
    private final transient ReentrantLock lock = new ReentrantLock();//安全锁机制
    private final PriorityQueue<E> q = new PriorityQueue<E>();//PriorityQueue来存取元素
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek();
                if (first == null)
                    available.await();
                else {
                    //根据元素的Delay进行判断
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        return q .poll();
                    first = null; // don’t retain ref while waiting
                    if (leader != null)
                       //没到时间阻塞等待
                        available.await();
                    else {
                        Thread thisThread = Thread. currentThread();
                        leader = thisThread;
                        try {
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null ;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }
……
}
我们来看一下DelayQueue的使用实例:
(1)实现一个Student对象作为DelayQueue的元素必须实现Delayed 接口的两个方法;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class Student implements Delayed { //必须实现Delayed接口
       private String name ;
       private long submitTime ;// 交卷时间
       private long workTime ;// 考试时间
       public String getName() {
             return this .name + ” 交卷,用时” + workTime;
      }
       public Student(String name, long submitTime) {
             this.name = name;
             this.workTime = submitTime;
             this.submitTime = TimeUnit.NANOSECONDS.convert(submitTime, TimeUnit.MILLISECONDS ) + System.nanoTime ();
            System. out.println(this.name + ” 交卷,用时” + workTime);
      }
       //必须实现getDelay方法
       public long getDelay(TimeUnit unit) {
//          返回一个延迟时间
             return unit.convert(submitTime – System.nanoTime (), unit.NANOSECONDS );
      }
       //必须实现compareTo方法
       public int compareTo(Delayed o) {
//          比较的方法
            Student that = (Student) o;
             return submitTime > that.submitTime ? 1 : ( submitTime < that.submitTime ? -1 : 0);
      }
}
(2)执行运行类如下:
package demo.thread;
import java.util.concurrent.DelayQueue;
public class DelayQueueTest {
       public static void main(String[] args) throws Exception {
             // 新建一个等待队列
             final DelayQueue<Student> bq = new DelayQueue<Student>();
             for (int i = 0; i < 5; i++) {
                  Student student = new Student(“学生” +i,Math.round((Math. random()*10+i)));
                  bq.put(student); // 将数据存到队列里!
            }
             //获取但不移除此队列的头部;如果此队列为空,则返回 null。
            System. out.println(“bq.peek()”+bq.peek().getName());
             //获取并移除此队列的头部,在可从此队列获得到期延迟的元素,或者到达指定的等待时间之前一直等待(如有必要)。
             //poll(long timeout, TimeUnit unit) 大家可以试一试这个方法
      }
}

运行结果如下:每次运行结果都不一样,一问,我们获得永远是队列里面的第一个元素;

学生0 交卷,用时8
学生1 交卷,用时6
学生2 交卷,用时10
学生3 交卷,用时10
学生4 交卷,用时9
bq.peek()学生1 交卷,用时6

可以慢慢的在以后的工作当中体会DelayQueue的用法;

文章的脚注信息由WordPress的wp-posturl插件自动生成

18 Aug 00:36

Java 9中将移除 Sun.misc.Unsafe

by 曲东方

原文链接    译者:曲东方

灾难将至,Java 9中将移除 Sun.misc.Unsafe

Oracle 正在计划在Java 9中去掉 sun.misc.Unsafe API。 这绝对将是一场灾难,有可能会彻底破坏整个 java 生态圈。 几乎每个使用 java开发的工具、软件基础设施、高性能开发库都在底层使用了 sun.misc.Unsafe。 下面是上面链接中文档提到一个小列表:

  • Netty
  • Hazelcast
  • Cassandra
  • Mockito / EasyMock / JMock / PowerMock
  • Scala Specs
  • Spock
  • Robolectric
  • Grails
  • Neo4j
  • Spring Framework
  • Akka
  • Apache Kafka
  • Apache Wink
  • Apache Storm
  • Apache Hadoop
  • Apache Continuum

… 这个列表很长。。。

然而, Oracle 看起来是铁了心毫无理由的去掉它。下面是一个来自他们邮件列表的评论: n

恕我直言 — sun.misc.Unsafe 必须死掉。 它是“不安全”的。它必须被废弃。请忽略一切理论上(想象中的)羁绊,从此走上正确的道路吧。

这个工程师似乎是毫无根据的憎恨 Unsafe。。。

Oracle应该怎么做?

当前Unsafe 类是一个强有力的工具。 没有必要去掉它。对这个类的特性有些明确的需求,这就是为什么事实上几乎每个 Java 程序都在使用它,不知不觉中许多流行的 Java库也在使用它。

提供完整的文档、发布 Unsafe 类

Oracle 应该接受现实,并将Unsafe转为公开 API,提供完善的文档和开发示例。 当前,没有准确的文档,开发中需要通过 stackoverflow 帖子或者其他一些随机的博客学习怎么使用 Unsafe。 移除 Unsafe 的一个主要论据是:使用它太容易让开发中犯错了。如果有完善的官方文档或许可以改善这一现状。

随 Unsafe一起发布新的替代 API

除了 Unsafe 文档外,Oracle 应该发布一个更易用的 API,提供 Unsafe 相同的功能。 这是上面文档中的提议的一部分。然而这不太应该以移除 Unsafe 为代价。 人们在开发新软件的时候就会逐步过渡到新的 API,Unsafe 就自动被废弃了。

这类似于向 Java 8引入 java.time 包中的新的 DateTime API。 新的日期 API 的引入并不表示之前的DateTime API 被彻底移除或者隐藏到某个特殊 JVM flag 里。那样也肯定会引发一些事故。

实际上最可能会变成什么样子?

根据事情的发展趋势,Oracle 看起来会:

  1. 在 Java 9正常模式下移除 Unsafe 类。
  2. 仅在必须的情况下通过向 JVM 传递一个特殊的 flag 启动 Unsafe

这将导致绝对的灾难!

  • 不仅类似 Cassandra 或Zookeeper 等基础软件,几乎所有的 Java 程序,包括 web 应用也会挂掉,因为他们使用的基础库可能在底层使用了 Unsafe
  • 从此打开 Unsafe flag 将会成为启动 JVM 的默认 flag 之一,因为如果不打开它的话 Java 应用会在毫无提示的情况下崩溃。
  • 因为大多数环境不会默认把这个JVM flag 打开,当他们的系统升级 Java时软件系统会挂掉。 Java 打破了向后兼容的承诺。所有的基础库、软件基础设施从此变为两个版本:
    • Java 9之前的版本 – 使用 Unsafe
    • Java 9兼容 – 不使用 Unsafe
  • 迁移至 Java 9的进程会因此而变缓慢,这将影响整个 Java 生态系统。这将会类似于 Python 2升级到 Python 3的过程。

这种错误 JVM 社区之前曾经犯过

你是不是任务这太荒唐了,Oracle 绝不可能犯这样的错误?事实上它曾做过类似的事情了, 例如Java 7中的字节码校验器

结论

现在是该让大家开始意识到这个问题的时候了。从 JVM中去掉Unsafe或者把它隐藏在某个特殊的 flag 里面势必导致一场灾难。

参考链接

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Java 9中将移除 Sun.misc.Unsafe

文章的脚注信息由WordPress的wp-posturl插件自动生成

14 Aug 00:41

Java 程序优化:字符串操作、基本运算方法等优化策略

by importnewzz

字符串操作优化

字符串对象

字符串对象或者其等价对象 (如 char 数组),在内存中总是占据最大的空间块,因此如何高效地处理字符串,是提高系统整体性能的关键。

String 对象可以认为是 char 数组的延伸和进一步封装,它主要由 3 部分组成:char 数组、偏移量和 String 的长度。char 数组表示 String 的内容,它是 String 对象所表示字符串的超集。String 的真实内容还需要由偏移量和长度在这个 char 数组中进行定位和截取。

String 有 3 个基本特点:

1. 不变性;

2. 针对常量池的优化;

3. 类的 final 定义。

不变性指的是 String 对象一旦生成,则不能再对它进行改变。String 的这个特性可以泛化成不变 (immutable) 模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。

针对常量池的优化指的是当两个 String 对象拥有相同的值时,它们只引用常量池中的同一个拷贝,当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。

下面代码 str1、str2、str4 引用了相同的地址,但是 str3 却重新开辟了一块内存空间,虽然 str3 单独占用了堆空间,但是它所指向的实体和 str1 完全一样。代码如下清单 1 所示。

清单 1. 示例代码
public class StringDemo {
 public static void main(String[] args){
 String str1 = "abc";
 String str2 = "abc";
 String str3 = new String("abc");
 String str4 = str1;
 System.out.println("is str1 = str2?"+(str1==str2));
 System.out.println("is str1 = str3?"+(str1==str3));
 System.out.println("is str1 refer to str3?"+(str1.intern()==str3.intern()));
 System.out.println("is str1 = str4"+(str1==str4));
 System.out.println("is str2 = str4"+(str2==str4));
 System.out.println("is str4 refer to str3?"+(str4.intern()==str3.intern()));
 }
}

输出如清单 2 所示。

清单 2. 输出结果
is str1 = str2?true
is str1 = str3?false
is str1 refer to str3?true
is str1 = str4true
is str2 = str4true
is str4 refer to str3?true

SubString 使用技巧

String 的 substring 方法源码在最后一行新建了一个 String 对象,new String(offset+beginIndex,endIndex-beginIndex,value);该行代码的目的是为了能高效且快速地共享 String 内的 char 数组对象。但在这种通过偏移量来截取字符串的方法中,String 的原生内容 value 数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了速度却浪费了空间。

下面代码演示了使用 substring 方法在一个很大的 string 独享里面截取一段很小的字符串,如果采用 string 的 substring 方法会造成内存溢出,如果采用反复创建新的 string 方法可以确保正常运行。

清单 3.substring 方法演示
import java.util.ArrayList;
import java.util.List;

public class StringDemo {
 public static void main(String[] args){
 List<String> handler = new ArrayList<String>();
 for(int i=0;i<1000;i++){
 HugeStr h = new HugeStr();
 ImprovedHugeStr h1 = new ImprovedHugeStr();
 handler.add(h.getSubString(1, 5));
 handler.add(h1.getSubString(1, 5));
 }
 }

 static class HugeStr{
 private String str = new String(new char[800000]);
 public String getSubString(int begin,int end){
 return str.substring(begin, end);
 }
 }

 static class ImprovedHugeStr{
 private String str = new String(new char[10000000]);
 public String getSubString(int begin,int end){
 return new String(str.substring(begin, end));
 }
 }
}

输出结果如清单 4 所示。

清单 4. 输出结果
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.lang.StringValue.from(Unknown Source)
at java.lang.String.<init>(Unknown Source)
at StringDemo$ImprovedHugeStr.<init>(StringDemo.java:23)
at StringDemo.main(StringDemo.java:9)

ImprovedHugeStr 可以工作是因为它使用没有内存泄漏的 String 构造函数重新生成了 String 对象,使得由 substring() 方法返回的、存在内存泄漏问题的 String 对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。

String 的 split 方法支持传入正则表达式帮助处理字符串,但是简单的字符串分割时性能较差。

对比 split 方法和 StringTokenizer 类的处理字符串性能,代码如清单 5 所示。

切分字符串方式讨论

String 的 split 方法支持传入正则表达式帮助处理字符串,操作较为简单,但是缺点是它所依赖的算法在对简单的字符串分割时性能较差。清单 5 所示代码对比了 String 的 split 方法和调用 StringTokenizer 类来处理字符串时性能的差距。

清单 5.String 的 split 方法演示
import java.util.StringTokenizer;

public class splitandstringtokenizer {
 public static void main(String[] args){
 String orgStr = null;
 StringBuffer sb = new StringBuffer();
 for(int i=0;i<100000;i++){
 sb.append(i);
 sb.append(",");
 }
 orgStr = sb.toString();
 long start = System.currentTimeMillis();
 for(int i=0;i<100000;i++){
 orgStr.split(",");
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 String orgStr1 = sb.toString();
 StringTokenizer st = new StringTokenizer(orgStr1,",");
 for(int i=0;i<100000;i++){
 st.nextToken();
 }
 st = new StringTokenizer(orgStr1,",");
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 String orgStr2 = sb.toString();
 String temp = orgStr2;
 while(true){
 String splitStr = null;
 int j=temp.indexOf(",");
 if(j<0)break;
 splitStr=temp.substring(0, j);
 temp = temp.substring(j+1);
 }
 temp=orgStr2;
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}

输出如清单 6 所示:

清单 6. 运行输出结果
39015
16
15

当一个 StringTokenizer 对象生成后,通过它的 nextToken() 方法便可以得到下一个分割的字符串,通过 hasMoreToken 方法可以知道是否有更多的字符串需要处理。对比发现 split 的耗时非常的长,采用 StringTokenizer 对象处理速度很快。我们尝试自己实现字符串分割算法,使用 substring 方法和 indexOf 方法组合而成的字符串分割算法可以帮助很快切分字符串并替换内容。

由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。

以上实例运行结果差异较大的原因是 split 算法对每一个字符进行了对比,这样当字符串较大时,需要把整个字符串读入内存,逐一查找,找到符合条件的字符,这样做较为耗时。而 StringTokenizer 类允许一个应用程序进入一个令牌(tokens),StringTokenizer 类的对象在内部已经标识化的字符串中维持了当前位置。一些操作使得在现有位置上的字符串提前得到处理。 一个令牌的值是由获得其曾经创建 StringTokenizer 类对象的字串所返回的。

清单 7.split 类源代码
import java.util.ArrayList;

public class Split {
public String[] split(CharSequence input, int limit) { 
int index = 0; 
boolean matchLimited = limit > 0; 
ArrayList<String> matchList = new ArrayList<String>(); 
Matcher m = matcher(input); 
// Add segments before each match found 
while(m.find()) { 
if (!matchLimited || matchList.size() < limit - 1) { 
String match = input.subSequence(index, m.start()).toString(); 
matchList.add(match); 
index = m.end(); 
} else if (matchList.size() == limit - 1) { 
// last one 
String match = input.subSequence(index,input.length()).toString(); 
matchList.add(match); 
index = m.end(); 
} 
} 
// If no match was found, return this 
if (index == 0){ 
return new String[] {input.toString()}; 
}
// Add remaining segment 
if (!matchLimited || matchList.size() < limit){ 
matchList.add(input.subSequence(index, input.length()).toString()); 
}
// Construct result 
int resultSize = matchList.size(); 
if (limit == 0){ 
while (resultSize > 0 && matchList.get(resultSize-1).equals("")) 
resultSize--; 
 String[] result = new String[resultSize]; 
 return matchList.subList(0, resultSize).toArray(result); 
}
}

}

split 借助于数据对象及字符查找算法完成了数据分割,适用于数据量较少场景。

合并字符串

由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。针对超大的 String 对象,我们采用 String 对象连接、使用 concat 方法连接、使用 StringBuilder 类等多种方式,代码如清单 8 所示。

清单 8. 处理超大 String 对象的示例代码
public class StringConcat {
 public static void main(String[] args){
 String str = null;
 String result = "";

 long start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 str = str + i;
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 for(int i=0;i<10000;i++){
 result = result.concat(String.valueOf(i));
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 StringBuilder sb = new StringBuilder();
 for(int i=0;i<10000;i++){
 sb.append(i);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }
}

输出如清单 9 所示。

清单 9. 运行输出结果
375
187
0

虽然第一种方法编译器判断 String 的加法运行成 StringBuilder 实现,但是编译器没有做出足够聪明的判断,每次循环都生成了新的 StringBuilder 实例从而大大降低了系统性能。

StringBuffer 和 StringBuilder 都实现了 AbstractStringBuilder 抽象类,拥有几乎相同的对外借口,两者的最大不同在于 StringBuffer 对几乎所有的方法都做了同步,而 StringBuilder 并没有任何同步。由于方法同步需要消耗一定的系统资源,因此,StringBuilder 的效率也好于 StringBuffer。 但是,在多线程系统中,StringBuilder 无法保证线程安全,不能使用。代码如清单 10 所示。

清单 10.StringBuilderVSStringBuffer
public class StringBufferandBuilder {
public StringBuffer contents = new StringBuffer(); 
public StringBuilder sbu = new StringBuilder();

public void log(String message){ 
for(int i=0;i<10;i++){ 
/*
contents.append(i); 
contents.append(message); 
contents.append("\n"); 
*/
contents.append(i);
contents.append("\n");
sbu.append(i);
sbu.append("\n");
} 
} 
public void getcontents(){ 
//System.out.println(contents); 
System.out.println("start print StringBuffer");
System.out.println(contents); 
System.out.println("end print StringBuffer");
}
public void getcontents1(){ 
//System.out.println(contents); 
System.out.println("start print StringBuilder");
System.out.println(sbu); 
System.out.println("end print StringBuilder");
}

 public static void main(String[] args) throws InterruptedException { 
StringBufferandBuilder ss = new StringBufferandBuilder(); 
runthread t1 = new runthread(ss,"love");
runthread t2 = new runthread(ss,"apple");
runthread t3 = new runthread(ss,"egg");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}

}

class runthread extends Thread{ 
String message; 
StringBufferandBuilder buffer; 
public runthread(StringBufferandBuilder buffer,String message){ 
this.buffer = buffer;
this.message = message; 
} 
public void run(){ 
while(true){ 
buffer.log(message); 
//buffer.getcontents();
buffer.getcontents1();
try {
sleep(5000000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} 
} 

}

输出结果如清单 11 所示。

清单 11. 运行结果
start print StringBuffer
0123456789
end print StringBuffer
start print StringBuffer
start print StringBuilder
01234567890123456789
end print StringBuffer
start print StringBuilder
01234567890123456789
01234567890123456789
end print StringBuilder
end print StringBuilder
start print StringBuffer
012345678901234567890123456789
end print StringBuffer
start print StringBuilder
012345678901234567890123456789
end print StringBuilder

StringBuilder 数据并没有按照预想的方式进行操作。StringBuilder 和 StringBuffer 的扩充策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立新的 char 数组,然后将原数组中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。如果能够预先评估大小,会提高性能。

 

数据定义、运算逻辑优化

使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快。其他变量,如静态变量、实例变量等,都在堆 (heap) 中创建,读写速度较慢。清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比。

清单 12. 局部变量 VS 静态变量
public class variableCompare {
public static int b = 0;
 public static void main(String[] args){
 int a = 0;
 long starttime = System.currentTimeMillis();
 for(int i=0;i<1000000;i++){
 a++;//在函数体内定义局部变量
 }
 System.out.println(System.currentTimeMillis() - starttime);

 starttime = System.currentTimeMillis();
 for(int i=0;i<1000000;i++){
 b++;//在函数体内定义局部变量
 }
 System.out.println(System.currentTimeMillis() - starttime);
 }
}

运行后输出如清单 13 所示。

清单 13. 运行结果
0
15

以上两段代码的运行时间分别为 0ms 和 15ms。由此可见,局部变量的访问速度远远高于类的成员变量。

位运算代替乘除法

位运算是所有的运算中最为高效的。因此,可以尝试使用位运算代替部分算数运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。清单 14 所示代码是一段使用算数运算的实现。

清单 14. 算数运算
public class yunsuan {
 public static void main(String args[]){
 long start = System.currentTimeMillis();
 long a=1000;
 for(int i=0;i<10000000;i++){
 a*=2;
 a/=2;
 }
 System.out.println(a);
 System.out.println(System.currentTimeMillis() - start);
 start = System.currentTimeMillis();
 for(int i=0;i<10000000;i++){
 a<<=1;
 a>>=1;
 }
 System.out.println(a);
 System.out.println(System.currentTimeMillis() - start);
 }
}

运行输出如清单 15 所示。

清单 15. 运行结果
1000
546
1000
63

两段代码执行了完全相同的功能,在每次循环中,整数 1000 乘以 2,然后除以 2。第一个循环耗时 546ms,第二个循环耗时 63ms。

替换 switch

关键字 switch 语句用于多条件判断,switch 语句的功能类似于 if-else 语句,两者的性能差不多。但是 switch 语句有性能提升空间。清单 16 所示代码演示了 Switch 与 if-else 之间的对比。

清单 16.Switch 示例
public class switchCompareIf {

public static int switchTest(int value){
int i = value%10+1;
switch(i){
case 1:return 10;
case 2:return 11;
case 3:return 12;
case 4:return 13;
case 5:return 14;
case 6:return 15;
case 7:return 16;
case 8:return 17;
case 9:return 18;
default:return -1;
}
}

public static int arrayTest(int[] value,int key){
int i = key%10+1;
if(i>9 || i<1){
return -1;
}else{
return value[i];
}
}

 public static void main(String[] args){
 int chk = 0;
 long start=System.currentTimeMillis();
 for(int i=0;i<10000000;i++){
 chk = switchTest(i);
 }
 System.out.println(System.currentTimeMillis()-start);
 chk = 0;
 start=System.currentTimeMillis();
 int[] value=new int[]{0,10,11,12,13,14,15,16,17,18};
 for(int i=0;i<10000000;i++){
 chk = arrayTest(value,i);
 }
 System.out.println(System.currentTimeMillis()-start);
 }
}

运行输出如清单 17 所示。

清单 17. 运行结果
172
93

使用一个连续的数组代替 switch 语句,由于对数据的随机访问非常快,至少好于 switch 的分支判断,从上面例子可以看到比较的效率差距近乎 1 倍,switch 方法耗时 172ms,if-else 方法耗时 93ms。

一维数组代替二维数组

JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList、Vector 等,数组的优点是随机访问性能非常好。一维数组和二维数组的访问速度不一样,一维数组的访问速度要优于二维数组。在性能敏感的系统中要使用二维数组,尽量将二维数组转化为一维数组再进行处理,以提高系统的响应速度。

清单 18. 数组方式对比
public class arrayTest {
 public static void main(String[] args){
 long start = System.currentTimeMillis();
 int[] arraySingle = new int[1000000];
 int chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingle.length;j++){
 arraySingle[j] = j;
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingle.length;j++){
 chk = arraySingle[j];
 }
 }
 System.out.println(System.currentTimeMillis() - start);

 start = System.currentTimeMillis();
 int[][] arrayDouble = new int[1000][1000];
 chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDouble.length;j++){
 for(int k=0;k<arrayDouble[0].length;k++){
 arrayDouble[i][j]=j;
 }
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDouble.length;j++){
 for(int k=0;k<arrayDouble[0].length;k++){
 chk = arrayDouble[i][j];
 }
 }
 }
 System.out.println(System.currentTimeMillis() - start);

 start = System.currentTimeMillis();
 arraySingle = new int[1000000];
 int arraySingleSize = arraySingle.length;
 chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingleSize;j++){
 arraySingle[j] = j;
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arraySingleSize;j++){
 chk = arraySingle[j];
 }
 }
 System.out.println(System.currentTimeMillis() - start);

 start = System.currentTimeMillis();
 arrayDouble = new int[1000][1000];
 int arrayDoubleSize = arrayDouble.length;
 int firstSize = arrayDouble[0].length;
 chk = 0;
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDoubleSize;j++){
 for(int k=0;k<firstSize;k++){
 arrayDouble[i][j]=j;
 }
 }
 }
 for(int i=0;i<100;i++){
 for(int j=0;j<arrayDoubleSize;j++){
 for(int k=0;k<firstSize;k++){
 chk = arrayDouble[i][j];
 }
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }
}

运行输出如清单 19 所示。

清单 19. 运行结果
343
624
287
390

第一段代码操作的是一维数组的赋值、取值过程,第二段代码操作的是二维数组的赋值、取值过程。可以看到一维数组方式比二维数组方式快接近一半时间。而对于数组内如果可以减少赋值运算,则可以进一步减少运算耗时,加快程序运行速度。

提取表达式

大部分情况下,代码的重复劳动由于计算机的高速运行,并不会对性能构成太大的威胁,但若希望将系统性能发挥到极致,还是有很多地方可以优化的。

清单 20. 提取表达式
public class duplicatedCode {
 public static void beforeTuning(){
 long start = System.currentTimeMillis();
 double a1 = Math.random();
 double a2 = Math.random();
 double a3 = Math.random();
 double a4 = Math.random();
 double b1,b2;
 for(int i=0;i<10000000;i++){
 b1 = a1*a2*a4/3*4*a3*a4;
 b2 = a1*a2*a3/3*4*a3*a4;
 }
 System.out.println(System.currentTimeMillis() - start);
 }

 public static void afterTuning(){
 long start = System.currentTimeMillis();
 double a1 = Math.random();
 double a2 = Math.random();
 double a3 = Math.random();
 double a4 = Math.random();
 double combine,b1,b2;
 for(int i=0;i<10000000;i++){
 combine = a1*a2/3*4*a3*a4;
 b1 = combine*a4;
 b2 = combine*a3;
 }
 System.out.println(System.currentTimeMillis() - start);
 }

 public static void main(String[] args){
 duplicatedCode.beforeTuning();
 duplicatedCode.afterTuning();
 }
}

运行输出如清单 21 所示。

清单 21. 运行结果
202
110

两段代码的差别是提取了重复的公式,使得这个公式的每次循环计算只执行一次。分别耗时 202ms 和 110ms,可见,提取复杂的重复操作是相当具有意义的。这个例子告诉我们,在循环体内,如果能够提取到循环体外的计算公式,最好提取出来,尽可能让程序少做重复的计算。

优化循环

当性能问题成为系统的主要矛盾时,可以尝试优化循环,例如减少循环次数,这样也许可以加快程序运行速度。

清单 22. 减少循环次数
public class reduceLoop {
public static void beforeTuning(){
 long start = System.currentTimeMillis();
 int[] array = new int[9999999];
 for(int i=0;i<9999999;i++){
 array[i] = i;
 }
 System.out.println(System.currentTimeMillis() - start);
}

public static void afterTuning(){
 long start = System.currentTimeMillis();
 int[] array = new int[9999999];
 for(int i=0;i<9999999;i+=3){
 array[i] = i;
 array[i+1] = i+1;
 array[i+2] = i+2;
 }
 System.out.println(System.currentTimeMillis() - start);
}

public static void main(String[] args){
reduceLoop.beforeTuning();
reduceLoop.afterTuning();
}
}

运行输出如清单 23 所示。

清单 23. 运行结果
265
31

这个例子可以看出,通过减少循环次数,耗时缩短为原来的 1/8。

布尔运算代替位运算

虽然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算确实是非常错误的选择。在条件判断时,Java 会对布尔运算做相当充分的优化。假设有表达式 a、b、c 进行布尔运算“a&&b&&c”,根据逻辑与的特点,只要在整个布尔表达式中有一项返回 false,整个表达式就返回 false,因此,当表达式 a 为 false 时,该表达式将立即返回 false,而不会再去计算表达式 b 和 c。若此时,表达式 a、b、c 需要消耗大量的系统资源,这种处理方式可以节省这些计算资源。同理,当计算表达式“a||b||c”时,只要 a、b 或 c,3 个表达式其中任意一个计算结果为 true 时,整体表达式立即返回 true,而不去计算剩余表达式。简单地说,在布尔表达式的计算中,只要表达式的值可以确定,就会立即返回,而跳过剩余子表达式的计算。若使用位运算 (按位与、按位或) 代替逻辑与和逻辑或,虽然位运算本身没有性能问题,但是位运算总是要将所有的子表达式全部计算完成后,再给出最终结果。因此,从这个角度看,使用位运算替代布尔运算会使系统进行很多无效计算。

清单 24. 运算方式对比
public class OperationCompare {
 public static void booleanOperate(){
 long start = System.currentTimeMillis();
 boolean a = false;
 boolean b = true;
 int c = 0;
 //下面循环开始进行位运算,表达式里面的所有计算因子都会被用来计算
 for(int i=0;i<1000000;i++){
 if(a&b&"Test_123".contains("123")){
 c = 1;
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }

 public static void bitOperate(){
 long start = System.currentTimeMillis();
 boolean a = false;
 boolean b = true;
 int c = 0;
 //下面循环开始进行布尔运算,只计算表达式 a 即可满足条件
 for(int i=0;i<1000000;i++){
 if(a&&b&&"Test_123".contains("123")){
 c = 1;
 }
 }
 System.out.println(System.currentTimeMillis() - start);
 }

 public static void main(String[] args){
 OperationCompare.booleanOperate();
 OperationCompare.bitOperate();
 }
}

运行输出如清单 25 所示。

清单 25. 运行结果
63
0

实例显示布尔计算大大优于位运算,但是,这个结果不能说明位运算比逻辑运算慢,因为在所有的逻辑与运算中,都省略了表达式“”Test_123″.contains(“123″)”的计算,而所有的位运算都没能省略这部分系统开销。

使用 arrayCopy()

数据复制是一项使用频率很高的功能,JDK 中提供了一个高效的 API 来实现它。System.arraycopy() 函数是 native 函数,通常 native 函数的性能要优于普通的函数,所以,仅处于性能考虑,在软件开发中,应尽可能调用 native 函数。ArrayList 和 Vector 大量使用了 System.arraycopy 来操作数据,特别是同一数组内元素的移动及不同数组之间元素的复制。arraycopy 的本质是让处理器利用一条指令处理一个数组中的多条记录,有点像汇编语言里面的串操作指令 (LODSB、LODSW、LODSB、STOSB、STOSW、STOSB),只需指定头指针,然后开始循环即可,即执行一次指令,指针就后移一个位置,操作多少数据就循环多少次。如果在应用程序中需要进行数组复制,应该使用这个函数,而不是自己实现。具体应用如清单 26 所示。

清单 26. 复制数据例子
public class arrayCopyTest {
public static void arrayCopy(){
int size = 10000000;
 int[] array = new int[size];
 int[] arraydestination = new int[size];
 for(int i=0;i<array.length;i++){
 array[i] = i;
 }
 long start = System.currentTimeMillis();
 for(int j=0;j>1000;j++){
 System.arraycopy(array, 0, arraydestination, 0, size);//使用 System 级别的本地 arraycopy 方式
 }
 System.out.println(System.currentTimeMillis() - start);
}

public static void arrayCopySelf(){
int size = 10000000;
 int[] array = new int[size];
 int[] arraydestination = new int[size];
 for(int i=0;i<array.length;i++){
 array[i] = i;
 }
 long start = System.currentTimeMillis();
 for(int i=0;i<1000;i++){
 for(int j=0;j<size;j++){
 arraydestination[j] = array[j];//自己实现的方式,采用数组的数据互换方式
 }
 }
 System.out.println(System.currentTimeMillis() - start);
}

 public static void main(String[] args){
 arrayCopyTest.arrayCopy();
 arrayCopyTest.arrayCopySelf();
 }
}

输出如清单 27 所示。

清单 27. 运行结果
0
23166

上面的例子显示采用 arraycopy 方法执行复制会非常的快。原因就在于 arraycopy 属于本地方法,源代码如清单 28 所示。

清单 28.arraycopy 方法
public static native void arraycopy(Object src, int srcPos, 
Object dest, int destPos, 
int length);

src – 源数组;srcPos – 源数组中的起始位置; dest – 目标数组;destPos – 目标数据中的起始位置;length – 要复制的数组元素的数量。清单 28 所示方法使用了 native 关键字,调用的为 C++编写的底层函数,可见其为 JDK 中的底层函数。

 

结束语

Java 程序设计优化有很多方面可以入手,作者将以系列的方式逐步介绍覆盖所有领域。本文是该系列的第一篇文章,主要介绍了字符串对象操作相关、数据定义方面的优化方案、运算逻辑优化及建议,从实际代码演示入手,对优化建议及方案进行了验证。作者始终坚信,没有什么优化方案是百分百有效的,需要读者根据实际情况进行选择、实践。

可能感兴趣的文章

12 Aug 00:37

Linux Load编程竞赛

by Tim

Linux load average的意思就是说 目前有ready的进程 但是cpu都在满,然后这个数是一分钟的平均值。公式:loadvg = tasks running + tasks waiting (for cores) + tasks blocked. 让CPU满是最简单的方式

为了更好理解load average,Tim某天在群征集一段代码使load average最高。于是乎,群友纷纷出手。

Erlang版

[~]$ cat load10
#!/usr/bin/env escript
%% -- erlang --
%%! -smp enable -sname load10
main(_) ->
I = erlang:system_info(scheduler_id),
random(5).

random(I) when I > 10 ->
random(I+1);
random(I) ->
spawn(fun() -> random(I+1) end),
random(I+1).

此代码将Linux load跑到了10-20,取得了领先

Shell版

#!/bin/sh
for((i=0;i&lt;10;i++));do
{
for((j=0;j&lt;1000000000000;j++));do echo '1'&gt;&gt;1; done
}&
done

Ruby

ruby -e "require 'thread';t = Mutex.new;10.times {fork {10000000.times{t.synchronize {x=Time.now}}}}"

Java版

import java.io.*;

public class LoadTest {

  public static void main(String[] args) throws IOException, InterruptedException {
      int count = 1000;
      for (int i = 0; i < count; i++) {
          final Thread t = new Thread(new Runnable() {
              public void run() {
                  try {
                      while (true) {
                          RandomAccessFile file = new RandomAccessFile("/tmp/test.bin", "rw");
                          file.seek(1024 * 1024 * 500);
                          file.write(1);
                          file.close();
                      }
                  } catch (IOException e) {
                  }
              }
          });
          t.start();
      }
      Thread.currentThread().join();
  }
}

C语言版

void main () {
int i=0;
for (i = 0; i < 1000; i++) {
if (fork () > 0) {
continue;
}
while (1) ;
}
getchar();
}

此版本远远超过了上面的Erlang版本,将load拉到1000+

Java版本2

public class Test {

   public static void main(String[] args) {
       for (int i = 0; i < 1000; i++) {
           new Thread() {
               @Override
               public void run() {
                   while (true) ;
               }
           }.start();
       }
   }
}

此版本和上面C版本效果类似

部分讨论:

不对呀,这个方法以前不行
CentOS 6吧[惊讶]
CentOS 5
有几个版本有bug,cpu的hz拿的不对,load特别低,7就好了[坏笑]

Java版本3

写两类线程,一类进行lockObject wait, 一类进行lockObject notifyall。 cpu上下文切换消耗 竞争激烈。Load能很高。

public class LoadHighDemo {

   public static void main(String[] args) throws Exception {
       LoadHighDemo demo = new LoadHighDemo();
       demo.runTest();
   }

   private void runTest() throws Exception {
       Object[] locks = new Object[5000];
       for (int i = 0; i < 5000; i++) {
           locks[i] = new Object();
           new Thread(new WaitTask(locks[i])).start();
           new Thread(new NotifyTask(locks[i])).start();
       }
   }

   class WaitTask implements Runnable {

       private Object lockObject = null;

       public WaitTask(Object obj) {
           lockObject = obj;
       }

       public void run() {
           while (true) {
               try {
                   synchronized (lockObject) {
                       lockObject.wait(new java.util.Random().nextInt(10));
                   }
               } catch (Exception e) {;
               }
           }
       }
   }

   class NotifyTask implements Runnable {

       private Object lockObject = null;

       public NotifyTask(Object obj) {
           lockObject = obj;
       }

       public void run() {
           while (true) {
               synchronized (lockObject) {
                   lockObject.notifyAll();
               }
               try {
                   Thread.sleep(new java.util.Random().nextInt(5));
               } catch (InterruptedException e) {
               }
           }
       }
   }
}

上物理机把5000个线程改成更多,如50000也行。
直接while true改5000应该能更多……
我的应该能一直上涨, load 1000, 2000, 3000

可以试试parkNanos(1)

刚才卡了。。。
看来不能在本机上跑

Python版

把5分钟 load avg 稳定在1024了
在本群感到了一种情怀。。。

#!/usr/bin/env python
import os
import sys
import time

def load_add_1():
time.sleep(30)
fd=os.open("test.txt",os.O_CREAT|os.O_RDWR|os.O_SYNC, 0644)
for i in xrange(10000*100):
os.write(fd," "*100)
sys.exit(0)

for i in xrange(8192):
if os.fork() == 0:
load_add_1()

此版本将load跑到8192

C语言版本2

https://gist.github.com/hongqn/37cdfde04d0c5a03fede

#include <unistd.h>
#include <stdio.h>

#define LOAD 16384

int main() {
int i;
char s[256];

for (i=0; i<LOAD; i++) {
if (vfork()) {
return 0;
}
}
scanf("%s", s);
return 0;
}

macbook 上 virtualbox 里一个 2G mem 单 core cpu 的虚机就可以轻松达到16,000
这个的原理是通过 vfork 产生指定个数的 D 状态进程,从而提高 load

Vfork() differs from fork in that the child borrows the parent’s memory and thread of control until a call to execve(2) or an exit (either by a call to exit(2) or abnormally.) The parent process is suspended while the child is using its resources.
vfork 的子进程只要不 execve 或者退出,父进程就一直挂着(在D状态)。这里就是让最后一个子进程用 scanf 等输入

通过以上题目,你是看到了热闹,还是看到了门道?

 

Similar Posts:
11 Aug 02:09

Java GC总结

by Kevin Lynx

Java GC相关的文章有很多,本文只做概要性总结,主要内容来源于<深入理解Java虚拟机>。

对象存活性判定

对象存活性判定用于确定一个对象是死是活,死掉的对象则需要被垃圾回收。主要包括的方法:

  • 引用计数
  • 可达性分析

可达性分析的基本思想是:

通过一系列的称为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链项链时,则证明此对象是不可用的。

在Java中有很多种类的对象可以作为GC Roots,例如类静态属性引用的对象。

垃圾收集算法

确定了哪些对象是需要回收之后,就可以运用各种垃圾收集算法收集这些对象,例如直接回收内存,或者回收并移动整理内存。

主要包括:

  • 标记清除(Mark-Sweep)算法:首先标记出需要回收的对象,然后统一回收被标记的对象
  • 复制(Copying)算法:将可用内存分块,当一块内存用完后将存活对象复制到其他块,并统一回收不使用的块。Java中新生代对象一般使用该方法
  • 标记整理(Mark-Compact)算法:基本同标记清除,不同的是回收时是把可用对象进行移动,以避免内存碎片问题
  • 分代收集,将内存分区域,不同区域采用不同的算法,例如Java中的新生代及老年代

如上,Java Hotspot虚拟机实现中将堆内存分为3大区域,即新生代、老年代、永久代。新生代中又分了eden、survivor0及survivor1,采用复制算法;老年代则采用标记清除及标记整理;永久代存放加载的类,类似于代码段,但同样会发生GC。

垃圾收集器

垃圾收集算法在实现时会略有不同,不同的实现称为垃圾收集器。不同的垃圾收集器适用的范围还不一样,有些收集器仅能用于新生代,有些用于老年代,有些新生代老年代都可以被使用。垃圾收集器可通过JVM启动参数指定。

上图中展示了新生代(年轻代)和老年代可用的各种垃圾收集器,图中的连线表示两种收集器可以配合使用。

  • Serial收集器,单线程收集,复制算法
  • ParNew收集器,Serial收集器的多线程版本
  • Parallel Scavenge收集器,复制算法,吞吐量优先的收集器,更高效率地利用CPU时间,适合用于服务器程序
  • Serial Old收集器,单线程收集,标记整理算法
  • Parallel Old收集器,标记整理算法,Parallel Scavenge收集器的老年代版本
  • CMS(Concurrent Mark Sweep)收集器,标记清除算法,以获取最短停顿时间为目标的收集器
  • G1收集器,较新的收集器实现

JVM有些参数组合了各种收集器,例如:

  • UseConcMarkSweepGC:使用ParNew + CMS + Serial Old收集器
  • UseParallelGC,运行在server模式下的默认值,使用Parallel Scavenge + Serial Old 收集器

GC日志

生产服务器一般会配置GC日志,以在故障时能够分析问题所在,一般的应用可配置以下JVM参数:

-XX:+UseParallelGC -XX:+DisableExplicitGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:./logs/gc.log 

输出日志类似:

1456772.057: [GC [PSYoungGen: 33824K->96K(33920K)] 53841K->20113K(102208K), 0.0025050 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1456863.534: [GC [PSYoungGen: 33824K->96K(33920K)] 53841K->20113K(102208K), 0.0020050 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1456962.061: [GC [PSYoungGen: 33824K->128K(33920K)] 53841K->20145K(102208K), 0.0014150 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
  • 1456772.057,自JVM启动后的时间值
  • GC表示本次进行的是一次minor GC,即年轻代中的GC
  • PSYoungGen垃圾收集器类型,这里是Parallel Scavenge
  • 33824K->96K(33920K),收集前后新生代大小,33920K为新生代总大小(eden+ 1 survivor)
  • 53841K->20113K(102208K),堆总大小及GC前后大小
  • 0.0025050 secs,GC时停顿时间

常见策略

JVM GC相关的有一些策略值得注意:

  • 对象优先在eden分配,当回收时(Eden区可用内存不够),将Eden和当前Survivor还存活着的对象一次性复制到另外一块Survivor,最后清理Eden和刚才用过的Survivor。这个过程称为一次MinorGC,每次MinorGC就会增加活着对象的年龄,当年龄超过某值(-XX:MaxTenuringThreashold)时,就会被转移到老年代(Tenured)。老年代发生GC时被称为FullGC
  • 每一次发生MinorGC而存活下来的对象其年龄都会加1,较老的对象会进入老年代
  • 当分配大对象(> PretenureSizeThreshold)时,其就会直接进入老年代
  • 当年轻代(Eden+Survivor)不足以容纳存活对象时,这些对象会被全部放入老年代(分配担保机制)

原文地址:http://codemacro.com/2015/08/10/java-gc-summary/
written byKevin Lynx posted athttp://codemacro.com

11 Aug 00:41

CGroup 介绍、应用实例及原理描述

CGroup 技术被广泛用于 Linux 操作系统环境下的物理分割,是 Linux Container 技术的底层基础技术,是虚拟化技术的基础。本文首先介绍了 Cgroup 技术,然后通过在 CentOS 操作系统上部署、配置、运行一个实际多线程示例的方式让读者对物理限制 CPU 核的使用有一个大概的了解,接着通过讲解 CGroup 内部的设计原理来让读者有进一步的深入了解 CGroup 技术。
10 Aug 04:45

高效 MacBook 工作环境配置

by 伯乐

工欲善其事,必先利其器,工具永远都是用来解决问题的,没必要为了工具而工具,一切工具都是为了能快速准确的完成工作和学习任务而服务。

本文记录 MacBook 整个配置过程,供新入手MacBook和觉得MacBook比较难用的同学参考。

// 作者:正鹏 & @隃墨,伯乐在线已获转载许可。

1. 硬件提升

笔记本电脑的特点是携带方便,缺点是屏幕太小,因此你首先需要再申请领用一个外接显示器,多一个屏幕会大大减少你切换应用程序的次数,显著提升你的工作效率,别忘了同时申请一个Mini DP转VGA的转接头用于连接显示器。为了配合多显示器,后面会推荐一个软件来管理多显示器窗口。

如果你资金宽裕,可以买个机械键盘和无线鼠标,进一步提升工作效率。

2. 系统设置

2.1 将功能键(F1-F12)设置为标准的功能键

MacBook键盘最上面一排的功能键(F1-F12)默认是系统亮度和声音之类的快捷设置,当MacBook作为你的娱乐电脑时,这样的默认设置是非常方便的,但是对于将MacBook作为工作电脑而且需要频繁使用功能键(F1-F12)的人,最好将功能键(F1-F12)的行为设置为标准的功能键。

首先打开System Preferences,点击Keyboard图标,勾选上Use all F1, F2, etc. keys as standard function keys。以后如果你要调节音量,就按住键盘左下角的fn键再按F11或者F12。

211
图2.1-1

2.2 设置Trackpad(触摸板)轻触为单击

当你首次使用MacBook,是否会觉得触摸板一点都不顺滑?那是因为你需要做如下设置。
打开System Preferences,点击Trackpad图标,勾选Tap to click选项,现在手指轻轻一碰触摸板,就达到鼠标单击的顺滑效果。

2.3 将Dock停靠在屏幕左边

为什么要将Dock停靠在屏幕左边?MacBook的屏幕是一个长方形,如果你将Dock放在下面,那么屏幕的可用宽度就会减少,另外人眼阅读时的顺序是从左往右,因此Dock放在左边更适合将MacBook作为工作电脑的人。

打开System Preferences,点击Dock图标,

  1. 将图标的Size调到合适大小
  2. 关闭Magnification特效(即鼠标放到Dock上图标放大的效果,此效果干扰注意力)
  3. Position on screen一栏,选择Left
  4. 勾选Minimize window into application icon

231
图2.3-1

2.4 全键盘控制模式

全键盘控制模式是什么? 举一个例子,如下图所示,我正在写一个文档,此文档还没有保存,也没有文件名,如果不不小心点了关闭按钮,将会弹出一个对话框:

241
图2.4-1

当前,[Save]按钮处于默认激活状态,按回车将会弹出保存对话框。但是如果我不想保存呢? 只能通过鼠标或者触摸板来移动光标后点击[Don't Save]来取消保存。那我能不能通过键盘控制光标激活[Don't Save]按钮呢? 答案是肯定的,做一个简单设置就好。

如图,首先打开System Preferences,点击Keyboard图标,选择Shortcuts这个Tab, 选中All controls

242
图2.4-2

现在当我再次试图关闭一个未保存的文件时,新弹出的对话框如下,有了些许变化,在[Don't Save]按钮上多了一个蓝色的外框,当你按键盘上的tab键的时候,蓝色的外框会在3个按钮间切换。 假设现在蓝色的外框在[Don't Save]按钮上,你按下回车,却发现系统依然进入了保存文件对话框,为什么蓝色的外框不起作用呢?那是因为蓝色的外框选中的按钮是由空格键触发的,当你按下空格键,系统就会不保存文件直接退出。 这样当你不方便使用鼠标和触摸板的时候,可以更快速的和你的MacBook交互。

243
图2.4-3

2.5 快速锁定屏幕

如果你长时间离开电脑,最好锁定你的屏幕,以防止数据泄露。 那如何快速的锁定你的MacBook呢? 答案是只需要一摸触摸板或者一甩鼠标就可以了。

  • 打开System Preferences,点击Desktop & Screen Saver图标,选择Screen Saver这个Tab,再点击Hot Corners...,在弹出的如下界面里面,右下角选择Put Display to Sleep,点击OK确定。251
    图2.5-1
  • 再打开System Preferences,点击Security & Privacy图标,在GeneralTab内,勾选Require password[immediately] after sleep or screen save begins

252
图2.5-2

现在当你离开电脑前时,记得一摸触摸板或者一甩鼠标将光标快速的移到屏幕的右下角,MacBook将立刻进入Screen Saver模式并且需要密码才能进入桌面。

3. 系统常用快捷键

点击这个文档,学习系统快捷键,适当使用快捷键将会提升你的工作效率。

4. 日常软件推荐

4.1 中文输入法

系统自带的输入法不是很好用,推荐安装搜狗输入法或者RIME输入法。安装完成后,打开System Preferences,选择Keyboard,切换到Shortcuts这个Tab下,勾选Select the previous input source,并点击上述文字后面的空白处,设置快捷键为Ctrl+Space(即如图所示的^Space)。

411
图4.1-1

4.2 窗口管理软件 – SizeUp

  1. 你是否经常想让某个Word文档占满屏幕的左半部分,旺旺聊天占满屏幕的右半部分,从而一边对着文档一边和小伙伴聊需求?
  2. 终于搞好了外接显示器,你是否经常将某个窗口在笔记本和外接显示器屏幕之间直接来回拖动?

SizeUp快速解决这样的需求,该软件可以永久免费试用,下载安装后打开SizeUp,再打开旺旺,快捷键按下control+option+command + M,则旺旺就会立即进入全屏模式。

然而大部分情况下,你会看到如下这个提示,这是因为SizeUp需要你的授权才能控制窗口。

421
图4.2-1

直接点击Open System Preferences或者打开System Preferences,点击Security & Privacy图标,在PrivacyTab内,点击Accessibility,然后将SizeUp加到右边的列表里面。(提示:你可能需要先点击右下角的黄色锁,输入密码后才能编辑右边的列表。)

422
图4.2-2

如果你此时接上了外接显示器,快捷键按下control+option + 方向键右键,则当前左边显示器激活的最前端窗口将被立即发送到右边的显示器。

下面列举一些SizeUp常用的快捷键,更多的快捷键和使用方式请查询其官方网站

  • control+option+command + M : 使当前窗口全屏
  • control+option+command + 方向键上键 : 使当前窗口占用当前屏幕上半部分
  • control+option+command + 方向键下键 : 使当前窗口占用当前屏幕下半部分
  • control+option+command + 方向键左键 : 使当前窗口占用当前屏幕左半部分
  • control+option+command + 方向键右键 : 使当前窗口占用当前屏幕右半部分
  • control+option + 方向键左键 : 将当前窗口发送到左边显示器屏幕
  • control+option + 方向键右键 : 将当前窗口发送到右边显示器屏幕

4.3 查找文件和应用程序以及无限想象力 – Alfred

如果你曾经使用过MacBook,你应该接触过Spotlight,就是屏幕中间弹出一个长条输入框,你输入文件名或者应用程序名,Spotlight将模糊查找到对应的候选项,按回车快速的打开你需要的文件或程序。

Alfred的能力远远超过了Spotlight, 你可以直接下载免费版安装使用,Alfred另外还提供了更强大的工作流(Workflows)和剪切板(Clipboard)管理等高级功能,需要购买Powerpack。对于日常的操作,免费版已经足够使用了。

因为Alfred可以完全取代Spotlight,下面先删除Spotlight占用的快捷键command + 空格,以供Alfred将来使用。

打开System Preferences,选择Keyboard,切换到Shortcuts这个Tab下,点击Spotlight,取消对应的2个快捷键设置。

431
图4.3-1

打开Alfred,在菜单栏点击Alfred图标,打开Preferences...

432
图4.3-2

如下图所示,设置Alfred的快捷键为command + 空格

433
图4.3-3

现在按下快捷键command + 空格,输入dash,则Alfred不区分大小写的将所有包含dash的应用程序,文档以及历史网址都列出来了,如下图所示,回车打开Dashcommand+2打开本Dashboard,你还可以移动键盘上下键或者光标来选择目标。


图4.3-4

更多关于Alfred的使用方式和无限想象力,请参考官方网站或者网上现有的大量的教程。

下面简单演示一下剪切板管理厂内查人工作流的使用。如下图所示,我使用快捷键打开剪切板管理器,列出来我最近复制过的文本片段,我可以快速的选取这些文本片段或者输入部分字符来查找

435
图4.3-5

4.4 聪明又美丽的日历 — Fantastical 2

打开Fantastical 2的网站,你一定会被她漂亮的外表所吸引,最可贵的是Fantastical还很聪明,当你在日历里面新建一个提醒的时候,输入如下内容“HTML training at 7:30pm tomorrow alert 5 min”, 则Fantastical会自动将日期设置为明天,然后将开始时间设置为晚上7点半,并且提前5分钟提醒,是不是很聪明?

441
图4.4-1

4.5 来杯免费咖啡 — Caffeine

今天下午给大老板和重要客户演示PPT,你仿佛看到了升职加薪走上人生巅峰,当你打开MacBook接上投影仪,口若悬河的讲解,突然MacBook进入休眠模式了,画面太美了,我不敢想了。

你应该立刻安装这款免费的良心软件—Caffeine,设置开机启动,点一下状态栏的咖啡杯图标,当咖啡是满的时候,MacBook将不会进入休眠模式,再点一下咖啡杯空了就正常休眠,我默认设置开机启动,咖啡杯保持满满的状态。

4.6 快速切换和打开应用程序 — Manico

MacBook系统默认设置了一个快捷键来显示当前运行中的应用程序,同时按下tab + command,将看到如下图的样式:

461
图4.6-1

如果你想要却换到Firefox,需要再按一下tab,如果要切换到日历,需要按两下‘tab’,如果一次性打开10几个应用程序,你经常需要按十几下tab才能却换到想要的程序。

Manico专为这个场景而设计,安装好后打开,默认快捷键是按住option,如图所示,此时按下数字7就能快速打开编号为7地图

462
图4.6-2

另外,推荐设置Manico使用左手边的字母加数字做索引,方便仅仅用左手就能快速切换应用程序。在菜单栏点击Manico图标,打开Preferences..., 在AppearanceTab里面,选择Uses left hand areaUse numeric and alphabet

463
图4.6-3

4.7 随心所欲的复制粘贴以及无限想象 — PopClip

  • 日常工作中,你有多少次是从一个应用程序复制一段文本然后粘贴到另外一个地方?
  • 有多少次是复制一个网址然后打开浏览器粘贴到地址栏然后回车打开?
  • 有多少次是复制一个名词,然后打开浏览器找到搜索引擎来搜索?

这些重复的操作模式都是可以简化的,你唯一需要的就是PopClip,当你选中一段文字(如下图,选中“当日收益”),PopClip就会弹出来一个快捷操作栏,你可以复制,剪切或者粘贴,更为强大的是,PopClip提供了很多免费的插件,例如使用指定的搜索引擎搜索选中的文字,或者选中英文单词做大小写转换等等。

471
图4.7-1

需要注意的是,PopClip需要你的授权才能弹出快捷状态栏,直接点击Open System Preferences或者打开System Preferences,点击Security & Privacy图标,在PrivacyTab内,点击Accessibility,然后将PopClip加到右边的列表里面并且勾选前面的checkbook。(提示:你可能需要先点击右下角的黄色锁,输入密码后才能编辑右边的列表。)

4.8 增强资源管理器 — XtraFinder

MacBook自带的资源管理器(Finder)已经可以满足一般的需要,但是当你有大量文件维护操作后,你就需要一个更强大的Finder。XtraFinder完全集成到Finder里面,你根本感觉不出它是一个第三方的应用程序,同时还提供很多增强特性,比如:

  • 像浏览器那样的标签页(Tab)
  • 支持双操作面板(Panel)
  • 增强的全局快捷键,例如新建文件(New File)等
  • 多彩的侧边栏图标
  • 快速在当前文件夹打开终端
  • 快速在当前文件夹新建文件

481
图4.8-1

4.9 随心所欲的全键盘控制 – Shortcat

在系统设置里面,我介绍了全键盘控制模式,但是此模式只能做简单的按钮控制,无法达到随心所欲的控制。下面介绍一款比较geek的软件,Shortcat帮助你完全使用键盘来控制系统,供有键盘强迫症的同学使用。

491
图4.9-1

4.10 来杯鸡尾酒 — Bartender

如果你看到这里,相信你已经被我推(hu)荐(you)的安装了一排软件,你的系统状态栏已经人满为患,有时候会因为当前激活的应用程序的菜单比较多挡住你要点击的状态栏图标,这个时候你需要一个酒保来帮你调理一下状态栏,Bartender将是我推荐的最后一个日常使用的App,你可以自定义隐藏某些不常用的状态栏图标,特别适合处女座强迫症。

4101
图4.10-1

4.11 快速进入Shell

go2shell是一个对开发者来说非常有用的app, 使用它可以在Finder里快速进入shell环境.

10 Aug 04:30

[转]自旋锁、排队自旋锁、MCS锁、CLH锁

by DLevin
转自:http://coderbee.net/index.php/concurrent/20131115/577

自旋锁(Spin lock)

自旋锁是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

自旋锁适用于锁保护的临界区很小的情况,临界区很小的话,锁占用的时间就很短。

简单的实现

import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {
    private AtomicReference<Thread> owner = new AtomicReference<Thread>();
    
    public void lock() {
        Thread currentThread = Thread.currentThread();// 如果锁未被占用,则设置当前线程为锁的拥有者
        while (owner.compareAndSet(null, currentThread)) { }
    }
    
    public void unlock() {
        Thread currentThread = Thread.currentThread();// 只有锁的拥有者才能释放锁
        owner.compareAndSet(currentThread, null);
    }
}


SimpleSpinLock里有一个owner属性持有锁当前拥有者的线程的引用,如果该引用为null,则表示锁未被占用,不为null则被占用。

这里用AtomicReference是为了使用它的原子性的compareAndSet方法(CAS操作),解决了多线程并发操作导致数据不一致的问题,确保其他线程可以看到锁的真实状态

缺点

  1. CAS操作需要硬件的配合;
  2. 保证各个CPU的缓存(L1、L2、L3、跨CPU Socket、主存)的数据一致性,通讯开销很大,在多处理器系统上更严重;
  3. 没法保证公平性,不保证等待进程/线程按照FIFO顺序获得锁。

Ticket Lock

Ticket Lock 是为了解决上面的公平性问题,类似于现实中银行柜台的排队叫号:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询。

当线程释放锁时,将服务号加1,这样下一个线程看到这个变化,就退出自旋。

简单的实现

import java.util.concurrent.atomic.AtomicInteger;  

public class TicketLock {
    private AtomicInteger serviceNum = new AtomicInteger(); // 服务号
    private AtomicInteger ticketNum = new AtomicInteger(); // 排队号
    public int lock() { // 首先原子性地获得一个排队号
        int myTicketNum = ticketNum.getAndIncrement(); // 只要当前服务号不是自己的就不断轮询
        while (serviceNum.get() != myTicketNum) { }
        return myTicketNum;
    }
    
    public void unlock(int myTicket) { // 只有当前线程拥有者才能释放锁
        int next = myTicket + 1;
        serviceNum.compareAndSet(myTicket, next);
    }
}

缺点

Ticket Lock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

下面介绍的CLH锁和MCS锁都是为了解决这个问题的。

MCS 来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。

CLH的发明人是:Craig,Landin and Hagersten。

MCS锁

MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLock {
    public static class MCSNode {
        volatile MCSNode next;
        volatile boolean isBlock = true; // 默认是在等待锁
    }

    volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
    private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater
            .newUpdater(MCSLock.class, MCSNode.class, "queue");

    public void lock(MCSNode currentThread) {
        MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
        if (predecessor != null) {
            predecessor.next = currentThread;// step 2

            while (currentThread.isBlock) {// step 3
            }
        }
    }

    public void unlock(MCSNode currentThread) {
        if (currentThread.isBlock) {// 锁拥有者进行释放锁才有意义
            return;
        }

        if (currentThread.next == null) {// 检查是否有人排在自己后面
            if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                // compareAndSet返回true表示确实没有人排在自己后面
                return;
            } else {
                // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                while (currentThread.next == null) { // step 5
                }
            }
        }

        currentThread.next.isBlock = false;
        currentThread.next = null;// for GC
    }
}

CLH锁

CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLock {
    public static class CLHNode {
        private boolean isLocked = true; // 默认是在等待锁
    }

    @SuppressWarnings("unused" )
    private volatile CLHNode tail ;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater
                  . newUpdater(CLHLock.class, CLHNode .class , "tail" );

    public void lock(CLHNode currentThread) {
        CLHNode preNode = UPDATER.getAndSet( this, currentThread);
        if(preNode != null) {//已有线程占用了锁,进入自旋
            while(preNode.isLocked ) {
            }
        }
    }

    public void unlock(CLHNode currentThread) {
        // 如果队列里只有当前线程,则释放对当前线程的引用(for GC)。
        if (!UPDATER .compareAndSet(this, currentThread, null)) {
            // 还有后续线程
            currentThread. isLocked = false ;// 改变状态,让后续线程结束自旋
        }
    }
}


CLH锁 与 MCS锁 的比较

下图是CLH锁和MCS锁队列图示:
CLH-MCS-SpinLock

差异:

  1. 从代码实现来看,CLH比MCS要简单得多。
  2. 从自旋的条件来看,CLH是在本地变量上自旋,MCS是自旋在其他对象的属性。
  3. 从链表队列来看,CLH的队列是隐式的,CLHNode并不实际持有下一个节点;MCS的队列是物理存在的。
  4. CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。

注意:这里实现的锁都是独占的,且不能重入的。



DLevin 2015-08-07 00:18 发表评论