多线程案例(下)

简介: 多线程案例(下)

三、定时器



在 Java 标准库中,为我们提供了 Timer 这个类,作为定时器。

Timer 类的核心方法为 schedule( )


schedule 包含两个参数:


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


程序清单8:


import java.util.Timer;
import java.util.TimerTask;
public class Test {
    public static void main(String[] args) {
        System.out.println("代码开始执行...");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("触发定时器");
            }
        },3000);
    }
}


输出结果: 3秒后触发定时器


2548cc25fea2492c8428b7edacd12db1.png


实现定时器


程序清单9:


import java.util.concurrent.PriorityBlockingQueue;
public class Test {
    //使用 Task 这个类来描述一个任务
    static class Task implements Comparable<Task>{
        //command 表示这个任务是什么
        private Runnable command;
        //time 表示这个任务什么时候到时间,它用毫秒级的时间戳来表示
        private long time;
        //约定参数 time 是一个时间差(类似于 3000)
        //希望 this.time 来保存一个绝对的时间 (毫秒级时间戳)
        public Task (Runnable command, long time) {
            this.command = command;
            //传入的时间再 + 时间戳
            this.time = System.currentTimeMillis() + time;
        }
        public void run() {
            command.run();
        }
        @Override
        public int compareTo(Task o) {
            return (int) (this.time - o.time);
        }
    }
    static class Timer {
        //使用优先级阻塞队列来组织这些任务
        private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
        //使用 locker 对象来解决忙等的问题
        private Object locker = new Object();
        public void schedule (Runnable command, long delay) {
            Task task = new Task(command,delay);
            queue.put(task);
            //每次插入新的任务都要唤醒扫描线程,让扫描线程能够重新计算 wait 时间,保证新的任务也不会错过
            synchronized (locker) {
                locker.notify();
            }
        }
        public Timer() {
            //创建一个扫描线程,这个扫描线程就是用来判定当前任务的,看看是不是已经到时间能执行了
            Thread t = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        //取出队列的首元素,判定时间是不是到了
                        try {
                            Task task = queue.take();
                            long curTime = System.currentTimeMillis();
                            if (task.time > curTime) {
                                //假设任务时间 9点,当前时间 8点,说明时间还未到
                                queue.put(task);
                                synchronized (locker) {
                                    locker.wait(task.time - curTime);
                                }
                            } else {
                                task.run();
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            break;
                        }
                    }
                }
            };
            t.start();
        }
    }
    public static void main(String[] args) {
        System.out.println("代码开始执行...");
        Timer timer = new Timer();
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("触发定时器");
            }
        },3000);
    }
}


输出结果:


eacb699ed8074db88415cd88003bf9e7.png


四、线程池(经典面试题)



引言


我们都知道多进程就是用来解决并发编程的方案,但进程的创建和销毁,开销比较大。


因此引入了线程,线程比进程要轻量很多。即便如此,在某些场景中,需要频繁地创建并销毁线程,而此时,线程的创建和销毁操作,相对来说也是有一定开销。那么有没有什么办法能够使线程调度优化变得更好一些呢?


1. 线程池的概念


那么就引入线程池这样的概念,现在我们将一个线程想象成一个东西,将线程池想象成一个储备池。而在某个场景需要使用线程的时候,我们就直接从池中取一个线程出来即可,而这个线程池并不是临时创建的,而是在某个任务需求之前就创建出来了这个池子。那么,当我们不需要某个线程的时候,就把线程还回池子中,此时,关于这样线程池的操作,比线程的创建和销毁的效率更高。


这就好比于你投简历之后面试,当你投的这家公司,某个团队需要10人,而有50人竞争这个岗位,那么,如果你通过了面试,就能直接入职。

假设你没有通过岗位,它会将你放入一个 "人才储备库 ",有一天,公司比较重视这个团队的一个业务,但这个业务人手又不够,却又比较赶工,那么原先需要10人的岗位,现在需要20人。那么,多出来需要招聘10人的这个岗位,就可以从 "人才储备库 "中挑选。


acc83968980d4d84a13e57bd4343275b.png


2. 线程池的好处


创建和销毁线程,本质上需要将用户态切换到内核态,然后再对内核态的 PCB 进行操作。如果使用线程池这个手段,我们拿放线程都只在用户态,不涉及对 PCB 的修改,在针对频繁的线程操作的场景下,显然,线程池能够带能更高效的操作。


3. 内核态和用户态


用户态就是应用程序执行的代码,内核态就是操作系统执行的代码,一般来说,两者之间的切换,是一个开销较大的过程。


而内核是很忙的,要给许许多多个进程同时提供服务,因此,一旦要将某个进程把某个任务交给内核来去做,此时什么时候能把事情做好,有些难以把握了。


这就像买高铁票一样,如果我们去高铁站的人工服务窗口购票,显然是很麻烦的一件事情,第一,你要排队,第二,售票员询问你乘坐的火车类型、车次、时间…,第三,售票员要为很多人服务,所以在面对形形色色的业务,他们所执行的时间较慢。而如果你直接从网上购票就不一样了,你可以按照自己的需求,很简单方便地就选购好了,退票、换票等等也比较直接。繁忙的售票员与你购票的场景就相当于内核态和用户态交互的过程,而你自己通过网上购票的场景就是用户态自己实现的过程,除了使用手机,就是自己与自己交互的过程,而此时的购票用手机就相当于一个池子。


2ee7d4ffa29245e4a7cd771b501dfba5.png



4. ThreadPoolExecutor 类


在 Java 标准库中,提供了现成的线程池组件,即 ThreadPoolExecutor 类。但这个类使用起来较为复杂,需要传入很多参数。


我们一起来看看一个其中参数最多的构造方法:


ThreadPoolExecutor( int corePoolSize, int maximumPoolSize, 
long keepAliveTime, TimeUnit unit, 
BlockingQueue<Runnable> workQueue, ThreadFactory , 
RejectedExecutionHandler handler ) 


5. ThreadPoolExecutor 类中构造方法中的参数是什么意思(面试题)


ThreadPoolExecutor 类中,包含的线程的数量并不是一成不变的。

它能够根据任务量来自适应,如果任务较多,就会多创建一些线程;


如果任务较少,就会少创建一些线程。


(1) corePoolSize:核心线程数


(2) maximumPoolSize:最大线程数


我们可以将线程池想象成是一个 " 公司 ",公司里面的每个员工,就相当于是一个线程。

corePoolSize 就相当于正式工的数量,maximumPoolSize 就相当于正式工+临时工


员工分成两类:

① 正式工:签了劳动合同的,不能随便辞退。正式员工允许摸鱼 ( 这样的线程即使是空闲,也不会被销毁 )

② 临时工:没签劳动合同,可以随时辞退。临时工不允许摸鱼 ( 如果临时工线程摸鱼的时间达到一定的程度了,就会被销毁 )


如果我们要解决的任务场景对应的任务量比较稳定的,就可以设置 corePoolSize 和 maximumPoolSize 尽量接近 (临时工就可以尽量少一些)

如果要解决的任务场景,任务量波动比较大,就可以设置 corePoolSize 和 maximumPoolSize相差更大一些 (临时工就可以多一些)


(3) keepAliveTime

这就相当于临时工摸鱼可以摸多长时间


(4) unit

keepAliveTime 的时间单位


(5) workQueue

阻塞队列,组织了线程池要执行的任务。


(6) threadFactory

线程的创建方式,通过这个参数,来设定不同线程的创建方式。

Factory 就是工厂的意思。


(7) RejectedExecutionHandler handler

拒绝策略,当任务队列满的时候,又来了新任务,这个参数就是来处理这件事情。他根据你的需求可能会做出如下决定:

丢弃最新的任务,丢弃最老的任务,阻塞等待,抛出异常…


6. Executors 类


由于 ThreadPoolExecutor 类使用起来比较复杂,所以 Java 标准库又提供了一组其他的类,相当于对ThreadPoolExecutor 又进行了一层封装,即 Executors 这个类。

这个类相当于一个 " 工厂类 ",我们通过这个类提供的一组工厂方法,就可以创建出不同风格的线程池实例了。

(1) newFixedThreadPool:创建出一个固定线程数量的线程池 ( 完全没有临时工的版本 )

(2) newCachedThreadPool:创建出一个数量可变的线程池 ( 完全没有正式员工, 全是临时工 )

(3) newSingle’ ThreadPool:创建出一个只包含一个线程的线程池 ( 只是特定场景下使用 )

(4) newScheduleThreadPool:能够设定延时时间的线程池 ( 插入的任务能够过一会再执行 ),相当于进阶版的定时器。


程序清单10:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
    public static void main(String[] args) {
        //1. 创建出一个线程数量为 10 的线程池的实例
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2. 给这个实例中加 1 个任务
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}


输出结果:


ffce1e99df8d4045aa6413a18a0c9b15.png


程序清单11:


import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
    public static void main(String[] args) {
        //1. 创建出一个线程数量为 10 的线程池的实例
        ExecutorService service = Executors.newFixedThreadPool(10);
        //2. 给这个实例中加 20 个任务
        for (int i = 0; i < 20; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}


输出结果:最后处于阻塞等待状态。


2e217059e0d8487097b588f2d7d9e321.png


我们在程序开头时,设置了当前线程池的线程数量为为 10个 工作线程,我们往任务队列中加入了 20个 任务,此时这 10个 工作线程就会从任务队列中,先取出 10个 任务,然后并发执行这些任务。之后,这些线程谁执行完了当前的任务,谁就去任务队列中重新取一个新的任务。直到把线程池任务队列中的任务都取完了,此时线程池的工作线程就阻塞等待,即等待着新任务的到来。


五、工厂模式



工厂模式存在的意义,就是在给构造方法填坑。


工厂方法:工厂方法其实就是普通的方法,方法里面会调用对应的构造方法。并进行一些初始化操作,最后返回这个对象的实例。


举个例子:假设有个 Point类,表示平面上第一个点,我们希望通过笛卡尔坐标和极坐标这两种方式来构造这个点。我们通过构造方法来试一试这个操作。代码如下,我们发现,IDEA 编译器出现编译时错误,说两个 double 参数的构造方法已经存在。也就是说,我们使用传统的构造方法,需要满足方法之间的重载。


① 方法名相同

② 方法的参数个数不同 / 方法的参数类型不同

③ 方法的返回值类型不作要求


3d01f0cd754e47e39c0b011c02f54d7f.png


解决上面的这个问题:

我们不使用构造方法来构造实例了,而是使用其他的方法来进行构造实例。这样用来构造实例的方法,就称为 " 工厂方法 ",顾名思义,工厂就是用来制造的!


拓展:工厂模式实际上就是一种设计模式,而设计模式就是与语言相关的,有些语言中存在一些语法缺陷,所以在某些情况下,就需要依赖设计模式来进行约束。


实现简单版本的线程池


步骤:


(1) 创建一个线程池类 Thread

(2) 使用一个阻塞队列组织若干个任务,将任务用 Runnable 表示

(3) 需要创建一些工作线程


程序清单12:


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class Test1 {
    static class Worker extends Thread {
        private BlockingQueue<Runnable> queue = null;
        public Worker (BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }
        /**
         * 工作线程的具体逻辑,需要从阻塞队列中取任务
         */
        @Override
        public void run() {
            while (true) {
                try {
                    Runnable command = queue.take();
                    //通过 run 方法来执行这个任务
                    command.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class ThreadPool {
        //包含一个阻塞队列,用来组织任务
        private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
        //将当前的工作线程都存放在顺序表中
        private List<Thread> workers = new ArrayList<>();
        private static final int MAX_WORKER_COUNT = 10;
        /**
         * submit 方法把任务存放到当前线程池中
         * submit 方法不光可以将任务放到阻塞队列中,也可以负责创建线程
         */
        public void submit (Runnable command) throws InterruptedException {
            if (workers.size() < MAX_WORKER_COUNT) {
                //如果当前工作线程的数量未达到线程数目的上限,就创建出新的线程
                //工作线程就专门通过一个类来完成
                Worker worker = new Worker(queue);
                worker.start();
                workers.add(worker);
            }
            queue.put(command);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadPool pool = new ThreadPool();
        for (int i = 0; i < 20; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}


输出结果:


aff7e6fcc37a48a8838b97aa275bca9b.png


目录
相关文章
|
7月前
多线程案例-定时器(附完整代码)
多线程案例-定时器(附完整代码)
306 0
|
2月前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
24 1
|
7月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
236 2
|
7月前
|
安全 Java
Java多线程基础-10:代码案例之定时器(一)
`Timer` 是 Java 中的一个定时器类,用于在指定延迟后执行指定的任务。它常用于实现定时任务,例如在网络通信中设置超时或定期清理数据。`Timer` 的核心方法是 `schedule()`,它可以安排任务在延迟一段时间后执行。`
149 1
|
7月前
|
Java Python 开发者
Python 学习之路 01基础入门---【Python安装,Python程序基本组成】
线程池详解与异步任务编排使用案例-xian-cheng-chi-xiang-jie-yu-yi-bu-ren-wu-bian-pai-shi-yong-an-li
512 3
Python 学习之路 01基础入门---【Python安装,Python程序基本组成】
|
7月前
|
SQL Dubbo Java
案例分析|线程池相关故障梳理&总结
本文作者梳理和分享了线程池类的故障,分别从故障视角和技术视角两个角度来分析总结,故障视角可以看到现象和教训,而技术视角可以透过现象看到本质更进一步可以看看如何避免。
84769 136
案例分析|线程池相关故障梳理&总结
|
7月前
|
Python
Python学习之路 02 之分支结构
Python学习之路 02 之分支结构
494 0
Python学习之路 02 之分支结构
|
3月前
|
安全 Java 调度
python3多线程实战(python3经典编程案例)
该文章提供了Python3中多线程的应用实例,展示了如何利用Python的threading模块来创建和管理线程,以实现并发执行任务。
67 0
|
4月前
|
消息中间件 安全 Kafka
"深入实践Kafka多线程Consumer:案例分析、实现方式、优缺点及高效数据处理策略"
【8月更文挑战第10天】Apache Kafka是一款高性能的分布式流处理平台,以高吞吐量和可扩展性著称。为提升数据处理效率,常采用多线程消费Kafka数据。本文通过电商订单系统的案例,探讨了多线程Consumer的实现方法及其利弊,并提供示例代码。案例展示了如何通过并行处理加快订单数据的处理速度,确保数据正确性和顺序性的同时最大化资源利用。多线程Consumer有两种主要模式:每线程一个实例和单实例多worker线程。前者简单易行但资源消耗较大;后者虽能解耦消息获取与处理,却增加了系统复杂度。通过合理设计,多线程Consumer能够有效支持高并发数据处理需求。
195 4
|
7月前
|
Java
线程池详解与异步任务编排使用案例-xian-cheng-chi-xiang-jie-yu-yi-bu-ren-wu-bian-pai-shi-yong-an-li
线程池详解与异步任务编排使用案例-xian-cheng-chi-xiang-jie-yu-yi-bu-ren-wu-bian-pai-shi-yong-an-li
91 0