并发编程之【synchronized】

目录

线程安全问题

需求

具体实现

测试结果

解决线程安全问题

解决方式

使用synchronized解决线程安全问题

synchronized几种常见使用方式

代码块

实例方法

静态方法

synchronized底层实现原理

概述

Monitor监视器

查看synchronized同步块的汇编码

使用jconsole命令查看synchronized程序

锁升级的过程

[64位JVM对象头中Mark Word结构](#64位JVM对象头中Mark Word结构)

无锁状态

偏向锁

轻量级锁

重量级锁


线程安全问题

多个线程同时操作共享变量,如有写操作可能会出现线程安全问题,这里通过一个出票的场景来演示线程安全问题

需求

一共有200张票,现通过3个出票机同时出票,出完为止

具体实现

java 复制代码
package concurrency;

public class TicketMachineTest {
    public static void main(String[] args) {
        // 出票任务
        TicketTask task = new TicketTask();
        // 3个出票机
        new Thread(task, "一号出票机").start();
        new Thread(task, "二号出票机").start();
        new Thread(task, "三号出票机").start();
    }
}

// 出票任务
class TicketTask implements Runnable {
    // 当前票号(从1开始)
    private int currentTicketNo = 1;
    // 一共200张票
    private static final int TOTAL_TICKET_COUNT = 200;

    @Override
    public void run() {
        while (true) {
            // 票已经全部出完
            if (currentTicketNo > TOTAL_TICKET_COUNT) {
                break;
            }
            // 还有剩余的票
            // 假设出票耗时5ms
            issueTicketTime(5);
            // 出票结果
            String result = String.format("%s-出了[%s]号票", getMachineName(), currentTicketNo++);
            System.out.println(result);
        }
    }

    /**
     * 获取出票机名字
     */
    public String getMachineName() {
        return Thread.currentThread().getName();
    }

    /**
     * 出票耗时
     */
    public void issueTicketTime(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

测试结果

通过测试,发现存在线程安全问题,具体体现在如下两个方面

  • 多个出票机出了同一张票
  • 实际出票数量超过了规定的总票数

解决线程安全问题

解决方式

单个JVM情况下,可以通过多种方式来解决如上面出现的线程安全问题

  • 使用synchronized
  • 使用ReentrantLock
  • 使用原子类

使用synchronized解决线程安全问题

这里主要介绍通过synchronized来解决线程安全问题

java 复制代码
// 出票任务
class TicketTask implements Runnable {
    // 当前票号(从1开始)
    private int currentTicketNo = 1;
    // 一共200张票
    private static final int TOTAL_TICKET_COUNT = 200;

    @Override
    public void run() {
        // 票已经全部出完
        if (currentTicketNo > TOTAL_TICKET_COUNT) {
            return;
        }
        while (true) {
            // currentTicketNo作为共享变量
            // 多个线程同时对currentTicketNo有写的操作
            // 这里使用synchronized来解决数据一致性问题
            synchronized (TicketTask.class) {
                // DCL
                if (currentTicketNo > TOTAL_TICKET_COUNT) {
                    break;
                }
                // 还有剩余的票
                // 假设出票耗时5ms
                issueTicketTime(5);
                // 出票结果
                String result = String.format("%s-出了[%s]号票", getMachineName(), currentTicketNo++);
                System.out.println(result);
            }
        }
    }

    /**
     * 获取出票机名字
     */
    public String getMachineName() {
        return Thread.currentThread().getName();
    }

    /**
     * 出票耗时
     */
    public void issueTicketTime(long time) {
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

synchronized几种常见使用方式

代码块

java 复制代码
package concurrency;

public class SynchronizedTest {
    private static final Object MONITOR = new Object();

    public static void main(String[] args) {
        // MONITOR对象锁
        synchronized (MONITOR) {

        }
    }
}
java 复制代码
package concurrency;

public class SynchronizedTest {
    public static void main(String[] args) {
        // SynchronizedTest类的字节码对象锁
        synchronized (SynchronizedTest.class) {

        }
    }
}

实例方法

java 复制代码
package concurrency;

public class SynchronizedTest {
    // this锁
    public synchronized void instanceMethod() {
        
    }
}

静态方法

java 复制代码
package concurrency;

public class SynchronizedTest {
    // 当前类的Class对象锁
    public static synchronized void staticMethod() {

    }
}

synchronized底层实现原理

概述

在JVM中,每个对象都关联一个监视器(Monitor),synchronized就是通过Monitor来实现线程同步的,对象头中的MarkWord会指向Monitor,当线程进入同步块时,会尝试获取Monitor,获取成功继续执行,否则阻塞等待

Monitor监视器

每一个对象都和一个监视器Monitor关联,Monitor包含如下几个部分

  • Owner:持有锁的线程
  • EntryList:等待锁的线程队列
  • WaitSet:调用wait方法的线程队列

查看synchronized同步块的汇编码

  • 使用【javap -c】命令查看汇编码
  • 通过下图可以看到,synchronized同步块是monitorenter和monitorexit的过程

使用jconsole命令查看synchronized程序

  • 使用了synchronized的程序如下
java 复制代码
package concurrency;

public class SynchronizedTest {
    private static final Object MONITOR = new Object();

    public static void main(String[] args) {
        // t1线程
        new Thread(() -> {
            synchronized (MONITOR) {
                try {
                    Thread.sleep(100_000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }, "t1").start();

        // t2线程
        new Thread(() -> {
            synchronized (MONITOR) {
            }
        }, "t2").start();

        // t3线程
        new Thread(() -> {
            synchronized (MONITOR) {
            }
        }, "t3").start();
    }
}
  • 使用jconsole命令查看t1、t2、t3线程的执行情况,可以看到t1线程获取到对象锁,t2和t3线程在等待锁而进入BLOCKED状态

锁升级的过程

为减少获取锁和释放锁带来的性能损耗,JDK6之后引入了偏向锁、轻量级锁、重量级锁的概率,并允许锁升级

64位JVM对象头中Mark Word结构

|------------------------------------------------------------------------------------|

| 锁状态 | 25bit | 31bit | 1bit | 4bit |

|--------------|------------------------|-----------------------|------|--------------|

| 无锁 | unused | hashCode | 0 | 01 |

| 偏向锁 | threadID+epoch | age | 1 | 01 |

| 轻量级锁 | 指向栈中锁记录 | (00) | | |

| 重量级锁 | 指向Monitor | (10) | | |

| GC标记 | | (11) | | |

|------------------------------------------------------------------------------------|

无锁状态

初始状态

偏向锁

在无竞争情况下,当一个线程访问同步块时,会在对象头和栈帧中的锁记录里存储偏向线程的ID,之后该线程进入和退出同步块时不需要CAS操作来加锁和释放锁

轻量级锁

当有另一个线程竞争锁时,偏向锁会升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,然后将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程会尝试使用自旋来获取锁

重量级锁

当轻量级锁竞争失败,并且自旋超过一定次数(或者等待线程超过一定数量)时,轻量级锁会升级为重量级锁。重量级锁会使得竞争失败的线程阻塞,直到持有锁的线程释放锁,然后唤醒阻塞的线程

相关推荐
古法老登瞎折腾2 小时前
大数据智能运维实战(十一):运维 MCP 的工程化——权限、限流、审计与错误处理
java·大数据·运维·spring ai·mcp
智能运维指南3 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
yz_aiks3 小时前
Spring AI 2.1 ChatMemory 已过时用什么替代:迁移到 Session Memory 实战(JDBC 存储与会话压缩)
java·spring boot·大模型·ai agent·spring ai
金銀銅鐵4 小时前
[Java] 用 GUI 展示 EnumMap 增删键值对时的内部变化
java·后端·ai编程
学长毕设4 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
jason成都4 小时前
RTKLIB-Java 多基线联合RTK:FIX率从0%到91.5%的三层根因修复
java·gnss·rtklib·rtklib_java
鱼宵4 小时前
Stream 终端操作一次讲透:collect、count、reduce 怎么选
java·stream·终端操作
白山编程大哥4 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list
Linux运维技术栈4 小时前
CVE-2026-91403 高危漏洞修复:Rocky Linux 9 多版本 JDK 环境清理实操
java·linux·运维
On The Road :4 小时前
Java 多线程详解:从入门到线程安全
java·python·安全