java并发编程
序言
读书笔记,温故而知新
并发编码可能存在的问题
为了让程序运行的更快,我们会用并发编程(多线程)来提高运行效率,同时多线程的使用也会付出一定的代价,带来一些问题。如:上下文的切换,死锁,硬件软件资源的限制。
上下文切换
如何定位:
可以用Lmbench测量上下文切换的时长,或者用vmstat测量切换次数。
使用jstack减少wating的线程。
如何优化上下文切换
无锁并发编程
cas算法
使用最少线程
协程
底层实现的原理
java代码最终会转化为字节码,被类加载器加载到jvm里面执行,最终是变化为汇编指令在cpu上执行,所以并发的一致性依赖于jvm和cpu的共同实现。
CPU中的实现:
总线锁
缓存锁(缓存一致性协议MESI)
缓存(cache)
cache的意义
为什么需要CPU cache?因为CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源。所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构:cpu -> cache -> memory)。
CPU cache有什么意义?cache的容量远远小于主存,因此出现cache miss在所难免,既然cache不能包含CPU所需要的所有数据,那么cache的存在真的有意义吗?当然是有意义的——局部性原理。
A. 时间局部性:如果某个数据被访问,那么在不久的将来它很可能被再次访问;
B. 空间局部性:如果某个数据被访问,那么与它相邻的数据很快也可能被访问
cache的物理映射
存储器的三个性能指标——速度、容量和每位价格——导致了计算机组成中存储器的多级层次结构,其中主要是缓存和主存、主存和磁盘的结构
多核CPU cache结构
MESI(缓存一致性协议)
cache的写方式
write through,write back, 写失效,写更新
cache line
cache line是cache与内存数据交换的最小单位,根据操作系统一般是32byte或64byte(LinkedTransferQueue
)。在MESI协议中,状态可以是M、E、S、I,地址则是cache line中映射的内存地址,数据则是从内存中读取的数据。
工作方式:当CPU从cache中读取数据的时候,会比较地址是否相同,如果相同则检查cache line的状态,再决定该数据是否有效,无效则从主存中获取数据,或者根据一致性协议发生一次cache-to--chache的数据推送
工作效率:当CPU能够从cache中拿到有效数据的时候,消耗几个CPU cycle,如果发生cache miss,则会消耗几十上百个CPU cycle;
CPU通知和cache line状态的扭转
E,M,S,I ~ LR,LW,RR,RW
JVM中的实现(锁):
分类
偏向锁
轻量级锁
重量级锁
锁保存的位置java对象头
MarkWord状态变化
偏向锁
1.偏向锁的竞争
2.偏向锁的撤销
轻量级锁
1.锁获取
2.释放锁
3.自选时间
JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化:
a:如果平均负载小于CPUs则一直自旋
b:如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
c:如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
d:如果CPU处于节电模式则停止自旋
重量级锁
总结
1:在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,如果以上两种都失败,则启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;
2:偏向锁是在无锁争用的情况下使用的,也就是同步开在当前线程没有执行完之前,没有其它线程会执行该同步快,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,一点有两个以上线程争用,就会升级为重量级锁;
3:如果线程争用激烈,那么应该禁用偏向锁。
优化
减少锁的时间
减少锁的粒度,或者锁粗化
消除缓存行的伪共享