Java EE初阶启程记09---多线程案例(2)

🔥个人主页: 寻星探路

🎬作者简介:Java研发方向学习者

📖个人专栏:、《

⭐️人生格言:没有人生来就会编程,但我生来倔强!!!



目录

一、定时器

1、定时器是什么

2、标准库中的定时器

3、实现定时器

二、线程池

1、线程池是什么

2、标准库中的线程池

3、实现线程池

三、总结-保证线程安全的思路

四、对比线程和进程


续接上一话:

一、定时器

1、定时器是什么

定时器也是软件开发中的一个重要组件,类似于一个"闹钟",达到一个设定的时间之后,就执行某个指定好的代码

定时器是一种实际开发中非常常用的组件。

比如网络通信中,如果对方500ms内没有返回数据,则断开连接尝试重连。

比如一⼀个Map,希望里面的某个key在3s之后过期(自动删除)。

类似于这样的场景就需要用到定时器。

2、标准库中的定时器

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

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

java 复制代码
 Timer timer = new Timer();
 timer.schedule(new TimerTask() {
     @Override
     public void run() {
         System.out.println("hello");
     }
 }, 3000);

3、实现定时器

定时器的构成

一个带优先级队列(不要使用PriorityBlockingQueue,容易死锁!)

队列中的每个元素是一个Task对象

Task中带有一个时间属性,队首元素就是即将要执行的任务

同时有一个worker线程一直扫描队首元素,看队首元素是否需要执行

1)Timer类提供的核心接口为schedule,用于注册一个任务,并指定这个任务多长时间后执行

java 复制代码
 public class MyTimer {
     public void schedule(Runnable command, long after) {
         // TODO
     }
 }

2)Task类用于描述⼀个任务(作为Timer的内部类),里面包含一个Runnable对象和一个time(毫秒时间戳)

这个对象需要放到优先队列中,因此需要实现 Comparable 接口

java 复制代码
 class MyTask implements Comparable<MyTask> {
     public Runnable runnable;
     // 为了⽅便后续判定, 使⽤绝对的时间戳. 
     public long time;

     public MyTask(Runnable runnable, long delay) {
         this.runnable = runnable;
         // 取当前时刻的时间戳 + delay, 作为该任务实际执⾏的时间戳 
         this.time = System.currentTimeMillis() + delay;
     }

     @Override
     public int compareTo(MyTask o) {
         // 这样的写法意味着每次取出的是时间最⼩的元素. 
         // 到底是谁减谁?? 俺也记不住!!! 随便写⼀个, 执⾏下, 看看效果~~ 
         return (int)(this.time - o.time);
     }
 }

3)Timer实例中,通过PriorityQueue来组织若干个Task对象

通过schedule来往队列中插入一个个Task对象

java 复制代码
 class MyTimer {
     // 核⼼结构
     private PriorityQueue<MyTask> queue = new PriorityQueue<>();
     // 创建⼀个锁对象 
     private Object locker = new Object();

     public void schedule(Runnable command, long after) {
         // 根据参数, 构造 MyTask, 插⼊队列即可. 
         synchronized (locker) {
             MyTask myTask = new MyTask(runnable, delay);
             queue.offer(myTask);
             locker.notify();
         }    
     }
 }

4)Timer类中存在一个worker线程,一直不停的扫描队首元素,看看是否能执行这个任务

所谓"能执行"指的是该任务设定的时间已经到达了

java 复制代码
 // 在这⾥构造线程, 负责执⾏具体任务了. 
 public MyTimer() {
     Thread t = new Thread(() -> {
         while (true) {
             try {
                    synchronized (locker) {
                    // 阻塞队列, 只有阻塞的⼊队列和阻塞的出队列, 没有阻塞的查看队⾸元素. 
                    while (queue.isEmpty()) {
                        locker.wait();
                    }
                    MyTask myTask = queue.peek();
                    long curTime = System.currentTimeMillis();
                    if (curTime >= myTask.time) {
                        // 时间到了, 可以执⾏任务了 
                        queue.poll();
                        myTask.runnable.run();
                    } else {
                        // 时间还没到
 
                        locker.wait(myTask.time - curTime);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
 }

二、线程池

1、线程池是什么

虽然创建线程/销毁线程的开销

想象这么⼀个场景:

在学校附近新开了一家快递店,⽼板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。

很快老板发现问题来了,每次招聘+解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到3个人,但还是随着业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没3个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着3个快递人员空闲的时候去处理。这个就是我们要带出的线程池的模式。

线程池最大的好处就是减少每次启动、销毁线程的损耗。

2、标准库中的线程池

使用Executors.newFixedThreadPool(10)能创建出固定包含10个线程的线程池。

返回值类型为ExecutorService。

通过ExecutorService.submit可以注册一个任务到线程池中。

java 复制代码
 ExecutorService pool = Executors.newFixedThreadPool(10);
 pool.submit(new Runnable() {
     @Override
     public void run() {
         System.out.println("hello");
     }
 });

Executors 创建线程池的几种方式:

newFixedThreadPool:创建固定线程数的线程池

newCachedThreadPool:创建线程数目动态增长的线程池

newSingleThreadExecutor: 创建只包含单个线程的线程池

newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令,是进阶版的Timer。

Executors 本质上是ThreadPoolExecutor类的封装。

ThreadPoolExecutor 提供了更多的可选参数,可以进⼀步细化线程池行为的设定

corePoolSize:正式员工的数量。(正式员工,一旦录用,永不辞退)

maximumPoolSize:正式员工+临时工的数目。(临时工:一段时间不干活,就被辞退)

keepAliveTime:临时工允许的空闲时间

unit:keepaliveTime 的时间单位,是秒,分钟,还是其他值

workQueue:传递任务的阻塞队列

threadFactory:创建线程的工厂,参与具体的创建线程工作,通过不同线程工厂创建出的线程相当于对一些属性进行了不同的初始化设置

RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理

AbortPolicy(): 超过负荷,直接抛出异常

CallerRunsPolicy():调用者负责处理多出来的任务

DiscardOldestPolicy():丢弃队列中最⽼的任务

DiscardPolicy():丢弃新来的任务

3、实现线程池

核心操作为submit,将任务加入线程池中

使用Worker类描述一个工作线程,使用Runnable描述一个任务

使用一个BlockingQueue组织所有的任务

每个worker线程要做的事情:不停的从BlockingQueue中取任务并执行

指定一下线程池中的最大线程数maxWorkerCount:当当前线程数超过这个最大值时,就不再新增线程了

java 复制代码
class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    // 通过这个⽅法, 来把任务添加到线程池中. 
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
    // n 表⽰线程池⾥有⼏个线程. 
    // 创建了⼀个固定数量的线程池. 
    public MyThreadPool(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        // 取出任务, 并执⾏~~ 
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
    }
 }
java 复制代码
 // 线程池 
 public class Demo {
     public static void main(String[] args) throws InterruptedException {
         MyThreadPool pool = new MyThreadPool(4);
         for (int i = 0; i < 1000; i++) {
             pool.submit(new Runnable() {
                 @Override
                 public void run() {
                     // 要执⾏的⼯作
                     System.out.println(Thread.currentThread().getName() + " hello");
                 }
             });
         }
     }
 }

三、总结-保证线程安全的思路

1、使用没有共享资源的模型

2、适用共享资源只读,不写的模型

a. 不需要写共享资源的模型

b. 使用不可变对象

3、直面线程安全(重点)

a. 保证原子性

b. 保证顺序性

c. 保证可见性

四、对比线程和进程

10.1 线程的优点

  1. 创建一个新线程的代价要比创建⼀个新进程小得多

  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  3. 线程占用的资源要比进程少很多

  4. 能充分利用多处理器的可并行数量

  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

10.2 进程与线程的区别

  1. 进程是系统进行资源分配和调度的一个独⽴单位,线程是程序执行的最小单位。

  2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。

  3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。

  4. 线程的创建、切换及终止效率更⾼。

相关推荐
武子康2 小时前
Java-141 深入浅出 MySQL Spring事务失效的常见场景与解决方案详解(3)
java·数据库·mysql·spring·性能优化·系统架构·事务
珹洺3 小时前
Java-Spring入门指南(十五)SpringMVC注解开发
java·spring·microsoft
小满、3 小时前
什么是Maven?关于 Maven 的坐标、依赖管理与 Web 项目构建
java·maven
半旧夜夏3 小时前
【设计模式】核心设计模式实战
java·spring boot·设计模式
froginwe113 小时前
Python 3 输入和输出
开发语言
小何好运暴富开心幸福3 小时前
C++之再谈类与对象
开发语言·c++·vscode
zhangfeng11334 小时前
R 导出 PDF 时中文不显示 不依赖 showtext** 的最简方案(用 extrafont 把系统 TTF 真正灌进 PDF 内核)
开发语言·r语言·pdf·生物信息
应用市场4 小时前
自建本地DNS过滤系统:实现局域网广告和垃圾网站屏蔽
开发语言·php
郝学胜-神的一滴4 小时前
中秋特别篇:使用QtOpenGL和着色器绘制星空与满月
开发语言·c++·算法·软件工程·着色器·中秋