美文网首页
『读书笔记』Java并发编程的艺术(并发编程挑战与基石)

『读书笔记』Java并发编程的艺术(并发编程挑战与基石)

作者: yuan_dongj | 来源:发表于2020-03-15 19:01 被阅读0次

    第一章 并发编程的挑战

    并发编程的目的是为了让程序运行的更快,但是启动更多的线程就能让程序更大限度的运行吗?不一定,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上下文切换,但是会牺牲CPU资源耗用。
    线程池内的线程不是越多越好,可以使用jstack jstack pid | grep thread-name -C 1 | grep java.lang.Thread.State | awk '{print $2$3}' | sort | uniq -c来查看线程池内线程状态,如果大部分线程都处于闲置状态,适当减少corePoolSize数量。
    并发编程时刻注意资源限制的存在,比如开多线程向数据库insert或者多线程下载网络资源,速度并不一定如预期,有可能反而更慢。

    相关文章

      网友评论

          本文标题:『读书笔记』Java并发编程的艺术(并发编程挑战与基石)

          本文链接:https://www.haomeiwen.com/subject/qgmpehtx.html