第一章 并发编程的挑战
并发编程的目的是为了让程序运行的更快,但是启动更多的线程就能让程序更大限度的运行吗?不一定,CPU通过分配时间片来实现多线程,线程存在上下文切换开销。
-
那么引起CPU上下文切换的原因都有哪些呢?
- 当前任务的时间片用完了,cpu正常调度
- 当前任务发生IO阻塞,被调用线程挂起
- 多任务抢占锁资源
- 用户主动挂起
-
我知道要减少CPU上下文切换次数,我们应该如何做才能减少CPU上下文切换呢?
- 无锁并发编程
- CAS
- 使用最少的线程,避免创建不必要的线程
- 协程
-
并发编程会有死锁的问题存在,我们应该如果避免死锁呢?
- 避免一个线程获取多把锁
- 避免一个线程在锁内占用多个资源
- 锁增加过期时间
-
并发编程除了CPU上下文切换,还有哪些瓶颈与挑战点呢?
- 硬件资源限制:服务带宽、硬盘读写速度、CPU处理速度...
- 软件资源限制:数据库连接数、socket连接数、文件句柄数...
-
对于资源限制的问题,我们应该如果解决呢?
- 硬件资源限制:集群
- 软件资源限制:资源池连接复用
- 根据不同的资源限制调整程序的并发度
第二章 JAVA并发机制的底层实现原理
我们已经知道并发编程都有哪些挑战了,那么java的并发基础机制都有哪些呢?
volatile、synchronize、cas、atomic
-
volatile是一个轻量级的synchronize,他可以阻止指令重排并保证可见性,原理是什么?
- 为了提高速度,CPU并不直接与内存通信,而是把数据从内存读到高速缓存区,CPU的缓存区的最小存储单位是缓存行
- 对volatile写操作会增加一行Lock前缀汇编指令,会将当前处理器的缓存行数据写回到系统内存中
- 其他处理器会检查自己缓存行对应的内存地址是否被修改,如果被修改,会将缓存行数据置为失效状态
- 缓存行是处理器级别,对于单核CPU,volatile的可见性没有意义,但是volatile的防指令重排还是有用的
- 一个有意思的操作:对于部分型号的处理器,Lock信号会锁定缓存行,缓存行是64个字节,对于频繁读写的volatile变量,可以通过填充数据到64字节来独占缓存行,volatile写避免与其他数据缓存行互相锁定。(骚操作。。不要用。。)
-
synchronize的锁升级了解一下
- 锁升级有存在的背景的,HotSpot作者发现大多数情况下,锁不仅不存在多线程竞争,而且还总是被同一线程获取
- 锁存储于对象头的Mark Word中
- 锁只能升级,不能降级
- 偏向锁不会主动解锁,因为锁经常被同一线程获取。
- 偏向锁加锁过程:检查对象头里是否存有线程ID,如果没有则CAS替换;如果有则检查对象头里线程ID是否是当前线程ID,如果是即表示占用锁;如果不是,申请撤销偏向锁,原偏向锁持有线程会暂时挂起,Mark Word重新偏向于其他线程,最后恢复挂起线程。
- 上面申请撤销偏向锁过程如果出现竞争,则膨胀成轻量锁
- 轻量锁加锁过程:线程会在自己栈帧中开辟空间用于存储锁记录,复制锁对象头Mark Word,尝试CAS锁对象头的Mark Word替换为指向栈帧中锁记录指针,如果成功则获取锁,如果失败则CAS自旋竞争锁。
- 轻量锁解锁过程:线程将自己栈帧锁记录空间中复制的Mark Word重新替换回对象头中,如果失败,表示锁存在竞争,锁会继续膨胀为重量锁。
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁没有额外开销,性能与不加锁代码相差无几 | 如果存在锁竞争,解锁有额外的性能消耗 | 锁竞争场景很少,系统并发低 |
轻量锁 | 加锁是CPU自旋操作,不会引发CPU上下文切换,程序响应速度快 | 如果竞争激烈,CPU自旋消耗大 | 追求响应速度,同步代码快执行速度很快 |
重量锁 | 加锁不会引发CPU自旋 | 线程阻塞,响应速度较慢 | 追求高吞吐,CPU不会浪费在自旋上 |
-
atomic操作原理
- CPU atomic:处理器会保证读取写入一个字节等基本操作的原子性,对于复杂操作,处理器由以下两种机制保证原子性。针对于以下两种机制,CPU提供多种指令来提供复杂原子操作,如:CMPXCHG指令。
- 总线锁:当前处理器核心独占共享变量内存,总线锁性能开销比较大
- 缓存锁:频繁使用的数据会在CPU高速缓存内,那么原子操作就可以在缓存内部执行,并直接修改内存区域,由缓存一致性保证其他核心CPU的缓存行数据一致。
- 有些处理器不支持缓存锁定;如果数据跨缓存行,不支持缓存锁定。
- JAVA atomic:JVM中的CAS就是使用CPU提供的原子交换指令CMPXCHG,CAS存在以下三大问题
- ABA问题:A->B->A,java解决办法:引入版本号。AtomicStampedReference先检查引用是否是预期,再检查版本号是否是预期,最后再把版本号跟引用一起放入Pair内CAS更新。
- 对于自旋CAS,CPU执行开销大。
- 只能保证一个共享变量的原子操作,这个可以把多个共享变量包装成一个对象,使用AtomicReference CAS更新。
- CPU atomic:处理器会保证读取写入一个字节等基本操作的原子性,对于复杂操作,处理器由以下两种机制保证原子性。针对于以下两种机制,CPU提供多种指令来提供复杂原子操作,如:CMPXCHG指令。
个人总结
悲观锁的竞争会引起线程上下文切换,乐观锁就是无锁并发编程,可以减少CPU上下文切换,但是会牺牲CPU资源耗用。
线程池内的线程不是越多越好,可以使用jstack jstack pid | grep thread-name -C 1 | grep java.lang.Thread.State | awk '{print $2$3}' | sort | uniq -c
来查看线程池内线程状态,如果大部分线程都处于闲置状态,适当减少corePoolSize数量。
并发编程时刻注意资源限制的存在,比如开多线程向数据库insert或者多线程下载网络资源,速度并不一定如预期,有可能反而更慢。
网友评论