Java多线程详解(线程同步)

2023-10-26

        嗨喽~小伙伴们我来了,

        上一章,我们通过几个例子(点击跳转)介绍了线程安全问题,而说到线程安全就不得不提到线程同步,它是解决线程安全的一种重要方法。本章就来简单地介绍一下线程同步

        从上一章的学习我们知道,当多个线程操作一个资源的时候,有可能由于线程的不确定切换出现数据不一致的安全问题,因此,我们要解决这个问题,就得想办法使得资源在某个时间戳只能被一个线程访问。基于这样的思想,我们提出了“队列与锁”的策略:

        通俗理解,就是将所有线程排成一个队列,给要共享的资源上把锁,只有线程获得该资源的锁之后,才能访问该资源

        这也是线程同步的基本思想。所谓线程同步,是指同一进程中的多个线程相互协调工作从而达到一致性。使用线程同步,在解决线程安全问题的同时还能提高程序的性能。

        基于“队列与锁”的思想,JDK中封装了一些用于实现线程同步的类和关键字。我们主要介绍synchronized 和 lock 两种。

一. synchronized

        我们知道,在java中,每个对象都有一把唯一的锁,这也是synchronized实现线程同步的基础。总的来说,synchronized实现线程同步主要有三种形式:

形式 特点
实例同步方法 锁的是当前实例对象,执行同步代码前必须获得当前实例的锁
静态同步方法 锁的是当前类的class对象,执行同步代码前必须获得当前类对象的锁
同步代码块 锁的是括号里的对象,对给定对象加锁,执行同步代码块必须获得给定对象的锁

        事实上,当两个线程同时对一个对象的某个方法进行调用时,只有一个线程能够抢到该对象的锁,因为一个对象只有一把锁。抢到该对象的锁之后,其他线程就无法访问该对象的所有synchronized方法,但仍可以访问该对象中的非synchronized方法

        下面,我们用代码来演示synchronized的三种用法。为了突出synchronized在线程安全方面的作用,我们采用对比(有synchronized和无synchronized)的方式来介绍。

        第一种,修饰实例同步方法。首先,来看一个简单的程序:



/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    //公共资源
    static int count = 0;
    public void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Synchronized(),"A").start();
        new Thread(new Synchronized(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

        如果小伙伴们学习了上一章的内容,应该很容易看出这个程序是存在线程安全问题的,它的输出结果如下:

        如果小伙伴们不太理解的话,可以去看我的上一篇博文。 

        上一篇博文中说到,对于“count++”,JVM是这样处理的:

1. 某线程从内存中读取count值到寄存器

2. 某线程在寄存器中修改count的值

3. 某线程将修改后的count值写入内存

        简单解释一下,我们开启了两个线程去执行increase()方法,如果没有任何保护机制,假设,当count的值累加到1000时,A线程从主内存中读取到寄存器的count值为1000,执行完“count++”操作后,寄存器中的count值为1001,刚想写入内存,B线程正好抢到了CPU的使用权,开始执行run()方法,由于未写入内存,B线程从内存中读取到的count值为仍为1000,执行完“count++”操作后,B线程成功地将1001写入内存,接着A线程将自己寄存器中的count值1001写入内存(覆盖了B线程的1001),由此导致,虽然执行了两个线程,但count的值只累加了一次,这样的情况多发生几次,计算结果自然就低于20000了。

        为了避免以上情况发生,我们给increase()方法加上修饰符synchronized,使得两个线程无法同时调用increase()方法,以保证上面的三步中的任何一步都不会被另外一个线程打断。

         这样,“count++”操作就永远不会因线程切换而出错。代码如下:



/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    //公共资源
    static int count = 0;
    //synchronized实现线程同步
    public synchronized void increase(){
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(new Synchronized(),"A").start();
        new Thread(new Synchronized(),"B").start();
        Thread.sleep(2000);
        System.out.println(count);
    }
}

        现在来看运行结果:

        没有任何问题。

        此外,使用synchronized时,有几个需要注意的地方,请看下面的代码:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public synchronized void running() throws InterruptedException {
        System.out.println("1");
        Thread.sleep(1000);
        System.out.println("2");
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //用同一个类创建两个对象
        Synchronized sync1 = new Synchronized();
        new Thread(sync1).start();
        Synchronized sync2 = new Synchronized();
        new Thread(sync2).start();
    }
}

        如果我们使用不同的对象访问,那么结果可能是不同步的

        因为synchronized修饰实例方法时锁的对象是this对象,而使用两个对象去访问,不是同一把锁。如果我们用同一对象访问:

//只创建一个对象
Synchronized sync = new Synchronized();
new Thread(sync).start();
new Thread(sync).start();

        那结果是同步的:

          接着,我们来看synchronized修饰静态方法的例子:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public static synchronized void running() throws InterruptedException {
        System.out.println("1");
        Thread.sleep(1000);
        System.out.println("2");
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //用同一个类创建两个对象
        Synchronized sync1 = new Synchronized();
        new Thread(sync1).start();
        Thread.sleep(2000);
        Synchronized sync2 = new Synchronized();
        new Thread(sync2).start();
    }
}

        虽然有sync1,sync2两个对象,但是它们是同一个类对象(Synchronized.class)产生的,而synchronized修饰静态方法时锁的是 Synchronized.class,因此两个线程仍然是同步的:

         最后一种,synchronzied修饰同步代码块,它可以锁任何指定的对象,语法示例如下:

synchronized (this){
      System.out.println("1");
      Thread.sleep(1000);
      System.out.println("2");
}

        this指代当前实例对象,可以换为任何对象。

        那么为什么要使用同步代码块呢?

        在某些情况下,我们编写的方法体可能比较庞大,同时又有一些耗时的操作,如果对整个方法体进行同步,效率会大大降低,所以我们希望能够只同步必要的代码块,对于一些不需要同步的或者耗时较长的操作,放到同步代码块之外,比如:


/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote synchronized用法举例
 */

public class Synchronized implements Runnable{
    public void running() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println("这是耗时操作。");
        }
        //需要同步的代码块写下面
        synchronized (this){
            System.out.println("1");
            Thread.sleep(1000);
            System.out.println("2");
        }
    }
    @Override
    public void run() {
        try {
            running();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Synchronized sync = new Synchronized();
        new Thread(sync).start();
        new Thread(sync).start();
    }
}

        运行结果如下:

        再运行一次:

        结果表明,需要同步的代码块确实实现了同步。 

二.  lock         

        前面使用的synchronized关键字可以实现多线程间的同步互斥,其实,在JDK1.5后新增的ReentrantLock 类同样可以实现这个功能,而且在使用上比 synchronized ​​​​​​​更为灵活。

        翻阅源码,可以看到 ReentrantLock 类实现了Lock接口:

         下面我们用ReentrantLock类来实现简单的线程同步:


import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author sixibiheye
 * @date 2021/8/31
 * @apiNote ReentrantLock实现线程同步
 */
public class LockDemo implements Runnable{
    private Lock lock = new ReentrantLock();
    /**
     * 
     * 按照规范,
     * lock()后面应紧跟try代码块,
     * 并将unlock()放到finally块的第一行
     * 
     */
    @Override
    public void run() {
        //上锁
        lock.lock();
        try {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "线程中的i=" + i);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
    public static void main(String[] args) 
        new Thread(new LockDemo(),"A").start();
        new Thread(new LockDemo(),"B").start();
    }
}

        其实关于Lock 和 ReentrantLock,还有许多其他的用法,限于篇幅,就不一一介绍了,有兴趣的小伙伴们可以查阅相关资料。

        了解了synchronized和ReentrantLock,对于上一章提出的三个线程安全问题,便可以轻松地解决了。下面提供使用synchronized的解决方式:


/**
 * @author sixibiheye
 * @date 2021/8/28
 * @apiNote 线程安全问题一-------取钱问题
 */
public class UnsafeBank {

    public static void main(String[] args) {
        //账户
        Account account = new Account(100,"买房基金");
        //你和你的妻子都要取钱
        Drawing you = new Drawing(account,50,"你");
        Drawing girlFriend = new Drawing(account,100,"妻子");
        you.start();
        girlFriend.start();
    }
}

//账户
class Account{
    int money; //余额
    String name; //卡名
    public Account(int money,String name){
        this.money = money;
        this.name = name;
    }
}

//银行
class Drawing extends Thread{
    Account account; //账户
    int drawingMoney; //取的钱
    int nowMoney; //现手上有的钱
    public Drawing(Account account,int drawingMoney,String name){
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }
    @Override
    public void run() {
        //注意锁的是account对象,而不是this
        synchronized (account){
            if(account.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName() + "钱不够了,取不了!");
                return;
            }
            try {
                sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //卡内余额
            account.money = account.money - drawingMoney;
            //手里的钱
            nowMoney = nowMoney + drawingMoney;
            //打印
            System.out.println(account.name + "余额为:" + account.money);
            System.out.println(Thread.currentThread().getName() + "手里的钱:" + nowMoney);
        }
        }
}


/**
 * @author sixibiheye
 * @date 2021/8/27
 * @apiNote 线程安全问题一-------买票问题
 */
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket,"小红").start();
        new Thread(buyTicket,"小白").start();
        new Thread(buyTicket,"小黑").start();
    }
}

class BuyTicket implements Runnable{
    //票数
    private int tickets = 10;
    //线程停止的标志位
    private boolean flag = true;
    //直接同步实例方法即可
    private synchronized void buy() throws InterruptedException {
        //判断是否有票
        if(tickets <= 0){
            flag = false;
            return;
        }
        //模拟延时,保证多人都能买到票
        Thread.sleep(20);
        //买票
        System.out.println(Thread.currentThread().getName() + "拿到了第" + tickets-- +"张票");

    }
    @Override
    public void run() {
        while (flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


import java.util.ArrayList;
import java.util.List;

/**
 * @author sixibiheye
 * @date 2021/8/28
 * @apiNote 线程安全问题一-------列表问题
 */

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread( () -> {
                //同步需要修改的list对象
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        //sleep保证上述for循环跑完再输出
        Thread.sleep(3000);
        //输出列表大小
        System.out.println(list.size());
    }
}

        小伙伴们可以思考一下如果使用ReentrantLock应该如何解决。

        下一章,我们将着重介绍Thread类中的一些常用方法(点击跳转),好啦~本章的内容到这就结束了,喜欢的小伙伴们点个赞鼓励支持一下吧~ 

         

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java多线程详解(线程同步) 的相关文章

  • 如何将本机库链接到 IntelliJ 中的 jar?

    我正在尝试在 IntelliJ 中设置 OpenCV 但是我一直在弄清楚如何告诉 IntelliJ 在哪里可以找到本机库位置 在 Eclipse 中 添加 jar 后 您可以在 Build Config 屏幕中设置 Native 库的位置
  • 如何让 BlazeDS 忽略属性?

    我有一个 java 类 它有一个带有 getter 和 setter 的字段 以及第二对 getter 和 setter 它们以另一种方式访问 该字段 public class NullAbleId private static final
  • 不同帐户上的 Spring Boot、JmsListener 和 SQS 队列

    我正在尝试开发一个 Spring Boot 1 5 应用程序 该应用程序需要侦听来自两个不同 AWS 帐户的 SQS 队列 是否可以使用 JmsListener 注解创建监听器 我已检查权限是否正确 我可以使用 getQueueUrl 获取
  • 序列的排列?

    我有具体数量的数字 现在我想以某种方式显示这个序列的所有可能的排列 例如 如果数字数量为3 我想显示 0 0 0 0 0 1 0 0 2 0 1 0 0 1 1 0 1 2 0 2 0 0 2 1 0 2 2 1 0 0 1 0 1 1 0
  • Mockito:如何通过模拟测试我的服务?

    我是模拟测试新手 我想测试我的服务方法CorrectionService correctPerson Long personId 实现尚未编写 但这就是它将执行的操作 CorrectionService将调用一个方法AddressDAO这将
  • 如何通过 javaconfig 使用 SchedulerFactoryBean.schedulerContextAsMap

    我使用 Spring 4 0 并将项目从 xml 移至 java config 除了访问 Service scheduleService 带注释的类来自QuartzJobBean executeInternal 我必须让它工作的 xml 位
  • .properties 中的通配符

    是否存在任何方法 我可以将通配符添加到属性文件中 并且具有所有含义 例如a b c d lalalala 或为所有以结尾的内容设置一个正则表达式a b c anything 普通的 Java 属性文件无法处理这个问题 不 请记住 它实际上是
  • org.apache.hadoop.security.AccessControlException:客户端无法通过以下方式进行身份验证:[TOKEN,KERBEROS] 问题

    我正在使用 java 客户端通过 Kerberos 身份验证安全访问 HDFS 我尝试打字klist在服务器上 它显示已经存在的有效票证 我收到的异常是客户端无法通过以下方式进行身份验证 TOKEN KERBEROS 帮助将不胜感激 这是一
  • 如何在java中将一个数组列表替换为另一个不同大小的数组列表

    我有两个大小不同的数组列表 如何从此替换 ArrayList
  • 过滤两次 Lambda Java

    我有一个清单如下 1 2 3 4 5 6 7 和 预期结果必须是 1 2 3 4 5 6 7 我知道怎么做才能到7点 我的结果 1 2 3 4 5 6 我也想知道如何输入 7 我添加了i gt i objList size 1到我的过滤器
  • HSQL - 识别打开连接的数量

    我正在使用嵌入式 HSQL 数据库服务器 有什么方法可以识别活动打开连接的数量吗 Yes SELECT COUNT FROM INFORMATION SCHEMA SYSTEM SESSIONS
  • Pig Udf 显示结果

    我是 Pig 的新手 我用 Java 编写了一个 udf 并且包含了一个 System out println 其中的声明 我必须知道在 Pig 中运行时该语句在哪里打印 假设你的UDF 扩展了 EvalFunc 您可以使用从返回的 Log
  • 如何在 Spring 中禁用使用 @Component 注释创建 bean?

    我的项目中有一些用于重构逻辑的通用接口 它看起来大约是这样的 public interface RefactorAwareEntryPoint default boolean doRefactor if EventLogService wa
  • 在 Jar 文件中运行 ANT build.xml 文件

    我需要使用存储在 jar 文件中的 build xml 文件运行 ANT 构建 该 jar 文件在类路径中可用 是否可以在不分解 jar 文件并将 build xml 保存到本地目录的情况下做到这一点 如果是的话我该怎么办呢 Update
  • 帮助将图像从 Servlet 获取到 JSP 页面 [重复]

    这个问题在这里已经有答案了 我目前必须生成一个显示字符串文本的图像 我需要在 Servlet 上制作此图像 然后以某种方式将图像传递到 JSP 页面 以便它可以显示它 我试图避免保存图像 而是以某种方式将图像流式传输到 JSP 自从我开始寻
  • jdbc mysql loginTimeout 不起作用

    有人可以解释一下为什么下面的程序在 3 秒后超时 因为我将其设置为在 3 秒后超时 12秒 我特意关闭了mysql服务器来测试mysql服务器无法访问的这种场景 import java sql Connection import java
  • 在我的 Spring Boot 示例中无法打开版本 3 中的 Swagger UI

    我在 Spring Boot 示例中打开 swagger ui 时遇到问题 当我访问 localhost 8080 swagger ui 或 localhost 8080 root api name swagger ui 时出现这种错误 S
  • 最新的 Hibernate 和 Derby:无法建立 JDBC 连接

    我正在尝试创建一个使用 Hibernate 连接到 Derby 数据库的准系统项目 我正在使用 Hibernate 和 Derby 的最新版本 但我得到的是通用的Unable to make JDBC Connection error 这是
  • 如果没有抽象成员,基类是否应该标记为抽象?

    如果一个类没有抽象成员 可以将其标记为抽象吗 即使没有实际理由直接实例化它 除了单元测试 是的 将不应该实例化的基类显式标记为抽象是合理且有益的 即使在没有抽象方法的情况下也是如此 它强制执行通用准则来使非叶类抽象 它阻止其他程序员创建该类
  • Spring Boot 无法更新 azure cosmos db(MongoDb) 上的分片集合

    我的数据库中存在一个集合 documentDev 其分片键为 dNumber 样本文件 id 12831221wadaee23 dNumber 115 processed false 如果我尝试使用以下命令通过任何查询工具更新此文档 db

随机推荐

  • 新闻列表案例(前端html,css)

    去掉列表默认的样式 无序和有序列表前面默认的列表样式 在不同浏览器显示效果不一样 而且也比较难看 所以 我们一般上来就直接去掉这些列表样式就行了 li list style none 代码如下
  • 看spring cloud开源项目Pig的云踩坑记

    最近看到一个有趣的开源项目pig 主要的技术点在认证授权中心 spring security oauth zuul网关实现 Elastic Job定时任务 趁着刚刚入门微服务 赶快写个博客分析一下 此篇文章主要用于个人备忘 如果有不对 请批
  • 汇编语言如何输出结果_量子计算遇上高性能计算系列(五)初识量子汇编语言...

    汇编语言是直接工作在硬件之上的最底层的编程语言 众所周知 计算机中所有的数据和指令都是由0和1组成的 例如 01010000 机器指令在CPU上运行的时候就是一组电平脉冲信号 而01010000对于人们来讲太难理解 因此 就产生出更加便于人
  • react做表格和分页功能

    import React memo useState useEffect from react import Table Pagination from antd import IDefaultParam from topFilter to
  • ASyncSocket库

    iphone的标准推荐CFNetwork C库编程 但是编程比较烦躁 在其它OS往往用类来封装的对Socket函数的处理 比如MFC的CAsysncSocket 在iphone也有类似于开源项目 cocoa AsyncSocket库 官方网
  • LLaMA模型加载报错_sentencepiece.SentencePieceProcessor_LoadFromFile(self, arg) TypeError: not a string

    tokenizer LlamaTokenizer from pretrained lora model path lora model path这一项不是string类型 运行命令有参数项目为 lora model ziqingyang c
  • html js动态时间轴,jQuery时间轴插件timeline.js

    插件描述 timeline js是一款jQuery时间轴插件 通过timeline js插件 你可以很容易的制作出水平或垂直时间轴效果 并可以像幻灯片一样前后切换时间点 简要教程 timeline js是一款jQuery时间轴幻灯片插件 通
  • C51单片机重要知识点总结

    文章目录 文章目录 00 写在前面 01 C51基本数据类型总结 02 C51数据类型扩充定义 03 关于单片机 04 单片机工作的基本时序 05 单片机复位 06 80C51的中断系统 07 定时器 08 串口通信 09 C语言基础 10
  • Unable to cast object of type ‘System.Byte‘ to type ‘System.Boolean‘

    mysql数据库 存储的一个字段类型为tinyint 查询数据的时候设置的数据类型bool 查询结果报错 解决方法 将为空的数据都设为0 查询资料时 tinyint在查询时会自动对应成bool类型 问题原因时数据不可为空 即使设置成bool
  • Hi3516Dv300 平台使用MIPI Tx点屏

    背景 公司新做了一块3516Dv300的开发板 其中有MIPI Tx接口 刚好公司库房还有好几百块的LCD屏 LCD屏是800x480的 还是原装屏 不用掉怪可惜的了 所以就让硬件的同事化了个转接板 使用的芯片是ICN6211 这货最大分辨
  • python pip 安装 删除缓存(cache)

    今天pip安装包时 一直使用缓存 非常不爽 pip删除缓存 安装操作 pip no cache dir install 包名 If using pip 6 0 or newer try adding the no cache dir opt
  • 什么是1G/2G/3G/4G/5G

    什么是1G 2G 3G 4G 5G 参考 http www 360doc com content 14 1213 22 5458405 432718054 shtml 介绍 1G 表示第一代移动通讯技术 在20世纪80年代前期 第一代模拟制
  • CIMCO Edit2022安装教程(非常详细)从零基础入门到精通,看完这一篇就够了(附安装包)

    软件下载 软件 CIMCO Edit 版本 2022 语言 简体中文 大小 251 79M 安装环境 Win11 Win10 Win8 Win7 硬件要求 CPU 2 0GHz 内存 4G 或更高 下载通道 百度网盘丨64位下载链接 htt
  • CommonJS是啥东西嘞

    AMD AMD要用define定一个模块 define dep1 dep2 function dep1 dep2 return function 包目录 package json包 bin用于可的目 lib用于JavaScript的目 do
  • sqli-libs基础篇总结(1-22)

    1 关于sqli labs 这个是sql注入的靶场 可以在git上下载 2 题目简介 前面的1 22题都是sql注入的基础题目 覆盖范围很广 不过都是针对mysql数据库的 1 4题 union注入 5 8题 布尔盲注 9 10题 延时盲注
  • sql server备份及导出表数据和结构

    一 备份表数据及结构 select into new table name from old tablename 二 导出表数据及结构 1 选中要导出的数据库 gt 任务 gt 生成脚本 或者在任务里面有生成脚本这个选项 好好找找能找到的
  • 高清变脸更快更逼真!比GAN更具潜力的可逆生成模型来了

    昨天上市即破发的小米 今天上午股价大涨近10 这下雷军要笑了 而且可以笑得更灿烂 更灿烂是什么样 来 我们用OpenAI刚刚发布的人工智能技术 给大家展示一下 当然这个最新的技术 不止这点本事 它的 想象力 很强大的说 比如 留胡子的硬汉版
  • 关于eclipse项目栏关闭项目不想再看到

    前言 如果你用是什么IntelliJ IDEA我这篇文章你就不用看了 我的建议还是用IDEA我也喜欢用 但是因为我们老师电脑卡的原因 这个编辑器比较吃配置所以用的eclipse 以前还用的myeclipse虽然我对编辑器没什么要求 但是我用
  • Jmeter常用线程组设置策略

    一 前言 在JMeter压力测试中 我们时常见到的几个场景有 单场景基准测试 单场景并发测试 单场景容量测试 混合场景容量测试 混合场景并发测试以及混合场景稳定性测试 在本篇文章中 我们会用到一些插件 在这边先给大家列出 Custom Th
  • Java多线程详解(线程同步)

    嗨喽 小伙伴们我来了 上一章 我们通过几个例子 点击跳转 介绍了线程安全问题 而说到线程安全就不得不提到线程同步 它是解决线程安全的一种重要方法 本章就来简单地介绍一下线程同步 从上一章的学习我们知道 当多个线程操作一个资源的时候 有可能由