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 引用逸出

相关推荐
小宇的天下2 小时前
Calibre :SVRF rule file example
java·开发语言·数据库
码农水水2 小时前
大疆Java面试被问:使用Async-profiler进行CPU热点分析和火焰图解读
java·开发语言·jvm·数据结构·后端·面试·职场和发展
m0_561359672 小时前
嵌入式C++调试技术
开发语言·c++·算法
Yang-Never2 小时前
Open GL ES -> 应用前后台、Recent切换,SurfaceView纹理贴图闪烁问题分析解决
android·开发语言·kotlin·android studio·贴图
Howrun7772 小时前
UE C++ 开发全生命周期 + 全场景的知识点清单
开发语言·c++
2301_763472462 小时前
C++中的享元模式高级应用
开发语言·c++·算法
我真的是大笨蛋2 小时前
MVCC解析
java·数据库·spring boot·sql·mysql·设计模式·设计规范
不会代码的小测试2 小时前
UI自动化-针对验证码登录的系统,通过首次手动登录存储cookie的方式后续访问免登录方法
开发语言·python·selenium
weixin_458923202 小时前
分布式日志系统实现
开发语言·c++·算法