JAVA多线程篇之锁的介绍

Posted by Jason Lee on 2019-07-21

概述

这篇文章介绍各种锁的分类。介绍的内容如下:

  1. 公平锁 / 非公平锁
  2. 可重入锁 / 不可重入锁
  3. 独享锁 / 共享锁
  4. 互斥锁 / 读写锁
  5. 乐观锁 / 悲观锁
  6. 分段锁
  7. 偏向锁 / 轻量级锁 / 重量级锁
  8. 自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

锁的种类详解

公平锁 / 非公平锁

  • 公平锁
    公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁
    非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
  • 有可能,会造成优先级反转或者饥饿现象。
  • 对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
  • 非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

具体可以参考JAVA多线程之AQS分析(1)

可重入锁 / 不可重入锁

  • 不可重入锁:只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单
  • 可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。

synchronizedReentrantLock都是可重入锁。

  • ReentrantLocksynchronized比较:
    • 1.前者使用灵活,但是必须手动开启和释放锁
    • 2.前者扩展性好,有时间锁等候(tryLock()可中断锁等候(lockInterruptibly()),锁投票等,适合用于高度竞争锁和多个条件变量的地方
    • 3.前者提供了可轮询的锁请求,可以尝试去获取锁(tryLock( )),如果失败,则会释放已经获得的锁。有完善的错误恢复机制,可以避免死锁的发生。

乐观锁 / 悲观锁

  • 悲观锁 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

  • 乐观锁 每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式(CAS实现的)

  • 使用场景
    乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
    但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

  • CAS算法:

    • 需要读写的内存值 V
    • 进行比较的值 A
    • 拟写入的新值 B
  • CAS 算法ABA的问题

    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

    JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

独享锁 / 共享锁

  • 独享锁 是指该锁一次只能被一个线程所持有。
  • 共享锁 是指该锁可被多个线程所持有。
    对于ReentrantLock而言,其是独享锁。
    但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
    读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

互斥锁 / 读写锁

  • 互斥锁
    一次只能一个线程拥有互斥锁,其他线程只有等待

互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒,而操作系统负责线程调度,为了实现锁的状态发生改变时唤醒阻塞的线程或者进程,需要把锁交给操作系统管理,所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的,加锁的时间大概100ns左右,而实际上互斥锁的一种可能的实现是先自旋一段时间,当自旋的时间超过阀值之后再将线程投入睡眠中,因此在并发运算中使用互斥锁(每次占用锁的时间很短)的效果可能不亚于使用自旋锁。

  • 读写锁
    读写锁分为读锁和写锁,多个读锁之间是不需要互斥的(读操作不会改变数据,如果上了锁,反而会影响效率),写锁和写锁之间需要互斥,也就是说,如果只是读数据,就可以多个线程同时读,但是如果你要写数据,就必须互斥,使得同一时刻只有一个线程在操作。

  • java中的读写锁 ReentrantReadWriteLock

自旋锁

  • 自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

  • 存在问题
    • 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
    • 自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
  • 优点:
    • 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
    • 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

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

一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

如果测试失败,则分为两种情况:

  • 1,对象的偏向锁标志位为0(当前不是偏向锁),说明发生了竞争,已经膨胀为轻量级锁,这时使用CAS操作尝试获得锁
  • 2,偏向锁标志位为1,说明还是偏向锁不过请求的线程不是原来那个了。这时只需要使用CAS尝试把对象头偏向锁从原来那个线程指向目前求锁的线程。
  • 本质:使用CAS取代互斥同步

  • 背景:『轻量级锁』是相对于『重量级锁』而言的,而重量级锁就是传统的锁。

  • 锁膨胀

  • 轻量级锁与重量级锁的比较:

    • 重量级锁是一种悲观锁,它认为总是有多条线程要竞争锁,所以它每次处理共享数据时,不管当前系统中是否真的有线程在竞争锁,它都会使用互斥同步来保证线程的安全;
    • 而轻量级锁是一种乐观锁,它认为锁存在竞争的概率比较小,所以它不使用互斥同步,而是使用CAS操作来获得锁,这样能减少互斥同步所使用的『互斥量』带来的性能开销。
  • 实现原理:
    对象头称为『Mark Word』,虚拟机为了节约对象的存储空间,对象处于不同的状态下,Mark Word中存储的信息也所有不同。Mark Word中有个标志位用来表示当前对象所处的状态。

  • 偏向锁作用:偏向锁是为了消除无竞争情况下的同步原语,进一步提升程序性能。

    • 与轻量级锁的区别:

      • 轻量级锁是在无竞争的情况下使用CAS操作来代替互斥量的使用,从而实现同步;
      • 而偏向锁是在无竞争的情况下完全取消同步。
    • 与轻量级锁的相同点:

      • 它们都是乐观锁,都认为同步期间不会有其他线程竞争锁。

      原理:当线程请求到锁对象后,将锁对象的状态标志位改为01,即偏向模式。然后使用CAS操作将线程的ID记录在锁对象的Mark Word中。以后该线程可以直接进入同步块,连CAS操作都不需要。
      但是,一旦有第二条线程需要竞争锁,那么偏向模式立即结束,进入轻量级锁的状态。
      + 优点:偏向锁可以提高有同步但没有竞争的程序性能。但是如果锁对象时常被多条线程竞争,那偏向锁就是多余的。偏向锁可以通过虚拟机的参数来控制它是否开启。

      具体请参考深入分析synchronized原理和锁膨胀过程

    • 三种对比

参考



支付宝打赏 微信打赏

赞赏一下