Java中的各种锁机制

产生死锁具备条件

  • 互斥条件: 该资源任意时刻都只能由一个线程占用
    • 无法破坏
  • 请求与保持条件: 一个进程因请求资源而阻塞时, 对以获取的资源不放
    • 解决方案: 一次性申请所有资源
  • 不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程强行剥夺, 只能自己使用完毕后才能释放资源
    • 解决方案: 占用部分资源的线程进一步申请其他资源时, 如果申请不到,主动释放已占有的资源
  • 循环等待条件: 若干线程直接形成一种头尾相连的循环等待资源的关系
    • 解决方案: 按序申请资源来预防, 释放资源则反序释放

Synchronized

Synchronized 原理

Synchrogazerd 是由 JVM 实现的一种互斥同步的一种方式, 被 Synchrogazerd 关键字修饰的程序块编译后的字节码生成了 monitorenter monitorexit 两个字节码指令
当虚拟机执行到 monitorenter 指令时, 首先尝试获取对象的锁, 如果这个对象没有锁定, 或者当前线程已经拥有了这个对象的锁, 把锁的计数器加一, 当执行 monitorexit 指令时将锁计数器减一, 当计数器为零时, 锁就释放了

JVM 对 Synchrogazerd 的优化

新增了几种锁及锁的升级
Synchrogazerd 优化后流程

偏向锁

对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。
偏向锁

轻量级锁

如果这个对象是无锁的,jvm就会在当前线程的栈帧中建立一个叫锁记录(Lock Record)的空间,用来存储锁对象的Mark Word 拷贝,然后把Lock Record中的owner指向当前对象。

JVM接下来会利用CAS尝试把对象原本的Mark Word 更新会Lock Record的指针,成功就说明加锁成功,改变锁标志位,执行相关同步操作。

如果失败了,就会判断当前对象的Mark Word是否指向了当前线程的栈帧,是则表示当前的线程已经持有了这个对象的锁,否则说明被其他线程持有了,继续锁升级,修改锁的状态,之后等待的线程也阻塞。
轻量级锁

自旋锁

即把线程进行阻塞操作之前先让线程自旋等待一段时间, 可能在等待期间其他线程已经解锁,这时就无须在让线程进行阻塞操作, 避免用户态到内核态的切换(及其消耗性能)
自旋锁

锁升级

锁升级

锁升级是不可逆的

当JVM检测到不同的竞争状况时,会自动切换到合适的锁实现,这就是锁的升级

  • 当没有竞争出现时,默认会使用偏向锁
    • JVM会利用CAS操作,在对象头上的Mark Word(标记词)部分设置线程ID,表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多情况下,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销
  • 如果有另一线程试图锁定某个被偏斜过的对象, JVM就会撤销偏向锁,切换到轻量级锁实现
  • 轻量级锁依赖CAS操作Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁,否则升级为重量级锁

ReentrantLock

ReentrantLock实现原理

锁的实现原理基本就是为了达到一个目的让所有的线程都能看到某种标记
Synchronized 是通过对象头中设置标记,而ReentrantLock 及所有Lock接口的实现类,都是通过用一个volatile修饰的int型变量,并保证每个线程都能拥有对改int的可见性和原子修改啊,起本质是所有的AQS框架

  1. AQS在内部定义了一个 volatile int state 变量,表示同步状态,当线程调用lock方法时,如果state=0说明没有任何线程占用共享资源的锁,可以获得锁并将state = 1,如果state=1,则说明线程目前正在使用共享变量,其他线程必须加入同步队列进行等待
  2. AQS通过内部Node内部类构成一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当线程获取锁失败后,就被添加到队列末尾
  3. AQS通过内部类 ConditionObject 构建等待队列,可有多个,当 condition 调用 wait()方法后,线程将会加入等待队列中,当调用signal()方法后,线程将从等待队列移动到同步队列中进行锁竞争
  4. AQS和Condition 各自维护了不同的队列,在使用lock和Condition的时候,其实就是两个队列的互相移动

Volatile

Java虚拟机提供轻量级的同步机制

  1. 保证可见性
    a. 保证次变量对所有线程的可见性.当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的,普通变量做不到这点
  2. 不保证原子性
    a. 基于volatile变量的运算在并发下不一定是安全的,在各个线程的工作内存,不存在一致性问题,(每次使用期都需要刷新到主内存)但是Java里面的运算并非原子操作,导致在并发性是一样不安全的
  3. 禁止指令重排
    a. 普通变量仅仅能保证该方法执行过程中得到正确结果,但是保证不了程序代码的执行顺序
    b. volatile由于内存屏障,可以保证避免指令重排的现象产生

指令重排

你写的程序,计算机并不是按照你写的那样去执行的

比如 singleton = new Singleton(); 这段代码分三步执行:

  1. 为 singleton 分配内存空间
  2. 初始化 singleton
  3. 将singleton 指向分配的内存地址
    执行顺序可能变成 1 -> 3 -> 2. 在单线程下不会出现问题. 在多线程下会导致一个线程获取到还没有初始化的实例.
    源代码 -> 编译器优化的重排 -> 指令并行也可能会重排 -> 内存系统也会重排 -> 执行
    处理器在进行指令重排的时候,考虑 数据间的依赖性

volatile 和 Synchronized

Synchronized 既能保证可见性,又能保证原子性,而 volatile 只能保证可见性,无法保证原子性

公平锁与非公平锁

非公平锁

主要表现在获取锁的行为上, 不是按照申请锁的时间前后等待线程分配锁, 当锁被释放后,所有线程都有机会竞争锁, 目的是为了提高性能, 缺点是可能会导致某些线程一直获取不到锁
Synchrogazerd 是非公平锁

公平锁

与非公平锁相反, 按照申请锁时间的先后顺序分配.性能没有非公平锁高
ReentrantLock 可以设置为公平锁

可重入锁

可重入锁是锁的一个基本要求,是为了解决自己锁死自己的情况.

比如,一个类中的同步方法调用另一个同步方法,如果Synchronized 不支持重入,进入方法二时当前线程获得锁,方法二中的方法一又要试图获取锁,此时不支持重入,他就要等待释放,把自己阻塞,自己锁死自己

Synchronized 实现可重入锁原理

在执行monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有的这个锁对象,就把这个锁的计数器 +1,本质就是通过这种方式实现了可重入锁

ReentrantLock 实现可重入锁原理

内部定义了同步器 Sync(既实现了 AQS, 又实现了 AOS, 提供了一种互斥锁持有的方式),其实就是加锁的时候通过CAS 操作,将线程对象放入到一个双向链表中,每次获取锁的时候,看下当前维护的哪个线程id和当前请求的线程ID是否一样,一样就可重入了

锁消除

指虚拟机及时编辑器在运行时,对一些代码要求同步,但被检测到不可能存在共享数据竞争的锁进行消除.主要根据逃逸分析

锁粗化

原则上,同步块的作用范围要尽量小,但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁进行互斥同步操作也会导致不必要的性能损耗
锁粗化就是增大锁的作用域

悲观锁

不管是否会产生竞争,任何的数据操作都必须加锁,用户态核心态转换,维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作
Synchronized 是悲观锁

乐观锁

每次去哪数据的时候都人为被人不会修改,所以不会上锁,但是在提交更新的时候回判断一下在此期间有没有去更新这个数据.核心算法是CAS(比较并交换)
避免了悲观锁独占对象的现象,同时也提高了并发性能

读写锁

多个可读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,只好等待对方操作结束,这样就可以保证不会读取到有争议的数据

StampedLock

提供类似读写锁的同时,还支持优化读模式,假设大多是情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过validate方法确认是否进入了写模式,如果没有进入,就成功避免了开销,如果进入,则尝试获取读锁

CAS(比较并交换)

比较当前工作内存中的值和主内存中的值,如果我期望的值达到了,那么久更新,否则就不更新一直循环

缺点:

  • 循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • ABA问题
    • 线程一从内存中取出A,此时线程二从内存中取出A,并且线程二进行了一些操作变成了B,然后又想数据变为A,此时线程一进行CAS操作时发现内存中仍然是A,然后线程一操作成功.虽然线程一的CAS操作成功了,但不代表这个过程没有问题
    • 解决方案,使用版本号,在变量前面追加版本号,每次变量更新时就把版本号加1

Synchronized 和 Lock 区别

  • Synchronized 内置的java关键字, Lock 是一个接口,有丰富的API
  • Synchronized 无法判断获取锁的状态, Lock 可以判断是否获取到了锁
  • Synchronized 会自动释放锁, Lock 必须要手动释放锁,否则会造成死锁
  • Synchronized 是非中断锁,必须等待线程执行完毕释放锁
  • Synchrogazerd 可以锁住方法和代码块, 而 Lock 只能锁住代码块
  • Lock可以使用读锁提高多线程读效率
  • Synchrogazerd 是非公平锁, ReentrantLock 可以选择公平锁和非公平锁
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值