1.为什么需要Lock

  1. 为什么synchronized不够用,还需要Lock

Lock和synchronized这两个最常见的锁都可以达到线程安全的目的,但是功能上有很大不同。

Lock并不是用来代替synchronized的而是当使用synchronized不满足情况或者不合适的时候来提供高级功能的

  1. 为什么synchronized不够用
  • 效率低:锁的释放情况较少,试图获得锁不能设定超时,不能中断一个正在试图获得锁的线程
  • 不够灵活:加锁和释放的时候单一,每个锁仅有单一的条件可能是不够的
  • 无法知道是否成功的获取锁

2.Lock锁的意义

  1. 与使用synchronized方法和语句相比, Lock实现提供了更广泛的锁操作。 它们允许更灵活的结构,可以具有完全不同的属性,并且可以支持多个关联的Condition对象。
  2. 锁是一种用于控制多个线程对共享资源的访问的工具。 通常,锁提供对共享资源的独占访问,一次只能有一个线程可以获取该锁,并且对共享资源的所有访问都需要首先获取该锁。 但是,某些锁可能允许并发访问共享资源,例如ReadWriteLock的读取锁。
  3. 使用synchronized方法或语句可访问与每个对象关联的隐式监视器锁,但会强制所有锁的获取和释放以块结构方式进行。当获取多个锁时,它们必须以相反的顺序释放锁。
  4. 虽然用于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操作具有相同的存储器同步效应作为一个成功的解锁动作。

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

实施注意事项

锁获取的三种形式(可中断,不可中断和定时)在其性能特征可能有所不同。 此外,在给定的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写代码的时候锁优化

  • 缩小同步代码块
  • 尽量不锁住方法
  • 减少请求锁的次数
  • 避免人为制造热点
  • 锁中尽量不要再包含锁
  • 选择合适的锁类型或者合适的工具类

Lock锁 精讲的更多相关文章

  1. 转: 【Java并发编程】之二十:并发新特性—Lock锁和条件变量(含代码)

    简单使用Lock锁 Java5中引入了新的锁机制--Java.util.concurrent.locks中的显式的互斥锁:Lock接口,它提供了比synchronized更加广泛的锁定操作.Lock接 ...

  2. java并发编程的艺术——第五章总结(Lock锁与队列同步器)

    Lock锁 锁是用来控制多个线程访问共享资源的方式. 一般来说一个锁可以防止多个线程同时访问共享资源(但有些锁可以允许多个线程访问共享资源,如读写锁). 在Lock接口出现前,java使用synchr ...

  3. 【原创】分布式之redis复习精讲

    引言 为什么写这篇文章? 博主的<分布式之消息队列复习精讲>得到了大家的好评,内心诚惶诚恐,想着再出一篇关于复习精讲的文章.但是还是要说明一下,复习精讲的文章偏面试准备,真正在开发过程中, ...

  4. 转 Redis 总结精讲 看一篇成高手系统-4

    转 Redis 总结精讲 看一篇成高手系统-4 2018年05月31日 09:00:05 hjm4702192 阅读数:125633   本文围绕以下几点进行阐述 1.为什么使用redis 2.使用r ...

  5. 多线程系列之自己实现一个 lock 锁

    我们面试中经常会被问到多线程相关知识,这一块内容往浅了说大家都会,但是一问到底层实现原理,我们往往就一脸懵逼. 这段时间准备好好学习多线程,接下来会写一系列关于多线程的知识. 我们首先要了解线程,百度 ...

  6. Java岗 面试考点精讲(基础篇01期)

    即将到来金三银四人才招聘的高峰期,渴望跳槽的朋友肯定跟我一样四处找以往的面试题,但又感觉找的又不完整,在这里我将把我所见到的题目做一总结,并尽力将答案术语化.标准化.预祝大家面试顺利. 术语会让你的面 ...

  7. 分布式之redis复习精讲

    看到一片不错的精简的redis文档,转载之,便于复习梳理之用 转自:https://www.cnblogs.com/rjzheng/p/9096228.html ------------------- ...

  8. 微软BI SSIS 2012 ETL 控件与案例精讲面试 200 问(SSIS 面试题,ETL 面试题)

    开篇介绍 本自测与面试题出自 微软BI SSIS 2012 ETL 控件与案例精讲 (http://www.hellobi.com/course/21) 课程,对于学完本课程的每一课时和阅读完相关辅助 ...

  9. 微软BI SSIS 2012 ETL 控件与案例精讲课程学习方式与面试准备详解

    开篇介绍 微软BI SSIS 2012 ETL 控件与案例精讲 (http://www.hellobi.com/course/21) 课程从2014年9月开始准备,到2014年12月在 天善BI学院  ...

  10. Linux高频命令精讲(三)

    [教程主题]:2.Linux高频命令精讲 [2.1]Linux的运行方式 图形运行方式 - 本地使用KDE/Gnome集成环境 - 运行X Server远程使用图形环境 命令行(字符运行)方式 - 本 ...

随机推荐

  1. FreeImage使用

    http://blog.csdn.net/byxdaz/article/details/6056509 http://blog.chinaunix.net/uid-20660110-id-65639. ...

  2. 阿里云ecs Linux平台安装mongodb数据库

    MongoDB提供了linux平台上32位和64位的安装包,你可以在官网下载安装包. 下载地址:http://www.mongodb.org/downloads 下载完安装包,并解压 tgz(以下演示 ...

  3. URAL 1303. Minimal Coverage(DP)

    题目链接 又是输出路径...这题完全受上题影响,感觉两个题差不多..用了基本上一样的算法写了,这题比较纠结,就是卡内存啊...5000*5000的数组开不了..然后没办法,水了好几次MLE,看了一下虎 ...

  4. FreeSWITCH 1.6关于视频通话的一些测试

    简单的测试了一下,暂时没把精力放到这一块. ① 视频编码透传的设置(使用代理模式). 修改internal.xml文件的以下参数: <param name="inbound-proxy ...

  5. string 字符串的分隔处理与list的相互转换

    在指定 String 数组的每个元素之间串联指定的分隔符 String,从而产生单个串联的字符串.(来源于MSDN) 有两个重载函数:[C#]public static string Join(   ...

  6. kakfa-性能相关

    1.增大partition最大连接数 kafka的集群有多个Broker服务器组成,每个类型的消息被定义为topic,同一topic内部的消息按照一定的key和算法被分区(partition)存储在不 ...

  7. Java第一个程序(CMD环境)

    在新学Java配置好JDK之后,根据例子用CMD命令运行第一个java程序出错.是这样的: 1,在某一个盘(最好是英文路径),新建记事本输入java程序,例如: 保存为HelloJava.java. ...

  8. Python连接webstocker获取消息

    简介(脚本都是根据网上资料改写) 此脚本主要是客户觉得webstcket不稳定,所以编辑一个脚本,不停的请求web服务器,当发生错误时,脚本自动退出(). 脚本内容 脚本一 # -*- coding: ...

  9. gradle入门教程

    1,https://gradle.org/ 下载过后解压缩,绿色软件不需要安装. 配置系统环境:GRADLE_HOME设置为解压缩之后的地址,PATH属性追加%GRADLE_HOME%\bin; 2, ...

  10. CF126B

    CF126B Password 题意: 给出一个字符串 H,找一个最长的字符串 h,使得它既作为前缀出现过.又作为后缀出现过.还作为中间的子串出现过. 解法: 沿着 $ next_n $ 枚举字符串, ...