深入理解Java的堆内存和线程内存
我们都知道Java对象都是在堆中创建的(开启逃逸分析的情况除外),比如一个线程中有一段这样的代码:
public class A{
public int xxx;
}
通过A a = new A();会在堆中创建一个对象,并引用a 指向了堆中对象的内存地址,也就是主内存中。
也就是说线程中的引用指向了主内存中的对象地址,很多Java程序员甚至以为因为持有引用,所以对这个引用的赋值或者读取都是直接根据地址操作主内存的对象,其实并不是这样的。
如果按照这个逻辑,线程中操作的对象就是主内存中对象(为了好理解,我直接认为主内存就是堆了);直接操作堆中对象, 那就是只有一个堆中对象,也不会存在多线程下不一致问题了,因为大家都是通过地址操作同一个对象,只有一个版本,就不会有不一致问题。
可事实并不是如此。线程内存和主内存是不一样的。当线程要读取a.xxx的时候,其实是通过该引用持有的内存地址去堆中读取这个对象的属性值,赋值给线程中的变量a.xxx;修改也一样,修改完了后将这个值覆盖堆中a的xxx属性的值(怎么实现稍后讲);
Java操作内存相关的指令有8个,lock(锁定),unlock(解锁),read(读取),load(载入),use(使用),assign(赋值),store(存储)
线程对象的操作主要是通过这个几个指令实现的,而不是我们想象的直接操作。
所以多线程下的时候,每个线程都去堆中读取对象的值,拷贝到自己线程变量中使用,修改完了再覆盖回去。这才会出现不一致和读到被别人修改的数据。
volatile关键字的作用之一就是每次使用前都和主内存(堆)进行上面的读取或者写回操作,保证了线程的可见性。如果按照线程对象就是直接操作堆中对象,那就根本就不需要这个关键字了,简单想象就知道线程中的对象也不是堆中的对象这个事实了,使用这个关键字就是希望线程中的对象和堆中的对象是一致的。
回答刚才留下的坑,那我们到底是怎么去堆中读取对象的内容的呢,比如上面的a.xxx, 对象的属性的开始地址相对对象开始地址是有一个偏移量的,
每个类型都有其规定的长度,只要从开始地址读取这个类型长度的地址就可以了。下面演示一种牛B的修改内存地址处值的方法。
这个偏移量的获取方法如下:
Field field = a.getClass().getDeclaredField("xxx");
long sexOffset = unsafe.objectFieldOffset(field); //sexOffset 就是这个偏移量;
unsafe 是sun.misc.Unsafe类,是不能通过new出来的。可以通过反射获取,Unsafe类是无锁机制。
public native Object getObjectVolatile(Object arg0, long arg1);
这个方法的实现类中就可以通过对象引用和偏移地址来获取其属性值。
这个类中还有很多牛B方法,很多都是通过传说的CAS算法实现的的,其实这种算法没那么复杂就是比较和替换,比较堆中的该地址处的值是否和期望值一样,一样就执 行相应的修改操作,并返回真,不一样就放弃操作,返回假。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现