乐趣区

关于java:Java开发工程师进阶篇-扫盲Java中的各种锁你学废了吗

作者:幻好

起源:恒生 LIGHT 云社区

-Java 中锁的概念

  • 多线程访问共享资源的时候,防止不了资源竞争而导致数据错乱的问题,所以咱们通常为了解决这一问题,都会在访问共享资源之前加锁。
  • 加锁的目标就是保障共享资源在任意工夫里,只有一个线程拜访,这样就能够防止多线程导致共享数据错乱的问题。

- 锁的类型

  • 依据锁的状态、个性以及设计,可能会有多种辨别:

    • 乐观锁 / 乐观锁、自旋锁、分布式锁、偏差锁、轻量级锁、重量级锁等。

- 乐观锁 / 乐观锁

线程是否须要锁住同步资源

  • 乐观锁与乐观锁是一种狭义上的概念,体现了对待线程同步的不同角度,在 Java 和数据库中都有此概念对应的理论利用。
  • 概念:

    • 乐观锁 认为对于同一个数据的并发操作,肯定是会产生批改;乐观的认为,不加锁的并发操作肯定会出问题。

      • 对于同一个数据的并发操作,乐观锁采取加锁的模式。
      • Java 中,synchronized 关键字和Lock 的实现类都是乐观锁。
    • 乐观锁 认为对于同一个数据的并发操作,是不会产生批改的;乐观的认为,不加锁的并发操作是没有事件的。

      • 在更新数据的时候,会采纳尝试更新前判断数据是否被别的线程更新。
      • 如果这个数据没有被更新,以后线程将本人批改的数据胜利写入。
      • 如果数据曾经被其余线程更新,则依据不同的实现形式执行不同的操作(例如报错或者主动重试)。
      • Java 中,通过应用无锁编程来实现,最常采纳的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的。
  • 场景:

    • 乐观锁适宜写操作多的场景,先加锁能够保障写操作时数据正确。
    • 乐观锁适宜读操作多的场景,不加锁的特点可能使其读操作的性能大幅晋升。
  • 应用示例:

    *

    • 通过调用形式示例,能够发现乐观锁根本都是在显式的锁定之后再操作同步资源,而乐观锁则间接去操作同步资源。
    • 为何乐观锁可能做到不锁定同步资源也能够正确的实现线程同步呢?

      • 乐观锁的次要实现形式“CAS”的技术原理。

        • CAS 全称 Compare And Swap(比拟与替换),是一种无锁算法。在不应用锁(没有线程被阻塞)的状况下实现多线程之间的变量同步。java.util.concurrent 包中的原子类就是通过 CAS 来实现了乐观锁。
    • 看下原子类 AtomicInteger 的源码,看一下AtomicInteger 的定义:

      • 依据定义咱们能够看出各属性的作用:

        • unsafe:获取并操作内存的数据。
        • valueOffset:存储valueAtomicInteger 中的偏移量。
        • value:存储AtomicIntegerint 值,该属性须要借助volatile 关键字保障其在线程间是可见的。
      • 查看AtomicInteger 的自增函数incrementAndGet() 的源码时,发现自增函数底层调用的是unsafe.getAndAddInt()
      • 通过 OpenJDK 8 来查看 Unsafe 的源码:
      • 依据 OpenJDK 8 的源码咱们能够看出,getAndAddInt() 循环获取给定对象 o 中的偏移量处的值 v,而后判断内存值是否等于 v。如果相等则将内存值设置为 v + delta,否则返回 false,持续循环进行重试,直到设置胜利能力退出循环,并且将旧值返回。整个“比拟 + 更新”操作封装在compareAndSwapInt() 中,在 JNI 里是借助于一个 CPU 指令实现的,属于原子操作,能够保障多个线程都可能看到同一个变量的批改值。
      • JDK 通过 CPU 的cmpxchg 指令,去比拟寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。而后通过 Java 代码中的 while 循环再次调用cmpxchg 指令进行重试,直到设置胜利为止。
    • CAS 尽管很高效,但也存在三大问题:

      • ABA 问题

        • CAS 须要在操作值的时候查看内存值是否发生变化,没有发生变化才会更新内存值。然而如果内存值原来是 A,起初变成了 B,而后又变成了 A,那么 CAS 进行查看时会发现值没有发生变化,然而实际上是有变动的。
        • ABA 问题的 解决思路 就是在变量后面增加版本号,每次变量更新的时候都把版本号加一,这样变动过程就从“A-B-A”变成了“1A-2B-3A”。
        • JDK 从 1.5 开始提供了AtomicStampedReference 类来解决 ABA 问题,具体操作封装在compareAndSet() 中。compareAndSet() 首先查看以后援用和以后标记与预期援用和预期标记是否相等,如果都相等,则以原子形式将援用值和标记的值设置为给定的更新值。
      • 循环工夫长开销大

        • CAS 操作如果长时间不胜利,会导致其始终自旋,给 CPU 带来十分大的开销。
      • 只能保障一个共享变量的原子操作

        • 对一个共享变量执行操作时,CAS 可能保障原子操作,然而对多个共享变量操作时,CAS 是无奈保障操作的原子性的。
        • Java 从 1.5 开始 JDK 提供了AtomicReference 类来保障援用对象之间的原子性,能够把多个变量放在一个对象里来进行 CAS 操作。

- 阻塞锁 / 自旋锁 / 适应性自旋锁

  • 背景:

    • 阻塞或唤醒一个 Java 线程须要操作系统切换 CPU 状态来实现,这种状态转换须要消耗处理器工夫。如果同步代码块中的内容过于简略,状态转换耗费的工夫有可能比用户代码执行的工夫还要长。
    • 在许多场景中,同步资源的锁定工夫很短,为了这一小段时间去切换线程,线程挂起和复原现场的破费可能会让零碎得失相当。如果物理机器有多个处理器,可能让两个或以上的线程同时并行执行,咱们就能够让前面那个申请锁的线程不放弃 CPU 的执行工夫,看看持有锁的线程是否很快就会开释锁。
  • 阻塞锁

    • 让线程进入阻塞状态进行期待,当取得相应的信号(唤醒,工夫)时,才能够进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
    • JAVA 中,可能进入 \ 退出、阻塞状态或蕴含阻塞锁的办法有,synchronized 关键字(其中的分量锁),ReentrantLockObject.wait()\notify(),LockSupport.park()/unpart()(j.u.c 常常应用)
  • 自旋锁

    • 在 Java 中,尝试获取锁的线程不会立刻阻塞,而是采纳循环的形式去尝试获取锁,这样的益处是缩小线程上下文切换的耗费,毛病是循环会耗费 CPU 性能。
      *
    • 自旋锁自身是有毛病的,它不能代替阻塞。自旋期待尽管防止了线程切换的开销,但它要占用处理器工夫。如果锁被占用的工夫很短,自旋期待的成果就会十分好。反之,如果锁被占用的工夫很长,那么自旋的线程只会白节约处理器资源。
    • 自旋期待的工夫必须要有肯定的限度,如果自旋超过了限定次数(默认是 10 次,能够应用-XX:PreBlockSpin 来更改)没有胜利取得锁,就该当挂起线程。
    • 自旋锁的实现原理同样也是 CAS,AtomicInteger 中调用unsafe 进行自增操作的源码中的 do-while 循环就是一个自旋操作,如果批改数值失败则通过循环来执行自旋,直至批改胜利。
    • 自旋锁在 JDK1.4.2 中引入,应用-XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
  • 自适应自旋锁

    • 自适应意味着自旋的工夫(次数)不再固定,而是由前一次在同一个锁上的自旋工夫及锁的拥有者的状态来决定。
    • 如果在同一个锁对象上,自旋期待刚刚胜利取得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次胜利,进而它将容许自旋期待继续绝对更长的工夫。
    • 如果对于某个锁,自旋很少胜利取得过,那在当前尝试获取这个锁时将可能省略掉自旋过程,间接阻塞线程,避免浪费处理器资源。
  • 在自旋锁中,另有三种常见的锁模式:TicketLockCLHlockMCSlock

- 无锁 / 偏差锁 / 轻量级锁 / 重量级锁

  • 背景:

    • 这四种锁是指锁的状态,专门针对synchronized 的,锁的降级是单向的,不可逆转。
    • synchronized 是乐观锁,在操作同步资源之前须要给同步资源先加锁,这把锁就是存在 Java 对象头里的。

      • 以 Hotspot 虚拟机为例,Hotspot 的对象头次要包含两局部数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
      • Mark Word:默认存储对象的 HashCode,分代年龄和锁标记位信息。这些信息都是与对象本身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会依据对象的状态复用本人的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标记位的变动而变动。
      • Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
      • Monitor 能够了解为一个同步工具或一种同步机制,通常被形容为一个对象。每一个 Java 对象就有一把看不见的锁,称为外部锁或者Monitor 锁。
    • 在自旋锁中提到的“阻塞或唤醒一个 Java 线程须要操作系统切换 CPU 状态来实现,这种状态转换须要消耗处理器工夫。如果同步代码块中的内容过于简略,状态转换耗费的工夫有可能比用户代码执行的工夫还要长”。这种形式就是 synchronized 最后实现同步的形式,这就是 JDK 6 之前synchronized 效率低的起因。这种依赖于操作系统Mutex Lock 所实现的锁咱们称之为“重量级锁”,JDK 6 中为了缩小取得锁和开释锁带来的性能耗费,引入了“偏差锁”和“轻量级锁”。
    • 目前锁一共有 4 种状态,级别从低到高顺次是:无锁、偏差锁、轻量级锁和重量级锁。锁状态只能降级不能降级。
  • 无锁

    • 无锁没有对资源进行锁定,所有的线程都能拜访并批改同一个资源,但同时只有一个线程能批改胜利。
    • 无锁的特点就是批改操作在循环内进行,线程会一直的尝试批改共享资源。如果没有抵触就批改胜利并退出,否则就会持续循环尝试。如果有多个线程批改同一个值,必定会有一个线程能批改胜利,而其余批改失败的线程会一直重试直到批改胜利。
    • CAS 原理及利用即是无锁的实现。无锁无奈全面代替有锁,但无锁在某些场合下的性能是十分高的。
  • 偏差锁

    • 指一段同步代码始终被一个线程所拜访,那么该线程会主动获取锁。升高获取锁的代价。
    • 在大多数状况下,锁总是由同一线程屡次取得,不存在多线程竞争,所以呈现了偏差锁。其指标就是在只有一个线程执行同步代码块时可能进步性能。
    • 当一个线程拜访同步代码块并获取锁时,会在 Mark Word 里存储锁偏差的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向以后线程的偏差锁。引入偏差锁是为了在无多线程竞争的状况下尽量减少不必要的轻量级锁执行门路,因为轻量级锁的获取及开释依赖屡次 CAS 原子指令,而偏差锁只须要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。
    • 偏差锁只有遇到其余线程尝试竞争偏差锁时,持有偏差锁的线程才会开释锁,线程不会被动开释偏差锁。偏差锁的撤销,须要期待全局平安点(在这个工夫点上没有字节码正在执行),它会首先暂停领有偏差锁的线程,判断锁对象是否处于被锁定状态。撤销偏差锁后复原到无锁(标记位为“01”)或轻量级锁(标记位为“00”)的状态。
    • 偏差锁在 JDK 6 及当前的 JVM 里是默认启用的。能够通过 JVM 参数敞开偏差锁:-XX:-UseBiasedLocking=false,敞开之后程序默认会进入轻量级锁状态。
  • 轻量级锁

    • 指当锁是偏差锁的时候,被另一个线程所拜访,偏差锁就会降级为轻量级锁,其余线程会通过自旋的模式尝试获取锁,不会阻塞,进步性能。
    • 产生过程:

      • 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标记位为“01”状态,是否为偏差锁为“0”),虚拟机首先将在以后线程的栈帧中建设一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word 的拷贝,而后拷贝对象头中的Mark Word 复制到锁记录中。
      • 拷贝胜利后,虚拟机将应用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的Mark Word
      • 如果这个更新动作胜利了,那么这个线程就领有了该对象的锁,并且对象Mark Word 的锁标记位设置为“00”,示意此对象处于轻量级锁定状态。
      • 如果轻量级锁的更新操作失败了,虚拟机首先会查看对象的Mark Word 是否指向以后线程的栈帧,如果是就阐明以后线程曾经领有了这个对象的锁,那就能够间接进入同步块继续执行,否则阐明多个线程竞争锁。
      • 若以后只有一个期待线程,则该线程通过自旋进行期待。然而当自旋超过肯定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁降级为重量级锁。
  • 重量级锁

    • 指当锁为轻量级锁的时候,另一个线程尽管是自旋,当自旋肯定次数的时候,还没有获取到锁,就会进入阻塞,该锁收缩为重量级锁。重量级锁会让其余申请的线程进入阻塞,性能升高。
    • 降级为重量级锁时,锁标记的状态值变为“10”,此时 Mark Word 中存储的是指向重量级锁的指针,此时期待锁的线程都会进入阻塞状态。
  • 综上,偏差锁通过比照Mark Word 解决加锁问题,防止执行 CAS 操作。而轻量级锁是通过用 CAS 操作和自旋来解决加锁问题,防止线程阻塞和唤醒而影响性能。重量级锁是将除了领有锁的线程以外的线程都阻塞。

- 偏心锁 / 非偏心锁

  • 偏心锁

    • 指多个线程依照申请锁的程序来获取锁,线程间接进入队列中排队,队列中的第一个线程能力取得锁。
    • 长处是期待锁的线程不会饿死。
    • 毛病是整体吞吐效率绝对非偏心锁要低,期待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非偏心锁大。
  • 非偏心锁

    • 指多个线程获取锁的程序并不是依照申请锁的程序,获取不到才会到期待队列的队尾期待,有可能后申请的线程比先申请的线程优先获取锁。(有可能,会造成优先级反转或者饥饿景象。)
    • 长处是能够缩小唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞间接取得锁,CPU 不用唤醒所有线程。
    • 毛病是处于期待队列中的线程可能会饿死,或者等很久才会取得锁。

  • 实例:

    • 对于 Java 的ReentrantLock 而言,通过构造函数指定该锁是否是偏心锁,默认是非偏心锁。非偏心锁的长处在于吞吐量比偏心锁大。

      • 依据代码可知,ReentrantLock 外面有一个外部类SyncSync 继承 AQS(AbstractQueuedSynchronizer),增加锁和开释锁的大部分操作实际上都是在 Sync 中实现的。它有偏心锁FairSync 和非偏心锁NonfairSync 两个子类。ReentrantLock 默认应用非偏心锁,也能够通过结构器来显示的指定应用偏心锁。
        *
      • 能够显著的看出偏心锁与非偏心锁的lock() 办法惟一的区别就在于偏心锁在获取同步状态时多了一个限度条件:hasQueuedPredecessors()
        *

        • 进入hasQueuedPredecessors(),能够看到该办法次要做一件事件:次要是判断以后线程是否位于同步队列中的第一个。如果是则返回 true,否则返回 false。
    • 对于Synchronized 而言,也是一种非偏心锁。因为其并不像ReentrantLock 是通过 AQS 的来实现线程调度,所以并没有任何方法使其变成偏心锁。
  • 综上,偏心锁就是通过同步队列来实现多个线程依照申请锁的程序来获取锁,从而实现偏心的个性。非偏心锁加锁时不思考排队期待问题,间接尝试获取锁,所以存在后申请却先取得锁的状况。

- 可重入锁 / 非可重入锁

  • 可重入锁

    • 又名递归锁,是指在同一个线程在外层办法获取锁的时候,在进入内层办法会主动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前曾经获取过还没开释而阻塞。
  • Java 中ReentrantLocksynchronized 都是可重入锁,可重入锁的一个长处是可肯定水平防止死锁。

    • 下面的代码中,类中的两个办法都是被内置锁synchronized 润饰的,doSomething() 办法中调用doOthers() 办法。因为内置锁是可重入的,所以同一个线程在调用doOthers() 时能够间接取得以后对象的锁,进入doOthers() 进行操作。
    • 如果是一个不可重入锁,那么以后线程在调用doOthers() 之前须要将执行doSomething() 时获取以后对象的锁开释掉,实际上该对象锁已被以后线程所持有,且无奈开释,所以此时会呈现死锁。
  • 非可重入锁导致死锁起因剖析

    • 通过重入锁ReentrantLock 以及非可重入锁NonReentrantLock 的源码来比照剖析一下为什么非可重入锁在反复调用同步资源时会呈现死锁。
    • 首先ReentrantLockNonReentrantLock 都继承父类 AQS,其父类 AQS 中保护了一个同步状态status 来计数重入次数,status 初始值为 0。
    • 当线程尝试获取锁时,可重入锁先尝试获取并更新status 值,如果status == 0 示意没有其余线程在执行同步代码,则把status 置为 1,以后线程开始执行。如果status != 0,则判断以后线程是否是获取到这个锁的线程,如果是的话执行status+1,且以后线程能够再次获取锁。而非可重入锁是间接去获取并尝试更新以后status 的值,如果status != 0 的话会导致其获取锁失败,以后线程阻塞。
    • 开释锁时,可重入锁同样先获取以后status 的值,在以后线程是持有锁的线程的前提下。如果status-1 == 0,则示意以后线程所有反复获取锁的操作都曾经执行结束,而后该线程才会真正开释锁。而非可重入锁则是在确定以后线程是持有锁的线程之后,间接将status 置为 0,将锁开释。

- 独享锁 / 共享锁

  • 独享锁和共享锁同样是一种概念。
  • 独享锁

    • 也叫排他锁,是指该锁一次只能被一个线程所持有。
    • 如果线程 T 对数据 A 加上排它锁后,则其余线程不能再对 A 加任何类型的锁。
    • 取得排它锁的线程即能读数据又能批改数据。JDK 中的synchronized 和 JUC 中Lock 的实现类就是互斥锁。
  • 共享锁

    • 指该锁可被多个线程所持有。
    • 如果线程 T 对数据 A 加上共享锁后,则其余线程只能对 A 再加共享锁,不能加排它锁。
    • 取得共享锁的线程只能读数据,不能批改数据。
  • 独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的办法,来实现独享或者共享。通过 ReentrantLockReentrantReadWriteLock 的源码来介绍独享锁和共享锁。



    • ReentrantReadWriteLock 有两把锁:ReadLockWriteLock,由词知意,一个读锁一个写锁,合称“读写锁”。再进一步察看能够发现 ReadLockWriteLock 是靠外部类 Sync 实现的锁。Sync 是 AQS 的一个子类,这种构造在 CountDownLatchReentrantLockSemaphore 外面也都存在。
    • ReentrantReadWriteLock 外面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁形式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保障并发读十分高效,而读写、写读、写写的过程互斥,因为读锁和写锁是拆散的。所以 ReentrantReadWriteLock 的并发性相比个别的互斥锁有了很大晋升。
    • 读锁和写锁的具体加锁形式的区别:

      • 最开始提及 AQS 的时候提到了state 字段(int 类型,32 位),该字段用来形容有多少线程获持有锁。
      • 在独享锁中这个值通常是 0 或者 1(如果是重入锁的话state 值就是重入的次数),在共享锁中state 就是持有锁的数量。
      • 然而在ReentrantReadWriteLock 中有读、写两把锁,所以须要在一个整型变量state 上别离形容读锁和写锁的数量(或者也能够叫状态)。
      • 于是将 state 变量“按位切割”切分成了两个局部,高 16 位示意读锁状态(读锁个数),低 16 位示意写锁状态(写锁个数)。
    • 写锁的加锁源码:
      • 这段代码首先取到以后锁的个数 c,而后再通过 c 来获取写锁的个数 w。因为写锁是低 16 位,所以取低 16 位的最大值与以后的 c 做与运算(int w = exclusiveCount(c);),高 16 位和 0 与运算后是 0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。
      • 在取到写锁线程的数目后,首先判断是否曾经有线程持有了锁。如果曾经有线程持有了锁(c!=0),则查看以后写锁线程的数目,如果写线程数为 0(即此时存在读锁)或者持有锁的线程不是以后线程就返回失败(波及到偏心锁和非偏心锁的实现)。
      • 如果写入锁的数量大于最大数(65535,2 的 16 次方 -1)就抛出一个 Error。
      • 如果当且写线程数为 0(那么读线程也应该为 0,因为下面曾经解决 c!= 0 的状况),并且以后线程须要阻塞那么就返回失败;如果通过 CAS 减少写线程数失败也返回失败。
      • 如果 c =0,w= 0 或者 c >0,w>0(重入),则设置以后线程或锁的拥有者,返回胜利!
      • tryAcquire() 除了重入条件(以后线程为获取了写锁的线程)之外,减少了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,起因在于:必须确保写锁的操作对读锁可见,如果容许读锁在已被获取的状况下对写锁的获取,那么正在运行的其余读线程就无奈感知到以后写线程的操作。
      • 因而,只有期待其余读线程都开释了读锁,写锁能力被以后线程获取,而写锁一旦被获取,则其余读写线程的后续拜访均被阻塞。写锁的开释与 ReentrantLock 的开释过程根本相似,每次开释均缩小写状态,当写状态为 0 时示意写锁已被开释,而后期待的读写线程才可能持续拜访读写锁,同时前次写线程的批改对后续的读写线程可见。
    • 读锁的代码:

      • tryAcquireShared(int unused) 办法中,如果其余线程曾经获取了写锁,则以后线程获取读锁失败,进入期待状态。
      • 如果以后线程获取了写锁或者写锁未被获取,则以后线程(线程平安,依附 CAS 保障)减少读状态,胜利获取读锁。
      • 读锁的每次开释(线程平安的,可能有多个读线程同时开释读锁)均缩小读状态,缩小的值是“1<<16”。
      • 所以读写锁能力实现读读的过程共享,而读写、写读、写写的过程互斥。
  • 实例:

    • 对于 JavaReentrantLock 而言,其是独享锁。然而对于Lock 的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
    • 读锁的共享锁可保障并发读是十分高效的,读写,写读,写写的过程是互斥的。
    • 独享锁与共享锁也是通过 AQS 来实现的,通过实现不同的办法,来实现独享或者共享。
    • 对于Synchronized 而言,当然是独享锁。

- 互斥锁 / 读写锁

  • 下面讲的独享锁 / 共享锁就是一种狭义的说法,互斥锁 / 读写锁就是具体的实现。
  • 实例:

    • 互斥锁在 Java 中的具体实现就是ReentrantLock
    • 读写锁在 Java 中的具体实现就是ReadWriteLock

- 分段锁

  • 分段锁其实是一种锁的设计,并不是具体的一种锁,对于 ConcurrentHashMap 而言,其并发的实现就是通过分段锁的模式来实现高效的并发操作。
  • 分段锁的设计目标是细化锁的粒度,当操作不须要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
  • 实例:

    • ConcurrentHashMap来说一下分段锁的含意以及设计思维,ConcurrentHashMap中的分段锁称为 Segment,它即相似于HashMap(JDK7 与 JDK8 中HashMap 的实现)的构造,即外部领有一个Entry 数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment 继承了ReentrantLock )。

- 乐观锁与乐观锁的基本原理

  • 乐观锁做事比拟乐观,它认为 多线程同时批改共享资源的概率比拟高,于是很容易呈现抵触,所以访问共享资源前,先要上锁
  • 乐观锁做事比拟乐观,它假设抵触的概率很低,它的工作形式是:先批改完共享资源,再验证这段时间内有没有发生冲突,如果没有其余线程在批改资源,那么操作实现,如果发现有其余线程曾经批改过这个资源,就放弃本次操作

-CAS 的原理

  • 在计算机科学中,比拟和替换(Conmpare And Swap)是用于实现多线程同步的原子指令。
  • 它将内存地位的内容与给定值进行比拟,只有在雷同的状况下,将该内存地位的内容批改为新的给定值,这是作为单个原子操作实现的。
  • 原子性保障新值基于最新信息计算;如果该值在同一时间被另一个线程更新,则写入将失败。

-CAS 的优缺点

  • ABA 问题

    • 如果变量 V,首次读取时是 A 值,并且在筹备赋值的时候,查看到它依然是 A 值,这样是否阐明它的值,没有被其余线程批改过?答案是否定的,因为在这段时间内,它的值可能被更改为其余的值,而后又改回成了 A 值,那 CAS 操作就会误认为它素来没有被批改过。这个问题,被称为 CAS 操作的ABA 问题。
    • JDK1.5当前的 AtomicStampedReference 类提供了这样的性能,其中的compareAndSet() 办法,就是首先查看以后援用是否等于预期援用,并且以后标记是否等于预期标记,如果全副相等,才会以原子的形式,将该援用和该标记的值,设置为给定的更新值。
  • 循环工夫长,开销大

    • 自旋 CAS(也就是不胜利就始终循环执行直到胜利),如果长时间不胜利,会给 CPU 带来十分大的执行开销。如果JVM 能反对处理器提供的 pause 指令,那么效率会有肯定的晋升,pause指令有两个作用,第一,它能够提早流水线执行指令(de-pipeline), 使 CPU 不会耗费过多的执行资源,提早的工夫取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它能够防止在退出循环的时候,因内存程序抵触(memory order violation),而引起 CPU 流水线被清空(CPU pipeline flush),从而进步 CPU 的执行效率。
  • 只保障单个共享变量的原子操作

    • CAS只对单个共享变量无效,当操作波及跨多个共享变量时,CAS操作有效。然而从 JDK1.5 开始,提供了 AtomicReference 类来保障援用对象之间的原子性,你能够把多个变量放在一个对象里,来进行 CAS 操作。所以能够应用锁,或者利用 AtomicReference 类,把多个共享变量合并成一个共享变量来操作。

- 锁的打消,粗化,降级,降级

  • 锁的打消

    • JIT 编译器(Just In Time 编译器)能够动静编译同步代码时,实用于一种叫做逃逸剖析的技术,来通过该项技术判断程序中所应用的锁对象是否只被一个线程所应用,而没有分布到其余线程中;如果状况就是这样的话,那么 JIT 编译器在编译这个同步代码时就不会生成synchronized 关键字所标识的锁的申请与开释机器码,从而打消了锁的应用流程。
  • 锁的粗化

    • JIt 编译器在执行动静编译时,若发现前后相邻的synchronized 块应用的是同一个锁对象,那么它就会把这几个synchronized 块合并为一个较大的同步块,这样做的益处在于线程在执行这些代码时,就不必频繁的申请和开释锁了,从而达到申请与开释锁一次,就能够执行齐全部的同步代码块,从而晋升了性能。
  • 锁的降级和降级

    • 锁的降级和降级次要都是对象头中通过Mark Word 中的锁标记位与是否是偏差锁标记位来实现的;

- 互斥锁与自旋锁的概念与区别?

  • 当曾经有一个线程加锁后,其余线程加锁则就会失败,互斥锁和自旋锁对于加锁失败后的解决形式是不一样的:

    • 互斥锁 加锁失败后,线程会 开释 CPU,给其余线程;
    • 自旋锁 加锁失败后,线程会 忙期待,直到它拿到锁;
  • 互斥锁是一种「独占锁」,比方当线程 A 加锁胜利后,此时互斥锁曾经被线程 A 独占了,只有线程 A 没有开释手中的锁,线程 B 加锁就会失败,于是就会开释 CPU 让给其余线程,既然线程 B 开释掉了 CPU,天然线程 B 加锁的代码就会被阻塞

    • 对于互斥锁加锁失败而阻塞的景象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被开释后,内核会在适合的机会唤醒线程,当这个线程胜利获取到锁后,于是就能够继续执行。
    • 所以,互斥锁加锁失败时,会从用户态陷入到内核态,让内核帮咱们切换线程,尽管简化了应用锁的难度,然而存在肯定的性能开销老本。
    • 那这个开销老本是什么呢?会有 两次线程上下文切换的老本

      • 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,而后把 CPU 切换给其余线程运行;
      • 接着,当锁被开释时,之前「睡眠」状态的线程会变为「就绪」状态,而后内核会在适合的工夫,把 CPU 切换给该线程运行。
    • 所以,如果你能确定被锁住的代码执行工夫很短,就不应该用互斥锁,而应该选用自旋锁,否则应用互斥锁。
  • 自旋锁是通过 CPU 提供的CAS 函数(Compare And Swap),在「用户态」实现加锁和解锁操作,不会被动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。

    • 个别加锁的过程,蕴含两个步骤:

      • 第一步,查看锁的状态,如果锁是闲暇的,则执行第二步;
      • 第二步,将锁设置为以后线程持有;
    • 自旋锁是最比较简单的一种锁,始终自旋,利用 CPU 周期,直到锁可用。须要留神,在单核 CPU 上,须要抢占式的调度器(即一直通过时钟中断一个线程,运行其余线程)。否则,自旋锁在单 CPU 上无奈应用,因为一个自旋的线程永远不会放弃 CPU。
  • 自旋锁与互斥锁应用层面比拟类似,但实现层面上齐全不同:当加锁失败时,互斥锁用「线程切换」来应答,自旋锁则用「忙期待」来应答

- 读写锁的优先级辨别?

  • 读写锁从字面意思咱们也能够晓得,它由「读锁」和「写锁」两局部形成,如果只读取共享资源用「读锁」加锁,如果要批改共享资源则用「写锁」加锁。
  • 读写锁的工作原理是:

    • 当「写锁」没有被线程持有时,多个线程可能并发地持有读锁,这大大提高了共享资源的拜访效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会毁坏共享资源的数据。
    • 然而,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其余写线程的获取写锁的操作也会被阻塞。
  • 写锁是独占锁,因为任何时刻只能有一个线程持有写锁,相似互斥锁和自旋锁,而读锁是共享锁,因为读锁能够被多个线程同时持有。
  • 读优先锁冀望的是,读锁能被更多的线程持有,以便进步读线程的并发性,它的工作形式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 依然能够胜利获取读锁,最初直到读线程 A 和 C 开释读锁后,写线程 B 才能够胜利获取读锁。

- 死锁的概念

  • 死锁是这样一种情景:多个线程同时被阻塞,它们中的一个或者全副都在期待某个资源被开释。因为线程被无限期地阻塞,因而程序不可能失常终止。
  • Java 中死锁产生的四个 必要条件

    • 互斥条件 ,即当资源被一个线程应用(占有) 时,别的线程不能应用;
    • 不可抢占,资源请求者不能强制从资源占有者手中篡夺资源,资源只能由资源占有者被动开释;
    • 申请和放弃,即当资源请求者在申请其余的资源的同时放弃对原有资源的占有;
    • 循环期待,即存在一个期待队列:P1 占有 P2 的资源,P2 占有 P3 的资源,P3 占有 P1 的资源。这样就造成了一个期待环路。
  • 当上述四个条件都成立的时候,便造成死锁。当然,死锁的状况下如果突破上述任何一个条件,便可让死锁隐没。

- 死锁产生的场景

  • 产生死锁的场景有:

    • 程序加锁时导致死锁;
    • 相互协作对象调用导致死锁;
    • 动静加锁时导致死锁;

- 死锁的定位

  • 在咱们理论我的项目中,为了解决死锁问题,前提是须要先定位到死锁产生的具体位置。
  • 死锁实例代码:
public class Deadlock {
    public static String obj01 = "objA";
    public static String obj02 = "objB";

    public static void main(String[] args) {LockA lockA = new LockA();
        LockB lockB = new LockB();
        new Thread(lockA).start();
        new Thread(lockB).start();}

    public static class LockA implements Runnable {
        @Override
        public void run() {
            try {System.out.println(new Date().toString() + "LockA 开始执行");
                synchronized (Deadlock.obj01) {System.out.println(new Date().toString() + "LockA 锁住对象 objA");
                    Thread.sleep(1000);
                    synchronized (Deadlock.obj02){System.out.println(new Date().toString() + "LockA 锁住对象 objB");
                        Thread.sleep(60 * 1000); // 为测试,占用了就不放
                    }
                    System.out.println(new Date().toString() + "LockA 开释对象 objA");
                }
                System.out.println(new Date().toString() + "LockA 开释对象 objB");
            } catch (Exception e) {e.printStackTrace();
            }
        }
    }

    public static class LockB implements Runnable {
        @Override
        public void run() {
            try {System.out.println(new Date().toString() + "LockB 开始执行");
                synchronized (Deadlock.obj02) {System.out.println(new Date().toString() + "LockB 锁住对象 objB");
                    Thread.sleep(1000);
                    synchronized (Deadlock.obj01){System.out.println(new Date().toString() + "LockB 锁住对象 objA");
                        Thread.sleep(60 * 1000); // 为测试,占用了就不放
                    }
                    System.out.println(new Date().toString() + "LockA 开释对象 objB");
                }
                System.out.println(new Date().toString() + "LockA 开释对象 objA");
            } catch (Exception e) {e.printStackTrace();
            }
        }
    }
}
  • 运行以上实例代码,而后通过一下办法对死锁进行定位:

      1. 应用命令jstack -l [pid] 进行定位
      • 首先在我的项目控制台输出命令jps,找到死锁的线程 pid
>jps
13136 Deadlock
15952 Launcher
18784 Launcher
9172 RemoteMavenServer36
20376 Jps
11996
- 而后发现 pid 为 13136 的线程,产生了死锁
  - 而后输出命令 `jstack -l 13136`,打印出线程具体信息
     - ![image.png](https://cdn.nlark.com/yuque/0/2021/png/3010199/1627740718472-84b9a835-a6b9-4799-998b-d0ce890e0058.png#align=left&display=inline&height=416&margin=%5Bobject%20Object%5D&name=image.png&originHeight=636&originWidth=987&size=59400&status=done&style=shadow&width=646)
  - 能够看到具体死锁产生的办法和代码行数,不便咱们解决死锁问题。
    1. 应用JConsole 图形化工具,定位死锁
    • 在命令控制台输出jconsole,运行工具

      • 抉择我的项目运行的过程连贯
      • 进入图形化监控界面后,进入线程界面,有个检测死锁的按钮,会自动检测死锁线程
      • 工具找到具体死锁的代码地位。

- 如何防止死锁

  • 零碎中产生死锁会导致多个工作无奈执行,造成无尽的期待,重大时会导致系统的解体等重大事故。
  • 为了防止死锁,得先从死锁的四个条件动手:

    • 1. 互斥;2. 不可剥夺;3. 循环期待;4. 申请放弃;
  • 具体方法:

      1. 防止应用多个锁,且只有须要时才持有锁,嵌套的synchronized 或者lock 非常容易呈现问题。
      1. 固定加锁的程序,所有线程尽量以固定的程序来取得锁。
      1. 超时主动开释锁,应用带超时的办法,为程序带来更多可控性。
      • Object.wait() 或者CountDownLatch.await(),都反对所谓的 timed wait,指定超时工夫,并为无奈失去锁时筹备退出逻辑。
      • 应用显式 Lock 锁,在获取锁时应用 tryLock() 办法。当期待 超过时限 的时候,tryLock() 不会始终期待,而是返回错误信息。
if(lock.tryLock() || lock.tryLock(timeout, unit)){// ...}
退出移动版