JAVAEE初阶相关内容第十弹--多线程(初阶)

目录

线程池:

线程存在的意义:

JAVA标准库中的线程池

工厂模式

重载和重写有什么区别?

submit

变量捕获

ThreadPoolExecutor

其中的参数意义:

corePoolSize       

maximumPoolSize   

long keepAliveTime TimeUnit unit 

BlockingQueue workQueue

ThreadFactory threadfactory

RejectedExecutionHandler handler

标准库提供了四个拒绝策略:

实现简单的线程池:


线程池:

线程存在的意义:

使用进程并发编程太重。此时我们引入了线程,线程也称为轻量级“进程”。创建线程比创建进程更高效,销毁线程比销毁进程更高效,调度线程比调度进程更高效。

此时,使用多线程编程就可以在很多时候替代进程来实现并发编程。随着并发编程的提高,我们对性能的标准提高,创建线程也变得不是很轻量了,开销也是比较大的,想要进一步提高效率吧,这里想到了两个办法:

1.轻量级线程--协程/纤程(但是在java标准库中目前没有加入),Go语言也是因为内置了协程,所以开发并发编程程序是有一定的优势的。

2.使用线程池,来降低创建/销毁线程的开销。

线程池类比于字符串常量池、数据库连接池(事先把需要使用的线程创建好,放到“池”中,后面不需要使用的时候,直接从池中获取,如果用完了,会还给池)这里获取和还的操作比创建/销毁要更高效。

创建线程/销毁线程,是由操作系统内核完成的,从池子中获取/还给池,是用户自己的代码就能实现,不必交给内核操作。

类比于在银行取钱,在银行的大厅里,用户都是自主的,就像程序中的“用户态”。用户态执行的是程序员自己的代码,这里想做什么,想怎么做都是程序员自己决定的,有些操作是在银行的柜台里进行完成的,就需要通过银行的工作人员,通过工作人员来间接完成。内核会给程序提供一些Api,称为系统调用,程序可以调用系统调用,驱使内核完成一些工作。

相比于内核来说,用户态,程序执行的行为是可控的。想要做某个工作,就会非常干净利落的完成(比如从池子里取/还给池子),如果通过内核,让系统从这里创建个线程,就需要通过系统调用,让内核来执行。此时不清楚在内核上背负了多少个任务(内核需要给所有的程序提供服务)

JAVA标准库中的线程池

在JAVA标准库中,也提供了现成的线程池,可以直接使用。

ExecutorService pool = Executors.newFixedThreadPool(10);

JAVAEE初阶相关内容第十弹--多线程(初阶)_第1张图片

工厂模式

使用普通方法来代替构造方法,创建对象

为什么要代替?构造方法有坑:只构造一种对象好办,如果要构造多种不同情况下的对象,就有问题。

插播:

重载和重写有什么区别?

重载(Overload)两个方法名一样,参数不一样(参数个数/参数类型),同一个作用域下,这俩方法在同一个类里可以构成重载,分别在父类的子类里,也是可能构成重载的。

重写(Override)在java中的方法重写与父类相关的,用一个新方法代替旧的,此时就要求新的方法和旧的方法,名字/参数都得一模一样才行。

普通方法,方法的名字没有限制,因此有多种方式构造,就可以直接使用不同的方法名即可。此时方法的参数是否要区分,就不重要了。

 ExecutorService pool = Executors.newFixedThreadPool(10);

此处构造出一个10个线程的线程池,然后就可以随时安排这些线程干活。

线程池提供了一个重要的方法,submit,可以给线程池提交若干任务。

JAVAEE初阶相关内容第十弹--多线程(初阶)_第2张图片

submit

线程池提供了一个重要的方法,submit,可以给线程池提交若干任务,运行程序后发现main线程结束了,但是整个进程没结束,因为线程池里面的线程都是前台线程,此时会阻止进程结束(上一节中的Timer也是同理)。在上面的代码截图中需要注意的是当前是向线程池中放1000个任务,1000个任务又是由这十个线程进行分配执行的,差不多是一个线程执行100个任务。但也不是严格的平均,进一步的可理解为这1000个任务,就在一个队列中排队,而这十个线程就依次来取队列中的任务,取一个就执行一个,执行完再取下一个。类比于【做核酸】。

变量捕获

JAVAEE初阶相关内容第十弹--多线程(初阶)_第3张图片 JAVAEE初阶相关内容第十弹--多线程(初阶)_第4张图片这些线程池用起来更麻烦一点,所以才给我们提供了工厂类,使用更简单。

在java在线文档中,java.util.concurrent包下,很多类都是和并发编程(多线程编程)密切相关的。这个包也简称juc。

JAVAEE初阶相关内容第十弹--多线程(初阶)_第5张图片

ThreadPoolExecutor

最后一组是最复杂的

其中的参数意义:

corePoolSize       

 核心线程个数   (正式员工)

maximumPoolSize   

最大线程个数 (临时工/实习生)    这两个之和是最大线程数。

【允许正式员工摸鱼,不允许实习生摸鱼,加入实习生摸鱼太久,就会被销毁】

如果任务多,显然是需要更多的线程,此时多一些线程,成本也是值得的,但是一个程序任务不一定始终都是很多,有时候会多写,有时候会少些,任务少,线程数量多就不合适,所以要对现有的线程进行一定的淘汰。

整体的策略:正式员工保底,临时员工动态的调节。

实际开发的过程中,线程池的线程数设置成多少是合适的?

具体的数字全部都是错的!!!

不同的程序特点不同,此时要设置的线程数也是不同的,考虑两个极端的情况:

(1)CPU密集型。每个线程要执行的任务都是狂转CPU的(一系列算术运算),此时线程池线程数最多也不应该超过CPU核数。因为这时设置的再大也是无效的,cou密集型任务,要一直占用cpu,那么多线程,CPU的位置不够。

(2)IO密集型。每个线程做的工作就是等待IO(读写硬盘,读写网卡,等待用户输入),不吃CPU,此时这样的线程处于阻塞状态,不参与CPU调度。这个时候多些线程也是可以的,理论上来说设置成无穷大也行。

然而,在实际的开发中并没有程序符合这两种理想模型,真正的程序往往一部分要吃CPU,一部分要等待IO,具体分成是不确定的。需要实践测试。

long keepAliveTime TimeUnit unit 

 描述了临时工可以摸鱼的最大时间 时间单位:(s,ms,分钟)

BlockingQueue workQueue

线程池的任务队列 此处使用阻塞队列也是和容易理解的,想象一下,每个工作线程,都是在不停的尝试take,如果有任务,就take成功,没有就阻塞

ThreadFactory threadfactory

用于创建线程,线程池是需要创建线程的

RejectedExecutionHandler handler

描述了线程池的“拒绝策略”,也是一个特殊的对象,描述了说当前线程池任务队列满了,如果继续添加任务会有什么样的行为。

标准库提供了四个拒绝策略:

JAVAEE初阶相关内容第十弹--多线程(初阶)_第6张图片

实现简单的线程池:

一个线程池里面至少有两个部分

1.阻塞队列:保存任务

2.若干个工作线程

class MyThreadPool1{
    //此处不涉及到“时间”,此处只有任务,直接使用Runnable即可
    private BlockingQueue queue = new LinkedBlockingQueue<>();
    //n表示线程的数量
    public MyThreadPool1(int n){
        //在这里创建出线程
        for (int i = 0; i < n; i++) {
            //创建线程,循环了n次
            Thread t = new Thread(() ->{
                while (true){
                    try {
                        Runnable runnable =queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
    //注册任务给线程池
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
public class ThreadD27 {
    public static void main(String[] args) {
        MyThreadPool1 pool = new MyThreadPool1(10);
        for (int i = 0; i < 1000; i++) {
            int n = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello"+ n);
                }
            });
        }
    }
}

以上是最简单的线程池。基本的代码形式:通过一个阻塞队列来保存任务,submit就是往队列里添加元素,工作线程不停的从队列里取元素。

接下来会对初阶进行整体的总结,随后开启进阶部分的内容~

你可能感兴趣的:(Javaee,java-ee,java,开发语言)