【从面试题建立知识体系】JAVA面试题------------Synchronized

Synchronized原理

Synchronized是什么?

Synchronized是Java中为了解决并发安全而提供的一把"锁",也叫同步锁。为了保证同一时间只会有一个线程执行上锁内的代码,以此达到并发安全的效果。

Synchronized实现

Synchronized作为java中的关键字是怎么体现当前方法或者代码块被上锁了?当一个线程来执行被Synchronized修饰的代码块时,如何判断自己竞争到了这把锁?Synchronized是怎么保证只会有一个线程来执行的?

Monitor

通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖底层操作的Mutex lock(互斥锁)实现。具体指令分别是monitorenter和monitorexit。
monitorenter:进入并获取对象监视器。
monitorexit:释放并退出对象监视器。

官网对monitorenter指令的介绍,就是说每一个对象都会和一个监视对象monitor关联,监视器被占用时会被锁住,其他线程无法来获取该monitor。当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。大体过程如下:

  1. 若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1,当前线程成为monitor的owner(拥有这把锁的线程);
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1(记录线程拥有锁的次数);
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,知道monitor的进入数变为0,才能重新尝试获取monitor的所有权;

官网对monitorexit指令的介绍,就是说能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程;执行monitorexit会将monitor的进入数减1,当monitor的进入数减为0时,当前线程退出。
monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异常时退出释放锁;

什么是monitor

monitor可以理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。与万物皆对象一样,每一个的java对象都有成为Monitor的潜质,因为在java的设计中,每一个java对象都被内置了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说的Synchronized的对象锁,MarkWord锁标志位为10,其中指针指向的是Monitor对象的起始地址。在Java虚拟机中,Monitor是由ObjectMonitor实现的,其主要的数据结构如下。
在这里插入图片描述

Synchronized通过Monitor上锁过程

每个Java对象都可以关联一个Monitor对象,如果使用Synchronized给对象上锁之后,该对象头的Mark Word中就被设置指向Monitor对象的指针,将对象与Monitor对象关联。

如下图所示,我们有一个临界区的代码,当Thread2执行到synchornized(obj),访问共享资源的时候:

  1. 首先会将synchornized中的锁对象的对象头中MarkWord去尝试指向操作系统提供的Monitor对象,让锁对象中的MarkWord和Monitor对象相关联。如果关联成功,将obj对象头中的markword的对象锁标识从01改为10.
  2. 因为该Monitor没有和其他的obj的MarkWord相关联,所以Thread2就成为了该Monitor的Owner(所有者)。
  3. 又来了一个Thread1执行synchornized(obj)代码,它首先会检查是否能执行临界区代码,即检查obj是否关联了monitor,此时已经有了关联了,它就会去看该monitor有没有所有者,发现已经存在所有者,Thread1就会进入该monitor的EntryList(阻塞队列),EntryList是一个列表,若此时Thread3也执行到synchornized(obj),也会进入该阻塞队列。
  4. 当Thread2执行完临界区代码后,通过执行monitorexit指令,最终把count进入数退出为0后,释放owner位置为null,此时就会通知monitor中阻塞队列里面的线程,这些线程通过竞争,出现新的所有者。

    在这里插入图片描述

synchronized同步方法原理

方法的同步并没有通过指令monitorenter和monitorexit来完成不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成

Synchronized的存储位置

了解Synchronized具体如何体现当前已被某一线程上锁成功,需要对JAVA对象内存布局进行了解,简单来说Synchronized的加锁体现是在对象的对象头中MarkWord锁标识位上。

java对象内存布局

在HotSpot虚拟机里,对象在堆内存中的存储布局可以分为三个部分:对象头(Header),实例数据(Instance Data)和对齐填充(Padding)。

HotSpot虚拟机对象的对象头包括两类信息。第一类是用于存储对象自身的运行时数据,如,哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个bit和bit,官方称他为"Mark Word"。对象需要存储的运行时数据很多,其实已经超出了32,64位的Bitmap结构所能记录的最大限度,但对象头里的信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个有着动态定义的数据结构,以便在绩效的空间内存存储尽量多的数据,根据对象的状态复用自己的存储空间。

也就是说,每个对象都会有一个对象头,由于对象头是与对象自身数据无关的存储数据,因此将对象头设计成可动态变化存储更多数据的结构。

例如在32的HotSpot虚拟机中,如对象未被同步锁锁定的状态下,Mark Word的32个bit存储空间中25个bit用于存储对象的哈希码,4个bit用于存储对象的分代年龄,2个bit用于存储锁标志位,1个bit固定为0。

  • 对象头

    对象头主要由三个部分组成:MarkWord,ClassPoint,length

     MarkWord:
     	包含一系列的标识,例如锁的标记,对象年龄等。在32位系统占4字节,64位中占8字节。
     Class Point:
     	指向对象所属的Class在的方法区的内存执行,通常在32位系统占4字节,在64位系统中8字节,64位JVM在1.6版本以后默认开启了压缩指针,那就占用4个字节
     Length:
     	如果对象是数据,还需要一个保存数组长度的空间,占4个字节。
    

MarkWord存储结构:

简单来说,对象头的存储数据由对象状态变化而变化。基本存储为偏向线程ID,偏向时间戳,指向栈帧中锁记录指针,HashCode,对象年龄,锁标识。在轻量级锁以及重量级锁状态时,64位虚拟机对象头62个bit存储栈帧中指向,另外2个bit存储当前锁标识。

偏向锁

当一个锁对象在被刚new出来且JVM开启了偏向锁时,线程尝试获取当前锁对象且查看markword中锁标志位为无锁时,会将第一次加锁成功的线程指针记录下来,因为在具体场景中,绝大多数获取到锁的线程都是同一个,那么在线程下一次获取锁的过程中只需要比对一下偏向锁中线程地址就可以确认是否能够加锁成功。并且可以减少同一个线程对于上锁和释放的操作。

也就是说偏向锁实际上是一种无锁的概念的,只是通过锁对象头的markword中线程指针进行判断,通过CAS比较并交换markword中偏向线程的地址。当发生锁竞争时会将偏向锁升级成轻量级锁。偏向锁是一种用户态的锁,不涉及线程上下文的切换,因此效率较高。

偏向锁的获取过程

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

偏向锁的释放

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁

偏向锁的撤销需要等到全局安全点,它会首先暂停拥有偏向锁的线程A,然后判断这个线程A,此时有两种情况:

1.A线程已经退出了同步代码块,或者是已经不在存货了,如果是上面两种情况之一的,此时就会撤销偏向锁,变成无锁状态。
2.A线程还在同步代码块中,此时A线程的偏向锁升级为轻量级锁。

轻量级锁

首先说明轻量级锁的底层实现,轻量级锁通过创建锁记录(Lock Record)对象,每个线程的栈帧中都会包含一个锁记录的结构(对象),内部可以存储锁定对象的Mark Word。

在进行锁定时,通过CAS和该对象头中的Mark word进行比较交换,如果成功了,那么此时Lock Record对象中就会存放锁对象的mark word,锁对象的mark word就会存放当前线程栈帧中的Lock Record对象指针。
在这里插入图片描述

重量级锁

重量级锁的实现是通过Monitor对象来实现的,是一种内核态的锁,会发生线程上线文的切换,属于将加锁的过程交给了系统进行管理。具体实现过程在上文中有讲到。

Synchronized锁升级过程

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值