编辑
2021-04-22
编程
00

目录

1. 锁的分类
1.1 大纲
1.2 乐观锁 vs 悲观锁
1.2.1 CAS
1.3 自旋锁 vs 适应性自旋锁
1.4 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
1.5 公平锁 VS 非公平锁
1.6 可重入锁 VS 非可重入锁
1.7 独享锁 VS 共享锁
2. AQS
2.1 ReentrantLock

1. 锁的分类

1.1 大纲

  • 是否锁同步资源:
    • 锁住:悲观锁
    • 不锁住:乐观锁
  • 锁同步资源失败,是否阻塞
    • 不阻塞:自旋锁,适应性自旋锁
  • 同步资源竞争区别:
    • 不锁资源,一个线程修改资源,其他线程重试:无锁
    • 同一个线程执行同步资源时自动获取资源:偏向锁
    • 多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放:轻量级锁
    • 多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒:重量级锁
  • 多线程竞争锁时是否要排队:
    • 排队:公平锁
    • 不排队:非公平锁
  • 一个线程中多个流程能不能获取同一把锁
    • 能:可重入锁
    • 不能:非可重入锁
  • 多个线程能不能共享一把锁:
    • 能:共享锁
    • 不能:排它锁

1.2 乐观锁 vs 悲观锁

悲观锁使用数据时先加锁,如 java 中的 synchronized 和 Lock

乐观锁不加锁,在更新数据前判断是否有线程更新了这个数据,没有则更新,有则报错或重试。在 java 中,乐观锁是通过无锁编程来实现的,如 CAS 算法,在原子类中的递增操作就是通过 CAS 自旋实现的。

使用场景:

  • 悲观锁适合写多的场景,先加锁保证写操作时数据正确
  • 乐观锁适合读多的场景。

1.2.1 CAS

CAS 全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过 CAS 来实现了乐观锁。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。

CAS 的问题:

  1. ABA问题,最终比较的时候还是A,但是中间曾经被修改过,然后又修改回来
  2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  3. 只能保证一个共享变量的原子操作

1.3 自旋锁 vs 适应性自旋锁

(锁同步资源失败,是否阻塞)

自旋的必要性:对于获取锁失败时,如果先进行阻塞(休眠,让出时间片),等到资源释放时在唤醒,这个过程需要切换 CPU 的状态,这种状态的切换需要耗费处理器时间。对于同步代码块的内容过于简单的情况下,状态切换的时间可能比执行代码地时间还长。让线程自旋等待资源释放,异常来避免线程切换的开销,这就是自旋锁

同理,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

适应性自旋锁: 自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,如果自旋刚刚成功并且持有锁的线程正在运行中,这准许自旋更长的瞬间。如果自旋很少成功获得过,则有可能直接阻塞线程,不做或者做少量自旋。

1.4 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

  • java对象头: 对象头主要由 Mark Word(标记字段)、Klass Pointer(类型指针)构成

    • Mark Word: 存储对象的HashCode,分代年龄和锁标志位信息
    • Klass Pointer: 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  • Monitor: 可以理解为一个同步工具或一种同步机制,是线程私有的数据结构,每一个线程都有一个可用monitor record列表。每一个被锁住的对象都会和一个monitor关联

锁状态Mark Word 存储内容标记
无锁对象的 hashCode、对象分代年龄、是否是偏向锁(0)01
偏向锁偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)01
轻量级锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
  • 无锁:没有对资源进行锁定,只有一个线程能修改成功:CAS
  • 偏向锁: 同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
    • 一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID
    • 再次进入时,检测Mark Word里是否存储着指向当前线程的偏向锁
  • 轻量级锁
    • 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁
    • 自旋修改偏向锁的线程id
  • 重量级锁
    • 轻量级锁自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁

偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

1.5 公平锁 VS 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。缺点是吞吐效率低,CPU 唤醒阻塞线程的开销大

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。如果获取锁时刚好锁释放,那么这个线程无需等待直接获取到锁。可以减少线程唤醒的开销,提高吞吐率,但是有可能排队的锁一直等不到锁。

ReentrantLock 默认使用非公平锁。有公平锁FairSync和非公平锁NonfairSync两个子

1.6 可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中 ReentrantLocksynchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

java
public class Widget { public synchronized void doSomething() { System.out.println("方法1执行..."); doOthers(); } public synchronized void doOthers() { System.out.println("方法2执行..."); } }

类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。

如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

1.7 独享锁 VS 共享锁

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。JDK中的synchronized和JUC中Lock的实现类就是互斥锁

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

2. AQS

java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。

AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架

2.1 ReentrantLock

ReentrantLock意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁

ReentrantLock 和 Synchronized 的对比

ReentrantLockSynchronized
锁实现机制依赖 AQS监视器模式
灵活性支持响应中断、超时、尝试获取锁不灵活
释放形式必须显示调用 unlock() 释放锁自动释放监视器
锁类型公平锁&非公平锁非公平锁
条件队列可关联多个条件队列关联一个条件队列
可重入性可重入可重入
java
// **************************Synchronized的使用方式************************** // 1.用于代码块 synchronized (this) {} // 2.用于对象 synchronized (object) {} // 3.用于方法 public synchronized void test () {} // 4.可重入 for (int i = 0; i < 100; i++) { synchronized (this) {} } // **************************ReentrantLock的使用方式************************** public void test () throw Exception { // 1.初始化选择公平锁、非公平锁 ReentrantLock lock = new ReentrantLock(true); // 2.可用于代码块 lock.lock(); try { try { // 3.支持多种加锁方式,比较灵活; 具有可重入特性 if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ } } finally { // 4.手动释放锁 lock.unlock() } } finally { lock.unlock(); } }

本文作者:Yui_HTT

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!