讲师中心 微信公众号
AI工具推荐 视频效率加速

Java多线程安全共享变量与周期性输出实践指南

冬墨同学_1404

冬墨同学_1404

发布时间:2025-11-23 14:24:17

|

962人浏览过

|

来源于php中文网

原创

Java多线程安全共享变量与周期性输出实践指南

本教程深入探讨java多线程环境下,一个线程递增变量,另一个线程周期性打印的实现方法。文章阐述了共享变量的挑战及java内存模型,并提供了两种线程安全方案:利用atomicinteger进行原子操作以确保数据一致性,以及通过linkedblockingqueue实现生产者-消费者模式进行线程间通信,从而有效解决并发问题。

在多线程编程中,实现一个线程递增共享变量,而另一个线程周期性地读取并打印该变量的值,是一个常见的需求。然而,直接在多个线程间共享一个基本类型变量(如int)并进行读写操作,往往会遇到线程安全问题,导致数据不一致或不可预测的结果。本教程将深入探讨这一挑战,并提供两种健壮且常用的解决方案。

理解并发共享变量的挑战

当多个线程同时访问和修改同一个变量时,如果不采取适当的同步机制,就会出现问题。这主要是因为Java虚拟机(JVM)为了优化性能,可能会对指令进行重排序,或者允许每个线程拥有变量的本地缓存副本。这意味着一个线程对变量的修改,可能不会立即对其他线程可见。

考虑以下一个简单的、不安全的共享变量示例:

class UnsafeCounter {
    int counter = 0;

    public void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }
}

public class UnsafeSharedVariableDemo {
    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter unsafeCounter = new UnsafeCounter();

        // 线程1:递增计数器
        Thread incrementer = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                unsafeCounter.increment();
            }
            System.out.println("Incrementer finished. Final count (from incrementer's perspective): " + unsafeCounter.getCounter());
        });

        // 线程2:周期性打印计数器
        Thread printer = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.println("Printer read counter: " + unsafeCounter.getCounter());
                    Thread.sleep(100); // 每100毫秒打印一次
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        incrementer.start();
        printer.start();

        incrementer.join(); // 等待递增线程结束
        printer.join();     // 等待打印线程结束
        System.out.println("Main thread: Final counter value: " + unsafeCounter.getCounter());
    }
}

在上述代码中,printer线程可能会多次打印出相同的旧值,甚至在incrementer线程已经修改了counter之后。这是因为Java内存模型(JMM)规定,没有同步机制时,一个线程对共享变量的修改不保证对其他线程立即可见。为了解决这个问题,我们需要建立“Happens-Before”关系,确保操作的可见性和有序性。

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

方案一:使用原子类进行线程安全操作

java.util.concurrent.atomic包中的原子类提供了无锁的线程安全操作。它们通过使用底层的硬件指令(如CAS,Compare-And-Swap)来保证操作的原子性,并确保变量的修改对所有线程立即可见,从而建立Happens-Before关系。AtomicInteger是处理整数计数的理想选择。

以下是使用AtomicInteger实现线程安全计数器和周期性打印的示例:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounterDemo {
    private final AtomicInteger counter = new AtomicInteger(0); // 使用AtomicInteger作为共享计数器

    public void startThreads() {
        // 线程1:递增计数器
        Thread incrementerThread = new Thread(() -> {
            try {
                System.out.println("Incrementer thread started.");
                for (int i = 0; i < 50; i++) {
                    counter.incrementAndGet(); // 原子性递增
                    Thread.sleep(50); // 模拟工作负载
                }
                System.out.println("Incrementer thread finished. Final count: " + counter.get());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Incrementer thread interrupted.");
            }
        }, "Incrementer");

        // 线程2:周期性打印计数器
        Thread printerThread = new Thread(() -> {
            try {
                System.out.println("Printer thread started.");
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("Current counter value: " + counter.get()); // 原子性获取
                    Thread.sleep(200); // 每200毫秒打印一次
                }
            } catch (InterruptedException e) {
                System.out.println("Printer thread interrupted. Exiting loop.");
            } finally {
                System.out.println("Printer thread finished.");
            }
        }, "Printer");

        incrementerThread.start();
        printerThread.start();

        // 等待递增线程完成
        try {
            incrementerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 递增线程完成后,中断打印线程
        printerThread.interrupt();
        try {
            printerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("All threads finished. Final counter value from main: " + counter.get());
    }

    public static void main(String[] args) {
        new AtomicCounterDemo().startThreads();
    }
}

工作原理与优点:

Java调试
Java调试

通过 JDWP 实现 Java 远程调试:连接远程 JVM、设置断点、单步执行代码、查看变量值、获取调用栈。适用于需要调试远程 Java 应用、排查运行时状态、追踪代码执行流程的场景。 术语注解 JDWP:Java Debug Wire Protocol,Java 调试有线协议(Java 远程调试底层通信协议) remote JVM:远程 Java 虚拟机 step through code:代码单步调试(步入 / 步过 / 步出调试) inspect variables:变量查看 / 变量检视 stac

下载
  • AtomicInteger内部通过CAS操作确保了incrementAndGet()和get()等方法的原子性。
  • 每次对AtomicInteger的修改都会刷新到主内存,并且读取操作会从主内存获取最新值,从而保证了线程间的可见性。
  • 相较于使用synchronized关键字进行方法或代码块锁定,原子类通常在竞争不激烈时性能更优,因为它避免了操作系统级别的线程上下文切换开销。

方案二:通过消息队列实现线程间通信

除了直接共享变量,另一种更解耦、更健壮的线程间通信方式是使用消息队列。一个线程作为“生产者”将数据放入队列,另一个线程作为“消费者”从队列中取出数据。java.util.concurrent包提供了多种并发集合,其中LinkedBlockingQueue是一个常用的选择,它支持阻塞式的存取操作,非常适合实现生产者-消费者模式。

以下是使用LinkedBlockingQueue实现计数器和周期性打印的示例:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class MessageQueueCounterDemo {
    private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10); // 容量为10的阻塞队列

    public void startThreads() {
        // 线程1:生产者 - 递增并放入队列
        Thread producerThread = new Thread(() -> {
            try {
                System.out.println("Producer thread started.");
                for (int i = 1; i <= 50; i++) {
                    queue.put(i); // 将当前计数值放入队列,如果队列满则阻塞
                    System.out.println("Producer added: " + i + ", Queue size: " + queue.size());
                    Thread.sleep(50); // 模拟生产数据的耗时
                }
                System.out.println("Producer finished adding 50 items. Signaling consumer to stop.");
                queue.put(-1); // 发送一个特殊值作为结束信号
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Producer thread interrupted.");
            } finally {
                System.out.println("Producer thread finished.");
            }
        }, "Producer");

        // 线程2:消费者 - 周期性从队列取出并打印
        Thread consumerThread = new Thread(() -> {
            try {
                System.out.println("Consumer thread started.");
                while (true) {
                    Integer value = queue.take(); // 从队列取出数据,如果队列空则阻塞
                    if (value == -1) { // 收到结束信号
                        System.out.println("Consumer received termination signal. Exiting.");
                        break;
                    }
                    System.out.println("Consumer retrieved: " + value + ", Queue size: " + queue.size());
                    Thread.sleep(200); // 模拟消费数据的耗时
                }
            } catch (InterruptedException e) {
                System.out.println("Consumer thread interrupted. Exiting loop.");
            } finally {
                System.out.println("Consumer thread finished.");
            }
        }, "Consumer");

        producerThread.start();
        consumerThread.start();

        // 等待生产者和消费者线程完成
        try {
            producerThread.join();
            consumerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("All threads finished. Final queue size: " + queue.size());
    }

    public static void main(String[] args) {
        new MessageQueueCounterDemo().startThreads();
    }
}

工作原理与优点:

  • LinkedBlockingQueue是线程安全的,其put()和take()方法内部处理了所有的同步细节,确保了数据的一致性。
  • 当队列为空时,take()方法会阻塞消费者线程,直到有数据可用;当队列已满时,put()方法会阻塞生产者线程,直到有空间可用。这天然地实现了线程间的协调。
  • 这种模式解耦了生产者和消费者,它们只需要知道队列的存在,而无需直接访问对方的状态。这使得系统更具弹性、可扩展性更强。
  • 适用于复杂的生产者-消费者场景,例如数据处理管道、任务调度等。

总结与注意事项

在Java中实现多线程共享变量并周期性输出,关键在于正确处理线程安全和数据可见性。

  1. 原子类(AtomicInteger等):

    • 优点:简单直接,适用于单一变量的原子操作,通常比synchronized有更好的性能(在低竞争环境下)。
    • 适用场景:计数器、标志位、简单状态的原子更新。
    • 注意事项:原子类只能保证单个操作的原子性。如果需要对多个变量进行复合操作,仍需其他同步机制(如synchronized或Lock)。
  2. 消息队列(LinkedBlockingQueue等):

    • 优点:高度解耦,天然支持生产者-消费者模式,易于扩展和维护,处理复杂数据流和任务调度非常有效。
    • 适用场景:任务队列、事件驱动系统、需要缓冲和流量控制的场景。
    • 注意事项:引入了额外的对象(队列元素),可能会有轻微的性能开销。需要设计好消息协议(例如本例中的-1作为结束信号)。

在实际开发中,应根据具体的业务需求、数据特性和性能要求来选择合适的并发工具。java.util.concurrent包是Java并发编程的核心,其中包含了大量强大的工具,如Semaphore、CountDownLatch、CyclicBarrier、ExecutorService等,深入学习这些工具将极大地提升多线程编程的能力。始终记住,并发编程是复杂的,彻底理解Java内存模型和Happens-Before关系是编写正确、高效并发代码的基础。

热门AI工具

更多
SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

花生AI
花生AI Hot

一款AI视频创作工具,主要用于B站推出的AI视频创作工具,适合需要提升相关任务效率的用户。

小橡皮AI

一款AI工具,主要用于小橡皮一键去AI味,将机器味重的内容改得更有活人感,防止因为 AI 内容被平台打上 AI 标签,以及限流,适合需要提升相关任务效率的用户。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

咔片AIPPT

一款在线AI演示文稿制作工具,可根据主题和内容需求辅助生成PPT结构与页面,提高演示材料制作效率。

PixPix
PixPix Hot

PixPix是一款面向电商视觉生产的AI商品图生成工具。

相关专题

更多
string转int
string转int

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

7079

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3385

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

4428

2025.08.29

C++中int的含义
C++中int的含义

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

3005

2025.08.29

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

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

4618

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

1289

2025.12.24

java多线程相关教程合集
java多线程相关教程合集

本专题整合了java多线程相关教程,阅读专题下面的文章了解更多详细内容。

443

2026.01.21

C++多线程相关合集
C++多线程相关合集

本专题整合了C++多线程相关教程,阅读专题下面的的文章了解更多详细内容。

579

2026.01.21

Docker Desktop怎么设置端口映射
Docker Desktop怎么设置端口映射

本专题聚焦Docker Desktop容器端口映射与访问配置,整理从创建容器、设置端口绑定到验证本地服务访问的操作流程。针对端口被占用、映射配置错误、容器服务无法访问等情况,也提供相应的检查方向,方便用户完成端口配置。

0

2026.11.03

热门下载

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

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
dev.java 官方:Learn Java
dev.java 官方:Learn Java

共0课时 | 0人学习

Java JDBC数据库连接官方教程
Java JDBC数据库连接官方教程

共0课时 | 0人学习

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn