CAS 操作需要 CPU 支持,将比较 和 交换 作为一条指令来执行, CAS 一般有三个参数,内存位置,预期原值,新值 ,所以UnSafe 类中的 compareAndSwap 用属性相对对象初始地址的偏移量,来定位内存位置。
线程的同步
线程同步出现的根本原因是访问公共资源需要多个操作,而这多个操作的执行过程不具备原子性,被任务调度器分开了,而其他线程会破坏共享资源,所以需要在临界区做线程的同步,这里我们先明确一个概念,就是临界区,他是指多个任务访问共享资源如内存或文件时候的指令,他是指令并不是受访问的资源。
POSIX 定义了五种同步对象,互斥锁,条件变量,自旋锁,读写锁,信号量,这些对象在 JVM 中也都有对应的实现,并没有全部使用 POSIX 定义的 api,通过 Java 实现灵活性更高,也避免了调用native方法的性能开销,当然底层最终都依赖于 pthread 的 互斥锁 mutex 来实现,这是一个系统调用,开销很大,所以 JVM 对锁做了自动升降级,基于AQS的实现以后在分析,这里主要说一下关键字 synchronized 。
当声明 synchronized 的代码块时,编译而成的字节码会包含一个 monitorenter 和 多个 monitorexit (多个退出路径,正常和异常情况),当执行 monitorenter 的时候会检查目标锁对象的计数器是否为0,如果为0则将锁对象的持有线程设置为自己,然后计数器加1,获取到锁,如果不为0则检查锁对象的持有线程是不是自己,如果是自己就将计数器加1获取锁,如果不是则阻塞等待,退出的时候计数器减1,当减为0的时候清楚锁对象的持有线程标记,可以看出 synchronized 是支持可重入的。
刚刚说到线程的阻塞是一个系统调用,开销大,所以 JVM 设计了自适应自旋锁,就是当没有获取到锁的时候, CPU 回进入自旋状态等待其他线程释放锁,自旋的时间主要看上次等待多长时间获取的锁,例如上次自旋5毫秒没有获取锁,这次就6毫秒,自旋会导致 CPU 空跑,另一个副作用就是不公平的锁机制,因为该线程自旋获取到锁,而其他正在阻塞的线程还在等待。除了自旋锁, JVM 还通过 CAS 实现了轻量级锁和偏向锁来分别针对多个线程在不同时间访问锁和锁仅会被一个线程使用的情况。后两种锁相当于并没有调用底层的信号量实现(通过信号量来控制线程A释放了锁例如调用了 wait(),而线程B就可以获取锁,这个只有内核才能实现,后面两种由于场景里没有竞争所以也就不需要通过底层信号量控制),只是自己在用户空间维护了锁的持有关系,所以更高效。