前言:大家好,我是小威,24届毕业生,在一家满意的公司实习。本篇文章是关于CAS的介绍以及在我们new对象时,Java虚拟机会为我们做些什么贡献。
本篇文章记录的基础知识,适合在学Java的小白,也适合复习中,面试中的大佬🤩🤩。
如果文章有什么需要改进的地方还请大佬不吝赐教👏👏。
小威在此先感谢各位大佬啦~~🤞🤞
🏠个人主页:小威要向诸佬学习呀
🧑个人简介:大家好,我是小威,一个想要与大家共同进步的男人😉😉
目前状况🎉:24届毕业生,在一家满意的公司实习👏👏🎁如果大佬在准备面试,可以使用我找实习前用的刷题神器哦刷题神器点这里哟
💕欢迎大家:这里是CSDN,我总结知识的地方,欢迎来到我的博客,我亲爱的大佬😘
以下正文开始
当我们使用new关键字创建对象时,Java虚拟机遇到字节码new指令时,会做些什么事情呢,下面慢慢分析。
当Java虚拟机遇到new指令时,首先会检查执行这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载过,解析过和初始化过。没有的话会先执行类加载的过程。
类加载检查通过之后,虚拟机会为新建的对象分配内存空间,注意,此内存空间大小在类加载完成后就已经确定好了。因此为对象分配内存空间就相当于把一块已经确定好大小的内存从Java堆中划分出来为对象分配。但是此时对于内存,还会有两种情况,因此对应这两种不同的分配方式,即“指针碰撞”和“空闲列表”。
指针碰撞,顾名思义,就是指针的移动。
指针碰撞的概念为:假设Java堆中的内存是完整的,所有被使用过的内存被放到一边,没有使用过(空闲的)的内存被放到另一边,中间有一个指针,这个指针作为分界点的指示器,在为对象分配内存空间时,指针指示器会向空闲内存的方向移动一段与对象相同大小内存的距离。这种分配方式就是“指针碰撞”。
空闲列表,顾名思义,就是空闲的内存在表上名列出来。
空闲列表的概念为:如果Java堆中的内存空间不是完整的,被使用过的内存和空闲的内存交织在一起,就无法使用指针碰撞的方式分配内存了,这时候,虚拟机会维护一张表,表上会记录哪些内存是空闲可用的,在为新建对象分配空间时,就会找到一个相对与新建对象足够大的空间划分给新建对象,同时更新表上的记录,这种方式被称为“空闲列表”。
上面我们就空闲(可用)空间的划分,讨论了两种为新建对象分配空间的情况。除此之外,我们还要考虑线程安全的问题,在并发的情况下,如果正在给新建对象A分配内存,另一个新建对象B也需要分配内存,此时指针还未修改,新建对象B同时使用了原来的指针分配内存,就可能出现差错。
对于这种情况,解决的方案也有两种解决方式,即CAS失败重试和本地线程分配缓存的方式。
首先简单介绍一下什么是CAS,CAS(Compare And Swap),从英文名来看是比较并交换的意思,顾名思义,CAS操作的确有这么两个操作。CAS是乐观锁,即在线程执行的时候不会加锁,位于UnSafe类中。
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
以CompareAndSwapObject方法为例(如下代码),CompareAndSwapObject方法一共有四个变量,第一个是要修改的字段对象,第二个是字段在对象内的偏移量,第三个是字段的期望值,第四个是当该字段的值和期望值相等的话,更新字段的新值。
public native boolean compareAndSwapObject(Object obj, long offset, Object expect, Object update);
那么CAS原理是什么呢,通过一个例子来解释一下。假设内存地址中有一变量S,S的值为1,此时线程1的期望值为1,线程1想要修改变量S的值为2,在线程1修改成功之前,此时线程2将S的值修改2,并且修改成功了,因此内存地址中S的值为2,线程1发现期望值和内存地址中S的值不一致,就会发生自旋现象(也就是失败重试,重新获取内存地址中的值),直到自旋成功。先会进行Compare,如果期望值和内存地址中S的值相等,再会进行Swap操作,更新内存地址中的值。
再多说一些,CAS不需要加锁,难道就是完美的吗?
其实不然,CAS操作可能会发生典型的ABA问题,就上一个例子来说,线程2将变量S的值修改为2,然后又把S的值修改为1,这相对与线程1来说,变量S的值没有发生变化,但是CAS操作也没有检测到这种情况的发生。
出现此种情况,最简单的解决方法就是在变量上加上版本号,每当变量改变一次,变量的版本号就进行更新+1,因此虽然变量的值会改变,但是通过版本号可以得知变量值是否改变过。
回归到刚才的话题,可以使用CAS加上失败重试的方式保证更新操作的原子性,对分配内存空间的动作进行同步处理。
顾名思义,基于这种方法,会用到缓冲区,即把内存分配的动作按照线程划分在不同的内存空间中进行,每个线程会在Java堆中预先分配一块内存空间,哪个线程需要分配内存,就在哪个线程的本地缓冲区里面分配,只有本地缓冲区使用完了,分配新的缓存区才需要同步锁定。这种方法叫做本地线程分配缓冲(Thread Local Allocation Buffer,即TLAB)。
等待内存分配步骤完成后,虚拟机需要将分配到的内存空间初始化为0(对象头除外)。这样才可以保证对象的实例字段在Java代码中可以不赋初值就可以直接使用,并且此时程序访问这些字段时,这些字段都是0值。
随后,Java虚拟机对对象进行一些其他的设计,才会完整地产生此对象。但是实际上,这才是构造函数阶段,并没有完成对象的创建,还需要执行Class文件中的方法,并且对象需要的其他资源和状态信息也没有构造好,此时的字段值均为0。
直到对对象初始化完成后,一个真正可用的对象才算是完全的被构造出来了。
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起讨论🍻
希望能和诸佬们一起努力,今后进入到心仪的公司
再次感谢各位小伙伴儿们的支持🤞