synchronized总结

一、synchronized 的特性

1. 原子性

  • 确保被保护的代码块或方法在同一时刻只能被一个线程执行

  • 防止多线程并发访问导致的数据不一致问题

假设:

  • 电影院有 100张票

  • 3个售票窗口 同时 售票

  • 每个窗口都尝试卖出这100张票

无同步的情况(竞态条件)

java 复制代码
public class CinemaTicketProblem {
    public static void main(String[] args) {
        TicketSystem ticketSystem = new TicketSystem();
        
        // 创建3个售票窗口(线程)
        Thread window1 = new Thread(ticketSystem, "窗口1");
        Thread window2 = new Thread(ticketSystem, "窗口2");
        Thread window3 = new Thread(ticketSystem, "窗口3");
        
        System.out.println("=== 电影院开始售票(无同步版) ===");
        window1.start();
        window2.start();
        window3.start();
    }
}

class TicketSystem implements Runnable {
    private int tickets = 100;  
    
    @Override
    public void run() {
        while (tickets > 0) {
            try {
                Thread.sleep(50);  // 模拟售票耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 非原子操作:检查票数 → 卖票 → 减库存
            System.out.println(Thread.currentThread().getName() + 
                " 卖出第 " + tickets + " 号票");
            tickets--;
        }
        System.out.println(Thread.currentThread().getName() + " 结束售票");
    }
}

问题 :当tickets=1时,三个窗口可能同时检查到tickets > 0为真,然后都卖出了"第1张票",导致:

  • 卖了3次"第1张票"

  • 实际多卖了票(卖出了101张或更多)

使用synchronized保证原子性

java 复制代码
public class CinemaTicketSolution {
    public static void main(String[] args) throws InterruptedException {
        TicketSystemSync ticketSystem = new TicketSystemSync();
        
        Thread window1 = new Thread(ticketSystem, "窗口1");
        Thread window2 = new Thread(ticketSystem, "窗口2");
        Thread window3 = new Thread(ticketSystem, "窗口3");
        
        System.out.println("=== 电影院开始售票(同步版) ===");
        window1.start();
        window2.start();
        window3.start();
        
        // 等待所有售票结束
        window1.join();
        window2.join();
        window3.join();
        
        System.out.println("=== 售票结束,最终余票:" + ticketSystem.getRemainingTickets() + " ===");
    }
}

class TicketSystemSync implements Runnable {
    private int tickets = 100;
    
    // synchronized保证整个售票过程的原子性
    @Override
    public void run() {
        while (true) {
            if (!sellTicket()) {
                break;  // 票已售完
            }
        }
        System.out.println(Thread.currentThread().getName() + " 结束售票");
    }
    
    // 关键:售票方法用synchronized修饰
    private synchronized boolean sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(50);  // 模拟售票耗时
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            System.out.println(Thread.currentThread().getName() + 
                " 卖出第 " + tickets + " 号票");
            tickets--;
            return true;
        }
        return false;
    }
    
    public synchronized int getRemainingTickets() {
        return tickets;
    }
}

2. 可见性

  • 线程解锁前,必须将共享变量的最新值刷新到主内存

  • 线程加锁时,将清空工作内存中共享变量的值,需要从主内存重新读取

  • 遵循 happens-before 原则中的监视器锁规则

例一:

办公室项目进度板

理想情况(有可见性)

早上9:00:项目经理更新白板:"项目deadline提前到明天"

↓ 立即广播通知

所有团队成员瞬间看到最新deadline

↓ 大家调整工作计划

java 复制代码
public class ProjectBoard {
    // volatile保证可见性
    private volatile String deadline = "下周五";
    
    public void updateDeadline(String newDeadline) {
        deadline = newDeadline;  // 修改立即对所有线程可见
        System.out.println("项目经理更新deadline为:" + newDeadline);
    }
    
    public void checkDeadline(String staffName) {
        System.out.println(staffName + " 看到deadline:" + deadline);
    }
}

例二:

火车站售票大屏(无可见性)

场景描述

  • 火车站后台系统:剩余票数 tickets = 1

  • 显示大屏(线程A):读取票数显示

  • 10个售票窗口(线程B-K):同时卖最后一张票

java 复制代码
public class TicketSystemProblem {
    // 没有volatile,修改可能不及时可见
    private int remainingTickets = 1;
    private boolean soldOut = false;
    
    public void sellTicket(String windowName) {
        if (!soldOut) {
            // 窗口1:卖票成功,剩余0
            remainingTickets--;
            soldOut = true;
            
            try { Thread.sleep(100); } // 模拟处理时间
            System.out.println(windowName + " 卖出最后一张票");
        } else {
            System.out.println(windowName + ":票已售完");
        }
    }
    
    public void displayScreen() {
        // 大屏线程一直显示剩余票数
        while (true) {
            // 可能一直显示旧值1,看不到已售出
            System.out.println("大屏显示:剩余 " + remainingTickets + " 张票");
            try { Thread.sleep(50); } catch (Exception e) {}
        }
    }
}

可能发生的场景:

时间线:

10:00:00 - 窗口1:卖出最后一张票,remainingTickets=0

10:00:01 - 大屏显示:剩余1张票 ← 看不到更新!

10:00:02 - 旅客A看到大屏显示有票,排队30分钟

10:00:03 - 窗口2:检查soldOut还是false(看不到更新)

10:00:04 - 窗口2:试图卖票,系统出错!

10:00:32 - 旅客A排到窗口:票已售完,愤怒投诉

3. 有序性

  • 通过互斥执行确保代码有序性

  • 禁止指令重排序优化(as-if-serial 语义在单线程内依然有效)

做菜的错误顺序

正常做菜顺序

public void cookRamen() {

  1. 烧水(); // 第一步

  2. 下面条(); // 第二步(必须等水开)

  3. 加调料(); // 第三步

  4. 出锅(); // 第四步

}

处理器"优化"后的重排序

// 编译器/CPU觉得这样更高效:

public void cookRamen() {

  1. 加调料(); // 先做这个(因为烧水要等)

  2. 烧水(); //

  3. 出锅(); // ← 面条还没下!

  4. 下面条(); // 顺序错了!

}

结果:端出一碗开水+干调料,面条还在手里。

二、synchronized 的使用方式

1. 同步代码块

java 复制代码
// 锁对象可以是任意对象
synchronized(lockObject) {
    // 需要同步的代码
}

2. 同步实例方法

java 复制代码
public synchronized void method() {
    // 锁的是当前实例对象 this
}

3. 同步静态方法

java 复制代码
public static synchronized void staticMethod() {
    // 锁的是当前类的 Class 对象
}

三、synchronized 的锁机制

1. 锁的存储位置

  • 锁信息存储在 Java 对象头中的 Mark Word 中

  • 包括锁状态标志位、指向锁记录的指针等信息

2. 锁升级过程(JDK 1.6 优化)

无锁状态

  • 初始状态,没有线程竞争

偏向锁

  • 适用场景:只有一个线程访问同步块

  • 原理:当线程访问同步块时,通过 CAS 操作将线程 ID 记录到 Mark Word

  • 优点:加锁解锁不需要额外的 CAS 操作

轻量级锁

  • 适用场景:多个线程交替执行,没有实际竞争

  • 原理

    1. 在当前线程的栈帧中创建锁记录(Lock Record)

    2. 将对象头的 Mark Word 复制到锁记录中

    3. 使用 CAS 尝试将对象头指向锁记录

  • 加锁失败会升级为重量级锁

重量级锁

  • 适用场景:多个线程竞争激烈

  • 原理:通过操作系统互斥量(mutex)实现,线程会进入阻塞状态

  • 涉及用户态到内核态的切换,性能开销较大

3. 锁消除

  • JIT 编译器在运行时,如果检测到不可能存在共享数据竞争,会消除锁

4. 锁粗化

  • 将多个连续的加锁、解锁操作合并为一次范围更大的锁操作

  • 减少不必要的锁竞争开销

5. 锁的释放

  • 正常执行完成同步代码块

  • 抛出异常跳出同步代码块

  • 调用锁对象的 wait() 方法(会释放锁并进入等待池)

四、synchronized 的优缺点

优点

  1. 使用简单,语义清晰

  2. JVM 原生支持,自动管理锁的获取和释放

  3. 锁升级机制提高了性能

缺点

  1. 不够灵活,获取和释放锁的方式单一

  2. 不可中断,等待锁的线程会一直阻塞

  3. 无法实现公平锁

  4. 读写场景下效率不如 ReentrantReadWriteLock

五、注意事项

  1. 避免锁的嵌套:可能导致死锁

  2. 锁对象的选择

    • 通常使用 private final 对象

    • 不要使用 String 常量、基本类型包装类等作为锁

  3. 同步范围:尽量缩小同步代码块的范围,提高并发性能

  4. 避免在构造方法中使用 synchronized:可能导致 this 引用逸出

相关推荐
R1nG8638 分钟前
多线程安全设计 CANN Runtime关键数据结构的锁优化
开发语言·cann
初次见面我叫泰隆8 分钟前
Qt——5、Qt系统相关
开发语言·qt·客户端开发
亓才孓13 分钟前
[Class的应用]获取类的信息
java·开发语言
开开心心就好21 分钟前
AI人声伴奏分离工具,离线提取伴奏K歌用
java·linux·开发语言·网络·人工智能·电脑·blender
Never_Satisfied25 分钟前
在JavaScript / HTML中,关于querySelectorAll方法
开发语言·javascript·html
80530单词突击赢34 分钟前
JavaWeb进阶:SpringBoot核心与Bean管理
java·spring boot·后端
3GPP仿真实验室1 小时前
【Matlab源码】6G候选波形:OFDM-IM 增强仿真平台 DM、CI
开发语言·matlab·ci/cd
devmoon1 小时前
在 Polkadot 上部署独立区块链Paseo 测试网实战部署指南
开发语言·安全·区块链·polkadot·erc-20·测试网·独立链
lili-felicity1 小时前
CANN流水线并行推理与资源调度优化
开发语言·人工智能
爬山算法1 小时前
Hibernate(87)如何在安全测试中使用Hibernate?
java·后端·hibernate