ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

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

2022-02-23 19:31:33  阅读:197  来源: 互联网

标签:Java 对象 Mark CAS 线程 Word 机制 锁为


Java对象结构和内部锁

Java对象结构

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

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

对象头

  • Mark Word

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

  • Class Pointer

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

Java内置锁

我们知道了Mark Word中含有对象的锁信息,那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中的线程有资格获取锁进入的队列

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

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

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

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

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

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

这些方法都由监视器完成,监视器在底层交给了操作系统来实现,即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时锁释放。

标签:Java,对象,Mark,CAS,线程,Word,机制,锁为
来源: https://www.cnblogs.com/ekikun/p/15928917.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有