Java并发编程中, 锁机制对控制线程间共享内存的使用有重要的意义. 那么在Java内部锁是如何实现的呢?
首先要明确一个概念.
Java中的锁是对象级别的概念, 也就是每个对象都天生可以作为一个锁使用.
究其底层实现, 实际上锁是存在于Java对象头的MarkWord
字段里的, 根据锁的级别, 存储结构不同, 但是都存在一个2bit的锁标识位.
悲观锁是synchronize
内部的实现机制, java 1.6之后, 对悲观锁进行了改进, 也就是现在不是所有同步操作都一定会被阻塞. 如今的悲观锁按照锁的获取与释放方式被划分为偏向锁, 轻量级锁, 重量级锁. 锁标识位分别为01, 00, 10. 这几种状态会随着锁的竞争情况逐渐升级(但是不会降级)
偏向锁是一种出现竞争才释放锁的机制.
具体来说, 作为锁的对象头部会保存一个偏向锁标识
, 存储当前获得锁的线程ID. 假设当前是线程A获得了锁, 但是线程A执行完同步块之后并不释放锁, 而是等到其他线程竞争之后才进行锁的释放. 为什么要这样设计呢? 因为如果下次还是线程A来访问同一同步块, 那么就无需通过CAS操作来重新对锁对象的对象头进行更新, 简单比对一下线程id是否相等即可.
以下是偏向锁的获取与释放流程图:
对上图的一点补充解释:
在竞争偏向锁的时候, 需要对持有偏向锁的线程进行检查, 如果线程已死, 则自动解锁, 将锁对象的对象头的markword中的线程id置空, 如果线程当前已不使用同步块, 解锁, 将锁对象的markword中额线程id置空, 并恢复线程. 同时其他线程可以通过CAS操作将锁对象的对象头markword中的对象id设置为自己的id, 也就是获得了偏向锁.
当偏向锁的撤销与获取经常出现的时候(也就是锁的竞争经常发生的时候), 就会升级为轻量级锁.
线程通过CAS获取轻量级锁的方法与获取偏向锁的过程不同, 它总是将锁对象的Markword先拷贝到栈空间, 然后试图通过CAS更新Markword, 如果成功说明获得了锁, 如果失败说明其他线程成功更新了Markword(也就是获得了锁), 则通过自旋CAS尝试重新获取锁(始终消耗CPU时间片), 如果在较短的时间内成功, 则锁不会升级, 如果长时间未获得锁, 则锁会升级到重量级锁.
重量级锁在线程获得锁失败时不会进行自旋CAS尝试重新获取锁, 而是会进入阻塞态(让出CPU时间片, 在其他线程释放锁后由CPU调度重新进行锁竞争).
三种锁并没有好坏之分, 分别有各自适用的场合:
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 在没有锁竞争的情况下, 加锁极快, 且无需解锁 | 如果较多线程竞争锁, 会使得加解锁的过程额外费时 | 长时间一个线程访问同步块的场景 |
轻量级锁 | 竞争线程不会阻塞, 响应快 | 如果长时间无法获得锁, 自旋CAS会带来较大的CPU开销 | 追求响应时间, 且同步块执行速度块的场景 |
重量级锁 | 线程无法获取锁时总是阻塞, 及时让出CPU资源 | 线程阻塞, 需要通过调度重新竞争锁, 响应缓慢 | 追求高吞吐量, 且同步块执行时间较长的场景, 如包含大量I/O操作 |
乐观锁允许所有的线程在不发生阻塞的情况下创建一份共享内存的拷贝. 这些线程接下来可能会对它们的拷贝进行修改,并企图把它们修改后的版本通过CAS操作写回到共享内存. 如果, 另一个线程已经修改了共享内存, 这个线程将不得不再次获得一个新的拷贝, 在新的拷贝上做出修改, 并尝试再次通过CAS把它们写回到共享内存中去.
称之为"乐观锁"的原因就是, 线程获得它们想修改的数据的拷贝并做出修改, 乐观的假设在此期间没有其他线程对共享内存做出修改, 当这个乐观假设成立时, 这个线程仅仅在无锁的情况下完成共享内存的更新. 当这个假设不成立时, 线程所做的工作就会被丢弃.
无论乐观假设成功或者失败, 乐观锁总是没有使用锁进行并发的控制.
乐观锁仅适用于共享内存竞用不是非常高的情况.
如果共享内存上的需要修改的内容非常多, 那么会因为CAS更新共享内存失败, 从而浪费大量的CPU周期用在重新拷贝和修改上.
AtomicInteger
等.