查理·芒格:如何过上痛苦的生活?

在大学毕业典礼上,诞生过很多著名的演讲,如斯蒂夫·乔布斯 2005 年在斯坦福毕业典礼上的演讲,如 JK·罗琳 2008 年在哈佛大学毕业典礼上的演讲。大多数毕业演讲者会选择描述如何获得幸福的生活,而今天的作者查理·芒格(Charlie Thomas Munger)使用 逆向思维的原则,令人信服地从反面阐述了一名毕业生 如何才能过上痛苦的生活。至于那些宁愿继续保持无知和郁闷的读者,建议你们千万别阅读这篇讲稿。

痛苦人生的药方

在我听过的 20 次哈佛学校的毕业演讲中,哪次曾让我希望它再长些呢?这样的演讲只有约翰尼·卡森的那一次,他详述了保证痛苦人生的卡森药方。所以呢,我决定重复卡森的演讲,但以更大的规模,并加上我自己的药方。毕竟,我比卡森演讲时岁数更大,同一个年轻的有魅力的幽默家相比,我失败的次数更多,痛苦更多,痛苦的方式也更多。我显然很有资格进一步发挥卡森的主题。

那时卡森说他无法告诉毕业的同学如何才能得到幸福,但能够根据个人经验,告诉他们如何保证自己过上痛苦的生活。卡森给的确保痛苦生活的处方包括:

  1. 为了改变心情或者感觉而使用化学物质;

  2. 妒忌,以及

  3. 怨恨。

我现在还能想起来当时卡森用言之凿凿的口气说,他一次又一次地尝试了这些东西,结果每次都变得很痛苦。

要理解卡森为痛苦生活所开处方的第一味药物(使用化学物质)比较容易。我想补充几句。我年轻时最好的朋友有四个,他们非常聪明、正直和幽默,自身条件和家庭背景都很出色。其中两个早已去世,酒精是让他们早逝的一个因素;第三个人现在还醉生梦死地活着——假如那也算活着的话。

虽然易感性因人而异,我们任何人都有可能通过一个开始时难以察觉直到堕落之力强大到无法冲破的细微过程而染上恶瘾。不过呢,我活了 60 年,倒是没有见过有谁的生活因为害怕和避开这条诱惑性的毁灭之路而变得更加糟糕。

妒忌,和令人上瘾的化学物质一样,自然也能获得导致痛苦生活的大奖。早在遭到摩西戒律的谴责之前,它就已造成了许多大灾难。如果你们希望保持妒忌对痛苦生活的影响,我建议你们千万别去阅读塞缪尔·约翰逊(编者注:Samuel Johnson,1709——1784,英国作家,文学研究者和批评家)的任何传记,因为这位虔诚基督徒的生活以令人向往的方式展示了超越妒忌的可能性和好处。

就像卡森感受到的那样,怨恨对我来说也很灵验。如果你们渴望过上痛苦的生活,我找不到比它更灵的药方可以推荐给你们了。约翰逊说得好,他说生活本已艰辛得难以下咽,何必再将它塞进怨恨的苦涩果皮里呢。

对于你们之中那些想得到痛苦生活的人,我还要建议你们别去实践狄斯雷利的权宜之计,它是专为那些无法彻底戒掉怨恨老习惯的人所设计的。在成为伟大的英国首相的过程中,迪斯雷利学会了不让复仇成为行动的动机,但他也保留了某种发泄怨恨的办法,就是将那些敌人的名字写下来,放到抽屉里。然后时不时会翻看这些名字,自得其乐地记录下世界是怎样无须他插手就使他的敌人垮掉的。

本杰明·迪斯雷利(Benjamin Disraeli,1804 – 1881,英国保守党领袖,两度出任英国首相)

查理·芒格的四味药

好啦,卡森开的处方就说到这里。接下来是芒格另开的四味药。

第一,要反复无常,不要虔诚地做你正在做的事。只要养成这个习惯,你们就能够绰绰有余地抵消你们所有优点共同产生的效应,不管那种效应有多么巨大。如果你们喜欢不受信任并被排除在对人类贡献最杰出的人群之外,那么这味药物最适合你们。养成这个习惯,你们将会永远扮演寓言里那只兔子的角色,只不过跑得比你们快的不再只是一只优秀的乌龟,而是一群又一群平庸的乌龟,甚至还有些拄拐杖的平庸乌龟。

我必须警告你们,如果不服用我开出的第一味药,即使你们最初的条件并不好,你们也可能会难以过上痛苦的日子。我有个大学的室友,他以前患有严重的阅读障碍症,现在也是。但他算得上我认识的人中最可靠的。他的生活到目前为止很美满,拥有出色的太太和子女,掌管着某个数十亿美元的企业。如果你们想要避免这种传统的、主流文化的、富有成就的生活,却又坚持不懈地做到为人可靠,那么就算有其他再多的缺点,你们这个愿望恐怕也会落空。

说到「到目前为止很美满」这样一种生活,我忍不住想在这里引用克洛伊斯的话来再次强调人类生存状况那种「到目前为止」的那一面。克洛伊斯曾经是世界上最富裕的国王,后来沦为敌人的阶下囚,就在被活活烧死之前,他说:「哎呀,我现在才想起历史学家梭伦说过的那句话,『在生命没有结束之前,没有人的一生能够被称为是幸福的。』」

我为痛苦生活开出的第二味药是,尽可能从你们自身的经验获得知识,尽量别从其他人成功或失败的经验中广泛地吸取教训,不管他们是古人还是今人。这味药肯定能保证你们过上痛苦的生活,取得二流的成就。

只要看看身边发生的事情,你们就能明白拒不借鉴别人的教训所造成的后果。人类常见的灾难全都毫无创意——酒后驾车导致的身亡,鲁莽驾驶引起的残疾,无药可治的性病,加入毁形灭性的邪教的那些聪明的大学生被洗脑后变成的行尸走肉,由于重蹈前人显而易见的覆辙而导致的生意失败,还有各种形式的集体疯狂等等。你们若要寻找那条通往因为不小心、没有创意的错误而引起真正的人生麻烦的道路,我建议你们牢牢记住这句现代谚语:「人生就像悬挂式滑翔,起步没有成功就完蛋啦。」

避免广泛吸取知识的另一种做法是,别去钻研那些前辈的最好成果。这味药的功效在于让你们得到尽可能少的教育。

如果我再讲一个简短的历史故事,或许你们可以看得更清楚,从而更有效地过上与幸福无缘的生活。从前有个人,他勤奋地掌握了前人最优秀的成果,尽管开始研究分析几何的时候他的基础并不好,学得非常吃力。最终,他本人取得的成就引起了众人的瞩目,他是这样评价他自己的成果的:

「如果说我比其他人看得更远,那是因为我站在巨人的肩膀上。」

这人的骨灰如今埋在威斯敏斯特大教堂里,他的墓碑上有句异乎寻常的墓志铭:

「这里安葬着永垂不朽的艾萨克·牛顿爵士。」

我为你们的痛苦生活开出的第三味药是,当你们在人生的战场上遭遇第一、第二或者第三次严重的失败时,就请意志消沉,从此一蹶不振吧。因为即使是最幸运、最聪明的人,也会遇到许许多多的失败,这味药必定能保证你们永远地陷身在痛苦的泥沼里。请你们千万要忽略爱比克泰德(编者注:爱比克泰德出生在希拉波利斯城的奴隶家庭,而且患有终身残疾,他认为所有人都应该完全自由地掌握自己的生活,也应该与自然和谐相处。)亲自撰写的、恰如其分的墓志铭中蕴含的教训:「此处埋着爱比克泰德,一个奴隶,身体残疾,极其穷困,蒙受诸神的恩宠。」

为了让你们过上头脑混乱、痛苦不堪的日子,我所开的最后一味药是,请忽略小时候人们告诉我的那个乡下人故事。曾经有个乡下人说:「要是知道我会死在哪里就好啦,那我将永远不去那个地方。」大多数人和你们一样,嘲笑这个乡下人的无知,忽略他那朴素的智慧。如果我的经验有什么借鉴意义的话,那些热爱痛苦生活的人应该不惜任何代价避免应用这个乡下人的方法。若想获得失败,你们应该将这种乡下人的方法,也就是卡森在演讲中所用的方法,贬低得愚蠢之极、毫无用处。

「以避免失败为目标而成长」

卡森采用的研究方法是把问题反过来想。就是说要解出 X,得先研究如何才能得到非 X。伟大的代数学家雅各比用的也是卡森这种办法,众所周知,他经常重复一句话:「反过来想,总是反过来想。」雅各比知道事物的本质是这样的,许多难题只有在逆向思考的时候才能得到最好的解决。例如,当年几乎所有人都在试图修正麦克斯韦的电磁定律,以便它能够符合牛顿的三大运动定律,然而爱因斯坦却转了个 180 度大弯,修正了牛顿的定律,让其符合麦克斯韦的定律,结果他发现了相对论。

作为一个公认的传记爱好者,我认为假如查尔斯·罗伯特·达尔文是哈佛学校 1986 届毕业班的学生,他的成绩大概只能排到中等。然而现在他是科学史上的大名人。如果你们希望将来碌碌无为,那么千万不能以达尔文为榜样。

达尔文能够取得这样的成就,主要是因为他的工作方式。这种方式有悖于所有我列出的痛苦法则,而且还特别强调逆向思考:他总是致力于寻求证据来否定他已有的理论,无论他对这种理论有多么珍惜,无论这种理论是多么得之不易。与之相反,大多数人早年取得成就,然后就越来越拒绝新的、证伪性的信息,目的是让他们最初的结论能够保持完整。他们变成了菲利普·威利所评论的那类人:「他们固步自封,满足于已有的知识,永远不会去了解新的事物。」

达尔文的生平展示了乌龟如何可以在极端客观态度的帮助下跑到兔子前面去。这种态度能够帮助客观的人最后变成「蒙眼拼驴尾」游戏中惟一那个没有被遮住眼睛的玩家。

如果你们认为客观态度无足轻重,那么你们不但忽略了来自达尔文的训诲,也忽略了来自爱因斯坦的教导。爱因斯坦说他那些成功的理论来自「好奇、专注、毅力和自省」。他所说的自省,就是不停地试验与推翻他自己深爱的想法。

最后,尽可能地减少客观性,这样会帮助你减少获得世俗好处所需作出的让步以及所要承受的负担,因为客观态度并不只对伟大的物理学家和生物学家有效。它也能够帮助伯米吉地区的管道维修工更好地工作。因此,如果你们认为忠实于自己就是永远不改变你们年轻时的所有观念,那么你们不仅将会稳步地踏上通往极端无知的道路,而且还将走向事业中不愉快的经历给你带来的所有痛苦。

这次类似于说反话的演讲应该以类似于说反话的祝福来结束。这句祝语的灵感来自伊莱休·鲁特引用过的那首讲小狗去多佛的儿歌:「一步又一步,才能到多佛。」我祝福 1986 届毕业班的同学:

在座各位,愿你们在漫长的人生中日日以避免失败为目标而成长。■

节选自《穷查理宝典》(中信出版社)第四章,第一讲

JAVA 锁优化 笔记

高效并发是JDK一个非常重要的改进,HotSpot虚拟机开发团队花费大量的精力去实现各种锁的优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,这些技术都是为了线程间更高效地共享数据,以及解决竞争问题,提高程序执行效率。

自旋锁和自适应自旋
共享数据的锁定状态只会持续很短的时间,为了这一段时间挂起和恢复线程并不值得。如果机器有多个处理器,可以让多个线程同时并行执行,我们可以让后面请求锁的那个线程“稍等一段时间”,但是不放弃CPU执行时间,看看持有锁的线程是否很快就会释放掉锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这个技术就是所谓的自旋锁。

自旋锁在jdk1.4.2中已经引入,只不过默认是关闭状态,可以使用-XX:+UseSpinning参数开启,在jdk1.6中已经默认开启了。自旋等待不能代替阻塞,自旋要求有多处理器,自旋虽然避免了线程切换的开销,但是需要占用处理器时间,所以会白白消耗CPU资源,如果自旋时间非常长,就会带来资源的浪费。所以,自旋等待的时间必须有一定的限度,如果自旋次数超过了限定次数还没有成功获取锁,就应当使用传统的方式去挂起线程。自旋次数默认为10次,用户可以使用参数-XX:PreBlockSpin 配置。

jdk1.6引入自适应自旋锁,自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间和锁拥有者的状态来决定的。如果同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行,那么虚拟机会认为这次自旋也很有可能再次成功,进而允许自旋等待相对更长的时间。反之,如果对于某个锁,自旋很少获取到过,那么在以后获取这个锁的时候就会省略自旋的过程。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况会预测的越来越准。

锁消除
锁消除的主要判定依据来源于逃逸分析的数据支持,判断堆上的数据都不会逃逸出去从而被其他线程访问,那就可以吧他们当做栈上数据对待,认为数据是线程私有的,加锁操作就可以忽略。具体可以看StringBuffer 在一个方法内部 定义,作用域只在方法内,锁会安全的消除掉,编译后所有的同步操作就会直接执行。

锁粗化
如果一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体内,就算没有线程竞争,频繁的互斥同步操作也会导致性能损耗。如果虚拟机探测到有这种操作,会把加锁同步的范围扩展到整个操作的序列外,只进行一次加锁和解锁。

轻量级锁
轻量级锁并不是用来替代重量级锁的,本意是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能损耗。主要依赖虚拟机对象头数据,进行CAS操作来实现加锁和解锁操作。[备注:后续文章会详细介绍JAVA 对象的内存布局]。对象头[Mark Word]中有2bit存储锁标志位,1bit固定为0,其他状态(轻量级锁定,重量级锁定,GC标记,可偏向)
01 ——> 未锁定状态 –> 存储 对象哈希码 对象分代年龄
00 –> 轻量级锁定 –> 指向锁记录指针
10 –> 重量级锁定 –> 指向重量级锁的指针
11 –> GC标记 –> 不记录任何消息
01 –> 可偏向 –> 偏向线程ID 偏向时间戳 对象分代年龄

代码进入同步快,如果同步对象没有锁定(01 状态),虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced 前缀,即 Displaced Mark Word),然后虚拟机使用CAS操作尝试把对象的Mark Word 更新指向 Lock Record的指针。如果操作成功线程就拥有了该对象的锁,并把对象Mark Word 的标志位修改为”00″,表示对象处于轻量级锁定状态,如果更新失败,虚拟机首先检查对象的Mark Word是否指向当前线程的栈帧,如果是说明当前线程已经拥有这个对象的锁,那就可以直接进入同步块进行执行。否则,这个锁对象已经被其他线程抢占了。如果有2个以上线程竞争,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志位变为”10″,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待的线程也会进入阻塞状态。

解锁过程也是通过CAS操作的,如果对象的Mark Word 仍然指向着线程的锁记录,那就用CAS 操作把对象当前的Mark Word 和 线程中复制的Displaced Mark Word 替换回来。如果替换成功整个同步过程就成功了,如果替换失败,说明其他线程正在尝试获取该锁,那就再释放锁的同时唤醒其他被挂起的线程。

轻量级锁提升性能主要依据:“对于绝大部分的锁,整个同步周期内都是不存在竞争的”,这只是一个经验数据。如果存在锁的竞争,出了额外的CAS操作,还有互斥量的开销,轻量级锁会比传统的锁性能差。

偏向锁

轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS都不做了。

偏向锁的“偏”,是偏向的意思,锁会偏向于第一个获取到它的线程,如果下面的操作,没有其他线程去获取该锁,则持有偏向锁的线程将永远不会同步。

启用偏向锁:-XX:+UseBiasedLocking jdk1.6默认开启。
第一次获取锁需要把状态标志设置 01 (偏向模式) ,同时使用CAS操作把当前线程ID记录在Mark Word中,CAS操作成功,持有偏向锁的线程每次进入这个锁的同步块时,虚拟机没有任何同步操作(Locking,UnLocking,Update Mark Word)
当其他线程去获取这个锁的时候,偏向模式结束,根据锁对象是否处于锁定状态,撤销偏向后恢复到未锁定(01)或者轻量级锁(00),后续和轻量级锁执行流程一样。

偏向锁可以提高带有同步但是无竞争的程序性能。它同样是带有权衡收益(Trade Off)的优化,也就是说,并不一定对程序有利,如果程序锁竞争比较激烈,那偏向就是多余的,具体问题具体分析,有时候关闭偏向锁性能反而提升了。

LockSupport

concurrent包是基于AQS (AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:

  • Unsafe(提供CAS操作)
  • LockSupport(提供park/unpark操作)

因此,LockSupport非常重要。
两个重点
(1)操作对象
归根结底,LockSupport.park()和LockSupport.unpark(Thread thread)调用的是Unsafe中的native代码:

//LockSupport中
public static void park() {
     UNSAFE.park(false, 0L);
}
//LockSupport中
public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

Unsafe类中的对应方法:

    //park
    public native void park(boolean isAbsolute, long time);
    
    //unpack
    public native void unpark(Object var1);

park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。

与Object类的wait/notify机制相比,park/unpark有两个优点:
以thread为操作对象更符合阻塞线程的直观定义
操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

(2)关于“许可”

在上面的文字中,我使用了阻塞和唤醒,是为了和wait/notify做对比。

其实park/unpark的设计原理核心是“许可”:park是等待一个许可,unpark是为某线程提供一个许可。
如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。

有一点比较难理解的,是unpark操作可以再park操作之前。
也就是说,先提供许可。当某线程调用park时,已经有许可了,它就消费这个许可,然后可以继续运行。这其实是必须的。考虑最简单的生产者(Producer)消费者(Consumer)模型:Consumer需要消费一个资源,于是调用park操作等待;Producer则生产资源,然后调用unpark给予Consumer使用的许可。非常有可能的一种情况是,Producer先生产,这时候Consumer可能还没有构造好(比如线程还没启动,或者还没切换到该线程)。那么等Consumer准备好要消费时,显然这时候资源已经生产好了,可以直接用,那么park操作当然可以直接运行下去。如果没有这个语义,那将非常难以操作。

但是这个“许可”是不能叠加的,“许可”是一次性的。
比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。
继续阅读“LockSupport”

Java ReadWriteLock读写锁的使用

本文将提供Java中的ReadWriteLock和ReentrantReadWriteLock的示例。JDK 1.5中已经引入了ReadWriteLock和ReentrantReadWriteLock。ReentrantReadWriteLock是ReadWriteLock接口的实现,而ReadWriteLock扩展了Lock接口。ReentrantReadWriteLock是具有可重入性的ReadWriteLock的实现。ReentrantReadWriteLock具有关联的读写锁,可以重新获取这些锁。在本文,我们将通过完整的示例讨论ReadWriteLock和ReentrantReadWriteLock。

Lock
JDK 1.5中引入了java.util.concurrent.locks.Lock接口。Lock可以代替使用同步方法,并将有助于更有效的锁定系统。Lock在多线程环境中用于共享资源。Lock作用的方式是,任何线程必须必须首先获得锁才能访问受锁保护的共享资源。一次只有一个线程可以获取锁,一旦其工作完成,它将为队列中其他线程解锁资源。ReadWriteLock是扩展的接口Lock。

ReadWriteLock
JDK 1.5中引入了java.util.concurrent.locks.ReadWriteLock接口。ReadWriteLock是用于读取和写入操作的一对锁。如果没有写锁定请求,则多个线程可以同时获取读锁定请求。如果线程获得了对资源的写锁,则任何线程都无法获得对该资源的其他读或写锁。ReadWriteLock在读操作比写操作更频繁的情况下,效率更高,因为可以由多个线程同时为共享资源获取读锁定。

ReentrantReadWriteLock
JDK 1.5中引入了java.util.concurrent.locks.ReentrantReadWriteLock类。ReentrantReadWriteLock是的实现ReadWriteLock。我们将讨论ReentrantReadWriteLock的一些主要属性。

Acquisition order [获取顺序]
ReentrantReadWriteLock可以使用公平和非公平模式。默认是不公平的。当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量。当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

Reentrancy [重入]

什么是可重入锁,不可重入锁呢?”重入”字面意思已经很明显了,就是可以重新进入。可重入锁,就是说一个线程在获取某个锁后,还可以继续获取该锁,即允许一个线程多次获取同一个锁。比如synchronized内置锁就是可重入的,如果A类有2个synchornized方法method1和method2,那么method1调用method2是允许的。显然重入锁给编程带来了极大的方便。假如内置锁不是可重入的,那么导致的问题是:1个类的synchornized方法不能调用本类其他synchornized方法,也不能调用父类中的synchornized方法。与内置锁对应,JDK提供的显示锁ReentrantLock也是可以重入的。

Lock downgrading [锁降级]
ReentrantReadWriteLock可以从写锁降级为读锁。这意味着,如果线程获得了写锁,则可以将其锁从写锁降级为读锁。顺序为:首先获取写锁,执行写操作,然后获取读锁,然后解锁写锁,然后在读操作之后最终解锁读锁。从读锁升级到写锁是不行的。
继续阅读“Java ReadWriteLock读写锁的使用”

CyclicBarrier 使用不当导致死锁问题

先上代码:


import java.util.concurrent.*;

public class App {

	public static ExecutorService pool = Executors.newFixedThreadPool(5);
	public static ExecutorService pool2 = Executors.newCachedThreadPool();

	public static void main(String[] args) {
		// 使用 CyclicBarrier 出现死锁问题
		// new GenTaskUseBarrier(pool, 10).start();
		// 使用 CachedThreadPool 解决 CyclicBarrier 死锁问题
		// new GenTaskUseBarrier(pool2, 10).start();
		// 使用 CountDownLatch 解决 CyclicBarrier 死锁问题
		// new GenTaskUseCountDown(pool, 10).start();

		// 模拟5个并发共享一个线程池
		// new MockConcurrence(pool, 5).start();
	}
}

class MockConcurrence extends Thread {
	ExecutorService pool;
	int count;

	public MockConcurrence(ExecutorService pool, int count) {
		super();
		this.pool = pool;
		this.count = count;
	}

	@Override
	public void run() {
		for (int i = 0; i < count; i++) {
			new GenTaskUseBarrier(pool, 5, false).start();
		}
	}
}

class GenTaskUseCountDown extends Thread {

	ExecutorService pool;

	int taskSize;

	public GenTaskUseCountDown(ExecutorService pool, int taskSize) {
		this.pool = pool;
		this.taskSize = taskSize;
	}

	@Override
	public void run() {
		CountDownLatch latch = new CountDownLatch(taskSize);
		for (int i = 0; i < taskSize; i++) {
			pool.submit(new MyTask(latch));
		}
		try {
			latch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		pool.shutdown();
		System.out.println("All task done!");
	}
}

class GenTaskUseBarrier extends Thread {

	ExecutorService pool;

	int taskSize;

	boolean autoClose = true;

	public GenTaskUseBarrier(ExecutorService pool, int taskSize) {
		this.pool = pool;
		this.taskSize = taskSize;
	}

	public GenTaskUseBarrier(ExecutorService pool, int taskSize, boolean autoClose) {
		this(pool, taskSize);
		this.autoClose = autoClose;
	}

	@Override
	public void run() {
		CyclicBarrier barrier = new CyclicBarrier(taskSize, new Runnable() {
			@Override
			public void run() {
				if (autoClose) {
					pool.shutdown();
				}
				System.out.println("All task done!");
			}
		});
		for (int i = 0; i < taskSize; i++)
			pool.submit(new MyTask(barrier));
	}
}

class MyTask extends Thread {

	CyclicBarrier barrier;

	CountDownLatch latch;

	public MyTask(CyclicBarrier barrier) {
		this.barrier = barrier;
	}

	public MyTask(CountDownLatch latch) {
		this.latch = latch;
	}

	@Override
	public void run() {
		try {
			System.out.println("线程" + Thread.currentThread().getName() + "正在执行同一个任务");
			// 以睡眠来模拟几个线程执行一个任务的时间
			Thread.sleep(1000);
			System.out.println("线程" + Thread.currentThread().getName() + "执行任务完成,等待其他线程执行完毕");
			// 用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
			if (barrier != null) {
				barrier.await();
			}
			if (latch != null) {
				latch.countDown();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
		System.out.println("所有线程写入完毕");

	}

}

输出结果:

线程pool-1-thread-3正在执行同一个任务
线程pool-1-thread-4正在执行同一个任务
线程pool-1-thread-2正在执行同一个任务
线程pool-1-thread-1正在执行同一个任务
线程pool-1-thread-5正在执行同一个任务
线程pool-1-thread-2执行任务完成,等待其他线程执行完毕
线程pool-1-thread-3执行任务完成,等待其他线程执行完毕
线程pool-1-thread-4执行任务完成,等待其他线程执行完毕
线程pool-1-thread-1执行任务完成,等待其他线程执行完毕
线程pool-1-thread-5执行任务完成,等待其他线程执行完毕

//卡死在这个地方

原因分析:
1.提交了10个任务,但是只有5个调度线程、每次只能执行一个解析任务;
2.前面5个任务执行后由于调用了 barrier.await 被阻塞了,需要等待其他两个任务都达到栅栏状态;
3.前面5个任务的线程被阻塞了,导致没有空闲的调度线程去执行另外两个任务;
4.前面5个任务等待其他两个任务的栅栏唤醒,而其他5个任务则等待第一个任务的线程资源,从而进入死锁状态。

解决方案 [修改Main方法注释 可以逐个测试]:
1、调整线程池调度线程个数,提交多少个任务开多少个资源,如果并发调用的时候共享同一个线程池调度非常容易出现问题,一定要小心
2、使用 CachedThreadPool,或者自定义线程池
3、更换协作工具类为 CountDownLatch,将主线程阻塞直到所有的解析任务都被执行完成。

缓存的使用和设计

原文地址 [本文整理发布]

缓存的一些基本常识

  • Cache(缓存): 从cpu的一级和二级缓存、Internet的DNS、到浏览器缓存都可以看做是一种缓存。(存贮数据(使用频繁的数据)的临时地方,因为取原始数据的代价太大了,所以我可以取得快一些)
  • Cache hit(缓存命中) Cahe miss(缓存未命中)

  • 缓存算法:缓存容量超过预设,如何踢掉“无用”的数据。例如:LRU(Least Recently Used) FIFO(First Input First Output)Least Frequently Used(LFU) 等等

  • System-of-Record(真实数据源): 例如关系型数据库、其他持久性系统等等。 也有英文书叫做authority data(权威数据)

  • serialization-and-deserialization(序列化与反序列化):可以参考:序列化与反序列化(美团工程师写的,非常棒的文章)
    serialization-and-deserialization

  • Scale Up (垂直扩容) 和 Scale out (水平扩容), 驴拉车,通常不是把一头驴养壮(有极限),而通常是一群驴去拉(当然每个个体也不能太差)。

    64258e75-ad29-32ef-ac55-45d95278ff0a

  • Write-through 和 write-behind
    154e49f4-f223-3b4f-9346-fa378dd2efd1

  • 阿姆而达定律:用于计算缓存加速比

  • LocalCache(独立式): 例如Ehcache、BigMemory Go
    (1) 缓存和应用在一个JVM中。
    (2) 缓存间是不通信的,独立的。
    (3) 弱一致性。

  • Standalone(单机):
    (1) 缓存和应用是独立部署的。
    (2) 缓存可以是单台。(例如memcache/redis单机等等)
    (3) 强一致性
    (4) 无高可用、无分布式。
    (5) 跨进程、跨网络

  • Distributed(分布式):例如Redis-Cluster, memcache集群等等
    (1) 缓存和应用是独立部署的。
    (2) 多个实例。(例如memcache/redis等等)
    (3) 强一致性或者最终一致性
    (4) 支持Scale Out、高可用。
    (5) 跨进程、跨网络

  • Replicated(复制式): 缓存数据时同时存放在多个应用节点的,数据复制和失效的事件以同步或者异步的形式在各个集群节点间传播。(也是弱一致性)这种用的不太多。

  • 数据层访问速度
    082dfcc7-1a06-3116-886f-055d4af98cf8
    继续阅读“缓存的使用和设计”

Memcached 总结

总结 memcached 教程 [必看]

Memcached 二三事 [版权归 原文链接,本文修改一些引用资源地址]

实际应用Memcached时,我们遇到的很多问题都是因为不了解其内存分配机制所致,下面就让我们以此为开端来开始Memcached之旅吧!官方wiki

为了规避内存碎片问题,Memcached采用了名为SlabAllocator的内存分配机制。内存以Page为单位来分配,每个Page分给一个特定长度的Slab来使用,每个Slab包含若干个特定长度的Chunk。实际保存数据时,会根据数据的大小选择一个最贴切的Slab,并把数据保存在对应的Chunk中。如果某个Slab没有剩余的Chunk了,系统便会给这个Slab分配一个新的Page以供使用,如果没有Page可用,系统就会触发LRU机制,通过删除冷数据来为新数据腾出空间,这里有一点需要注意的是:LRU不是全局的,而是针对Slab而言的。

一个Slab可以有多个Page,这就好比在古代一个男人可以娶多个女人;一旦一个Page被分给某个Slab后,它便对Slab至死不渝,犹如古代那些贞洁的女人。但是女人的数量毕竟是有限的,所以一旦一些男人娶得多了,必然另一些男人就只剩下咽口水的份儿,这在很大程度上增加了社会的不稳定因素,于是乎我们要解放女性。

好在Memcached已经意识到解放女性的重要性,新版本中Page可以调配给其它的Slab:

memcached -o slab_reassign,slab_automove

换句话说:女人可以改嫁了!这方面,其实Memcached的儿子Twemcache革命得更彻底,他甚至写了一篇大字报,以事实为依据,痛斥老子的无能,有兴趣的可以继续阅读:Random Eviciton vs Slab Automove

了解Memcached内存使用情况的最佳工具是:Memcached-tool。如果我们发现某个Slab的Evicted不为零,则说明这个Slab已经出现了LRU的情况,这通常是个危险的信号,但也不能一概而论,需要结合Evict_Time来做进一步判断。Replacing the cache replacement algorithm in memcached

在Memcached的使用过程中,除了会遇到内存分配机制相关的问题,还有很多稀奇古怪的问题等着你呢,下面我选出几个有代表性的问题来逐一说明:

Cache失效后的拥堵问题

通常我们会为两种数据做Cache,一种是热数据,也就是说短时间内有很多人访问的数据;另一种是高成本的数据,也就说查询很很耗时的数据。当这些数据过期的瞬间,如果大量请求同时到达,那么它们会一起请求后端重建Cache,造成拥堵问题,就好象在北京上班做地铁似的,英文称之为:stampeding herd,老外这里的用词还是很形象的。

一般有如下几种解决思路可供选择:

首先,我们可以主动更新Cache。前端程序里不涉及重建Cache的职责,所有相关逻辑都由后端独立的程序(比如CRON脚本)来完成,但此方法并不适应所有的需求。

其次,我们可以通过加锁来解决问题。以PHP为例,伪代码大致如下:

<?php
function query()
{
    $data = $cache->get($key);

    if ($cache->getResultCode() == Memcached::RES_NOTFOUND) {
        if ($cache->add($lockKey, $lockData, $lockExpiration)) {
            $data = $db->query();
            $cache->set($key, $data, $expiration);
            $cache->delete($lockKey);
        } else {
            sleep($interval);
            $data = query();
        }
    }

    return $data;
}
?>

不过这里有一个问题,代码里用到了sleep,也就是说客户端会卡住一段时间,就拿PHP来说吧,即便这段时间非常短暂,也有可能堵塞所有的FPM进程,从而使服务中断。于是又有人想出了柔性过期的解决方案,所谓柔性过期,指的是设置一个相对较长的过期时间,或者干脆不再直接设置数据的过期时间,取而代之的是把真正的过期时间嵌入到数据中去,查询时再判断,如果数据过期就加锁重建,如果加锁失败,不再sleep,而是直接返回旧数据,以PHP为例,伪代码大致如下:

<?php
function query()
{
    $data = $cache->get($key);

    if (isset($data['expiration']) && $data['expiration'] < $now) {
        if ($cache->add($lockKey, $lockData, $lockExpiration)) {
            $data = $db->query();
            $data['expiration'] = $expiration;
            $cache->set($key, $data);
            $cache->delete($lockKey);
        }
    }

    return $data;
}
?>

问题到这里似乎已经圆满解决了,且慢!还有一些特殊情况没有考虑到:设想一下服务重启;或者某个Cache里原本没有的冷数据因为某些情况突然转换成热数据;又或者由于LRU机制导致某些键被意外删除,等等,这些情况都可能会让上面的方法失效,因为在这些情况里就不存在所谓的旧数据,等待用户的将是一个空页面。

好在我们还有Gearman这根救命稻草。当需要更新Cache的时候,我们不再直接查询数据库,而是把任务抛给Gearman来处理,当并发量比较大的时候,Gearman内部的优化可以保证相同的请求只查询一次后端数据库,以PHP为例,伪代码大致如下:

<?php

function query()
{
    $data = $cache->get($key);
    //说明:如果多个并发请求的$unique参数一样,那么实际上Gearman只会请求一次。
    if ($cache->getResultCode() == Memcached::RES_NOTFOUND) {
        $data = $gearman->do($function, $workload, $unique);
        $cache->set($key, $data, $expiration);
    }

    return $data;
}
?>

Multiget的无底洞问题

Facebook在Memcached的实际应用中,发现了Multiget无底洞问题,具体表现为:出于效率的考虑,很多Memcached应用都已Multiget操作为主,随着访问量的增加,系统负载捉襟见肘,遇到此类问题,直觉通常都是通过增加服务器来提升系统性能,但是在实际操作中却发现问题并不简单,新加的服务器好像被扔到了无底洞里一样毫无效果。

为什么会这样?让我们来模拟一下案发经过,看看到底发生了什么:

我们使用Multiget一次性获取100个键对应的数据,系统最初只有一台Memcached服务器,随着访问量的增加,系统负载捉襟见肘,于是我们又增加了一台Memcached服务器,数据散列到两台服务器上,开始那100个键在两台服务器上各有50个,问题就在这里:原本只要访问一台服务器就能获取的数据,现在要访问两台服务器才能获取,服务器加的越多,需要访问的服务器就越多,所以问题不会改善,甚至还会恶化。

不过,作为被告方,Memcached官方开发人员对此进行了辩护:

请求多台服务器并不是问题的症结,真正的原因在于客户端在请求多台服务器时是并行的还是串行的!问题是很多客户端,包括Libmemcached在内,在处理Multiget多服务器请求时,使用的是串行的方式!也就是说,先请求一台服务器,然后等待响应结果,接着请求另一台,结果导致客户端操作时间累加,请求堆积,性能下降。

如何解决这个棘手的问题呢?只要保证Multiget中的键只出现在一台服务器上即可!比如说用户名字(user:foo:name),用户年龄(user:foo:age)等数据在散列到多台服务器上时,不应按照完整的键名(user:foo:name和user:foo:age)来散列的,而应按照特殊的键(foo)来散列的,这样就保证了相关的键只出现在一台服务器上。以PHP的 Memcached客户端为例,有getMultiByKey和setMultiByKey可供使用。

Nagle和DelayedAcknowledgment的延迟问题

老实说,这个问题和Memcached没有半毛钱关系,任何网络应用都有可能会碰到这个问题,但是鉴于很多人在写Memcached程序的时候会遇到这个问题,所以还是拿出来聊一聊,在这之前我们先来看看Nagle和DelayedAcknowledgment的含义:

先看看Nagle:

假如需要频繁的发送一些小包数据,比如说1个字节,以IPv4为例的话,则每个包都要附带40字节的头,也就是说,总计41个字节的数据里,其中只有1个字节是我们需要的数据。为了解决这个问题,出现了Nagle算法。它规定:如果包的大小满足MSS,那么可以立即发送,否则数据会被放到缓冲区,等到已经发送的包被确认了之后才能继续发送。通过这样的规定,可以降低网络里小包的数量,从而提升网络性能。

再看看DelayedAcknowledgment:

假如需要单独确认每一个包的话,那么网络中将会充斥着无数的ACK,从而降低了网络性能。为了解决这个问题,DelayedAcknowledgment规定:不再针对单个包发送ACK,而是一次确认两个包,或者在发送响应数据的同时捎带着发送ACK,又或者触发超时时间后再发送ACK。通过这样的规定,可以降低网络里ACK的数量,从而提升网络性能。

Nagle和DelayedAcknowledgment虽然都是好心,但是它们在一起的时候却会办坏事。下面我们举例说说Nagle和DelayedAcknowledgment是如何产生延迟问题的:

客户端需要向服务端传输数据,传输前数据被分为ABCD四个包,其中ABC三个包的大小都是MSS,而D的大小则小于MSS,交互过程如下:

首先,因为客户端的ABC三个包的大小都是MSS,所以它们可以耗无障碍的发送,服务端由于DelayedAcknowledgment的存在,会把AB两个包放在一起来发送ACK,但是却不会单独为C包发送ACK。

接着,因为客户端的D包小于MSS,并且C包尚未被确认,所以D包不会立即发送,而被放到缓冲区里延迟发送。

最后,服务端触发了超时阈值,终于为C包发送了ACK,因为不存在未被确认的包了,所以即便D包小于MSS,也总算熬出头了,可以发送了,服务端在收到了所有的包之后就可以发送响应数据了。

说到这里,假如你认为自己已经理解了这个问题的来龙去脉,那么我们尝试改变一下前提条件:传输前数据被分为ABCDE五个包,其中ABCD四个包的大小都是MSS,而E的大小则小于MSS。换句话说,满足MSS的完整包的个数是偶数个,而不是前面所说的奇数个,此时又会出现什么情况呢?答案我就不说了,留给大家自己思考。

知道了问题的原委,解决起来就简单了:我们只要设置socket选项为TCP_NODELAY即可,这样就可以禁用Nagle,以PHP为例:

<?php
$memcached->setOption(Memcached::OPT_TCP_NODELAY, true);
?>

如果大家意犹未尽,可以继续浏览:TCP Performance problems caused by interaction between Nagle’s Algorithm and Delayed ACK。

一次Memcached使用问题

收到群里反馈很多PHP请求执行超时,服务已经不可用。

查看服务器监控数据

发现 CPU 负载 在那段时间一直爬坡。 查询fpm slow 日志 发现请求都卡在了 memcache get 的方法上面。

发现出问题的请求有一个方法 频繁的读写一个key 这个 key 保存的内容 内存已经大于1MB, 一秒内有 500 多个请求,同时读写会导致memcache 的性能急剧下降。

自己写代码重现这个问题:
php 脚本 test-memcached.php:

<?php
$time_start = microtime(true);

$ac = new Memcached();
$ac->addServer('localhost', 11211);

#$ac->setOption(Memcached::OPT_SOCKET_SEND_SIZE, 1024*512);
#$ac->setOption(Memcached::OPT_SOCKET_RECV_SIZE, 1024*512);
#$ac->setOption(Memcached::OPT_RECV_TIMEOUT, 5);
#$ac->setOption(Memcached::OPT_SEND_TIMEOUT, 5);

$data = [];

for($i = 0; $i<30000; $i++){
  $data[] = md5($i);
}

$r = $ac->set('key', $data, 3600);
$set_time_end = microtime(true);
$set_time = $set_time_end - $time_start;
echo "Set Key Done in $set_time seconds \n";
$result = $ac->get('key');
$read_time_end = microtime(true);
$read_time = $read_time_end - $time_start;
echo "Read key Done in $read_time seconds \n";
$time_end = microtime(true);
$time = $time_end - $time_start;
echo "Did nothing in $time seconds\n";
?>

shell 测试脚本:

#!/bin/bash
date
for i in `seq 1 300`
do
{
 php test-memcached.php
 sleep 1
}&
done
wait #等待执行完成
date

分别修改 memcache key 保存内容的大小得到结果:

A:

for($i = 0; $i<30000; $i++){
  $data[] = md5($i);
}

B:

for($i = 0; $i<3; $i++){
  $data[] = md5($i);
}

测试结果:

A:

Did nothing in 18.769073009491 seconds
Did nothing in 14.950340032578 seconds
Did nothing in 19.823235034943 seconds
Did nothing in 20.014719009399 seconds
Did nothing in 20.894814014435 seconds
Did nothing in 20.827455997467 seconds
Did nothing in 15.345555067062 seconds
Did nothing in 18.984731197357 seconds
Did nothing in 21.08841586113 seconds

B:

Did nothing in 0.39238500595093 seconds
Did nothing in 0.42710494995117 seconds
Did nothing in 0.34245300292969 seconds
Did nothing in 0.084210872650146 seconds
Did nothing in 0.41426110267639 seconds
Did nothing in 0.12554693222046 seconds
Did nothing in 0.10487604141235 seconds
Did nothing in 0.60212993621826 seconds
Did nothing in 0.054439067840576 seconds
Did nothing in 0.11286902427673 seconds
Did nothing in 0.69583892822266 seconds
Did nothing in 0.14684200286865 seconds
Did nothing in 0.082473993301392 seconds
Did nothing in 0.34351587295532 seconds
Did nothing in 0.10698294639587 seconds


看结果可以得知 但 memcache 保存的内容变大的时候速度会非常慢,所以使用memcache 的时候要注意缓存值得大小。

memcache 缓存值最大大小是1M 引用 Oracle 网站的一个 FAQ

The default maximum object size is 1MB. In memcached 1.4.2 and later, you can change the maximum size of an object using the -I command line option.

For versions before this, to increase this size, you have to re-compile memcached. You can modify the value of the POWER_BLOCK within the slabs.c file within the source.

In memcached 1.4.2 and higher, you can configure the maximum supported object size by using the -I command-line option. For example, to increase the maximum object size to 5MB:

$ memcached -I 5m

If an object is larger than the maximum object size, you must manually split it. memcached is very simple: you give it a key and some data, it tries to cache it in RAM. If you try to store more than the default maximum size, the value is just truncated for speed reasons.

出现服务不可用的一个重要原因也有写缓存的次数和读缓存次数一样多。

最后修改测试脚本 依然保存很大的缓存值,但是只写入一次,测试并发的读取,基本都可以在1秒左右的读取到数据。

结论:
1.使用memcache的要注意缓存内容的大小,不要保存太大的内容在一个key上
2.缓存是读的次数远远大于写的次数,如果代码中发现写缓存的次数和读缓存的次数一样的时候要提高警惕
3.千万不要在数据库事务未提交的时候删除缓存

QA:
1.为啥使用MD5字符串?
因为memcache会自动压缩内容,如果用普通重复字符串测试效果不明显,线上出现问题的场景也是MD5串的缓存值,可以开启和关闭压缩选项?

测试代码下载