You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.5 KiB

synchronized、CAS、AQS

目录

  1. 思维导图
  2. 概述
  3. 常见术语
    • 悲观锁/乐观锁
    • 公平锁/非公平锁
    • 可重入锁/不可重入锁
    • 死锁
  4. synchronized
    • 使用方式
    • 实现原理
    • 锁优化
  5. CAS
  6. AQS
    • Lock
    • ReadWriteLock
  7. 参考

思维导图

概述

Java 中的并发锁大致上可以分为隐式锁和显式锁两种。隐式锁的代表就是 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer)队列同步器实现的,还有的地方把 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色,比如 AtomicInteger 等原子操作类。

常见术语

悲观锁和乐观锁

悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized,然鹅在真实环境中,大部分时候都不会产生冲突。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试),这种锁不会阻塞其他线程,也不涉及上下文切换,性能开销小,代表实现是 CAS。

公平锁和非公平锁

公平锁是指各个线程在获取锁前先检查有无排队的线程,按排队顺序去获取锁。非公平锁是指线程获取锁前不考虑排队问题,直接尝试获取锁。值得注意是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。

可重入锁和不可重入锁

如果一个线程已经获取到了锁,那么它可以访问被这个锁锁住的所有代码块,不可重入锁与之相反。可重入锁包括 synchronized 和 ReentrantLock。

死锁

synchronized

悲观、可重入,已经不那么重量级的锁。

使用方式
  • 修饰静态方法,锁 class 对象
  • 修饰非静态方法,锁当前实例对象
  • 修饰代码块,锁的是代码块里的对象

每个对象都有一个锁和一个等待队列,锁只能被一个线程持有,其他需要获取锁的线程需要阻塞等待,锁释放之后,对象会从队列中取出一个线程并唤醒,唤醒哪个线程是不确定的,不保证公平性。

多个线程是可以同时执行同一个 synchronized 实例方法的,只要它们访问的对象是不同的。

synchronized 锁住的是对象而非代码,只要访问的是同一个对象的方法,都得顺序访问。

实现原理

synchronized 是基于 Java 对象头和 Monitor 机制来实现的。

  • Java 对象头

    一个对象在内存中包含三个部分:对象头、实例数据、对其填充。其中 Java 对象头包含两个部分:

    • Class Metadata Adderess 类型指针

      存储类的元数据的指针,虚拟机通过这个指针找到它是哪个类的实例。

    • Mark Work 标记字段

      存储对象自身的运行时数据,包括 hashCode、GC 分代年龄、锁状态标志等。

  • Monitor

    Mark Word 有一个字段指向 monitor 对象,monitor 中记录了锁的持有线程,等待的线程队列等信息,前面说的每个对象都有一个锁和一个等待队列,就是在这实现的。

那 JVM 是如何把 synchronized 与 monitor 关联起来的呢?可以分为两种情况:

  1. 同步代码块,编译时会直接在同步代码块前加上 monitorenter 指令,代码块后加上 monitorexit 指令。
  2. 同步方法,虚拟机会为方法设置 ACC_SYNCHRONIZED 标志,在调用方法的时候根据这个标志判断是否是同步方法。
锁优化

JDK 1.6 对锁的实现引入了大量的优化,包括自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁来减少锁操作的开销。

锁主要存在四种状态:依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级但是不能降级,这种策略是为了提高获取锁和释放锁的效率。

  • 重量级锁

    重量级锁就是采用互斥量 Mutex 来控制对互斥资源的访问,在 Java 中被抽象为监视器锁 Monitor,这种同步方式成本非常高,包括内核态到用户态切换,性能差。

  • 自旋锁与自适应自旋

    在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间就去挂起、恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁,这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然占有处理器。在之后又引入自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

  • 锁消除

    虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

  • 锁粗化

    当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。

  • 轻量级锁

    对绝大部分锁来说,整个同步周期内不存在竞争,如果没有竞争,轻量级锁可以使用 CAS 操作来避免使用互斥量的开销。

  • 偏向锁

    偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。

CAS

CAS 即 CompareAndSwap 比较并替换。CAS 机制当中使用了三个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 和实际值相同时,才会将内存地址 V 对应的值修改为 B。

但是问题也是显而易见的:

  1. ABA 问题

    加一个版本号。

  2. 不能保证代码块的原子性

  3. 在多次尝试更新一个值时不成功,CPU 开销大

在 Java 并发包中 AtomicInteger、等都是它的实现。

AQS

参考

从 synchronized 到 CAS 和 AQS - 彻底弄懂 Java 各种并发锁

synchronize早已经没那么笨重

什么是 CAS 机制?