《Java并发编程的艺术》读书笔记一

《Java并发编程的艺术》读书笔记

为什么要并发?现在的机器都是带多核处理器的,为了能充分发挥多核处理器的优势,并发编程就派上用场了。当然,在单核处理器的情况下,如果碰到了IO密集型的程序,并发编程也能重复利用处理器。并发的多线程程序通常响应更快。

但是,并发程序更快并不是绝对的,线程之间的上下文切换是需要消耗时间的,如果上下文切换花费的时间多于多线程带来的程序执行的减少时间,那么并发程序反而比串行执行程序花费的时间更多。

而且,编写并发程序往往要比编写串行程序难。

Java并发机制的底层实现原理

volatile

volatile关键字能够保证被修饰的变量的可见性顺序性

由于CPU的速度和内存的速度有着巨大的差距,为了提高处理速度,CPU不直接与内存进行交互,而是先将数据读到CPU的高速缓存,然后CPU从高速缓存读取数据,写也先写到高速缓存中,然后再写入内存,每个CPU都有高速缓存,这样就会出现CPU读到的数据可能与内存中的数据不一致。

volatile变量的写,在写完高速缓存后会马上写入内存,每次读都会先使高速缓存实现,然后从内存读取。volatile变量也不会与其他变量发生重排序。

synchronized

synchronized关键字可以修饰方法,也可以用在同步代码块中,修饰静态方法时,使用的锁是当前类的Class对象,修饰普通方法时,使用的锁是当前对象,用在同步代码块的时候需要自己指定锁。

Java中的对象在对象头部分会存有HashCode值、分代年龄和锁标记。

synchronized使用的锁又4种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

偏向锁

研究发现,在多数情况下,锁总是由同一个线程多次获得,为了让线程获得锁的代价更低就引入了偏向锁。

在锁对象的锁标记中会记录偏向的线程的ID,在该线程要获得该锁时,只要测试一下这个锁对象的锁标记是否指向该线程就可以了,如果不是,则测试一下该锁对象是否是偏向锁,是的话就通过CAS设置该锁指向当前线程,如果不是,则通过CAS来竞争锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争锁时,持有该锁的线程才会释放锁。

轻量级锁

轻量级锁在获取的时候,会先将锁的对象头中的Mark Word数据复制到线程栈的锁记录中,然后通过CAS将锁的对象头中的标记指向该锁记录,如果成功则获得了锁。

释放轻量级锁时,通过CAS将锁记录复制到锁对象的对象头中,如果失败则升级到重量级锁。

重量级锁

重量级锁就是一般意义上的锁,会阻塞线程。

锁的对比

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗 如果线程间存在锁竞争,会带来额外的锁撤销消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提供了程序的响应时间 如果始终得不到锁,会自旋消耗CPU 最近响应时间,同步代码块执行速度块
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 最近吞吐量,同步代码块执行速度长

原子操作

原子操作是指不可被中断的一个或多个操作。

CAS(比较并交换),输入两个值,一个旧值和一个新值,如果旧值没有变化,就将其设置为新值,如果变化了就不做处理。

Java中可以通过锁和CAS来实现原子操作。从JDK1.5开始java.util.concurrent.atomic包中包含了一些支持原子操作的类。

CAS实现原子操作的问题

  • ABA问题

    旧值从A变成B然后又变回A,这种情况,CAS无法感知,会认为旧值一直没变过。

  • 循环时间长开销大

    自旋CAS如果长时间不成功,会给CPU带来非常的开销。

  • 只能保证一个共享变量的原子操作

    如果要对个共享变量的操作,CAS就无力了。

锁实现原子操作

使用锁实现原子操作,只有获得锁的线程才能进入代码块或方法,在锁所保护的代码块或方法内可以又任意多个操作。

除了偏向锁,其他锁的获取都是通过CAS方式来获取的。