2018年11月17日星期六

Practical Vim 读书笔记

刚工作的时候,看了王垠的一篇《编辑器之神和神之编辑器》,便落入了使用vim的深坑。工作了那么多年发现每个一段时间重新看vim东西总能学到新的知识,最近快速读完了《Practical Vim》,在这里把一些以前没注意的小技巧再重新整理一下。

normal mode

  • R进入逐字替换
  • f{char}搜索某个字符(比如c),按;可以跳到下一个c位置,按,跳到上一个c
  • 使用相对行号时,如何删除当前行开始向下数3行的内容? d3j
  • q/ 查看使用/搜索过的历史命令
  • q: 查看 ex 命令历史
  • m{char} 标记位置,`{char} 调整到标记位置,其中{char}小写字母是文件内标记,用大写字母是全局标记(可以在文件之间跳转)
  • **0(数字0)**是复制专用寄存器,只有y命令会覆盖它,使用y命令后,"0p可以黏贴寄存器内容。覆盖无名寄存器的操作(x,c,d等)不会覆盖0寄存器

insert mode

  • <C-r>{register} 直接输出寄存器的值,比如<C-r>a是输出a寄存器内容
  • <C-r>=6*35<CR> 使用表达式寄存器=,计算6*35的值并输出

virtual mode

  • v高亮选择文本,gv重选上次高亮选区
  • 高亮选中文本后,按o可以将光标在高亮文本起始末尾跳转,方便调整高亮文本

Ex command mode

  • [range] m {address} 把[range] 范围的文本移动到{address}
  • [range] t {address} 把[range] 范围的文本复制到{address}
  • <C-r>{register} 在命令行黏贴寄存器内容
  • [range] normal {command} 使用命令行执行普通模式命令,如 % normal A; 会在所有行后面追加字符**;**
  • 重复上一次执行的Ex 命令,使用 @:
  • <C-d> 显示可用命令列表,Tab补全命令
  • <C-r><C-w>插入当前光标下的单词到编辑的command中
[range] 范围语法支持:
符号地址
1文本第一行
$文本的最后一行
0虚拟行,位于文本第一行上方
.光标所在行
'm位置标记m所在行
'<高亮选区的起始行
'>高亮选区的结束行
%整个文件
  1. 允许使用+{number}-{number}对[range]进行偏移
  2. [range] 也支持使用模式匹配

Macro (宏)

录制宏:
  1. q{register} 开启录制
  2. 录入宏的内容
  3. q结束录制
调用宏:@{register}
批量调用: n@{register} ,这里n是执行宏的次数
向录制好的宏追加内容: 用 q{register大写} 开始录制,录制后的内容会追加到原来的宏后面。比如原来用 qa 录制了 dwi 操作,想要再后面加j,可以:
  1. 用 qA 开启宏a的追加
  2. 录制j
  3. q结束录制
  4. 此时@a执行的操作就是dwij
编辑宏:
宏用的寄存器和复制黏贴的寄存器是一样的,需要编辑好录制的宏,可以先将寄存器输出,编辑后复制到寄存器中,继续用@{register}执行宏。比如原来寄存器a录制的宏是dwij,现在想要改成dwi2j,可以:
  1. 在normal mode下,"ap 黏贴出宏内容 dwij
  2. 将内容改成 dwi2j
  3. 选中文本,y"a 覆盖寄存器a内容
  4. 再执行@a,操作就是 dwi2j了

Pattern 模式匹配

vim 的 search(搜索),substitute(替换)命令都支持Pattern,也就是正则表达式匹配
默认情况下,/ 搜索模式的正则表达式,正则表达式的元字符 .*()等,都需要加\转义,也就是在默认情况下/.*搜索的是 .*字符串,而不是任意个字符
magic:
在搜索表达式前加\m使用magic功能,除了 $.*^之外的元字符要加反斜杠,如\m.*搜索的就是任意字符,若不是.*字符串
very magic:
在搜索表达式前\v使用very magic 功能,正则表达式任何元字符都不用加\,如:/\v(a.c){3}$ 就是查找行尾三个字符是a{char}c的表表达式(如abc,acc,aec)。
强制关闭magic:
如果已经set magic,要在搜索表达式里强制不用magic,就在前面加\V,比如已经开启了set magic,使用 /\V.*搜索的是.*字符串。
单词界定:
在very magic 模式下,使用<word>可以以单词为界匹配字符,而不会匹配到其他内容。比如目前有个文本:
abc aa
abcdedf
1234
123abc
用 /\v<abc>搜索时,只有第一行 abc 匹配,123abc不会匹配。
模式匹配边界:
一个匹配的边界通常对应于一个模式的起始与结尾。但我们可以使用元字符 \zs 与 \ze对匹配进行裁剪,使其成为这个完整模式的一个子集(参见 :h /\zs ) 。元 字符 \zs标志着一个匹配的起始,而元字符\ze则用来界定匹配的结束。将二者相结合,我们可以定义一个特殊的模式,它们可以让我们定义一个模式匹配一个较大的文本范围,然后再收窄匹配范围。与单词定界符类似,\zs\ze 均为零宽度元字符。
vim中模式的特殊元字符 ,\zs 与\ze关键字示例:
必须转义的情况:
  1. 正向查找时,/必须转义。
  2. 反向查找时,?必须转义。
  3. 每次都要转义\
查找&替换
显示当前查找关键词个数: :%s///gn
将光标定位到匹配结果词尾:/lang/e
重用上次查找的模式
:%s/\va.c/123/g
等价于以下两个命令
/\v.c
%s//123/g

global 命令

:global命令的广义形式如下所示:
:g/{pattern}/[range][cmd]
:g/{re}/{cmd} 在匹配的行上执行命令,如 :g/re/d ,删除包含re字符的行
:v/{re}/{cmd} 在非匹配的行上执行命令 如 :v/re/d ,删除不包含re字符的行

2018年10月21日星期日

Java中的CAS与ABA问题

今天面试别人的时候,提到了CAS,本来想要引导他说出CAS潜在的ABA问题,发现自己也没发简单的向他解释清楚,需要好好梳理下。

什么是 CAS

维基百科的解释是:
In computer science, compare-and-swap (CAS) is an atomic instruction used in multithreading to achieve synchronization. It compares the contents of a memory location with a given value and, only if they are the same, modifies the contents of that memory location to a new given value. This is done as a single atomic operation. The atomicity guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail. The result of the operation must indicate whether it performed the substitution; this can be done either with a simple boolean response (this variant is often called compare-and-set), or by returning the value read from the memory location (not the value written to it).
CAS(compare and swap),简单地说就是一种在多线程的情况下,让每个线程修改某个数据是一种原子操作。要实现CAS,有几个关键的值:
  1. 要修改的变量内存中的值V
  2. 更新变量前事先记录的期望值E,取值来自V
  3. 将要更新的值A
一个典型的CAS更新操作如下:
  1. 读取内存中的值V,赋值给E
  2. 更新变量前,比较内存值V与E
  3. 如果V==E,将V更新成A
  4. 如果V!=E,重复步骤1
如此循环,直到步骤3更新操作完成,写成伪代码就是:
do{
    int e=v.getVal();
    synchronized(this){
        if(e==getVal()){
            v.setVal(a);
            break;
        }
    }

}while(true)
CAS在JDK中被广泛应用,比如java.util.concurrent包下面的Lock、AtomicInteger相关的类都有用到。
例如a++;这种操作在Java里面并不是原子操作(包含了值的累加和赋值两个动作),所以并发情况下竞争操作某一个变量,需要用AtomicXXX几个类。

举个例子

AtomicInteger 实现类似a++操作,使用的是它的incrementAndGet 方法,源码很简单:
    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            // 此处的 compareAndSet 由usafe对象提供硬件级别的原子操作
            if (compareAndSet(current, next))
                return next;
        }
    }
Lock中对于竞争变量的CAS,也是类似的操作。

什么是ABA问题

简单地说就是,多个线程同时使用CAS更新数据时:
  1. 线程1要将数据从A变成B时(此时线程1的期待值E=='A')
  2. 其他线程已经抢先更新了变量,把变量从A变成其他值,再变回A(如A->C->A)。
  3. 当线程1用CAS机制准备更新变量时,发现E==A,所以继续更新变量。

这样有什么问题?

虽然变量最终结果是对的,但是线程1更新变量前,变量已经经历了一系列变化才回到原值。对于某些场景,忽略变化会继续进行更新操作,会带来错误的结果。
比如:

银行账户扣费问题

某个银行账户扣款操作,由于系统故障,产生了2个线程(T1,T2)对同一账户进行扣款,正常防重逻辑应该是一个执行成功,另一个失败,但是如果使用上面的CAS操作,就是:
  1. 账户里有100元,需要扣款50元
  2. T1先完成操作,扣款50元,将账户值V改为50
  3. T2准备扣款,当时期待值E==100
  4. 此时有其他转账操作先于T2对V进行累加,比如转入50元,此时V又变成100
  5. T2进行CAS的更新操作,发现E==V==100,执行更新操作,又扣款50

堆栈操作问题

如果CAS中的操作,变量值V是栈顶指针,也会有同样的问题:
  1. 某个堆栈内容是:A-B-C,栈顶为A
  2. 线程1更新前,得到期望值E==A
  3. 其他线程对栈进行进行pop,push操作,pop A B,push D A,此时栈的内容为 A-D-C
  4. 此时栈顶还是A,但是内容已经改变,线程1要更新的堆栈,已经不是第2步拿到期望值E时,自己要操作的那个堆栈了

如何规避

思路也很简单,就是对得到的期望值E和变量值V,增加一个版本号(比如时间戳),对于不同时期操作产生的同一个值的V,版本号是不同的,比较E与V时,需要同时比较版本号。比如juc包的AtomicStampedReference实现:
public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;  //维护对象引用
        final int stamp;  //用于标志版本
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
    private volatile Pair<V> pair;
    ....
    /**
      * expectedReference :更新之前的原始值
      * newReference : 将要更新的新值
      * expectedStamp : 期待更新的标志版本
      * newStamp : 将要更新的标志版本
      */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair; //获取当前pair
        return
            expectedReference == current.reference && //原始值等于当前pair的值引用,说明值未变化
            expectedStamp == current.stamp && // 原始标记版本等于当前pair的标记版本,说明标记未变化
            ((newReference == current.reference &&
              newStamp == current.stamp) || // 将要更新的值和标记都没有变化
             casPair(current, Pair.of(newReference, newStamp))); // cas 更新pair
    }
}    

总结

  1. CAS 是一种自旋锁,由一个死循环+compareAndSet 实现。
  2. CAS 存在ABA隐患,对于需要关注竞争变量变化过程(不仅仅是变量的值)的场景,ABA问题必须关注。
  3. 解决ABA问题,需要在CAS的compare过程中,增加对期望值E和当前值V版本号的判断。

2018年10月7日星期日

随笔

国庆假期的最后2天,我基本上是11点前入睡,6点起床,起床后背背单词,做几一些简单的锻炼,这种感觉非常好。 目前来说,自己还做不到严格的自律,每天脑子里还是会时不时地蹦出一些非计划外的想法。但是我发现,如果保持一个良好的作息和精神状态,我是可以一点一点跟自己沟通,说服自己集中精力,抛弃不必要的突发念头。每天给自己设立了一些小目标,每天尽早地完成它,过段时间再review一下看看。 明天就要上班,月底就要离职了,得慢慢地把自己的状态调整回来。

2018年10月5日星期五

黄金周西安游

9月底突发奇想地想国庆和老婆来西安玩一次,于是就匆匆忙忙订了机票和住宿,提前请了2天假29号飞到了西安。28晚上自己刚从北京出差回来,简直马不停蹄。从29号启程到4号回家,我们看了长恨歌演出,逛了西安城区、华山、陕西历史博物馆、华清池、兵马俑、西安城墙。一路上吃吃喝喝,除了在兵马俑见识了千分之一的中国人口以外,其他时候体验不算太差。整座西安城给人一种浓厚的历史氛围和民族融合带来的活力,让沿海长大的我倍感新奇。

9月底10月初的西安,让我一个南方人感受到了秋天。一天气温十几度到二十几度,早晚偏冷,白天晒着太阳也不会满头大汗,真正的秋高气爽。所以每天出门我总是穿着一件薄长袖和夹克外套,热了就把外套脱下别在腰上。值得一提的是,西安城内经常能见到穿着汉服的姑娘小伙,也许是这座城市的道路和建筑本身古色古香,不想在其他地方,走在街上看着他们,竟然不觉得突兀,反而别有一番风味,有种“这果然是长安啊”的感叹。

西安的吃,只要吃得惯的话,简直不要太棒。
6天时间内,我们逛遍了回民街永兴坊,这里的吃的价格也不算便宜,但是分量够大,随便一份面食都是一大碗,我跟老婆为了多尝几口,总是要两人吃一份。第一天老婆同学请吃羊肉泡馍,见识了如何自助徒手撕膜,撕了半个小时手都麻了,煮上羊肉粉丝汤,满满的一大海碗,吃完都快扶着墙出。路边烤羊肉串一串10块,羊肉又大又肥。面馆里的水盆羊肉、biangbiang面、油泼面……每一个都在让我跟米饭说再见。肉夹馍每家卖的都不大一样,什么都能夹,简直西安汉堡。还有永兴坊的子长煎饼,长得跟广东肠粉一样,吃一份得排半小时队。路边随处可见的石榴汁,小镜糕,大甄糕,酸梅汤,花椒酸奶(真加花椒。。),油茶(这个怂了喝不惯),凉粉,酱牛肉,卤羊蹄,羊杂汤……几天下来,自己的味蕾仿佛打开了一个新世界。

由于我们住的酒店离西安城中心挺近的,我们一有空就往钟楼附近跑。钟楼算是西安城区的中心,再加上周围14公里的西安城墙,整个城内古色古香,建筑风格很有特色,满眼望去都是中国传统的中间高两边低的那种屋顶,这种我以前只在电视剧和故宫里看到。路也很宽,在市内骑自行车很舒服,道路横平竖直,房子坐落得也很规整,整个城市颜色跟中国大多数内陆城市一样,整体偏灰色调。虽然国庆路上人多,地上也不见什么垃圾。不知道是不是来的这几天天气好,空气虽然干燥,但是空气还算清新,蓝天白云的天空配合飒爽的秋意给人感觉很好。

这几天出门一般都是打的外加摩拜,出租车司机师傅跟大多数北方司机一样,喜欢唠嗑,平易近人。西安城区的街道方方正正,看好地图的话,骑车走路都不容易迷路(当然回民街还是要多走几次才懂)。比较痛苦的是去周边景区的大巴车,去华山、兵马俑、华清池我们都报的是一日团,一大早就要坐车出发,路上快则两个小时,堵车就要三四个小时,体验不是很好,特别是从兵马俑回来的时候,我们晚上6点返程,回来已经9点半了。

这次行程也算比较紧,第一晚我们去华清池看了长恨歌演出,华山、陕西历史博物馆、兵马俑华清池我们各花了一天时间,最后一天逛了西安城墙,然后在几个晚上陆续逛了钟楼附近和回民街永兴坊,大雁塔之类的景点就没再去了。
长恨歌演出门票260多块钱,需要坐车到华清池景区看(后来想想应该在参观兵马俑华清宫的那天晚上来看比较节省时间)。第一次看这种现场的真人歌舞剧表演,比较新鲜,有种春晚现场的感觉,大体上就是用现代声光电技术演了一出杨贵妃唐玄宗的爱情故事,后面还有鹊桥相会啥的(能把一个公公儿媳扒灰的故事写的如此美好也是囧。。)。可惜我艺术功底不够深刻,只能跟周围的大叔大妈看个新鲜。
第二天一大早坐大巴来登华山,两人比较怂,选择了缆车路线,西峰上北峰下,5个小时爬了华山西峰南峰。一整块花岗岩形成的华山,挺拔陡峭,地势险峻,山上很多台阶不到半个脚宽。站在华山上,俯瞰山下,真的会让人感叹祖国大好河山,雄伟瑰丽。可惜最近上班忽略了身体锻炼,西峰爬了不到一半就气喘吁吁,追不上老婆的步伐。看来回去还是要好好锻炼,有生之年要从山底下上一次,来东峰看日出。
第三天鉴于前一天爬华山太累,就选择了市内的陕西历史博物馆,还好提前订了30块钱的大唐珍宝馆门票,免去了排长队进馆的痛苦。陕博不愧为中国博物馆的Top,文物之多,时间跨度之长令人叹为观止。
第四天我们又跟了一日团参观了华清池和兵马俑,这一天算是体验最差的一天。华清池参观什么呢,就是看看以前杨贵妃杨玄宗洗澡的澡堂子,还有西安事变老蒋被抓之前的故居,都是历史遗迹,想要知道以前的情景,只能对着景观配合导游解说发挥想象力了。下午的兵马俑参观让我体验了什么是“世界第一大坑”。据说当天下午兵马俑的客流量达到了15万,从进博物馆大门到走到展馆门口,浩浩荡荡都是人头,排队的时候前胸贴后背,整个人被后面推着走。要进一号坑之前更是痛苦,博物馆方管理太混乱,三队人马三个方向进一个门,其中不乏插队的低素质游客,大家互不相让,差点爆发肢体冲突。进坑之后人山人海,根本看不到前面,人在队伍中,不由自主地被后面人推着走,还没仔细看完兵马俑的细节,就被推着到出口了。
第五天西安城墙游算是最满意的一天,跟老婆吃完一顿早午饭,慢慢悠悠地买上一张上城墙的票,租上一辆双人自行车,开始休闲游。中午到下午这段时间城墙上人不多,西安城墙虽然不是水泥路,但是一路上没有什么坡度,也足够宽敞,3个小时车程下来,轻轻松松绕城墙骑行了一圈半,两边都是充满古城特色的房子和风景,伴着凉爽的秋意,一扫前几天旅行的疲惫。

写在旅行结束后

最后坐飞机回来前,老婆问我,西安比起以前去过的重庆和成都怎么样?我觉得除了兵马俑那天的大坑以外,西安还是让我非常喜欢的。较慢的生活节奏,好吃的各种美食,浓厚的历史底蕴,人们美好的精神面貌。甚至我都觉得,下半辈子我们应该好好锻炼身体,攒钱来西安置业养老(笑)。人生不应该只有生活工作的那三点一线,应该多一点不一样的经历。相信有生之年,我还会再来几次。












2018年9月24日星期一

第N次开博客

也不知道这是第几次搭博客了,新浪博客、QQ空间、百度空间、CSDN、ITEYE、OSChina、自建博客……折腾博客的经历几乎跟自己的网龄一样长。写博文这件事,也没有一直坚持下来。所以,为什么这次又开了一个博客?自己总结了以下几点:
  1. 我的工作生活,需要一个宣泄的地方。
  2. 想要再锻炼一下自己写技术博客的能力。
  3. 很不习惯熟人看到我写的东西,不会再暴露任何个人信息了。
  4. 年纪一大,不想再折腾,免费博客服务是我的首选。
So,这次直接选了不用折腾的,再尝试一次,看看自己能做到哪一步吧。

First Blog

Test First Blog

Effective Java 3rd edition 读书笔记

最近把去年出版的 Effective Java 3rd Edition 的新章节读完了,把笔记统一整理一下。 Lambdas and Streams Item 42: Prefer lambdas to anonymous class Java ...