关于Java Memory Model

在这篇文章中我只打算讲一下Causality Requirements for Executions所提到的9条规则中的三条(5、6、7),这也是规范中不太容易理解的地方。当然,在此之前我会介绍一下与之相关的背景。我并不能保证这里所有的论述都是正确无误的,所以大家在阅读时如若发现问题还请指出。

1、背景

Java内存模型是由基本模型演化过来的,主要经历三个模型。

1.1、Sequential Consistency Memory Model

首先考虑的是Sequential Consistency Memory Model,它规定所有线程中的action需要按照一个total order来执行(每次执行时这个total order可以不一致),对于在单个线程中所执行的action,其顺序必须和代码中规定的顺序(program order)一致,并且每一步执行对所有线程来说都是可见的。这个模型太过强硬,要求和program order一致,如果使用这个模型,那么很多优化就做不了了,比如指令重排序以及读写缓存等。因此,需要找到一个稍微弱一点的模型。

1.2、Happens-Before Memory Model

接下来考虑的是Happens-Before Memory Model,这个模型规定了一系列happen-before关系,下面列出其中的一部分:

  • 如果x和y在同一个线程,并且在program order下x在y之前,那么x happen-before y
  • 一个volatile变量的写action happen-before后续对该变量的读action
  • 如果x happen-before y,并且y happen-before z,那么x happen-before z
  • ……

规范指出如果第一个action happen-before第二个,那么第一个action必须对第二个可见且排在第二个之前。看上面列出的第一条happen-before关系,这是不是意味着同一个线程内的指令无法重排了呢?当然不是,规范里面还提到一点来允许重排:

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

除了happen-before关系外,Happens-Before Memory Model还对一个读action(r)所能看见的写action(w)进行了规定:

  • w happen-before r,并且不存在其他的写操作w’满足w happen-before w’并且w’ happen-before r
  • w和r之间不存在happen-before关系

第一点还比较好理解,但是第二点又是讲了什么?在阐明之前先引入一个称作data race的概念,在对同一个共享变量所执行的action(读或写)中,如果两个action之间没有happen-before关系,且至少有一个为写action,那么称这两个action之间存在data race。下面看看去掉第二点就会导致什么,假设有两个线程,一个线程有一个对变量的写action,另一个线程有一个对相同变量的读action,这两个线程之间没有做任何同步,也就是说这两个action之间不存在happen-before关系。只根据第一点,那么这里所提到的读action将永远看不见写action,这是不符合常理的,而引入第二点则允许data race的发生。

关于这两点,举个例子说明下:

init:a = 0, b = 0

Thread1        | Thread2
---------------|----------
1, r1 = a      | 3, r2 = b
2, b = 1       | 4, a = 2

列出有效的happen-before关系如下,将a happen-before b记为hb(a, b):

hb(a  = 0, r1 = a)
hb(r1 = a, b  = 1)
hb(b  = 0, r2 = b)
hb(r2 = b, a  = 2)

根据第一条规则,r2 = b可以看见0,r1 = a可以看见0;根据第二条规则,r2 = b可以看见1,r1 = a可以看见2。也就是说,对于这段代码,下面四种结果在该内存模型下都是允许的:

(a) r1 = 0; r2 = 0
(b) r1 = 2; r2 = 0
(c) r1 = 0; r2 = 1
(d) r1 = 2; r2 = 1

得到其他结果比较简单,对于结果d,我们看看如何执行才能得到它。编译器在分析Thread1时会发现1和2之间并没有相关,所以可以因为某些原因将b = 1提前,执行过2后在Thread2中执行3,接着执行4,最后由Thread1执行1,这样就得到了结果d。

到目前来看,Happens-Before Memory Model没有什么问题,但事实上它是有缺陷的。我们将上面的例子修改如下:

init:a = 0, b = 0

Thread1        | Thread2
---------------|----------
r1 = a         | r2 = b
if(r1 != 0)    | if(r2 != 0)
   b = 1       |   a = 2

此时唯一正确的结果是r1 = 0; r2 = 0。但是在Happens-Before Memory Model下,r1 = 2; r2 = 1也是合法的,因为这里happen-before关系不变,r2 = b可以看见b = 1,这将导致a = 2的执行,然后得到r1 = a = 2,到此已经执行完了,当我们拿个r1 = 2这个结果时,后验的发现这确实将导致b = 1的执行,因此r2看见b = 1也就没问题了,这种循环推理被称作因果关系(causality)。看着都感觉有些荒唐,也就是说,抽象的模型已经无法适应现实的程序了,需要改进。

1.3、Java Memory Model

现在我们遇到的问题在于什么时候可以将一个写的位置提前,规范给出:

An action can occur earlier in an execution than it appears in program order. However, that write must have been able to occur in the execution without assuming that any additional reads see values via a data race.

注:引用来自[1]

前面Happens-Before Memory Model中我们讲了一个读action可以看到的写action满足两个条件,一个是满足happen-before关系,另一个是不存在happen-before关系。而这里要求的是如果一个写action可以提前,那么它必须在仅仅看见与其满足happen-before关系的write条件下会发生,而无须考虑不存在happen-before关系的写action。还是看下前面的例子,在Happens-Before Memory Model中我们将b = 1提前了,应用上面的限制我们会发现,在Thread1中happen-before关系如下:

hb(a = 0, r1 = a)
hb(b = 0, r1 = a)
hb(r1 = a, b = 1)

在这种happen-before的限制下,b = 1是无法执行的,因此它不能被提前。

至此,我们得到了Java Memory Model,Java Memory Model仅仅在Happens-Before Memory Model添加了对causality的限制。

另一个需要考虑的问题是,给出一段程序和一个输出结果,如何判断这个结果是否合法呢?思路是,找到所有的合法结果,看看给定的结果是否在其中。事实上,找到这些结果本身是不可能的,但不妨碍我们对简单的案例进行分析。整个过程分为两步:

第一步,列出所有待提交的action,首先提交初始条件里的action,然后根据上文提到的happen-before限制来递归的提交剩下的action,通过这种方式我们可以找到哪些action一定会发生,因而可以将其提前。

第二步,重新开始一个新的提交过程,列出所有待提交的action,首先提交上一步中收集的可以提前的action(由于提交的action是经过检验的action,因此后续提交的读action是可以看见它们),然后递归提交剩下的action,全部提交完后便会得到一个结果,在提交过程中选择不同的提交顺序便会得到不同的结果。

下面通过一个例子(来自JMM规范制定小组所发布的一系列Test Cases)来简要讲一下流程:

init:x = y = 0

Thread1        | Thread2
---------------|----------
r3 = x         | r2 = y
if (r3 == 0)   | x = r2
  x = 42       |    
r1 = x         |   
y = r1         |    

output: r1 == r2 == r3 == 42

如果要得到r2 = 42,则需要将y = 42提前,那么能否这样做呢?编译器在对Thread1做intra-thread分析时会发现写入x的值要么为0要么为42,如果r3 != 0,那么必然有x = 42,如果r3 == 0,那么接下来就会执行x = 42,所以可以保证r1 = x看到的值为42,因此将其改为r1 = 42,将y = r1改为y = 42并提前。第二步中的提交过程如下:

列出所有待提交的action如下,括号里代表当前可以看到的值,这里省略了初始状态x = 0以及y = 0:

r3 = x  (0)
x  = 42
r1 = x  (42)
y  = r1 (42)	# to be committed
r2 = y  (0)
x  = r2

经过分析,y = 42可以提前,因此首先提交y = r1 (42):

y  = r1 (42)    # committed
r3 = x  (0)
x  = 42
r1 = x  (42)	
r2 = y  (0)     # to be committed 可以看见y = r1(42)
x  = r2

起初r2 = y只能看见happen-before关系的write,也就是y = 0,提交y = r1(42)后,r2 = y现在也可以看见与它无happen-before关系的y = 42,为了得到结果,接下来就提交r2 = y:

y  = r1 (42)    
r2 = y  (42)    # committed
r3 = x  (0)
x  = 42
r1 = x  (42)	     
x  = r2 (42)    # to be committed

提交x = r2:

y  = r1 (42)    
r2 = y  (42)   
x  = r2         # committed
r3 = x  (0)     # to be committed, 可以看见x = r2(42) 
x  = 42
r1 = x  (42)	# to be committed, 可以看见x = r2(42)

把剩下的两个action提交了:

y  = r1 (42)    
r2 = y  (42)   
x  = r2 (42)    
r3 = x  (42)     # committed
r1 = x  (42)	 # committed

到此说明了给出的结果是合法的。

2、Rules

这里不打算列出其他规则,以及规范在上下文中所给出的各种形式化定义,只列出开头所提到的三条规则,然后对其进行分析。如果说前面的流程看明白了,那么这里应该是水到渠成,只是形式化的东西多少显得有些抽象。
5 \(W_i|C_{i-1}=W|C_{i-1}\)
6 For any read \(r\in A_i-C_{i-1}\) we have \(W_i(r)\stackrel{hb_i}{\longrightarrow} r\)
7 For any read \(r\in C_i-C_{i-1}\) we have \(W_i(r)\in C_{i-1}\) and \(W(r)\in C_{i-1}\)

先看rule 6,这一条讲了对于当前正在提交的每一个读action (r),它所能看到的写action(w)都必须满足w happen-before r。我们知道happen-before关系是偏序的,不会得到违反因果关系的结果,这一条已经满足了causality requirements。但是只有这一条还不够,它限制了data race的发生,这不是我们所希望的,这也是引入rule 7的原因。根据前面的描述,我们不能让这个读action看到所有与它有data race的写action,一个自然的想法是当这个读action被提交后,它可以看见于它先提交的写action(justified action),因为既然写action已经提交了,说明会发生,那么这个读action可以看见它就是正常的事情。这样操作会导致一个读action在提交时和最终所看到的写action是不一样的。

事实上,我们可以将这里的\(Ai\)放宽为\(A\),改为\(r\in A-C_{i-1}\),意思是,所有未提交的以及当前正在提交的读action所能看到的写action都应该满足happen-before关系。只有当一个读action被提交后,它才有机会看到与它有data race的写action。

接着看rule 5,这一条不是很容易理解,为什么要对上一步的集合\(C_{i-1}\)进行限制呢?在rule 6中我们讲了,一个读action在提交时和最终所看到的写action可能是不一样的,那么改变在什么时候发生呢?根据rule 5,我们可以看出改变发生在提交读action后的下一个步。rule 5规定了对于上一步所得集合\(C_{i-1}\)中的每个读action,在这一步中它所看见的写action需要和最终它所看见的写action一样,也就是说,如果要做出改变,必须在这一步改变完并且固定下来。

如果将rule 5的限制改为\(C_i\)又会怎样呢?根据rule 6,正在提交的读action所看见的写action与其满足happen-before关系,改变后的rule 5又会限制这个所看见的写action与最终该读action所看见的写action一致,也就是说所有提交的读action永远都只能看见与其满足happen-before关系的写action,这依然禁止了data race的发生。

事实上,我们过去过来就在讲同一件事,一个读action在什么时候能看到与其有data race关系的写action。

前两条弄明白了再看rule 7就比较简单了。rule7包含两条规则:

  • \(W_i(r)\in C_{i-1}\)规定了rule 6中满足happen-before的写action必须已经提交过了。
  • \(W(r)\in C_{i-1}\)规定了读action最终所看见的写action也必须已经提交过了,这里有两层意思:
    第一、如果读action所看见的写action不变(即一直是当前这一步中所看见的满足happen-before关系的写action),那么这两个规则表达的是同一个意思,即该写action必须已经提交了。
    第二、如果读action所看见写action会变,根据rule 5,当前所提交的写action在下一步可以改为看见满足data race的写action,但是该写action必须在上一步已经提交过了。

总之,rule 7的两条限制了一个读action所能看见的写action必须在它之前提交。

再来看rule 7的第二个规则。这个规则使用了\(W(r)\),表达的是最终的结果,这里并没有限制改变会发生在哪一个步,根据rule 5我们可以得出改变发生在下一步,因此我们可以将 \(W(r)\in C_{i-1}\)改为\(W_{i+1}(r)\in C_{i-1}\)。事实上,进一步分析可知,当前为第i步,rule 5在这里的上下文中所讲的是下一步i+1,因此在当前这一步中可将rule 5写为\({W_{i+1}|C_{i}=W|C_{i}}\),由于\(r\in C_i-C_{i-1} \in C_i\),所以有\(W_{i+1}(r)=W(r)\),进而得出\(W_{i+1}(r)\in C_{i-1}\)。

3、举例

这一部分根据上面几条rules的描述来检验一下完整的提交流程,所使用的例子在前面出现过,为了方便,这里重新贴出来:

init:x = y = 0

Thread1        | Thread2
---------------|----------
r3 = x         | r2 = y
if (r3 == 0)   | x = r2
  x = 42       |    
r1 = x         |   
y = r1         |    

output: r1 == r2 == r3 == 42

提交流程如下表所示,Action即被操作的对象,Value指当前所写或所读的值,Commited In指明Action是在哪一个步中被提交的,Final Value In指明在哪一步中Action所看见的值被固定下来。

Action    | Value | Commited In | Final Value In | index
----------|-------|-------------|----------------|-------
x  = 0    | 0     | C1          | E1             |   1
y  = 0    | 0     | C1          | E1             |   2
y  = 42   | 42    | C1          | E1             |   3
r2 = y    | 0     | C2          | E2             |   4
r2 = y    | 42    | C2          | E3             |   5
x  = r2   | 42    | C3          | E3             |   6
r3 = x    | 0     | C4          | E4             |   7
r3 = x    | 42    | C4          | E5             |   8
r1 = x    | 0     | C5          | E5             |   9
r1 = x    | 42    | C5          | E6             |   10

根据前面的分析y = 42可以被提前。一开始集合为空集,此时提交r2 = y并不能使其看到42。所以第一步需要提交y = 42以及初始化的两个action,第二步提交r2 = y,注意第4行中Value为0,因为根据rule 6此时r2 = y只能看见与其满足happen-before的值,也就是y = 0。根据rule 5和rule 7,在第三步中我们可以让r2 = y看见y = 42,除此之外,第三步还必须再提交其他的action,不然上一节所提到的几条rule会因为i不满足关系而失效,因此在第三步中我们提交x = r2。从第四步开始我们回到Thread1,首先提交r3 = x,同前面r2 = y的分析,此时r3 = x还只能看见x = 0。接着在第五步中让r3 = x看见x = r2写入的值,并提交r1 = x,同样的道理,提交时只能看见满足happen-before关系的写action,虽然x = 42 happen-before r1 = x,但是根据rule 7,一个读所能看见的写必须先行提交,由于x = 42并未提交,所以r1 = x此时仍然只能看见x = 0。在最后的第六步中,我们将r1 = x修改为可以看见x = r2写入的值,也就是42。到此便完成了全部提交。

参考:
[1]:The Java Memory Model by. Jeremy Manson
[2]:Causality Test Cases
[3]:The Java Language Specification
[4]:The Java Memory Model: a Formal Explanation

读《最好金龟换酒》

现在已经夜里两点半了,正值盛夏,我居住在一个大城市边缘的一栋水泥楼房的顶层,没有空调,因为睡不着觉就把这本书给读完了,但考虑到明早要上班,所以我必须在半个小时内将该说的话说清楚。

姑娘文笔还不错吧,字里行间透露出作者曾经读了一大堆文学名著和历史典籍,又是干金融的,对屌丝们来说简直就是女神了。一开始我就不喜欢她的腔调(我对女神有成见),你是不是因为生活枯燥找不到存在的意义而出去旅游这件事不是我关心的,但跨越大半个地球跑到美洲国家吃呀玩呀,顺便整几个历史典故然后发一顿牢骚给大家普及一些众所周知的常识这种行为是让我所不能容忍的。要不是看在姑娘长的还不错的份儿上,我就要开阶级炮了。

但我继续读下去时发现了一些不一样的东西。这让我想起了之前给我爹推荐电影一事,我揣摩着他的口味用心推荐很多片子,看了一会他就说不好看,大片也不好看,嫌它们啰哩啰嗦的,这让我很苦恼,他希望全片都是高潮,都曾喷射状。看来女人也满足不了他,好莱坞更是靠边站了。在更早之前我也有过类似的看法,有的片子就是如此的枯燥乏味,不过,如果用心去看,或者仅仅不抱着敌视的眼光去看,就有可能发现其中好玩的东西,不过,有可能是流出来的,而不是曾喷射状。

作者后来在山上学西班牙语那一段确实感动了我,姑娘绝对有着一颗悲天悯人的心,虽然不能给现实带来颠覆性的改变,但至少可以感化一部分铁石心肠,让一另部分人活的更安心。我向来不认为养尊处优的城里人能够在面对贫困人群时始终保持着一种平等和体谅的心态,大部分人考虑自我要更多一些。但是作者会因为向当地孩童不小心透露了手机价钱并惊吓到对方时感到惴惴不安,并且懂得隐藏在城里吃了上好冰淇淋的事实(好像也没隐藏,但是也感到懊悔了)。这些小细节如萤火一般幽浮于作者接触的人身边,照亮着他们,这或许就是人性中美好的一部分(人性:The Good Parts)。

虽然姑娘讲的大部分道理和体会对我来说没什么用,因为很多对于我来说都像常识般的存在,或许这表明我和作者的价值观在某种程度上是一致的。这样一想,再读此书时心态明显好了许多,不再持着批判的态度来审视里面的文字。上一次读游记还是在七八年前上高中时读的余秋雨跟凤凰台去非洲时写的日记,于飞沙走石中爬在滚烫的车盖子上就开始感天慨地的写法让我至今心生厌恶,目光短浅的我还未从上次的打击中完全恢复过来以至于还不能体会到游记真正的价值所在。可能有机会要重读此书。

姑娘给了我一种很精致的感觉,像python代码一样,生活得错落有致,不然就会出bug。我开始喜欢上姑娘了。

再谈谈书中提到的于我来说好玩的地方。由于某些原因我对拉翔有着一种天然的敏感,读周国平的书一大堆,目前就只记得他说过一句类似下面的话:拉翔是人类一件很私人的事情,不能随便展示出来。经过多少年的历练,我已经恢复的差不多了,可以和人类正常浅谈拉翔之事,可是,姑娘在罗赖马山上对拉翔一事绘声绘色的实描还是让我有点惊慌失措,特别是他老公用她使用过的翔袋这一行为被她描述为真爱时,我预感这将如晴天霹雳般划刻入我的脑海。多年后,再谈起这本书,浮在脑子里的估计就是那份双人翔袋了。

姑娘的表达能力毫无疑问是非常不错的,遣词造句能力是一流的,这给我释放了巨大的想象空间。而且文笔也小有幽默,说不定这几个小时是我今天最快乐的时光了,哇哈哈~

感谢傅姑娘,祝你幸福!

注:此文写于2015-08-04 02:44

谜一样

这半年,我不知该看往何处。曾经有过比现在更迷惘的时刻,却不曾像现在这般每天都焦虑不安,假期里,整天整天的感到手足无措。这个世界对我来说,又变得,像谜一样。

这他妈就是矫情!

这是我所担心的,也正因为如此,我几乎从未和任何人谈论过我的真实想法,我怕被别人这样认为。这些想法琐碎而又毫无意义,我努力的控制着它们,以保证在和其他人对话时不会冒失出现,在这种反复纠缠中我变得更为躁郁不安。我甚至知道这一切的来由,但始终没办法将其剔除,我试了又试。

在这里我似乎有了自由的理由,可以尽情抒发内心的挣扎,换得片刻安宁。然而,我依然不敢大写特写,因为我深知与人类浩瀚的历史上那些震撼人心的困难相比,我的则渺小到尘埃里,我那幼稚的自卑感也充分体现在我所面对困难的尺度上,我没有勇气将其写出,哪怕是在不起眼的角落。

我对未知世界的渴求远远超出了我目前的探索能力,我曾想自底而上系统的了解我所在意的领域,由点及面,如同雨滴击打水面时荡起的四下而去的波纹,波纹所及便是我所了解的知识,假以时日,我便可以吞下整个海洋。然而,我的狂妄准确的印证了我一贯的无知,对于我自己来讲,这完全是一种吃力不讨好的行为,即使这样做到老也不过是这个领域的百科全书,可是,光看百科全书你连麦子都种不出来。何况,根本就做不到。

被一个困难击倒后,我便会想出另一个不可能完成的任务,并为这个绝妙的设想而感到振奋,辗转反侧,不眠不休,尽管事实上它华丽而空洞。用不了多久,我便又缴械投降了。就这样反反复复,无休无止。在这种反复自我创伤的过程中,我疲惫不堪,我甚至隐隐感到,是我,一直在不怀好意的为难自己。可是,为什么要这样做呢?

我静静地坐在那里,思考着这些问题,窥探着时间穿过胸膛时所留下的第四维的剪影,忽然发现,一个如此连续的我和多年前相比显得如此分裂,要不是这副躯体,我何曾是我!

时间在老去,不曾停歇,我如同一个接近三十岁的女人整天为年龄所忧虑着,怕岁月撕毁容颜,却做不出一个像样的打算,这便是问题所在。我不想成为某一类人,但同样我也害怕无法成为另一类人,否则我就迷失了自己,像陈列在博物馆里的展船,这不是一个好的归宿。

在这个超级复杂的系统里我活的小心翼翼,谨慎地看待发生的一切,试图分辨着什么,但却依然感到活的不明就里。可是,那些所谓的为了崇高理想而活说到底又怎么能不算另一种不明就里呢?

如此而已。

请把你的微笑留下

一首歌上来,像喝了一口烈酒,胃里翻江倒海,惦记着,我他妈到底什么时候才能够正经地写一篇记叙文,白描也行,没有味道也不要紧,重要的是要打破高中语文老师给我下的该死的死咒,这死咒箍的我头疼,箍的我蛋疼,箍的我一天到晚傻不拉几的,如冰柜里雪白的裸体鸡,脖子老弯向下体。

心里难平我便会看看星空,好让自己明白,无论我傻缺到天上还是傻缺到地里,都不重要,相对这浩淼的宇宙,地球都显得格外傻缺,上面的人类就不提了。如果那时是白天或者是晚上但有浓云,我就试着回忆对着银河数星星的日子,不一样的方法,但有着相同的目的。那时正值秋夜,没有奶奶温柔的怀抱,我蹲在厕所涨红了脸,汗流通背,咬着牙,攥紧拳头,奋力苦战最后几分钟,死命把大便往直肠前面拱,差不多时,突然卡壳了,我临危不乱,大吼一声,把拳头大小的屎厥子逼出我的身体。由于太过用力,导致心跳加速,眼睛开始迷乱起来,一抬眼便看到了猎户座,这名腰里别刀的野男子,老师说那是刀,专门砍偷他女人的男人和女人,但后来我又听到了其他说法,一说是剑,一说是腰带,但都没有和我想的一样的,我怎么看怎么觉得那像斜伸的大屌。快二十年过去了,我依然只认识猎户座,每次我和姑娘指出哪个是猎户座,她们便说,哇,阿贵好厉害,真浪漫……再用个什么星图之类软件,她们都要飞升起来,恨不得把我揽入怀抱,然后拍死,接着钉入猎户座,成为她们永久的星座。猎户座是我肛门永远的红肿,面对整个星空,我就只认识这个红肿星座,从头到尾一点都不浪漫,这说明浪漫都是用来骗傻缺的,都是一群什么人啊。

刚在听的歌是《把悲伤留给自己》,老歌,第一次听时比接触红肿星座还要早。当时听不懂,只觉得开头的口琴很好听,但至今我不会吹像样的曲子,我什么乐器都不会,我什么都不会,有人曾经跟我说你不会弹琴不会画画不会写诗不会搞对象,怎么还有勇气活下去。我说,我会吹竖笛。他说我故意要激怒他,然后疯狂的反驳,说,你傻缺,你有毛病,竖笛是屎,竖笛是癌,竖笛是你傻缺的命脉,你得癌了,你要去医院接受治疗,最好开个刀,在脑壳子里。这种说法来得早,扎的深,我开始认为搞音乐的搞艺术的都屌炸天,有气质,个个忧郁,能够徒步千里不拉屎。后来,我发现了真相,大部分人都傻缺,搞艺术的也不例外,该傻缺的比傻缺还傻缺,都是一群什么人啊。当然,我真喜欢那些真屌炸天的,真有气质的,真忧郁的,真徒步千里不拉屎的,你们都是我心中的偶像,血液里的活佛,你们丰富了我的生活,让那些虚伪的傻缺滚一边去吧。

前两天刚重听了另一首歌,一个和《把悲伤留给自己》这首歌的作者搞过暧昧同时也是他徒弟的一名女歌手唱的,这首歌最开始是在电影《天下无贼》中听到的,歌名叫《知道不知道》。曲子很老了,但新填的歌词很动人,每每听来,便如漫步于夏日的海边,沐浴着晚风,层层涌起小浪涛轻轻拍打我的小心肝,一不小心泪腺便失禁了,哗啦啦的流个不停。

我在想为什么没有暧昧这种单独的感情,不是说不存在,而是为什么不能被正常的广大人民群众所接受,现在不是讲究专业精分嘛,英特尔芯片流水线都搞到逆天的24步了,为什么可接受的感情还这么二进制。让我们来研究研究暧昧,什么叫暧昧,初次相遇,最好是偶遇,电梯,花园,咖啡馆入口处等旮旯,空间越逼仄越好,都有老公老婆男友女友就更好,聊了两句,甚是欢喜,没想到在这傻缺遍地的地方竟能遇到活宝,遇到活宝后其他人就更傻缺了。聊的越开心,接下来暧昧的程度越大。聊星座,聊书,聊电影,聊经济,聊历史,最好都能聊聊偏冷门的东西,这将极大的丰富你在对方眼里不是傻缺的可能,没有什么他妈是不能聊的,连对话本身都可以聊。聊着聊着便开始图不轨了,想把对方捧入手心,变成自己的红肿星座,想一直聊下去,聊到睡着,醒来后接着聊,大便的时候也不怕熏到对方。临走时,约好,下次接着聊,最好是一个新的地方,最老的地方是分手用的。晚上睡不着,旁边有自己的老公甚至有孩子也不踏实,就是睡不着,开始操心红肿星座怎么样了,于是就发短信问,你睡着了吗?短短几个字,融入无限的温柔,老公做梦都想不到的温柔,像彗星的彗尾,弥散在夜空中。十分钟没回,二十分钟还没回,开始流汗,坐起来,挠挠头,重新对了下号码,发现写错了,开心极了,仿佛收到了红肿星座的回复一样,一开心,便又睡着了。第二天见了面,先矜持,仿佛隔了数年未见似的,女方用手把头发撩到耳朵后面,怎么温柔怎么来,男方则礼貌大方,双方开始聊天,双方开始傻笑,聊天和傻笑是并行运算的。仿佛每句话都饱含笑点,别人都用看傻缺的眼神看着你们,你们无视他们。聊的内容不能和昨天太重合,这意味着你得懂很多,不然就像是干射,很容易被对方识破你就那么点存货,女方会因为这点而向你脸上泼咖啡——臭流氓,男方会借着去洗手间的时侯从后门偷偷溜走。总的来说,这一次又聊的很开心,和昨日相比开心程度有所减少,但深入的知足感又弥补了这一点,甚至升华了这一感觉,你们感受到了很多,那种砰砰砰心跳的感觉真是奇妙,像鸦片,吸食之后觉得对方金光闪闪,彼此像虎符的另一半,咬合程度精确到爆,你想和他或她融为一体了,你想开始不要脸了,当然,也只是想。你们像是站在高海拔缺氧的山脊看着纷乱的一切,笑着一个个男男女女找不到另一个个男男女女,你们怎么就这么幸运,就这么仓惶的相遇,虽然太晚又太早,为此,你们有时候都恨自己。正聊得开心时,这时女方接到一个电话,是老公打来的,我今天有会,你晚上去接一下孩子,声音有点大,对面的红肿星座也听到了,露出傻缺的表情,这一瞬间你们感觉像是被剥光了衣服,赤条条的站在高海拔缺氧的山脊。后来,你们一起吃饭,你们一起看电影,你们一起杀人一起放火,你们一起干了所有你们认为不应该干却又不得不干的事情。后来,你们相互发现也就这样了,也就只能这样?你们望着灰色的天空,无助地如此安静,细细的回忆发生的一切,聊天再有深度又能如何,能发论文不?一开始再开心又如何,是可持续发展吗?如果不是可持续发展,能很快就死掉吗?当又开始想和其他人搞暧昧时怎么搞?怎么搞?如果和最开始的那个没分手就尼玛四角恋了,打麻将都够凑一桌了,你们开始觉得自己怎么就这么贱这么傻缺呢,你爱我我爱你你不爱我我不爱你这些乱七八糟的一切都有什么鸡巴意思?当然,这个很难想得通。后来,你们突然发现,暧昧不好,暧昧不好,二进制最完美,要么爱,要么滚蛋。

我睡不着,看书也睡不着,看我看不懂的书也睡不着。最近有很多五年前八年前就应该思考的问题都灌入我的大脑中,来的更猛,持续的更久,我没办法,只得起身迎战。如今不是秋天,没有红肿星座可看,天空雾霾严重,连站牌都看不清,更不用谈彩霞和星空了。这些问题具体我说不清楚,都是一些关于人,关于风,关于脚趾头,关于农夫山泉,关于一切的鸡巴问题。有的人想通了,然后出家了,有的人想不通,然后也出家了,有的人还没想就跳楼了,有的人醉了,有的人疯了,有的人谈了17场恋爱,每场都如初恋,有的人搞科学,在成就梦想的时候也成了刽子手,成了刽子手后又大喊我不要当刽子手我不要当刽子手,有的人在搞学术,有的人自认为在搞学术,有的人打扫大街,日复一日,有的人吃包子,碗口大的包子,一顿5个,有的人把污水偷偷排入江里,他们夜观星空也发现了自己的渺小,有的人说谎,有的人不说谎却傻缺的可以,有的人感慨今天好多人从他身边擦身而过,明天相同的人几乎没有,于是开始伤感,有的人旅游,大谈旅游很好玩,脱困解乏,比大保健来的还要巧妙,有的人不开心却说不出原因,有的人瞧不起别人,有的人瞧不起所有人,有的人想干一票就走,有的人想干十票而名垂千古,有的人是人,有的人不是人,人不是人。而这天我什么也干不了,我只能在酷暑中流汗,在酷暑中喝水,在酷暑中大便,躺在床上,我想着生想着死,我想,对于别人来说,我也只是有的人。

下面这首《歌声与微笑》是我能够正确识别红肿星座的前几年学的,教我们唱歌的是一个顶着锅盖头的女同学,很单纯,虽然我曾经知道单纯为何物,但是,聪明的,你告诉我,我们的单纯为什么一去不复返呢?

请把我的歌带回你的家。
请把你的微笑留下。
请把我的歌带回你的家,
请把你的微笑留下。
明天明天这歌声,
飞遍海角天涯,飞遍海角天涯。
明天明天这微笑,
将是遍野春花,将是遍野春花。

「读知乎」修复版

自从我把系统升级到MIUI 6(擦,暴露了)后「读知乎」就不能用了,每次进去之后都像一坨小鸡那样卡死在那里。原来的项目因为版权问题已经停止更新了,因此,我擅作主张修复了一些让我不愉快的bug,并增加了图片放大功能。下面的apk是没有签名的,所以要安装得先删掉之前的官方版本。点击下面图标下载。

歌词字级同步——ProgressText

天天动听里的歌词是字级同步的,在Android中如何实现呢?首先它的字有一层不同颜色的描边,另一方面随着时间推进一种颜色会逐渐代替另一种颜色。

对于第一个问题把 TextView 中的 Paint 改成 getPaint().setStyle(Paint.Style.FILL_AND_STROKE) 是没有用的,因为使用的是和字体相同的颜色来描边。所以这里采用了一个笨办法,真是笨办法,但比用阴影、设置一大一小叠字要好:-)

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    setTextColor(strokeColor);
    getPaint().setStrokeWidth(strokeWidth);
    getPaint().setStyle(Paint.Style.STROKE);
    super.onDraw(canvas);
}

先把字画出来,然后用不同的颜色描下边就可以了。

对于第二个问题要使用到 BitmapShader ,它继承自 Shader,文档里这样描述的:

A subclass of Shader is installed in a Paint calling paint.setShader(shader). After that any object (other than a bitmap) that is drawn with that paint will get its color(s) from the shader.

Paint 绑定 Shader 后,用它画出来的东西颜色都是从 Shader 中来取,Shader 的大小可能比所要画的物体要小,就如铺地板一样,地板总比地面要小,所以 Shader 中引入三种铺砖模式:

  1. CLAMP——用Shader边上的颜色来画超出的范围,原地板是[l,M,r],铺出来是…[l][l][l,M,r][r][r]…
  2. REPEAT——按正常情况下重复,原地板是[L,R],铺出来是[L,R][L,R][L,R]…
  3. MIRROR——边镜像边重复,原地板是[L,R],铺出来是[L,R][R,L][L,R]…

这里设置一个2个像素的BitmapShader,左边和右边像素颜色不一样,设置x方向的铺砖模式为CLAMP,y方向的铺砖模式为REPEAT,然后移动 Shader ,移动可以通过设置 Matrix 实现:

shader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, 
                                  Shader.TileMode.REPEAT);
matrix.setTranslate(pixels, 0);//左右移动pixels
shader.setLocalMatrix(matrix);

最后在先画 Shader,然后去掉 Shader 给字描边:

@Override
protected void onDraw(Canvas canvas) {
    getPaint().setStyle(Paint.Style.FILL);
    getPaint().setShader(shader);
    super.onDraw(canvas);

    getPaint().setShader(null);
    setTextColor(strokeColor);
    getPaint().setStrokeWidth(strokeWidth);
    getPaint().setStyle(Paint.Style.STROKE);
    super.onDraw(canvas);
}

具体实现见https://github.com/withparadox2/ProgressText,首先添加dependencies。
在Layout里声明:

在头部添加:xmlns:custom="http://schemas.android.com/apk/res-auto"
<com.withparadox2.progresstext.ProgressText
    android:id="@+id/progress_text"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textSize="50sp"
    custom:after_progress_color="@android:color/holo_purple"
    custom:before_progress_color="#FFFF00"
    custom:stroke_color="@android:color/black"
    custom:stroke_width="1px"/>

然后就可以设置进度了:

textView = (ProgressText) findViewById(R.id.progress_text);
textView.setText("绿岛小夜曲");
textView.setProgressBypercentage(0.68f);
//textView.setProgressBypixels(150);

效果如下: