线程池,以及线程池的实现以及面试常问的问题,工厂模式,常见的锁策略(面试常考,要了解,不行就背)

一、💛

线程池的基本介绍

内存池,进程池,连接池,常量池,这些池子概念上都是一样的~~

如果我们需要频繁的创建销毁线程,此时创建销毁的成本就不能忽视了,因此就可以使用线程池。

提前创建好一波线程,后续需要使用线程,就直接从池子里面拿一个即可,当线程不再使用,就放回池子里面。(本来是需要创建线程/销毁线程,现在是从池子里面获取到现成的线程,并且把线程归还到池子里面

那么为啥从池子里面拿就比系统里面创建线程更加高效呢?

不用线程池:如果是系统这里创建线程,需要调用系统API,进一步的由操作系统内核完成,完成线程的创建过程(内核是给所有进程提供服务的,这样你想干的事情,就需要等一等,等多长时间我们是未知的,是不可以控制的)

使用线程池:上述的内核中进行的操作都是提前做好了的,现在的取线程过程,纯粹的是用户使用代码完成的(纯用户态)-是可控制的。

二、💙

工厂模式:去生产的功能(字面意思),用于生产对象,一般情况下我们创建对象都是new,通过构造方法,但是构造方法有时候存在巨大的缺陷(构造方法是固定就是类名,有的类需要使用多种不同构造方式->(方法重载仅要求参数的个数和类型有区别)

~比如说:表示坐标->这种无法构成重载

public class Circle {
    public Circle(double x,double y){         //笛卡尔坐标
        
    };
    public  Circle(double r,double a){        //极坐标
         
    };
}
所以上面的代码也不对,构不成方法重载

使用工厂模式来解决上述问题,不使用构造方法来,(我刚开始也在想为什么是静态,直到我自己去试一下,明白了也就是说不用构造方法创建对象,假如不是静态方法,那么该怎么调用他呢,不是静态的,可是只能用对象.方法才可以调用)用普通方法来构造对象,这样方法就可以任意的了,普通方法内部去new对象~由于普通方法的目的是创建对象,然后调用方法来设置属性,所以方法一般都是静态的。(类名.方法)

三、💜 

//Executors:工厂类,后面的是工厂方法。这句话是创建一个固定线程数量的线程池
//线程池对象:ExecutorService service
        ExecutorService service= Executors.newFixedThreadPool(4);
//创建一个线程数组,动态变化的线程池。
        ExecutorService service2= Executors.newCachedThreadPool();
//包含单个线程(比原生创建API更简单一些指Thread)
        ExecutorService service3= Executors.newSingleThreadExecutor();
//类似于定时器效果,添加一些任务,执行,被执行的时候,不是只有有一个扫描线程来执行,可能是有多个共同执行
        ExecutorService service4= Executors.newScheduledThreadPool();
    }

 

四、❤️

面试题:谈谈java库中的线程池构成方法的参数和含义

这个方法最复杂,而且别的参数这个参数都有,所以就只解释这个方法就行。

int corePoolSize:核心线程数

int maximunPoolSize:最大线程数

ThreadPoolExcutor:里面的线程个数,并

非固定不变的,会根据当前任务的情况动态变化(自适应)

corePoolSize:至少要这些线程,哪怕你的线程都没任务也要这些个线程(如同公司里面的正式员工)

maximumPoolSize:最多不超过这些线程,哪怕干冒烟了,也不能比这个更多了(正式员工+实习生)

long keepAliveTime, TimeUnit unit:实习生线程,空闲时间超过指定阈值(允许实习生摸鱼的最大时间),就可以销毁了

BlockingQueue<Runnable>workQueue:线程池内部有很多很多,任务可以使用阻塞队列管理,线程池可以内置阻塞队列,也可以手动一个。

RejectedExecutionHandler   handler:线程池考的重点,拒绝方式/拒绝策略,线程池有一个阻塞队列,当队列满了,继续加任务如何处理。

1.ThreadPoolExecutors.AbortPolicy:直接抛出异常,线程池就不干活了,(小王喊我打球,我在学习,我直接崩溃了,我哇哇大哭)

2.ThreadPoolExecutor.callerRunsPolicy:谁说添加这个任务的线程,谁就去执行这个任务

,我会直接说:我没空,自己投去吧)

3.ThreadPoolExecutor.DiscardPolicy:把新的任务丢弃,(不打球了,我接着学习)

4.ThreadPoolExecutor.DiscardPolicy:丢弃最早的任务,执行新的任务(放弃学习,去打球)

有的线程公司会推荐使用这个。

 

五、💚 

具体实现一个线程池

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

//通过这个方法把任务添加到线程池中
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }


//n表示一个线程池里面有几个字段,创建了一个固定数量的线程池
    public MyThreadPool(int n) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    Runnable runnable = null;
                    try {
                        runnable = queue.take();      //从队列中提取这个任务
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    runnable.run();                  //运行这个任务
                }
            });
            t.start();                              //开启这个线程
        }
    }
}
public class Demo4 {

    public static void main(String[] args) throws InterruptedException {
        //获取当前引用实例
        MyThreadPool myThreadPool = new MyThreadPool(4);
        for (int i = 0; i < 1000; i++) {               //循环很关键哈,这个循环是添加1000次任务,但是假如你放到run里面,就会变成一个任务,至少内容是执行1000遍,线程是执行多个任务,但是不能一个任务还分担
            myThreadPool.submit(new Runnable() {           //(安排任务,安排一个任务)
                @Override                                  //这个任务进行的工作
                public void run() {

                    System.out.println(Thread.currentThread().getName() + " love");
                }
            });
        }
    }
}

面试问题2号:创建线程池的时候,线程个数怎么数的:

网路上查资料很多:假设cpu逻辑数是N,线程的个数:N,N+1,2N···

准确的说都不准确,因为不同的项目要做的工作是不同的:

cpu密集型线程工作:全是运算大部分工作在cpu上完成,cpu给他安排核心,才能概括,假如cpu N个核心,线程数量最好也是为N,如果多了,线程也只能是排队等待,没有新的进展

Io密集型线程工作:涉及大量等待时间,等待的过程,不要cpu,所以这里线程多,也会给cpu造成负担,cpu16核,整个32个线程,不犯毛病(不耗cpu,甚至cpu占用很低)———

实际上,一部分cpu密集,一部分Io密集,是我们工作中的常态,此时一个线程多少在cpu上执行,多少等待IO,说不好,更好的做法:自己去性能测试一下,找到性能和开销比较均衡的数值


六、💔 

多线程进阶开启:

常见的锁策略

如果工作中,真正要实现一把锁,需要理解锁策略

1.乐观锁VS悲观锁

乐观锁:预测,不太会出现锁冲突的情况

悲观锁:预测,这个场景非常容易锁冲突

2.重量级锁VS轻量级锁

重量级锁:加锁开销比较大,花的时间多,占有系统资源,一个悲观锁,很可能是重量级锁

轻量级锁:花的时间少,占有资源少,加锁的开销比较小的,很可能是乐观锁

悲观,乐观是加锁之前堆冲突概率的预测,决定工作的多少,重量,轻量,是加锁后,考虑实际的锁开销

3.自旋锁VS刮起等待锁

自旋锁是轻量级的一种典型实现,在用户态通过自旋的方式(while循环),实现类似加锁的操作(一直在疯狂的舔,这种锁会耗一定的cpu,但是是最快速度拿到锁的)

挂起等待锁:通过内核态,借助系统提供的锁机制,当出现锁冲突,会牵扯到内核对线程的调度,是冲突的线程出现挂起(阻塞等待)重量级锁的一种典型体现,发现锁被占用后,自己该干啥干啥,偶尔听到了消息,又去找这个锁,耗费cpu少,但无法第一时间拿到锁(小摆烂)

4.读写锁VS互斥锁

读写锁:把读操作,写操作分开了

假如两个线程 一个读加锁,另一个还是读加锁,那么两个不会有锁竞争(目的:就是把这种情况处理,这样多线程的效率会更高)

一个读,一个写,两个都是写都会有锁竞争,但是两个读没事,在开发中读操作会比写操作更加频繁

互斥锁:写了就不能读,读了就不可以写,

5.公平锁VS非公平锁

公平锁是遵循先来后到这个规则的

非公平锁相当于超市促销,都来抢位置,不遵守顺序

操作系统自带锁(pthread-mutex)是非公平锁,要实现公平锁,就需要一些额外的数据结构来支持(比如需要有办法记录每个线程的阻塞等待时间)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狗哥不是甜妹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值