在初识“锁”时,我第一次上锁,锁的是基本数据类型,代码频频报错,所以基本数据类型为什么不能上锁?对象在堆中的形态是怎么样的?Java 上锁究竟干了什么?
堆&栈的共有与私有
首先是堆和栈的区别,栈中的数据是私有的,对于其他线程而言是看不到当前线程的栈帧具体内容,而堆内的数据是公开的,一个进程下的所有线程都能够看到
对一个数据上锁,其根本原因是担心数据在多线程的情况下被不同线程同时进行访问/修改等操作,导致数据的不一致,而在栈中,只有单一线程能够进行操作,自然而然没有加锁的必要,而堆中,堆中的数据公开,自然需要上锁。
锁不住的基本数据类型?
基本数据类型无法上锁
在 JDK 1.8 之后,静态变量会被放置在堆内存之中,那么在堆内存中的数据是公开的 -> 静态基本数据类型变量可以被上锁?
答案是不行的,对于上锁的条件,必须得是【Object】,这与 Java 底层对于上锁这一事件做的操作有关
包装类陷阱!
有人可能会想:既然基本类型不能加锁,那改用 Integer 包装类总行吧?这其实是一个严重的误区。
- 首先,Integer 对象是不可变的,任何修改操作(如自增)都会让变量指向一个全新的对象,导致多线程锁定的不再是同一个实例;
- 其次,Integer 存在常量池缓存,锁定同一个数值可能导致不相关的代码块共用一把锁,进而引发非预期的阻塞甚至死锁。
如下图,明明是两个对象,却对应着同一个地址,这就是因为包装类在底层使用了缓存
公开非静态变量中的基本数据类型
对于只设置了 public 但不设置 static 的非静态公开局部变量,它会被锁住吗?答案是 会
非静态公开的局部变量想要使用则需要创建当前的类对象,使用类对象来调用,但是类对象会被放置在堆中,如果锁会锁住整个类对象,所以如果使用的是非静态公开的局部变量,会被间接锁住,是因为变量的对象被锁导致的访问该变量的线程只能有一个。
Java 上锁的底层操作
当使用【synchronized】对对象进行上锁时,会在其对象头的 Mark Word 部分中的【锁信息】中标注已上锁,
但是基本数据类型没有对象头,基本数据类型有其固定的范围,通过固定的字节就能够访问,修改,实在是没有多出来地方放“是否上锁”之类的信息了
Monitor
Monitor(监视器)是 Java 中实现线程同步的核心机制,可以看作一个“独占门锁”。想象一个只有一张票的电影院(共享资源),Monitor 就像检票员,确保同一时间只有一个观众(线程)能进去看电影,其他人得在门口排队等着。
在 Java 中,Monitor 是 synchronized 关键字的底层实现,用于保证多线程访问共享资源时的线程安全。每个 Java 对象都可以关联一个 Monitor,充当锁的角色。
对象的内存模型
在 Java 进程启动后的内存分配为(下图来自 guide):
其中每一个对象: 在内存中包含三部分:对象头、实例数据、对齐填充。
对象头文件
对象头文件,会存储该对象的一些信息,Mark Word 存储了对象目前的状态:是否被上锁,是否应该被 GC;对象元数据指针则指向方法区中的对应的类元数据
注意:如果当前对象是数组,对象头中还包含一块专门记录数组长度的区域
实例数据
存放当前被创建对象的实例信息,主要是存放类的数据信息,父类的信息,对象字段属性信息。
对齐填充
为了字节对齐,填充的数据,不是必须的。因为 CPU 读取数据并不是一字节一字节地读,以 64 位系统为例,CPU 会以 8 字节为基本单位读取信息;当然更不必说存储,所以将对象调整为 CPU 一口能吃得下的水准,能提高性能