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

重量级锁

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

相关推荐
青云计划10 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿10 小时前
Jsoniter(java版本)使用介绍
java·开发语言
探路者继续奋斗11 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
消失的旧时光-194312 小时前
第十九课:为什么要引入消息队列?——异步系统设计思想
java·开发语言
A懿轩A12 小时前
【Java 基础编程】Java 面向对象入门:类与对象、构造器、this 关键字,小白也能写 OOP
java·开发语言
乐观勇敢坚强的老彭13 小时前
c++寒假营day03
java·开发语言·c++
biubiubiu070613 小时前
谷歌浏览器无法访问localhost:8080
java
大黄说说13 小时前
新手选语言不再纠结:Java、Python、Go、JavaScript 四大热门语言全景对比与学习路线建议
java·python·golang
烟沙九洲13 小时前
Java 中的 封装、继承、多态
java
识君啊13 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端