java中的锁

synchronized 关键字锁定代码库
可重入锁 java.util.concurrent.lock.ReentrantLock
可重复读写锁 java.util.concurrent.lock.ReentrantReadWriteLock

可重入锁

  • 指在同一个线程在外层方法获取锁的时候,进入内层方法会自动获取锁。JDK 中基本都是可重入锁,避免死锁的发生。上面提到的常见的锁都是可重入锁。

公平锁 / 非公平锁

  • 公平锁:多个线程按照申请锁的顺序依次获得锁,如java.util.concurrent.lock.ReentrantLock.FairSync
  • 非公平锁:指多个线程获取锁的顺序并不是按照申请锁的顺序,而是有可能后申请的线程优先获得锁。如synchronized,java.util.concurrent.lock.ReentrantLock.NonfairSync等

独享锁 / 共享锁

  • 独享:指锁一次只能被一个线程所持有。如synchronized ,java.util.concurrent.lock.ReentrantLock 都是独享锁
  • 共享:指锁可以被多个线程所持有。ReadWriteLock 返回的 ReadLock 就是共享锁

悲观锁 / 乐观锁

  • 悲观锁:一律会对代码进行加锁,synchronized ,java.util.concurrent.lock.ReentrantLock 都是悲观锁
  • 乐观锁:默认不会进行并发修改,通常不断采取CAS算法不断尝试更新
  • 悲观锁适合写多读少的场景 ,乐观锁适合读多写少的场景

粗粒度锁 / 细粒度锁

  • 粗粒度锁,就是把执行的代码块都锁定
  • 细粒度锁,就是锁住尽可能小的代码块,java.util.concurrent.ConcurrentHashMap 中的分段锁就是一种细粒度锁
  • 粗粒度锁和细粒度锁是相对的,没有什么标准

偏向锁 / 轻量级锁 / 重量级锁

  • JDK 1.5 之后新增锁的升级机制,提升性能。
  • 通过 synchronized 加锁后,一段同步代码一直被同一个线程所访问,那么该线程获取的就是偏向锁
  • 偏向锁被一个其他线程访问时,Java 对象的偏向锁就会升级为轻量级锁
  • 再有其他线程会以自旋的形式尝试获取锁,不会阻塞,自旋一定次数仍然未获取到锁,就会膨胀为重量级锁

自旋锁

  • 自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式来获取锁。这样的好处是减少线程的上下文切换消耗,缺点是循环占有,可能会CPU空转,浪费资源。

锁的升级

锁的状态由低到高:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

只能升级,不能降级。

锁状态 存储内容 标志位
无锁 对象的hashCode,对象分代年龄,是否是偏向锁(0) 01
偏向锁 偏向线程ID,偏向时间戳,对象分代年龄,是否偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量的指针 11

锁的对比

锁 优点 缺点 场景
偏向锁 加解锁不需要额外消耗,和执行非异步方法相比仅存在纳秒级差距。 若线程之间存在竞争,会代带来额外的锁撤销的消耗(撤销偏向锁会Stop The World) 使用于只有一个线程访问同步代码场景
轻量级锁 竞争的线程不会阻塞,提高了系统的响应速度 始终得不到锁的竞争线程,不断自旋会消耗CPU 追求响应速度,同步代码块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间慢 追求吞吐量,同步块执行速度较慢

锁如何升级

  • 无锁

    无锁是指线程通过无限循环来执行更新操作,如果执行成功就退出循环,如果执行失败(有其他线程更新了值),则继续执行,直到成功为止。CAS操作就属于无锁。如果从性能的角度来看,无锁状态的性能是非常高的。

  • 偏向锁

    偏向锁的前提假设是当一个线程获取锁,后面还有大概率该线程还会需要继续持有这把锁 。

    当有另外一个线程区尝试获取这个锁的时候,偏向模式就宣告结束。

    虚拟机启用偏向锁的参数-XX:UseBiasedLocking。如果当前偏向锁已启动,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的同步块时,虚拟机都可以不用再进行同步操作了。

  • 轻量级锁

    当开始有锁的竞争了,那么偏向锁就会升级到轻量级锁;

    当获取锁冲突多,时间越长的时候,线程肯定无法继续在这里死等了,所以只好先挂起,然后等前面获取锁的线程释放了锁之后,再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。

    在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为01状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。

    然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为00,即表示此对象处于轻量级锁定状态。 轻量级锁的前提假设是对于绝大部分的锁,在整个同步周期内都是不存在竞争的,通过CAS操作来避免时候互斥锁的开销

    关于自旋锁:

    自旋锁是一种通过让线程不释放当前的CPU执行一个忙循环,来尝试获取锁的方式。 自旋锁的前提假设是锁被其它线程占用的时间很短 。如果其它线程占用锁的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能上的浪费。自旋次数的默认值是10次,用户可以通过使用参数-XX:PreBlockSpin来更改。

  • 重量级锁

    当有两个及以上的线程争用同一个锁,那么轻量级锁就不再有效,要膨胀为重量级锁。锁标志的状态值变为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

    • 偏向锁只有设置了-XX:UseBiasedLocking参数才会存在
    • 假设启用了偏向锁,对象头的锁标志位是01(和未锁定状态一样),但是存储的内容是偏向线程ID、偏向时间戳
    • 当线程获取偏向锁是通过CAS操作将对象头中存储的偏向线程ID更新为当前线程的ID
    • 对象是否被锁定是指对象头是否指向线程的锁记录(Lock Record)
    • 只有是轻量级锁或者重量级锁时对象才会被锁定