Java中的多线程(下)

news2024/11/16 7:35:48

作者:~小明学编程 

文章专栏:JavaEE

格言:热爱编程的,终将被编程所厚爱。
在这里插入图片描述

目录

多线程案例

单例模式

饿汉模式

懒汉模式

阻塞式队列

为什么要引入阻塞队列

Java中的阻塞队列

模拟实现阻塞队列

定时器

标准库中的定时器

模拟实现定时器

MyTask类

MyTimer

线程池

为什么需要线程池

标准库中的线程池

模拟实现线程池


多线程案例

单例模式

单例模式下会保证我们的实例只有一份,单例模式分为两种一种是饿汉模式,一种是懒汉模式,下面我们对两种模式分别的进行介绍。

饿汉模式

//实现单例模式-饿汉模式
class Singleton{
    //在类里面就创建一个实例,该实例是唯一的。
    private static Singleton instance = new Singleton();
    //我们将构造方法设为私有这样就保证我们无法再创建实例了
    private Singleton() {}
    //提供公开的一个方法使得我们获取到这个唯一的实例
    public static Singleton getInstance() {
        return instance;
    }
}

前面说到要保证只有一份实例所以我们的构造方法必须是私有的这样才能保证不会再new新的对象,然后我们还得在类里面创建一个静态的实例,因为是静态的所以能保证其唯一性,最后再通过一个静态的方法来获取我们那个唯一的实例。

所谓的饿汉模式就是很着急,我们刚开始无论你用不用到都会创建一个实例,这样就会有一个好处,那就是我们不用担心线程的安全,因为我们只需要读不用更改,我们这个类加载完之后实例就创建好了。

懒汉模式

class Singleton1 {
    private static Singleton1 instance = null;
    private Singleton1(){}

    public static Singleton1 getInstance() {
        if (instance==null) {
            instance = new Singleton1();
        }
        return instance;
    }
}

懒汉模式就是我们在刚开始的时候不去创建实例,等我们第一次用的时候我们再去创建实例,好处就是节省了资源的开销,但是这样写会有问题,当我们多个线程同时去竞争第一次创建的时候就会产生线程安全问题,会同时的创建多个实例,所以我们就得加锁。

class Singleton1 {
    private static volatile Singleton1 instance = null;
    private Singleton1(){}
    public static Singleton1 getInstance() {
        if (instance==null) {
            synchronized (Singleton1.class) {
                if (instance==null) {
                    instance = new Singleton1();
                }
            }
        }
        return instance;
    }
}

首先我们先说说下面这两个if()判断,第一个判断是判断我们当前是不是要加锁如果我们已经有实例对象了就不用再去加锁浪费资源了,如果我们判断确实是第一次使用,这个时候可能会有很多个线程都通过了这个if()语句,然后大家开始开始竞争这把锁,竞争到了之后再次判断一下我们是不是第一次使用,如果是第一次的话我们要new()这个实例对象,如果我们没有这个if()语句那就是多个线程轮流来new()这个实例了。

此外我们要对instance的实例加上一个volatile的关键词来防止指令的重排序,防止编译器的优化导致其它的线程可能读到一个非空的instance。

阻塞式队列

阻塞式队列和普通的队列一样都是具有先进先出的特点不同的是阻塞队列是属于线程安全的,当我们的队列为空的时候我们还想要出的时候就会进入阻塞状态直到队列里面有元素才能出,当我们的队列满的时候我们还想要入队列的时候此时也会进入阻塞的状态,直到我们出了一个元素才会结束阻塞的状态。

为什么要引入阻塞队列

要想知道为什么我们要引入阻塞队列我们首先就要了解生产者与消费者的模式,所谓的生产者与消费者的模型就是一个人负责生产,一个人负责消费,如果其中的一个人生产的过多了,我们的消费者的压力就会很大,因为都要进行消费,消费量一下子就大了起来。

这样我们的生产者就影响到了我们的消费者,这就不遵循我们代码的规范我们要尽可能的保证高内聚,低耦合,显然刚刚耦合过高了,这个时候我们就又想了一个办法那就是引用一个队列,我们把生产者所有生产的东西都放到这个队列里面去,然后消费者稳定消费就行了,这样各干各的互不影响。

Java中的阻塞队列

BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性

    public static void main(String[] args) throws InterruptedException {
        BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
        blockingDeque.put(5);
        System.out.println(blockingDeque.take());//5

    }

我们通常采用put()和take()方法,其它的方法不具有阻塞性质。

模拟实现阻塞队列

class MyBlockingDeque {
    int[] queue = new int[100];
    int head = 0;//队列头
    int tail = 0;//队列尾
    int size = 0;//已经存在的元素个数
    private Object object = new Object();
    public void put(int val) throws InterruptedException {
        synchronized (object) {
            if (size==queue.length) {//如果队列已经满了,那就阻塞
                object.wait();
            }
            queue[tail] = val;
            tail++;
            if (tail>= queue.length) {
                tail = 0;
            }
            size++;
            object.notify();//唤醒阻塞
        }

    }
    public int take() throws InterruptedException {
        synchronized (object) {
            if (size==0) {//想要拿而且还为空那就阻塞等待里面有元素
                object.wait();
            }
            int ret = queue[head];
            head++;
            if (head>= queue.length) {
                head = 0;
            }
            size--;
            object.notify();//唤醒阻塞
            return ret;
        }
    }

}
public class Demo9 {
    private static MyBlockingDeque myBlockingDeque = new MyBlockingDeque();

    public static void main1(String[] args) throws InterruptedException {
        myBlockingDeque.put(1);
        myBlockingDeque.put(2);
        System.out.println(myBlockingDeque.take());
    }
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                int num = 0;
                while (true) {
                    System.out.println("入队"+num);
                    num++;
                    try {
                        myBlockingDeque.put(num);
//                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread.start();
        Thread thread1 = new Thread(()->{
            while (true) {
                try {
                    Thread.sleep(500);
                    System.out.println("出队"+myBlockingDeque.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
    }

}

这里我们对队列进行了测试,发现当我们阻塞的时候就会停下来然后等待唤醒重新继续运行程序。

定时器

所谓的定时器就像我们的闹钟一样,到了特定的时间就去执行某项工作,我们Java中也提供了类似的功能,也就是到达指定的时间再去执行。

标准库中的定时器

1.标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule。

2.schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后
执行 (单位为毫秒)。

    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("3000");
            }
        },3000);
        System.out.println("main");

    }

这样看上去我们还是不够了解其具体的情况,然后我们试着来实现这样一个类,方便我们更加深刻的了解其原理。

模拟实现定时器

MyTask类

Mytask类的功能主要就是描述我们的任务单位,我们每个任务包含了哪些属性,比如说最重要的两点我们的任务要有我们执行的代码,还要有我们的时间(啥时候执行),这都是中古要的信息,现在我们要把他们都放在一起然后统一描述。

class MyTask implements Comparable<MyTask>{
    private Runnable runnable;//定义一个接口
    private long time;//记录时间
    //创建任务的时候就传入相应的接口和时间
    public MyTask(Runnable runnable, long delay) {
        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);
    }
}

首先就是定义两个变量,runnable用于描述我们要执行的代码,接着就是time告诉我们啥时候要去执行这些代码。

接着是我们的构造方法,构造方法中要传入任务和时间,其中时间要加上我们当前的时间戳。

最后看到我们还要实现我们的comparable接口然后重写compareTo这个方法,因为我们的任务不止一个后面将会做比较哪个任务最近。

MyTimer

class MyTimer {
    //用优先级队列存放我们的多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long relay) {
        MyTask myTask = new MyTask(runnable,relay);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }
    private Object locker = new Object();
    public MyTimer() {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                      try {
                        MyTask task = queue.take();
                        long time = System.currentTimeMillis();//获取当前的时间
                        //如果当前的时间不到任务的时间
                        if (time<task.getTime()) {
                            queue.put(task);//放回去
                            synchronized (locker) {
                                locker.wait(task.getTime()-time);//控制等待时间避免cpu空转
                            }
                        } else {
                            task.run();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        };
        thread.start();
    }

}

首先我们要用一个容器来存放我们的多个任务,在多个数据结构的对比之下我们的优先级队列是最适合的,因为我们首先要找到我们的最小值,也就是距离我们执行任务最近的那个单位,堆刚好能满足并且调整的时间复杂度还很低。

先说说我们的schedule这个方法,该方法主要就是传达任务的,告诉我们任务的内容和时间,然后下面就是构造方法了,首先先开启一个线程,然后拿出我们的最前面的任务判断时间是否合理,如果不合理的话这个时候我们就将任务再次放回去并且开始阻塞,等待相应的时间再重新开始,方式无意义的循环,同时我们每放一个任务也会唤醒线程,重新找到最近时间的任务。

线程池

为什么需要线程池

就和字符串常量池一样,我们用线程池的核心思想也是为了节省资源,我们开辟和回收一个线程都是要消耗时间的,所以我们就想我们搞一个线程池,里面放着指定数量的线程,用到的时候直接就可以拿来用了,不用再去开辟了,大大的节省了时间,相应的也会消耗一定的资源,线程池最大的好处就是减少每次启动、销毁线程的损耗。

标准库中的线程池

使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService
通过 ExecutorService.submit 可以注册一个任务到线程池中

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

    }

Executors 创建线程池的几种方式

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

模拟实现线程池

//模拟实现线程池类
class MyThreadPoll {
    //1.描述一个任务,用runnable来实现
    //2.用一个数据结构(阻塞队列)来存放我们的任务
    private LinkedBlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    //3.描述一个线程,工作线程的任务就是从队列中取出任务然后执行
    static class Worker extends Thread {
        private LinkedBlockingDeque<Runnable> queue = null;
        //从主类里面调用出我们的queue
        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();
                }
            }
        }
    }
    private List<Thread> list = new ArrayList<>();

    public MyThreadPoll(int n) {//选择我们想要放的线程数量
        for (int i = 0; i < n; i++) {
            Worker worker = new Worker(queue);//new一个新线程
            worker.start();//开始线程
            list.add(worker);//将线程放入线程池list
        }
    }
    //将我们的任务加到任务队列里面去
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

核心思想是一个放任务,然后我们的构造方法一直的处理任务。

首先我们用一个阻塞队列来存放我们的任务,然后我们用静态内部类的方式来构造组织一个我们的任务任务的目的就是执行里面的run()方法,然后我们在调用submit的时候就是往队列里面放任务,queue里面有任务了就不阻塞了构造方法已经开启了我们指定的线程,这些线程都堵塞在take()那里,所以一有任务就开始抢占执行。

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

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

相关文章

docker的虚悬镜像是什么?

虚悬镜像是什么? 答:仓库名、标签都是<none>的镜像,俗称:dangling image 我们使用Dockerfile写一个: 1:编写 from ubuntu CMD echo action is success2:构建 docker build . 注意没有 -t 产生原因: 1:构建时候因为编写错误导致 2:删除的时候 对于这样…

数据溢出的二进制原理

char 类型的数据占一个字节&#xff0c;一个字节有 8 位&#xff0c;最高位为符号位&#xff0c;1表示负数&#xff0c;0表示正数。在计算机中&#xff0c;数据用补码表示&#xff0c;正数的补码是它本身&#xff0c;负数的补码为 “符号位不变&#xff0c;其他位取反后再加1”…

Spring Cloud 系列之OpenFeign:(4)集成OpenFeign

目录 传送门 服务间调用 集成OpenFeign 说明文档 添加pom依赖 启用OpenFeign 声明OpenFeign接口 改造远程调用 定义OpenFeign接口 测试OpenFeign调用 传送门 Spring Cloud Alibaba系列之nacos&#xff1a;(1)安装 Spring Cloud Alibaba系列之nacos&#xff1a;(2)单…

离散数学数理逻辑部分【2】

文章目录命题逻辑等值演算公式的使用【重点】析取范式和合取范式【重点】范式存在定义【了解】求公式A的范式的步骤&#xff1a;【重点】极大项和极小项【重点】主合取范式和主析取范式【重点】等式演算求主析取范式【重点】真值表求主析取范式【了解】主范式的应用【重点】推理…

Python : 使用python实现教务管理系统(GUI界面+数据库)

一、设计目的 1.熟悉Python和相关软件的操作。 2.基于本学期所学Python知识&#xff0c;熟练应用掌握&#xff0c;制作符合要求的教务管理系统。 3.会对程序运行中的错误代码进行分析&#xff0c;找出合理的解决方案。 4.掌握tkinter开发流程&#xff0c;布局方法和主要组件&a…

C语言位域

如果程序的结构中包含多个开关量&#xff0c;只有 TRUE/FALSE 变量&#xff0c;如下&#xff1a; struct {unsigned int widthValidated;unsigned int heightValidated; } status; 这种结构需要 8 字节的内存空间&#xff0c;但在实际上&#xff0c;在每个变量中&#xff0c;…

JavaScript游戏开发(4)(笔记)

文章目录八、角色动作状态的管理8.1 准备部分8.2 角色状态改变的基本方式8.3 完善整个代码8.4 存在的问题九、简单的横板动作卷轴游戏9.1 准备部分9.2 输入管理器9.3 状态管理器9.4 背景管理器9.5 敌人管理器9.6 碰撞检测、UI绘制9.7 更多的角色状态与特效9.8 完善游戏附录素材…

跳槽、换房、不忘输出,与你分享我匆忙的 2022~

前些日子下班回家的瞬间&#xff0c;忽然想起去年春节还在跟老爸吐露职场的困境和对房子的无奈。哪曾想过了不到半年的时间竟全部解决&#xff0c;令我不禁感叹人生的捉摸不透。 让我姑且花点文字记录下&#xff0c;与你分享我这一年的匆匆忙忙&#xff5e; 目录前瞻&#xf…

Listener监听器 | 监听域对象创建和销毁、使用监听器统计网站在线人数

目录 一&#xff1a;监听域对象创建和销毁 1、什么是监听器&#xff1f;监听器有什么用&#xff1f; 2、Servlet规范中提供了哪些监听器&#xff1f; 3、实现一个监听器的步骤 4、HttpSessionBindingListener 5、HttpSessionIdListener & HttpSessionActivationList…

【编译原理】实验二:NFA到DFA

目录 实验二 NFA 到 DFA 一、实验目的 二、预备知识 三、实验内容 NFA向DFA的转换的思路 NFA和DFA之间的联系 NFAToDFA.h 文件 main.c 文件 RegexpToPost.c 文件 PostToNFA.c 文件 NFAFragmentStack.c 文件 PostToNFA.h 文件 NFAFragmentStack.h 文件 NFAStateStack.h 文件 dem…

【C++】使用yaml-cpp操作yaml文件

目录 1 安装yaml-cpp 2 工程结构 &#xff08;1&#xff09;test.yaml的内容 &#xff08;2&#xff09;CmakeLists.txt &#xff08;3&#xff09;代码 3 运行结果 4 报错处理 1 安装yaml-cpp &#xff08;1&#xff09;cd 到yaml-cpp下载的目的路径 例如&#xff1a;…

spring之反射机制之Spring-DI核心实现

文章目录前言一、回顾反射机制之反射调用方法1、编写一个方法类SomeService2、通过反射机制调用SomeService类中的方法二、反射机制之Spring-DI核心实现前言 调用一个方法当中含有几个要素&#xff1f; 1、调用哪个对象 2、调用哪个方法 3、调用方法的时候传什么参数 4、方法执…

一文弄懂 React ref

前言 对于 Ref 理解与使用&#xff0c;一些读者可能还停留在用 ref 获取真实 DOM 元素和获取类组件实例层面上 其实 ref 除了这两项常用功能之外&#xff0c;还有很多别的小技巧 通过本篇文章的学习&#xff0c;你将收获 React ref 的基本和进阶用法&#xff0c;并且能够明白…

LeetCode HOT 100 —— 621. 任务调度器

题目 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行&#xff0c;并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间&#xff0c;CPU 可以完成一个任务&#xff0c;或者处于待命状态。 然…

使用 SwiftUI 布局协议构建六边形网格,如何制作在六边形网格中显示子视图的通用 SwiftUI 容器

我们将要制作的组件可以作为Swift 包使用。 SwiftUI 非常擅长构建矩形框架的层次结构。随着最近的加入,Grid它变得更好了。然而,今天我们要构建一个疯狂的六边形布局。当然,没有专门的布局类型。所以我们用协议建立我们自己的Layout! 绘制一个六边形 让我们首先为我们的…

在linux中配置redis去中心化集群

目录 前情回顾 一、集群配置 二、启动redis集群 三、检验是否成功 成功&#xff01; 前情回顾 linux中配置redis主从复制及开启哨兵模式 一、集群配置 查看所有的redis服务进程 ps -ef | grep redis 关闭所有的redis服务&#xff08;6379,6380,6381) kill -9 99168 kill …

第十章:数据库恢复技术

1、【多选题】下列哪些属于事务的特征&#xff1a; 正确答案&#xff1a; AD 2、【多选题】下列关于故障恢复的说法正确的是&#xff1a; 正确答案&#xff1a; BC 3、【多选题】下列说法错误的是&#xff1a; 正确答案&#xff1a; AB

无线通信网络优化的自动路测系统设计(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 无线通信网络是一个动态的网络&#xff0c;无线网络优化是一项贯穿于整个网络发展全过程的长期工程。在网络建成投入运营以后,…

学习笔记 - Word、WPS分别设置背景色

学习笔记 - Word、WPS分别设置背景色前言实现原理实现步骤模拟背景色1. 插入矩形形状2. 调整矩形&#xff1a;位置、文字环绕、大小。3. 调整颜色实现按节分别设置1. 插入分节符2. 取消“同前节”3. 矩形入进页眉建议场景参考资料前言 Word、WPS 都没有自带此功能。只能统一设…

node.js+uni计算机毕设项目基于微信小程序的校园快递代取平台(程序+小程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等…