并发编程之【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替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程会尝试使用自旋来获取锁

重量级锁

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

相关推荐
开源之眼7 小时前
《github star 加星 Taimili.com 艾米莉 》为什么Java里面,Service 层不直接返回 Result 对象?
java·后端·github
Maori3167 小时前
放弃 SDKMAN!在 Garuda Linux + Fish 环境下的优雅 Java 管理指南
java
用户908324602738 小时前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
小王和八蛋8 小时前
DecimalFormat 与 BigDecimal
java·后端
beata8 小时前
Java基础-16:Java内置锁的四种状态及其转换机制详解-从无锁到重量级锁的进化与优化指南
java·后端
IT探险家8 小时前
你的第一个 Java 程序就翻车?HelloWorld 的 8 个隐藏陷阱
java
随风飘的云8 小时前
SpringBoot 的自动配置原理
java
SimonKing8 小时前
觅得又一款轻量级数据库管理工具:GoNavi
java·后端·程序员
Seven9710 小时前
BIO详解:解锁阻塞IO的使用方式
java
oak隔壁找我20 小时前
JVM常用调优参数
java·后端