0

0

Java多线程中对象与引用的深度解析

霞舞

霞舞

发布时间:2025-11-28 14:13:02

|

601人浏览过

|

来源于php中文网

原创

java多线程中对象与引用的深度解析

本文深入探讨了Java多线程环境中对象与引用、堆与内存的关系,以及线程如何安全地共享和访问对象。通过阐明引用变量与实际对象实例的区别,并结合Java内存模型(JMM)的“Happens-Before”原则,解释了并发编程中可见性和有序性的挑战。文章还通过具体代码示例分析了安全与不安全的并发场景,并提供了避免常见陷阱的专业指导。

Java多线程中对象与引用的核心概念

在Java多线程编程中,理解对象、引用、堆内存与栈内存之间的关系是至关重要的。许多初学者常误认为一个对象“属于”创建它的线程,或者当线程进入循环时就无法再与其中声明的对象交互。然而,这种理解并不完全准确。

1. 对象在堆上的生命周期

在Java中,所有对象实例(如通过 new 关键字创建的实例)都存储在堆内存(Heap)中。堆内存是所有线程共享的区域。这意味着,一个对象一旦被创建,它就存在于堆上,任何持有该对象引用的线程都可以访问它,而不管这个引用最初是在哪个线程中获得的。

2. 引用变量与对象实例的区别

理解引用变量和对象实例是解决混淆的关键。

立即学习Java免费学习笔记(深入)”;

  • 对象实例(Object Instance):这是通过 new 操作符在堆上实际分配的内存区域,包含了对象的数据和方法。可以将其想象成一个“房子”。
  • 引用变量(Reference Variable):这是一个存储在栈内存(对于局部变量)或堆内存(对于实例变量)中的变量,它不包含对象本身,而是包含一个指向堆上对象实例的内存地址。可以将其想象成一个“地址簿页面”,上面写着房子的地址。

考虑以下代码片段:

whatTime wt = new whatTime();

这行代码执行了两个截然不同的操作:

  1. new whatTime():在堆内存中创建了一个 whatTime 类的实例(“房子”)。
  2. whatTime wt = ...:在当前线程的栈帧中声明了一个名为 wt 的局部引用变量(“地址簿页面”),并将刚刚创建的 whatTime 实例的内存地址赋值给它。

其他线程无法直接访问当前线程的栈内存,因此它们无法直接访问 wt 这个引用变量本身。但是,它们可以获得 wt 所指向的那个堆上的 whatTime 实例的地址副本。

3. 线程间的引用传递

当我们将一个引用变量传递给另一个线程时,Java采用的是值传递(pass-by-value)机制。这意味着传递的是引用变量的副本,而不是引用变量本身。

threadA ta = new threadA(wt);
ta.start();

在这段代码中:

  1. new threadA(wt):创建了一个 threadA 类的实例,并将其地址赋给了局部引用变量 ta。
  2. 在 threadA 的构造函数 threadA(whatTime wt) 中,mainClass 线程将它持有的 whatTime 对象的引用 wt 的一个副本传递给了 threadA 实例。
  3. threadA 实例内部的 this.wt = wt; 将这个副本存储为 threadA 实例的一个成员变量。

现在,mainClass 线程和 threadA 线程都各自持有一个指向同一个 whatTime 对象的引用。它们都拥有“地址簿页面”的副本,这些副本都指向堆上的同一个“房子”。因此,threadA 线程可以随时通过它自己的 wt 引用来调用 whatTime 对象的方法,就像 mainClass 线程也可以一样。

百宝箱
百宝箱

百宝箱是支付宝推出的一站式AI原生应用开发平台,无需任何代码基础,只需三步即可完成AI应用的创建与发布。

下载

Java内存模型(JMM)与并发挑战

虽然多个线程可以共享和访问同一个对象,但这并不意味着并发访问总是安全的。Java为了提高性能,允许编译器和处理器进行指令重排,并使用CPU缓存。这可能导致在多线程环境下出现可见性(Visibility)有序性(Ordering)问题。

1. CPU缓存与可见性问题

现代CPU为了提高数据访问速度,会在每个核心内部设置高速缓存(Cache)。当一个线程修改了共享变量的值时,这个修改可能首先写入CPU缓存,而不是立即写入主内存。如果另一个线程从主内存读取这个变量,它可能读取到的是旧值,因为缓存中的新值尚未刷新到主内存。这就是可见性问题

2. 指令重排与有序性问题

编译器和处理器为了优化执行效率,可能会对指令进行重排序,只要不改变单线程程序的执行结果。但在多线程环境下,这种重排可能导致一个线程观察到另一个线程的操作顺序与预期不符,从而引发有序性问题

3. “Happens-Before”原则

为了解决这些并发问题,Java内存模型(JMM)引入了“Happens-Before”原则。如果操作A“Happens-Before”操作B,那么操作A的结果对操作B是可见的,并且操作A在操作B之前执行。JMM定义了一系列规则来建立Happens-Before关系,包括:

  • 程序次序规则(Program Order Rule):在一个线程内,前面的操作Happens-Before后面的操作。
  • 管程锁定规则(Monitor Lock Rule):一个对锁的解锁操作Happens-Before后续对同一个锁的加锁操作。
  • volatile变量规则(Volatile Variable Rule):一个对 volatile 变量的写操作Happens-Before后续对同一个 volatile 变量的读操作。
  • 线程启动规则(Thread Start Rule):Thread.start() 方法的调用Happens-Before新线程中的任何操作。
  • 线程终止规则(Thread Termination Rule):线程中所有操作Happens-Before Thread.join() 的成功返回。
  • 线程中断规则(Thread Interruption Rule):对线程 interrupt() 方法的调用Happens-Before被中断线程检测到中断事件。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成Happens-Before它的 finalize() 方法的开始。
  • 传递性(Transitivity):如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

4. 不安全的并发访问示例

如果多个线程同时访问并修改同一个共享变量而没有建立适当的Happens-Before关系,程序行为将是不可预测的。

class Example {
  int x;

  void crazy() {
    x = 1;
    new Thread(() -> x = 5).start(); // 线程1修改x
    new Thread(() -> x = 10).start(); // 线程2修改x
    System.out.println(x); // main线程读取x
  }
}

在上述 crazy() 方法中,main 线程启动了两个新线程,它们都试图修改共享变量 x。由于没有同步机制(如 synchronized 或 volatile),main 线程在打印 x 的值时,可能会打印出 1、5 或 10。甚至,在某些极端的CPU架构和JVM实现下,也可能打印出其他意想不到的值。这种行为是不可预测的,且难以测试和调试。

原示例代码的安全性分析

回顾最初的问题代码:

public class mainClass {
   public static void main(String[] args) {
      whatTime wt = new whatTime(); // (1)
      threadA ta = new threadA(wt); // (2)
      ta.start(); // (3)

      while (true) {
         // main线程进入无限循环
      }
   }
}

public class threadA extends Thread {
   private whatTime wt; // (4)

   public threadA(whatTime wt) {
      this.wt = wt; // (5)
   }

   public void run() {
      while (true) {
         try {
            Thread.sleep(10000);
            System.out.println("threadA: " + wt.getTime()); // (6)
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
}

public class whatTime {
   public long getTime() {
      return System.currentTimeMillis(); // (7)
   }
}

这段代码是安全的,原因如下:

  1. 引用 wt 的安全发布:在 mainClass 的 main 方法中,whatTime wt = new whatTime(); (1) 创建了 whatTime 对象。然后,这个引用 wt 被传递给 threadA 的构造函数 (2),最终存储在 threadA 实例的 wt 字段中 (4, 5)。最重要的是,threadA 实例在 ta.start() (3) 之前就已经完全构造并持有了 whatTime 对象的引用。根据JMM的线程启动规则,ta.start() 操作Happens-Before threadA 线程中的任何操作。这意味着当 threadA 线程开始执行其 run() 方法时,它能够可靠地看到 wt 引用所指向的 whatTime 对象。
  2. whatTime 对象的特性:whatTime 类只有一个 getTime() 方法,该方法调用 System.currentTimeMillis()。System.currentTimeMillis() 是一个静态方法,它不依赖于 whatTime 对象的任何内部状态,也不会修改 whatTime 对象的任何字段。换句话说,whatTime 对象本身并没有任何可变的共享状态。即使有多个线程同时调用 getTime() 方法,它们也不会相互干扰,因为它们都在读取系统时间,而不是修改 whatTime 对象的共享数据。
  3. 无共享可变状态:在整个示例中,whatTime 对象的内部状态(如果有的话)并未被任何线程修改。wt 引用本身在 mainClass 和 threadA 中都是只读的(一旦初始化就不会再改变)。因此,不存在多个线程同时修改共享可变状态而导致的数据不一致问题。

所以,尽管 main 线程进入了一个 while(true) 循环,这仅仅意味着 main 线程本身在忙碌地执行一个空循环,但它并不妨碍 threadA 线程通过其持有的 wt 引用访问 whatTime 对象。

并发编程的最佳实践

  1. 谨慎处理共享可变状态:这是并发编程的核心挑战。如果多个线程需要访问同一个对象,并且其中至少一个线程会修改该对象的状态,那么必须采取适当的同步措施来保证数据的一致性和可见性。
  2. 优先使用不可变对象:如果一个对象在创建后其状态就不能再改变,那么它是线程安全的。例如,String 类就是不可变的。在多线程环境中尽可能使用不可变对象可以大大简化并发编程。
  3. 利用Java并发工具包(java.util.concurrent):Java提供了功能强大的并发工具包,其中包含了许多已经实现好同步机制的类,如 ConcurrentHashMap、AtomicInteger、CountDownLatch、ExecutorService 等。这些工具通常比手动使用 synchronized 或 volatile 更高效、更安全。
  4. 理解同步机制:当需要手动同步时,深入理解 synchronized 关键字、volatile 关键字以及 java.util.concurrent.locks 包下的锁机制是必不可少的。选择正确的同步机制取决于具体的场景和性能需求。
  5. 避免死锁和活锁:不当的锁使用可能导致死锁(多个线程互相等待对方释放资源)或活锁(线程不断重试但始终无法成功)。设计并发代码时应考虑如何避免这些问题。
  6. 区分引用和对象:始终牢记引用变量只是指向堆上对象的地址。一个引用变量的生命周期(栈上)与它所指向的对象(堆上)的生命周期是不同的。
  7. 线程安全地发布对象:确保当一个对象被多个线程共享时,它的所有字段都已经被正确初始化,并且该对象是线程安全地“发布”给其他线程的。例如,在构造函数中完成所有初始化,并使用 final 字段,或者通过 synchronized 块或 volatile 字段来发布。

通过遵循这些原则并深入理解Java内存模型,开发者可以编写出健壮、高效且线程安全的并发应用程序。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

1010

2023.08.02

while的用法
while的用法

while的用法是“while 条件: 代码块”,条件是一个表达式,当条件为真时,执行代码块,然后再次判断条件是否为真,如果为真则继续执行代码块,直到条件为假为止。本专题为大家提供while相关的文章、下载、课程内容,供大家免费下载体验。

106

2023.09.25

c++中volatile关键字的作用
c++中volatile关键字的作用

本专题整合了c++中volatile关键字的相关内容,阅读专题下面的文章了解更多详细内容。

75

2025.10.23

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

443

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

605

2023.08.10

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

443

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

605

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

765

2023.08.10

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 4.3万人学习

C# 教程
C# 教程

共94课时 | 11.2万人学习

Java 教程
Java 教程

共578课时 | 81万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号