多线程问题(三)

news2024/12/25 12:36:13

目录

一、线程安全的单例模式

1、饿汉模式 

2、懒汉模式 

二、阻塞队列 

三、定时器 

1、标准库中定时器的使用用法

2、模拟实现定时器 

a、首先需要创建出一个专门的类来表示schedule中的任务(TimerTask)

b、使用合适的数据结构组织任务 

c、实现schedule方法注册到队列中去 

d、执行到达时间的任务 

 四、线程池


一、线程安全的单例模式

单例模式:是一种设计模式,使用单例模式能保证一个类在程序中只存在唯一的一个实例

单例模式可以分为饿汉式和懒汉式。实现单例模式的类的构造方法一般为私有的,防止在类外创建出多个实例。

1、饿汉模式 

饿汉模式就是在类加载的时候就创建了实例。不管用不用得到实例,实例都会首先创建好,所以饿汉模式是线程安全的

饿汉模式的代码实现:

public class Singleton1 {
    /**
     * 模拟实现饿汉模式
     */
    private static Singleton1 instance=new Singleton1();
    private Singleton1(){
        
    }

    public static Singleton1 getInstance() {
        return instance;
    }
}

2、懒汉模式 

懒汉模式相较于饿汉模式比较优化,不会在开始就创建出实例而是在用的时候才进行创建

代码实现: 

public class Singleton2 {
    /**
     * 懒汉模式的模拟实现
     */
    private static Singleton2 instance;
    private Singleton2(){
        
    }
    public Singleton2 getInstance(){
        if(instance==null){
            instance=new Singleton2();
        }
        return instance;
    }
}

在获取到实例时候进行的操作先进行读然后再进行创建,这个操作不是原子性的,就在多线程抢占式执行的过程中存在线程安全问题,于是就对该操作进行加锁,锁对象采用该类的类对象,因为类对象只有一份这样可以保证多个线程在调用getInstance方法时使用的锁对象都是同一个:

public Singleton2 getInstance(){
        synchronized(Singleton2.class){
            if(instance==null){
                instance=new Singleton2();
            }
        }
        return instance;
    }

这样修改之后,在每次获取实例的时候都需要进行加锁,即使实例已经创建好了,这样就有可能引起锁的竞争十分影响效率,于是在加锁之前就先进行if判断实例是否已经创建好,避免盲目加锁:

public Singleton2 getInstance(){
        if(instance==null){
            synchronized(Singleton2.class){
                if(instance==null){
                    instance=new Singleton2();
                }
            }
        }
        return instance;
    }

但是这也会引起内存可见性问题,多个线程对instance进行读操作,就直接在寄存器读取当内存发生变化时最新信息就无法得到,出现多加锁问题所以就需要在定义类实例时加上volatile关键字进行修饰:

private  static  volatile Singleton2 instance;

所以,最终多线程环境下的懒汉模式为:

public class Singleton2 {
    /**
     * 懒汉模式的模拟实现
     */
    private  static  volatile Singleton2 instance;
    private Singleton2(){

    }
    public Singleton2 getInstance(){
        if(instance==null){
            synchronized(Singleton2.class){
                if(instance==null){
                    instance=new Singleton2();
                }
            }
        }
        return instance;
    }
}

二、阻塞队列 

阻塞队列是一种特殊的队列,但同样遵守先进先出的规则 。

阻塞队列的特性:

队列为空时,继续出队列就会阻塞,直到有线程向该队列中添加元素。

队列满时,继续入队列时就会阻塞,直到有线程从该队列中取走元素。

Java的标准库也存在阻塞队列:BlockingQueue,但它只是一个接口,真正实现类是LinkedQueue,其常用方法有put()(入队列)、take()(出队列),还有offer()、pull()、peek()等方法,但这些方法并不带阻塞性。

生产者——消费者模型 

生产者负责生产资源,而消费者负责消费资源,如果没有阻塞队列则消费者和消费者之间有着较高的耦合性,两者都需要为彼此提供一些接口方法,使用了阻塞队列之后可以让两者充分地解耦合。

还有如果不使用阻塞队列,消费者需求暴涨就会出现生产者所需的资源突然增多,如果硬件条件不足,系统就会崩溃,但是使用了阻塞队列之后,需求增多只会影响阻塞队列的数据增多而对于生产者并没有太大的影响,如果消费者需求突减,也只是会让阻塞队列的数据减少所以阻塞队列还可以对请求实现“削峰填谷”。

利用数组模拟实现阻塞队列:

public class MyBlockingQueue<T> {
    private T[] data= (T[]) new Object[1000];
    private int size;
    private int tail;
    private int head;
    //添加元素
    public void put(T t) throws InterruptedException {
        synchronized (this){
            if(size==data.length){
                //队列已满,继续添加元素就会阻塞
                this.wait();
            }
            data[tail]=t;
            tail++;
            if(tail>= data.length){
                tail=0;
            }
            size++;
            //队列添加元素成功,就会唤醒因队列为空造成阻塞的线程
            this.notify();
        }

    }
    //取出元素
    public T take() throws InterruptedException {
        synchronized (this){
            if(size==0){
                //队列为空,继续取出元素就出阻塞
                this.wait();
            }
            T t=data[head];
            head++;
            if(head>=data.length){
                head=0;
            }
            size--;
            //队列取出元素成功,就会唤醒因队列已满而阻塞的线程
            this.notify();
            return t;
        }
    }


}

模拟实现生产者——消费者模型:

public static void main(String[] args) {
        MyBlockingQueue<Integer> mq=new MyBlockingQueue<>();
        Thread producer=new Thread(()->{
            try {
                int num=1;
                while(true){
                    mq.put(num);
                    System.out.println("生产第"+num+"个");
                    num++;
                   Thread.sleep(500);
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        producer.start();
        Thread consumer=new Thread(()->{
            try {
                int num=0;
                while(true){
                    num=mq.take();
                    System.out.println("消费第"+num+"个");
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        consumer.start();
    }

运行结果:

由于两者设置的 休眠时间一致,所以生产者生产出一个资源就会立即被消费者消费。

三、定时器 

定时器就相当于一个闹钟,到达设定的时间之后,就会执行某段设定好的代码。

定时器是一个常用的组件,就比如在加载网页的时候,一般当加载一段时间后未加载出来,就会显示检查网络设置。

1、标准库中定时器的使用用法

在标准库java.util.Timer中,使用Timer类定义一个实例对象,然后再使用其核心方法是schedule,此方法的两个参数分别是执行的任务是什么,还有在多久之后执行。

使用演示:

public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("测试标准库中的定时器方法");
            }
        },3500);
        System.out.println("main方法");
    }

运行结果:  

先执行main方法,在3500ms之后执行TimerTask中的任务。 

2、模拟实现定时器 

a、首先需要创建出一个专门的类来表示schedule中的任务(TimerTask)

 在该类中需要定义一个Runnable接口来定义任务,再定义一个时间来表示任务在多久之后执行,还需要定义一个run方法用于描述任务。在定义构造方法时应该注意参数中的时间是延迟时间而不是执行任务的时间,需要用系统当前的时间加上延迟时间。

class MyTask{
    private Runnable runnable;
    private long time;
    public MyTask(Runnable runnable,long delay){
        this.runnable=runnable;
        this.time=System.currentTimeMillis()+delay;
    }
    public long getTime(){
        return time;
    }
    public void run(){
        runnable.run();
    }
}

b、使用合适的数据结构组织任务 

 在定时器中需要不断比较多个任务的时间,来决定任务的执行顺序,可以使用PriorityQueue优先级队列来实现,但是在不断地取任务的过程中涉及线程安全问题,就采用PriorityBlockingQueue来实现。

 public PriorityBlockingQueue<MyTask> pbq=new PriorityBlockingQueue<>();

c、实现schedule方法注册到队列中去 

public void schedule(Runnable runnable,long delay){
        MyTask mytask=new MyTask(runnable, delay);
        pbq.put(mytask);
    }

d、执行到达时间的任务 

 在Mytimer类的构造方法创建出一个线程用于从队列中取出一个任务,如果任务时间小于当前时间就将任务重新加入到队列中去,否则就执行该任务。

public MyTimer() {
        Thread t=new Thread(()->{
            while(true){
                try {
                    MyTask task=pbq.take();
                    long currentTime= System.currentTimeMillis();
                    if(currentTime< task.getTime()){
                        pbq.put(task);
                    }else{
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }

但是这样模拟实现后,在main方法中测试是运行结果显示有问题:

显示需要在MyTask类中实现Comparable接口,因为是优先级队列需要进行排序就要实现Comparable接口的comparaTo()方法。

@Override
    public int compareTo(Object o) {
        MyTask task=(MyTask) o;
        return (int)(this.getTime()-((MyTask) o).getTime());
    }

但是此时的代码还存在一个问题:就是队列中的任务的执行时间还没到,执行线程就会一直进行时间判断,处于忙等的状态,于是就可以利用wait和notify,wait等待任务执行时间减去当前时间,当在队列中加入任务时,就需要进行唤醒,需要查看新加入的任务是否需要执行。这样就可以使执行变得更加有效率。

public class MyTimer{
    public PriorityBlockingQueue<MyTask> pbq=new PriorityBlockingQueue<>();
    Object locker=new Object();
    public void schedule(Runnable runnable,long delay){
        MyTask mytask=new MyTask(runnable, delay);
        pbq.put(mytask);
        synchronized (locker){
            locker.notify();
        }

    }
    public MyTimer() {
        Thread t=new Thread(()->{
            while(true){
                try {
                    MyTask task=pbq.take();
                    long currentTime= System.currentTimeMillis();
                    if(currentTime< task.getTime()){
                        pbq.put(task);
                        synchronized(locker){
                            locker.wait(task.getTime()-System.currentTimeMillis());
                        }
                    }else{
                        task.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

 四、线程池

每次创建和销毁线程时,都需要在内核态支持运行,这样的效率十分低,所以就在反复创建线程时就使用线程池直接在用户态运行,这样就可以极大地减少每次创建线程和销毁线程的损耗。

标准库中的线程池叫做ThreadPoolExecutor,在java.util.concurrent包中,也有简化版本的线程池Executor。

使用标准库中的Executors类的newFixedThreadPool(int n)创建出n个线程的线程池,然后再使用该对象.submit()注册任务到线程池中。

public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        for(int i=0;i<1000;i++){
            int a=i+1;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务:"+a);
                }
            });
        }

    }

判定线程池中恰当的线程数目:

需要进行性能测试。

例如写一个服务器程序,服务器程序里面通过线程池使用多线程来处理用户请求,此时就可以对服务器进行性能测试:构造出一些请求发送给服务器,利用服务器不同的线程数对比程序处理的速度和程序持有的CPU占用率来找到一个合适的平衡点得到合适的线程池中的数目。 

创建线程池的几种方式:

  • newFixedThreadPool:创建固定线程数的线程池;
  • newCachedThreadPool:创建线程数目动态增长的线程池;
  • newSingleThreadExecutor:创建只包含单个线程的线程池;
  • newScheduledThreadPool:创建延迟时间后执行命令或者定期执行命令,是进阶版的定时器。

模拟实现线程池: 

public class MyThreadPool {
    //1.用Runnable来实现描述一个任务
    //2.使用阻塞队列来存放任务
    private LinkedBlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();
    //3.描述一个工作线程就是从队列中取任务并执行
    static class Worker extends Thread{
        private LinkedBlockingDeque<Runnable> queue=null;
        public Worker(LinkedBlockingDeque<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> list=new ArrayList<>();

        public MyThreadPool(int n){
            for(int i=0;i<n;i++){
                Worker worker=new Worker(queue);
                worker.start();
                list.add(worker);
            }
            
    }
    //5.实现添加任务方法
    public void submit(Runnable runnable) throws InterruptedException {
            queue.put(runnable);
    }
}

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

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

相关文章

Servlet基础教程 (保姆级教学)

Servlet基础教程一、Servlet 是什么二、第一个 Servlet 程序2.1 创建项目2.2 引入依赖2.3 创建目录2.4 编写代码2.5 打包程序2.6 部署程序2.7 验证程序三、更方便的部署方式3.1 安装 Smart Tomcat 插件3.2 配置 Smart Tomcat 插件四、常见的访问出错4.1 出现 4044.2 出现 4054.…

【jrebel and xrebel问题记录】激活时出现LS client not configued

教程目录问题描述所使用的环境和版本解决过程手动下载jrebel结束语问题描述 笔者在重装另一台电脑的时候又遇到了这个安装jrebel and xrebel进行激活的问题 但是我在网上找了很多的办法&#xff08;其实都是相同的办法&#xff0c;只是在尝试别人不同的用于激活的服务器&#…

【Java编程进阶】方法初识

推荐学习专栏&#xff1a;Java 编程进阶之路【从入门到精通】 文章目录1. Java 方法初识2. 方法的创建与使用3. 方法的分类3.1 无参无返回值3.2 无参带返回值3.3 有参无返回值3.4 有参带返回值4. 递归方法5. 总结1. Java 方法初识 方法是组合在一起来执行操作语句的集合&#…

k8s收集日志

k8s收集日志 一.收集控制台日志 采用fluentdeskibana来做 所需要的文件可以在这里找 https://github.com/kubernetes/kubernetes/tree/v1.23.0/cluster/addons/fluentd-elasticsearch1.创建目录并下载所需文件 cd /root/k8s/yaml/efk [rootworker1 efk]# ll total 44 -rw-…

绝缘子红外图像检测项目(TF2)

目录 1. 项目背景 2. 图像数据集介绍 labelimg的安装流程&#xff1a; 1. 打开Anaconda Prompt&#xff08;Anaconda3&#xff09; 2. 创建一个新环境来安装labelimg 3. 激活新创建的环境labelimg 4.输入 5.输入labelimg 即可运行 3. 模型介绍 4. 模型性能测试 1. 项目…

Linux学习笔记——Linux实用操作(二)

04、Linux实用操作 4.6、IP地址、主机名 4.6.1、IP地址、主机名 学习目标&#xff1a; 掌握什么是IP地址掌握什么是主机名掌握什么是域名解析 4.6.1.1、IP地址 1、每一台联网的电脑都会有一个地址&#xff0c;用于和其它计算机进行通讯。 IP地址主要有2个版本&#xff0…

2023上半年软考高级-信息系统项目管理师【名师授课班】

信息系统项目管理师是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目之一&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资…

2022年圣诞节 | matlab实现炫酷的圣诞树

*2022年圣诞节到来啦&#xff0c;很高兴这次我们又能一起度过~ 这里的部分代码已经在网上出现过&#xff0c;做了部分优化。是matlab版本。 一、内容介绍 这段代码是一个生成3D圣诞树的Matlab函数。运行该函数时&#xff0c;它使用圆柱函数创建圣诞树的 3D 表面&#xff0c;对…

【一】微服务技术栈导学

微服务技术栈导学什么是微服务&#xff1f;微服务技术栈注册中心配置中心服务网关分布式缓存分布式搜索消息队列分布式日志服务&系统监控和链路追踪自动化部署微服务技术栈包含知识点学习路线知识内容来自于黑马程序员视频教学和百度百科。博主仅作笔记整理便于回顾学习。如…

Android设计模式详解之适配器模式

前言 适配器模式在Android开发中使用率很高&#xff0c;如ListView、RecyclerView&#xff1b; 定义&#xff1a;适配器模式把一个类的接口变换成客户端所期待的另一个接口&#xff0c;从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作&#xff1b; 使用场景&…

2023年加密行业会更难吗?欧科云链研究院“七大趋势预测”

回望2022&#xff0c;加密行业遭遇了种种不可控因素而导致的艰难险阻&#xff0c;也在变革与发展中孕育着生机与活力。 这一年&#xff0c;我们亲眼目睹了Luna暴雷&#xff0c;三箭资本、FTX这些曾经被认为“大而不倒”的机构接连倒下&#xff0c;市场信心严重受挫&#xff1b;…

登陆港股市场,阳光保险的 “价值锚点”

不确定性环境里&#xff0c;信心比黄金还重要。 最近&#xff0c;利好信号频频出现在保险行业&#xff0c;资本信心不断加固。上个月月底&#xff0c;个人养老金制度启动实施&#xff0c;市场迅速传来喝彩声。这不仅将加快推动养老保险作为第三支柱的壮大&#xff0c;而且还为…

ARM体系架构中的存储系统

在计算机系统当中&#xff0c;数据的存储是以字节为单位的&#xff0c;每个地址单元当中都可以存放一个字节的数据&#xff0c;每个字节为8bit。在C语言中编译器为char型的数据分配了一个字节的存储空间&#xff0c;为long型的数据分配了4个字节的存储空间&#xff0c;为int型的…

【NI Multisim 14.0编辑环境——工具栏】

目录 序言 一、工具栏 &#x1f34a;1.“标准”工具栏 &#x1f34a; 2.视图工具栏 &#x1f34a;3.“主”工具栏 &#x1f34a;4.“元器件”工具栏 &#x1f34a;5.“Simulation”&#xff08;仿真&#xff09;工具栏 &#x1f34a;6.“Place probe”&#xff08;放置探针…

ARM64内存虚拟化分析(2)常用结构体

内存虚拟化相关的几个重要结构体如下图所示&#xff1a; 这里介绍几个结构体以及相互之间有关系。 &#xff08;1&#xff09;AddressSpace结构体 它用于表示一个虚拟机或虚拟CPU能够访问的所有物理地址。其中&#xff1a; root&#xff1a;指向根MR Current_map&#xff1…

营销在中国

&#xff08;1&#xff09;4P、4C、4R、4I作为一个企业&#xff0c;不外乎就是两个是&#xff1a;产-销。你生产-客户购买&#xff0c;这个交易能做成&#xff0c;不外乎在于交换的价值&#xff0c;以及交易的价格-成本。一、4P4P&#xff0c;是美国密歇根大学教授杰罗姆麦卡锡…

向量的点乘与X乘以及意义

一、向量的点乘 向量的点乘&#xff08;dot&#xff09;是一个标量积&#xff0c;也叫向量的内积、数量积。 点乘公式&#xff1a; 有向量a b a(a1,a2,a3,...,an) b(b1,b2,b3,...,bn); 那么向量a(dot)ba1b1a2b2a3b3....anbn 从上面我们能可以看出&#xff0c;点乘得到的结…

2022年个人融资方法和工具研究报告

第一章 理论和概况 1.1 融资概念 融资&#xff0c;英文为Financing&#xff0c;指为支付超过现金或转账的购货款而采取的货币交易手段&#xff0c;或者为取得特定资产而筹集资金所采取的货币手段。融资通常指货币资金的持有者和需求者之间&#xff0c;直接或间接地进行资金融…

Appium基础 — 模拟手势点击坐标

1、模拟手势点击坐标 在定位元素的时候&#xff0c;你使出了十八班武艺还是定位不到&#xff0c;怎么办呢&#xff1f;&#xff08;面试经常会问&#xff09; 那就拿出绝招&#xff1a;点击元素所在位置的坐标。&#xff08;坐标定位&#xff09; 详细说明&#xff1a; 如下…

OpenWrt无法打开国内网站的解决方法

最近小半个月以来根据lean和lieno源码编译的OpenWrt固件在使用了smartdns之后会出现无法打开国内网页的情况。 诡异的是&#xff0c; 重启防火墙之后&#xff0c; 能正常访问百度等国内网站&#xff0c; 但是&#xff0c; 过上一段时间&#xff0c; 就又不行了。 在经过近一个…