为了保证制作简历的安全性和流畅性,建议您使用Chrome浏览器进行访问
# 先马后看
别犹豫了,马就完事了。在这里每个人都是分享者,你可以分享技能/干货/安装包/电影/图书等等宇宙内的所有资源。
···
1627人正在讨论
#
灵魂流浪者
哈尔滨理工大学·2022届

2020年,地产还值得应届生加入吗?

先上结论,2020届应届生进入地产行业是一个好的选择,但不是最好的选择。 一、是一个好的选择 1、行业越难过,校招生越吃香 地产行业的【黄金十年】与【白银十年】的提法大家应该一点也不陌生,经过18,19年的横盘以及今年疫情的巨大冲击,地产行业前途未卜,显然高速增长的时代已经一去不返。地产行业凉凉,是不是应届生就不能加入呢? 其实不是的,反而是【行业越难过,校招生越吃香】。 行情不好,才需要新鲜血液。以往黄金时代的时候,房价飙升,傻子都能卖得出房子,房子造成什么样都有人买,那么稳定,优秀的“人才”就不是企业发展的重要因素,但是行业低迷,消费者需求多样化,一方面要优化产品,提升物业质量,符合消费升级大趋势,另一方面要做好营销,了解新的消费者在哪里,在想什么?——招募优质的校招生人才为企业注入新的发展动力就尤为重要。现在许多企业校招的营销管培生,都要211或者浙商大,广财这样的强双非了。旭辉,新希望地产的营销管培底薪可以开到一万,中南置地9000,阳光城8000。 兼并留下的人才缺口需要校招人才补充。地产行业不好过,房企之间的竞争就更加激烈,强者恒强,19年千亿房企就有30多家,而很多房企一年到头也买不了十几亿,越来越多的头部房企以收购兼并的方式拿地,资本运作,资产重组,比如世茂对福晟的所谓【强强联手】,拿了你的地,你的项目,不要你的人,那么人才的空缺就从校招管培生中培养,这些来自985,211的名校生有如一张白纸,学习能力强,潜力大,又有95后的年轻化思维方式,在系统的培养下,很容易在短时间内成为独当一面的人才,而且这些校招培养的人才对于企业来说稳定,忠诚,重视程度很高。 地产行业不好过,所以寻求多元化发展路径——商业,长租公寓,办公平台,甚至(龙湖的)智慧系统,(碧桂园的)机器人,都需要更多元,更优质,更专业的应届生人才的加入。 所以综上,校招生在大趋势上必然是拥有越来越多的机会,越来越大的重视程度,作为校招生加入地产行业,是一个好的选择。 2、各行各业都不好过,地产行业是高性价比选择 如果是地产行业凉凉,那么其他行业呢?其实也未必是好的选择。 我们被国家的高速发展惯坏了,以为没有结构化跃迁机会的行业就不是好的行业。可是回首过去十年,除了互联网,金融,地产,有多少行业是朝阳行业呢?而展望未来十年,可以预见的是旧的人口红利已经渐显疲态,而新的技术比如AI,区块链还没有落地带来结构性增长的可能。 以前的大家追捧的投行,券商,产品经理,运营策划,或许都过着“辛苦谋生”的职业生涯,今天996,明天ICU。 地产行业对于应届生至少是高薪行业,且是高性价比行业 地产行业的薪酬一直和互联网,金融齐平,算是应届生高薪的聚集地。2020届房企校招管培生的薪酬中位数可以达到18w,最高的合景泰富,蓝光,阳光城的PRO可以超过30万,头部房企龙湖,金地等都实施了Special offer的薪酬策略,即分等级定薪,最高可达22万,抢夺优秀人才。 而相比互联网和金融,地产行业还有两点高性价比 第一个是岗位多,门槛低,投行,券商,咨询,VC,PE,那些所谓的一毕业年薪百万的岗位,挤破了头也很难进;而互联网大厂也是层层考核,但是地产相对来说,公司多,岗位多,选择多 第二个是全国各地都有岗位,与互联网基本分布在“北上广深杭”五个城市相比,房企的岗位就友善的多,只要有地的地方就有大企业的城市公司,碧桂园,恒大这种走三四线开发路线的尤是如此。进互联网做三到五年,没有根本性职业突破还是买不起房子,一线城市的房价不是开玩笑的。到时候三十岁进退维谷,留,留不下,回,又没有相对应的岗位。 但是如果你作为地产行业管培生去了长沙,重庆,南昌这种二线城市发展,房价一万多一平,第一年年薪20万,三到五年升经理,三五十万一年,加上理财,外快,很容易凭借自己的力量买房买车,站稳脚跟。 二、不是最好的选择 1、动荡,裁员, 明天和意外不知道谁先来 行业好过的时候,岗位多,70分的候选人加上名校or名企备件或许很容易就拿到了一个100分的岗位offer,但是现在行业不好过了,100分的岗位就只要100分的人。所以70分的人就纷纷被优化。 我们最喜欢说的一句话是:行业在洗牌期,能去大公司就去大公司。但是大公司如何呢?碧桂园,恒大,华夏幸福,近日来,无一不大量裁员,融创这样的TOP4房企竟然也有解约20届应届生的行为。确实行业动荡,应届生如果加入要做好一起度过寒冬的准备。 2、996?地产行业是007 地产行业本来就是资金密集型行业,较少的人员产出较大的项目收益。加班,熬夜,赶项目是家常便饭,而现在行业不景气,一个人当三个人用的情况各家企业都一样。要加入地产行业,就要做好007的准备。 三、我的建议 1、大环境如此,调整好心态。不要用以往五年的经验去看未来五年的机会,修炼个人能力,经济情况不好的时候就做100分的人才,不被大环境左右。 2、就业求稳,国企应当被排上重要的选择权重。保利,中海,招商蛇口,金茂,建发……这些优质是国企应该是大家的首选,无论如何,国企有其政治任务和社会责任,是很难做得出大批量裁员的动作的。 3、多方向探索,为自己留后路。趁着低年级多行业实习,互联网,地产,快消,也多了解选调生和教师的政策待遇,多渠道了解就业信息。
分享
评论
先马后看
七格隆咚锵
广东白云学院·2022届

为什么有那么多应届生想进银行?银行到底有哪些吸引人的地方?

“在银行上班不一定非要学金融、会计之类商科专业,大多数专业都行啊。” 难怪毕业之后这么多人都选择进银行工作! 但是进银行工作有什么好处?银行为什么能成为应届毕业生工作热门之选? 在回答这个问题之前,我先给大家盘点一下进银行内部主要岗位: 银行内主要岗位有四种: 柜员:柜员是银行的最前台一线人员,是直接接触客户群体的基层人员,也就是大家去银行网点看到的,属操作型岗位,负责存取款、汇兑等操作。这个岗位看似很光鲜,实则是最低端、最没价值的岗位,但先做柜员后转客户经理是很正常的渠道。 客户经理:早期也叫信贷员,主要负责向客户营销各种业务,包括拉存款、贷款、维护银企关系、准备信贷流程的各环节材料,是银行主要业务的一线员工,也是最有发展前景、压力最大的岗位。 中台业务支持岗位:主要有风险管理(负责贷款项目审查)、财务管理(银行很大一部分数据核算包括收入、利润、存款、各种口径统计职责都在财务部)、运营(管理现金、凭证等)、资金交易。 后台保障岗位:和其他单位类似。 回到主要问题,近年来各大商业银行虽然进入了红利周期的最后阶段,利润增长越来越难,那为什么依旧有这么多人挤着进银行? 究其原因,不外乎以下几点: 1、银行招聘门槛低 银行招聘每年集中在8月底到12月,银行招聘条件较为统一。 ①招聘对象:2019银行秋招主要是面向19届毕业生进行,但是像光大银行、平安银行、邮储银行、建设银行、农发行、人民银行往届生也是可以报考的。所以2020银行秋招主要是面向2020届毕业生进行的,部分银行2019届毕业生也是可以报考的。 ②专业要求:2019银行秋招主要招收但不限于经济、金融、会计、管理、法学、理学、工学、计算机、文学、数学、物理、工程等专业,值得注意的是计算机类专业的招聘人数较往年有所增长,这也会是今后招聘的一个趋势。 ③英语要求:2019银行秋招虽然大多数银行对英语四六级做了要求。但是像人民银行、农发行、工商银行客服经理岗、中国银行柜员岗位、建设银行放宽英语等级要求。中原银行、光大银行、浦发银行、广发银行对于英语没有明确要求。 2、招聘人数多 银行校园招聘机会很多,报考时各行都可以报名,秋季招聘是考银行最好的时机,但万一错过了,也还有银行春季补招。但相对于春招而言,还是建议大家参加秋季招聘,秋季招聘银行放出的岗位多,招聘人数多,有机会还是应该去尝试的。相对应届毕业生来讲,银行校园招聘没有工作经验的要求,成功就业的机会也大很多。 以2019银行秋招为例:共有22家银行进行了大规模的全国招聘,招聘人数大约为8w+,其中五大行就招聘了近6万人,工商银行仍旧是招聘人数最多的银行,共计招聘17000+人,对于正在择业的你来说,机会多多哟! 银行提供的工作岗位众多,考生可以根据自己的能力和志向来做不同的选择。一般银行招聘岗位有信息技术岗位、综合营销类岗位、运营支持类岗位。 3、考试公平透明 银行招聘考生数量大、地域分布广,对透明性和公正性的要求尤为突出。目前各大银行的招聘笔试一般采取全国范围内计算机统一考试的方式,这种方式不仅高效、稳定,而且在很大程度上确保了银行招聘的公平公正,更有利于优秀的考生从中脱颖而出。在面试环节,各大银行也逐渐寻求与社会专业人才测评机构强强联合,不断提升自身选拔人才的科学性和有效性。可以说,银行业招考正在向着更专业、更公平、更高效的方向发展。 4、薪资待遇好 银行工作福利待遇好、发展前景好,银行工作人员除了基本的工资之外,还有季度奖、年终奖、交通补助、餐费补助、节假日补助、甚至有六险二金,简直逆天呀!据相关统计数据表明,银行业的薪资是地区平均薪资的1.5倍。 银行也是非常注重人才的培养的,擅长挖掘有潜力的新人,不仅能让员工学到很多知识,如果你有能力,也会很容易从支行升职到总行。有很多人也称在银行工作,有着“养老”的意思,每个人所想要的生活是不一样的,你如果喜欢积极上进,银行会给你提供很多机会。如果你稳定低压悠闲的生活,银行也是一个不二之选。 就目前的情况来看,就业依然是很大的问题,而且很多小伙伴在就业时会面临着很多问题,例如离家远、不稳定、工资低、待遇差等等。所以说对于应届毕业生来讲,各大银行校园招聘,无疑为大家提供了很多的就业机会,小伙伴们可要好好抓住这个不错的工作机会哦。 如果你想问我,你是否适合进入银行工作,那么我建议大家在做决定之前要想清楚这么几个问题: 1)你是否能接受从基层做起,并且做好有可能在基层待一辈子的心理准备。 2)你是否能接受一份体面,有社会地位,能解决温饱,但是基本不可能挣大钱,发家致富的工作。 3)你是否有足够的耐心慢慢等待你的前辈退休,然后你才有机会晋升。 4)你是否能够接受所有国企都存在的一些“情况”。 如果这些你都觉得没问题,那进银行是个不错的选择,银行确实是一个适合稳定舒适型而非成就创业型人才待的地方。
分享
评论
先马后看
不俗
上海海事大学·2022届

投行工资究竟有多高?

说起金融人挤破头都想进的投行,有人羡慕,有人难受,羡慕的原因就不多说了,光鲜亮丽Money多。但另一方面,投行的工作压力大相信你也有所耳闻,新闻屡爆出高盛为实习生“减负”的新闻,说 1 天只工作17个小时就够了的消息都能让不少投行朋友喜大普奔。 01 【什么是投行?】 投资银行,又简称为投行,是很多金融人心目中的圣地。一般来说,投资银行是为公司、政府或者是富人提供建议、筹集资金的机构。举个例子来说: “有一个投行菜鸟问,什么是投行?”前辈拿了一些烂水果问他:“你打算怎么把这些水果卖出去?” 菜鸟想了半天说:“我按照市场价打折处理掉。” 这位前辈摇头,拿起一把水果刀,把烂水果去皮切块,弄个漂亮的水果盘:“这样,按照几十倍的价格卖掉”。 在国内人们习惯把投行分成三块:外资投行,本土投行,中金。 其中,外资投行又分为两种。大号的华尔街投行,也就是文中要开八的顶级投行,被俗称为bulge bracket,包括大摩,高盛,摩根大通,美银美林,德意志银行,瑞银,瑞信,花旗投行部,汇丰投行部等等;小号的投行们统称叫做boutique,就不一一列举了。 对于很多人来说,进入投行,就意味着:高起点、高薪水、高社会地位。华尔街投行拥有高端的人才和关系网络,他们给出的pay基本都是global pay。第一年的非后台部门的entry-level的薪水基本在60W。 02 【顶级投行的门槛有多高?】 具体可参见爆火国产剧《欢乐颂》里的安迪。毕业于哥伦比亚商学院,精准干练的言谈举止,对数字极为敏感,有超强的记忆力,性格高冷。 正如安迪一样,能进入投行的基本都是智商情商双碾压的天才,堪称为开挂的顶配~ 从硬件条件看, 他们一般是: 名校毕业,良好的背景,美国名校参见哈佛、耶鲁、普林斯顿、斯坦福、麻省理工等五所;国内名校基本只看清北复交,最好是理工本科+金融硕士; 通过 CPA,CFA,ACCA 或各种司法考试; 拥有丰富的投行实习经历,律师事务所经验,会计师事务所经历等; 好到爆的英语能力,托福110以上。 再看看软性标配: 天生的领导力,看着就像一个成功人士; 即使面对大人物,也可以自信得体; 善于从新的角度/多角度分析问题; 出色的销售能力、沟通能力和应变能力; 背景很深,可能是政府部门高层或上市公司高管的亲属; 善于给未来做计划和分析风险。 但是,即使具备了以上这些条件,可能你还是进不了投行哦。一般来说,由于投行很多时候是要去见高级客户的,所以他们也会看长相,主要看气质,关键在颜值。这个也很重要。 此外,投行更喜欢多才多艺的应聘者,因为那意味着“令自己有趣",只有这样的人才能承受住投行巨大的工作压力…… 那么,进投行到底有多难?来看看以下的数据资料: 每年暑假,摩根士丹利暑期项目提供的分析师和助理岗位都会收到大约90000份申请,但录取率不到2%;高盛的录取率同样低的可怜,录取率2.06%。相比之下,哈佛5.9%的录取率就算很高了。看来,要想获得华尔街的实习门票,比上哈佛还要难啊。 所以,非智商情商双碾压的人,一般是进不了投行大门的,哪怕只是做一个小小的实习生。、 看过电影《当幸福来敲门》和《华尔街之狼》的都知道,投行的工作节奏有多快,工作压力有多大。但是,挤破头要进入投行的年轻人仍然不在少数。以高盛为例,90%收到高盛offer的人都会选择接受。 此外,在2015年《财富》100家最适宜工作的公司排行榜上,高盛名列第45位。这是自1998年该榜单设立以来,年年上榜的”13家“常青树”公司之一。有很多员工也认为,高盛是最适宜工作的公司之一。 03 【投行的职业阶梯是怎样的?】 基本上所有的投资银行都有固定的职业阶梯,只要没有被开除,那你的职业道路就应该是这样的: Analyst → Associate → Vice President(VP) → Senior Vice President (SVP) / Director→ Managing Director (MD) Analyst:投行中的Analyst一般都是为各大院校应届生准备的一个2年的Program。表现顶尖的人才有机会在两年后继续留在公司,更有可能再过一年就被升职成为Associate。 Associate:按照之前所说的投行Ladder,Associate是比Analyst高一级的职位。这个职位要么是从Analyst晋升而来,要么是公司从各名校招入的MBA学生。一般来说,你需要在Associate这个位置上工作3年左右才能升职成为VP。 Associate的首要任务之一就是检查Analyst的工作,当然,所谓“检查”很可能就像下面的这个对话一样。除此之外,Associate要负责Presentation中文字部分的编写,并且负责大多数建模工作Work,并且主要负责与客户的沟通。 04 【投行工资究竟有多高?】 大家都知道,投资银行是个高薪资高回报的行业。在美国人均收入不到4万美元的时候,高盛人年均薪酬就超过了70万美元。高盛、摩根斯坦利等投资银行每年往往净收入的大约一半都用在了员工薪酬上。 即便在国内,进入投行,也意味着起薪就能达到几十万。这对于打工者来说,收入简直在金字塔的顶端,难怪大家都对金融行业趋之若鹜。 《亲密敌人》中曾经对投行的高工资有一个略微夸张的表述:"我们这种人就不值得同情,工作三四年,年薪几百万,住的都是五星酒店,坐的永远是商务舱,我们就不应该睡觉!" 实际上是什么样的呢?请看下图: 看完这张图,你可能瞬间就明白了,为什么金融危机的时候,美国人民那么憎恨银行家了。 但是,金融人热爱投行并不只是因为久负盛名的天价薪酬。 除了高工资外,他们最注重的是能够有机会与一群超级精英共事,而扁平、共识驱动的合作文化更是锦上添花。正如高盛的CEO劳埃德•布兰克费恩说:“你不一定是最聪明的人,但我们这里一定是由聪明的人、有趣的人和对世界好奇的人所组成的最强组合。” 没有人会认为在这里工作是轻松的。但高盛人表示,积极的一面让这一切都变得值得。因为在投行工作的经历让他们变成更优秀的自己。 很多投行男也在自己的努力之下,慢慢变成高富帅,一年买车两年买房三年迎娶白富美,从而走上人生的巅峰呢~ 就像前两年,世界小姐张梓琳的老公聂磊就是一名典型投行男,人大国际经济贸易专业硕士,任职中信证券债务资本市场部SVP(高级副总裁),曾参与长江电力等上市公司的债券承销工作。 李念的老公林和平,是软银赛富投资顾问有限公司合伙人,哈佛大学法学博士,公司管理着22亿美元的资金。 连比尔-克林顿的独生女儿切尔西-克林顿,嫁给的也是投资银行家。她的老公马克-梅兹文斯基(Marc Mezvinsky)曾就职于高盛集团宏观和自营交易部门,两人可谓青梅竹马。 据说,最近三届美国总统的两个出阁女儿也都嫁给了投行家,因为世界各国很多政要的子女们自己也想进投行,但是没有实力,只好曲线救国了~ 05 【投行人的压力到底有多大?】 让我们来看看高盛集团总部四层的交易大厅。你们是不是密恐症都要犯了。 在很多人的脑海里,投行员工可谓是社会精英,收入丰厚、衣着光鲜、出入各种高大上。但他们熬夜加班也是家常便饭,心理压力极大,经常都有过劳死和自杀的人。 因为投行挑选的都是最优秀的人,新人进入一家公司就必须要面临"up or out"的残酷局面。像游泳一样,游得出来还行,游不出来就淹死了。 投行的员工每天必须大量浏览《华尔街日报》、《财富》、《商业周刊》等大量专业财经报刊,关注来自各大网站的最新金融报道,对这些信息进行仔细研读、消化和吸收,并将自己对信息的领悟与见解传递给客户。 此外,投行的等级制度特别森严。一般进入投行的职位都是从分析师(analyst)开始,基本上得熬三年左右,才能做到助理的职位,然后要再熬大约三年半的时间,才可能升至副总裁。 分析师的工作最辛苦,几乎全年都要无休止加班,所有脏活累活都要做。做调研、准备材料,改PPT,写Memo,打文件,做pitchbook,给老板深夜送招股书等等。黑莓不离身,一天七天几乎都在工作,熬夜通宵是常事。平均每周需要工作100个小时,夸张的时候120小时也要扛下来。 关于投行圈的“早睡早起”,金融圈里有一个流传已久的噩梦“the magic roundabout(神奇旋转木马)”:早上7点钟,的士司机会在银行楼下等你,带你回家,等你上楼洗个澡,再把你带回公司。这就是投资银行兼并重组部门的生活写照。 除了日夜颠倒的工作,投行狗们还经常要各种出差。但是如果你以为他们的出差是度假,那就大错特错了。一般来讲,他们的日程包括:提着大量文件参加各种会议、研讨会;在餐桌上与客户博弈;回到酒店撰写报告;赶到机场奔向下一个出差地点…… 分析师升到助理后会好一点,工作时间80小时左右,升到副总裁后生活节奏可以接近常人,如果能升任合伙人,就立马变身超级钻石王老五了。例如陈好老公刘海峰就是银行家,毕业于美国哥伦比亚大学,曾任摩根士丹利亚洲投资部联席主管,现任科尔伯格·克拉维斯集团全球合伙人兼大中华区总裁。所以陈好婚后就立马淡出娱乐圈,享受生活去了。 也许有人会说,升到助理后,然后再到VP,再到合伙人就好了啊。 但是很多人可能根本就熬不到那个时候就离开了公司,甚至离开了这个世界。如果说人与人的关系有7年之痒,而投资银行则相当于平均6.25年来个彻底大换血,那投行的雇员与他们雇主之间的关系只有6.25年,比男女关系还要不稳定。 投行最常见的裁员方式是这样的:你早上起来,正兴致勃勃要去上班,结果接到一个通知,哥们儿你被裁了,你的门禁卡我们已经作废了,公司配给你的电话我们也停机了。我们会让你的助理把你的私人物品装箱寄回给你。你就不用来了,拜拜! 在过人的工作压力之下,投资银行业的人员流动性也很高,据统计平均每年投资银行企业的人员流失率是16%。 华尔街光鲜的另一面:“等我死了再睡。”很多鲜活的例子已经验证了。 她,28岁投行女,被很多朋友亲切的称为:丁会计,生前很优秀和热爱生活。却在离28岁生日还有一周的时候去世,原毕马威KPMG女员工。之前,她曾因身体原因从KPMG跳至CICC中金,后却确诊胃癌。病后,她说想起PWC25岁病逝员工潘洁。最后一条微博发于2014年4月23日。 此外还有更多例子,2014年2月,摩根大通一名雇员从香港中环遮打大厦30层平台跳下,结束了他的人生道路。香港媒体报道称,死者在不久前以数百万港元置业,外界怀疑其由于工作压力大而选择结束生命。 去年4月,高盛驻旧金山科技、传媒、通讯业分析师SarvshreshthGupta猝然逝去,年仅22岁。据Gupta父亲说,他每天加班工作到凌晨5点已经成为一种常态,长时间的工作及较少的睡眠时间,令金融圈成为抑郁症的高发群体。 因为高压力,他们的排压方式与他们光鲜的职业并不符,也属于常在河边走的“高危人群”。有很多人靠莫达非尼片等药物来提振自己、有的酗酒、还有的就是性和赌博。这也是香港兰桂坊90%的消费者来自金融圈的原因。 另外,可能就是因为种种压力,投行人士无处排解压力,所以投行充斥着各种乱七八糟的奇葩事,最常见的就是各种“ 桃色八卦”,甚至有投行女曾经将自己比作“小姐”,说出了以下名言: “ 千万不要看轻了小姐,因为我们也跟小姐差不多,要想赚大钱,就不要端着,尤其要学会理解男人爱嫖娼爱乱搞这件事,这样才能无往而不利,做事才能顺风顺水。” 最后回归选择投行工作这个话题,很多时候工作就像找对象,适合自己的才是最好的。但如果你非常想进投行的话,那就去提高自己的各项能力吧!
分享
评论
先马后看
老丛
河北工业大学·2022届

那么多人想去金融行业??你们真的了解它嘛...

因为有在金融行业多个大型企业的实习与工作经历,因此对金融行业各层次的就业情况有大概的了解,总体而言,金融行业的岗位和平台的待遇差距很大,即使同是应届生,如果在同一个公司的不同岗位,那待遇薪酬也会差数倍,可能前5%的薪酬高于后95%的薪酬。 一、本科生 毕业去向: (1) 主流去向包括银行、证券公司、P2P性质企业、保险公司、期货公司、担保公司等,主要岗位包括柜员、客户经理、理财经理、业务员等; (2) 水平高些的本科生,可以去银行的管培、四大会计事务所、华为的财经岗位、金融国企; (3) 即使是毕业于清北复交人,能本科毕业直接去证券公司、基金公司、银行、资产管理公司的投研岗位的也极少。 能力要求: 本科生找工作时,大部分企业对本科生工作能力的要求,除了靠谱踏实以外,更看重其销售、沟通交流、找资源的能力。 二、研究生 毕业去向: (1) 除了可能依旧去上面这些本科生的岗位以外,还可能去银行/券商/基金公司的的投研、管理岗位,具体情况因研究生毕业学校以及个人能力而异;(2) 去券商、基金、投行的投研岗位的人数总体而言还是偏少的,这些岗位对院校背景要求也较高。 (3) 还有部分财经硕士会去考公务员、实体企业、外资行,但这部分总体还是占比很小。 能力要求: 靠谱踏实的性格、良好的沟通交流能力肯定还是要的,除此之外,对研究分析能力、写报告的能力也会有要求。比如券商的研究报告,大部分都是给刚毕业入职两年内的员工写的。 总体而言,金融行业,首先,家庭资源、名校背景、研究生学历这个肯定很关键;其次,良好的实习经历和高含金量的证书(个人觉得是CPA)对职业生涯会有很大帮助,实习和考证学到的东西,会比课堂上学到的,更贴近实践操作很多。 三、具体就业去向(硕士) 下面我就根据自己的实习工作经历及同学的交流,谈谈目前经济金融硕士的就业情况。 经济金融硕士的就业非常多元化,既有去主流的银行、券商、基金、信托等金融机构,也有去银保监会、证监会、国开行、公务员等体制内的岗位,还有部分去华为、苏宁、大疆、腾讯、网易等实体企业。 (1)金融类监管机构,主要是中央(人民)银行、银保监会、证监会、政策性银行 总部录取的难度较大,省市分部会好进不少,具体待遇因城市而异。比如在国开行深圳分行第一年也有20+W(含年终),但在一些二线城市,则是拿着公务员的工资,干市场化机构的活。工作强度总体中等,可以积攒体制内资源人脉。 (2)商业银行,包括四大行和股份制商行、城市商业银行 银行的岗位种类较多,研究生的就业主要集中于管培生、资管、投行、金融市场部等部门。大部分管培生都需要去轮岗2年左右,但以后就是银行的骨干精英;资管部门,负责发理财产品配置资产;银行的投行部门更侧重于债券承销;金融市场部门,名利双收的三高岗位(高收入、高起点、高地位)。总行相比分行的进入难度会更大,当然平台和待遇会好很多,例如四大行的总行是可以解决北京户口的!! (3)证券公司、基金管理公司、信托投资公司、金融控股集团等金融公司 (1)券商:热门岗位主要是行业研究、投行部门。投行和行研的工作强度都很大,投行偏向体力活,经常出差熬夜赶材料;行研是脑力体力兼具,不仅要绞尽脑汁写出所在行业的深度研究报告,作为卖方也需要较强的勾兑能力。前十券商的base一般是月薪1.5左右,主要看年终奖,碰到一波牛市,三五年一线城市买房不是梦!去年股市行情太差,券商研究员招的极其少,今年名额更多不少。 (2)基金:热门岗位主要分研究员和交易、风控等部门。基本工资比券商高,作为买方研究员,进入的难度会更高,大部分本科要求清北复交,当了基金经理的话,碰到一波牛市,也是靠海别墅的节奏。(3)信托公司:总体待遇和福利都很可观,竞争也是挤得头破血流。(4)金融控股集团:有民企巨头成立的,如苏宁、万达等,也有国资背景的如越秀金控、杭州金控。 (4)四大资产管理公司 根据我周边小伙伴的反馈情况,我的感觉是五个字:闷声发大财!!工作强度总体不小,但如果跟到好的团队,做出业务,年终奖会很高。举个栗子,我认识的小伙伴在四大资管之一,目前月薪税前18k左右,第二年年终发了15w左右,当然工作强度不比投行低。 (5)VC、PE、FA、实体企业的战略投资部门 工作内容主要是做行业分析,搜寻优质创业公司,协助领导做投资决策。是不是感觉很高大上!但刚毕业就做VC、PE,学习曲线总体较慢,并且奖金主要来自投资项目的退出。适合有工作经验或者产业背景的人,个人建议应届毕业生慎重选择VC/PE。 (6)读博深造 读博的特点是“回报周期长、回报率高”,举个栗子,很多省市的人才引进,给清北博士的待遇是副处级。认识一个北大金融博士,毕业直接去某二线城市当金融集团副总,不到30岁。当然普通博士的待遇也都是20+万起步! (7)外资行 包括摩根、高盛、德银等外资,待遇极好,也非常难进,本科基本要求清北复交,第一年50-100w之间,一线城市买房压力不大。 (8)其他就业 主要包括公务员、选调生、省市的人才引进、华为财经人员、网易、腾讯、苏宁、大疆管培生等。 四、薪酬待遇 至于薪酬待遇方面,贫富差距很大,可能前5%的人拿得工资比后95%的都多(所以我比较建议,如果本科毕业时没找到好的岗位,去读个两年的专业硕士)。举个例子吧,同在中信证券工作,如果是做投行的话,今年提了工资已经涨到2万元一个月,年终另算,当然工作强度非常大,周末也在加班。但如果你在中信的营业部做客户经理,我估计一个月也就5000-6000吧。 这是今年披露的行业薪酬,当然,关于平均薪酬嘛你懂的,你和马云家产平均下,也有百亿美元,所以我觉得平均薪酬再除以2,可能比较接近中位数。
分享
评论
先马后看
苏三公子
华南理工大学·2022届

Java 知识点整理:集合、JVM、并发

P18:List List 是一种线性列表结构,元素是有序、可重复的。 *ArrayList * 底层由数组实现,随机访问效率高,读快写慢,由于写操作涉及元素的移动,因此写操作效率低。 ArrayList 实现了 RandomAcess 标记接口,如果一个类实现了该接口,那么表示这个类使用索引遍历比迭代器更快。 三个重要的成员变量: 复制代码1transient Object[] elementData; elementData 是 ArrayList 的数据域,transient 表示它不会被序列化,不使用 elementData 直接序列化是因为这是一个缓存数组,出于性能考虑通常会预留一些容量,当容量不足时会扩充容量,因此可能会有大量空间没有存储元素,采用这样的方式可以保证只序列化实际有值的那些元素而不需要序列化整个数组。 复制代码1private int size; size 表示当前 List 的实际大小,elementData 的大小是大于等于 size 的。 复制代码1protected transient int modCount = 0; 该成员变量继承自 AbstractList,记录了ArrayList 结构性变化的次数。所有涉及结构变化的方法都会增加该值,包括add()、remove()、addAll()、removeRange() 及clear() 等。 在使用迭代器遍历 ArrayList 时不能修改元素,modCount 统计 ArrayList 修改次数,expectedModCount 则是在迭代器初始化时记录的modCount 值,每次访问新元素时都会检查 modCount 和 expectedModCount是否相等,如果不相等就会抛出异常。 LinkedList 底层由链表实现,与 ArrayList 相反,需要顺序访问元素,即使有索引也需要从头遍历,因此写快读慢。 LinkedList 实现了 Deque 接口,具有队列的属性,可在尾部增加元素,在头部获取元素,也能操作头尾之间任意元素。 所有成员变量都被 transient 修饰,序列化原理和ArrayList类似。 Vector 和 Stack Vector 的实现和 ArrayList 基本一致,底层使用的也是数组,它和 ArrayList 的区别主要在于:(1)Vector 的所有公有方法都使用了 synchronized 修饰保证线程安全性。(2)增长策略不同,Vector 多了一个成员变量 capacityIncrement 用于标明扩容的增量。 Stack 是 Vector 的子类,实现和 Vector基本一致,与之相比多提供了一些方法表达栈的含义。 P19:HashSet HashSet 中的元素是无序、不重复的,最多只能有一个 null 值。 HashSet 的底层是通过 HashMap 实现的,HashMap 的 key 值即 HashSet 存储的元素,所有 key 都使用相同的 value ,一个static final 修饰的变量名为 PRESENT 的 Object 类型的对象。 由于 HashSet 的底层是 HashMap 实现的,HashMap 是线程不安全的,因此 HashSet 也是线程不安全的。 去重:对于基本类型的包装类,直接按值进行比较。对于引用数据类型,会先比较 hashCode() 返回值是否相同,如果不同则代表不是同一个对象,如果相同则继续比较equals()方法返回值是否相同,都相同说明是同一个对象。 P20:HashMap JDK 8 之前 底层实现是数组 + 链表,主要成员变量包括:存储数据的 table 数组、键值对数量 size、加载因子 loadFactor。 table 数组用于记录 HashMap 的所有数据,它的每一个下标都对应一条链表,所有哈希冲突的数据都会被存放到同一条链表中,Entry 是链表的节点元素,包含四个成员变量:键 key、值 value、指向下一个节点的指针 next 和 元素的散列值 hash。 在 HashMap 中数据都是以键值对的形式存在的,键对应的 hash 值将会作为其在数组里的下标,如果两个元素 key 的 hash 值一样,就会发送哈希冲突,被放到同一个下标中的链表上,为了使 HashMap 的查询效率尽可能高,应该使键的 hash 值尽可能分散。 HashMap 默认初始化容量为 16,扩容容量必须是 2 的幂次方、最大容量为 1<< 30 、默认加载因子为 0.75。 1.put 方法:添加元素 ① 如果 key 为 null 值,直接存入 table[0]。② 如果 key 不为 null 值,先计算 key 对应的散列值。③ 调用 indexFor 方法根据 key 的散列值和数组的长度计算元素存放的下标 i。④ 遍历 table[i] 对应的链表,如果 key 已经存在,就更新其 value 值然后返回旧的 value 值。⑤ 如果 key 不存在,就将 modCount 的值加 1,使用 addEntry 方法增加一个节点,并返回 null 值。 2.hash 方法:计算元素 key 对应的散列值 ① 处理 String 类型的数据时,直接调用对应方法来获取最终的hash值。② 处理其他类型数据时,提供一个相对于 HashMap 实例唯一不变的随机值 hashSeed 作为计算的初始量。③ 执行异或和无符号右移操作使 hash 值更加离散,减小哈希冲突的概率。 3.indexFor 方法:计算元素下标 直接将 hash 值和数组长度 - 1 进行与操作并返回,保证计算后的结果不会超过 table 数组的长度范围。 4.resize 方法:根据newCapacity 来确定新的扩容阈值 threshold ① 如果当前容量已经达到了最大容量,就将阈值设置为 Integer 的最大值,之后扩容就不会再触发。② 创建一个新的容量为 newCapacity 的 Entry 数组,并调用 transfer 方法将旧数组的元素转移到新数组.③ 将阈值设为(newCapacity 和加载因子 loadFactor 的积)和(最大容量 + 1 )的较小值。 5.transfer:转移旧数组到新数组 ① 遍历旧数组的所有元素,调用 rehash 方法判断是否需要哈希重构,如果需要就重新计算元素 key 的散列值。② 调用 indexFor 方法根据 key 的散列值和数组的长度计算元素存放的下标 i,利用头插法将旧数组的元素转移到新的数组。 6.get 方法:根据 key 获取元素的 value 值 ① 如果 key 为 null 值,调用 getForNullKey 方法,如果 size 为 0 表示链表为空,返回 null 值。如果 size 不为 0,说明存在链表,遍历 table[0] 的链表,如果找到了 key 为 null 的节点则返回其 value 值,否则返回 null 值。② 调用 getEntry 方法,如果 size 为 0 表示链表为空,返回 null 值。如果 size 不为 0,首先计算 key 的散列值,然后遍历该链表的所有节点,如果节点的 key 值和 hash 值都和要查找的元素相同则返回其 Entry 节点。③ 如果找到了对应的 Entry 节点,使用 getValue 方法获取其 value 值并返回,否则返回 null 值。 *JDK 8 开始 * 使用的是数组 + 链表/红黑树的形式,table 数组的元素数据类型换成了 Entry 的静态实现类 Node。 1.put 方法:添加元素 ① 调用 putVal 方法添加元素。② 如果 table 为空或没有元素时就进行扩容,否则计算元素下标位置,如果不存在就新创建一个节点存入。③ 如果首节点和待插入元素的 hash值和 key 值都一样,直接更新 value 值。④ 如果首节点是 TreeNode 类型,调用 putTreeVal 方法增加一个树节点,每一次都比较插入节点和当前节点的大小,待插入节点小就往左子树查找,否则往右子树查找,找到空位后执行两个方法:balanceInsert 方法,把节点插入红黑树并对红黑树进行调整使之平衡。moveRootToFront 方法,由于调整平衡后根节点可能变化,table 里记录的节点不再是根节点,需要重置根节点。⑤ 如果是链表节点,就遍历链表,根据 hash 值和 key 值判断是否重复,决定更新值还是新增节点。如果遍历到了链表末尾,添加链表元素,如果达到了建树阈值,还需要调用 treeifyBin 方法把链表重构为红黑树。⑥ 存放元素后,将 modCount 值加 1,如果节点数 + 1大于扩容阈值,还需要进行扩容。 2.get 方法:根据 key 获取元素的 value 值 ① 调用 getNode 方法获取 Node 节点,如果不是 null 值就返回 Node 节点的 value 值,否则返回 null。② 如果数组不为空,先比较第一个节点和要查找元素的 hash 值和 key 值,如果都相同则直接返回。③ 如果第二个节点是 TreeNode 节点则调用 getTreeNode 方法进行查找,否则遍历链表根据 hash 值和 key 值进行查找,如果没有找到就返回 null。 3.hash 方法:计算元素 key 对应的散列值 Java 8 的计算过程简单了许多,如果 key 非空就将 key 的 hashCode() 返回值的高低16位进行异或操作,这主要是为了让尽可能多的位参与运算,让结果中的 0 和 1 分布得更加均匀,从而降低哈希冲突的概率。 4.resize 方法:扩容数组 重新规划长度和阈值,如果长度发生了变化,部分数据节点也要重新排列。 重新规划长度 ① 如果 size 超出扩容阈值,把 table 容量增加为之前的2倍。② 如果新的 table 容量小于默认的初始化容量16,那么将 table 容量重置为16。③ 如果新的 table 容量大于等于最大容量,那么将阈值设为 Integer 的最大值,并且 return 终止扩容,由于 size 不可能超过该值因此之后不会再发生扩容。 重新排列数据节点 ① 如果节点为 null 值则不进行处理。② 如果节点不为 null 值且没有next节点,那么重新计算其散列值然后存入新的 table 数组中。③ 如果节点为 TreeNode 节点,那么调用 split 方法进行处理,该方法用于对红黑树调整,如果太小会退化回链表。④ 如果节点是链表节点,需要将链表拆分为 hashCode() 返回值超出旧容量的链表和未超出容量的链表。对于hash & oldCap == 0 的部分不需要做处理,反之需要放到新的下标位置上,新下标 = 旧下标 + 旧容量。 线程不安全:Java 7 扩容时 resize 方法调用的 transfer 方法中使用头插法迁移元素,多线程会导致 Entry 链表形成环形数据结构,Entry 节点的 next 永远不为空,引起死循环。Java 8 在 resize 方法中完成扩容,并且改用了尾插法,不会产生死循环的问题,但是在多线程的情况下还是可能会导致数据覆盖的问题,因此依旧线程不安全。 红黑树:红黑树是一种自平衡的二叉查找树。 特性:红黑树的每个节点只能是红色或者黑色、根节点是黑色的、每个叶子节点都是黑色的、如果一个叶子节点是红色的,它的子结点必须是黑色的、从一个节点到该节点的叶子节点的所有路径都包含相同数目的黑色节点。 左旋:对节点进行左旋,相当于把节点的右节点作为其父节点,即将节点变成一个左节点。 右旋:对节点进行右旋,相当于把节点的左节点作为其父节点,即将节点变成一个右节点。 插入:① 被插入的节点是根节点,直接将其涂为黑色。② 被插入节点的父节点是黑色的,不做处理,节点插入后仍是红黑树。③ 被插入节点的父节点是红色的,一定存在非空祖父节点,根据叔叔节点的颜色分类处理。 删除:① 被删除的节点没有子节点,直接将其删除。② 被删除节点只有一个子节点,直接删除该节点,并用其唯一子节点替换其位置。③ 被插入节点有两个子节点,先找出该节点的替换节点,然后把替换节点的数值复制给该节点,删除替换节点。 调整平衡:在插入和删除节点后,通过左旋、右旋或变色使其重新成为红黑树。① 如果当前节点的子节点是一红一黑,直接将该节点设为黑色。② 如果当前节点的子结点都是黑色,且当前节点是根节点,则不做处理。③ 如果当前节点的子节点都是黑色且当前节点不是根节点,根据兄弟节点的颜色分类处理。 JVM 15 P1:运行时数据区 程序计数器 程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。程序计数器是线程私有的,各条线程之间互不影响,独立存储。 如果线程正在执行的是一个 Java 方法,计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是本地(Native)方法,计数器值则应为空(Undefined)。 此内存区域是唯一一个在《 Java 虚拟机规范》中没有规定任何内存溢出情况的区域。 Java 虚拟机栈 Java 虚拟机栈是线程私有的,每当有新的线程创建时就会给它分配一个栈空间,当线程结束后栈空间就被回收,因此栈与线程拥有相同的生命周期。栈主要用来实现方法的调用与执行,每个方法在执行的时候都会创建一个栈帧用来存储这个方法的局部变量、操作栈、动态链接和方法出口等信息。当一个方法被调用时,会压入一个新的栈帧到这个线程的栈中,当方法调用结束后会弹出这个栈帧,回收掉调用这个方法使用的栈空间。 该区域有两类异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError 异常。如果 JVM 栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError 异常(HotSpot 不可以动态扩展,不存在此问题)。 本地方法栈 本地方法栈与虚拟机栈的作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法(字节码)服务,而本地方法栈是为虚拟机栈用到的本地(Native)方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定的本地方法。 《 Java 虚拟机规范》对本地方法栈中方法所用语言、使用方式与数据结构无强制规定,具体的虚拟机可根据需要自由实现,例如 HotSpot 直接将虚拟机栈和本地方法栈合二为一。 与虚拟机栈一样,本地方法栈也会在栈深度异常和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError 异常。 Java 堆 Java 堆是虚拟机所管理的内存中最大的一块。堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,Java 里几乎所有的对象实例都在这里分配内存。 Java 堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。但对于大对象(例如数组),多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。 Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 JVM 都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,虚拟机将抛出 OutOfMemoryError 异常。 方法区 方法区和 Java 堆一样是各个线程共享的内存区域,它用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 JDK 8 之前使用永久代来实现方法区,这种设计导致了 Java 应用容易遇到内存溢出问题,因为永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小。JDK 6 时 HotSpot 的开发团队就准备放弃永久代,改用本地内存来实现方法区,JDK 7 时已经把原本放在永久代的字符串常量池、静态变量等移出,到了 JDK8 时永久代被完全废弃,改用在本地内存中实现的元空间来代替,把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间。 《 Java 虚拟机规范》对方法区的约束很宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小或可扩展外,还可以选择不实现垃圾回收。垃圾回收行为在该区域出现较少,主要回收目标是针对常量池的回收和对类型的卸载,一般来说该区域的回收效果比较难令人满意,尤其是类型的卸载,条件十分苛刻。如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError 异常。 运行时常量池 运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。一般来说,除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译出来的直接引用也存储在运行时常量池中。 运行时常量池相对于 Class 文件常量池的另一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是说并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被利用的较多的是String 类的 intern() 方法。 由于运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。 直接内存 直接内存不是 JVM 运行时数据区的一部分,也不是《 Java 虚拟机规范》中定义的内存区域,但是这部分内存也被频繁使用,而且也可能导致内存溢出异常。 JDK 1.4 中新加入了 NIO 模型,引入了一种基于通道与缓冲区的 IO 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作,这样能在一些场景中显著提高性能,避免了在 Java 堆和 Native堆中来回复制数据。 本机直接内存的分配不会收到 Java 堆大小的限制,但还是会受到本机总内存大小以及处理器寻址空间的限制,一般配置虚拟机参数时会根据实际内存去设置 -Xmx 等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError 异常。 P2:对象创建的过程 当 JVM 遇到一条字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查这个引用代表的类是否已被加载、解析和初始化,如果没有就必须先执行类加载过程。 在类加载检查通过后虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。假设 Java 堆内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点指示器,分配内存就是把该指针向空闲方向挪动一段与对象大小相等的距离,这种方式叫"指针碰撞"。 如果 Java 堆中的内存不是规整的,那么虚拟机就必须维护一个列表记录哪些内存块是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例并更新列表上的记录,这种方式叫做"空闲列表"。 选择哪种分配方式由堆是否规整决定,堆是否规整又由所用垃圾回收器是否带有空间压缩整理能力决定。因此使用 Serial、ParNew 等带压缩整理的收集器时,系统采用指针碰撞;当使用 CMS 这种基于清除算法的垃圾收集器时,理论上只能采用空间列表分配内存。 分配内存的线程安全问题:对象创建在虚拟机中十分频繁,即使修改一个指针所指向的位置在并发情况下也不是线程安全的,可能出现正给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针来分配内存的情况。解决该问题有两个方法:① 虚拟机采用 CAS 加失败重试的方式保证更新操作的原子性。② 把内存分配的动作按照线程划分在不同空间进行,即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲 TLAB,哪个线程要分配内存就在对应的 TLAB 分配,只有 TLAB 用完了分配新缓冲区时才需要同步。 内存分配完成后虚拟机必须将分配到的内存空间(不包括对象头)都初始化为零值,保证对象的实例字段在 Java 代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型对应的零值。之后虚拟机还要对对象进行必要设置,例如对象是哪个类的实例、如何找到类的元数据信息等。 至此从虚拟机的视角来看一个新的对象已经产生了,但从程序的角度来说对象创建才刚开始。此时构造方法,即 Class 文件中的 init 方法还没有执行,所有字段都为默认零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说 new 指令后会接着执行 init 方法,按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全被构造出来。 P3:对象的内存布局 在 HotSpot 虚拟机中,对象在堆内存中的存储布局可分为三个部分。 对象头 对象头包括两类信息,第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID等,这部分数据叫做"Mark Word"。 对象头的另一部分是类型指针,即对象指向它的类型元数据的指针,JVM 通过该指针来确定对象是哪个类的实例。并非所有虚拟机实现都必须在对象数据上保留类型指针,查找对象的元数据不一定要经过对象本身。此外如果对象是一个 Java 数组,在对象头还必须有一块用于记录数组长度的数据。 实例数据 实例数据部分是对象真正存储的有效信息,即程序员在代码里所定义的各种类型的字段内容。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。 对齐填充 这部分不是必然存在的,仅仅起占位符的作用。由于 HotSpot 虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,而对象头已经被设为正好是 8 字节的整数倍,因此如果对象实例数据部分没有对齐,就需要对齐填充来补全。 P4:对象的访问定位 Java 程序会通过栈上的 reference 数据来操作堆上的具体对象,而具体对象访问方式是由虚拟机决定的,主流的访问方式主要有使用句柄和直接指针两种。 使用句柄 如果使用句柄访问,Java 堆中将可能会划分出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。 优点是 reference 中存储的是稳定句柄地址,在对象被移动(处于垃圾收集过程中)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。 直接指针 如果使用直接指针访问的话,Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话就不需要多一次间接访问的开销。 优点就是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用的就是直接指针来进行对象访问。 P5:内存溢出异常 Java 堆溢出 Java 堆用于存储对象实例,我们只要不断创建对象,并且保证GC Roots到对象有可达路径来避免垃圾回收机制清除这些对象,那么随着对象数量的增加,总容量触及最大堆容量的限制后就会产生OOM异常。例如在 while 死循环中一直 new 创建实例。 Java 堆内存的 OOM 是实际应用中最常见的 OOM 情况,常规的处理方法是先通过内存映像分析工具对 Dump 出来的堆转储快照进行分析,确认内存中导致 OOM 的对象是否是必要的,即分清楚到底是出现了内存泄漏还是内存溢出。 如果是内存泄漏,可进一步通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些GC Roots相关联才导致垃圾收集器无法回收它们,一般可以准确定位到对象创建的位置进而找出产生内存泄漏代码的具***置。 如果不是内存泄漏,即内存中的对象确实都是必须存活的那就应当检查 JVM 的堆参数设置,与机器的内存相比是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。 虚拟机栈和本地方法栈溢出 由于HotSpot虚拟机不区分虚拟机和本地方法栈,因此设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常: StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。例如一个递归方法不断调用自己。 该异常有明确错误堆栈可供分析,容易定位到问题所在。 OutOfMemoryError:如果 JVM 栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。HotSpot 虚拟机不支持虚拟机栈的扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OOM异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。 运行时常量池溢出 String类的intern方法是一个本地方法,它的作用是如果字符串常量池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的 String 对象的引用,否则会将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。 在 JDK6及之前常量池都分配在永久代,因此可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代的大小,间接限制常量池的容量。在 while 死循环中不断调用intern方法,之后将导致运行时常量池溢出。 在 JDK7 及之后版本不会导致该问题,因为存放在永久代的字符串常量池已经被移至 Java 堆中。 方法区溢出 方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量的类去填满方法区,就会导致溢出。例如使用 JDK 的反射或 CGLib 直接操作字节码在运行时生成大量的类会导致溢出。当前的很多主流框架如Spring、Hibernate等对类增强是都会使用CGLib这类字节码技术,增强的类越多,就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。 JDK 8 之后永久代完全被废弃,取而代之的是元空间,HotSpot 提供了一些参数作为元空间的防御措施: -XX:MaxMetaspaceSize:设置元空间的最大值,默认 -1,表示不限制即只受限于本地内存大小。 -XX:MetaspaceSize:指定元空间的初始大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量空间就适当降低该值,如果释放了很少的空间就适当提高该值。 -XX:MinMetaspaceFreeRatio:作用是在垃圾收集之后控制最小的元空间剩余容量百分比,可减少因为元空间不足导致的垃圾收集的频率。类似的还有-XX:MinMetaspaceFreeRatio,用于控制最大的元空间剩余容量百分比。 本机直接内存溢出 直接内存的容量大小可通过 -XX:MaxDirectMemorySize 指定,如果不去指定则默认与 Java 堆的最大值一致。 由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见有什么明显的异常情况,如果发现内存溢出后产生的Dump 文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因。 P6:判断对象是否是垃圾 在堆中存放着所有对象实例,垃圾收集器在对堆进行回收前,首先要判断对象是否还存活着。 引用计数算法 在对象中添加一个引用计数器,如果有一个地方引用它计数器就加1,引用失效时计数器就减1,如果计数器为0则该对象就是不再被使用的。该算法原理简单,效率也高,但是在 Java中很少使用,因为它存在对象之间互相循环引用的问题,导致计数器无法清零。 可达性分析算法 当前主流语言的内存管理子系统都是使用可达性分析算法来判断对象是否存活的。这个算法的基本思路就是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots之间没有任何引用链相连,则此对象是不可能再被使用的。 可作为GC Roots的对象: 在虚拟机栈中引用的对象,如线程被调用的方法堆栈中的参数、局部变量等。 在方法区中类静态属性引用的对象,如类的引用类型静态变量。 在方法区中常量引用的对象,如字符串常量池中的引用。 在本地方法栈中 JNI 即 Native 方法引用的对象。 JVM 内部的引用,如基本数据类型对应的 Class 对象,一些常驻异常对象,系统类加载器等。 所有被 synchronized 同步锁持有的对象。 P7:引用类型 无论通过引用计数还是可达性分析判断对象是否存活,都和引用离不开关系。在 JDK1.2 之前引用的定义是:如果 reference 类型数据存储的数值代表另外一块内存的起始地址,那么就称该 reference 数据是代表某块内存、某个对象的引用。在 JDK 1.2之后 Java 对引用的概念进行了扩充,按强度分为四种: 强引用:最传统的引用定义,指代码中普遍存在的引用赋值。任何情况下只要强引用存在,垃圾收集器就永远不会回收被引用的对象。 软引用:描述一些还有用但非必需的对象。只被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围中进行二次回收,如果这次回收还没有足够的内存才会抛出 OOM 异常。 弱引用:描述非必需对象,引用强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。 虚引用:也称幽灵引用或幻影引用,是最弱的引用关系。一个对象是否有虚引用存在,完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象实例。该引用的唯一目的就是为了能在这个对象被垃圾收集器回收时收到一个系统通知。 P8:GC 算法 标记-清除算法 原理:分为标记和清除两个阶段,首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象,或者标记存活的对象并统一回收所有未被标记的对象。标记过程就是判断对象是否属于垃圾的过程。 特点:① 执行效率不稳定,如果堆中包含大量对象且其中大部分是需要被回收的,这时必须进行大量标记和清除,导致效率随对象数量增长而降低。② 内存空间碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时无法找出足够的连续内存而不得不提前触发另一次垃圾收集。 标记-复制算法 原理:将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块的空间用完了,就将还存活着的对象复制到另一块,然后再把已使用过的内存空间一次清理掉。 特点:① 实现简单、运行高效,解决了内存碎片问题。② 代价是将可用内存缩小为原来的一半,浪费了过多空间。 HotSpot 的新生代划分: 把新生代划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。发生垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor 空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,即每次新生代中可用空间为整个新生代的90%。 标记-整理算法 原理:标记-复制算法在对象存活率较高时要进行较多的复制操作,效率将会降低。并且如果不想浪费空间,就需要有额外空间进行分配担保,应对被使用内存中所有对象都100%存活的极端情况,所以老年代一般不使用此算法。老年代使用标记-整理算法,标记过程与标记-清除算法一样,只是后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。 特点:标记-清除与标记-整理的本质差异在于前者是一种非移动式回收算法而后者是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险策略:① 如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且这种移动必须全程暂停用户线程才能进行。② 如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和内存访问器来解决。所以是否移动对象都存在弊端,移动则内存回收时更复杂,不移动则内存分配时更复杂。 P9:垃圾收集器 经典垃圾收集器:指 JDK 11之前的全部可用垃圾收集器。 Serial 最基础、历史最悠久的收集器,该收集器是一个使用复制算法的单线程工作收集器,单线程的意义不仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调它进行垃圾收集时必须暂停其他所有工作线程直到收集结束。 Serial 是虚拟机运行在客户端模式下的默认新生代收集器,优点是简单高效,对于内存受限的环境它是所有收集器中最小的;对于单核处理器或处理器核心较少的环境来说,Serial 收集器由于没有线程交互开销,因此可获得最高的单线程收集效率。 ParNew 实质上是 Serial 的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。 ParNew 是虚拟机运行在服务端模式下的默认新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始,ParNew 加 CMS 收集器的组合就不再是官方推荐的服务端模式下的收集器解决方案了,官方希望他能被 G1 完全取代。 Parallel Scavenge 新生代收集器,基于标记-复制算法,是可以并行的多线程收集器,与 ParNew 类似。 特点是它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能缩短收集时用户线程的停顿时间,而 Parallel Scavenge 的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。自适应调节策略也是它区别于 ParNew 的一个重要特性。 Serial Old Serial 的老年代版本,同样是一个单线程收集器,使用标记-整理算法。 Serial Old 是虚拟机在客户端模式下的默认老年代收集器,用于服务端有两种用途:一种是 JDK 5 及之前与 Parallel Scavenge 搭配使用,另一种是作为CMS 发生失败时的预案。 Parellel Old Parallel Scavenge 的老年代版本,支持多线程收集,基于标记-整理算法实现。这个收集器直到 JDK 6 才开始提供,在注重吞吐量优先的场景可以有效考虑Parallel Scavenge 加 Parallel Old 组合。 CMS 以获取最短回收停顿时间为目标的收集器,如果希望系统停顿时间尽可能短以给用户带来更好的体验就可以使用 CMS。 基于标记-清除算法,过程相对复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。 其中初始标记和重新标记仍然需要 STW(Stop The World,表示系统停顿),初始标记仅是标记 GC Roots 能直接关联到的对象,速度很快。并发标记就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,耗时较长但不需要停顿用户线程,可以与垃圾收集线程并发运行。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那一部分对象的标记记录,该阶段停顿时间比初始标记稍长,但远比并发标记短。最后是并发清除,清理标记阶段判断的已死亡对象,由于不需要移动存活对象,因此该阶段也可以与用户线程并发。 由于整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器都可以和用户线程一起工作,所以从总体上说CMS 的内存回收过程是与用户线程并发执行的。 CMS 是 HotSpot 追求低停顿的第一次成功尝试,但还存在三个明显缺点:① 对处理器资源非常敏感,在并发阶段虽然不会导致用户线程暂停,但会降低总吞吐量。② 无法处理浮动垃圾,有可能出现并发失败而导致另一次 FullGC。③ 由于基于标记-清除算法,因此会产生大量空间碎片,给大对象分配带来麻烦。 G1 开创了收集器面向局部收集的设计思路和基于Region的内存布局,是一款主要面向服务端的收集器,最初设计目标是替换CMS。 G1 之前的收集器,垃圾收集的目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可以面向堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收受益最大,这就是 G1 的 MixedGC 模式。 不再坚持固定大小及数量的分代区域划分,而是把 Java 堆划分为多个大小相等的独立区域(Region),每一个 Region 都可以根据需要扮演新生代的 Eden 空间、Survivor 空间或老年代空间。收集器能够对扮演不同角色的 Region 采用不同的策略处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。 跟踪各个 Region 里面的垃圾堆积的价值大小,价值即回收所获得的空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值收益最大的 Region。这种回收方式保证了 G1 在有限的时间内获取尽可能高的收集效率。 G1的运作过程: 初始标记:标记 GC Roots 能直接关联到的对象并修改 TAMS 指针的值,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。该阶段需要 STW 但耗时很短,是借用进行 MinorGC 时同步完成的。 并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图,找出需要回收的对象。该阶段耗时长,但可与用户线程并发执行,当对扫描完成后还要重新处理 SATB 记录的在并发时有引用变动的对象。 最终标记:对用户线程做一个短暂暂停,用于处理并发阶段结束后仍遗留下来的少量 SATB 记录。 筛选回收:对各个 Region 的回收价值和成本排序,根据用户期望的停顿时间指定回收计划,可自由选择任意多个 Region 构成回收集然后把决定回收的那一部分的存活对象复制到空的 Region 中,再清理掉整个旧的 Region 的全部空间。该操作必须暂停用户线程,由多条收集器线程并行完成。 可以由用户指定期望的停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300毫秒比较合适。G1不会存在内存空间碎片的问题,但 G1 为了垃圾收集产生的内存占用和程序运行时的额外执行负载都高于CMS。 低延迟垃圾收集器:指 Shenandoah 和 ZGC,这两个收集器几乎整个工作过程全都是并发的,只有初始标记、最终标记这些阶段有短暂停顿,停顿的时间基本上是固定的。 Shenandoah 相比 G1 内存布局同样基于 Region,默认回收策略也是优先处理回收价值最大的 Region。但在管理堆内存方面,与 G1 有不同:① 支持并发整理,G1 的回收阶段不能与用户线程并发。②默认不使用分代收集,不会有专门的新生代 Region 或老年代 Region。③ 摒弃了在 G1 中耗费大量内存和计算资源去维护的记忆集,改用名为连接矩阵的全局数据结构来记录跨 Region 的引用关系。 ZGC JDK11中新加入的具有实验性质的低延迟垃圾收集器,和 Shenandoah 的目标高度相似,都希望在尽可能对吞吐量影响不大的前提下实现在任意堆大小下都可以把垃圾收集器的停顿时间限制在 10ms 以内的低延迟。 基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理,以低延迟为首要目标。内存布局也采用基于 Region 的堆内存布局,但不同的是 ZGC 的 Region 具有动态性,是动态创建和销毁的,并且区容量大小也是动态变化的。 P10:内存分配与回收策略 以 Seial + Serial Old 客户端默认收集器组合为例: 对象优先在 Eden 区分配 大多数情况下对象在新生代 Eden 区分配,当 Eden 区没有足够空间进行分配时虚拟机将发起一次 MinorGC。 可通过 -XX:Xms 和 -XX:Xmx 设置堆大小, -Xmn 设置新生代的大小, -XX:SurvivorRatio 设置新生代中 Eden 和 Survivor的比例。 大对象直接进入老年代 大对象是指需要大量连续内存空间的对象,最典型的是很长的字符串或者元素数量很庞大的数组。大对象容易导致内存明明还有不少空间时就提前触发垃圾收集以获得足够的连续空间才能安置它们,当复制对象时大对象就意味着高额内存复制开销。 HotSpot 提供了 -XX:PretenureSizeThreshold 参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间来回复制产生大量内存复制操作。 长期存活对象进入老年代 虚拟机给每一个对象定义了一个对象年龄计数器,存储在对象头。对象通常在 Eden 诞生,如果经历过第一次 MinorGC 仍然存活并且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 MinorGC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的年龄阈值可通过 -XX:MaxTenuringThreshold 设置。 动态对象年龄判定 为了适应不同程序的内存状况,虚拟机并不永远要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代,无需等到 -XX:MaxTenuringThreshold 参数设置的年龄。 空间分配担保 发生 MinorGC 前,虚拟机必须先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次 MinorGC可以确定是安全的。 如果不成立,虚拟机会先查看 -XX:HandlePromotionFailure 参数的值是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 MinorGC,如果不满足或不允许担保失败就会改成一次 FullGC。 之所以说冒险是因为新生代使用复制算法,为了内存利用率只使用其中一个 Survivor 作为备份,因此当出现大量对象在 MinorGC 后仍然存活的情况时需要老年代进行分配担保,把 Survivor 无法容纳的对象直接送入老年代。 P11:故障处理工具 jps:虚拟机进程状况工具 jps 即 JVM Process Status,参考了 UNIX 命令的命名格式,功能和 ps 命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)是一致的,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位就必须依赖 jps 命令。 jps 还可以通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程状态,参数 hostid 为 RMI 注册表中注册的主机名。 jstat:虚拟机统计信息监视工具 jstat 即 JVM Statistic Monitoring Tool,是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。 一些参数的含义:S0 和 S1 表示两个 Survivor 区,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示所有 GC 总耗时。 jinfo:Java 配置信息工具 jinfo 表示 Configuration Info for Java,作用是实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值就只能使用 jinfo 的 -flag 选项进行查询。jinfo 还可以把虚拟机进程的 System.getProperties() 的内容打印出来。 jmap:Java 内存映像工具 jmap 表示 Memory Map for Java,jamp 命令用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 命令一样,有部分功能在 Windows 平台下受限,除了生成堆转储快照的 -dump 选项和用于查看每个类实例的 -histo 选项外,其余选项都只能在 Linux 使用。 jhat:虚拟机堆转储快照分析工具 jhat 表示 JVM Heap Analysis Tool,JDK 提供 jhat 命令与 jmap 搭配使用来分析 jamp 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器中查看。 jstack:Java 堆栈跟踪工具 jstack 表示 Stack Trace for Java,jstack 命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过 jstack 查看各个线程的调用堆栈,就可以获知没有响应的现场到底在后台做什么或等待什么资源。 除了上述的基础故障处理工具,还有一些可视化故障处理工具,例如 JHSDB 基于服务性代理的调试工具、JConsole Java 监视与管理控制台、VisualVM 多合一故障处理工具、Java Mission Control 可持续在线监控工具。 P12:类加载机制和初始化时机 在 Class 文件中描述的各类信息最终都需要加载到虚拟机后才能运行和使用。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java类型,这个过程被称为虚拟机的类加载机制。与其他在编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在程序运行期间完成的,这种策略让 Java 进行类加载时增加了性能开销,但却为 Java 应用提供了极高的扩展性和灵活性,Java 可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。 一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分统称为连接。加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的动态绑定特性。 关于何时需要开始类加载的第一个阶段"加载",《 Java 虚拟机规范》没有强制约束,但对于初始化严格规定了有且只有6种情况: 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有初始化则需要先触发初始化。典型场景有:① 使用new关键字实例化对象。② 读取或设置一个类型的静态字段。③ 调用一个类型的静态方法。 对类型进行反射调用时,如果类型没有初始化则需要先触发初始化。 当初始化类时,如果其父类没有初始化则需要先触发父类的初始化。 当虚拟机启动时,用户需要指定一个要执行的主类即包含 main 方法的类,虚拟机会先初始化该类。 当使用 JDK 7 新加入的动态语言支持时,如果 MethodHandle 实例的解析结果为指定类型的方法句柄且这个句柄对应的类没有进行过初始化,则需要先触发其初始化。 当一个接口定义了默认方法时,如果该接口的实现类发生初始化,那接口要在其之前初始化。 除了这六种情况外其余所有引用类型的方式都不会触发初始化,称为被动引用。被动引用的实例:① 子类使用父类的静态字段时,只有直接定义这个字段的父类会被初始化。② 通过数组定义使用类。③ 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。 接口的加载过程和类真正有所区别的是当初始化类时,如果其父类没有初始化则需要先触发其父类的初始化,但在一个接口初始化时并不要求其父接口全部完成了初始化,只有在真正使用到父接口时(如引用接口中定义的常量)才会初始化。 P13:类加载过程 加载 加载是类加载的第一个阶段,在该阶段虚拟机需要完成三件事:① 通过一个类的全限定类名来获取定义此类的二进制字节流。② 将这个字节流所代表的静态存储结构转化为方法区的运行时数据区结构。③ 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。 加载结束后,虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中了,方法区中的数据存储格式完全由虚拟机自定义实现。类型数据安置在方法区之后,会在 Java 堆中实例化一个 Class 对象,这个对象将作为程序员访问方法区中类型数据的外部接口。加载与连接的部分动作是交叉进行的,加载尚未完成时连接可能已经开始。 验证 验证是连接的第一步,目的是确保 Class 文件的字节流中包含的信息符合约束要求,保证这些信息不会危害虚拟机的安全。Java 语言本身是安全的,但如果虚拟机不检查输入的字节流,对其完全信任的话,很可能因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃。 验证主要包含了四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。 验证对于虚拟机的类加载机制来说是一个非常重要但非必需的阶段,因为验证只有通过与否的区别,只要通过了验证其后就对程序运行期没有任何影响了。如果程序运行的全部代码都已被反复使用和验证过,在生产环境的就可以考虑关闭大部分类验证措施缩短类加载时间。 准备 准备是正式为类变量分配内存并设置零值的阶段,该阶段进行的内存分配仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。如果变量被final修饰,编译时 Javac 会为变量生成 ConstantValue 属性,那么在准备阶段虚拟机就会将该变量的值设为程序员指定的值。 解析 解析是将常量池内的符号引用替换为直接引用的过程。 符号引用:符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。与虚拟机内存布局无关,引用目标并不一定是已经加载到虚拟机内存中的内容。 直接引用:直接引用是可以直接指向目标的指针、相对偏移量或者能间接定位到目标的句柄。和虚拟机的内存布局直接相关,引用目标必须已在虚拟机的内存中存在。 解析部分主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符合引用进行。 初始化 初始化是类加载过程的最后一步,直到该阶段,JVM 才真正开始执行类中编写的代码。 准备阶段时变量已经赋过一次系统零值,而在初始化阶段会根据程序员的编码去初始化类变量和其他资源。 初始化阶段就是执行类构造器 <client> 方法的过程,该方法是 Javac 编译器自动生成的。 P14:类加载器和双亲委派模型 类加载阶段中"通过一个类的全限定名来获取描述该类的二进制字节流"的动作被设计为放到 JVM 外部实现,以便让应用程序自己决定如何获取所需的类,实现这个动作的代码就是类加载器。 比较两个类是否相等:对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在虚拟机中的唯一性,每一个类加载器都拥有一个独立的类名称空间。只有在两个类是由同一个类加载器加载的前提下才有意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 从 JVM 的角度看只存在两种不同的类加载器:一种是启动类加载器,由 C++ 语言实现,是虚拟机自身的一部分;另一种是其他所有类加载器,由 Java 语言实现,独立存在于虚拟机外部且全部继承自抽象类 java.lang.ClassLoader。 自 JDK1.2 起 Java 一直保持着三层类加载器、双亲委派的类加载结构。 启动类加载器:负载加载存放在 JAVA_HOME/lib 目录,或者指定路径中存放的能够被虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,如果用户需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 扩展类加载器:负载加载 JAVA_HOME/lib/ext 目录,或者系统变量所指定的路径中的类库。这种扩展机制在 JDK 9 后被模块化所取代,由于扩展类加载器是由 Java 编写的,开发者可以直接在程序中使用扩展类加载器来加载 Class 文件。 应用程序类加载器:也称系统类加载器,负载加载用户类路径上的所有类库,同样可以直接在代码中使用。如果应用程序中没有自定义类加载器,一般情况下该类加载器就是程序中默认的类加载器。 双亲委派模型 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承关系来实现的,而通常使用组合关系来复用父加载器的代码。 如果一个类加载器收到了类加载请求,它不会自己去尝试加载这个类,而首先将该请求委派给自己的父加载器完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成请求时,子加载器才会尝试自己完成加载。 好处是 Java 中的类跟随它的类加载器一起具备了一种带有优先级的层次关系,可以保证某个类在程序的各个类加载器环境中都是同一个类,对于保证程序的稳定运行极为重要。 P15:Java 程序运行的过程 通过 Javac 编译器将 .java 代码转为 JVM 可以加载的 .class 字节码文件。 Javac 编译器是由 Java 语言编写的程序,从 Javac 代码的总体结构看,编译过程可以分为 1 个准备过程和 3 个处理过程:① 准备过程:初始化插入式注解处理器。② 解析与填充符号表过程:进行词法、语法分析,将源代码的字符流转为标记集合,构造出抽象语法树。填充符号表,产生符号地址和符号信息。③ 插入式注解处理器的注解处理过程。④ 分析与字节码生成过程,包括标注检查,对语法的静态信息进行检查;数据流及控制流分析,对程序动态运行过程进行检查;解语法糖,将简化代码编写的语法糖还原为原有的形式;字节码生成,将前面各个步骤的信息转换为字节码。 Javac 属于前端编译器,完成了从程序到抽象语法树或中间字节码的生成,在此之后还有一组内置于 JVM 内部的后端编译器,即即时编译器或提前编译器,来完成代码优化以及从字节码生成本地机器码的过程。 通过即时编译器 JIT 把字节码文件编译成本地机器码。 Java 程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为"热点代码",热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,在运行时虚拟机会把这些代码编译成本地机器码,并尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。 客户端编译器的执行过程:① 平***立的前端将字节码构造成一种高级中间代码表示 HIR。② 平台相关的后端从 HIR 中产生低级中间代码表示 LIR。③ 在平台相关的后端使用线性扫描算法在 LIR 上分配寄存器,并在 LIR 上做窥孔优化,然后产生机器代码。 服务端编译器专门面向服务端的典型应用场景,并为服务器的性能配置针对性调整过的编译器,也是一个能容忍很高优化复杂度的高级编译器,它会执行大部分经典的优化动作,如无用代码消除、循环表达式外提、消除公共子表达式、基本块重排序等,还会实施一些与 Java 语言特性相关的优化,如范围检查消除、空值检查消除等,也可能根据解释器或客户端编译器提供的性能监控信息进行一些不稳定的预测性激进优化。 还可以通过静态的提前编译器 AOT 直接把程序编译成与目标机器指令集相关的二进制代码。 并发 20 P1:Java 内存模型 Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则,关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。此处的变量包括实例字段、静态字段和构成数组元素的对象,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争问题。为了获得更好的执行效率,JMM 没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器是否要进行调整代码执行顺序这类优化措施,JMM 遵循一个基本原则:只要不改变程序执行结果,编译器和处理器怎么优化都行。例如编译器分析某个锁只会单线程访问就消除该锁,某个 volatile 变量只会单线程访问就把它当作普通变量。 JMM 规定了所有变量都存储在主内存中,每条线程还有自己的工作内存,工作内存中保存了被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间中进行,而不能直接读写主内存中的数据。不同线程之间也无法直接访问对方工作内存中的变量,两个线程之间的通信必须经过主内存,JMM 通过控制主内存与每个线程的工作内存之间的交互来提供内存可见性保证。 关于主内存与工作内存之间的交互,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存等实现细节,JMM 定义了 8 种原子操作: lock:作用于主内存变量,把变量标识为一条线程独占的状态。 unlock:作用于主内存变量,把处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定。 read:作用于主内存变量,把变量值从主内存传到工作内存。 load:作用于工作内存变量,把 read 从主存中得到的值放入工作内存的变量副本。 use:作用于工作内存变量,把工作内存中的变量值传给执行引擎,每当虚拟机遇到需要使用变量值的字节码指令时执行该操作。 assign:作用于工作内存变量,把从执行引擎接收的值赋给工作内存变量,每当虚拟机遇到给变量赋值的字节码指令时执行该操作。 store:作用于工作内存变量,把工作内存中的变量值传送到主内存。 write:作用于主内存变量,把 store 从工作内存取到的变量值放入主内存变量中。 如果要把一个变量从主内存拷贝到工作内存,就要按顺序执行 read 和 load ,如果要把变量从工作内存同步回主内存,就要按顺序执行 store 和 write 。JMM 只要求这两种操作必须按顺序执行,但不要求连续,也就是说 read 和 load、store 和 write 之间可插入其他指令。这种定义十分严谨但过于复杂,之后 Java 将内存操作简化为 lock、unlock、read 和 write 四种,但这只是语言描述上的等价化简。 P2:as-if-serial 和 happens-before as-if-serial as-if-serial 的语义是:不管怎么重排序,单线程程序的执行结果不能被改变,编译器和处理器必须遵循 as-if-serial 语义。 为了遵循 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。 as-if-serial 语义把单线程程序保护了起来,给了程序员一种幻觉:单线程程序是按程序的顺序执行的,使程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。 happens-before 先行发生原则,是 JMM 中定义的两项操作之间的偏序关系,它是判断数据是否存在竞争,线程是否安全的重要手段。 JMM 将 happens-before 要求禁止的重排序按是否会改变程序执行结果分为两类。对于会改变结果的重排序 JMM 要求编译器和处理器必须禁止这种重排序,对于不会改变结果的重排序,JMM 对编译器和处理器不做要求。 JMM 存在一些天然的 happens-before 关系,无需任何同步器协助就已经存在。如果两个操作的关系不在此列,并且无法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进行重排序。 程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。 管程锁定规则:一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。 volatile 规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。 线程启动规则:线程对象的 start 方法先行发生于此线程的每一个动作。 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。 线程中断规则:对线程 interrupt 方法的调用先行发生于被中断线程的代码检测到中断事件的发生。 对象终结规则:一个对象的初始化完成先行发生于它的 finalize 方法的开始。 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C 。 区别 as-if-serial 保证单线程程序的执行结果不被改变,happens-before 保证正确同步的多线程程序的执行结果不被改变。这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行的并行度。 P3:指令重排序 重排序指从源代码到指令序列的重排序,在执行程序时为了提高性能,编译器和处理器通常会对指令进行重排序,分为三种: 编译器优化的重排序:编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作操作看上去可能是乱序执行。 从 Java 源代码到最终实际执行的指令序列,会分别经历编译器优化重排序、指令级并行重排序和内存系统重排序,这些重排序可能会导致多线程程序出现内存可见性问题。 对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令,即一组用于实现对内存操作顺序限制的处理器指令,通过内存屏障指令来禁止特定类型的处理器重排序。JMM 属于语言级的内存模型,它确保在不同的编译器和处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。 P4:原子性、可见性和有序性 原子性 由 JMM 直接保证的原子性变量操作包括 read、load、assign、use、store 和 write,基本数据类型的访问都是具备原子性的,例外就是 long 和 double 的非原子性协定,允许虚拟机将没有被 volatile 修饰的 64 位数据的操作划分为两次 32 位的操作。 如果应用场景需要更大范围的原子性保证,JMM 还提供了 lock 和 unlock 操作满足这种需求,尽管 JVM 没有把这两种操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是 synchronized 关键字。 可见性 可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知修改。JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性,无论是普通变量还是volatile变量都是如此,区别是 volatile 保证新值能立即同步到主内存以及每次使用前立即从主内存刷新,因此说 volatile 保证了多线程操作变量的可见性,而普通变量则不能保证。 除了 volatile 之外,还有两个关键字能实现可见性,分别是 synchronized 和 final,同步块的可见性是由"对一个变量执行unlock 前必须先把此变量同步回主内存中,即先执行 store 和 write"这条规则获得的。final 的可见性是指:被 final 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把"this"引用传递出去,那么其他线程就能看到 final 字段的值。 有序性 有序性可以总结为:在本线程内观察所有操作是有序的,在一个线程内观察另一个线程,所有操作都是无序的。前半句是指"as-if-serial 语义",后半句是指"指令重排序"和"工作内存与主内存同步延迟"现象。 Java 提供了 volatile 和 synchronized 保证线程间操作的有序性,volatile 本身就包含了禁止指令重排序的语义,而 synchronized 则是由"一个变量在同一个时刻只允许一条线程对其进行lock操作"这条规则获得的,该规则决定了持有同一个锁的两个同步块只能串行进入。 P5:volatile 关键字 JVM 提供的最轻量级的同步机制,JMM 为 volatile 定义了一些特殊的访问规则,当一个变量被定义为 volatile 后具备两种特性: 保证此变量对所有线程的可见性 可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是立即可以得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。 volatile 变量在各个线程的工作内存中不存在一致性问题,但 Java 的运算操作符并非原子操作,这导致 volatile 变量运算在并发下仍是不安全的。 禁止指令重排序优化 使用 volatile 变量进行写操作,汇编指令操作是带有 lock 前缀的,相当于一个内存屏障,后面的指令不能重排到内存屏障之前的位置。只有一个处理器时不需要使用内存屏障,但如果有两个或更多的处理器访问同一块内存,且其中有一个在观测另一个,就需要使用内存屏障来保证一致性了。 使用 lock 前缀的指令在多核处理器中会引发两件事:① 将当前处理器缓存行的数据写回到系统内存。② 这个写回内存的操作会使其他在CPU里缓存了该内存地址的数据无效。 这种操作相当于对缓存中的变量做了一次 store 和 write 操作,可以让 volatile 变量的修改对其他处理器立即可见。 静态变量 i 执行多线程 i++ 的不安全问题 通过反编译会发现一个自增语句是由 4 条字节码指令构成的,依次为getstatic、iconst_1、iadd、putstatic,当getstatic把 i 的值取到操作栈顶时,volatile保证了 i 的值在此刻是正确的,但是在执行iconst_1、iadd这些指令时,其他线程可能已经改变了i的值,而操作栈顶的值就变成了过期的数据,所以 putstatic 指令执行后就可能把较小的 i 值同步回了主内存。 即使编译出来只有一条字节码指令也不能意味着这条指令就是一个原子操作,一条字节码指令在解释执行时,解释器要运行很多行代码才能实现它的语义。如果是编译执行,一条字节码指令也可能转化成若干条本地机器码指令。 适用场景 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。 变量不需要与其他状态变量共同参与不变约束。 volatile的内存语义 从内存语义角度来说,volatile的写-读与锁的释放-获取具有相同的内存效果。 写内存语义:当写一个volatile变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。 读内存语义:当读一个volatile变量时,JMM 会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。 volatile指令重排序的特点 当第二个操作是volatile 写时,不管第一个操作是什么都不能重排序,确保写之前的操作不会被编译器重排序到写之后。 当第一个操作是volatile 读时,不管第二个操作是什么都不能重排序,确保读之后的操作不会被编译器重排序到读之前。 当第一个操作是volatile 写,第二个操作是 volatile 读时不能重排序。 JSR-133 增强 volatile 语义的原因 在旧的内存模型中,虽然不允许 volatile 变量之间重排序,但允许 volatile 变量与普通变量重排序,可能导致内存不可见问题。为了提供一种比锁更轻量级的线程通信机制,严格限制了编译器和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相同的内存语义。 P6:final 关键字 final 可以保证可见性,被 final 修饰的字段在构造器中一旦被初始化完成,并且构造器没有把 this 的引用传递出去,那么在其他线程中就能看见 final 字段的值。 JSR-133 增强 final语义的原因 在旧的 JMM 中,一个严重的缺陷就是线程可能看到 final 域的值会改变。比如一个线程看到一个 int 类型 final 域的值为0,此时该值是还未初始化之前的零值,过一段时间之后该值被某线程初始化后这个线程再去读这个 final 域的值会发现值变为1。 为了修复该漏洞,JSR-133 通过为 final 域增加写和读重排序规则,提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造方法中没有逸出),那么不需要使用同步就可以保证任意线程都能看到这个final域在构造方法中被初始化之后的值。 写 final 域重排序规则 禁止把 final 域的写重排序到构造方法之外,编译器会在final域的写之后,构造方法的 return之前,插入一个Store Store屏障。该规则可以确保在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域不具有这个保障。 对于引用类型,增加了约束:在构造方法内对一个 final 引用的对象的成员域的写入,与随后在构造方法外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。 读 final 域重排序规则 在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM 禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个Load Load 屏障,该规则可以确保在读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。 P7:synchronized 关键字 最基本的互斥同步手段就是 synchronized 关键字,它是一种块结构的同步语法。synchronized 经过 Javac 编译后,会在同步块的前后分别形成 monitorenter 和 monitorexit 两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的Class对象;对于同步方法块,锁是 synchronized 括号里的对象。 根据《 Java 虚拟机规范》的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁。如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,那么就把锁的计数器的值增加 1,而在执行 monitorexit 指令时会将锁计数器的值减 1。一旦计数器的值为 0,锁随即就被释放了。如果获取锁对象失败,那当前线程就应该被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。 被 synchronized 修饰的同步块对一条线程来说是可重入的,并且同步块在持有锁的线程执行完毕并释放锁之前,会无条件地阻塞后面其他线程的进入。从执行成本的角度看,持有锁是一个重量级的操作。在主流 JVM 实现中,Java 的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统帮忙完成,这就不可避免陷入用户态到核心态的转换中,进行这些状态转换需要耗费很多的处理器时间。 每个 Java 对象都有一个关联的 monitor 监视器,当这个对象由同步块或同步方法调用时,执行方法的线程必须先获取到对象的监视器才能进入同步块或同步方法。例如有两个线程 A 和 B 竞争锁资源对应的 monitor,当线程 A 竞争到锁时,会将 monitor 中的 owner 设置为 A,把线程 B 阻塞并放到等待竞争资源的 ContentionList 队列。ContentionList 中的部分线程会进入 EntryList,EntryList 中的线程会被指定为 OnDeck 竞争候选者线程,如果获得了锁资源将进入 Owner 状态,释放锁资源后进入 !Owner 状态。被阻塞的线程会进入 WaitSet。 不公平的原因 所有收到锁请求的线程首先自旋,如果通过自旋也没有获取锁资源将被放入 ContentionList 队列,该做法对于已经进入队列的线程是不公平的。 为了防止 ContentionList 尾部的元素被大量线程进行 CAS 访问影响性能,Owner 线程会在释放锁时将 ContentionList 的部分线程移动到 EntryList 并指定某个线程为 OnDeck 线程,Owner 并没有将锁直接传递给 OnDeck 线程而是把锁竞争的权利交给它,该行为叫做竞争切换,牺牲了公平性但提高了性能。 P8:锁优化 JDK 6 对 synchronized 做了很多优化,引入了适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提高锁的效率,锁一共有 4 个状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高获得锁和释放锁的效率。 自旋锁与自适应自旋 互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给 JVM 的并发性能带来了很大压力。同时虚拟机开发团队也注意到了在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂机和恢复线程并不值得。现在绝大多数的个人电脑和服务器都是多核心处理器系统,如果物理机器有一个以上的处理器或者处理器核心,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环,这项技术就是所谓的自旋锁。 自旋锁在 JDK 1.4中就已经引入,只不过默认是关闭的,在 JDK 6中就已经改为默认开启了。自旋等待不能代替阻塞,自旋等待本身虽然避免了线程切换的开销,但它要占用处理器时间,所以如果锁被占用的时间很短,自旋的效果就会非常好,反之只会白白消耗处理器资源。因此自旋的时间必须有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程。自旋次数的默认次数是 10 次。 在 JDK 6 中对自旋锁的优化,引入了自适应自旋。自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能之间省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行时间的增长以及性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越精准。 锁消除 锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上的数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。 锁粗化 原则上我们在编写代码时,总是推荐将同步块的作用范围限制得尽量小,只在共享数据得实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁得线程也能尽可能快拿到锁。 大多数情况下这种原则是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,那么即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能消耗。 如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。 偏向锁 它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都去掉,连 CAS 操作都不去做了。 偏向锁的意思就是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。 当一个线程访问同步代码块并获取锁时,会在对象头和帧栈中的锁记录里存储锁偏向的线程 ID,以后该线程再进入和退出同步代码块不需要进行 CAS 操作来加锁和解锁,只需要简单地测试一下对象头的"Mark Word"里是否存储着指向当前线程的偏向锁。如果测试成功表示线程已经获得了锁,如果失败则需要再测试一下"Mark Word"中偏向锁的标识是否设置成了 1 即表示当前使用偏向锁,如果设置了就尝试使用 CAS 将对象头的偏向锁指向当前线程,否则使用 CAS 方式竞争锁。 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销需要等待全局安全点即此时没有正在执行的字节码,它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态则将对象头设为无锁状态。如果线程还活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的"Mark Word"要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。 轻量级锁 轻量级是相对于操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为重量级锁。轻量级锁并不是用来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 在代码即将进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word的拷贝。然后虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向锁记录的指针,如果这个更新操作成功了,即代表该线程拥有了这个对象的锁,并且锁标志位将转变为"00",表示此对象处于轻量级锁定状态。 如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前线程以及拥有了这个对象的锁,直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态变为"10",此时Mark Word中存储的就是指向重量级锁的指针,后面等待锁的线程也必须进入阻塞状态。 解锁操作也同样是通过 CAS 操作来进行,如果对象的 Mark Word 仍然指向线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程复制的 Mark Word 替换回来。假如能够替换成功,那整个同步过程就顺利完成了,如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时唤醒被挂起的线程。 偏向锁、轻量级锁和重量级锁的区别 偏向锁的优点是加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距,缺点是如果线程间存在锁竞争会带来额外锁撤销的消耗,适用于只有一个线程访问同步代码块的场景。 轻量级锁的优点是竞争的线程不会阻塞,提高了程序的响应速度,缺点是如果线程始终得不到锁会自旋消耗CPU,适用于追求响应时间和同步代码块执行非常快的场景。 重量级锁的优点是线程竞争不使用自旋不会消耗CPU,缺点是线程会被阻塞,响应时间很慢,适应于追求吞吐量、同步代码块执行较慢的场景。 P9:Lock 接口 自 JDK 5 起 Java类库提供了 juc 并发包,其中的 Lock 接口成为了一种全新的互斥同步手段。基于Lock 接口,用户能够以非块结构来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步。 重入锁 ReentrantLock 是 Lock 接口最常见的一种实现,它与 synchronized 一样是可重入的,在基本用法上也很相似,不过它增加了一些高级功能,主要包括以下三项: 等待可中断:是指持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待而处理其他事情。可中断特性对处理执行时间非常长的同步块很有帮助。 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁,而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized中的锁是非公平的,ReentrantLock在默认情况下也是非公平的,但可以通过带有布尔值的构造器要求使用公平锁。不过一旦使用了公平锁,将会导致性能急剧下降,明显影响吞吐量。 锁绑定多个条件:是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象。在 synchronized中,锁对象的 wait 跟它的notify/notifyAll 方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联时就不得不额外添加一个锁,而 ReentrantLock 可以多次调用 newCondition 方法。 一般优先考虑使用synchronized:① synchronized 是 Java 语法层面的同步,足够清晰和简单。② Lock 应该确保在 finally 中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证,而使用 synchronized 可以由 JVM 来确保即使出现异常锁也能被正常释放。③ 尽管在 JDK 5 时ReentrantLock 的性能领先于 synchronized,但在 JDK 6 进行锁优化之后,二者的性能基本能够持平。从长远来看 JVM 更容易针对synchronized进行优化,因为 JVM 可以在线程和对象的元数据中记录 synchronized 中锁的相关信息,而使用Lock的话 JVM 很难得知具体哪些锁对象是由特定线程持有的。 ReentrantLock 的可重入实现 以非公平锁为例,通过 nonfairTryAcquire 方法获取锁,该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求则将同步状态值进行增加并返回 true,表示获取同步状态成功。 成功获取锁的线程再次获取锁,只是增加了同步状态值,这就要求 ReentrantLock 在释放同步状态时减少同步状态值。如果该锁被获取了 n 次,那么前 n-1 次 tryRelease 方法必须都返回fasle,只有同步状态完全释放了才能返回 true,该方法将同步状态是否为 0 作为最终释放的条件,当同步状态为 0 时,将占有线程设置为null,并返回 true 表示释放成功。 对于非公平锁只要 CAS 设置同步状态成功则表示当前线程获取了锁,而公平锁则不同。公平锁使用 tryAcquire 方法,该方法与nonfairTryAcquire 的唯一区别就是判断条件中多了对同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true 表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。、 P10:读写锁 ReentrantLock 是排他锁,在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一个读锁和一个写锁,通过分离读写锁使并发性相比一般的排他锁有了很大提升。 除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。只需要在读操作时获取读锁,写操作时获取写锁即可,当写锁被获取时后续的读写操作都会被阻塞,写锁释放之后所有操作继续执行,编程方式相对于使用等待/通知机制的实现方式变得简单。 读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在同步状态即一个整形变量上维护多个读线程和一个写线程的状态。如果在一个 int 型变量上维护多种状态,就一定要按位切割使用这个变量,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。 写锁是一个支持重入的排他锁,如果当前线程已经获得了写锁则增加写状态,如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获得写锁的线程则当前线程进入等待状态。写锁的释放与 ReentrantLock 的释放过程类似,每次释放均减少写状态,当写状态为 0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。 读锁是一个支持重入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功地获取,而所做的只是线程安全地增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取则进入等待状态。读锁的每次释放均会减少读状态,减少的值是(1<<16),读锁的每次释放是线程安全的。 锁降级指的是写锁降级成为读锁,如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级指的是把持住当前拥有的写锁,再获取到读锁,随后释放先前拥有的写锁的过程。 锁降级中读锁的获取是必要的,主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程 A 获取了写锁修改了数据,那么当前线程是无法感知线程 A 的数据更新的。如果当前线程获取读锁,即遵循锁降级的步骤,线程 A 将会被阻塞,直到当前线程使用数据并释放读锁之后,线程 A 才能获取写锁进行数据更新。 P11:AQS 队列同步器 队列同步器是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 类型的成员变量表示同步状态,通过内置的 FIFO 队列来完成资源获取线程的排队工作。 使用方式 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法 getState、setState 和 compareAndSetState 来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型地同步组件。 和锁的关系 同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所关注的领域。 同步队列 AQS 中每当有新的线程请求资源时,该线程都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源。等待队列通过双向链表实现,线程会被封装在链表的 Node 节点中,Node 的等待状态包括:CANCELLED 表示线程已取消、SIGNAL 表示线程需要唤醒、CONDITION 表示线程正在等待、PROPAGATE 表示后继节点会传播唤醒操作,只会在共享模式下起作用。 两种模式 独占模式表示锁会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取到锁继续执行,在同一时间内只能有一个线程获取到这个锁,ReentrantLock 就采用的是独占模式。 共享模式表示多个线程获取同一个锁的时候有可能会成功,ReadLock 就采用的是共享模式。 独占模式通过 acquire 和 release 方法获取和释放锁,共享模式通过 acquireShared 和 releaseShared 方法获取和释放锁。 独占式的获取和释放流程 在获取同步状态时,同步器调用 acquire 方法,维护一个同步队列,使用 tryAcquire 方法安全地获取线程同步状态,获取状态失败的线程会构造同步节点并通过 addWaiter 方法被加入到同步队列的尾部,并在队列中进行自旋。之后会调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞节点被中断实现,移出队列或停止自旋的条件是前驱节点是头结点并且成功获取了同步状态。 在释放同步状态时,同步器调用 tryRelease 方法释放同步状态,然后调用 unparkSuccessor 方法(该方法使用 LockSupport 唤醒处于等待状态的线程)唤醒头节点的后继节点,进而使后继节点重新尝试获取同步状态。 只有当前驱节点是头节点时才能够尝试获取同步状态原因 头节点是成功获取到同步状态的节点,而头节点的线程释放同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。 维护同步队列的FIFO原则,节点和节点在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头结点的线程由于中断而被唤醒)。 共享式的获取和释放流程 在获取同步状态时,同步器调用 acquireShared 方法,该方法调用 tryAcquireShared 方法尝试获取同步状态,返回值为 int 类型,当返回值大于等于 0 时表示能够获取到同步状态。因此在共享式获取锁的自旋过程中,成功获取到同步状态并退出自旋的条件就是该方法的返回值大于等于0。 释放同步状态时,调用 releaseShared 方法,释放同步状态后会唤醒后续处于等待状态的节点。对于能够支持多线程同时访问的并发组件,它和独占式的主要区别在于 tryReleaseShared 方法必须确保同步状态安全释放,一般通过循环和 CAS 来保证,因为释放同步状态的操作会同时来自多个线程。 P12:线程 现代操作系统在运行一个程序时会为其创建一个进程,而操作系统调度的最小单位是线程,线程也叫轻量级进程。在一个进程中可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上告诉切换,让使用者感觉到这些线程在同时执行。 生命周期 ①NEW:初始状态,创建后还没有启动的线程处于这种状态,此时还没有调用 start 方法。②RUNNABLE:Java 线程将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能正在等待操作系统分配CPU时间片,也有可能正在执行③BLOCKED:阻塞状态,阻塞状态与等待状态的区别是阻塞状态在等待一个排它锁,在程序等待进入同步区域时线程将进入这种状态。④WAITING:等待状态,表示线程进入等待状态,处于该状态的线程不会被分配CPU时间片,进入该状态表示当前线程需要等待其他线程通知或中断。会导致线程陷入等待状态的方法:没有设置超时参数的wait 和 join方法、LockSupport的 park 方法。⑤TIME_WAITING:限期等待状态,该状态不同于 WAITING,可以在指定时间内自行返回。会导致线程陷入限期等待状态的方法:设置了超时参数的 wait 和 join 方法、LockSupport 的 parkNanos 和 parkUntil 方法。⑥TERMINATED:终止状态,表示当前线程已经执行完毕或异常退出。 实现方式 ①继承 Thread 类并重写 run方法。优点是实现简单,缺点是不能继承其他类,功能单一。②实现 Runnable 接口并重写 run 方法,并将该实现类作为参数传入 Thread 构造器。优点是避免了单继承的局限性,适合多个相同程序代码的线程共享一个资源,实现解耦操作,代码和线程独立。③实现 Callable 接口并重写 call 方法,包装成 FutureTask 对象并作为参数传入Thread构造器。优点是可以获取线程执行结果的返回值。④可以通过线程池创建。 方法 ① wait是Object类的方法,调用wait方法的线程会进入WAITING状态,只有等待其他线程的通知或被中断后才会解除阻塞,调用wait方***释放锁资源。② sleep 是 Thread 类的方法,调用 sleep 方***导致当前线程进入休眠状态,与 wait 不同的是该方法不会释放锁资源,进入的是 TIMED-WAITING 状态。③ yiled 方***使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到 RUNNABLE 状态,与其他线程一起重新竞争CPU时间片。④ join 方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。 守护线程 守护线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作,当 JVM 中不存在非守护线程时,JVM 将会退出,可以通过 setDaemon(true) 将线程设置为daemon线程,但必须在线程启动之前设置。守护线程被用于完成支持性工作,但是在 JVM 退出时守护线程中的 finally 块并不一定会被执行,因为当 JVM 中没有非守护线程时需要立即退出,所有守护线程都将立即终止,因此不能依靠 finally 确保执行关闭或清理资源的逻辑。 P13:线程间通信 通信是指线程之间以何种机制来交换信息,在命令式编程中线程之间的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里线程之间没有公共状态,线程之间必须通过发送消息来显示通信。Java 并发采用共享内存模型,线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。 volatile 和 synchronized 关键字 volatile 可以修饰字段,告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回主内存,它能保证所有线程对变量访问的可见性。 synchronized 可以修饰方法或以同步块的形式使用,它主要确保多个线程在同一个时刻只能有一个线程处于方法或同步块中,保证了线程对变量访问的可见性和排他性。 等待/通知机制 等待通知机制是指一个线程 A 调用了对象 O 的 wait 方法进入等待状态,而另一个线程 B 调用了对象 O 的 notify 或 notifyAll 方法,线程 A 收到通知后从 wait 方法返回,进而执行后序操作。两个线程通过对象 O 完成交互,对象上的 wait 和 notify/notifyAll 就如同开关信号,用来完成等待方和通知方之间的交互工作。 管道 IO 流 管道 IO 流和普通文件IO 流或网络 IO 流的不同之处在于它主要用于线程之间的数据传输,传输的媒介为内存。管道流主要包括了四种具体实现:PipedOutputStream、PipedInputStream、PipedReader 和 PipedWriter,对于 Piped 类型的流必须要通过 connect 方法进行绑定。 Thread.join 如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止之后才返回,这里涉及了等待/通知机制。join 方法的底层是通过 wait 方法实现的,当线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。 ThreadLocal ThreadLoacl 是线程变量,内部以 ThreadLocal 为键,任意对象为值的存储结构实现。该结构绑定在每个线程上,存储的值在每个线程中都是一个唯一副本,每个线程可以通过 ThreadLocal 对象访问自己唯一的值。 这种存储结构叫 ThreadLocalMap ,是 ThreadLocal 的一个静态内部类,是一个弱引用集合,它的存值、取值实现类似于 HashMap,使用 set 设置值,使用 get 获取值。使用弱引用的目的是为了节约资源,如果执行过程中发生了 GC,ThreadLocal 是 null 但由于 ThreadLocalMap 生命周期和线程一样,不会被回收,这时候就会导致 ThreadLocalMap 的 key 不存在而 value 还在的内存泄漏问题,解决办法是使用完 ThreadLocal 后执行remove操作。 P14:ConcurrentHashMap JDK 8 之前 ConcurrentHashMap 用于解决 HashMap 的线程不安全和 HashTable 的并发效率低下问题,HashTable 之所以效率低下是因为所有线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器一部分数据,那么多线程访问容器不同数据段的数据时线程间就不会存在锁竞争,从而有效提高并发效率,这就是 ConcurrentHashMap 的锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。 get 操作实现简单高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。get 的高效在于这个过程不需要加锁,除非读到空值才会加锁重读。get 方法里将要使用的共享变量都定义为 volatile 类型,volatile 保证了多线程的可见性,可以多线程读,但是只能保证单线程写,在 get 操作里只需要读所以不用加锁。 put 操作必须加锁,put 方法首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。 size 操作用于统计元素的数量,必须统计每个 Segment 的大小然后求和,在统计结果累加的过程中,之前累加过的 count 变化的几率很小,因此 ConcurrentHashMap 的做法是先尝试两次通过不加锁的方式统计结果,如果统计过程中容器大小发生了变化则再通过加锁的方式统计所有 Segment 的大小。判断容器是否发生变化是根据 modCount 变量确定的。 JDK 8 开始 JDK 8 的实现摒弃了 Segment 分段概念,使用 Synchronized 和 CAS 来保证线程安全。 get 操作同样不需要同步控制,put 操作时如果没有出现哈希冲突,就使用 CAS 方式来添加元素,如果出现了哈希冲突就使用 synchronized 加锁的方式添加元素。 P15:CAS 操作 CAS 表示 Compare And Swap,比较并交换,CAS 需要三个操作数,分别是内存位置 V、旧的预期值 A 和准备设置的新值 B。CAS 指令执行时,当且仅当 V 符合 A 时,处理器才会用 B 更新 V 的值,否则它就不执行更新。但不管是否更新都会返回 V 的旧值,这些处理过程是原子操作,执行期间不会被其他线程打断。 在 JDK 5 后,Java 类库中才开始使用 CAS 操作,该操作由 Unsafe 类里的 compareAndSwapInt 等几个方法包装提供。HotSpot 在内部对这些方法做了特殊处理,即时编译的结果是一条平台相关的处理器 CAS 指令。Unsafe 类不是给用户程序调用的类,因此在 JDK 9 之前只有 Java 类库可以使用 CAS,譬如 juc 包里的 AtomicInteger类中 compareAndSet 等方法都使用了Unsafe 类的 CAS 操作来实现。 尽管 CAS 既简单又高效,但这种操作无法涵盖互斥同步的所有场景,并且 CAS 从语义上来说存在一个逻辑漏洞:如果 V 初次读取的时候是 A,并且在准备赋值的时候检查到它的值仍为 A,这依旧不能说明它的值没有被其他线程更改过,因为这段时间内假设它的值先改为了 B 又改回 A,那么 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 ABA 问题,juc 包提供了一个 AtomicStampedReference,原子更新带有版本号的引用类型,它可以通过控制变量值的版本来解决 ABA 问题。这个类并不常用,大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决该问题,改用传统的互斥同步可能会比原子类更高效。 P16:原子操作类 Java 从 JDK 5 开始提供了 java.util.concurrent.atomic 包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。到 JDK 8 该包共有17个类,依据作用分为四种:原子更新基本类型类、原子更新数组类、原子更新引用类以及原子更新字段类,atomic 包里的类基本都是使用 Unsafe 实现的包装类。 原子更新基本类型 AtomicInteger 原子更新整形、 AtomicLong 原子更新长整型、AtomicBoolean 原子更新布尔类型。 getAndIncrement 以原子方式将当前的值加 1,首先在 for 死循环中取得 AtomicInteger 里存储的数值,第二步对 AtomicInteger 当前的值进行加 1 操作,第三步调用 compareAndSet 方法进行原子更新,该操作先检查当前数值是否等于 expect,如果等于则说明当前值没有被其他线程修改,则将值更新为 next,否则会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。 atomic 包中只提供了 三种基本类型的原子更新,atomic 包里的类基本都是使用 Unsafe 实现的,Unsafe 只提供三种 CAS 方法:compareAndSwapInt、compareAndSwapLong 和 compareAndSwapObject,例如原子更新 Boolean 时是先转成整形再使用 compareAndSwapInt 进行 CAS,所以原子更新 char、float、double 也可以用类似思路实现。 原子更新数组 AtomicIntegerArray,原子更新整形数组里的元素、 AtomicLongArray 原子更新长整型数组里的元素、 AtomicReferenceArray 原子更新引用类型数组里的元素。 原子更新引用 AtomicReference 原子更新引用类型、AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 类型的标记位、 AtomicStampedReference 原子更新带有版本号的引用类型,关联一个整数值用于原子更新数据和数据的版本号,可以解决 ABA 问题。 原子更新字段 AtomicIntegerFieldUpdater 原子更新整形字段的更新器、 AtomicLongFieldUpdater 原子更新长整形字段的更新器AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器。 由于原子更新字段类都是抽象类,每次使用的时候必须使用静态方法 newUpdater 创建一个更新器,并且需要设置想要更新的类和字段。并且更新类的字段必须使用 public volatile 修饰。 JDK 8 更新的类 DoubleAccumulator 、 LongAccumulator、DoubleAdder、LongAdder、Striped64。 P17:并发工具类 等待多线程完成的 CountDownLatch CountDownLatch 允许一个或多个线程等待其他线程完成操作,构造器接收一个 int 类型的参数作为计数器,如果要等待 n 个点就传入 n。每次调用 countDown 方法时计数器减 1,await 方***阻塞当前线程直到计数器变为0,由于 countDown方法可用在任何地方,所以 n 个点既可以是 n 个线程也可以是一个线程里的 n 个执行步骤。 同步屏障 CyclicBarrier 同步屏障的作用是让一组线程到达一个屏障或同步点时被阻塞,直到最后一个线程到达屏障时,屏障才会解除,所有被拦截的线程才会继续运行。构造器中的参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 自己已到达屏障,然后当前线程被阻塞。还支持在构造器中传入一个 Runable 类型的任务,当线程到达屏障时会优先执行该任务。适用于多线程计算数据,最后合并计算结果的应用场景。 CountDownLacth 的计数器只能用一次,而 CyclicBarrier 的计数器可使用 reset 方法重置,所以 CyclicBarrier 能处理更为复杂的业务场景,例如计算错误时可用重置计数器重新计算。 控制并发线程数的 Semaphore 信号量用来控制同时访问特定资源的线程数量,它通过协调各个线程以保证合理使用公共资源。信号量可以用于流量控制,特别是公共资源有限的应用场景,比如数据库连接。Semaphore 的构造器参数接收一个 int 值,表示可用的许可数量即最大并发数。使用acquire 方法获得一个许可证,使用 release 方法归还许可,还可以用 tryAcquire 尝试获得许可。 线程间交换数据的 Exchanger 交换者是用于线程间协作的工具类,用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。这两个线程通过 exchange 方法交换数据,如果第一个线程先执行exchange方法它会阻塞等待第二个线程执行exchange方法,当两个线程都到达同步点时这两个线程就可以交换数据,将本线程生产出的数据传递给对方。应用场景包括遗传算法、校对工作等。 P18:线程池 好处 ① 降低资源消耗,复用已创建的线程降低线程创建和消耗的开销。② 提高响应速度,当任务到达时,任务可以不需要等到线程创建就可以立即执行。③ 提高线程的可管理性,线程是稀缺资源,如果无限制地创建不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。 当提交一个新任务到线程池时的处理流程 ① 判断核心线程池里的线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务,此时 workCount < corePoolSize,这一步需要获取全局锁。② 如果核心线程池已满,判断工作队列是否已满,如果没有就将新提交的任务存储在工作队列中,此时 workCount >= corePoolSize。③ 如果工作队列已满,判断线程池的线程是否都处于工作状态,如果没有则创建一个新的工作线程来执行任务,此时 workCount < maximumPoolSize,这一步也需要获取全局锁。④ 如果线程池已满,按照拒绝策略来处理任务,此时 workCount > maximumPoolSize。 线程池采取这种设计思路是为了在执行 execute 方法时尽可能地避免获取全局锁,在线程池完成预热之后,即当前运行的线程数大于等于corePoolSize 时,几乎所有的 execute 方法都是执行步骤 2,不需要获取全局锁。 线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。线程池中的线程执行任务分为两种情况:①在 execute 方法中创建一个线程时会让这个线程执行当前任务。②这个线程执行完任务之后,就会反复从工作队列中获取任务并执行。 可以使用 execute 和 submit 方法向线程池提交任务。execute 用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功了。submit 用于提交需要返回值的任务,线程池会返回一个 Future 类型的对象,通过该对象可以判断任务是否执行成功,并且可以通过该对象的 get 方法获取返回值,get 会阻塞当前线程直到任务完成,带超时参数的 get 方***在阻塞当前线程一段时间后立即返回,这时任务可能还没有完成。 创建线程池的参数 ① corePoolSize:线程池的基本大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池的基本大小时就不再创建。② workQueue:工作队列,用于保存等待执行任务的阻塞队列。③ maximumPoolSize:线程池最大数量,如果工作队列已满,并且创建的线程数小于最大线程数,则线程池还会创建新的线程执行任务,如果使用无界阻塞队列该参数无意义。④ threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。⑤ handler:拒绝策略,默认策略下使用 AbortPolicy 直接抛出异常,CallerRunsPolicy 表示重新尝试提交该任务,DiscardOldestPolicy 表示抛弃队列里最近的一个任务并执行当前任务,DiscardPolicy 表示直接抛弃当前任务不处理。⑥ keepAliveTime:线程活动的保持时间,线程池工作线程空闲后保持存活的时间,所以如果任务很多,且每个任务的执行时间较短,可以调大时间提高线程的利用率。⑦ unit:线程活动保持时间的单位。 关闭线程池 可以通过调用 shutdown 或 shutdownNow 方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法中断线程,所以无法响应中断的任务可能永远无法终止。区别是 shutdownNow 首先将线程池的状态设为 STOP,然后尝试停止所有正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设为 SHUTDOWN,然后中断所有没有正在执行任务的线程。通常调用 shutdown 来关闭线程池,如果任务不一定要执行完则可以调用 shutdownNow。 合理设置线程池 首先可以从以下角度分析:①任务的性质:CPU密集型任务、IO密集型任务和混合型任务。②任务的优先级:高、中和低。③任务的执行时间:长、中和短。④任务的依赖性:是否依赖其他系统资源,如数据库连接。 性质不同的任务可以用不同规模的线程池分开处理,CPU密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2 * Ncpu。混合型的任务,如果可以拆分,将其拆分为一个 CPU 密集型任务和一个 IO 密集型任务,只要这两个任务执行的时间相差不是太大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要分解。 优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 处理。 执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行。 依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越长 CPU 空闲的时间就越长,因此线程数应该尽可能地设置大一些提高CPU的利用率。 建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。 P19:阻塞队列 阻塞队列支持阻塞的插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。 Java中的阻塞队列 ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。 LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大长度为 Integer 的最大值。 PriorityBlockingQueue,支持优先级排序的无界阻塞队列,默认情况下元素按照顺序升序排序。可以自定义 compareTo 方法指定元素排序规则,或者初始化时指定 Comparator 对元素排序,不能保证同优先级元素的顺序。 DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存系统和定时任务调度。 SynchronousQueue,不存储元素的阻塞队列,每一个 put 操作必须等待一个 take 操作。默认使用非公平策略,也支持公平策略,适用于传递性场景,吞吐量高于 ArrayBlockingQueue 和 LinkedBlockingQueue。 LinkedTransferQueue,由链表组成的无界阻塞队列,相对于其他阻塞队列多了 tryTransfer 和 transfer 方法。transfe方法:如果当前有消费者正在等待接收元素,可以把生产者传入的元素立刻传输给消费者,如果没有消费者等待接收元素,会将元素放在队列的尾节点并等到该元素被消费者消费了才返回。tryTransfer 方法用来试探生产者传入的元素能否直接传给消费者,如果没有消费者等待接收元素则返回 false,和transfer 的区别是无论消费者是否消费都会立即返回。 LinkedBlockingDeque,由链表组成的双向阻塞队列,可以从队列的两端插入和移出元素,在多线程同时入队时减少了竞争。 实现原理 使用通知模式实现,当生产者往满的队列里添加元素时会阻塞生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。例如 JDK 中的 ArrayBlockingQueue 使用了 Condition 实现。当往队列里插入一个元素,如果队列不可用,那么阻塞生产者主要通过LockSupport 的 park 方法实现,park 在不同的操作系统中使用不同的方式实现,在 Linux 下使用的是系统方法 pthread_cond_wait 实现。 P20:Executor 框架 Java 的线程既是工作单元,也是执行机制。从 JDK 5开始把工作单元与执行机制分离开来,工作单元包括 Runnable 和 Callable,而执行机制由 Exectuor 框架提供。 在 HotSpot 的线程模型中,Java 线程被一对一映射为本地操作系统线程,Java 线程启动时会创建一个本地操作系统线程,当该 Java线程终止时,这个操作系统线程也会被回收,操作系统会调度所有线程并将它们分配给可用的CPU。 在上层,Java 多线程程序通常把应用分解为若干任务,然后使用用户级的调度器即 Executor 框架将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。 Executor 框架主要由三部分组成:①任务,包括被执行任务需要实现的接口,Runnable 或 Callable。②任务的执行,包括任务执行机制的核心接口 Executor 以及继承自 Executor 的 ExecutorService 。③异步计算的结果,包括接口 Future 和实现 Future 接口的 FutureTask 类。 ThreadPoolExecutor ThreadPoolExecutor 是 Executor框架最核心的类,是线程池的实现类,主要有三种。 ① FixedThreadPool,可重用固定线程数的线程池,corePoolSize 和 maximumPoolSize都被设置为创建时的指定参数 nThreads,当线程池中的线程数大于 corePoolSize 时,keepAliveTime 为多余的空闲线程等待新任务的最长时间,超过这个时间后的多余线程将被终止,将其设置为 0L 时多余空闲线程将被立即终止。该线程池使用的工作队列是无界阻塞队列 LinkedBlockingQueue,适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,适用于负载比较重的服务器。 ② SingleThreadExecutor,使用单个线程的线程池,corePoolSize 和 maximumPoolSize都被设置为 1,其他参数和 FiexedThreadPool相同。适用于需要保证顺序执行各个任务,并且在任意时间点不会有多个线程是活动的的应用场景。 ③ CachedThreadPool,一个根据需要创建线程的线程池,corePoolSize 被设置为0,maximumPoolSize 被设置为 Integer 最大值。该线程池使用的工作队列是没有容量的 SynchronousQueue,但由于 maximumPoolSize 设为 Integer最大值,如果主线程提交任务的速度高于线程处理的速度,线程池会不断创建新线程,极端情况下会创建过多线程而耗尽CPU和内存资源。适用于执行很多短期异步任务的小程序,或者负载较轻的服务器。 ScheduledThreadPoolExecutor 继承自 ThreadPoolExecutor,主要用来在给定的延迟之后运行任务,或者定期执行任务。其功能与 Timer 类似但更强大和灵活。Timer对应的是单个后台线程,而 ScheduledThreadPoolExecutor 可以在构造器中指定多个对应的后台线程数。 主要有两种:① ScheduledThreadPool:创建固定线程个数的线程池,适用于需要多个后台线程执行周期任务,同时需要限制后台线程数量的应用场景。② SingleThreadScheduledExecutor:只包含一个线程的线程池,适用于单个后台线程执行周期任务,同时需要保证顺序执行任务的应用场景。 实现原理是将待调度任务放入一个DelayQueue 中,调度任务主要有三个 long 类型的参数,time 表示这个任务将要被执行的具体时间,sequenceNumber 表示这个任务被添加到线程池的序号,period 表示任务执行时间间隔。DelayQueue封装了一个PriorityQueue,队列按照 time 进行排序,如果相同则比较sequenceNumber,越小的排在前面,即如果两个任务的执行时间相同,先提交的任务先被执行。
分享
8
先马后看
一口一个橘猫
广东财经大学·2022届

毕业想要成为产品经理,那这篇内容你真的不能错过!

人人都在说产品经理,校招求职时产品经理也是赤手可热的岗位。那么你了解到底什么是产品经理?产品经理的工作是做什么的?大家比较熟悉的产品经理就是腾讯的张小龙,江湖人称“微信之父”。如果没有熟悉这个行业的话,也只是懵懵懂懂。 图片: https://uploader.shimo.im/f/XLgSn2xcDT78E91t.png 我们公司的产品小哥给大家归类整理了一下产品经理的必备能力和产品经理的工作流程,方便大家对产品经理有一定的了解。 产品工作全流程:产品梳理、产品设计、产品优化、产品落地、产品运营 01 产品经理的必备能力 基本技能 必备:原型输出、文档编写、思维导图、流程图 前端:html、java,后端:HTTP,以及编程语言 沟通能力 对于产品人而言,沟通能力也是尤为重要,所谓沟通能力不是说这个人有多么的能说会道,多么的口才伶俐,而是你的想法表述出来,能让开发、设计明白,且他们提出的疑点和建议你能明白并能深刻理解,这样沟通才是最有效的。 像一些小型创业公司,团队人数较少,每个人负责的板块大家都很熟悉,需要和研发、前端、设计沟通的话都比较方便。那如果是像腾讯、阿里、百度这样的大公司,每个部门员工很多,大家所负责的工作都不同,产品和技术这边对接的话就没有这么方便了。如果关于产品的功能和展现你不能很明确的讲清你的想法,就会浪费双方工作时间,这完全是没必要的。 逻辑思维 有个强大的逻辑思维能力可以作用在前期调研、产品分析、绘制原型、逻辑梳理、撰写文档、数据分析、推广运营.....可以这么说逻辑思维能力贯穿了工作的每个环节。 拥有逻辑思维能力首先你得思考,做行业分析你得思考,做竞品分析你得思考,画原型的时候你得思考,和研发沟通的时候你也得思考。产品经理要发乐于思考,善于总结,能够对产品做出正确的逻辑判断和价值判断。 认知和好奇心 对于认知来说,我们一致认为好奇心是基础。一个产品经理如果对身边的事情、对这个世界没有好奇,那很难会有丰富的视野,也很难愿意去经历各种各样的生活。 对人和事有较准确的认知很大程度上都源于有足够多的经验,这些经验既可以来源于阅读和其他人的信息,也可以来源于自己的实践经历。 最终,产品经理需要做到的就是有效的洞察。能比一般人看到的东西更多、更准确,对各种问题和各种事物的判断更深入、更细致,这就是产品经理在认知方面最高的要求。 张小龙在他那著名的腾讯内部八小时演讲中提到: 产品经理应该有博而不专的积累。美术、音乐、阅读、摄影、旅游等文艺行为貌似不能直接转化为生产力,但是合格的产品经理需要广博的知识储备,以此才能了解和认识大数量的人群,理解时代的审美,让自己的所思所感符合普通用户的思维范式。以此为基础,设计的产品才不会脱离人群。 02 产品经理的工作流程 产品设计阶段:产品设计阶段是指产品从搜集需求到立项通过中间的时间段 1. 获取需求 需求来源:市场调研、问卷调查、竞品分析、项目负责人的意见 每一个成熟的产品,都是经过不断迭代生长,不断打磨,最后才能被大众认可。在这个过程中,需求是第一出发点,首先得有人需要这个产品,产品才有产生的理由。 这个需求不光是产品的创始人的需求,更是市场大众的需求。如果产品展现出来只能够贴合一小部分人群,那就只能算是一个小众的产品。如果需要更强大更包容的产品,需要收集市场大众的需求。 这个需求来源可以是市场问卷调查反馈得出,也可以是产品经理观察判断得出。对需求进行分析也是产品改进的方向。 Pony马化腾曾提出一个广为流传的“10/100/1000法则”,即产品经理每个月必须做10个用户调查,关注100个用户博客,收集1000个用户体验反馈。 2. 需求思路确认 需求确认通常是指产品迭代版本时,领导对需求确认和对产品经理的一个逻辑审查过程,产品经理需要将产品的流程图,思维导图整理出来,进行完善。 的一个过程,这一过程主要审查产品经理计划要做事情是否合理,方向是否正确,以确保此产品的正确性。 3. 方案评审 产品经理需要输出原型图,完成框架,完成功能所需要满足全部功能点,一一列举出来,交给项目负责人评审,负责人对原型图进行评审并给出建议,发现问题,解决问题。 4. 立项 方案落地,并与前端及技术部讨论方案的可行性,技术的难点,解答技术部的问题。 项目跟进阶段:项目跟进阶段指的是完成原型到设计以及开发的时间段 1. 设计稿确认 产品经理将于设计师对接,确定设计师的设计稿,确认点分为三种:设计稿与方案的一致性,设计稿的视觉效果、设计稿是否有漏掉的部分,确认后方可开发。 2. 前端静态页面验收 产品经理在此节点主要关注2个方面: 1.前端页面是否与设计稿一致。 2.是否存在兼容性bug及交互体验问题。此节点验收通过后,将交付给后台开发人员了。 3. 开发跟进 产品经理在此节点主要关注是进度有没有偏差,当出现不可抗拒的原因导致延期什么的,此时产品经理将制定应对措施,通知内外部相关人员做好安排 4. 项目验收 对已完成的功能的测试,产品经理需要把所有的功能全部测试一遍 开发初期,尤其是创业公司,请做用户验证。这是我们进行试错和避免方向偏差的最简单办法,以免我们误入歧途造成较大的沉没成本。 这里的验证指的是通过数据来分析用户在产品内的使用行为。数据是最客观的,比如说你上线了一个新功能,也许在用户调研的时候,很多人说很喜欢,但是如果数据表现非常差,则证明大部分用户真的不喜欢你新做的这个功能。那就要反思一下哪里出现了问题,是需求把握的不好,还是实现方案有问题,甚至考虑一下新功能是否应该继续存在。 验证一下用户的需求和想法也是验收产品的成果。如果能被用户骂,说明还是一件好事情,用户还是希望你变好的。如果用户压根都不想反馈,直接弃用了,这才是一个失败透顶的产品。 项目发布阶段:项目发布阶段指的是产品开发完成后到正式发布的时间段 1. 内部培训 产品经理将对内部相关人员进行培训,如客服、市场、运营等相关人员进行培训,使他们认识产品,了解产品,会熟练使用产品。 2. 项目上线 项目上线的过程主要由技术来实现,产品经理则需要把握上线的时间 3. 发布公告 项目上线后产品经理发出公告,通知内外部的人员,让他们知道新产品,新功能上线 会原型、写文档,流程图,这是产品的必备技能。同时更需要学习开发技术,不求会,但求看得懂,能理解。对于产品,就是理解产品存在的本质,用户代表的群体,群体中的人性,以及把握住面向未来的机会。
分享
评论
先马后看
毛毛毛毛毛大侠
美团

面试后没消息?这样做,入职率增加80%!

面试,大家都经历过N次,但是在HR迟迟没有回复时,这时候,你应该如何做呢? 1:面试良好却迟迟没有回音?   Rachel Pu:“与其在被动等待中焦灼,不如一次性给自己一个“痛快”,说不定还有转机。” 对于大部分程序猿来说,人生最大的痛点就是面试,而我就是他们中的一个。前几天想跳个槽,追求个“溢价”。 面试的时候跟HR谈的还不错,腹稿做的也充分,直接与直属上级见了面,彼此印象也很好,让我回家等消息。本来欢欢喜喜的觉得下周一肯定就能入职了,谁知道一周过去了还没个信。 媳妇出了个招,让我问问HR咋回事,我反反复复犹豫了一两个小时,最后给HR发了微信(面试加的):我是XXX,上周一应聘了程序猿的岗位,对贵公司十分感兴趣,我相信6年的工作经验我能很好的为公司效力。 后来HR回复我才知道,上级领导个人原因离职了,招聘计划有变,入职需要半个月之后,如果能等待,可以过后直接入职。幸亏问了,不然我还在等待中焦灼……求职者前期一定要留存HR的联系方式,且面试结束后首先要主动与HR确认面试结果的最终通知时间。如果没有在约定时间接到通知,大家可在约定期限的最后一天与HR联系,获取准确的面试结果。 同时,短信及微信问询为较私人的沟通途径。求职者若使用短信或微信询问面试结果一定要注意措辞,比如:“您好,X经理/女士/先生,我是X月X日参加贵司面试的XX,应聘的XX岗位,我十分认可贵司的企业文化及管理模式,相信前期积累的工作经验使我能够更快的融入到新的工作岗位中。 无论面试结果通过与否,感谢您提供的这次面试的机会,盼复您的信息。”如此,在询问面试结果的同时既加深的HR对你的印象,又表达了强烈的求职意愿。 2:面试感觉不佳,还有补救机会吗?  @茉莉花茶:“主动与HR联络,弥补不足,你就胜过了80%的面试者。” 作为一名“靠嘴吃饭”的房产销售,说错话可是大忌,而我就犯了这个大忌。 HR对我的印象一般,但共同面试的直属领导对于我的销售能力还是十分认可的,所以在我面试后等待3天无果时,我便对自己的面试进行了复盘。 想到了HR对我不满意的点,我随之给她发了短信: 您好HR,我是XX上周五面试了置业顾问的岗位,感谢您给我的机会。在面试中我觉得有一个问题我答的不是特别好,重新思考之后我认为……没想到的是,随后就得到了HR的回信,我又去了一趟公司聊了聊,入职搞定! 虽然求职者都会在面试前做足功课,但对于HR出其不意的“课外题”大部分求职者总会回答的不尽如人意甚至“说错话”。发生这种情况并不代表没有转机,面试感谢函就是再一次“推销”自己和加深或扭转HR印象的机会,能够为求职成功又增加一记砝码。发送面试感谢函的最好时间是面试结束后的24小时内。 感谢函内容要表达出对应聘职位的兴趣,重点突出你的个人特色与优势,针对面试表现不佳的方面进行补充说明,并在最后表达感谢,以提醒面试官注意回顾你的面试,给出面试结果。 3:HR让回家等待复试,怎么就没消息了   @七月上:“没通知并不是你被PASS了,还有可能是HR……忘记了……” 生完孩子在家呆了一年,因为一直有做兼职,所以自己的“笔杆子”还是很硬的。面试了几家公司,HR们对我的印象都还不错。其中有一个我最为看好的公司,告诉我下周一复试,到时候与领导定准时间会通知我。可都等到了周五,已经收到其他公司的入职offer,但最心仪的应聘单位的HR还是没消息,我合计着:难道是被pass了? 我是选择已经发入职offer的公司,还是给心仪的公司打电话再争取、确认一下? 给HR打过电话才知道,原来领导一直出差,今天刚回来,她忙着汇报其它工作,给面试的事儿给忘了。我想过无数的理由,却没想过遇见个“糊涂”的HR…… 后来HR与领导确认好面试时间,我也顺利的通过了面试,现在已经入职了!有的时候沉稳并不是好事,主动才能为自己争取最大利益。拨打电话确认面试结果是最直接的方式,但在电话沟通过程中要注意技巧和方法。尽量不占用面试者私人时间、选择在办公时间内拨打电话。 在沟通中适当的表明已收到其他企业的入职offer,但目前未回复的单位为首选企业,能够体现出入职的强烈意愿。比如:“不好意思,打扰您,我是XX,X月X日曾来贵司应聘过XX岗位。我对贵司十分向往,现在收到了另一家单位的录用通知,但贵司仍是我心里的首选,所以冒昧的问一下面试结果,希望能够有加入贵司的机会。” 写到这里,我认为: 等待面试结果的过程是忐忑的、煎熬的。许多应聘者在HR迟迟没有回复时就默认自己被pass了,或因担心主动询问面试结果打扰到HR工作会拉低印象分,其实不然。 善用有效的方法去沟通,不仅能够加深HR对你印象,还能更快的获取到结果。机会只会留给有准备的人,而“主动”是能够为自己争取最大利益的职场利器。 古有“毛遂自荐”,后有20世纪英国著名的喜剧演员查理·卓别林。 要知道,所有的机会都是掌握在自己手中,向前一步,一切皆有可能!
分享
评论
先马后看
ZhongYi Hui
首都师范大学·2022届

连这些都还没搞清楚,你就想进银行???

想要求职银行,真的得多了解一些。 外资银行、国有银行、股份制银行、民营银行,你真的能分清吗? 薪资待遇差别大吗? 让我们抛开成见,一起来了解一下! 01 商业银行的分类 商业银行如果按照股权性质分类,可以分为外资银行、国有银行、股份制银行以及地方性的民营银行。 外资银行 在国内主要有汇丰银行、渣打银行、花旗银行、星展银行... ‍特点是其总部和高级管理层大部分来自国外,采用一些国外的管理方式和经营理念来做中国市场。 同时,外资银行希望能够利用他们关于海外的商业知识,不管是经济还是政策方面的信息,来帮助国内客户,以获得更好的资本渠道,不论是投资、借贷还是其他的金融服务。 国有银行 就是大家所熟知的“四大”或“五大”,“四大”就是中农工建,交通银行有一些外资股份,汇丰银行是其目前最大的股东之一。 国有银行由于背靠政府,它们拥有最多最全面的牌照,覆盖最全面的网络,拥有最多的网点,可以提供最便捷的服务。但是其缺点是国有银行的服务质量可能并不会那么高或是整体结构比较臃肿和滞后。比如电子商务的兴起,国有银行在这方面的跟进速度相较于民营银行或股份制银行就有一些差距。 股份制银行 比如民生银行、中信银行、招商银行等; 其优点是既有国有资本,又兼有民营或地方性资本,这使得它们对本土的市场更加了解,对市场的反应速度更快,提供的服务通常更好一些。 因为它们的决策通常会在本地进行,而不是像外资银行或者国有银行需要上报到总部去决策。这种决策的分散性有助于股份制银行针对本地市场制定出更具体更本地化的服务。 民营银行 是从2014年才获批筹建的,例如:微众银行、网商银行、新网银行、华瑞银行、三湘银行等。 民营银行的资本金主要来自民间,其对利润最大化有着更为强烈的追求,如果没有健全的监管机制进行有效监管,民营银行往往会因风险问题而陷入失败。民企办银行一个很重要的动机就是希望为企业搭建一个资金平台,为企业融资提供便利,所以一旦关联企业出现问题,贷款无法偿还,民营银行会面临巨大风险。 虽然才获批筹集的时间不长,但是也不失为一种求职的好机会哦! 02 银行薪酬PK:民营银行不容小觑 ‍外资银行 外资银行分为前台(业务部门)、中台(风控合规内审)和后台(技术、运营等)不同职位,其待遇可以算是中上水平,应届生年薪大概是10w左右。 据外资银行内部员工反映,外资银行的基本工资一般每年审核一次,时间在3月或4月,每年平均涨幅约为10%-30%,视各员工绩效考评综合确定。当然,每一年也会有针对特别情况进行调薪的,如参考市场工资水平上升压力而进行特别审核和调整。 此外,外资银行还有加班补贴、综合津贴以及其他福利,如各类保险保障计划、五险一金、商业医疗保险和特别险种等。浮动薪酬,这部分很大程度上由银行当年业绩以及你的上级对你的评价共同决定。 总之,在外资银行工作优势还是比较多的,有着高大上的企业文化,优越的工作环境,同事来自全球,素质较高,工作强度没有非常大,假期也较多。 不过如果你的英语水平不是特别好,则可以向国有五大行看齐。在世界银行Top1000的排行榜中,中国国有五大行可是常年牢牢占据Top1! 国有银行(以中国银行为例) 1. 行内正式员工数量超28万,薪资体系分13级,分级定薪。 2. 薪资构成: 1)工资及奖金 本科约4000元/月(三级学历,依次递增,幅度1000元左右)+绩效奖金 2)法定福利+额外福利(六险一金) 3)现金福利 租房补贴(约1000余/月)+交通补贴(约700余/月)+餐补(约400余/月)+防暑降温补助+过节费(几千不等)+通讯补助(约200左右) 民营银行 都说工行、建行待遇好,民营银行也不甘示弱啊! 相较于传统商业银行,民营银行的用人机制会更灵活,在薪资待遇也会更好一些。在招聘上,民营银行会选择与自身的战略重心契合度更高的候选人,如果是有意补足信息技术类人才,往往希望能招聘到成熟型员工,能够在入职后立即解决问题。用通俗的话说,就是在资源有限的情况下,把钱花在刀刃上。 在民营银行眼中,一位负责大数据风控的人才可以年薪超百万。 猎聘网上某民营银行风险中心寻找大数据风控总经理,工作地点在上海,开出的年薪是120万-180万,强调希望人选有中资银行背景和互联网公司双重经验;还有的民营银行在北京以33万-46万年薪招聘反欺诈系统研发岗,在上海以24万-48万年薪招聘高级/资深JAVA工程师,在深圳以36万-72万年薪招聘风险建模人员,以36万-72万年薪招聘金融产品管理资深经理。 相较于传统商业银行,民营银行的用人机制会更灵活,在薪资待遇也会更好一些。在招聘上,民营银行会选择与自身的战略重心契合度更高的候选人,如果是有意补足信息技术类人才,往往希望能招聘到成熟型员工,能够在入职后立即解决问题。用通俗的话说,就是在资源有限的情况下,把钱花在刀刃上。 以上就是银行的基本科普了,今年银行秋招放出的岗位数量质量都十分客观,有意向的小伙伴不要错过~
分享
评论
先马后看
青铜
南京师范大学·2022届

在奢侈品行业工作,真的和想象中一样光鲜亮丽吗?

在没有接触过奢侈品行业前,我们对它的全部想象都来自于电影。 光鲜亮丽的衣着妆容、精致高楼里有着落地窗的办公室、掌握着全世界人民这一年疯狂追逐的新款会长什么样子…… 除了表面看到的这些色彩、欲望堆积的意象之外,一间奢侈品公司内部是如何运作的?他们的工作日常是怎样?像刻板想象中那样充斥着八卦和秘密满天飞吗? 今天来聊聊奢侈品行业。 01 【“在奢侈品行业工作,是不是都很会打扮?”】 首先,一提到奢侈品行业工作,很多人只会想到门店工作。这是误解。 奢侈品算是时尚行业里的排头兵和标杆了,所以基本同事的颜值和打扮能力都是比较强的,超出一般白领的水平。 当然也应该在这个行业,所以有很多内购的机会,以低于市场价很多很多的价格买入奢侈品,比如2折内部特卖买L字头的法国品牌的针织衫,质感还是很不错的; 不要以为在时尚行业工作就是天天穿着高定。 工作就是工作,你试试让你连续在灯光下检查一个小时的几百万的钻石项链是否掉钻了,D-E级的折射光只是让你想哭一下,这样有点眼泪出来湿润一下眼睛继续盯着检查。 02 【在奢侈品行业工作久了,是不是很容易迷失自己?】 这一行天花板是很高的,所以你总会接触到比你更好的人,相应的就会非常希望自己也变成那样的人,更有向上的动力。 同时也特别需要守住心,因为人想拥有美好的事物是本性,心态就非常重要了。不嫉妒,不激进,一步一步扎实的学东西。其实蛮难的,因为每天接触的是顶尖的东西,眼睛总会比手高。 很多小姑娘非常向往在时尚奢侈品类的公司工作,希望自己满身名牌审美卓越受人追捧。但这在各行各业都是需要一个漫长的积累过程的。 看的高想要的就高,不能严格要求自己,扎实的学东西,再去获取自己想要的一切,就很容易心态失衡。 03 【这些公司里面都是什么样子?】 一般奢侈品公司都会工作细分,每个部门负责专业的区域。 Merchandising部门:买手部,负责店铺货品的采购。 不同的区域会有不同的买手,而且同一个城市,不同的店进的货品数量及类型也会有差别,内地一般分成南区(上海,南京等地)和北区(北京,大连等地)。买手决定这个区域的顾客能买到哪些东西。 很多奢侈品品牌内,Merchandisers到了一定级别可以代表店铺/地区或者带客人去参加时装秀。 Visual Merchandising部门:视觉陈列部,负责店铺货品的陈列。 作息不定,需要经常出差,而且一般来说春夏和秋冬交替时,经常需要熬夜作业,因为不能在店铺正常营业时间进行橱窗布置。 Retail部门:零售部。 奢侈品门店一般会有两种形式:直营店和其他渠道经销商,二三线城市网上渠道或百货商店这种多品牌渠道情况会多些。很多大牌以直营为主,尤其是LV,是没有任何的分销商,淘宝或者机场等多品牌同时出现的地方,只有直营的精品店。大品牌的门店店长也经常要和买手部一起去时装周采购。 品牌商会对多渠道经销商进行一定干预,比如店铺的设计,需要每年订货量达到多少及每年拿出定额的费用进行广告宣传之类的。奢饰品的retail和快时尚还是有很大的区别的。像有些奢侈品的高级珠宝品牌的店长,每年可以带客户去参加全球限额的高定珠宝展。 Client Relationship Management部门:客户关系部,维护为主。 奢侈品大牌的CRM,通常会通过分析店铺或者客户行为的大数据而制定品牌战略。在不定期的时候,会为不同的客户群体提供具有针对性的品牌活动。如快闪门店,展览,沙龙,节日礼遇之类。 Marketing &Public Relations:市场公关部门。 Marketing和PR都是有机会在各种高大上的活动中接触到明星和KOL的岗位。想要做好这份工作,既需要大量的创意,还需要一定的sense和观察力,来发掘客户最真实的需求,把自己代入到他们的角色里去理解客户的定位。
分享
2
先马后看
甜甜酱
西安电子科技大学·2022届

游戏策划职责和日常

1.系统策划 ⑴什么是系统? 这里有必要先介绍一下系统的概念和定义,后面的章节也会讲到,作一个铺垫,大家理解一下。 关于系统本身,有很多定义,关于游戏系统,大抵逃不过两个关键词:元素+规则 游戏中有很多名词,诸如:装备、等级、时装、宠物、战斗、技能、地图、锻造、强化、材料、商城、交易所、好友、公会商店、公会祝福、属性…… 你如果进入一个游戏,点开所有的界面,把看到的名词都记录下来,你会发现有些名词,他们天生好像有所关联,比如:装备-强化-材料-属性,战斗-技能-等级 仔细品一品,为什么他们好像有关联? 装备可以用对应材料进行强化,强化失败不返还材料,有几率导致装备掉段,强化成功使装备属性提升XXX…… 等级每提升X级,获得技能点可以用于提升技能,技能等级越高在战斗中造成的伤害越高…… 所以你会发现,这些名词往往可以通过一系列的逻辑语句串联起来,而这些逻辑语句,我把它称为“规则”,这些名词便是“元素”。类比化学中的元素周期表一样,元素是组成化学物质的最小单位,而如何组成便是规则,规则不同,最终的化学产物也不同。举个例子,氧元素+规则:2个氧组合=氧气,3个氧组合=臭氧。 所以在游戏设计中,游戏元素+对应规则=最终功能。功能与功能之间的区别就在于其规则和元素不同。 所以你可以得到关于游戏系统的一个定义:游戏系统是游戏中一类游戏元素组成的相互作用的功能整体,一类元素则是在某些规则下涉及的元素组合。 所以,你现在告诉我装备系统是指什么?应该能够回答的有模有样吧? ⑵系统策划日常 系统策划的工作其实就几乎等同于设计整个游戏(高级系统/资深系统),只不过其中的一些细节会交给专门的策划去做,根据项目进度,每天的日常也会有不同,例如: 项目开发前期,每天和主策/制作人讨论某系统的一些做法细节和游戏循环。比如:为了让玩家每天做XXX,所以有XXX的系统去帮助他进行游戏循环,然后成长线怎样铺设,怎么给玩家目标,某系统好像驱动力不是很足是什么原因,都是常见的讨论内容 确定了框架后,出设计文档,和主策讨论文档细节,确定后交给程序制作 程序在做的期间可能有些问题,你此时要负责和他们沟通,确保做出来的东西是你想要的 系统做出来后,自己要去游戏中进行验收,必要时联系测试人员协助验收,有不对的地方要及时开单给程序做(开单:策划的需求用项目管理软件编写好需求后派发给指定程序员,方便管理和验收) 几个系统都出来后,你要去游戏中跑一跑,看看几个系统是不是达到自己的设计目的了,有没有要改动的地方,及时出文档给程序改动(传说中的改需求) 游戏上线后,系统策划还可能去做一些活动和玩法,毕竟这时候可能没有新的系统需要设计了,就会去做一些活动和玩法设计…… 部分公司没有执行策划,有时候还会去填一些具体的内容 …… 总之,系统策划的工作就是和主策确定了设计方向和目的后,写设计文档,沟通程序推进制作和验收,上线后根据反馈对系统进行微调,设计活动和玩法……(当然大厂一般有专人负责活动和玩法) 系统策划往往是大部分校招同学想面的岗位,毕竟相当于设计游戏整体框架和样貌。 2.数值策划 ⑴什么是数值? 这里简单说一下,后面数值章节会专门细讲,其实一句话,游戏中你能看到的所有“数字”都是数值策划有意为之,怎么理解? 比如: 装备A为什么+20点攻击力,B为什么+12,强化一级为什么+300攻击? 商店里的技能书为什么卖120钻石? 为什么扫荡地图,10次一般会出2个材料? 为什么完成新手引导恰好能升到5级? 为什么艾希的大招距离是全图,宽度是XX? …… 数值是什么,这里给一句话,后面会讲:数值是游戏中各元素的量化指标和用来衡量元素间的关系 数值策划包含3大模块:成长,经济,平衡 成长是指:玩家每天付出了多少时间/金钱,能得到怎样的回报(往往是战力提升) 经济是指:游戏中物品怎样产出,怎样交易,多少价值,货币怎样流通怎么消耗的 平衡是指:职业平衡,英雄平衡…… 数值是需要和系统密切配合的,往往在项目中,系统策划和数值策划会每天进行沟通 只有系统是不够的,需要加入内涵来具体定量,举个例子大家一下就明白了: 系统策划:今天我想吃肉 程序员:吃什么肉?猪肉,牛肉做法不一样!!!别最后改需求! 数值策划:吃多少?是吃固定价格,还是吃固定重量? 大家是不是懂了,系统其实很空,某个系统想要达到目的,让玩家感受XXX,是需要数值去配合的,强化系统想给玩家很爽的体验,强化一级+50点攻击和50000点攻击体验是完全不同的。 所以系统往往会和数值沟通某系统想要给玩家的感受/体验,然后数值会设计其中的公式/投放,再和系统沟通“你看,这个样子符合你想要的体验吗?” ⑵.数值策划日常 一般来说数值策划有一个总Excel表格,里面包含了所有系统的数值设计,在RPG游戏中,一般是每个系统的战力投放和成长曲线等等,3大模块的内容全在一个Excel里面。 这个总表,你能看到每个系统对玩家的成长占比,玩家随着体验到的游戏内容(等级)不断增加,每个系统的战力怎样提升的,每天大概能提高多少,付费100元能提高多少……这些问题基本在数值总表中能够找到对应的答案。 日常工作内容可能是: 主策/系统策划:昨天那个副本我感觉太强了,你重新算一下给我…… 主策/系统策划:这个宠物系统我希望对战力提升只有20%,你设计一下数值和投放 “这个怪物怎么这么厉害,调一下” 这张地图通关时长有点长了,降低一下,你重新算一下地图基准和怪物…… …… 所有涉及具体体验的部分,基本需要数值介入调整。 3.文案策划 ⑴什么是文案? 游戏文案实际上非常好理解,玩家在游戏中能够看到一切文字部分,都是由文案策划有意为之 装备A叫做“炫酷吊炸天炮”,相信我,这是文案策划写出来的! 实际上游戏的文案不仅仅是这些细节部分,更多的其实是架构整个游戏的世界观,剧情,角色塑造,是非常有技术含量的工种,一个拥有好文案策划的游戏,抛开表现不谈,你是能够对游戏的剧情,角色,有一个清晰的认知的,这是提高游戏代入感和粘度非常重要的一环。 更高级一点,游戏如果运营时间长,往往会打造IP,例如魔兽世界,魔兽世界观下衍生了多少作品?英雄联盟从S6赛季开始就在疯狂完善IP架构和优化世界观,在完善游戏世界观的同时为整个英雄联盟ip的手游链条做铺垫,也是离不开早期文案策划的努力。 角色的对话,任务的对白,每一句都是对游戏世界观,剧情,角色性格的塑造。 ⑵文案策划日常 项目早期,往往就是游戏世界观和剧情的整个架构,会出一套文字和制作人主策讨论并优化。角色这一块,也会单独进行塑造。 整个游戏的世界观剧情,角色会对美术、系统产生一定需求,所以也会频繁和相应的负责人聊。 项目中后期,会具体进行任务、剧情、对白等编写和填表等等。 上线后,主要是进行后续的内容塑造,联合运营,一起做好每个活动,节日内容等等。 这里需要提一点,优秀的文案策划是会对游戏系统,关卡提出一定要求的,比如我新手剧情设计好了,就会对关卡有一定需求,比如关卡里需要有一个NPC跟着主角…… 4.关卡策划 ⑴什么是关卡? 这个也非常好理解,大部分地图向的游戏,例如CF,地下城,MMO副本…… 关卡本质上是游戏提供给玩家一定区域的活动舞台 正因为如此,关卡其实是玩家最小循环的单元,密切关乎着游戏体验,所以关卡设计非常非常重要,好的关卡策划也越来越值钱。 关卡设计往往有几大重要的元素: 关卡目标:这个副本目标是什么?打BOSS?拿到物品?拯救人质?这类目标往往涉及包装层,但国内的端游手游一般在包装上都会让你打个BOSS 关卡剧情:这部分关系到世界观和剧情,好的关卡背景和剧情能够提高代入感,以及提高通关后的成就感(玩家会觉得自己影响了游戏世界中的某一环) 关卡地图:地图是关卡设计的核心,关卡设计逃不开地图,地图主要需要去考虑玩家的移动路线,每个路线怎样投放怪物,剧情放在哪个通道等等,整体关卡地图能够给玩家心理带来怎样的体验?好的关卡策划会在地图这块去追求“心流体验”(后面章节会讲),起伏式情绪…… 关卡NPC:部分关卡会有一些NPC负责进行交互和对白,也是提高代入感的一个重要方式 关卡物品:主要是掉落这部分的内容 关卡规则:怎样胜利?三星通关是什么?人质死了算失败吗? ⑵关卡策划日常 设计关卡,设计关卡,设计关卡…… 关卡策划是属于策划中分工比较细的一类了,所以工作内容比较单一,但是想要做好很难,日常就是做关卡,大部分时间在做地图,偶尔会和领导聊聊某活动的目标和想要的效果,结合着去做关卡。 5.策划分工小结 随着游戏行业成熟,策划的分工是越来越细,越细的岗位,对策划水平的要求越高,不然没必要分这么细。 系统策划可以细分到具体板块,比如社交,成长。 数值策划可以细分到三大模块,不过由于数值策划需要统一的游戏架构和认知,一般不会这样去分,往往一个数值策划负责就够了。 文案策划根据游戏的品牌品质要求,世界观,剧情,角色,都是细分的方向。 还有一些,战斗策划,执行策划…… 当然对于分工这一节的讲解,对于各位校招同学来说,最重要的核心在于:理解想要面试岗位的职责和工作内容 这是面试官考量你态度和职业规划能力非常重要的一环 当初我在面试时,面试官就问我“你了解系统策划吗?你知道你上班后会做什么吗?” 所以,大家试着扪心自问吧,看看能否表达的清晰。
分享
7
先马后看
海盐不喝汽水
南昌大学·2022届

关于Spring Cloud

Spring Cloud Spring Cloud 是什么 集成了多个框架工具,来解决微服务终于到的各种问题: 远程调用(RPC) 负载均衡 重试(容错) 降级 熔断 监控 配置中心 Spring Cloud 是一个框架集,微服务全家桶 Spring Cloud 不是什么 不是一个独立框架 Spring Cloud 和 Dubbo 区别 Dubbo 只解决远程调用,不能解决其他问题 如果遇到其他问题,需要自己研发,或自己集成其他工具。 Spring Cloud 是微服务全家桶,结合Spring Boot来实现“开箱即用” eureka Spring Cloud 支持多款注册中心工具:eureka,zookeeper,consul....... eureka运行机制: 注册 应用启动时,会连接注册中心进行注册。 如果无法连接注册中心,它会一次一次的反复进行注册,直到成功为止。 拉取 每个服务都会从注册中心拉取注册表,每30秒会重新拉取一次注册表进行更新。 心跳 客户端会每30秒向 eureka 注册中心发送一次心跳数据,eureka 连续3次收不到心跳,认为服务已经死掉,会从注册表删除服务。 自我保护模式 是一种特殊情况 由于网络不稳定,造成 85%以上的服务器出现心跳异常,这时会进入自我保护模式,在保护模式下,所有服务都不删除,直到网络恢复,会自动退出自我保护模式。 eureka 和 zookeeper 区别 eureka 强调 AP - 可用性、分区容错性 集群 - 对等结构 zookeeper 强调 CP - 一致性、分区容错性 集群 - 主从结构 RestTemplate Spring Cloud 使用的一个远程调用工具 (服务之间相互调用) RestTemplate 是Springboot提供的一个远程调用工具 getForObject("url", 转换类型, 提交的参数) postForObject("url", 协议体数据, 转换类型) Ribbon Ribbon 的作用,解决的问题: 负载均衡(微服务系统必须功能) 访问压力可以分散到多态服务器 重试(不是必须功能) 当第一次请求失败或等待超时,可以自动发起重试请求 Ribbon 负载均衡 ribbon依赖(在 eureka client 中已经包含) 添加 @LoadBalanced注解,用Ribbon封装 RestTemplate 对象,对 RestTemplate功能进行增强 Ribbon 重试 容错 当调用后台服务失败,可以自动重试,如果重试成功,可以向客户端正常返回结果。 添加Ribbon重试: 添加 pring-retry 依赖 设置重试参数: MaxAutoRetries - 单台服务器的重试次数 MaxAutoRetriesNextServer - 更换服务器的次数 OkToRetryOnAllOperations - 是否对所有类型请求都重试,默认只对 get 重试,如果要对所有请求重试设置成 true 下面两个超时设置不能在 yml 中配置,需要在 java 代码中设置 connectTimeout - 建立连接超时时间 readTimeout - 已建立连接并发送了请求,等待响应的超时时间 Hyxtrix 容错工具 Hystrix提供的容错功能: 降级 熔断 降级 当调用后台服务失败,或超时,可以向客户端返回降级结果 。 快速失败: 客户端不必长时间阻塞等待后台服务结果,超时后可以快速获得反馈。 防止雪崩、防止错误传播 熔断 熔断就像家里的电箱,有一个总闸,如果访问量过大(过热),触发熔断,家里的电路会被断开。 当系统访问量过大,出现大量的失败情况时,会触发熔断,断路器打开后,所有的请求直接执行降级代码返回降级结果。 触发熔断的条件: 10秒内20次请求(必须首先满足) 50%失败,执行了降级代码 添加 Hystrix 降级 hystrix 依赖 添加主程序注解@EnalbleCircuitBreaker,启用 hystrix 断路器,触发断路器自动配置 添加降级代码 复制代码1234@HystrixCommnad(fallbackMethod="降级方法")public void a() {    restTemplate.getForObject("http://.....");} hystrix 的超时 默认超时时长是1秒 hystrix dashboard hystrix仪表盘,对hystrix出现错误的情况进行监控 actuator Springboot 提供的一个项目监控工具,可以监控项目的各种运行数据,hystrx利用actuator,可以添加hystrix的监控数据。 添加 actuator: actuator依赖 暴露监控数据: 在.yml配置文件中输入即可知晓 复制代码1234567m.e.w.e.i="*"  # 暴露所有的监控数据 m.e.w.e.i=health  # 暴露健康状态数据 m.e.w.e.i=health,beans,env,hystrix.stream # 暴露多种监控数据 m.e.w.e.i=hystrix.stream 搭建仪表盘项目 hystrix仪表盘是一个完全独立的项目,启动后,需要在它的界面上指定监控数据的路径。 新建 hystrix-dashboard 项目 添加 hystrix dashboard 依赖 yml 配置选择端口 4001 主程序添加注解,启用 hystrix dashboard:@EnableHystrixDashboard Feign 声明式客户端 只需要定义一个抽象的接口,就可以通过接口调用远程服务,不需要写具体调用代码。 例如调用后台商品服务,接口可以这样定义: 复制代码12345@FeignClient(name="item-service")public interface ItemFeignClient {    @GetMapping("/{orderId}")    JsonResult<List<Item>> getItems(@PathVarible String orderId);} 通过注解,配置以下三点: 服务id - 确定调用哪个远程服务 路径 - 调用一个服务的哪个路径 参数 - 向这个路径提交什么参数数据 集成 Ribbon Feign集成了Ribbon,提供了Ribbon的默认配置。 默认已经启用了负载均衡和重试,0配置就可以使用Ribbon。 重试的默认配置: MaxAutoRetries: 0 MaxAutoRetriesNextServer: 1 ReadTimeout: 1000 集成 Hystrix Feign可以集成Hystrix,默认不启用Hystrix,Feign不推荐启用Hystrix(后面再进行分析)。 启用Hystrix,添加基础配置: 添加 Hystrix 完整依赖 yml 配置在Feign中启用hystrix:feign.hystrix.enabled=true 主程序添加 @EntalbeCircuitBreaker 添加降级代码 添加一个单独的降级类,需要实现声明式客户端接口 复制代码123<a href="/profile/664319079" data-card-uid="664319079" class="js-nc-card" target="_blank" from-niu="default">@Component public class ItemFeignClientFB implements ItemFeignClient {    实现降级方法,返回降级响应}</a> 在接口上,还需要指定降级类: 复制代码1234@FeignClient(name="item-service",fallback=降级类)interface ItemFeignClient {     } 添加Hystrix监控 actuator依赖 配置暴露 hystrix.stream 监控端点 复制代码1m.e.w.e.i=hystrix.stream 访问 http://localhost:3001/actuator/hystrix.stream 访问仪表盘,访问上面的监控数据
分享
1
先马后看
暮草兮
中国人民大学·2022届

分享一下字节春招和华为春招,希望我失败经验能为大家铺铺路

经常通过刷应届生来积攒经验,和为面试做准备,第一次在这里给大家做分享。虽然我没有成功拿下两个公司的offer,但是我还是想把我的经验分享出来,进而帮助更多的同学。也希望更多的同学可以在春招中拿到理想offer。    1. 字节跳动---销售管培生。     从一月疫情开始,我就没少投递,也接到了很多电话,虽然很多都只是简单的与公司之间相互了解,但确实为我后期的面试打下了一定的基础。    3月13号,我进行了简历的投递。22号做了题,当时做的时候由于有点事就很快的做完了,没有太当回事,24号收到了面试邀请。    在面试前开了一次空宣,让过了笔试的大家在一起看了字节的空宣,必须说字节还是很好的公司。在空宣中,hr说投递的简历超过1w5人,最后我看来参加空宣的大概200人吧,所以竞争还是很激烈的。    第一次面试是群面,给了一段材料就是把3000w和7000w分给不同的部门的题。我们小组10个人但是不知道什么原因只来了8个,这是我第一次进行无领导小组讨论,又是线上的模式。其实很紧张,在此之前在牛客和知乎上了看了一些攻略分享,但是真正开始的时候还是很紧张,小组成员都比较nice,但是实力也很强都是有着各种名企实习经验的,所以我觉得简历上有相关的实习或者知名企业的经历还是蛮重要的。我第一次进行这种小组讨论,主要还是以为大家提供idea和抓住一些题目的要求为主吧。对了,我记得当时好像有组员迟到了,结果面试官问问题的时候就问他为什么迟到了?还是比较尴尬的。所以建议大家都能按要求参加面试吧,毕竟很多时候面试官就在那里开着视频等你加入。    第二面是hr面,当时我们组里还剩下7个人(应该是上海地区),我们前6个都是一个男面试官,他问了很多简历的上的内容,比如说说实习的经历,学生会的经历等,还问了对于字节,对于岗位的看法,最后我问了他对于这个岗位希望招的什么样的应聘者?他让我先谈了谈,他的回答中把这个岗位一分为二,一个是销售,一个是管培生。所以,如果大家对这个岗位感兴趣不妨这个看一下。    4月2得到消息我没有通过,一个有很多快消实习经历的同学通过了,所以这也侧面体现了实习经验对于找工作的重要性    2. 华为---消费者管培生    2月底投递的简历,据说很多华为是从秋招没有审查的简历开始进行审查面试的,所以其实投递时间有点晚了    3月13号下午,接到了华为电话,当时面试官简单的了解了一下我的情况,然后说会安排一个面试环节。    3月19号下午,进行了一面专业面,华为的一面时间有些长大概用了50分钟。我的面试官是一男一女,以男面试官为主。他们主要就学业的一些问题,比如研究生的论文,市场营销的4p,4c ,SWOT等方面进行了考察,也问了关于疫情的看法,还有关于实习方面和学生会方面。女面试主要问了一下关于市场营销中to C的经验看法。总体来说问的很细,很深。这也是我春招面试以来觉得最专业的面试过程了,聊天过程也是比较愉悦的。    4月3号,华为主管面。在主管推迟了很多次定好的时间以后,终于敲定了时间,但是最后还是临时被通知提前时间。这个面试我始终没有看到面试官的脸,他将屏幕推向了天花板,也有来回走动,推来窗帘等。问的问题也是很刁钻。各种质疑面试者。    1.年龄大了或者年龄小了,然后就是对每一年的经历进行回顾    2.质疑学校,你的学校好像不如英美名校啊    3.质疑能力,对于实习经历和校园经历的质疑    4.质疑出国英语能力,让我们说一段英语(我是比较反感的,有点像是尴尬的让一段表演节目)    5.让我出题考察一下自己    6.没有给我反问的机会,直接让我下线了    总体来说,就是大家常说的打击型面试吧,后来与其他面试者进行交流发现他们的面试过程也不尽相同。都是质疑我们的各种能力和经历。    大概1小时之内就可以在官网上看到结果,最后我们四个都没有通过,大家建了群也进行了讨论,有说可能缩招了,也有说华为春招结束了的。    我觉得大家还是要做好各种准备吧,当时我并没有想到会是这样的面试形式,所以,当面临时还是显得有些准备不足。    提示:    1.简历最好有几段相关实习或者知名大企业实习经历    2.多投些简历,有些公司可能你不想去,但是面试或者电话的经历都能对未来面试有所帮助    3.做好各种准备,不仅仅是面试会问的题目还有心理准备,做好打遭遇战的准备    4.了解公司的信息详细一些,不仅仅是公司的简介,还有公司相关的大新闻    5. 多跟一起面试的大家交流一下,每次面试多总结    我总是挂到最后这一俩轮上,所以进一步的内容也不能分享了。希望大家以我的失败经历为教训拿下理想offer    大家加油 奥利给!
分享
3
先马后看
张凯
中国科学技术大学·2022届

看一个双非二本(0实习)大三学生如何拿到阿里、腾讯offer

如果你想走前端方向 如果你现在前端零offer 如果你想进大厂 抑或你只是想看看自己的技术栈是不是缺少了什么 欢迎star《前端面试精粹》 我做这个平台的愿景是:"为前端小白定制战略化斩获Offers的计划"。 前言 编程路上,作为很多像我一样的双非本科生,大厂似乎遥不可及…… 后来,我经历了2020年春招(暑假实习)发现,“意识”和“努力”同样重要。 欢迎阅读我的文章,我不敢保证你能一定能收获顶尖offer,但能保证按照我的思路,会比“随缘”的自己,要爬得更高。 即使你并非前端方向,该文章同样适合你,因为在这场战役中,我希望唤醒你强大的“意识”,为你披荆斩棘! 适用对象 心有大厂,互联网/计算机方向的在校生。 尤其是,认为大厂离自己遥不可及的“菜鸟”。 作者介绍 苏一恒,双非21届(大三)学生,自学前端一年左右,然后在2020年春招拿到阿里、腾讯、墨刀等暑期实习offer。 我的经历 2020年: 1月份,考试季,做出第一版简历。 2月份,开始尝试在牛客上一件投递了十余家公司,仅有一家小公司给面试机会,但由于技术栈和入职日期不符,遂主动放弃。 3月份,以牛客内推为主,投递阿里(提前批),被虐惨。上旬开始“边补知识,边投递”的策略。经历了米哈游、美团、蚂蚁、快手、腾讯、360的求虐笔试/面试经历,并不断总结和学习知识栈。最后在下旬时拿到“墨刀”、“袋鼠云”、“亚信科技”等小厂的offer。 4月份,开启了边在墨刀远程实习,边继续学习和投递的战略,在整个月中,经历了有道、腾讯、百度、阿里、京东的面试/笔试,明显感觉比上个月更有信心。最后在下旬拿到深圳腾讯和北京阿里的offer,主动放弃了京东、有道的面试流程(当时京东已经到复试了)。 战略打法 通过和一位211同学的聊天,我终究明白了,并非大厂不给双非机会,而是意识不到位。在我们学校(双非),有大批的学生不知道何时招聘,甚至有人认为“毕业后投简历是校招”的观念。 首先这里声明,校招只针对于在校生,一旦毕业,就只能是社招,而社招的难度远大于校招。比如阿里,社招招收p7起步,一般没有个三五年工作经验是进不来的。 校招种类 有些“春招提前批”、“秋招正式批”等等,这些概念都是约定俗成的,大家别钻牛角尖,大致意思理解即可。 秋招:一般在9-10月,互联网公司提前和应届生签订三方协议,应届生在毕业后能直接以正式员工身份入职。竞争力最强。 春招:一般在3-4月,互联网公司在秋招中因为某些原因,导致最后职位空缺了一些,需要组织一次新的招聘,当然也有其他公司是常年招聘,这里不再多说。 提前批:在正式批开始之前,部分公司的部门会主动开启提前批,为了提前抢夺人才或应对职位空缺。 正式批:正式批次,即上面所说的3-4月、9-10月,这个期间竞争力最大,HC也最多。 补招/补录:别的同学的offer过期,企业需要补充空位,然后就开始补录。 实习种类 日常实习:面试完毕,直接入职,一般要求每周工作3-5天,适合学校在公司周边的在校生,面试难度较低,机会不多。 暑期实习:一般在每年的3-4月份,互联网公司招一批暑假实习生进行干活、培养并给予转正机会,面试难度中等,比日常实习简单。 入职前实习:一般指签订三方后,为了提前熟悉公司环境而选择的主动实习,并非强制要求,实习生可以为了累积经验而自行安排。 竞争对手 作为学生,一般竞争对手也是学生,一般分为以下几类: 从学校性质(来自某大厂): 清华北大 985/211 双非/其他 从学历: 博士 硕士 本科 专科 从地域: 海外 国内 大厂竞争力有多大?我看过一个阿里内推人的数据,在他内推了几千人后,推录比:“100:1”。当然,我这种以偏概全的说法是不对的,所以只作为参考即可。 如何备战 不同的时期会有不同的战略,但最大的原则就是:“即刻行动”。不管你处于哪一个时期,都要执行这黄金法则: 黄金法则三步走: 完善简历、投简历 被虐 总结,重复以上 介绍几条常见备战路线(最好的还是第一条): 大三寒假投递春招暑期实习岗=>大三暑假实习=>边实习边投秋招=>签三方 大三暑假秋招=>签三方 大四参加春招补招=>签三方 研一、研二找机会实习=>最后一次春招/秋招投三方公司 大三: 寒假春节前后:以备战为主,利用寒假做一个拿得出手的项目并部署,练习算法(leecode和剑指offer) 2月中下旬:认准牛客“讨论区”,尤其关注“内推”和“提前批”,开始第一波投递。 3月:边投递、边被虐、边总结、边投递,并在这个期间不断更新简历 4月:经历了一个月的“虐学”,你会觉得4月份的面试/笔试比3月要轻松得多,坚持下去,打法不变。 大四: 按照“大三”的策略,更针对性地以“拿到offer”为最高目标,边投递、边被虐、边总结、边投递,直到毕业前能拿到相对满意的offer为止。 职位选择 互联网职业分为技术类和非技术类,根据自己的喜好进行选择,一旦确定某个职位:首先去大厂招聘平台上看职位要求,针对要求不断学习,让自己一步步地往“最匹配候选人”方向上靠拢。并根据此来进一步完善简历。 技术类:前端、后端、算法、安卓、数据分析等等。 非技术类:产品经理、HR、人力、产品运营、新媒体运营等等。 制作简历 简历是给面试官和hr看的,对于铺天盖地的简历,并非是花里胡哨就能吸引眼球。对于技术岗,简洁、大方、有份量的简历更吸引人眼球。 一份精心制作的简历有以下好处: 在完善简历过程中查漏补缺 简历初筛更高的通过率 面试官的提问更有针对性,也起一定的引导作用 面试被挂后有更高被捞的机会 列举一些简历常用技巧: 1-2页为宜 简短的个人信息放到首部 把展现能力的地方放到显眼的位置 实习/项目1-3个,配上说明和链接(有实习优先实习,无实习写项目) 精炼话术 …… 关于捞人 对于很多公司(尤其大厂),如果你在当前部门的应聘流程中挂了,你的简历会被释放到简历池中,供其他缺人的部门挑选,之后会发起新的面试。其中对于部分公司来说,是可以投递二次简历的,详情以对应公司为准。 关于笔试 互联网笔试一般考察”专业技能“、“数据结构”、”算法“、”网络“、”操作系统“等,也不乏”在线编程题“、”发挥题“。很多公司也会有专门的行为/性格测试题,小部分公司(vivo)甚至会刷人哦!值得注意的是,有的技术笔试不作为刷人的标准(如美团),所以不要轻易放弃。 当然,不要作弊,全程视频(有的还有音频)监控,屏幕检测,页面跳出检测等等,一旦进入黑名单,互联网这条路怕是越走越窄了。 关于面试 面试一般分为技术面+HR面,分别考察硬技能和软素质。 对于大厂,一般有2+技术面,1轮HR面,时间跨度很大,同学们要有耐心。对于阿里,做要好面试1个月的准备,而且很大概率突击面试(突然电话就来了)。对于腾讯,每次面试前都会约面,这个比较良心了。 对于小厂,一般有1-2轮技术面,1轮HR面,主要还是看技术面。 面试一般分为视频面试和电话面试: 在视频面试中,一般以牛客平台为主,也有的公司使用自己的平台(如京东) 在电话面试中,面试官提问你来回答,能不能get到面试官的点,就看造化了 投递方式 首选内推,内推并非是能获得”降难度录取“,而是更快的进入面试流程、方便查询面试结果、更高几率通过简历初筛,有的公司能免一次笔试。总之,好处多多,尽可能地去找内推,这个一般在牛客网的讨论区中很多。 自己去官网也能投递,不过效果不如内推,所以你懂得。 投递平台 首选牛客的讨论区,上面有大量内推人的联系方式,其次可 以考虑拉勾、boss进行海投。不要以为自己投了十几份了,会不会应付不过来?我前期投了四五十份简历的时候,只有5次面试机会,心理压力更大些。 公司优先级 时刻要明白,你的目的是最高的大厂目标。为了未来能进大厂,想尽办法累积实习经验,期间不断总结和学习。 大厂核心 > 大厂边缘 > 独角兽公司 > 有核心产品的中厂 > 有核心产品的小厂 > …… > 中小厂外包 校招法则 金三银四(三月和四月是春招最激烈时期)、金九银十(九月和十月是春招最激烈时期) 实习转正是进入大厂最容易的路 对于没有意愿的公司,不择手段累积面试经验;对于意愿公司,竭尽所能变成面试官的理想候选人 当有保底offer后,心里才稳 阿里某组长:更青睐于实习经验>高学历 写在最后 每个人都有从校园菜鸟到职场老鸟的过程,我希望在你真正摘下学生头衔之前,深刻意识到招聘这场战争的残忍。 如果真的帮到你了,那我很荣幸,能够成为你的引路人。 真心希望你能分享给你最好的朋友,求职路上,我不想你那么孤独,希望心理上的压力,能有人分担,一起共勉。 你需要马上行动,从投递到求虐,或许半年之后你会主动来感谢我的。
分享
13
先马后看
﹃夜\凄凉
江南大学·2022届

面试高级算法梳理笔记

1.1 说明 本篇为《挑战程序设计竞赛(第2版)》读书笔记系列,旨在: 梳理算法逻辑 探索优化思路 深入代码细节 1.2 目录 原文首发于个人博客Jennica.Space,按算法难度划分为初中高三个级别,详细目录及链接如下: 初级篇 穷竭搜索 贪心 动态规划 数据结构 图论 数论 中级篇 二分搜索 常用技巧 数据结构(二) 动态规划(二) 网络流 计算几何 高级篇 数论(二) 博弈论 图论(二) 常用技巧(二) 智慧搜索 分治 字符串 1.3 题解 配套习题及详解同步发布在GitHub仓库acm-challenge-workbook,持续更新。预计在2017年3月完成,欢迎watch。习题难度从国内机试、国外IT名企面试到ACM地区赛不等,吃透算法习题册,应聘足以。 1.4 题库 Google Code Jam(GCJ) Peking University Online Judge(POJ) CodeForces(CF) LeetCode(LC) Aizu Online Judge(AOJ) 2.1 穷竭搜索 2.1.1 核心思想 深度优先搜索(DFS):从某个状态开始,不断转移,直至无法转移,回退到前一步,再继续转移到其他状态,直到找到最终解。通常采用递归函数或者栈(Stack)来实现。 宽度优先搜索(BFS):从初始状态开始,总是先搜索至距离初始状态近的状态。每个状态都只经过一次,因此复杂度为O(状态数*转移方式数)。通常采用循环或队列(Queue)实现。 2.1.2 优化细节 特殊状态枚举:可行解空间多数可采用DFS,但当其比较特殊时,可简短地实现。 全排列使用STL中的next_permutation 组合或子集使用位运算 剪枝:明确知道从当前状态无论如何转移都不会存在解的情况下,不再继续搜索而是直接跳过。 栈内存与堆内存: main函数中的局部变量存储在栈内存中,统一分配后不再扩大,影响栈深度,与机器设置有关。通常,C++中执行上万次递归是可行的。 new或malloc的分配的是堆内存,全局变量存储在堆内存中,使用全局变量代替局部变量可减少栈溢出的风险。 加深深度优先搜索(IDDFS):初始的DFS递归深度限制为1,在找到解之前不断增加递归深度。 2.2 贪心 2.2.1 核心思想 贪心算法:遵循某种规律,不断贪心选取当前最优策略。 贪心证明: 与其它选择方案相比,该算法并不会得到更差的解(归纳法) 不存在其他的解决方案(反证法) 2.3 动态规划 2.3.1 核心思想 动态规划(DP):通过定义某种最优子状态,进行状态间转移达到最终解。 记忆化搜索:将重复状态通过标记降低复杂度。 多种形式的DP:搜索的记忆化或利用递推关系的DP,或从状态转移考虑的DP。状态定义和循环顺序都会影响复杂度。 2.3.2 优化细节 使用memset初始化 重复循环数组 dp仅bool是一种浪费 根据规模改变DP对象 2.3.3 经典模型 背包问题(01背包,完全背包) 最长子序列(LCS,LIS) 划分数(第二类Stirling数,Bell数) 2.4 数据结构 2.4.1 核心思想 优先队列:包含两类操作插入和取值。插入一个数值,获取最小值并删除。堆可高效实现优先队列。 堆:儿子的值一定不小于父亲的值的一种二叉树。插入时先在堆末插入,不断上移直至无大小颠倒。取值时,删除最小值,将堆末节点复制到根,不断下移直至无大小颠倒。插入和取值复杂度都为O(logn)。 二叉搜索树:对所有节点都满足,左子树上的所有节点比自己小,右子树上的所有节点比自己大。插入与查询类似二分,删除时将删除节点左子树最大值或右子树(无左子树时)上移,每种操作复杂度都为O(logn)。 并查集:一种管理元素分组情况的数据结构。可以查询两个元素是否同组,可以合并两组元素,但不能进行分割操作。一次操作复杂度为阿克曼函数反函数a(n),比O(logn)快。 2.4.2 优化细节 平衡二叉树(AVL):当左右子树深度差超过1时,将更深的子树旋转上移,达到整棵树的平衡,避免二查搜索树退化后复杂度升至O(n)。 路径压缩:并查集向上的递归操作中,沿途所有节点一旦向上走到一次根节点,就把其到父亲的边直接连向根。 并查集的同组:广义可表示组内所有元素代表的情况同时发生或不发生。 STL标准库: 优先队列:priority_queue(默认根为最大值) 二查搜索树:set(集合)、map(键和值对应)、multiset和multimap(可存放重复键值) 2.5 图论 2.5.1 核心思想 图:顶点集合为V、边集为E的图记作G=(V,E),从u到v的边记作e=(u,v)。根据边是否有向分为有向图和无向图,根据是否有环分为有环图和无环图。图可由邻接表和邻接矩阵两种方式表示。 Bellman-Ford算法(单源最短路):记录起点到每个点i的最短距离d[i],用所有的边条件持续更新d[i],直到每个d[i]都已经为最小无法更新。图可包含负权边,包含负环的判断方法为将所有d[i]初始化为0,第V次d[i]是否仍存在更新。复杂度为O(EV)。 Dijkstra算法(单源最短路):从起点出发出发,更新s所有可到达的边j,若d[j]有更新,则加入最小堆,以便下次找到剩余集合中d[i]最小的点i,再从i出发BFS,直到到达终点t。不能处理包含负权边的图。复杂度为O(ElogV)。 Floyd-Warshall算法(多源最短路):定义从i到j且经过k的最短路为d[i][j]用d[i][k]+d[k][j]来更新,三重循环直接得到任意两点间的最短路。图可包含负权边,包含负环的判断方法为是否存在顶点i使d[i][i]为负。复杂度O(V^3)。 Prim算法(最小生成树):假设V的子集X已经构造了部分最小生成树,那么接下来就是选取从X到X的补集中权值最小的边加入。可使用最小堆维护待选的边,复杂度为O(ElogV)。 Kruskal算法(最小生成树):将所有边升序排列,依次取出每条最小的边,若该边的两个端点不在相同并查集内,则将该边加入最小生成树,并将两点用并查集连接。耗时最多的操作为边的排序,复杂度O(ElogE)。 2.5.2 优化细节 最短路本质是动态规划,最小生成树本质是贪心。 Bellman-Ford算法和Floyd-Warshall算法可处理包含负权边的图,并结合各自特性判断是否存在负环。 差分约束:将不等式组转化为包含负权边的单源最短路问题,一般采用Bellman-Ford方法解决。若d[i]+x>=d[j],则创建有向边e(i,j)=x。从起点s到终点t的最短路d[t]为s和t允许的最大差。若存在负环,则不等式组无解;若d[t]=INF,则s和t相差可任意。 2.6 数论 2.6.1 核心思想 辗转相除算法(最小公约数):gcd(a,b)=gcd(b,a%b),循环至b为0,此时得到最小公约数为a。 扩展欧几里德算法(解二元一次方程):求解ax+by=gcd(a,b),类似辗转相除法。求extgcd(a,b,&x,&y)时,递归求得d=extgcd(b,a%b,y,x)的解存入y和x。则ax+by=gcd(a,b)的解为x和y-(a/b)*x。 素数筛法:通过已求得的素数,将所求范围内所有该素数的倍数都标记为合数。依序遍历空间,未被筛掉的即为新的素数。复杂度O(nloglogn),可看作线性的。 反复平方法(快速幂):求x的n次幂,可二分递归求x的n/2次幂,即x^n=(x^(n/2))^2 * x^(n&1)。复杂度为O(logn)。 2.6.2 优化细节 ax+by=gcd(a,b)的解大小:x的绝对值不大于b,y的绝对值不大于a。若要求得满足某个范围的解,可通过参数k调节,x+=k(b/d)、y-=k(a/d)为原方程的解簇。 线性素数筛法:遍历解空间,无论当前数是否为素数,将已经求得得素数集合中的数乘以它得到合数标记筛去。并且若该数为合数,它乘以的素数为它的因子,则对该数不再继续循环已有的素数集合。上述可保证,每个合数都只通过乘以它最小的因子得到,即复杂度为线性。注意,该方法使得已有的素数集合中的组合并不一定被立即筛去,在以后遍历到特定合数时才会被标记。 模运算:用64位处理对32数的模,避免发生溢出。模运算对加减乘可以直接应用,但对同模的两边做除法时,若原始ac=bc(mod m),则a-b=m*k/c,则a=b(mod m/gcd(m,c))。 3.1 二分搜索 3.1.1 核心思想 二分搜索:对于某个有序区间,每次查找区间中点是否满足条件,并以此为依据,决定递归查询左半区间或右半区间。反复循环上述折半过程,直到条件或精度被满足。 3.1.2 优化细节 STL:以函数lower_bound()和up_bound()的形式实现了二分搜索 结束判定:1次循环可将区间减半,循环100次可达到精度10^-30 。还可通过区间长度与EPS判断,但要避免EPS太小因浮点数精度问题造成的死循环。 3.1.3 经典模型 有序数组中查找某值 判断一个假定解是否可行 最大化最小值 最大化平均值 3.2 常用技巧 3.2.1 核心思想 尺取法:又称两点法,通过在区间上标记并顺序移动头尾两点,将复杂度降为线性。 反转(开关问题):若为初末态确定,则可通过贪心求得最少步骤。高斯消元法也可求得一组可行解,且自由变元有限,所以也可以求得最优解。 集合的整数表示:通过二进制将集合状态映射至整数。涉及到的位运算包括:与、或、非(取反)、异或、取负(取反+1)、逻辑左移右移、交、并。遍历所有子集或找到所有大小为k的子集,都可以通过位运算操作求得字典序升序的二进制码。 折半枚举(双向搜索):当全局枚举复杂度太大时,可将条目折半,分别枚举所有情况。复杂度降为原本平方根。 坐标离散化:将数列排序并去重,将原数列离散化映射至有限可控的区间。 3.3 数据结构(二) 3.3.1 核心思想 线段树:是一棵完美二叉树,树上的每个节点表示一个区间。根节点维护整个区间,其他每个节点维护父节点二分后的某个区间。查询和更新复杂度都是O(logn)。 树状数组(BIT):将线段树每个节点的右儿子去掉,用每个节点表示区间的右边界代表该节点的索引,这样就可以通过一个线性数组维护所有必要的区间。索引的二进制最低位的1表示区间长度,值为x&-x。求和和更新复杂度都是O(logn)。 平方分割:将n个元素装入√n个桶内,每个桶√n个元素的分桶法,每个桶分别维护自己内部的信息。对区间的复杂度操作可降至O(√n)。 3.3.2 优化细节 懒惰标记:线段树可以通过在父节点上维护一个懒惰标记,来表示整棵子树的状态。在自顶向下的查询操作中,在递归该父节点时,将标记下移至两个儿子节点,并且更新父节点真正维护的直接变量。 稀疏表:与线段树类似的是其区间长度都是2的整数次幂,但每个长度层级的区间左端点都连续。进行RMQ查询时,只需要找到不大于区间的最大2的整数幂,根据这个长度,待求解的左端点及右端点减去长度即为该行在稀疏表内的列的值。预处理时时间和空间复杂度都高达O(nlogn),但结合二分查找的单次查询比线段树快,只需要O(loglogn)。 维护多项式:如果需要维护的变量可以表示为索引i的n次多项式,则可以用n+1个树状数组来维护。或者,线段树的每个节点维护n+1个值。 区域树:线段树的每个节点可以维护一个数组或维护一棵线段树,适合对矩形的区域进行处理。并且,和树状数组一样,多重线段树嵌套可以实现高维度的区域树。 3.4 动态规划(二) 3.4.1 核心思想 状态压缩DP:通常结合进制数的特点,将每个状态压缩表示为整数。复杂特殊状态的转移就可以表示为整数下标的等式。 矩阵快速幂:若动态规划的递推关系式可以表示为多元一次多项式,则可以通过某常系数矩阵的幂与初始向量的乘积求得最后的结果向量。其中求幂可以结合基于二分思想的快速幂算法。用n表示幂次数,m表示向量规模,则复杂度为O(m^3logn)。 3.4.2 优化细节 结合数据结构:某些时候涉及到更新和查询操作时,如果利用线段树等高级数据结构处理,可以使得转移方程中线性的遍历转化为对数级别的查询。 3.5 网络流 3.5.1 核心思想 最大流:增加反向补偿边,在残流网络上不断寻找增广路。常用朴素寻找增广路的Ford-Fulkerson算法,复杂度为O(FE)。通过最短路算法预处理为分层图的Dinic算法,复杂度为O(EV^2)。 最小割:将割中的边全部删去后,源点无通路可再到达汇点。最小割是最大流的强对偶问题,数值相等。 二分图匹配:匈牙利算法递归每个顶点,每次递归,将已有匹配删除看能否得到更优解。 一般图匹配:常用Edmonds算法,较为复杂,尽量转化为二分图求解。 最小费用流:在残流网络上扩展最短增广路时,使用边的花费作为边权寻找最短路。f(e)表示e中的流量,h(v)表示势(残流网络中s到v的最短路),d(e)表示考虑势后e的长度。复杂度为O(FElogV)或O(FV^2)。或者通过不断消去负圈得到最小费用流。 3.5.2 优化细节 最大流变体: 多个源点汇点:构造超级源点S和汇点T,用S连至多个源点,所有汇点连至T。 无向图:构造正反方向的两条边,容量与无向边相同。 顶点也有流量限制:将每点拆为入点和出点,入点至出点连边。 最小流量限制:构造超级源S汇T,对于每条边构造逆向的容量为下限的满流圈;连接S到s及t到T之前,通过满流判断可行解。 部分边容量发生变化:若影响原流,则设法将残流网络中对应边的容量减少或通过构造逆向圈等价减少。 容量为负数:求最小割时可能出现负边,此时应通过数值变换设法消除负边。 最小费变体: 类最大流变体:构造方式相似。 最小流量限制:将原边容量减少下限,构造新边容量为下限且费用为原费用减去一个足够大的数。 流量任意:由于点的势会不断增加,所以仅在势为负数时增广,即能保证费用不断减小。 费用为负:不能用Dijkstra算法,要用Bellmen-Ford算法处理负权边。另外,可以通过对所有边的费用和最终结果进行适当的变形避免负权边。 最小化有流边的费用之和:无法通过最小费用流模型求解,需要建其他模。 匹配相关对偶问题: 连通图中,最大匹配+最小边覆盖=顶点数 最大独立集+最小顶点覆盖=顶点数 二分图中,最大匹配=最小顶点覆盖 3.6 计算几何 3.6.1 核心思想 平面扫描:扫描线在平面上按既定轨迹移动时,不断根据扫描线扫过的部分更新,从而得到整体所求结果。扫描的方法,可以从左向右平移与y轴平行的直线,也可以固定射线的端点逆时针旋转。 凸包:包围原点集的最小凸多边形的点组成的集合,称为原点集的凸包。凸包上的点不被原点集任意三点连成的三角形包含在内部。通过Graham扫描算法,将点集按坐标排序后,分为上下两条链求解。每次末尾加入新顶点,如果出现了凹的部分,则把凹点删去。Graham可以在O(nlogn)的时间内构造凸包。最远点对一定是凸包上的对踵点,可以通过旋转卡壳法不断找寻对踵点,在O(n)复杂度内求解。 数值积分:通常在复杂的几何相交或求和问题中,通过对某个方向变量的微分,将立体切片或将平面切成线段后积分得到结果。 3.6.2 优化细节 向量表示:可以使用STL的complex类表示向量,并进行相应的内积外积操作。 计算误差:计算几何中的浮点数大小比较采取与ESP结合的方式,如a<0等价于a<-ESP,a≤0等价于a<ESP。由于double类型的精确尾数大约是10进制下的15位,当ESP太小时,可能造成假阴性。C++中可以采用更高精度的long double,Java可以使用BigDecimal。 极限情况:当可行解可以取连续一段值时,很多时候只要考虑边界的极限情况。 4.1 数论(二) 4.1.1 核心思想 线性方程组:可以采用高斯消元法求解。将方程组用矩阵表示后,遍历每列,保留该列系数最大的行(列主元高斯消元,减少误差),并将其乘以一定倍数用于消除其他行的该列元素。 一次同余方程:ax=b (mod m)。定义a的逆元为y满足ay=1 (mod m),则x=yb(mod m)。逆元y可以用扩展欧几里得求解。 费马小定律:若p为素数,a与p互素,则有a^(p-1)=1 (mod p)。 欧拉定理:对于一个正整数N的素数幂分解N=P1^q1P2^q2...Pn^qn,欧拉函数φ(N)=N(1-1/P1)(1-1/P2)...*(1-1/Pn),意义是不大于N且与N互素的正数个数。此时,对于与N互素的x,有x^φ(N)=1 (mod N)。费马小定律可以看作欧拉定理的推广。 线性同余方程组:若有解则一定有无数解,解的全集可写作x=b (mod m)。初始化为x=0,m=1。逐次加入一个新的方程ax=b (mod m),将上一步的x用mt+b的形式代入,转化为一次同余方程。 中国剩余定理:n=ab(a、b互素),则(x mod n)等价于(x mod a,x mod b)。所以,通过对n的质因数分解,可以通过只考虑模n的素因子的幂p^k来计算。 n!模p:将阶乘表示为n!=ap^e,则e=n/p+n/p^2+n/p^3+…。由于阶乘中不能被p整除的项呈现周期性,乘积为(p-1)!^(n/p)*(n mod p)!。根据威尔逊定理,(p-1)!=-1(mod p)。考虑可以被p整除的部分,通过全部除以p,可以递归到n/p的范围考虑。 组合数模p:将n和m用p进制法表示后,根据Lucas定理,Lucas(n,m,p)=c(n%p,m%p)*Lucas(n/p,m/p,p) ,则对于每一位,计算其组合数模p,将答案相乘即为C(n, m)模p。 容斥原理:先不考虑重叠的情况,将所有对象计算出来,再不断递归把重复计算的数目排斥出去,直到结果既无遗漏又无重复。由于递归时排斥采取减法,从全局来看应根据地柜深度的奇偶性判断符号正负。 莫比乌斯函数:在容斥定理中,每次排斥的规模d如果是n的约数,则被加减多次的总和只和n/d有关。求这个系数的函数叫莫比乌斯函数,记作µ(n/d)。若x可以被大于1的完全平方数整除,则µ(x)=0;否则计算x的质因子个数k,µ(x)=(-1)^k。莫比乌斯反演定理利用µ(x)推出,f(n)=∑g(d)等价于g(d)=∑µ(n/d)*f(d)。 Polya计数定理:在组合问题中,有时要求把旋转和翻转之后的状态看作相同态,计算本质不同的个数。此时应把所有方案重复计算相同次数,再把结果除以重复的次数。另外,立方体的染色、相同颜色的数量限制、相邻状态限制,都可以用Polya求解。 4.2 博弈论 4.2.1 核心思想 必胜策略:任意方式都无法转移到必胜态的为必败态N,存在一种方式可以转移到必败态的为必胜态P。 Nim:初始有n堆石子,每堆有ai石子,游戏规则是每次从某堆中取走至少一颗,判断初始状态是否必胜。若ai数组异或结果非0则为必胜态,否则为必败态。 Grundy数:当前状态的Grundy值是除任意一步所能转移到的状态的Grundy值以外的最小非负整数。所以,从Grundy为x出发,可以转移到Grundy为0到x-1的状态。Grundy数等价于Nim中的石子个数,再通过异或判断状态必胜与否。 4.2.2 优化细节 取放石子:Grundy数可以转移到更大值,等价于Nim中放回石子。但可以通过采取对应策略再转移到原值的状态,所以对胜负没有影响。但此时,状态可能出现循环,所以需要注意可能会出现不分胜负、达成平局、永不结束的情况。 复合游戏:由于在Nim中,只要异或值相同,石子堆数不影响局面性质。所以对分割后的各部分取异或,就可以用一个Grundy数来表示几个游戏复合而成的状态。 4.3 图论(二) 4.3.1 核心思想 强连通分量:图中任意两点可互达的子图叫做强连通分量。任意有向图都可以分解为若干个不相交的强连通分量。将图中的强连通分量都缩成一个顶点,可以将原图转化为DAG(有项无环图)。 强连通分量分解:通过两次DFS实现。第一次DFS时,回溯时为顶点标号(后序遍历)。标号越小表示离图尾越近,即搜索树的叶子。第二次DFS时,将所有边反向后,从标号大的顶点开始遍历,每次遍历的顶点集合组成一个强连通分量,记录该分量的拓扑序。接着,再取未访问节点中最大标号的顶点开始DFS,拓扑序加一。直到顶点全部遍历,算法结束。总的复杂度是O(V+E)。 2-SAT:对于每个子句中文字个数不超过二的合取范式,可以将每个子句等价转化为两个蕴含关系,将所有蕴含关系为边、每个变量取真取假为点,构建有向图。图中的每个强连通分量内,若某个点为正确,则分量中所有顶点都为真。对于每个布尔变量,考虑其取真取假的两个点,点所在的强连通分量拓扑序大的点情况为真。由此,得到一组合法的布尔变量赋值。 LCA(最近公共祖先):有根树中,两个结点的公共祖先中距离最近的那个成为LCA。高效求解LCA可以采用倍增法预处理加二分搜索,或中序遍历后利用线段树或BIT做RMQ求解。 4.4 常用技巧(二) 4.4.1 核心思想 栈的应用:在栈内维护一个下标和对应值都单向递增的序列,则可求距离自己最近的比自己大的值。 双向队列的应用:队列中维护一个以某区间内最小值开始,单向递增的序列,则可求窗口大小一定的滑动最小值。 倍增法:通过预处理,计算出距离每个点2的次幂处的状态。由于转移到的目的地满足一定条件,且具有与下标单向相关性,所以可以通过二分搜索,每次将2的幂减1,判断是否出终极目标的位置。 4.4.2 优化细节 数量的二进制表示:在一定数量的物品中挑选若干个,可以通过每次是否添加2的次幂个物品来决定最终结果。将原本枚举的复杂度O(n)降至二进制位数O(logn)。 连续状态转移:在DP中,如果某状态可由连续的下标的一些状态转移来,并要求其最值。可以试着把状态转移方程分为两部分,一部分为常量,另一部分为只与前一状态下标有关。则问题转化为,求某个函数在某个滑动窗口内的最值。如果窗口大小固定不变,则可利用双向队列求解滑动最值。 4.5 智慧搜索 4.5.1 核心思想 剪枝:调整搜索顺序,从分支少或影响大的部分开始搜索。无任何效用或无法到达最终态的步骤可以提前剪枝。没有最优解则剪枝,通常可以通过贪心等算法求得最优解下界的下界。 IDA:通过搜索判断是否有某个不超过x的解,将x从0开始每次加1,首次求到解的x就是最优解。这样,在搜索过程中,就不会访问比最优解更大的状态。迭代加深搜索(IDDFS)类似于宽度有限搜索,按距离初始状态的远近访问各个状态,而IDA会通过估算下届提前剪枝优化。 A*:BFS和Dijkstra利用下界优化,将优先队列中的键值改为初始状态到当前状态的距离加上到目标状态的距离下界。此时,优先队列顶端元素未必是初始状态到当前状态的最短路。 4.5.2 优化细节 IDA与A对比: IDA针对DFS,A针对BFS。 IDA不怎么花费内存,A需要关于搜索空间的线性的内存。 通过不同路径到达同一状态,IDA效率急剧下降,而A可以通过选取合适的下届保证每个状态至多检查一次。 IDA*在不断增加递归深度限制时重复搜索了很多状态,但总的访问状态数和最后一次访问状态数是同一数量级的。 4.6 分治 4.6.1 核心思想 分治:将问题划分为更小规模的子问题,递归解决子问题,再将结果合并从而高效解决问题。 数列上的分治:每次递归数列长度减半,合并时除将子问题的情况合并外,还需要考虑左右两个子数列交互问题。通常需要在递归时,维护数列状态,如排序或某些统计值大小。 树上的分治:树的重心是指删除该顶点后最大子树顶点数最小的点。通过树的重心来分解树,可以避免分解不均匀导致的退化现象。按重心分割,可以保证,每次划分后子树的大小都不超过n/2,所以递归深度不超过O(logn)。 平面上的分治:将待求解平面按x或y坐标分为两部分,各子问题递归求解,再合并。对于两子平面交互的部分,通常可以通过一些限制条件,只考虑有可能达到最优解的一些状态,可以极大降低复杂度。 4.6.2 优化细节 树的递归:递归分解无根树时,可以代入两个参数,当前节点及父节点。在更新当前节点时,其所有相连顶点中,除去父节点及已被分解出去的子树根节点,其余就是可以继续递归的子节点。 4.7 字符串 4.7.1 核心思想 KMP:通过DP计算next数组,next[i]表示以模式串的第i个字符结尾的后缀与模式串前缀的最长公共子串中,公共子串结尾的位置。当模式串与母串进行匹配时,若发生字符不匹配的情况,可以将母串指针位置保持不变,将模式串的指针前移至next位置的后一个字符,若依然不等,递归next直到相等或者超出模式串头。复杂度O(m+n)。 Trie:树上的边对应字符,从根到节点的路径上的字符序列即为该节点表示的字符串。Trie是一个高效维护字符串集合的数据结构,查找长度为l的字符串复杂度为O(l),同时节约空间。 AC自动机:又叫Aho-Corasick算法,将多个模式串的所有前缀用Trie表示,再在Trie上进行KMP。 Robin-Carp哈希:将字符串看作b进制数,循环移动头尾,可以得到每个串的哈希结果,用来判断两字符串是否匹配。可推广到二维情况的哈希算法。 后缀数组(SA):将字符串的所有后缀按字典序排列后得到的数组,sa[i]表示排名第i的后缀的起始位置,rank[i]表示其实位置为i的后缀的排名。利用倍增的思想,可以在log(n(logn)^2)时间内得到后缀数组。通过计算得到的长度为k的所有后缀及其排名,利用rank[i]和rank[i+2]组合得到长度为2k的后缀及排名。 高度数组(LCP):后缀数组中两个相邻后缀的最长公共前缀。由于h[i-1]≥h[i]-1,可以从左到右遍历后缀头的位置,通过尺取法,在O(n)时间内求得。由于高度数组的传递性,结合RMQ,可以求得任意两个后缀间的最长前缀。 4.7.2 经典模型 串的状态转移:KMP/AC自动机 单字符串匹配:KMP/Robin-Carp哈希 多字符串匹配:Robin-Carp哈希/AC自动机/SA+二分搜索/扩展KMP 最长公共子串:strcat+SA+LCP+RMQ 最长回文子串:strcat+SA+LCP+RMQ/Manacher
分享
9
先马后看
淡色
纽约大学·2022届

博士学位真的那么重要吗?上交大博士亲述科研心路,获4万高赞

十三 转载整理自 时间规划局 量子位 报道 | 公众号 QbitAI都说读博就像一场 ** ,“一入红门深似海,从此半点不由人”。还时不时曝出博士生抑郁、甚至自杀等负面新闻。但为什么每年都有那么多人前赴后继的选择读博,博士生学历真的那么重要吗?最近,知乎上一位叫做“时间规划局”的博主,便在这一问题下做了回答。用最朴实无华的语言,讲述了自己从硕士抑郁,到发现科研新大陆,而后读博、放弃博士后,以及工作、创业,这一路上的艰辛与收获。 他的故事打动、鼓舞了许多网友,第一次的回答(删过一次)便揽获了近4万的赞同。 甚至许多网友纷纷表示,这是他们在知乎上看到的最好的故事。 那么,到底是什么样的回答,让网友们受鼓舞至此?量子位经授权,整理了这篇文章内容,与读者一起分享。博士生学历真的很重要吗?用讲道理的方式去说服一个人接受你的观点,通常是很难的,那我就讲讲自己的故事吧,很长,要表达的,都在里面了。时间拉回到十六年前,2004年的第一场雪,比以往来的更早一些。我和师兄、师姐蹲在冷呵呵的实验室里,看着面前那台崭新的机器,嘎吱嘎吱向外喷涂着被融化后的ABS树脂。对于研一的我来说,一切是那么新奇和高端。我们的研究方向是导师的一个教育部课题,基于RE(reverse engineering,逆向工程)和RP(rapid prototyping,快速成型)的机械零部件快速设计制造。让我万万没想到的是,十二年后,也就是2016年左右,这项技术突然火了起来,换了一个叫“3D打印”的名字,变成了可以改变世界的技术,做一份PPT能很快拿到千万级的投资。但这一切都和我无缘了,因为在2005年夏天,师兄带领师姐成功发表了4篇核心期刊论文,而我还没来得及动手,导师的项目就成功结题了。然后他和其他导师合作,拿到了一个牛轰轰的863项目,两千万经费,我的“3D打印”之梦就此破灭,被迫转行。我可是给杨叔子院士写过邮件,和他探讨过RE/RP技术前景,他还鼓励我好好干呢,呜呜,拜拜了。导师拍着我的肩膀,告诉我不要气馁,能力更大的人要承担更大的科研任务。我感觉他是在忽悠,但我没有证据,只是很惋惜费了我1年时间、足够发表2篇论文的实验数据。那也是我的心血呀,最后我忍痛把数据都送给学院里另一位工程硕士,保障他顺利毕业了,他非常感激我,请我在学校后门吃了一顿酸菜鱼。那个酸菜鱼馆再向北走200米,是宽阔的长江,江里的鱼都很美味,值得我们每个从这里毕业的人,毕生怀念。日子还要继续,我收拾心情,转向863项目的研究工作。只是当时我还不知道,这个项目,它深刻的改变了我后来的人生。在2005年,我参与的这个863项目可以说相当前沿和新潮,我负责的那部分研究内容,需要 ** 高性能、低表面粗糙度、纳米级的磁性薄膜(NiFe、CoFeB之类的)、搭建一套稳定的飞秒激光泵浦-探测(pump-probe)实验平台、编写数据采集软件并借助Matlab进行数据处理和运算,通过研究软磁薄膜与飞秒激光相互作用的超快瞬态动力学效应,为研发面向未来的太赫兹器件奠定坚实的理论与实验基础。这对机械制造及自动化专业的我来说,简直一脸懵,就像刘姥姥进了大观园,太高端太刺激了,又因为自己啥也不懂而感到深深的自卑。在这种自卑感的驱动下,我的苦逼日子开始了,连续一年半的时间,我没有在晚上11点前回过宿舍,一直做的事情只有三件:查文献,做实验,编写Matlab。 有那么3个月的时间,我一直处于抑郁状态,因为辛苦做出来的实验数据,波形上总是有异常的震荡和杂波,我一遍遍的用磁控溅射方法制作薄膜、一遍遍的检查pump-probe平台、一遍遍的检查数据采集软件,最后我崩溃了,因为根本发现不出什么问题,每个环节都好像是正常的。3个多月过去了,我差不多也得上抑郁症了,和女朋友也时不时的吵架,一切都是灰暗的,看不到希望。那是一个极其偶然的下午,我继续穿着净化服,在洁净室里的激光平台上重复着实验,妄图优化并查找出问题根源。洁净室有一扇封闭的玻璃窗,有个师弟坐在窗外的电脑前勤奋的工作着。我神经兮兮的盯着数据采集界面,祈祷不要悲剧重现,可是震荡和杂波还是如约而至,盘踞在实验数据界面。压抑的实验室里,我分明听到响亮的打脸声,啪啪啪啪,令人头晕目眩,眼冒金星,孤独而绝望,那一刻,我想死的心都有。突然,有人在实验室外大喊师弟的名字,说他女朋友在外面等他,这个有点莽撞的家伙,猛地站起来往外跑,然后悲剧发生了,他一脚踢掉了他那个台式电脑的机箱电源,笨重的CRT显示屏一下熄灭了。师弟大喊一声卧槽,因为他那台电脑是实验室的顶配,里面每天都在不停的运行模拟程序,机箱和显示器都贴了纸条不准关闭,这一关机,跑了7~8天的模拟进程,大部分都毁掉了。我两步走到窗边敲了敲,看着师弟灰白的脸色,他勉强挤出一丝笑容,隔着玻璃大声说“导师估计要砍我了,好几个人都等着我的模拟结果呢”,我满是同情但也不知道怎么安慰他, 目送他慢慢走出去了。我两步走回去,又开始观察实验结果,我发现竟然出现了奇迹,从2分钟前,也就是我走过去看师弟意外关掉电脑的那一刻,数据波形上的震荡和杂波消失了,光滑平顺接近完美,阶跃突变的信号响应也如高台跳水下落般的完美曲线,这就是我苦苦追寻了一年想要的实验结果啊,我激动的要哭了好吗? 看着被师弟熄灭的显示器,我恍然大悟,这种CRT的垃圾显示器,后方会产生大量的电磁辐射,一般情况下也没什么影响,但是我的pump-probe实验,需要良好的电磁屏蔽,激光锁相放大、震荡、弱信号采集都受不了CRT显示器的干扰。很快导师把CRT显示器都换成了液晶,把所有的电脑都搬离净化实验室,还给实验室的墙上加装了电磁屏蔽网,同时也给师弟的顶配电脑装了不间断备用电源,防止他再把电脑关掉。后来的硕士阶段,没有了悬念,我得到了大量完美数据支撑研究结果,科研之路如开挂一般,发了好几篇论文,还在毕业前收到了SCI期刊Physica B的录用通知。尽管当时影响因子只有1不到,但是硕士就发SCI期刊论文,在只会发中文核心期刊和EI会议论文的机械学院,还是引起了很大轰动,导师也觉得挺有面子。我闭着眼睛就能毕业了,心态轻松惬意,和女朋友也不吵架了。但很快我又遇到了新的困惑,我虽然用实验验证了飞秒激光和磁性薄膜相互作用的超快动力学现象的确存在,但背后存在的物理机制以及它能够应用到哪些具体的方向,我完全想不明白,我也不知道实验结果的理论基础在哪里。我和导师去探讨,他觉得我想太多了,他说这是和基础学科相关的,我只是看到了表面,并没有深入进去。但当时的我,内心经历了科研过程中哥伦布发现新大陆的狂喜,我已经爱上了这种感觉,我想把这背后一切搞明白啊。我想继续纠缠着导师,他又拿到了新的课题,没空理我了,给我丢下一句话,想把理论搞明白,就去读博士吧。研三的十月份,大家纷纷开始找工作了,那些研究模具、精密加工、电气控制的同学,很快都拿到好几个offer,我唯一的一次面试,是上汽到学校招聘,有个师兄是上汽的中层领导,回来面试我们。轮到面试我的时候,他拿一罐可乐,说“师弟你几分钟可以把这个造型做出来?”,我苦笑了一下,我说Pro/E和CATIA都没学啊,我研究磁性薄膜和飞秒激光去了。师兄笑着说你这个太高端了,我们不需要这么深奥的,然后我就被刷掉了。这么高端的研究方向,工作肯定是不好找的,面试过这一次,我再也没去面试过了,因为我之前已经查好了,国内有个课题组研究磁性薄膜和MEMS芯片方向,我也和那边打过几次电话交流了,我想要的答案,那些潜在的电磁学机理,在那里可以找到答案。硕士阶段告一段落,博士阶段的故事更精彩。 考试,复试,收到录取通知书,告别硕士的导师,告别硕士生涯,博士阶段就开始了。令我感到很满意的是,这里真的是可以找到答案的地方,课题组的几个牛人,有精通电磁学理论计算的,有精通薄膜 ** 工艺的,有精通基于薄膜制作MEMS芯片的,真是太爽了,学院里还有一条3英寸的芯片流片线,这下从理论到实验再到具体的芯片产品制造,都齐全了。反正不知道别人是什么感觉,我觉得自己就像进入了阿里巴巴的宝库,我延续了硕士时期的风格,一头扎进实验室里,就不怎么想出来了,像海绵一样吸取养分,向别人不断的请教。在硕士阶段的基础上,我基于NiFe和CoFeB材料的软磁薄膜性能越做越好,我不但会用磁控溅射机,我还学会了用气相沉积设备、外延设备、硅刻蚀设备、离子束溅射与刻蚀设备、光刻机、电镀设备、烧结炉、离子注入掺杂设备、SEM(扫描电子显微镜)、XRD(x射线衍射仪)、VSM(振动样品磁强计)等一系列设备。管理实验室设备的老师,都把我收为关门弟子了, 晚上如果他有事,下班时都把钥匙给我,让我关净化间的水电气,早上再老早过去开实验室。实验室的设备加起来上亿,对我也是真的信任了,这也让我比别人有了更多的时间和自由度去做实验,加速了我的科研进程。而且我跟着师兄学会了手撕麦克斯韦方程组,我有个100多页的演草纸,上面全是手写的求解麦克斯韦方程的计算过程,我把麦克斯韦方程应用于求解软磁薄膜的高频磁阻变化以及随外界磁场和电场变化的响应曲线,得到的结果可以直接拟合实验结果,相似度非常高,然后就可以用理论计算得出的优化参数去指导实验工作。 当时有个磁阻变化率的指标,我可以和全球几个主要的相同研究方向的课题组一决高下,经常是他们做到30%,没多久我40%的结果就发论文了,然后看到他们刊出的65%的论文,我已经做到80%了,后来根据理论模型得到的参数,我不用 ** 薄膜的方式了,换成磁导率更高的薄带材料,指标竟然可以做到200%以上,导致国外的几个竞争对手很眼红,就赶紧跟着换材料、发论文,和我们进行比赛。我超级享受这样的过程,中间还会和他们互通邮件聊一聊,就好像两个拳手打擂台赛,休息的间隙还在交流你用什么牌子的蛋 ** 啊,你平时吃牛肉还是猪肉啊之类的,感觉既好玩又好笑。2008年暑期,是我论文最高产的时段。我的实验工作进展顺利,手里的数据很丰富,我首先想到的是Physica B,毕竟我是老客户了,我翻出了之前发论文时给主编写过的邮件,追加了一封邮件,大意是我换了新的研究环境,有了新的研究进展,希望能把一些重要的结果在他们论文上分享给全球的同行们,然后我还说,你应该知道,现在北京正在开奥运会,希望你多关注这场盛会并享受体育带来的快乐,然后我把投稿的论文编号也发给他,希望他能关注并处理。没想到,过了不到1天,主编给我回复了一封热情洋溢的邮件,感谢我对他们期刊的关心,并说自己最喜欢看跳水和体操类比赛,讲了几个我不知道的名字的运动员,最关键的是,他说收到我的投稿了,会尽快让同事处理。然后,我的这篇SCI论文从投稿到修改到接受,只用了26天的时间,你说老外不讲私人感情吗?我觉得他们肯定是讲的,毕竟都是人嘛,和你聊的很好,而且你的论文做的工作又不错,加急给你发表了,也就是他们举手之劳的事情。老外也都讲感情,这个发现对我产生了很大影响,我每次投稿都要找个主编或副主编邮件聊聊天,结果2008年的暑假,我搞定了3篇SCI论文。 有时正在看中国队的比赛,夺冠热血沸腾的时候,一封邮件提醒弹出来,是SCI核心期刊的论文录用通知,那种美妙的、刺激的、眩晕的愉悦感觉,不做科研的人永远体会不到,也许像 ** 后的感觉。但 ** 是短暂的、要命的,而科研成就带来的愉悦是永恒的、健康的。这样的日子过了1年多,有一次和导师聊天,他说一直在思索除了单纯的做器件追求性能,他更想把器件具体的应用到某个方向,开拓出一个应用领域,但这几年下来,他一直没找到合适的方向,如果找到了这样的方向,他很想发一篇Applied physics letter的论文,这是他年轻时候的一个小心愿,不过到现在还没实现。我听了心里一哆嗦,这怎么和我最近几个月在实验室里没事瞎琢磨的想法那么一致呢?我也想干这个事啊,我都琢磨了很多具体方案了啊,你不问我都不知道怎么说啊,我努力帮你发个APL圆梦好了啊。再加上我平时也关注娱乐新闻,当时张国荣自杀还没几年,梅艳芳也患了宫颈癌去世了,我因为《胭脂扣》喜欢这两位明星,心里惆怅了很长一段时间。我看了一篇国外的报道,梅艳芳的去世是因为当时对宫颈癌的HPV病毒没法做到早期筛查和分型检测,HPV病毒有很多个亚型,最致命的有6种,每一种对应的治疗手段和用药都要有所区别的,所以不能做到早期筛查会出人命(因为早期的病毒在体液中的浓度含量很低很低),筛查出来之后,无法做到精确分型,也会因为治疗手段的不太对症而延误治疗。这些事情,我都考虑过很多天了,我一直在想怎么去解决这个问题,今天导师既然主动提起,我立刻满怀激动的和导师谈了2个小时,并把我的具体实验方案和如何检测讲了一遍。导师听完以后一直笑,可能他也觉得我讲的不错,问我这些想法怎么来的,我说我天天都在考虑这些东西啊,其实他不知道的是,我这2月和搞生物检测的另一个同学谈过几十次了,他说针对HPV每个亚型目前已经有特定的标记物了,每种标记物只认识对应的其中一个亚型,对其他的都熟视无睹,标记物就像触手一样,会紧紧的抓住它认识的那一个亚型的病毒细胞。我内心里喊了一句卧槽,思路来了:我用纳米磁性粒子修饰到病毒细胞表面、用微流体芯片分成不同检测区域、每个区域修饰不同的标记物、每个区域的标记物对应抓取不同的HPV亚型细胞、哪个或哪几个区域细胞抓的多、哪几个区域的纳米磁性粒子就多、我的传感器去检测哪几个区域的磁型号更强,不就可以检测出体液样本中包含哪种或哪几种HPV病毒了吗?而成熟的PCR扩增技术又可以解决早期病毒细胞浓度极低而检测不到的问题,从而解决早期筛查的困扰。 怎么做高性能传感器、怎么刻蚀微流体芯片、怎么制作纳米磁性粒子、怎么在微流体芯片检测区域表面修饰标记物、怎么在病毒细胞表面修饰纳米粒子、怎么PCR扩增、怎么处理弱磁检测信号,我和同学两个人一合作,全部都搞定了啊。兴奋,激动,我一下子又找到了硕士阶段哥伦布发现新大陆的感觉,我仔细查过文献,全球的科研圈,压根没有人用我们这种传感器做过这样的研究,这次导师主动找我聊,正好我把想法全盘托出。导师继续微笑着看我,当场批了10万块的预算,让我买各种耗材就开始整了。2009年,整个一年我都很忙碌,按照之前的思路,各项试验进行的很顺利,我们一气呵成,在APL上刊出了研究成果,主编对我们还美言了一番,感谢选择他的期刊之类的话。反正也不重要了,我已经和导师举杯相庆了,我帮导师完成了课题组发表APL的心愿。导师问我接下来的想法,我说我们的高性能传感器,除了检测HPV,还能检测胃癌、肺癌、前列腺癌等各种标记物能识别的癌症细胞,能早期筛查和分型检测,我要像开挂一样的发表论文了。说干就干,2010年我们又转向胃癌细胞的检测,改进了微流体芯片和传感器结构,顺利发表了两篇更高水平的论文,其中一篇在Biosensor & Bioelectronics上也是快速发表,这个杂志当年影响因子6.5,那时还没现在这么多灌水的,现在影响因子都要破10了,JACS感觉到了压力。其实按照正常的发展路径,我接下来会成为一个科研能手,手握大把高质量论文,顺便申请一堆专利,承担各种国家的研究项目。但人生总是充满了变化,谁又能说的清楚呢,有两件事情的发生,对我接下来的人生方向产生了深远影响。2010年,发生了两件对我很重要的事,第一件事是学校基本不再留博士毕业生任教了,这让我的导师很遗憾,原本我们计划好让我留校,然后在新领域大干一番,因为3年时间里,我已经以一作身份发了8篇SCI核心期刊,加上和别人合作的,共计14篇论文,影响因子累计40多了,而且手里正在做的实验和相关结果,已经够再发7~8篇高水平论文了,甚至冲一冲Nature materials子刊(材料应用领域的终极神刊,影响因子40),都是有可能的。用导师的话讲,我一个人可以顶别人一个课题组加起来的成果了,但现在政策变了,我没法留校了,他感觉非常遗憾。他建议我去国外读博后,然后再回来学校,有了国外的经历,就可以满足留校资格了。因为当时那几个和我是竞争对手的课题组,一直追我的进度,而且也在跟着我转向传感器进行生物检测的方向,这三个课题组的负责人都给我写邮件,法国、美国还有西班牙,邀请去做个博士后研究,然后可以考虑留下任教。面对导师的建议和国外几个课题组的邀请,我最终选择了放弃,原因是我不想继续做科研了,我要换个人生方向。让我产生这种想法的原因,是源于2010年初我参加的一次在线会议,那是一个传感器国际论坛,在新加坡,我因为没有争取到经费支持,没能到现场参加。通过视频,我观看了日本那个叫Mohri的学者,做了关于磁传感器芯片的演讲,这个演讲简直给我留下了巨大的阴影,让我明白了天外有天、人外有人。本来这个叫Mohri的学者,之前也一直在做和我们类似的器件研究,后来我们转向生物检测应用,他们就没有声音了。我因为做了一点生物检测的工作,发了几篇论文,就开始琢磨这个东西应该可以产业化的,但我们搭建的实验平台粗大笨重,信号处理都是PCB板级的电路,高频信号激励是买了一台笨重的阻抗分析仪还有一台信号发生器,整个实验系统满满当当占了半个实验室。但是,这个Mohri教授,沉寂的几年,他竟然找到日本爱知钢铁投资了他,把传感器的敏感单元、信号处理、阻抗匹配、高频信号激励、电磁屏蔽等模块,全部集成化做进了一个2mm*2mm的封装芯片里面,然后给日本的手机厂商供货,开始做手机里面的电子罗盘和地磁方位检测了。 这个讲座让我整个人是崩溃的,2mm*2mm的面积是我的传感器敏感单元的尺寸,而其他的环节,我用了半间实验室的面积,人家却都集成都芯片里面了,而且已经过了研发的阶段,而且已经给手机厂商供货了,而且都申请了很多专利了。虽然单片的性能比我的差一大截,但在手机领域的应用,人家Mohri的产品是够用的,我只能用一大堆笨重的仪器,去做各种生物检测然后刷论文,离实际变成集成化的产品,还差了十万光年距离。我开始怀疑我的研究方向的意义,我从事应用科学研究,就是应该像Mohri一样,把成果变成实实在在的产品,去推动某一个产业的进步,但看着我那一大堆笨重的仪器,又有种深深的无力感,就算再牛,我一个人也搞定不了这件事,这是个系统工程啊。爱知钢铁给Mohri配置了50人的研发团队,涵盖电子、半导体、传感器、芯片封装各个领域的工程师,而我有什么,我就是一个孤单的螳螂,高高举起手臂,却推动不了事业的车轮。但机会总是留给有准备的人,我08年无意间做的一件事情,在2010年我面临人生抉择时,开始发挥作用了。我之前讲过了,和管实验室的老师关系处的很好,实验室的设备我基本都会使用,机缘巧合下,我认识了一个已经毕业的师兄,他回来学校想做一些实验,关于陶瓷材料掺杂和烧结的东西,那两个破设备几乎都没人用,像垃圾一样丢在那里。听说我会使用,已经毕业的师兄找到我,给了我一些样品,让我帮他做实验,我有实验室的钥匙,晚上10点后,其他人都走了,我一个人留下来,一边制作我的薄膜传感器,一边帮师兄做样品。2008年9~12月,4个月的时间,我做了6个批次的样品,师兄很感激我,请我吃了一顿酸菜鱼(还记得工程硕士的同学也请我吃的酸菜鱼吗?),然后给我讲了一个非常精彩的故事,他希望我可以沿着这个方向做下去。我看到命运之神向我招手,他手指一个充满挑战的方向,让我冲上去,挑战未知的困难。师兄给我的样品是一种陶瓷芯片,是非常好的产品方向,07年国家要对机动车实施排放管制,所有汽车都要出厂强制安装,但是当时,全球只有德国、美国、日本三家公司有相关技术储备,中国这一块市场注定被别人垄断。 师兄给的样品正是这个方向,他委托我做的样品,经过几家机构的检测,性能相当不错,接下来,要找到一个合适的代工厂伙伴,把这种陶瓷芯片批量化生产,然后在经过集成化的产品封装设计,把信号传输、热保护、抗振动、机械固定等装置与芯片集成起来,做成终端产品,与发动机匹配使用。国外几家公司的技术严密封锁,核心的芯片工艺没有人能够接触得到,也不会对外销售,只能靠自己做出来。我和师兄只做出了初版的芯片产品,还需要优化才能量产,而且后续还牵涉到封装设计和匹配使用,我们对这些一无所知。想了解全套的东西,只有到一家国外一线的汽车公司的发动机部门去工作,还要正好负责这一块产品的开发,那就可以以客户的身份,了解这些东西。但是说起来容易,哪里有那么正好的机会,一个大牌的汽车公司,正好发动机部门有这个新产品开发的职位空缺?机缘巧合,天赐良机,2010年12月,某一线国际品牌汽车公司,发动机研发部招聘,有个职位专门负责这个新产品的开发,和师兄商量之后,我毫不犹豫的投了简历,干脆利落的被录用了,工资对于刚毕业的博士来说也算不错。不能让我留校,导师为了补偿我,就让我提前毕业了,还帮我争取到了校级优秀毕业生和校级优秀毕业论文,我很感谢他,从他身上学到了很多科学方法和科研精神,让我终生感激。临走时,导师单独和我长谈了一次,他说的一番话,我觉得很好的诠释了博士学位的意义:得到博士学位不是终点,不代表你以后能比别人成功,但博士学位能代表的是,只要你愿意,你可以做好这个世界上几乎所有的、有技术含量的事情。就这样,我告别了博士导师,告别了科研之路,踏上一场未知之旅,但我没有任何犹豫,我要追寻的东西,已经不在校园里了。不知不觉已经8000字了,博士阶段告一段落,但博士毕业后的经历会更刺激。看一个凡人如何在人生路上步步修炼,所有内容都是真人真事,和大家一起分享。2011年开始工作了,工作的前2年,我和刚进入博士阶段一样,每天都在接触和吸收新的东西,由于核心芯片已经做出来了,我很快能理解总成产品的各种技术细节,而且我每天在琢磨的是,如果让我来研发和生产这个产品,我应该如何改进,可以做的更好。 工作2年之后,每次到供应商那里,他们都有点怕我了,因为我提出的问题他们已经开始回答不了,他们不敢在我面前有任何的隐瞒和虚假数据,我对产品了解的深度已经超过了他们。到第3年结束,我觉得差不多了,这个职位已经没有更多可以让我学习的东西了,我和师兄商量了一下,计划第4年离开汽车公司,开启属于自己的事业。 但在离开之前,还有两件事必须完成:1 和代工厂一起努力,把我们核心的芯片产品批量生产的一致性和稳定性的问题解决,成品率提升到90%以上;2 我必须自己组建创业团队,然后去找投资机构,拿到天使轮融资。第1件是技术上的事,总归是能解决的,我们加班加点、多多搬砖,逐渐能达到量产的标准了,但是第2件事,我又开始懵了,刘姥姥又一次进大观园,完全不知道怎么去做,只有查资料慢慢学着准备。我印象深刻的是有一本电子书,叫《给你一个亿,你能干什么》,里面有一个章节叫商业计划书 (BP) 的21条军规,特别感谢作者查立先生,我按着他讲的内容,一步步操作,最终做出了一份当时自认为很满意、但现在看起来比较垃圾的第一版BP。然后我准备各种资料,学着展示产品和性能测试报告,组建技术/质量/采购/生产管理的团队,向别人描绘市场前景,做财务分析,做盈利预测,做现金流量预算,做SWOT,用了1年的时间,在朋友的介绍下,有两个天使投资人的资金到位了,感谢金主爸爸,可以放开手脚干了。2015年我从公司正式离职,最后的一年感觉挺对不起主管,我经常迟到早退,去筹备自己的事情,但主管还是宽容的忍受了我一年,大概也是因为其他人没法专业而深刻的管理这么多传感器产品吧。临别时,请主管和同事们一起吃了一顿酸菜鱼 (第三顿了),主管祝我前程似锦,我祝他以后别再有我这么不听话的下属。然后他好像受到了我的启发,在我走后的3个月,也麻溜的离职,跳槽到杭州湾南岸去了,据说工资翻了一倍还多。2015年,我和师兄的芯片已经可以稳定量产了,我基于前面4年的工作经验并融入很多改进优化,在2016年终于有了完全自己制造出来的总成产品,并趁热打铁申请了一系列专利。2016年又发生了两件影响重大的事情,可能是我在做天使轮融资的时候,讲了太多次BP,导致我的演讲技能大幅提升,讲起项目口才就好的不行。我参加了一个领军人才项目评比,滔滔不绝的向很多专家评委进行介绍,由于产品有独创性,芯片是自主产权,而且市场前景巨大,公司成立一年就开始有销售收入了,项目答辩取得很好的名次,拿了300万政府资助,对创业初期起到了很好的资金补充。当地政府看我这么能讲,产品又好,就怂恿我去参加省里的创新创业大赛,我去了又是一顿讲,拿到了第二名,领导很开心,然后省里市里都给公司奖励,省市的领导接见我,还不停到公司走访,给了很多支持。 他们的态度都很诚恳,能看得出对核心技术、对踏踏实实的创业者那种发自内心的尊重和鼓励,以至于我和他们都成了很好的朋友。我们的目标是一致的,都是希望尽快把产业做大,打破垄断格局,我可以收获成功,他们能得到一家成功的企业,带动就业、带动税收、带动人才的聚集。这种良性互动的局面在中国沿海省份已成普遍现象,让更多的技术型企业成长起来,是中国产业结构转型成功必须要做的事。2016年的第二件大事来了,我记得那天是科技局的一位局长和我约了时间,他说每年科技部都会举办一次全国创新创业大赛,我在省里取得了好名次,他们决定推荐我参加全国创新创业大赛。局长一脸严肃的和我说,已经四年了,他推荐的本地企业,没有一家通过预赛并进入半决赛的。我问大概多少家进行比赛,局长说各省推荐能进入国家赛的,基本是按照每个大的行业1000名,到国家赛的预赛里从1000家企业选出100家进入半决赛,然后半决赛从100家选出10家进入决赛,两轮10进1之后,基本剩下的10家都是非常厉害的企业了,大家再争冠亚军和第三名。我说领导啊,你是不是觉得我比较能吹,才推荐我去参加的,这么残酷的淘汰赛,我也没底啊。领导笑了,说你这样的博士真不多见,技术专家,口才又滔滔不绝,也不紧张,我看好你啊。说不紧张是假的,当我一个人背着包,坐上去参加大赛的高铁时,心里是惊恐不安的。1000名来自全国各地的人才们,把我丢进去都找不到在哪里,完全不起眼。但我在准备PPT的时候,一直回响着博士导师说的话,一篇论文、一份报告、一个项目,最核心的灵魂在于你把创新性讲清楚,最好的创新性不是你比别人做的好,而是你做了别人根本没做过的事情,开创了一个领域。我的产品,放到世界范围肯定不是原创的东西,因为有3家国外巨头公司已经做出来了,但是放到中国范围内,除了我根本没有人做过啊,我决定从这个角度开始讲。比赛时是10个企业分为一组,PPT讲完第一遍,我以小组第一从1000个企业里杀出来,进入到半决赛;PPT讲完第二遍,我再次以小组第一从100个企业里杀出来,进入到总决赛,这时,只剩下10家企业了。出人意料的,是总决赛的前一天晚上,在酒店的会议室里,大赛的带队老师,把我们10家企业都召集在一起,说了一些恭喜的话,让我们加油之类的。然后又很严肃的说,你们10位入选者,都是博士学位,国家想在未来的10年内,从本土选拔近万名专家人才,入选“万 人计划专家”,这也是没办法的事,国家要转型升级,要科技强国,没有专家人才,都是空谈,而这一万多名“万 人计划专家”,就是科技强国的领军者,国家迫切需要你们。这次请你们来开会,就是想告诉大家,你们都是博士,如果明天能进入前三名,科技部有一个大赛通道,把你们推荐到中组部,再经过一轮答辩和评选,就可以入选“万 人计划专家”,希望你们把握这个弥足珍贵的机会。老师的一番话讲完,我看了看其余9个家伙,好像眼睛都发红了,身上已经散发出隐隐的杀气,看起来明天要拼命了。不由得心中一寒,也赶紧假装很有杀气的样子,不能在气势上先输给他们。回想起大家的杀气,我吓得夜里一直睡不着,我总觉得按照之前预赛和半决赛的套路,只讲创新性是不行的,要有一些更打动人的东西。我翻出来几只芯片样品,小小的,拿在手里,那样的不起眼,但又充满了科技感和铜臭味,我盯着它们看了一个小时,我决定,我不再只是强调创新性了,我要讲一个关于芯片的煽情故事。12个小时后,我排在第6顺位出场,听完主持人的转场介绍,我从位置上站起来,伴着场下的掌声上台了,PPT投放在巨大的屏幕上,镁光灯在眼前聚焦,我看不到台下的任何人,我仿佛回到了学校夜晚12点的实验室里,静悄悄的只有我一个人不停的奋斗着。我从口袋里掏出芯片,举在手上,今天,我要给大家讲一个芯片的故事,这个记载了我8年青春回忆的小物件,就这样被我举着,竟然感觉沉甸甸的,我想起了这些年的辛劳、奋斗、挫折、彷徨、希望、喜悦、兴奋、刺激,就在这个时刻,浓缩为8分钟的演讲,与大家分享吧。我已经不记得当时的细节了,只记得自己讲完后,鞠躬致谢,恍惚中看到台下有很多人站了起来,连绵不断的在鼓掌,持续了半分钟还没有停下来。时间定格在10位参赛者都结束了演讲,计分屏上我的名字排在第二位,我揉了揉眼睛,没错,我是总决赛第二名了。科技部信守了他们的承诺,2年半之后,通过新一轮的答辩和选拔,我入选了中组部国家万 人 计划专家,我会和另外一万多名专家一起,成为国家科技强国、升级转型的领军者,在这个古老的国家伟大复兴的道路上,与国外的竞争对手展开厮杀,最终杀出一条血路,加冕为王。我和师兄创办的公司,从16年开始到现在,每年的销售额都以2~3倍的速度增长,后续还会增长的更快,公司的估值今年也到了5亿(是估值,不是个人资产)。但我们依然买不起大别墅,不舍得换保时捷,依然加班到夜里12点以后,依然把公司利润的大部分都投入到研发中去,依然出差的时候不舍得住超过400元的酒店,依然像守财奴一样守着投资人新投入的几千万现金不敢乱花。但我们从学院里那个阿里巴巴宝库学到的东西,还有我们可以手撕麦克斯韦方程组的功底,可以让我们再开发出几款市场容量过千亿的科技产品,可以再组建几个团队,再创办几家估值超过5亿的公司,再开拓几个无人涉及的研究领域。 在美丽的丽江古镇,有一座雪山,终年云雾缭绕,即使在最晴朗的日子,阳光也无法穿透云层。传说只有本领非凡、与山齐高者,才能看到云朵以上的风景。世间虽少有非凡之人,但看过者,无不终生称道绝世美景。不工作的时候,我喜欢叫朋友们一起打王者,一起吃鸡;我喜欢没事就去逗逗狗,撸撸猫,和小朋友一起到处玩;我喜欢所有Alan Walker的歌,不开心的时候就一直听;我和朋友到迪厅蹦迪,吃火锅撸串喝酒,K歌到深夜;情人节还有结婚纪念日,我给老婆买花买钻戒买LV包;我到菜场买菜经常被坑,去小区扔垃圾没分好类会被骂; 身边的人,几乎很少知道我上面讲的那些硕士、博士还有创业的事情,我平时展示的,只是我想展示的,那云朵以下、接连地气、炊烟袅袅的风景。而云朵以上的风景,只有与山齐高者才能看到,你没看见,不代表没有。就像你不会手撕麦克斯韦方程组,没看过杨-米尔斯理论的推导过程,你就不知道杨振宁的伟大紧随麦克斯韦之后,薛定谔和霍金与他相比,都是弟弟。你所看到的,只是他82岁那年,与28岁年轻妻子的传闻。疫情还没爆发的1月初,我去了一位朋友的公司,他是很早就归国的行业专家,在贝尔实验室和西门子待了十几年,办了一家纳斯达克上市的公司,然后回国创业。到公司的时候,他开发的机器人正在惟妙惟肖的讲课,对着摄像头给孩子们网络直播,脸上的表情还可以各种变换,憨态可掬。坐在他的办公室喝茶,桌子上也有个机器人,这位朋友和我聊他在美国的经历,聊自己的最新产品。然后他扶着桌上的机器人,打开一个开关说:“我经常喜欢把机器人比作我们的国家,你看头部的这个处理器代表政治中心,胸部的电池包代表驱动发展的工业动力,双腿是支撑国家的农业基础,手臂代表执法机构,嘴巴代表外交和宣传部门,体内的各种电线电缆代表交通网络,漂亮的衣服、面容和发型代表娱乐业和服务业。”然后他顿了一下,打开机器人的后背开关,里面露出两个像液压挺杆的支撑柱,牢牢支撑着机器人沉重的金属躯体,他指着两根支撑柱说,“搞技术和研发的都在这里了,有了我们,国家可以顶天立地的站着,不用卑躬屈膝,也不需要下跪乞求,我们是脊梁。”如果让我选出人生中最幸福的两件事,那么第一件,是在大学的图书馆里遇见我孩子的妈妈,而第二件,就是选择读博。仅以此文,纪念过往。参考链接 https://www.zhihu.com/question/366627317/answer/1151278214作者系网易新闻·网易号“各有态度”签约作者 — 完 —
分享
1
先马后看