Java锁详解

1 篇文章 0 订阅

Java中关于锁的使用,有Lock和synchronized两种,其中ReentrantLock是java.util.concurrent包下提供的一套互斥锁,而对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。

公平锁

是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁

是指多个线程获取的顺序并不是按照申请锁的顺序,有可能后申请的线程比

先申请的线程优先获取锁在高并发的情况下,有可能会造成优先级后传或者饥饿想象。

非公平锁会直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁

可重入锁

就是说某个线程已经获得锁了,可以再次获得该锁而不会发生死锁

悲观锁

当访问临界资源时,始终会加锁,上锁期间别人不能修改数据,直至操作完成才能释放锁。悲观锁会做出最坏的打算,认为别人会修改数据。

乐观锁

不会上锁,在更新临界资源时,会判断在此期间是否有人修改了数据,如果别人修改了数据,就放弃操作。乐观锁在执行的时候会做最好的打算,认为别人不会修改数据。

乐观锁主要有两种实现:CAS机制和版本号机制

Lock锁

ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

Lock 锁都是使用 AQS 同步器来实现锁,通过重写 tryAcquire 方法获取锁,通过 tryRelease 方法释放锁

AQS 结构:

1、ExclusiveOwner:表示当前持有锁的线程

2、state:一个 volatile int 整形,用来表示锁的添加情况,不同的锁有不同的实现,例如 ReentrantReadWriteLock 通过将 32 的 int 分为高 16 位与低 16 位分别表示读写锁的使用情况

3、AcquireQueue:等待队列,是一个双向的链表,当一个被阻塞的线程加入时,会将其前驱节点标记为 -1,表示在锁释放时,需要唤醒后继节点,利用双向链表的性质可以实现公平锁,ReentrantReadWriteLock 通过对链表添加 share/exclusive 属性来实现读写锁特性

4、Condition:可以通过 AQS 获取多个 Condition 对象,每一个 Condition 对象本质上就是一个等待队列,线程调用哪一个 Condition 的 await 方法,就将线程放入哪一个等待队列,同样的在唤醒时,调用哪个 Condition 的 signal 方法,就唤醒对应的线程

  • tryAcquire

    • 1、当一个线程尝试获取锁,会先判断 state 是否为 0,若为 0,进行加锁,若不为 0,判断 ExclusiveOwner 是否是自己,若是自己,则发生锁重入,加锁

    • 2、加锁时,使用 CAS 的方式为 state 变量加 1,若成功,则成功获取锁/锁重入,若失败,则获取锁失败

    • 3、线程获取锁失败,会进入 acquireQueue 方法

  • acquireQueue

    • 1、在 acquireQueue 方法中,线程会在一个死循环中不断尝试获取锁,只有在成功获取锁后才会退出循环

    • 2、在循环中,获取锁失败后,会使用 shouldParkAfterFail 方法,调用 unsafe 的park 方法阻塞线程

    • 3、若在 park 的过程中被 interrupt 打断,若调用的是 lock 获取锁,是不可打断锁逻辑,线程在被打断后会重新回到 while 循环中,继续尝试获取锁,并且会将打断标记重置,保证线程可以继续被 park;若调用的是 lockInterruptbly,则是可打断逻辑,调用的 acuireInterruptibly等待队列方法,在被打断后,通过直接抛出 InterruptException 异常的方式来终止等待

  • tryRelease

    • 1、因为支持可重入锁,必须释放到 state 等于 0 才算完全释放

    • 2、释放时,会将 ExclusiveOwner 重置为 null,并唤醒 EntryList 等待队列,EntryList 等待队列中所有的标记为 -1 的节点被唤醒后都会去唤醒其后继节点,被唤醒的节点会继续进入 acquireQueue方法获取锁

    • 3、若实现公平锁,则线程在被唤醒时,会检查其是否存在前驱节点,若存在则继续阻塞与 Sychronized 比较:可打断/可重入/可公平/多 Condition 等待条件

synchronized锁

默认为非公平锁

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

  • 从JDK1.6版本之后,synchronized本身也在不断优化锁的机制,有些情况下他并不会是一个很重量级的锁了。优化机制包括自适应锁、自旋锁、锁消除、锁粗化、轻量级锁和偏向锁。

  • 锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。

  • 偏向锁

    1、通过修改对象头中的偏向锁标记为 1,并且将 HashCode 与 Age 部分替换为当前持有偏向锁的线程 ID

    2、在线程获取偏向锁时,若 ThreadID 就是自己,就直接获取,没有额外开销,若 ThreadID不是指向自己,且没有发生争抢,就进行冲偏向

    3、偏向锁重偏向发生一定次数,JVM 就会认为不该添加偏向锁,并为当前类的所有对象以及以后创建的对象都撤销偏向锁,并将锁升级为轻量级锁

  • 轻量级锁

    1、轻量级锁的原理是通过在线程栈中创建一个锁记录,在锁记录中保存锁对象头的信息,并将标记加锁情况的两位改为 10 表示轻量级锁,将对象头中其他部分改为锁记录的指针

    2、在线程获取轻量级锁时,根据加锁情况的两位发现加了轻量级锁,若锁记录指向自己的线程栈,则在线程栈中再添加一条空的锁记录,用来表示锁重入,在释放时按后进先出的顺序弹出锁记录

    3、若当前持有轻量级锁的线程不是自己,则发生了争抢,但轻量级锁不会立刻升级为重量级锁,而是线程在原地 while 自旋一定次数后,若还没获取到锁,就升级为重量级锁,自旋的次数是 JVM动态决定的,根据自旋的成功与否对其进行加减

  • 重量级锁

    1、通过 monitor 管程来进行加锁,monitor 中主要有 Owner 指向当前持有锁的线程,EntryList 表示等待队列,以链表的方式存储被 Sychronized 阻塞的线程,WaitSet 表示调用了 wait 方法进行等待的线程队列

    2、在线程获取锁时,将对象头中的锁信息改为 00 表示重量级锁,并调用 monitorEnter 指令获取对象的 monitor 管程,将对象头中其他部分替换为 monitor 的指针,将 monitor 的 Owner 指向当前线程

    3、线程获取锁时,若对象已经加了重量级锁,就通过对象头的 monitor 指针找到 monitor,并根据 Owner判断是否是锁重入,如果不是,就进入等待队列 EntryList 中,并进入 Blocking 状态

    4、在线程释放锁时,会调用 monitorExit 指令进行释放,并唤醒 EntryList 中的线程,唤醒的线程可能会与新的尝试获取锁的线程争抢锁,并再次回到等待队列,因此是非公平锁

    5、持有锁的线程可以通过调用 wait 方法进入 wait 状态,由于 waitSet 属于 monitor 并只有一个,因此notify 与 notifyAll 会 随机唤醒一个/唤醒所有 waitSet 中的线程,粒度比 AQS 的 Condition 要大

synchronized和ReentrantLock的异同

  • 相似点

    他们加锁都是阻塞式同步加锁,即当一个线程获得了对象锁,其他访问该块的线程都被阻塞在同步块外

  • 区别

    1. 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。

    2. 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。

    3. 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。

    4. 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。

    5. 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值