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

重量级锁

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

相关推荐
Mr_Xuhhh8 小时前
LeetCode 热题 100 刷题笔记:从数组到字符串的经典解法(续)
java·数据结构·算法
皙然8 小时前
AQS模型详解:Java并发的核心同步框架(从原理到实战)
java·开发语言·jvm
愤豆8 小时前
08-Java语言核心-JVM原理-垃圾收集详解
java·开发语言·jvm
逸Y 仙X8 小时前
文章十四:ElasticSearch Reindex重建索引
java·大数据·数据库·elasticsearch·搜索引擎·全文检索
wregjru8 小时前
【读书笔记】Effective C++ 条款8:别让异常逃离析构函数
java·开发语言
烤麻辣烫8 小时前
I/O流 进阶流
java·开发语言·学习·intellij-idea
冷血~多好8 小时前
mysql实现主从复制以及springboot实现读写分离
java·数据库·mysql·springboot
山川行8 小时前
Python快速闯关专栏的总结
java·开发语言·笔记·python·算法·visual studio code·visual studio
默归8 小时前
Java云原生时代面临的挑战与变革
java·开发语言·云原生