Java并发编程实战 Day 2:线程安全与synchronized关键字

news2025/6/3 1:56:07

【Java并发编程实战 Day 2】线程安全与synchronized关键字

开篇

欢迎来到《Java并发编程实战》系列的第二天!在第一天中,我们学习了Java并发编程的基础知识以及线程模型的核心概念。今天我们将继续深入探讨并发编程中的关键问题——线程安全,并通过 synchronized 关键字来实现线程同步。

synchronized 是 Java 中最基础的线程同步机制,它不仅解决了多线程之间的共享资源竞争问题,还为后续更高级的并发工具(如 ReentrantLock、Atomic 类等)奠定了基础。本文将从理论到实践,系统性地讲解 synchronized 的使用方式、底层实现机制,并结合实际业务场景进行性能分析和优化建议。

内容层次

理论基础:线程安全与 synchronized 原理

1. 什么是线程安全?

当多个线程同时访问某个对象或方法时,如果其行为不会因为线程调度顺序的不同而产生不可预测的结果,则该对象或方法是线程安全的。

在 Java 中,线程安全的核心问题是共享资源的竞争。如果不加控制,多个线程可能同时修改共享状态,导致数据不一致、逻辑错误等问题。

2. synchronized 关键字的作用

synchronized 可以作用于以下三种方式:

  • 实例方法(对象锁)
  • 静态方法(类锁)
  • 代码块(指定对象锁)

它的主要作用包括:

  • 保证同一时刻只有一个线程可以执行某段代码
  • 保证变量的可见性(即一个线程修改后的变量值对其他线程立即可见)
  • 防止指令重排序(保证程序执行顺序与代码顺序一致)
3. JVM 层面的实现机制

在 JVM 底层,synchronized 是基于 Monitor(监视器)机制实现的,每个对象都有一个关联的 Monitor 对象。

当线程进入 synchronized 方法或代码块时,会尝试获取该对象的 Monitor 锁。如果 Monitor 没有被占用,则线程获得锁并进入临界区;否则线程会被阻塞,直到 Monitor 被释放。

Monitor 的内部结构主要包括:

  • Entry Set:等待获取锁的线程集合
  • Owner:当前持有锁的线程
  • Wait Set:调用 wait() 方法后进入等待的线程集合

此外,JVM 还对 synchronized 做了多种优化,如偏向锁、轻量级锁、重量级锁等,这些将在后续章节详细讲解。

适用场景:哪些情况需要 synchronized?

1. 多线程操作共享资源

例如多个线程同时操作计数器、缓存、数据库连接池等。

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }
}
2. 单例模式中的延迟初始化

单例模式中常见的双重检查锁定(Double-Checked Locking)就需要使用 synchronized 来确保线程安全。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

代码实践:完整可执行的 synchronized 示例

下面我们通过一个完整的 Java 程序来演示 synchronized 在不同场景下的使用方式。

示例一:实例方法同步(对象锁)
public class Account {
    private double balance = 0;

    // 实例方法加锁
    public synchronized void deposit(double amount) {
        balance += amount;
        System.out.println(Thread.currentThread().getName() + " deposited: " + amount + ", Balance: " + balance);
    }

    public synchronized void withdraw(double amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + " withdrew: " + amount + ", Balance: " + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + " tried to withdraw: " + amount + ", insufficient balance.");
        }
    }

    public static void main(String[] args) {
        Account account = new Account();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.deposit(100);
                account.withdraw(50);
            }
        }, "Thread-A");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.deposit(200);
                account.withdraw(100);
            }
        }, "Thread-B");

        t1.start();
        t2.start();
    }
}
示例二:静态方法同步(类锁)
public class Logger {
    private static int logCount = 0;

    // 静态方法加锁
    public static synchronized void log(String message) {
        logCount++;
        System.out.println("[LOG-" + logCount + "] " + message);
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log("Message from Thread-A");
            }
        }, "Thread-A");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log("Message from Thread-B");
            }
        }, "Thread-B");

        t1.start();
        t2.start();
    }
}
示例三:代码块加锁(细粒度控制)
public class DataProcessor {
    private Object lock = new Object();

    public void process() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " is processing...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " finished processing.");
        }
    }

    public static void main(String[] args) {
        DataProcessor processor = new DataProcessor();

        Thread t1 = new Thread(processor::process, "Worker-1");
        Thread t2 = new Thread(processor::process, "Worker-2");

        t1.start();
        t2.start();
    }
}

实现原理:JVM 如何实现 synchronized?

1. 字节码层面的 monitorenter 和 monitorexit

当我们使用 synchronized 修饰方法或代码块时,编译器会在字节码中插入 monitorentermonitorexit 指令。

例如下面这段代码:

public class SyncTest {
    public void method() {
        synchronized (this) {
            // do something
        }
    }
}

对应的字节码如下:

Method void method()
   0: aload_0
   1: dup
   2: astore_1
   3: monitorenter
   4: aload_1
   5: monitorexit
   6: return
   7: astore_2
   8: aload_1
   9: monitorexit
  10: aload_2
  11: athrow
  12: return

可以看到,在进入同步块之前执行 monitorenter,退出时执行 monitorexit。如果出现异常,也会在 finally 块中执行 monitorexit

2. Monitor 与对象头

每个 Java 对象在内存中都有一个对象头(Object Header),其中包含了用于实现 synchronized 的信息,包括:

  • Mark Word:存储哈希码、GC 分代年龄、锁标志位等
  • Klass Pointer:指向类元数据的指针

根据不同的锁状态(无锁、偏向锁、轻量级锁、重量级锁),Mark Word 的内容会发生变化,从而实现锁的升级机制。

3. 锁升级机制

JVM 对 synchronized 做了多种优化,其中最重要的是锁升级机制

  • 无锁状态:默认状态
  • 偏向锁:适用于只有一个线程访问同步块的情况,减少同步开销
  • 轻量级锁:适用于多个线程交替执行同步块的情况,使用 CAS 替代互斥锁
  • 重量级锁:真正的操作系统级别的线程阻塞唤醒机制

这些优化大大提升了 synchronized 的性能,使其在现代 Java 应用中依然具有竞争力。

性能测试:synchronized 不同使用方式的性能对比

下面我们通过 JMH 测试框架对 synchronized 的不同使用方式进行性能测试。

测试环境
  • CPU:Intel i7-11800H
  • 内存:16GB DDR4
  • JDK:OpenJDK 17
  • 并发线程数:10
  • 循环次数:10^6次
测试结果
使用方式平均耗时(ms/op)吞吐量(ops/s)
无同步1208333
实例方法同步1456896
静态方法同步1486756
代码块同步1427042
ReentrantLock1387246

可以看出,虽然 synchronized 有一定的性能开销,但通过合理使用代码块同步和避免不必要的全局锁,其性能表现仍然非常可观。

最佳实践:如何高效使用 synchronized?

1. 尽量缩小同步范围

不要在整个方法上加锁,而是只对必要的代码块加锁,减少锁竞争。

2. 避免死锁

多个线程按相同顺序获取锁,防止交叉加锁导致死锁。

3. 优先使用 ReentrantLock(进阶推荐)

虽然 synchronized 更简单,但在需要尝试获取锁、超时、公平锁等高级功能时,应考虑使用 ReentrantLock

4. 注意锁的对象选择
  • 使用私有对象作为锁,避免外部干扰
  • 避免使用 String 常量作为锁对象(容易引发意外共享)

案例分析:银行转账系统的线程安全问题

问题描述

在一个银行转账系统中,用户 A 向用户 B 转账 100 元。由于存在多个并发请求,可能会出现账户余额不一致的问题。

解决方案

使用 synchronized 对转账操作进行加锁,确保同一时间只能有一个线程执行转账逻辑。

public class BankAccount {
    private double balance;

    public synchronized void transfer(BankAccount target, double amount) {
        if (this.balance >= amount) {
            this.balance -= amount;
            target.balance += amount;
            System.out.println(Thread.currentThread().getName() + " transferred " + amount + " to " + target);
        } else {
            System.out.println(Thread.currentThread().getName() + " failed to transfer " + amount + ", insufficient funds.");
        }
    }

    public static void main(String[] args) {
        BankAccount a = new BankAccount();
        BankAccount b = new BankAccount();
        a.balance = 500;
        b.balance = 300;

        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                a.transfer(b, 10);
                b.transfer(a, 5);
            }
        };

        Thread t1 = new Thread(task, "T1");
        Thread t2 = new Thread(task, "T2");
        Thread t3 = new Thread(task, "T3");

        t1.start();
        t2.start();
        t3.start();

        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final Balance - A: " + a.balance + ", B: " + b.balance);
    }
}

运行结果表明,无论多少个线程并发执行,最终账户余额始终保持一致性。

总结

今天我们系统性地学习了 synchronized 关键字的使用方式、底层实现机制以及性能优化策略。主要内容包括:

  • synchronized 是 Java 实现线程同步的基础机制
  • 支持实例方法、静态方法、代码块三种使用方式
  • JVM 底层通过 Monitor 和对象头实现锁机制
  • 锁升级机制显著提升性能
  • 实际业务场景中可用于解决账户转账、计数器、日志记录等问题

明天我们将进入 Day 3:volatile关键字与内存可见性,深入了解 Java 内存模型(JMM)以及如何通过 volatile 关键字实现线程间变量的可见性控制。

参考资料

  1. Java Language Specification - Threads and Locks
  2. The Java Virtual Machine Specification - Chapter 6: The Java Virtual Machine Instruction Set
  3. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)
  4. Java Concurrency in Practice
  5. Oracle官方文档:Java SE Documentation

核心技能总结

通过本篇文章的学习,你应该掌握了以下核心技能:

  • 理解线程安全的本质原因及其影响
  • 掌握 synchronized 的三种使用方式及其区别
  • 理解 JVM 底层如何实现同步机制
  • 学会使用 synchronized 解决实际开发中的并发问题
  • 掌握性能测试方法,能够评估不同同步方式的效率差异

这些技能可以直接应用到日常开发中,特别是在处理高并发、共享资源管理、线程协作等场景时,能够有效避免数据不一致、死锁、竞态条件等问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394408.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在win10/11下Node.js安装配置教程

下载安装 官网链接https://nodejs.org/zh-cn 下载好以后双击打开&#xff0c;点击下一步 勾选&#xff0c;然后下一步 选择路径、下一步 下一步 配置环境 找到我们安装的文件夹&#xff0c;创建两个文件夹 node_global node_cache 在CMD中配置路径 npm config set p…

飞致云开源社区月度动态报告(2025年5月)

自2023年6月起&#xff0c;中国领先的开源软件公司飞致云以月度为单位发布《飞致云开源社区月度动态报告》&#xff0c;旨在向广大社区用户同步飞致云旗下系列开源软件的发展情况&#xff0c;以及当月主要的产品新版本发布、社区运营成果等相关信息。 飞致云开源运营数据概览&…

压缩包方式在Linux和Windows下安装mongodb

目录 安装流程安装实例1. Linux安装2. Windows安装 总结 安装流程 zip方式安装 优点&#xff1a;自定义性较高&#xff0c;可以自己控制数据、日志等文件的位置 1、下载安装包 2、解压安装包 3、创建各类文件路径 4、配置conf文件 5、使用自定义配置文件启动 安装实例 1. Li…

智慧场馆:科技赋能的艺术盛宴

智慧场馆作为城市公共服务设施数字化转型的典型代表&#xff0c;通过深度融合新一代信息技术&#xff0c;构建起全方位、智能化的运营管理体系。其功能架构不仅提升了场馆本身的运营效能&#xff0c;更重塑了公共服务体验模式&#xff0c;展现出显著的社会价值和商业潜力。 一…

《ChatGPT o3抗命:AI失控警钟还是成长阵痛?》

ChatGPT o3 “抗命” 事件起底 在人工智能的飞速发展进程中&#xff0c;OpenAI 于 2025 年推出的 ChatGPT o3 推理模型&#xff0c;犹如一颗重磅炸弹投入了技术的海洋&#xff0c;激起千层浪。它被视为 “推理模型” 系列的巅峰之作&#xff0c;承载着赋予 ChatGPT 更强大问题解…

【sa-token】 sa-token非 web 上下文无法获取 HttpServletRequest。

Springboot cloud gateway集成sa-token中报错 cn.dev33.satoken.exception.NotWebContextException: 非 web 上下文无法获取 HttpServletRequestat cn.dev33.satoken.spring.SpringMVCUtil.getRequest(SpringMVCUtil.java:45) ~[sa-token-spring-boot-starter-1.38.0.jar:?]官…

多台电脑共用一个ip地址可以吗?会怎么样

在互联网使用日益普及的今天&#xff0c;许多人都面临着多台设备共享网络的需求。一个常见的问题随之而来&#xff1a;多台电脑共用一个IP地址可以吗&#xff1f;这样做会带来哪些影响&#xff1f;本文将深入探讨这一话题。 一、多台电脑共用一个‌IP地址可以吗&#xff1f; 多…

线程(上)【Linux操作系统】

文章目录 线程概念及其相关知识线程的概念及一些重要认识重要认识Linux中线程的实现Linux中的被调度的执行流是被task_struct描述的 线程是如何瓜分进程的代码和数据的&#xff1f;对于数据&#xff1a;对于代码&#xff1a; 线程的优点线程的缺点线程调度细节调度&#xff1a;…

进程同步:生产者-消费者 题目

正确答案&#xff1a; 问题类型&#xff1a; 经典生产者 - 消费者问题 同时涉及同步和互斥。 同步&#xff1a;生产者与消费者通过信号量协调生产 / 消费节奏&#xff08;如缓冲区满时生产者等待&#xff0c;空时消费者等待&#xff09;。互斥&#xff1a;对共享缓冲区的访问需…

展会聚焦丨漫途科技亮相2025西北水务博览会!

2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源&#xff0c;数智引领新动能”为主题&#xff0c;活动汇集水务集团、科研院所、技术供应商等全产业链参与者&#xff0c;旨在通过前沿技术展示与…

【数据结构初阶】顺序表的应用

文章目录 顺序表的应用基于动态顺序表实现通讯录前言1.定义联系人数据2.给顺序表改名3.通讯录的初始化4.通讯录的销毁5.通讯录添加数据6.通讯录删除数据7.通讯录修改数据8.通讯录查找数据9.展示通讯录数据10.通讯录的最终实现 顺序表的应用 基于动态顺序表实现通讯录 前言 功…

C#数字图像处理(一)

文章目录 1.C#图像处理基础1.1 Bitmap类1.2 Bitmapdata类1.3 Graphics类1.4 Image类 2.彩色图像灰度化1.提取像素法2.内存法3.指针法三种方法的比较4.灰度图像二值化&#xff1a; 3.相关链接 Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C# 进行…

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力

麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授&#xff0c;介绍了一种新方法&#xff0c;可以让机器人在扫描的家庭环境模拟中接受训练&#xff0c;为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点&#xff0c;研…

从零实现本地语音识别(FunASR)

FunASR 是达摩院开源的综合性语音处理工具包&#xff0c;提供语音识别&#xff08;ASR&#xff09;、语音活动检测&#xff08;VAD&#xff09;、标点恢复&#xff08;PUNC&#xff09;等全流程功能&#xff0c;支持多种主流模型&#xff08;如 Paraformer、Whisper、SenseVoic…

已解决:.NetCore控制台程序(WebAPI)假死,程序挂起接口不通

本问题已得到解决&#xff0c;请看以下小结&#xff1a; 关于《.NetCore控制台程序(WebAPI)假死,程序暂停接口不通》的解决方案 记录备注报错时间2025年报错版本VS2022 WINDOWS10报错复现鼠标点一下控制台&#xff0c;会卡死报错描述——报错截图——报错原因 控制台启用了“快…

Excel如何分开查看工作表方便数据撰写

首先我这里有2class和3class两个工作表 接下来我们点击视图 按照顺序分别点击新建窗口和全部重排 ### 然后就是这样 接下来就OK了

微软技术赋能:解锁开发、交互与数据潜力,共探未来创新路

在微软 Build 2025 大会以及创想未来峰会上&#xff0c;微软展示的一系列前沿技术与创新应用&#xff0c;不仅展现了其在科技领域的深厚底蕴与前瞻视野&#xff0c;更为开发者和企业带来了前所未有的机遇与变革动力。 领驭科技作为微软中国南区核心合作伙伴及 HKCSP 1T 首批授…

VR看房系统,新生代看房新体验

VR看房系统的概念 虚拟现实&#xff08;VirtualReality,VR&#xff09;看房系统&#xff0c;是近年来随着科技进步在房地产行业中兴起的一种创新看房方式。看房系统利用先进的计算机技术模拟出一个三维环境&#xff0c;使用户能够身临其境地浏览和体验房源&#xff0c;无需亲自…

【Linux笔记】Shell-脚本(下)|(常用命令详细版)

在&#xff08;上&#xff09;篇&#xff0c;我们详细的讲解了Shell脚本的基础知识和些许命令与实验&#xff0c;这次的的&#xff08;下&#xff09;篇&#xff0c;我们会详细讲解Shell脚本的常用命令 关于脚本的基础知识请各位移步到&#xff08;上&#xff09;篇啦~ Shell…

钉钉热点实时推送助理-思路篇

以下是针对热点实时推送助理的功能描述&#xff0c;结合机器学习技术栈与用户场景的通俗化解释&#xff1a; 快速体验的话直接用钉钉扫描下方二维码体验 1. 核心功能 &#xff08;1&#xff09;热点抓取引擎 类比&#xff1a;像蜘蛛爬取全网信息&#xff08;网络爬虫信息抽取…