Java锁机制(内部锁为核心)

Java对象结构和内部锁

Java对象结构

我们知道每一个Object类都自带锁,在了解锁之前我们先了解Java的对象结构。

Java锁机制(内部锁为核心)

Java的对象分为三个部分,主要的部分就是对象头和对象体,填充部分是因为JVM规定对象的起始地址必须为8字节的整数倍,所以在实例不满足8字节的整数倍情况下进行填充,对象头一定满足。

对象头

  • Mark Word

    长度为一个JVM字,取决于JVM为32位还是64位;包含了对象的关键信息,例如hashCode, GC分代年龄,锁状态标识,线程持有的锁等

  • Class Pointer

    指向对象所属类的Class对象,长度也为一个JVM Word,不过通常可以进行指针压缩

Java内置锁

我们知道了Mark Word中含有对象的锁信息,那Java内置锁有什么信息呢,我们可以分为以下几种状态,注意,这个状态的顺序只能由低到高,是不可逆的,随着竞争的情况升级。

Java锁机制(内部锁为核心)

Java锁机制(内部锁为核心)

无锁即对象不会被线程竞争,所以不需要锁,在它的Mark Word中也没有锁相关的信息。

这里补充一个概念:安全点是在程序执行期间的所有GC Root已知并且所有堆对象的内容一致的点。

偏向锁

如果一个线程获得了一个对象的锁,但是实际上,不会有其他的线程来和它竞争这个锁,那么此时运用偏向锁应该是最高效的。锁对象的Mark Word的结构中锁标记位变为01,并且记录下线程的ID和标志位,之后线程再次获取该锁时,比较Mark Word中的信息和自己的信息,如果一致就直接进入同步区,无需其他操作。

偏向锁的获取流程:

  • 访问Mark Word中偏向锁标志位是否设置成1,锁标志位是否为01——确认为可偏向状态。
  • 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
  • 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,线程Lock Recrod的obj指向锁对象,然后执行(5);如果竞争失败,执行(4)。
  • 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  • 执行同步代码。

偏向锁的释放:

如果出现其他线程竞争锁,检查锁拥有线程是否存活,如果非存活则直接撤销偏向锁,如果存活需要将该线程先挂起,然后升级为轻量级锁,这些操作需要在安全点进行。

轻量级锁

出现竞争时,锁转向轻量级锁。

当一个抢锁线程发现锁对象没有锁定时,它会在栈帧中记录拷贝一份当前锁对象的Mark Word,然后用CAS操作尝试修改对象Mark Word,把标记位修改为00,剩下内容替换为指向线程栈中Displaced Mark Word中的指针,这个操作如果成功,则说明线程此时占有锁对象,可以进入同步区。当锁重入时,DisPlaced Mark Word置空,obj指向对象头,表示为冲入锁。

与偏向锁不同的是,同步区方法执行完毕,轻量级锁会自动释放锁,这是一个CAS操作。

线程竞争过程中,如果一个线程无法CAS替换Mark Word成功,则自旋尝试替换,直到自旋次数达到阈值,膨胀为重量级锁 。

重量级锁

重量级锁采用了监视器机制,Java中每一个对象都关联一个监视器(Monitor),监视器保障代码的互斥,一次只能有一个线程进入临界区,得不到许可的线程将被阻塞。

膨胀到重量级锁后,会指向ObjectMonitor对象,它有以下核心变量:

  • _owner指针
  • cxq,锁先入队列
  • waitSet,阻塞队列
  • EntryList,cxq中的线程有资格获取锁进入的队列

线程抢占锁的流程图如下:

Java锁机制(内部锁为核心)

Cxq的本质其实是一个单向链表,链表头插实现队列,也就是说在链表头添加入队线程,从队尾获取线程;不过线程进入Cxq之前,会先自旋尝试获取锁,即将owner修改为自己,这样对Cxq中的线程不公平。

当owner线程释放锁时,cxq中的线程会进入entrylist,entrylist中的线程,通常为head节点会被指定为onDeck线程

JVM在释放锁后,让onDeck线程去参与锁竞争,这个过程中会和新入的抢锁线程竞争,所以还是不公平的。

如果一个正在执行的owner线程调用wait方法阻塞,将进入waitset队列,直到被主动唤醒。

下面是notify和wait的执行流程:

Java锁机制(内部锁为核心)

这些方法都由监视器完成,监视器在底层交给了操作系统来实现,即OS中熟悉的mutex互斥机制。

CAS与锁

CAS

CAS操作即比较并替换,在Java中通常通过调用Unsafe类的方法进行实现

Unsafe提供的方法为原子性的比较并替换方法,方法接收四个参数:

  • 所属的对象
  • 所在的位置(是一个long类型的偏移量)
  • 预期原值
  • 预期更新后的值

Unsafe类通过第一个参数和第二个参数计算对象的具体位置,然后依赖CPU的原子性操作,在相应的位置上获取值,与期望值相比较,如果符合期望值,就将其修改为新值,否则继续自旋更新直到成功

这里说的期望值是我们的方法在进行CAS操作时,先在内存获取的值,进入CAS操作时很可能被其他的线程修改了。

ABA问题

如果一个线程执行某个操作时预期值是A,而它正准备操作时,另一个线程进来把A修改为B,然后再修改为A,回到该线程,预期值仍是A,对它而言可以正确执行操作;然而事实是两个A不能等同于一个A。

解决的方法是可以给对象的引用设置一个唯一的标记,比较时不仅比较值还需要比较标记,二者相等时才能进行正确的修改。

乐观锁和悲观锁

悲观锁

悲观锁悲观的认为并发一定会造成数据冲突。

悲观锁的机制就是独占锁的机制,把同步区加锁防止并发访问,进入区后释放锁,悲观锁具有强烈的独占性和排他性。sychronized中的重量级锁就是典型的悲观锁。悲观锁又能分为共享锁和排他锁,共享锁为只读不写,排他锁为互斥访问的锁。

悲观锁的特点为安全性足够高,加锁和释放合理很难出现并发相关的问题,缺点为悲观锁的开销通常比较大,在竞争不大的情况下,频繁加锁释放,挂起线程会降低效率,同时有可能引起死锁。

乐观锁

乐观锁严格来说加的锁是比较轻量的,它更期望使用数据本身来解决,核心机制就是冲突检测和数据更新,这是乐观锁的思想,CAS是乐观锁的一种实现。

锁的其他类别

公平/非公平锁

这里的公平/非公平指的是线程获得锁的顺序是否按照锁申请的顺序得到的。

可中断/不可中断锁

sycronized使用的内置锁是不可中断锁,这里指的是否可以中断指的是抢锁的过程是否能响应中断,即其他线程调用的Thread.interrput方法是否能在抢锁过程中做出响应。

Lock中的lockInterruptibly和tryLock方法就可以响应抢锁时中断。

可重入锁/不可重入锁

通俗地介绍可重入的概念:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。

sycronized的可重入原理

主要依赖的是owner指针和计数器,在owenr指针指向某个线程后,计数器会置1,其他的线程申请锁将阻塞;如果是owner线程再次申请锁,将得到锁,并且将计数器递增;每次线程退出同步区后,计数器减一,直到计数器为0时锁释放。

上一篇:Gerrit管理员手记(1): Gerrit Code Review使用指南


下一篇:BUUCTF:voip