由浅到深认识Java语言(33):多线程

该文章Github地址:https://github.com/AntonyCheng/java-notes

在此介绍一下作者开源的SpringBoot项目初始化模板(Github仓库地址:https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址:https://blog.csdn.net/AntonyCheng/article/details/136555245),该模板集成了最常见的开发组件,同时基于修改配置文件实现组件的装载,除了这些,模板中还有非常丰富的整合示例,同时单体架构也非常适合SpringBoot框架入门,如果觉得有意义或者有帮助,欢迎Star & Issues & PR!

上一章:由浅到深认识Java语言(32):多线程

42.多线程

生产者与消费者案例

生产者和消费者一边生产一边消费,即两条不同的线程共同操作同一个资源,该问题会面临线程安全问题,示例如下:

package top.sharehome.Test;

public class Demo {
    public static void main(String[] args) {
        Resourse r = new Resourse();
        Produce produce = new Produce(r);
        Customer customer = new Customer(r);
        Thread t0 = new Thread(produce);
        Thread t1 = new Thread(customer);
        t0.start();
        t1.start();
    }
}

/**
 * 定义资源对象
 */
class Resourse {
    //该计数器用来记录生产的个数
    int count;
    //该标记用来提示生产者或者消费者工作
    //true:生产好了,等待消费
    //false:消费好了,等待生产
    boolean flag;
}

/**
 * 生产者线程
 */
class Produce implements Runnable {
    private Resourse r;

    public Produce(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (r) {
                if (r.flag == true) {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                r.count++;
                System.out.println("生成第" + r.count + "个");
                r.flag = true;
                r.notify();
            }
        }
    }
}

/**
 * 消费者线程
 */
class Customer implements Runnable {
    private Resourse r;

    public Customer(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (r) {
                if (r.flag == false) {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("消费第" + r.count + "个");
                r.flag =false;
                r.notify();
            }
        }
    }
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

多生产和多消费

示例代码如下:

package top.sharehome.Test;

public class Demo {
    public static void main(String[] args) {
        Resourse r = new Resourse();
        Produce produce = new Produce(r);
        Customer customer = new Customer(r);
        new Thread(produce).start();
        new Thread(produce).start();
        new Thread(produce).start();
        new Thread(customer).start();
        new Thread(customer).start();
        new Thread(customer).start();
    }
}

/**
 * 定义资源对象
 */
class Resourse {
    //该计数器用来记录生产的个数
    int count;
    //该标记用来提示生产者或者消费者工作
    //true:生产好了,等待消费
    //false:消费好了,等待生产
    boolean flag;
}

/**
 * 生产者线程
 */
class Produce implements Runnable {
    private Resourse r;

    public Produce(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (r) {
                if (r.flag == true) {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                r.count++;
                System.out.println(Thread.currentThread().getName()+"生成第" + r.count + "个");
                r.flag = true;
                r.notify();
            }
        }
    }
}

/**
 * 消费者线程
 */
class Customer implements Runnable {
    private Resourse r;

    public Customer(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (r) {
                if (r.flag == false) {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+"消费第" + r.count + "个");
                r.flag =false;
                r.notify();
            }
        }
    }
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

安全问题产生的原因:

  1. 线程本身就是一个新创建的方法栈内存,并不代表具体的功能,所以在线程唤醒时,CPU并不会分类选择,而是随机选择,即正常顺序应该是生产者生产后消费者消费,但是CPU不认识谁是生产者,谁是消费者,所以会随机选择;
  2. 线程的唤醒 notify() 并不能指定唤醒谁,但是会优先唤醒第一个等待的线程;
  3. 被唤醒的线程,已经进行过 if 判断,一旦醒来就会继续执行,不会理会我们所设置的标志 flag;

解决办法:

由于Java并不能控制 CPU等硬件,所以第一条原因我们没有办法进行优化;

所以我们能做的就是改进唤醒机制,为了避免一个线程唤醒后同类型线程再被唤醒不判别就开始运行,我们可以统一重置一下运行顺序,即在同一时间唤醒全部线程,给予所有线程等可能性的被选概率,然后将 if 判断改为 while 循环判断,防止不满足标识符要求就开始运行;

全部重新唤醒的方法是 对象.notifyAll() ;

示例如下:

package top.sharehome.Test;

public class Demo {
    public static void main(String[] args) {
        Resourse r = new Resourse();
        Produce produce = new Produce(r);
        Customer customer = new Customer(r);
        new Thread(produce).start();
        new Thread(produce).start();
        new Thread(produce).start();
        new Thread(customer).start();
        new Thread(customer).start();
        new Thread(customer).start();
    }
}

/**
 * 定义资源对象
 */
class Resourse {
    //该计数器用来记录生产的个数
    int count;
    //该标记用来提示生产者或者消费者工作
    //true:生产好了,等待消费
    //false:消费好了,等待生产
    boolean flag;
}

/**
 * 生产者线程
 */
class Produce implements Runnable {
    private Resourse r;

    public Produce(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (r) {
                while (r.flag == true) {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                r.count++;
                System.out.println(Thread.currentThread().getName()+"生成第" + r.count + "个");
                r.flag = true;
                r.notifyAll();
            }
        }
    }
}

/**
 * 消费者线程
 */
class Customer implements Runnable {
    private Resourse r;

    public Customer(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (r) {
                while (r.flag == false) {
                    try {
                        r.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(Thread.currentThread().getName()+"消费第" + r.count + "个");
                r.flag =false;
                r.notifyAll();
            }
        }
    }
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

sleep()和wait()的区别

  • sleep() 在休眠的过程中,同步锁不会丢失,不会释放;
  • wait() 在等待时会释放同步锁,被唤醒之后需要重新获得同步锁才能开始执行;

生产者和消费者案例性能问题

  • wait() 方法和 notify() 方法,本地方法调用 OS 的功能,和操作系统交互,JVM 找到 OS,把线程停止,频繁等待与唤醒,导致 JVM 和 OS 交互的次数过多;
  • notifyAll() 方法唤醒全部的线程,会造成巨大的线程资源浪费,为了一个线程不得已唤醒了全部的线程;
阻塞队列方案(两大接口优化)

用 Lock 接口替换了同步锁 synchronized,提供了更加灵活,性能更好的锁定操作;

用 Condition 集合接口中 await() 方法替换掉需要设置在 synchronized 中的 wait() 方法;

用 Condition 集合接口中 signal() 方法替换掉需要设置在 synchronized 中的 notify() 方法;

问题分析如下:

由于类似于生产者和消费者的案例具有极其严重的性能问题,而该性能问题主要是由于 wait() 方法和 notify() 或者 notifyAll() 方法的自身缺陷所造成的,其次就是 CPU 并不能分辨出哪一条进程是生产者,哪一条进程是消费者而导致的资源争夺问题,阻塞队列就是来解决这两种问题的;

优化需求:

  • 减少或者阻断程序通过本地方法和 OS 的交互,减少操作系统的运行压力;
  • 将每一类进程做好分类,用容器装起来,避免资源的相互争夺;

优化思路:

  • 我们可以用使用 Lock 接口中的 lock() 方法和 unlock() 方法,来替换掉同步代码块,以便能够更加灵活地上锁和解锁;
  • 我们可以用 Condition 接口作为容器,分别容纳两类线程;该接口也成为线程的阻塞队列;
    • Condition 容器是一个队列集合(先进先出)接口,专门用来装线程对象,通过 Lock 接口中的 newCondition() 方法返回一个 Condition 对象而产生,即获得该队列的前提是先有锁,一把锁让两个或多个队列分别轮流使用
  • 再调用 Condition 对象下特有的 await() 方法和 signal() 方法来分别替换 wait() 和 notify() ;
    • await() 让线程释放锁,同时进入队列;
    • signal() 让线程再次获得锁,同时退出队列

示例如下:

package top.sharehome.Test;

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

public class Demo {
    public static void main(String[] args) {
        Resourse r = new Resourse();
        Produce pro = new Produce(r);
        Customer cus = new Customer(r);
        new Thread(pro).start();
        new Thread(pro).start();
        new Thread(pro).start();
        new Thread(cus).start();
        new Thread(cus).start();
        new Thread(cus).start();
    }
}

/**
 * 定义资源对象
 */
class Resourse {
    //该计数器用来记录生产的个数
    private int count;
    //该标记用来提示生产者或者消费者工作
    //true:生产好了,等待消费
    //false:消费好了,等待生产
    private boolean flag;
    private Lock lock = new ReentrantLock();
    //生产者线程的阻塞队列
    private Condition pro = lock.newCondition();
    //消费者线程的阻塞队列
    private Condition cus = lock.newCondition();

    /**
     * 生产者线程
     */
    public void getCount() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //获取锁
        lock.lock();
        //无限等待,直至标识符改变
        while (flag) {
            try {
                pro.await();
                //使用Condition集合中特有的等待方法,随即释放锁
                //同时将生产者线程放入了Condition集合之中
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.count++;
        System.out.println(Thread.currentThread().getName() + "生成第" + this.count + "个");
        //修改标签,完成生产
        this.flag = true;
        //唤醒消费线程队列的一个
        cus.signal();
        //释放锁
        lock.unlock();
    }


    /**
     * 消费者线程
     */
    public void setCount() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //获取锁
        lock.lock();
        //无限等待,直至标识符改变
        while (!this.flag) {
            try {
                cus.await();
                //使用Condition中特有的等待方法,释放锁
                //同时将消费者线程放入了Condition集合之中
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "消费第" + this.count + "个");
        //修改标签,完成消费
        this.flag = false;
        //唤醒生产线程队列的一个
        pro.signal();
        //释放锁
        lock.unlock();
    }
}

/**
 * 生产者类
 */
class Produce implements Runnable {
    private Resourse r;

    public Produce(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.getCount();
        }
    }
}

/**
 * 消费者类
 */
class Customer implements Runnable {
    private Resourse r;

    public Customer(Resourse r) {
        this.r = r;
    }

    @Override
    public void run() {
        while (true) {
            r.setCount();
        }
    }
}

打印效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Lock锁的实现原理

这个技术不开源,技术的名称叫做轻量级锁;

这种锁效率较高,因为使用的是 CAS 锁技术(Compare And Swap),也称为自旋锁;

图示如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

若此时多出一个自减的 ThreadC,那么会出现一个更严重的问题,即 ABA 问题,也就是说在线程 A 自旋的过程中,线程 B 和线程 C 又自增又自减,线程 A 判断时会认为该值没有被改变,从而停止自旋,进行赋值,这是错误的;

若想解决 ABA 问题,我们需要加入一个只增不减的 version 值,即”版本号“,依此判断该变量是否被修改过;

CAS 锁的存在意义在于提高性能,但是不排除一些特殊的情况,例如该线程因为不明原因运行较其他线程慢,那么 CAS 就失去了存在意义,反而会拖慢进程,所以 JDK 对于此类问题做出了限制,当竞争的线程大于等于 10 ,或者单个线程自旋超过 10 次的时候,JDK 会强制取消 CAS 锁,升级其为重量级锁,即让 OS 锁定 CPU 和内存的总线,synchronized 同步锁就是一种重量级锁,所以它的一些方法能够直接和 OS 交互,效率较低;

下一章:由浅到深认识Java语言(34):多线程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/495779.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

记一次 .NET某防伪验证系统 崩溃分析

一:背景 1. 讲故事 昨晚给训练营里面的一位朋友分析了一个程序崩溃的故障,因为看小伙子昨天在群里问了一天也没搞定,干脆自己亲自上阵吧,抓取的dump也是我极力推荐的用 procdump 注册 AEDebug 的方式,省去了很多沟通…

【Python BUG】CondaHTTPError解决记录

问题描述 CondaHTTPError: HTTP 429 TOO MANY REQUESTS for url https://mirrors.ustc.edu.cn/anaconda/pkgs/free/win-64/current_repodata.json Elapsed: 00:26.513315 解决方案 找到用户路径下的 .condarc文件,建议用这个方法前和我一样做个备份,方…

蛋糕店怎么弄一个微信小程序_开启蛋糕店新篇章

微信小程序,开启蛋糕店新篇章——甜蜜触手可及 在这个数字化、智能化的时代,微信小程序以其便捷、高效的特点,成为了众多商家与消费者之间的桥梁。对于蛋糕店而言,拥有一个专属的微信小程序,不仅可以提升品牌形象&…

家用超声波清洗机高端品牌推荐!4款值得入手的热门超声波清洗机

急着洗眼镜的朋友先不要慌,虽然洗眼镜是日常生活中最常见的操作,但是在清洗眼镜方面也是有讲究的,不是随随便便把眼镜擦一下就算清洁干净了!因为我们拿眼镜布擦眼镜的时候,布料粗糙的微粒就会跟砂纸一样打磨着镜片&…

YOLOv5-小知识记录(一)

0. 写在前面 这篇博文主要是为了记录一下yolov5中的小的记忆点,方便自己查看和理解。 1. 完整过程 (1)Input阶段,图片需要经过数据增强Mosaic,并且初始化一组anchor预设; (2)特征提…

快递鸟物流轨迹地图API接口,包裹行程尽在掌握!

在快节奏的现代生活中,物流行业作为连接生产者与消费者的桥梁,其重要性不言而喻。随着电子商务的飞速发展,人们对物流信息的实时性和准确性要求越来越高。为了满足这一需求,快递鸟物流轨迹地图API应运而生,为广大用户提…

jsp将一个文本输入框改成下拉单选框,选项为字典表配置,通过后端查询

一,业务场景: 一个人员信息管理页面,原来有个最高学历是文本输入框,可以随意填写,现在业务想改成下拉单选框进行规范化,在专科及以下、本科、研究生三个选项中选择; 二,需要解决问…

职场中人如何做好时间管理提高工作效率?高效时间管理软件

在职场中,时间就是金钱,效率就是生命。面对繁杂的工作任务和紧迫的时间限制,做好时间管理显得尤为重要。只有合理规划时间,才能提高工作效率,从而在激烈的职场竞争中脱颖而出。 那么,职场中人如何做好时间…

mysql80-DBA数据库学习1-数据库安装

掌握能力 核心技能 核心技能 mysql部署 官网地址www.mysql.com 或者www.oracle.com https://dev.mysql.com/downloads/repo/yum/ Install the RPM you downloaded for your system, for example: yum install mysql80-community-release-{platform}-{version-number}.noarch…

大唐电信AC管理平台弱口令登录及信息泄露

大唐电信AC简介 大唐电信科技股份有限公司是电信科学技术研究院(大唐电信科技产业集团)控股的的高科技企业,大唐电信已形成集成电路设计、软件与应用、终端设计、移动互联网四大产业板块。 大唐电信AC集中管理平台存在弱口令及敏感信息泄漏漏…

如何在Windows通过eXtplorer结合cpolar搭建个人文件服务器并实现无公网ip远程访问

文章目录 1. 前言2. eXtplorer网站搭建2.1 eXtplorer下载和安装2.2 eXtplorer网页测试2.3 cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1. 前言 通过互联网传输文件,是互联网最重要的应用之一,无论是…

推特Twitter有直播功能吗?如何用Twitter直播?

现在各大直播平台已经成为社交媒体营销的一种重要渠道,它让品牌能够即时地与全球受众进行互动。据统计,直播市场正在迅速增长,预计到2028年将达到2230亿美元的规模。在这个不断扩张的市场中,许多社交媒体平台如YouTube、Facebook、…

【OS探秘】【虚拟化】【软件开发】【网络安全】在Windows11上安装Kali Linux虚拟机

一、所需原料 Windows 11主机、Oracle VM VirtualBox虚拟化平台、Kali Linux镜像文件 二、安装步骤 1、 在VBox管理器中,点击“新建”,进入向导模式,指定各个字段的值: 2、 安装完成,启动虚拟机: 3、 选择…

[linux初阶][vim-gcc-gdb] OneCharter: vim编辑器

一.vim编辑器基础 目录 一.vim编辑器基础 ①.vim的语法 ②vim的三种模式 ③三种模式的基本切换 ④各个模式下的一些操作 二.配置vim环境 ①手动配置(不推荐) ②自动配置(推荐) vim是vi的升级版,包含了更加丰富的功能. ①.vim的语法 vim [文件名] ②vim的三种模式 命令…

慧天[HTWATER]:采用CUDA框架实现耦合模型并行求解

慧天[HTWATER]软件简介 针对城市排水系统基础设施数据管理的需求,以及水文、水力及水质模拟对数据的需求,实现了以数据库方式对相应数据的存储。可以对分流制排水系统及合流制排水系统进行地表水文、管网水力、水质过程的模拟计算。可以对城市低影响开发…

CV论文--2024.3.28

1、Efficient Video Object Segmentation via Modulated Cross-Attention Memory 中文标题:通过调制交叉注意力记忆进行高效视频对象分割 简介:最近,基于Transformer的方法在半监督视频对象分割方面取得了出色的结果。然而,由于这…

【C++】手撕哈希表的闭散列和开散列

> 作者:დ旧言~ > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:手撕哈希表的闭散列和开散列 > 毒鸡汤:谁不是一边受伤,一边学会坚强。 > 专栏选自:C嘎嘎进阶 > 望小伙伴们…

通过在线编程彻底搞懂transformer模型之三:为啥大语言模型都做不好数学题

为什么大语言模型做不好数学题?这个要从大语言模型的原理来讲。 这里是这篇文字的视频讲解,可能视频讲得更清楚一些: 写代码彻底搞懂attention注意力机制 – LLM transformer系列,附:在线编程地址 现代大语言模型都源自于2017年…

Excel 十字交叉聚光灯查询,再也不用担心看串行与列

当Excel表格行列较多时,要想跟条件找到目标数据可以用查找引用函数自动调取,如果又想让找出来的结果突出显示,有什么好办法呢? 先来看一个做好的案例效果,用户选择查询条件后,结果突出显示。 当查询条件变…

第20篇:逻辑门控D锁存器

Q:基本RS锁存器存在不确定状态,本篇我们设计可以消除不确定状态的锁存器--逻辑门控D锁存器。 A:逻辑门控D锁存器逻辑图: 其工作原理:在CLK1期间,数据输入端D的值被传输到输出端Q,而当CLK由1 跳…