Java并发编程实战读后感

1. 前言

该书由Doug Lea之外的另外一位Java并发大神Brian Goetz和Tim Peierls合著,算是Java并发领域的一本经典书籍。此书从2013年入手之后,拿起放下了三次。之前两次自己对并发的研究还不是很深,基本属于一知半解,工作当中也极少用到并发,看了就忘。最近半年在阅读JDK源代码,特别是阅读完部分java.util.concurrency包之后,对并发的感觉更深。这个时候回头来看看这本书,才真正体会到了其中的真谛,确实是字字珠玑。

本文记录下自己阅读完获得的一些感悟,具体的API就不会在这里叙述,记录更多设计和方法的部分,欢迎读者一同探讨。

2. 当我们讨论线程问题,我们在说什么

当我们讨论线程问题时,其实关注的是两个概念:可见性与原子性。

可见性

可见性就是对于AB两个线程共同操作了一个变量V,设计中A先修改了变量V;那么,A对V的修改对B是否可见。

在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量的值,那么总能得到相同的值。听起来似乎是一个很简单的问题,其实不然。

在多线程和现代处理器的环境下,上述的过程就没有那么的简单。

当读线程和写线程在不同的线程中执行时,我们无法确保执行读操作的线程能适时的看到其他线程写入的值。另外在现代处理器中,一个CPU通常包含多个核。变量V的修改并非直接在内存中修改,而是现在某个核的寄存器和本地缓存中进行修改,然后再写入到内存中。这个时候,V的修改才会对其他核上的线程可见。要实现这些功能,Java通过一系列的CPU指令帮我们除了了不同CPU厂商之间的实现差异。通过内存屏障,在CPU处理到内屏屏障时,强制将本地缓存中的变量值与其他CPU同步实现可见性的语义。

原子性

原子性就是保证操作是原子的,所有中间状态都不会被其他线程访问到。

比如对一个Int的赋值x = 1;可以认为是原子的。但是自增操作x++;就不是原子的,因为需要三条jvm指令去完成它:1. 读取变量x;2. 对变量x加1;3.将结果赋值给变量x。如果在多线程环境下,因为时序的关系就可能导致x最终出现多个值。那么这个时候,自增操作就不能称为是原子的,非原子的状态操作就是线程不安全的。

2.1 安全性问题

在前面我们提到了,如果多线程环境下不满足可见性和原子性,就会发生线程不安全,那到底什么是线程安全呢?在书中这么定义:

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

在这个定义下有这么几个描述需要进一步说明:1)正确的行为;2)主调代码中不需要任何额外的同步或协调。

2.1.1 正确的行为

什么是正确的行为?正确的行为就是符合需求规范定义的行为,这些行为的描述一般都是遵照时间顺序的,比如:

如果事件A先发生,对变量X复制1;事件B后发生,对变量X复制2;那么X的值应该是2

上面的一个描述就是一个** Requirement Specification 需求规范 ** 。这些描述通常都是遵循 ** Sequential Consistency 顺序一致性 **,满足人脑的一般思维模式(思考事情的时候不会从平行宇宙,多维时空角度来想这件事情该怎么做)。

2.1.2 额外的同步或协调

我们说一个类不是线程安全的,应该指的是这个类在没有额外的同步或协调下,会产生可见性和原子性等问题,例如我们经常说HashMap是非线程安全的,其实就是说使用HashMap如果要达到可见性和原子性的要求。

我们通过Collections.synchronizedMap(hashmap)进行包装之后,hashmap就变成了线程安全的类。如果翻看源码,其实Collections.synchronizedMap(hashmap)所做的事情,就是加了一段装饰而已:

synchronized(mutex){
hashmap.xxx();
}

如果将这段放在调用处,也可以让一个HashMap编程线程安全,但就加上了额外的同步和协调,就没办法说明HashMap是线程安全的。

2.2 活跃性问题

如果说线程的安全性是指“永远不发生糟糕的事情”, 那么线程的活跃性就是指“某件正确的事情最终会发生”。当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,表现为死循环——死循环外的正确的事情永远不会发生。

在多线程环境下,通常表现为阻塞或挂起。例如线程A等待线程B持有的某个资源,而线程B一直不释放,那么A就会永久的等待下去。还有包括死锁、饥饿、活锁等,这些都属于活跃性问题。

2.3 性能问题

如果说线程的活跃性问题是指“某件正确的事情最终会发生”,那么性能问题就是指“某件正确的事情最终会发生,应该尽快发生”。性能问题包含很多方面,如服务时间过长、响应不灵敏、吞吐率过低、资源消耗过高或者伸缩性较低等。

3. Java为我们提供的线程安全基础设施有哪些?

Java 自1.5开始,提供了功能强大的java.util.concurrency包,其中提供了大量的并发工具供不了解并发的我们使用,帮助构建线程安全的程序。当然,里面大部分是Doug Lea大神写的。

3.1 内置锁

看新的东西之前,还是要看到Java提供的 synchronized关键字。自1.2版本开始Java就把该关键词作为了最基础的同步机制,称为内置锁。内置锁可以作用在方法、代码块中,作用在方法时表示用该类的当前实例作为锁对象加锁。

如果线程(注意,讨论的对象是线程)需要访问实例的同步方法,则需要先获取实例的内置锁,在执行完成后自动释放。如果该实例的锁已经被另外的线程获取,则当前线程会在该锁上排队等待,等待之前一个线程释放锁。锁的获取与释放都通过编译器加入monitor_entermontior_exit指令实现。

内置锁一度是java中进行同步的唯一方法,很多遗留方法还是使用了内置锁进行同步,比如著名的Vertex,Collections里面的同步包装器等。在1.6之后,内置锁的性能也得到了很大的提升,在还未具备很强的并发经验之前,还是应该优先选择内置锁。

之所以之后,会产生更多的工具来替代内置锁的部分功能,主要是因为内置锁的最大缺点——无法控制。上面我们说了,内置锁是通过编译器和JVM指令实现的,因此在程序员角度无法对加锁和解锁行为进行太多的控制;另外内置锁也是不可中断,而且错综复杂的调用关系会让内置锁的加锁组合、加锁顺序变得难以管理。因此,Java在1.5中加入了Lock接口和对应实现,称为显示锁。

3.2 显示锁(Lock, Condition, 条件谓语)

显式锁的顶层接口为Lock,提供了ReenterantLock, ReadWriteLock等实现。

使用Lock的一个模式如下:

Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 更新对象状态
// 捕获异常,并在必要时回复不变性条件
} finally {
lock.unlock();
}

3.3 信号量、栅栏、闭锁

Lock本质上开始一个开闭锁,只有开闭两个状态。在应用中,通常还会衍生出很多其他的需求。比如一个并行计算的需求,存在一个100万个数据集合,求这100万个数的和。单线程的场景就是把这些数从第一个加到最后一个,最后输出。在多核环境下,我们可以将这个问题并行化,把100万数据分成100份每份1万数据,然后由一个线程进行计算,最后将这100个线程的计算结果相加就是这100万个数据的最终结果。

由于调度机制的原因,这100个线程可能会以任意的顺序结束,但只有这100个线程全部完成的时候我们才能获得最终结果。因此需要一定的协调机制,在100个线程都结束时,调用最后的结果输出程序。这个需求都可以通过信号量、栅栏、闭锁等实现。具体实现等有时间再写一篇文章介绍(又给自己挖了坑)。

3.4 Non-blocking算法和Lock free算法

我们在2.2节中提到了活跃性和性能,通过同步处理的多线程问题,都或多或少的影响了活跃性和性能。比如线程A获得了一个锁并在执行一些耗时的计算,如果线程B同样想获得这个锁,那么程序的活跃性和性能就收到了影响。

java 1.5之后,jvm开始支持硬件的CAS(Compare and Swap)指令,CAS接受三个参数(variable, expectedValue, newValue)。CAS的语义是这样的,如果变量variable的值和expectedValue相等,那么就将variable赋值为newValue;如果和expectedValue不相等,就返回失败。

jdk1.5 使用CAS操作引入了AtomicInteger等一些列原子量,可以保证“先检查再修改”引起的多线程安全问题。听起来很高大上,其实就是一个乐观锁的思路,假设在当前线程修改期间,其他线程不会对原数据进行修改。在写入之前做一次检查,如果被修改了,则进行重试,重试到成功为止。典型代码如下:

while(true){
int old = getState(); // 1. 读取旧制
int new = old + 1; // 2.对旧值做出修改
if(CAS(old, new){ //
return true;
}
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容