3.8多线程

news2024/10/3 10:39:59

案例一-线程安全的单例模式(面试)

是一种设计模式,设计模式针对写代码时的一些常见场景给出一些经典解决方案

单例模式的两种典型实现

  • 饿汉模式

  • 懒汉模式

饿汉的单例模式:比较着急去进行创建实例

懒汉的单例模式,是不太着急创建实例,,只是在用的时候,才真正创建

这个是类对象,也就是.class文件,被JVM加载到内存后,表现出的模样

类对象里就有.class文件中的一切信息

包括:类名是啥,类里有哪些属性,每个属性叫什么.....

有了这些信息才能实现反射

1.饿汉模式

针对唯一实例的初始化,比较着急,类加载阶段,就会直接创建实例

对于getinstance 仅仅是读取了变量的内容, 如果是多个线程只是读同一个变量

不修改,此时任然是线程安全的

class Singleton{ //饿汉模式 //使用static创建唯一实例,并且立即进行实例化.是这个类的唯一实例 private static Singleton instance=new Singleton(); //防止程序员一不小心创建,把构造方法设为private private Singleton(){} //创建一个方法.拿到唯一实例 public static Singleton getInstance(){ return instance; }}

2.懒汉模式

1)初始模板

这个方法的好处就是在真正使用getInstacne的时候才会真的创建实例

这里既包含了读,也包含了修改

读和修改是分成两个步骤的,并不是原子性的

这样就很可能涉及到线程安全的问题

还是根之前的count++同样的原理

先是读取到cpu上,然后实例化,再存到对应的内存上

2)避免线程不安全

加锁,

不能随便加synchronized

这里的读没加锁,写加锁,还是没用.还是没用把读和写放在一起

这里的类对象作为锁对象,类对象在一个程序中只有一份,能保证多个线程调用getinstance的时候都是针对同一个对象加锁

3)锁竞争问题

虽然初始化问题的线程安全搞定了

但是如果初始化后,if的条件就不成立了,getinsance就只剩下读操作,就线程安全 了,但是如果代码就这样,

无论初始化前后每次进入都会加锁,也就存在大量锁竞争

这样速度就变慢了

解决方案

让初始化之前才进行加锁,初始化之后就不加锁了.

就再进行一条条件判定

加锁很有可能导致代码出现阻塞,

第一个if判定的是否要加锁

第二个if判定是否要创建实例

3)内存可见性问题

如果多个线程都去调用这里的getinsance.就会造成大量的读内存操作

很有可能会让编译器把读内存操作优化成读寄存器操作

如果这样的话,就算已经实例化第一层就会误认为是null

就会导致不该加锁的锁给加了

但是不会影响第二层的if->synchroized可以预防内存可见性

解决方法:

给instance加上volatile

保证insance不会被编译器优化

4)总结

①给正确的位置加锁

②双重if判定

③volatile内存可见性问题

案例二--------阻塞队列

阻塞队列符合先进先出规则的队列

0.功能

1.线程安全

2,产生阻塞效果

1)如果队列为空,尝试出队列,就会出现堵塞,阻塞到队列不为空为止

2)如果队列为满,尝试入队列,就会出现阻塞,阻塞到入列不为满为止

基于上述特性,就可以实现生产者-消费者模型

阻塞队列可以作为生产者消费者模型中的交易场所

生产者消费者模型.是服务器开发的场景常用

1.优点

优点1 可以让多个服务器程序之间充分的解耦合

此时A和B的耦合性比较强

开发A代码的时候就得充分了解到B提供的一些接口

开发B代码的时候也得充分了解A是如何调用的

一旦B发生改动,A也要改动

使用生产者消费者模型,就可以降低这里的耦合

对于 请求:A是生产者,B是消费者

对于响应:A是消费者,B是生产者

阻塞队列都是作为交易场所的

A和B都只需要关注如何与队列交互

优点二:能够对请求进行"削峰填谷"

假设请求突然暴涨

A作为入口服务器,计算量轻,问题可能还好

但是B作为应用服务器,计算量大,需要的资源系统也多,如果请求更多了,需要资源进一步增加,如果主机的硬件不够,程序就挂了

A请求暴涨=>阻塞队列的请求暴涨

由于阻塞队列本来计算量小,只是单纯存数据,所以能抗住更大压力

对于B来说,依然按照原来额度速度来消费数据,不会因为A的暴涨二暴涨

削峰:这种峰值不是持续的,过去了就恢复了

填谷:按照原来哦的频率处理之前挤压的数据

3.java标准库的阻塞队列

4.自身实现

1)实现一个循环队列->底层用数组实现->双指针

①.入队列

把新元素放到tail的位置上,并且tail++

②出队列

把head位置上上的元素返回回去,并且head++

③循环

当指针head/tail到达数组末尾的时候,就需要从头开始,重新循环

④判断空还是满

空和满都是head和tail重合

1)浪费一个格子,head==tail认为是空

head==tail+1认为是满

2)额外创建一个变量,size.记录元素额度个数

size==0 空

size==arr.length 满

class MyBlockingQueue{
    //底层用数组实现
    private int[] arr=new int[1000];
    //初始化
    private int size,head,tail;
    //入队列
    //实现一个locker类,拥有wait和notify方法
    private Object locker=new Object();
    public void put(int a) throws InterruptedException {
        synchronized(locker){
            if(size==arr.length){
             //   return;//如果满了就暂时返回
                locker.wait();
            }
            arr[tail]=a;size++;
            tail=(tail+1)%arr.length;//循环队列
            locker.notify();
        }

    }
    //出队列
    public Integer  take() throws InterruptedException {
        synchronized (locker){
            if(size==0){
                //return null;//如果是空的就暂时返回,但是-1明显不高,就把int包装类
                locker.wait();
            }
            int a=arr[head];
            head=(head+1)% arr.length;size--;
            locker.notify();
            return a;
        }
    }
}

注意:

2)让队列线程安全

保证多线程环境下,调用put和take都是线程安全的

但是put和take每一步都是操作公共变量

于是我们直接对整个方法加锁

实现一个locker类,读写就是locker

3)实现阻塞效果

关键要点就是使用wait和notify机制

对于put来说:阻塞条件,就是队列为满

对于take来说:阻塞条件,就是队列为空

加锁以后.如果成功take了.就说明不再空了,就可以再take以后唤醒

5,实现一个简单的生产者-消费者模型

    public static void main(String[] args) {
        MyBlockingQueue queue=new MyBlockingQueue();
        Thread producer=new Thread(()->{
            int num=0;
            while (true){
                try {
                    queue.put(num);
                    System.out.println("生产了"+num);
                    num++;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer=new Thread(()->{
           while(true){
               try {
                   int tmp = queue.take();
                   System.out.println("消费了"+tmp);
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        });
        customer.start();
    }

案例三-定时器

像闹钟一样,进行定时,在一定时间之后,被唤醒并执行某个之前设定好的任务

1.标准库的的定时器

java.util.Timer

核心方法 就一个:schedule 参数有两个:任务是什么.多长时间之后执行

任务就是一段代码

Timer有自己专门的线程,来负责执行注册的任务

2.实现一个定时器

1)描述任务

创建一个专们的类表示定时器的任务(TimerTask)

2)组织任务

使用一定的数据结构把这些任务放在一起

咱们的需求就是,当运行的时候,快速找到所有任务中最小的任务

那很明显就是用堆->用带有阻塞的队列

因为此处的队列需要考虑到线程安全问题,可能存在多个线程里进行注册任务.同时还有一个专门的任务来执行,所以需要考虑

3)执行时间到了的任务

需要执行时间最靠前的任务,就需要有个线程,不停地检查当前优先级队首元素看是不是到了

4)实现比较器

如果要实现自己的类,要接上比较器接口->堆如果是自己的类型,就一定要在自己类里实现自己比较器并接上比较器接口

5)解决忙等问题

如果不加任何限制,这个循环会执行的非常快

如果队列是空的,就会阻塞,

如果没空,而且任务时间没到,就会不停地循环

这就叫忙等

浪费CPU

基于wait来实现

指定等待时间,计算出当前时间和任务的目标之间的时间差,就等待这么长时间即可

到了等待时间就会唤醒

问题:既然指定了一个等待时间,为什么不直接用sleep.而用wait呢

因为sleep不能被中途唤醒

而wait能够被中途唤醒

因为在等待过程中,可能要插入新的任务,新的任务很有可能在所有任务之前的

比如在schedule操作中,就需要假如一个notify操作

一旦有任务进来,就唤醒

class MyTask implements Comparable<MyTask>{
    //1.描述一个任务
    private Runnable runnable;
    //2.描述一个时间
    private long time;
    //3.创建一个任务
    public  MyTask(Runnable runnable,long delay){
        //描述的时间是一个时间jiange,而不是一个确切的时间
            this.runnable=runnable;
            this.time=System.currentTimeMillis()+delay;
    }
    public void run(){
        runnable.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time-o.time);//小根堆
    }
}
public class MyTimer {
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
    private Object locker=new Object();
    public void schedule(Runnable runnable,long delay){
        MyTask task=new MyTask(runnable,delay);
        queue.put(task);
        synchronized (locker){
            locker.notify();
        }
    }
    public MyTimer(){
        //创建这个线程,看是不是时间到了,该执行任务了
        Thread t=new Thread(()->{
            while (true){
                //先取出队首元素
                try {
                    MyTask task=queue.take();
                    long curTime=System.currentTimeMillis();//看一下当前时间
                    if(curTime<task.getTime()){
                        queue.put(task);
                        long dec=task.getTime()-curTime;
                        synchronized (locker){
                            locker.wait(dec);
                        }
                    }else{
                        task.run();//时间到了,执行
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

3.总结

1.描述一个任务 runable+time

2.使用优先级队列来组织若干个任务PriorityBlockingQueue

3.实现schedule来注册任务到队列里

4.创建个扫描线程,让它不停地获取最小元素,并且判定时间是否到达

5.要注意自我创建的类要实现比较器 并且要注意解决这里的忙等问题

四.线程池

1.原因

进程频繁创建销毁,开销比较大,所以我们想到了进程池或者线程

线程,虽然比进程轻了.但是如果创建销毁的频率比较多,开销还是有的,解决方案:线程池或协程

线程池:把线程提前创建好,放在池子里.后面用到线程直接从池子里取,就不需要在系统申请

线程用完了.也不是还给系统.而是放回池子里,以备下次再用

这会创建销毁过程,就快很多

问题?线程放池子里就比系统申请释放来的快?

代码是最上面的应用程序来运行.这里的代码都称为"用户态运行的代码

创建线程,本身需要内核的支持

创建线程的本质就是在内核建立一个PCB(进程控制块)加到链表里

所以这是要进入内核态来运行

而把创建好玩的线程放到池子里,由于池子就是用户态实现的,这个放到池子/从池子里取的过程,不涉及到内核态,纯粹的用户态代码能够完成

一般情况下,纯用户态的操作效率高于经过内核态操作,

2.java标准库线程的使用

构造方法

① int corePoolSize 核心线程数->正式员工的数量

② int maximumPoolSize 最大线程数(正式员工+临时工)

③ long keepAliveTime 允许临时工摸鱼的时间

③TimeUnit unit 时间的单位

⑤ BlockingQueue workQueue 任务队列

线程池会提供一个submit方法,让程序员把任务注册到线程池中,加入到这个任务队列中

⑥ ThreadFactory threadFactory 线程工厂 线程是如何创建的

⑦RejectedExecutor handler 拒绝策略

当任务队列满了,该怎么做

1)直接忽略最新任务,2)阻塞等待,3)直接丢弃最老的任务

虽然线程池的参数很多,但是最重要的参数还是线程的个数

3.面试问题

程序要并发的多线程完成一些任务,如果使用线程池的话,这里的线程池设为多少合适?

正确做法:通过性能测试的方式 找到合适的值

例如,写一个服务器程序,服务器通过线程池,多线程的处理用户请求

就可以对这个服务器进行性能请求,构造一些请求发送给服务器,测试性能,这里的请求就需要构造很多,根据不同的线程池的线程数,来观察程序处理任务的速度和CPU的占用率

CPU占用率是很重要的,比如线上服务器,一定要留有一定的冗余,假如请求突然暴涨,如果CPU都快满了,这个时候服务器就会挂了

4.自用的线程池

Excutors

本质是针对ThreadPoolExecutor进行了封装,提供了一个默认参数

要知道线程池里面有什么

1)能够描述任务(用Runnable)

2)需要一个数据结构组织任务(直接使用BlockingQueue

3)能够描述工作线程

4)要组织线程

5)要能实现往线程池里添加任务

class MyThreadPool {
    // 1. 描述一个任务. 直接使用 Runnable, 不需要额外创建类了.
    // 2. 使用一个数据结构来组织若干个任务.
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 3. 描述一个线程, 工作线程的功能就是从任务队列中取任务并执行.
    static class Worker extends Thread {
        // 当前线程池中有若干个 Worker 线程~~ 这些 线程内部 都持有了上述的任务队列.
        private BlockingQueue<Runnable> queue = null;

        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            // 就需要能够拿到上面的队列!!
            while (true) {
                try {
                    // 循环的去获取任务队列中的任务.
                    // 这里如果队列为空, 就直接阻塞. 如果队列非空, 就获取到里面的内容~~
                    Runnable runnable = queue.take();
                    // 获取到之后, 就执行任务.
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    // 4. 创建一个数据结构来组织若干个线程.
    private List<Thread> workers = new ArrayList<>();

    public MyThreadPool(int n) {
        // 在构造方法中, 创建出若干个线程, 放到上述的数组中.
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    // 5. 创建一个方法, 能够允许程序猿来放任务到线程池中.
    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++修炼之路】26.C++11(语法糖)

每一个不曾起舞的日子都是对生命的辜负 C11C11(语法糖)本节目标一.C11简介二.统一的列表初始化2.1 {}初始化2.2 std::initializer_list三.声明3.1 auto3.2 decltype3.3 nullptr四.总结C11(语法糖) 本节目标 C11简介 列表初始化 变量类型推导 一.C11简介 在2003年C标准委员…

信息收集-

url&#xff1a; https://en.wikipedia.org:443/wiki/hypertext_Transfer_Protocol?id123#HTTP/1.1_response_messages https&#xff1a;协议 en.wikipedia.org&#xff1a;域名 443&#xff1a;端口 wiki/hypertext_Transfer_Protocol&#xff1a;文件路径 id123&…

Unity 混合操作(Blending)

渲染图形时&#xff0c;在执行所有着色器并应用所有纹理后&#xff0c;像素将写入到屏幕。这些像素与已有像素的组合方式由 Blend 命令控制。用于生成透明对象。《Unity Shader入门精要》大致解释&#xff1a;片元通过了模板测试和深度测试之后&#xff0c;会进行混合步骤。如果…

三、SpringMVC的拦截器

1. SpringMVC的拦截器 针对请求和响应进行的额外的处理.在请求和响应的过程中添加预处理,后处理和最终处理. 2. 拦截器的应用场景 1、日志记录&#xff1a;记录请求信息的日志 2、权限检查&#xff0c;如登录检查 3、性能检测&#xff1a;检测方法的执行时间 3. 拦截器执行的…

调用一个函数时发生了什么?

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; 前言&#xff1a; 用C语言写代码&#xff0c;如果一个工程相对复杂时&#xff0c;我们往往会采取封装函数的方式。在主函数中调用函数 这一看似简单的过程&#xff0c;实际上有很多不宜观察的细节&#xff0…

计算机组成原理|第三章(笔记)

目录第三章 系统总线3.1 总线的基本概念3.2 总线的分类3.2.1 片内总线3.2.2 系统总线3.2.3 通信总线3.3 总线特性及性能指标3.3.1 总线的特性3.3.2 总线性能指标3.4 总线结构3.4.1 单总线结构3.4.2 多总线结构3.4.3 总线结构举例3.5 总线控制3.5.1 总线判优控制3.5.2 总线通信控…

ALG和STUN

目录 ALG 应用层网关讲解 Client1使用FTP主动模式建立FTP Client1使用FTP被动模式建立FTP STUN讲解 ALG 应用层网关讲解 用来替换应用层信息 Client1使用FTP主动模式建立FTP 主动模式&#xff1a;服务器收到客户端发来的请求FTP的地址和端口 服务器使用20端口直接向客户端建…

webpack dll 提升构建速度

DLL&#xff0c;动态链接库&#xff08;Dynamic Link Library 或者 Dynamic-link Library&#xff09;&#xff0c;由微软公司提出。目的是为了节约应用程序所需的磁盘和内存空间。 在一个传统的非共享库中&#xff0c;如果两个程序调用同一个子程序&#xff0c;就会出现两份那…

Redis-6集群

文章目录前言Redis集群原理搭建Redis集群集群拓展后记前言 前两期介绍和搭建了Redis的主从复制架构和哨兵模式&#xff0c;虽然哨兵模式能够实现自动故障转移主备切换&#xff0c;一定程度上提高了系统的容错性 但这两种架构模式都不能解决单节点的并发压力和物理上线的问题&…

行测-判断推理-图形推理-样式规律-空间重构-立体拼合

有凸必有凹&#xff0c;排除A CD显然不对选BA和4能组成长方体B和3能组成长方体C和1能组成长方体选D这两个东西应该在同一侧&#xff0c;排除A C中间应该要由凸起&#xff0c;排除D选B图1向左旋转90图2逆时针旋转90选A

内网环境解决SSL证书问题

本来这个没什么好写的&#xff0c;但是坑实在有点多&#xff0c;不得不写个文章记录下来。 创建证书看这里&#xff01;&#xff01;&#xff01; 很多知识点要结合这个页面内容来看。 创建证书已经看过相关文章&#xff0c;然后用unity跑的时候发现连不上&#xff0c;完全没…

【Go】基于VS Code配置Go语言环境

教程 1. 安装go环境 打开官网&#xff1a;https://go.dev/dl/&#xff0c;找到对应系统的环境文件。 安装完成后软件会自动把路径添加到环境变量&#xff0c;可以在命令行中检验一下 2. 在VS Code中安装必要的插件 这一步我理解的就是提供语法高亮和代码提示 3. 打开一个路…

基于智能边缘和云计算的数字经济服务细粒度任务调度机制

数字经济被各国视为推动经济增长的必然选择&#xff0c;为经济高质量发展提供了新机遇、新路径。对于中国市场而言&#xff0c;云计算背后的强大基础是数字经济不可阻挡的发展趋势。在数字经济中&#xff0c;云作为基础设施成为构建数字经济金字塔的基础。为缓解数字经济服务器…

Windows下实现Linux内核的Python开发(WSL2+Conda+Pycharm)

许多软件可以通过Python交互&#xff0c;但没有开发Windows版本&#xff0c;这个时候装双系统或虚拟机都很不方便&#xff0c;可以采取WSL2CondaPycharm的策略来进行基于Linux内核的Python开发。启动WSL2&#xff0c;安装Linux内核教程&#xff1a;旧版 WSL 的手动安装步骤 | M…

交并比(IOU)的计算方法

交并比&#xff08;IOU, Intersetion Over Union&#xff09;,意思就是交集和并集的比值&#xff0c;用来评价两个几何图形的面积重合度&#xff0c;在目标检测算法模型中&#xff0c;通常被用来计算预测框与真实框的误差&#xff08;损失函数&#xff09;或者在非极大值抑制&a…

便捷式储能电源核心技术--单相逆变器设计

便捷式储能电源核心技术–单相逆变器设计 1.逆变器的规格参数 输入电压直流400V输出电压交流rms220V开关频率10kHz滤波电容6.23uF控制方式单极性倍频2.视频学习链接 视频学习链接 3.主电路仿真设计

日志服务搭建-ES-FileBeat-Kibana

1次订单量突增问题&#xff0c;导致了有几个数据没有录库&#xff0c;但是确有支付的记录&#xff0c;啥玩意&#xff0c;还能有这个操作&#xff0c;组内安排问题定位&#xff0c;解决&#xff0c;一看打出来的日志&#xff0c;只有支付有&#xff0c;生成订单这边没有&#x…

读书笔记-终身学习

前言人需要终身成长&#xff0c;也需要终身学习&#xff0c;以下是记录个人读书学习的笔记总结&#xff0c;希望能给大家一点借鉴&#xff0c;仅供参考。笔记1、《匠人精神》秋山利辉是日本神奈川县横滨市都筑区“秋山木工”的经营者&#xff0c;从事订制家具制作业务。是一家小…

Pinia不酸,保甜

为什么是Pinia 怎么说呢&#xff0c;其实在过往的大部分项目里面&#xff0c;我并没有引入过状态管理相关的库来维护状态。因为大部分的业务项目相对来说比较独立&#xff0c;哪怕自身功能复杂的时候&#xff0c;可能也仅仅是通过技术栈自身的提供的状态管理能力来处理业务场景…

使用Git Hook技术定义和校验代码提交模板

1.背景 使用Git做项目的版本控制时&#xff0c;在版本系统中会有很多的代码的提交记录&#xff0c;我们使用git log命令就会得到如下图中的提交记录&#xff1a; 当我们的项目比较简单&#xff0c;规模较小、开发人员也只有一两个的时候&#xff0c;其实可以不用定义代码的提…