一文带你理解Java锁们:乐观锁,悲观锁,公平锁,轻量级锁,锁粗化...

·  阅读 1492

乐观锁

乐观锁时常抱有乐观的想法,即默认读多写少,且遇到并发写入的可能性低。所以不会直接上锁,而是在每次更新的时候,比较版本号,如果版本号一致,则更新,如果不一致,则失败进行重读。

CAS Compare and Swap

CAS 就是一种常见的乐观锁实现。他包含3个参数CAS(V,E,N),V表示被更新的变量,E表示旧的预期值,N表示新值。只有V等于E时,才会对V值进行更改,把V更改成N。如果V值和E值不一样,说明这个变量已经被其他线程改动了,CAS更改失败。

CAS 导致的ABA问题

  • 线程A取出V变量的值V1
  • 与此同时,线程B取出V变量的值V1,并对V进行了操作,将他的值变成了V2,接下来又将变量V的值由V2改回了V1
  • 线程A接下来进行CAS操作,此时因为线程B已经把值给改回V1了,CAS的操作会成功
  • 尽管成功,在这期间,其实变量V的值已经被修改过了
    这个问题就可以通过版本号解决,每次执行修改操作,都会带上一个版本号,只有版本号和数据版本号一致,才可以执行,成功后将版本号+1

悲观锁

悲观锁和乐观锁正好相反。悲观锁默认写入操作多,而且会经常遇见并发写入的操作,所以每次读写数据时都会上锁。每次修改和读数据时,都需要拿到锁才可以。常见的悲观锁实现有Synchronized 和 ReentrentLock

Synchronized

Synchronized是独占式的悲观锁,可以把任意一个非NULL对象当作锁。

Synchronized 作用范围

  • 修饰实例方法
  • 修饰静态方法,锁住的是Class实例,因此,静态方法锁会锁住所有调用改方法的线程
  • 作用于方法块

Sychronized核心组件

  • Wait Set: 被wait方法阻塞的线程放置区
  • Contention List:竞争队列,所有请求锁的线程所在地
  • Entry List:Contention List中获得了锁的候选资格的线程所在地
  • OnDeck:任意时刻,只有一个线程正在竞争锁资源,他被放在这个区
  • Owner:当前获得资源的线程
  • !Owner:当前释放锁的线程

ReentrentLock

ReentrentLock继承Lock接口,是一种可重入锁,他在Synchronized的基础上,还提供了可响应中断,定时锁等方法

Lock接口内的重要方法

  • void lock():如果锁处于空闲状态,当前线程获取到锁
  • boolean tryLock():如果锁可用,立即返回true,反之false
  • void unlock():当前线程释放持有的锁
  • Condition newCondition():条件对象,与当前的锁绑定,调用其中的await方法后,线程释放锁。调用signal方法,唤醒wait线程

Sychronized 和 ReentrentLock的区别

  • ReentrentLock显示的获得,释放锁,而Sychronized隐式的获得,释放锁
  • ReentrentLock可响应中断锁,Sychronized不可以
  • ReentrentLock是API级别的,Sychronized是JVM级别的
  • ReentrentLock可以实现公平锁,在新建的时候设置就好
  • ReentrentLock可以通过Condition绑定条件
  • ReentrentLock发生异常,如果没有unlock,很可能出现死锁,所以一定要由finally模块,进行对锁的释放,Sychronized发生异常会自动释放线程占用的锁

Semaphore 信号量

是一种基于计数的信号量,可以设定一个阈值,多个线程竞争信号,超过阈值的线程数后,新的线程再申请信号可能会被阻塞
调用acquire()和release()进行锁住和解锁

可重入锁

指的是同一线程的外层函数获得锁后,内层递归的函数仍然可以获取该锁。

公平锁和非公平锁

公平锁

获得锁的线程顺序遵循先来先得原则,优先排队等待的线程会比后来的线程现获取到锁。每次都需要维护一个公平队列,来保证实现先到先得。

非公平锁

不存在排队等待问题,每次都会直接尝试获取锁。因为相比较公平锁而言,不存在排队的问题,所以会性能比公平锁高很多。

重量级锁 Mutex Lock

Synchronized是通过对象内部的monitor监视器锁来实现的,而这个moniter本质又是依赖于操作系统的Mutex Lock来实现。由于涉及到操作系统和线程的切换,导致使用synchronzied的成本高,效率低,而我们也把这种需要涉及操作系统Mutex Lock的锁称之为“重量级锁”,为了提高性能,我们引入了优化的轻量级锁

轻量级锁

轻量级锁的“轻量级”,其实是相对于重量级锁而言的。本意是再没有多线程竞争的前提下,减少重量级锁的高成本。因此,轻量级锁适用于线程交替执行同步块,一旦出现多个线程竞争同一把锁的情况,轻量级锁就会膨胀成重量级锁。

偏向锁

在很多情况下,锁不仅不存在多线程竞争,反而是经常由某一线程长期多次获得。因此,偏向锁的引入便是为了实现在某个线程获取到锁后,消除这个线程再次重入锁的开销,形成一种对这个线程的偏向。这种偏向的引入是为了在无多线竞争的情况下尽量减少不必要的开销,因此,偏向锁在轻量锁的基础上进一步提高了性能。

锁优化

  • 减少锁的持有时间,只在有线程安全需要的时候才加锁
  • 减小锁粒度:把一个大对象的一个锁,拆成很多小对象,然后分别上锁,增加并行度,降低竞争。ConcurrentHashmap就是最好的例子,他使用了segment做到了锁粒度的减小。
  • 锁分离:根据功能分离,最好的例子:读锁和写锁
  • 锁粗化:要求每个线程持有锁的时间尽量短,而且使用完公共资源后,立刻释放锁
  • 锁消除:一旦编译时发现上锁的地方其实并不是一个共享对象,即并不需要锁维护线程安全,就可以消除这个锁。

volatile关键字

  • 其实volatile并不算是锁,但却也能实现线程安全。
  • 他实现了变量的可见性,即一个线程如果修改了带volatile关键字的变量的值,这个新的值立刻会被其他线程所获取。因为不存在上锁操作,所以也就不存在线程阻塞的情况,这使得volatile比synchronized更轻量。
  • 需要注意的是,不同volatile变量直接不能相互依赖。
分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改