Java Web基础篇之漫谈Java锁

1、Java中的锁分类

乐观锁 & 悲观锁

  • 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
  • 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。

  • 悲观锁在Java中的使用,就是利用各种锁。
  • 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新;包括Synchronized锁优化中使用的偏向锁、轻量级锁、自旋锁。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。Java中Synchronized与ReentrantLock都是可重入锁。

独享锁 & 共享锁

  • 独享锁是指该锁一次只能被一个线程所持有(ReentrantLock, Synchronized)。
  • 共享锁是指该锁可被多个线程所持有(ReadWriteLock.ReadLock)。

互斥锁 & 读写锁

上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

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

公平锁 & 非公平锁

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

参考:
Java中的锁分类


2、Synchronized实现锁

实现原理

  • 同步代码块的实现是通过monitorenter和monitorexit 指令,执行monitorenter指令时当前线程将试图获取对象锁所对应的 monitor 的持有权,方法是正常结束还是异常结束完成monitorexit指令。

  • 同步方法的实现是依靠方法修饰符上的ACC_SYNCHRONIZED 来完成的,当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程需要先去获取monitor。

两种方式本质上都是对一个对象的监视器的获取,这个获取过程是排他的,也就是同一时刻只有一个线程获取到由synchronized所保护对象的监视器。
对象、监视器、同步队列与执行线程之间的关系如下图:
 对象、监视器、同步队列与执行线程之间的关系

监视器的内部机制:
当有多个线程同时请求某个对象监视器时,对象监视器会设置几种状态来区分请求的线程:

  • Contention List:所有请求锁的线程被首先放置在该竞争队列中,
  • Entry List:Contention List 中有机会获得锁的线程被放置到Entry List
  • Wait Set:调用wait()方法被阻塞的线程被放置到Wait Set中
  • OnDeck:任何一个时候只能有一个线程竞争锁 该线程称作OnDeck
  • Owner:获得锁的线程成为Owner
  • !Owner:释放锁的线程

转换关系如下图:
监视器实现

Java虚拟机对synchronized的优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。偏向锁的撤销与获取流程图如下:
偏向锁的撤销与获取流程图

扫描二维码关注公众号,回复: 6200762 查看本文章

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

Mark Word标识位

JVM运行时区域:
JVM运行时区域
锁对应的Mark Word标识位:
Mark Word在不同锁状态下的标志位存储

轻量级锁及膨胀流程图

轻量级锁及膨胀流程图

三种锁对比:

三种锁对比

  • 偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。
  • 而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。
  • 可见偏向锁,轻量级锁,自旋锁都是乐观锁。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

参考:
Java中常用的锁机制
Java锁浅谈
从volatile和synchronized的底层实现原理看Java虚拟机对锁优化所做的努力
深入理解Java并发之synchronized实现原理
彻底了解synchronized(推荐)
synchronized实现之对象监视器monitor的实现
Java 中的锁 – 偏向锁、轻量级锁、自旋锁、重量级锁


3、ReentrantLock实现锁

AQS实现

队列同步器(简称:同步器)AbstractQueuedSynchronizer(英文简称:AQS,也是面试官常问的什么是AQS的AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

同步器提供的模板方法:

同步器提供的模板方法

同步器可重写的方法:

同步器可重写的方法

AQS内部类:

 AQS内部类

ConditionObject内部类

使用synchronized的时候是使用wait和notify进行线程间通信,使用ReentrantLock的时候是使用Condition实现的线程间通信,而这正是AbstractQueuedSynchronizer帮我们进一步封装的Condition接口:
 Condition接口

Node内部类

同步队列的基本结构:
同步队列的基本结构

同步状态:
同步状态

同步状态被设计为是AQS中的一个整形变量,用于表示当前共享资源的锁被线程获取的次数,并且是多线程可见的。
(1)如果是独占式的话state的值0表示该共享资源没有被其他线程所锁住可以被使用,其他值表示该锁被当前线程重入的次数;例如下文中的重入锁ReentrantLock。

(2)如果是共享式,该 state值被分为高16位和低16位,高16位表示读状态,低16位表示写状态,用一个整形维护多种状态。例如:ReentrantReadWriteLock实现读写锁,用整数state表示读写锁状态,关于ReentrantReadWriteLock后期会介绍

ReentrantLock的类图结构

ReentrantLock的类图结构

Sync内部类

实现的接口

同步状态维护

前边介绍到的AQS支持独占式获取与释放同步状态、共享式获取与释放同步状态。而ReentrantLock被设计为独占式的获取与释放同步状态,意思就是排他锁,即同一时刻只能有一个线程获取到锁,这里的同步状态是AQS的一个全局变量state

摘自:
Java中的队列同步器AQS和ReentrantLock锁原理简要分析

相关:
读写锁ReentrantReadWriteLock深入分析
Java多线程系列–“JUC锁”03之 公平锁(一)
Java多线程系列–“JUC锁”04之 公平锁(二)
Java多线程系列–“JUC锁”05之 非公平锁


4、Synchronized与ReentrantLock区别

Synchronized与ReentrantLock的区别

参考:
Java中常用的锁机制
Java并发编程:Lock


5、锁优化的几条建议

  • 使用乐观锁(CAS)
  • 减少锁的粒度
  • 减少锁持有的时间
  • 分段锁技术(参考ConcurrentHashMap实现)
  • 使用读写锁代替排他锁

参考:
关于锁优化的几点建议

猜你喜欢

转载自blog.csdn.net/zangdaiyang1991/article/details/90074935