一篇好文,带你深刻了解Lock锁 !

1.为何须要Lock

  1. 为何synchronized不够用,还须要Lock

       Lock和synchronized这两个最多见的锁均可以达到线程安全的目的,可是功能上有很大不一样。java

       Lock并非用来代替synchronized的而是当使用synchronized不知足状况或者不合适的时候来提供高级功能的程序员

  1. 为何synchronized不够用算法

    • 效率低:锁的释放状况较少,试图得到锁不能设定超时,不能中断一个正在试图得到锁的线程
    • 不够灵活:加锁和释放的时候单一,每一个锁仅有单一的条件多是不够的
    • 没法知道是否成功的获取锁

      2.Lock锁的意义

  2. 与使用synchronized方法和语句相比, Lock实现提供了更普遍的锁操做。 它们容许更灵活的结构,能够具备彻底不一样的属性,而且能够支持多个关联的Condition对象。
  3. 锁是一种用于控制多个线程对共享资源的访问的工具。 一般,锁提供对共享资源的独占访问,一次只能有一个线程能够获取该锁,而且对共享资源的全部访问都须要首先获取该锁。 可是,某些锁可能容许并发访问共享资源,例如ReadWriteLock的读取锁。
  4. 使用synchronized方法或语句可访问与每一个对象关联的隐式监视器锁,但会强制全部锁的获取和释放以块结构方式进行。当获取多个锁时,它们必须以相反的顺序释放锁。
  5. 虽然用于synchronized方法和语句的做用域机制使使用监视器锁的编程变得更加容易,而且有助于避免许多常见的涉及锁的编程错误,但在某些状况下,您须要以更灵活的方式使用锁。 例如,某些用于遍历并发访问的数据结构的算法须要使用“移交”或“链锁”:您获取节点A的锁,而后获取节点B的锁,而后释放A并获取C,而后释放B并得到D等。 Lock接口的实现经过容许在不一样范围内获取和释放锁,并容许以任意顺序获取和释放多个锁,从而启用了此类技术。

    3.锁的用法

       灵活性的提升带来了额外的责任。 缺乏块结构锁定须要手动的去释放锁。 在大多数状况下,应使用如下惯用法:编程

Lock lock = new ReentrantLock();
lock.lock();
try{

}finally {
  lock.unlock();
}

       当锁定和解锁发生在不一样的范围内时,必须当心以确保经过try-finally或try-catch保护持有锁定时执行的全部代码,以确保在必要时释放锁定。
       Lock实现经过使用非阻塞尝试获取锁( tryLock() ),尝试获取可被中断的锁( lockInterruptibly以及尝试获取锁),提供了比使用synchronized方法和语句更多的功能。可能会超时( tryLock(long, TimeUnit) )。安全

       Lock类还能够提供与隐式监视器锁定彻底不一样的行为和语义,例如保证顺序,不可重用或死锁检测。 若是实现提供了这种特殊的语义,则实现必须记录这些语义。服务器

       请注意, Lock实例只是普通对象,它们自己能够用做synchronized语句中的目标。 获取Lock实例的监视器锁与调用该实例的任何lock方法没有指定的关系。 建议避免混淆,除非在本身的实现中使用,不然不要以这种方式使用Lock实例。数据结构

4.内存同步

       全部Lock实现必须强制执行与内置监视器锁所提供的相同的内存同步语义,如Java语言规范中所述 :并发

  • 一个成功的lock操做具备一样的内存同步效应做为一个成功的锁定动做。
  • 一个成功的unlock操做具备相同的存储器同步效应做为一个成功的解锁动做。

       不成功的锁定和解锁操做以及可重入的锁定/解锁操做不须要任何内存同步效果。ide

实施注意事项工具

       锁获取的三种形式(可中断,不可中断和定时)在其性能特征可能有所不一样。 此外,在给定的Lock类中,可能没法提供中断正在进行的锁定的功能。 所以,不须要为全部三种形式的锁获取定义彻底相同的保证或语义的实现,也不须要支持正在进行的锁获取的中断。 须要一个实现来清楚地记录每一个锁定方法提供的语义和保证。 在支持锁获取中断的范围内,它还必须服今后接口中定义的中断语义:所有或仅在方法输入时才这样作

5.Lock提供的接口

图片

5.1 获取锁

void lock(); // 获取锁。
  1. 最普通的的获取锁,若是锁被其余线程获取则进行等待
  2. lock不会像synchronized同样在异常的时候自动释放锁
  3. 所以必须在finally中释放锁,以保证发生异常的时候锁必定被释放

注意:lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁、lock()就会陷入永久等待状态

5.2 获取中断锁

void lockInterruptibly() throws InterruptedException;

       除非当前线程被中断,不然获取锁。
       获取锁(若是有)并当即返回。

       若是该锁不可用,则出于线程调度目的,当前线程将被挂起,并在发生如下两种状况之一以前处于休眠状态:

  • 该锁是由当前线程获取的;
  • 其余一些线程中断当前线程,并支持锁定获取的中断。

       若是当前线程:在进入此方法时已设置其中断状态;要么获取锁时被中断,而且支持锁获取的中断,而后抛出InterruptedException并清除当前线程的中断状态。

注意事项

       在某些实现中,中断锁获取的能力多是不可能的,而且若是可能的话多是昂贵的操做。 程序员应意识到多是这种状况。 在这种状况下,实现应记录在案。与正常方法返回相比,实现可能更喜欢对中断作出响应。Lock实现可能可以检测到锁的错误使用,例如可能致使死锁的调用,而且在这种状况下可能引起(未经检查的)异常。

注意 synchronized 在获取锁时是不可中断的

5.3 尝试获取锁

boolean tryLock();

       非阻塞获取锁(若是有)并当即返回true值。 若是锁不可用,则此方法将当即返回false值。相比于Lock这样的方法显然功能更增强大,咱们能够根据是否能获取到锁来决定后续程序的行为
注意:该方法会当即返回,即使在拿不到锁的时候也不会在一只在那里等待

该方法的典型用法是:

Lock lock = new ReentrantLock();
if(lock.tryLock()){
  try{
    // TODO
  }finally {
    lock.unlock();
  }
}else{
  // TODO
}

5.4 在必定时间内获取锁

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

       若是线程在给定的等待时间内获取到锁,而且当前线程还没有中断,则获取该锁。
       若是锁可用,则此方法当即返回true值。 若是该锁不可用,则出于线程调度目的,当前线程将被挂起,并处于休眠状态,直到发生如下三种状况之一:

  1. 该锁是由当前线程获取的。
  2. 其余一些线程会中断当前线程,并支持锁定获取的中断。
  3. 通过指定的等待时间若是得到了锁,则返回值true 。

       若是通过了指定的等待时间,则返回值false 。 若是时间小于或等于零,则该方法将根本不等待

注意事项

       在某些实现中,中断锁获取的能力多是不可能的,而且若是可能的话多是昂贵的操做。 程序员应意识到多是这种状况。 在这种状况下,实现应记录在案。与正常方法返回或报告超时相比,实现可能更喜欢对中断作出响应。Lock实现可能可以检测到锁的错误使用,例如可能致使死锁的调用,而且在这种状况下可能引起(未经检查的)异常。

5.5 解锁

void unlock(); //释放锁。

注意事项
       Lock实现一般会限制哪些线程能够释放锁(一般只有锁的持有者才能释放锁),而且若是违反该限制,则可能引起(未经检查的)异常。

5.6 获取等待通知组件

Condition newCondition(); //返回绑定到此Lock实例的新Condition实例。

       该组件与当前锁绑定,当前线程只有得到了锁。 才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
注意事项

Condition实例的确切操做取决于Lock实现。

5.7总结

       Lock对象锁还提供了synchronized所不具有的其余同步特性,如可中断锁的获取(synchronized在等待获取锁时是不可中断的),超时中断锁的获取等待唤醒机制的多条件变量Condition等,这也使得Lock锁具备更大的灵活性。Lock的加锁和释放锁和synchronized有一样的内存语义,也就是说下一个线程加锁后能够看到前一个线程解锁前发生的全部操做。

6.锁的分类

根据一下6种状况能够区分多种不一样的锁,下面详细介绍

6.1要不要锁住同步资源

是否锁住 锁名称 实现方式 例子
锁柱 悲观锁 synchronized、lock synchronized、lock
不锁住 乐观锁 CAS算法 原子类、并发容器

悲观锁又称互斥同步锁,互斥同步锁的劣势:

  1. 阻塞和唤醒带来的性能劣势
  2. 永久阻塞:若是持有锁的线程被永久阻塞,好比遇到了无限循环,死锁等活跃性问题
  3. 优先级反转

悲观锁:

       当一个线程拿到锁了以后其余线程都不能获得这把锁,只有持有锁的线程释放锁以后才能获取锁。

乐观锁:

       本身才进行操做的时候并不会有其余的线程进行干扰,因此并不会锁住对象。在更新的时候,去对比我在修改期间的数据有没有人对他进行改过,若是没有改变则进行修改,若是改变了那就是别人改的那我就不改了放弃了,或者从新来。

开销对比:

  1. 悲观锁的原始开销要高于乐观锁,可是特色是一劳永逸,临界区持锁的时间哪怕愈来愈长,也不会对互斥锁的开销形成影响
  2. 悲观锁一开始的开销比乐观锁小,可是若是自旋时间长,或者不停的重试,那么消耗的资源也会愈来愈多

使用场景:

  1. 悲观锁:适合并发写多的状况,适用于临界区持锁时间比较长的状况,悲观锁能够避免,大量的无用自旋等消耗
  2. 乐观锁:适合并发读比较多的场景,不加锁能让读取性能大幅度提升

    6.2可否共享一把锁

是否共享 锁名称
能够 共享锁(读锁)
不能够 排他锁(独占锁)

共享锁:

       获取共享锁以后,能够查看可是没法修改和删除数据,其余线程此时也能够获取到共享锁也能够查看但没法修改和删除数据

案例:ReentrantReadWriteLock的读锁(具体实现后续系列文章会讲解)

排他锁:

       获取排他锁的以后,别的线程是没法获取当前锁的,好比写锁。

案例:ReentrantReadWriteLock的写锁(具体实现后续系列文章会讲解)

6.3是否排队

是否排队 锁名称
排队 公平锁
不排队 非公平锁

非公平锁:

       先尝试插队,插队失败再排队,非公平是指不彻底的按照请求的顺序,在必定的状况下能够进行插队

存在的意义:

  • 提升效率
  • 避免唤醒带来的空档期

案例:

  1. 以ReentrantLock为例,建立对象的时候参数为false(具体实现后续系列文章会讲解)
  2. 针对tryLock()方法,它是不遵照设定的公平的规则的

       例如:当有线程执行tryLock的时候一旦有线程释放了锁,那么这个正在执行tryLock的线程立马就能获取到锁即便在它以前已经有其余线程在等待队列中

公平锁:

       排队,公平是指的是按照线程请求的顺序来进行分配锁

案例:以ReentrantLock为例,建立对象的时候参数为true(具体实现后续系列文章会讲解)

注意:

       非公平也一样不提倡插队行为,这里指的非公平是指在合适的时机插队,而不是盲目的插队

优缺点:

非公平锁:

  • 优点:更快,吞吐量大
  • 劣势:有可能产生线程饥饿

公平锁:

  • 优点: 线程平等,每一个线程按照顺序都有执行的机会
  • 劣势:更慢,吞吐量更小

    6.4 是否能够重复获取同一把锁

是否能够重入 锁名称
能够 可重入锁
不能够 不可重入锁

案例:以ReentrantLock为例(具体实现后续系列文章会讲解)

6.5是否能够被中断

是否能够中断 锁名称 案例
能够 可中断锁 Lock是可中断锁(由于tryLock和lockInterruptibly都能响应中断)
不能够 不可中断锁 Synchronized就是不可中断锁

6.6等锁的过程

是否自旋 锁名称
自旋锁
阻塞锁

使用场景:

  1. 自旋锁通常用于多核的服务器,在并发度不是很高的状况下,比阻塞锁效率高
  2. 自旋锁适合临界区比较短小的状况,不然若是临界区很大,线程一旦拿到锁,好久之后才会释放那也不合适的,由于会浪费性能在自旋的时候

    7.锁优化

7.1 虚拟机中带的锁优化

  1. 自旋锁
  2. 锁消除
  3. 锁粗化

这三种锁优化的方式在前一篇Synchronized文章种全部讲解

7.2写代码的时候锁优化

  • 缩小同步代码块
  • 尽可能不锁住方法
  • 减小请求锁的次数
  • 避免人为制造热点
  • 锁中尽可能不要再包含锁
  • 选择合适的锁类型或者合适的工具类
相关文章
相关标签/搜索