创建线程池不是说现用先创建,而是要是可以复用线程池中的线程,就很好地避免了大量用户态和内核态的交互,不需要频繁的创建和销毁线程
一)什么是池化技术?什么是线程池?
1)池化技术是提前准备好一些资源,在需要的时候可以重复使用这些准备好的资源,池化技术的优点主要有两个:提前使用和重复利用
2)线程池会先启动若干数量的线程,这些线程都会处于睡眠状态,当有一个新的任务到来的时候,线程池就会唤醒某一个睡眠的线程,让他来进行处理客户端的请求,当处理完整个请求之后,线程又处于睡眠的状态,线程池可以很好地提高程序执行的性能,因为频繁的创建和销毁线程是很浪费时间的;
3)现在假设有一个大型的网络中心,高峰期每一秒的客户端请求的并发数超过100,如果为每一个客户端都创建一个线程,那么耗费的CPU资源和内存资源都是非常惊人的,假设如果现在拥有一个数量是200的线程池,那么将会节约大量的系统资源和内存资源,使得有更多的CPU资源和内存资源来处理实际的业务,而不是把这些资源放在创建和销毁线程上;
1)复用线程:降低资源消耗
咱们的线程创建是需要开辟虚拟机栈,本地方法栈,程序计数器等一些私有的线程的内存空间,而销毁的时候又要回收这些私有空间资源,线程就可以重复的进行使用了,使得有更多的CPU资源和内存资源放在实际的业务处理上
2)提高响应速度
咱们的线程池使用已有线程来进行执行任务的,而线程当有人来的时候,现使用现创建,涉及到大量用户态和内核态的交互,所以说线程池可以更快的响应,就已经有PCB了;
3)管理线程数和任务数:线程池提供了更多的管理功能
3.1)控制最大并发数:控制最大并发线程数,线程池可以创建固定的线程数,从而避免了无限创建线程的问题,当线程创建过多的时候,会出现系统执行变慢,因为CPU核数是固定的,能够处理的任务数量也是一定的,但是当线程过多的时候,就会造成线程恶意争抢和线程频繁切换的问题,从而导致程序执行效率变慢,所以合适数量的线程才是高性能运行的关键
3.2)控制任务最大数:如果任务无限多,而内存又不足,就会导致程序执行报错,所以我们采用拒绝策略来进行处理多出的任务,从而保证系统可以健康的运行;
4)提供定期执行的功能
其他的常见的池化技术:
1)内存池:
1.1)内存池在进行创建的过程中,首先在程序启动的过程中,会向操作系统申请一大块内存
1.2)然后每次用户请求内存的时候,就会返回内存池中的一块空闲的内存,并将这块内存的标志置为true,标记为已经使用;
1.3)当内存使用完毕释放内存的时候,也不是真正地调用 free 或 delete 的过程,而是把内存放回内存池的过程,且放回的过程要把标志置为false;
1.4)最后,应用程序结束就会将内存池销毁,将内存池中的每一块内存释放
优点:减少了内存碎片的产生,提高了内存的使用频率
缺点:会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到
2)数据库连接池:
2.1)数据库连接池的基本思想是在系统初始化的时候将数据库连接作为对象存储在内存中,当用户需要访问数据库的时候,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象;
2.2)在使用完毕后,用户也不是将连接关闭,而是将连接放回到连接池中,以供下一个请求访问使用,而这些连接的建立、断开都是由连接池自身来管理的
2.3)同时还可以设置连接池的参数来控制连接池中的初始连接数、连接的上下限数和每个连接的最大使用次数、最大空闲时间等。当然,也可以通过连接池自身的管理机制来监视连接的数量、使用情况等
二)谈谈线程池的参数:
1)这个核心线程数如果设置成0,那么表示没有任务的时候销毁线程池,如果核心线程数大于0表示线程池即使没有任务的时候最少的线程池的数量等于这个值,如果设置比较小,那么会频繁的创建和销毁线程,如果创建的值比较大,那么会浪费大量的系统资源
2)如果线程池空闲的时间超过了此时间,那么多余的线程就会被销毁,直到线程池中的线程的销毁数量等于corepoolsize,如果本身corepoolsize等于maxmunsize,那么线程池在空闲的时候也不会销毁任何线程
3)通过线程工厂可以设置线程的名字,线程的优先级,线程的类型是用户线程还是守护线程
ThreadFactory dactory=new ThreadFactory(){}在里面重写newThread方法里面有一个参数Runnable r,返回值是Thread,可以根据这个参数来创建线程
4)线程的优先级可以使用整数来进行表示,范围是从1到10,数字越大表示优先级越高,,需要注意的是现成的优先级越高表名该线程在竞争CPU资源的时候越有可能被调度执行,但是现成的优先级在不同的操作系统或者是JAVA虚拟机上可能会有所不同,所以现成的优先级仅仅是给操作系统一个提示,告诉它应该先调度哪一个线程,但是操作系统不会严格按照此要求来调度线程,在JAVA中可以使用setPriority()和getPriority()来设置和获取线程的优先级
public class ExectorOperator{ public static void main(String[] args) throws InterruptedException { ThreadFactory factor=new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread=new Thread(r); thread.setDaemon(false);//设置为守护线程 thread.setName("李佳伟的线程"); thread.setPriority(Thread.MAX_PRIORITY); return thread; } }; ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(10),factor); executor.submit(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"正在运行"); } }); //Thread.sleep(100000); } }
三)如何设置corePoolsize 和maximumpoolsize之间的关系呢?
1)如果解决的任务和任务量比较稳定,我们会把corePoolsize和maximumpoolsize
之间的关系设得会比较接近,让临时工尽量少一些;
2)但是如果解决的任务场景,任务量波动比较大,可以让corePoolsize和maximumpoolsize之间的差值大一些,多多的进行招临时工,让临时工的数量多一些;
3)这个题太开放了,和机器,执行任务的类型有关,还是要通过实验来进行,从节省资源和执行效率的角度进行权衡,通过实验的方式来进行测试比较合适;
4)为什么线程池里面还要销毁线程?
1)这是在性能和资源之间在做平衡,线程完全不销毁,相当于是性能最大化,创建线程销毁线程的次数最少,但是吃的资源也是最多的,尤其是内存资源;
2)线程不消毁,线程越多,只是把性能跳到了最大限度,但是这些线程也会占用一定的资源,如果任务量更少,进程不干活,拜拜,把内存资源节省出来给其他工作去使用
3)其中的keepAlivetime,的时间越短,希望吃的资源越少,时间越短,就可以更快地释放资源,临时工线程就可以早早得进行走人,线程就可以及时地释放,把系统资源让出来;
进行使用多线程,不就是说为了让程序跑得更快吗?但是咱们为啥不让CPU占用率太高呢?
对于线上服务器来说,要对CPU资源留有一定的冗余,随时应对一些可能的突发情况,如果说请求已经本身已经快把CPU占用完了,这个时候突然来了一波请求的峰值,请求暴涨,此时我们的服务器很有可能就会挂
5)如何设置线程池中线程的个数?
有一个程序,这个程序要进行并发的,多线程来进行处理一些任务,如果使用线程池的话,这里面的线程数我们进行设置成多少是比较合适的呢?
1)针对这个问题,不能给出一个具体的值,只要是能够回答出一个具体的数字,那么一定是错误的
2)我们只能通过一系列的方法来找到一个具体的值,而不是说直接再进行测试之前,就直接进行拍板子一定就行的
正确的方式:要通过性能测试的方式,来找到合适的值
1)比如说可以写一个服务器程序,服务器里面通过线程池,多线程的处理用户请求
我们就可以针对这个服务器进行性能测试,比如说构造一些请求,发送给服务器,因为是要要进行测试性能,所以这里面的构造请求就需要构造很多,比如说每秒构造500/1000/2000请求,根据实际的业务场景,来进行构建一个合适的值;
2)我们再进行性能测试的时候,我们就可以根据不同的线程池中的线程数,来进行观察两个指标,程序处理任务的速度,程序持有的CPU的占用率
2.1)当我们的线程数多了,整体的速度会变快,但是CPU的占用率也会变高
2.2)当我们的线程数少了,整体的速度会变慢,但是CPU的占有率也会下降
我们需要在合适的情况下找到一个平衡点,所以说应该需要找到一个让程序速度可以接受,并且CPU占用也合理的这样一个平衡点,这才是我们正确的做法
2.2)因为不同类型的程序,因为单个任务,里面CPU的计算的时间和阻塞的时间分布是不相同的,因此光去拍脑门进行设置是不可以的,一定要进行性能测试
6)在JAVA中创建线程池有哪几种方式?
大体上创建线程池一共有两种方式,要么通过ThreadPoolExcutor创建线程池,要么通过Excutors执行器创建线程池
1)Executors.newFixedThreadPool:创建一个固定数量的线程池,可控制并发的线程数,超出的任务会在队列中等待,创建出一个固定线程数量的线程池,完全没有零时工的版本,参数指定了线程的个数
//1.创建一个固定大小的线程池,可以控制并发线程数 ExecutorService threadPool= Executors.newFixedThreadPool(2); Runnable runnable=new Runnable() { @Override public void run() { System.out.println("任务被执行"+Thread.currentThread().getName()); } }; threadPool.submit(runnable); threadPool.submit(runnable); threadPool.submit(runnable); threadPool.submit(runnable); //打印结果: 任务被执行pool-1-thread-2 任务被执行pool-1-thread-1 任务被执行pool-1-thread-2 任务被执行pool-1-thread-1 //先执行两个任务,剩余的任务直接进行排队执行2)Exectors.newCatchedThreadPool:创建出了一个数量可变的线程池,完全没有正式员工,全是临时工,是一个自动扩容的线程池,会根据任务量来自动进行扩容,如果线程数超过处理所需,那么多余的线程会进行缓存一会被回收,如果线程数不够就新创建线程
创建出了一个数量可变的线程池,完全没有正式员工,全是临时工,是一个自动扩容的线程池,会根据短时间内的任务量来自动进行扩容,适合短时间内有大量突发任务的场景
package OperateNode; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExectorOperator { public static void main(String[] args) { ExecutorService service=Executors.newCachedThreadPool(); for(int i=0;i<10;i++){ service.submit(new Runnable() { @Override public void run() { System.out.println("线程池正在创建任务"+Thread.currentThread().getName()); } }); } } } 打印结果: 线程池正在创建任务pool-1-thread-2 线程池正在创建任务pool-1-thread-1 线程池正在创建任务pool-1-thread-4 线程池正在创建任务pool-1-thread-3 线程池正在创建任务pool-1-thread-7 线程池正在创建任务pool-1-thread-5 线程池正在创建任务pool-1-thread-9 线程池正在创建任务pool-1-thread-6 线程池正在创建任务pool-1-thread-10 线程池正在创建任务pool-1-thread-8 从上面可以看出线程池创建了10个线程来进行执行相应的任务3)ScheduleExecutorServiceservice=Exectors.ScheduledThreadPool
newScheduleThreadPool:能够设定延长时间的线程池,插入的任务能够过一会再执行,相当于进阶版的定时器,带有定时器功能的线程池;
ScheduledExecutorService service=Executors.newScheduledThreadPool(5); //添加定时任务1s后执行 System.out.println(new Timestamp(System.currentTimeMillis())); service.schedule(new Runnable() { @Override public void run() { System.out.println("我是大佬"); } },1,TimeUnit.SECONDS); System.out.println(new Timestamp(System.currentTimeMillis())); } }4)Exectors.newSingleThreadScheduledExector
创建一个单线程并且可以执行延迟任务的线程池;
ScheduledExecutorService service =Executors.newSingleThreadScheduledExecutor(); service.schedule(new Runnable() { @Override public void run() { System.out.println("大佬"); } },2, TimeUnit.SECONDS); }5)Exectors.newSingleThreadExecutor:创建一个单个线程数的线程池,拥有任务队列,保证任务先进先出的执行顺序
package OperateNode; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class ExectorOperator { public static void main(String[] args) { ExecutorService service= Executors.newSingleThreadExecutor(); for(int i=0;i<10;i++){ final int index=i; service.submit(new Runnable() { @Override public void run() { System.out.println("当前任务"+index+"正在被执行"+Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } } 打印结果为: 当前任务0正在被执行pool-1-thread-1 当前任务1正在被执行pool-1-thread-1 当前任务2正在被执行pool-1-thread-1 当前任务3正在被执行pool-1-thread-1 当前任务4正在被执行pool-1-thread-1 当前任务5正在被执行pool-1-thread-1 当前任务6正在被执行pool-1-thread-1 当前任务7正在被执行pool-1-thread-1 当前任务8正在被执行pool-1-thread-1 当前任务9正在被执行pool-1-thread-1
public static void main(String[] args) { ExecutorService service= Executors.newSingleThreadExecutor(); for(int i=0;i<10;i++){ final int index=i; service.submit(new Runnable() { @Override public void run() { System.out.println("当前线程池正在运行第"+index+"个任务"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }那么单个线程的线程池有什么意义呢?
1)可以进行复用线程,即使是单个线程池也可以进行复用线程
2)提供了任务管理功能,单个线程池也有任务队列,在任务队列可以进行存储多个任务,并且当任务队列满了之后,保证任务顺序执行,可以执行拒绝策略,这些都是线程所不会具备的;
6)创建一个抢占式执行的线程池:Exectors.newWorkStealingPool:创建一个抢占式执行的线程池,就是任务执行顺序不确定,在JDK1.8中才可以进行使用
public class ExectorOperator { public static void main(String[] args) { ExecutorService service=Executors.newWorkStealingPool(); //下面是执行流程 for(int i=0;i<10;i++){ final int index=i; service.submit(new Runnable() { @Override public void run() { System.out.println(index+"被"+Thread.currentThread().getName()+"执行"); } }); } while(!service.isTerminated()){ //确保任务完成 } }}
7)为什么创建线程池一定要使用ThreadPoolExecutor?
ThreadPool是最原始也是最推荐的手动创建线程池的方式,可以通过参数来进行控制最大任务数和最大线程数和拒绝策略,让线程池的执行更加透明和可控,明确线程池的运行规则,规避资源耗尽的风险,都是从汤神最先教的前四个来说
1)FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM
2)CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
3)而默认情况下任务队列LinkedBlockingQueue 的存储容量是 Integer.MAX_VALUE,也有可能会导致内存溢出;
8)手动实现一个线程池
//3.描述一个线程,咱们的工作线程的任务就是循环的从任务队列中取出任务并进行执行,就能够拿到上面的队列去除就执行,取不到就进行阻塞 class Worker extends Thread{ public BlockingQueuequeue; public Worker(BlockingQueuequeue) { this.queue=queue; } public void run() { //我们要循环的来进行获取任务队列中的任务,只要我们任务队列中的任务不为空,那么我们就可以循环取到,就可以取出任务并执行 //如果队列为空,就一直进行阻塞,如果队列不为空,就获取到里面的任务 while (true) { try { Runnable runnable = queue.take(); runnable.run(); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class ThreadPool{ public ThreadPool(int n){ for(int i=0;i Worker worker=new Worker(queue); worker.start(); workers.add(worker); } } //1.描述一个任务,直接使用Runnable,我们不需要额外在进行创建类了 //2.我们使用一个数据结构来进行组织若干个任务 private BlockingQueuequeue=new LinkedBlockingQueue<>(); //3.我们在外已经创建了一个工作线程来进行描述任务 //4.我们通过list这个数据结构来进行组织若干个工作线程 Listworkers =new ArrayList<>(); //5.创建一个方法,允许我们的程序员来存放任务到我们的线程池里面 public void submit(Runnable runnable){ if(workers.size()<10){ Worker worker=new Worker(queue); worker.start(); workers.add(worker); } queue.add(runnable); } public void shutDown() throws InterruptedException { for(Thread t: workers){ t.interrupt(); } for(Thread t: workers){ t.join(); } } public static void main(String[] args) { ThreadPool pool=new ThreadPool(10); for(int i=0;i<100;i++){ pool.submit(new Runnable() { @Override public void run() { System.out.println("添加任务"); } }); } } }
9)谈谈工厂模式:
工厂模式主要是为了创建实例,给构造方法填坑,正经八百的创建实例是通过构造方法,应为构造方法的限制比较多:
class point//表示平面上的一个点 { public point(double x,double y){}//按照平面直角坐标系建立一个点,笛卡尔坐标 public point(double r,double a){}//按照极坐标方程来建一个点(有一个角度和半径的长度) }1)这两个方法的参数个数和类型都一样,无法编译,无法构成重载
2)构造方法的方法名都是固定的,无法体现出意义,也无法看出是通过哪一种方式来进行构,可读性比较差;
1)想要通过构造一个点的时候,想要通过一个平面坐标系来进行构造,也想要通过极坐标来进行构造,当前参数无法构成重载
2)不使用构造方法来创建实例,而是调用其他的方法创建实例,这样创建构建实例的方法,叫做工厂方法,通过工厂方法来构建出一个个的实例;
3)工厂方法其实就是一个普通的方法,在普通方法里面调用线程池的构造方法,上面的这几个工厂方法里面,调用了ThreadpoolExcutor的构造方法,同时对应的参数进行了传递,并返回这样的ThreadpoolExcutor的实例
4)工厂方法其实是普通的方法,在这个方法里面就会进行调用对应的构造方法,并进行一些初始化操作,并返回对象的实例,我们可以看到这些工厂方法的名字就是非常直观的
10)线程池中的工作线程出现异常怎么办?
在JAVA中线程池中的线程出现异常的时候,默认会将异常往外抛,同时这个工作线程会因为异常而销毁,需要程序员手动处理异常
1)再进行传递任务中处理异常,使用try catch来进行对任务进行包裹
2)使用Future来获取异常结果,线程池的提交,可以通过Future.get()来获取到异常的执行结果,如果在任务执行过程中出现异常,也会抛出一个ExecutionException,其中包含了任务i执行过程中的实际异常
public class MyThreadPool{ public static void main(String[] args) { BlockingQueuequeue=new LinkedBlockingQueue<>(); ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 12, 10,TimeUnit.SECONDS,queue, new ThreadFactory() { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("张三"); return t; } }, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println("我是自定义拒绝策略"); } }); Futurefuture=executor.submit(new Callable () { @Override public String call() throws Exception { System.out.println(10 / 0); return "我没有出现异常"; } }); String str= null; try { str = future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(str); }3)通过线程工厂给每一个新设置的线程设置一个setUncaughtExceptionHandler,这个处理器会在线程未捕获异常而即将被终止的时候被调用
public class MyThreadPool { public static void main(String[] args) { BlockingQueuequeue = new LinkedBlockingQueue<>(); ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 12, 10, TimeUnit.SECONDS, queue, new ThreadFactory() { public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setUncaughtExceptionHandler((Thread thread, Throwable e) -> { //捕获异常 System.out.println("出现异常"); }); return t; } }, new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println("我是自定义拒绝策略"); } }); executor.execute(new Runnable() { @Override public void run() { System.out.println(10 / 0); } }); } }11)谈谈阻塞队列:
1)阻塞队列要设计有界队列,不可以使用无界队列,因为如果使用无界队列,那么可能会导致任务过多从而导致内存溢出
2)阻塞队列是一种特殊的队列,它在普通队列的基础上,提供了两种附加的功能,当队列为空的时候获取队列中元素的消费者线程会被阻塞,同时会唤醒生产者线程去生产元素
3)当队列中的元素满了的时候,向队列中添加元素的生产者线程被阻塞,同时会唤醒消费者线程,其中阻塞队列能够容纳的元素个数通常情况下是有限的,比如说我们去实例化一个ArrayBlockingList,可以在构造方法中传入一个整形的数字,表示这个基于数组的阻塞队列中能够容纳的元素个数,这种队列就称之为有界队列,但是无界队列就是没有设置固定大小的队列,他的任务存储量很大,比如说LinkedBlockingQueue,他的默认队列长度是Integer.MAX_VALUE,所以程序员根本感知不到长度限制,无界队列存在比较大的潜在风险,如果在并发量比较大的情况下,线程池中几乎可以无限地去添加任务,容易导致存溢出的问题OOM,当队列为空的时候获取元素的线程会等待队列非空,当队列满的时候,存储元素的线程会等待队列可用;
4)实际上要想实现阻塞队列,就需要满足两个条件,队列元素的存储还有线程的阻塞和唤醒,而ArrayBlockingQueue是一个基于数组实现的阻塞队列,也就是说队列元素是存在于一个数组结构里面,因为数组的长度是有限制的,为了达到循环生产和循环消费的目的,ArrayBlockingQueue使用到了循环队列,为了实现线程的阻塞和唤醒,使用到了JUC包下面的ReentranLock和Condition;
一)解耦合:
最大的用处解耦合:写了两个代码,一个代码中的两个代码块的关联关系很复杂,这样耦合就比较高,两个模块的关联关系尽量小,简单,整体的代码是可以相互理解的,耦合比较低,
1)例如A要传输一定的数据给B,如果直接传输,此时就要求,要么是A向B传输数据,要么是B向A拉取数据,都是需要A和B进行相互交互的,A和B之间存在着一些关联关系,如果B挂了,A也就会有太大的影响;
2)在我们开发A代码的时候就必须充分了解B提供的一些接口,开发B代码的时候要充分了解A是怎么调用的;
3)未来如果需要进行扩展,扩展也搞一个C,让A也给C传输数据,这个改动就可能比较复杂,因为本来是A和B进行传输的,多了一个C,那么就是A想C传输数据,或者是C和B来向A拉取数据,改动比较复杂,就认为A和B的耦合比较高,B挂了,对A没啥影响
2)肖锋填谷
12)说说线程池的拒绝策略:
new ThreadPoolExecutor.discardPolicy
new ThreadPoolExecutor.discardOlestPolicy
new ThreadPoolExecutor.CallerrunsPolicy
new ThreadPoolExecutor.AbortPolicy
new RejectExecutionHandler重写RejectExecution方法
RejectedExecutionHandler
拒绝策略:当线程池的任务超过任务队列可以存储的最大值之后,执行的策略:
1)第一种拒绝策略是AbortPolicy(饿报它,泡了see),这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException属于RuntimeException,让你感知到任务被拒绝了,这是线程池的默认拒绝策略;
2)第二种拒绝策略是DiscardPolicy:当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失;
3)第三种拒绝策略是DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务
4)第四种拒绝策略是:CallerRunsPolicy(考乐runs泡了see):
调用当前的mian线程执行这个任务,把任务交给添加此任务的线程来进行执行
5)实现自定义拒绝策略:
实现自定义拒绝策略的时候要做什么?
1)首先将这个任务记录下来,后面追溯问题才能及时发现
2)给程序员发一个邮件
1)DisCardPolicy:忽略此任务,忽略最新的任务:
Runnable runnable=new Runnable() { @Override public void run() { System.out.println("当前线程被执行"+Thread.currentThread().getName()); } }; //线程池的默认策略:DiscardPolicy,忽略最新添加的任务,下面我们只创建只有一个线程的线程池 ThreadPoolExecutor executor=new ThreadPoolExecutor(1,1,100, TimeUnit.SECONDS,new LinkedBlockingQueue<>(1),new ThreadPoolExecutor.DiscardPolicy()); executor.submit(runnable); executor.submit(runnable); executor.submit(runnable); executor.submit(runnable);当前线程被执行pool-1-thread-1
当前线程被执行pool-1-thread-11)从上述结果可以看出给线程池添加了四个任务,当添加第一个任务的时候,先判断当前线程数是否小于核心线程数,当前线程数是0,新创建线程执行任务,此时线程数为1
2)此时我们再添加一个任务,先判断当前线程数是否大于核心线程数,不小于;那么将这个任务添加到阻塞队列
3)此时我们添加第三个任务,当前线程数不小于核心线程数,况且任务队列已经满了,况且当前线程数等于最大线程数,那么执行拒绝策略,直接进行忽略;
2)AbortPolicy拒绝策略:JDK的默认拒绝策略并且终止策略,线程池会抛出异常并终止执行此任务
public class ExectorOperator { public static void main(String[] args) { Runnable runnable=new Runnable() { @Override public void run() { System.out.println("当前线程被执行"+Thread.currentThread().getName()); } }; //线程池的默认策略:AbortPolicy,任务队列满了直接拒绝 ThreadPoolExecutor executor=new ThreadPoolExecutor(1,1,100, TimeUnit.SECONDS,new LinkedBlockingQueue<>(1),new ThreadPoolExecutor.AbortPolicy()); executor.submit(runnable); executor.submit(runnable); executor.submit(runnable); executor.submit(runnable); } }从上面结果可以看出,给线程池添加了四个任务,而线程池添加了4个任务,而线程池只执行了两个任务就结束了,其他两个任务执行了终止策略,并抛出了拒绝执行的异常
3)自定义拒绝策略:
当然除了JDK提供的四种拒绝策略之外,还可以通过实现new RejectedExecutionHandler,并重写rejectExecution方法来实现自定义拒绝策略
package OperateNode; import java.sql.Time; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ExectorOperator { public static void main(String[] args) { Runnable runnable=new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()+"正在执行"); } }; ThreadPoolExecutor executor=new ThreadPoolExecutor(1, 1, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println("我是拒绝策略"); } }); executor.submit(runnable); executor.submit(runnable); executor.submit(runnable); executor.submit(runnable); } }十三)线程池的执行流程:从线程池的执行方法execute方法说起
1)先进行判断当前线程数是否大于核心线程数,如果为false就新建线程(正式工)并执行任务
2)如果为true,在进行判断当前任务队列是否满了,如果为false,就把任务添加到任务队列里面等待线程执行
3)如果返回结果是true,在进行判断当前线程数是否大于最大线程数,结果是false,那么就新建线程执行此任务,否则就执行线程池的拒绝策略
十四)什么叫做后台线程
如果当前线程是后台线程,那么就不会影响进程退出
如果线程不是后台线程,是前台线程,就会影响到线程退出
1)咱们的创建的Thread t1和Thread t2都默认是前台的线程,即使我们的main方法都执行完毕,进程也是不能退出的,我们必须得等到t1,t2两个线程完全进行退出了之后,也就是说全部线程执行完了之后,咱们的整个进程才能进行退出
2)假设t1和t2是后台线程,此时如果main方法执行完毕,整个进程就会直接退出,t1和t2就会被强制直接进行终止了
在JAVA语言中,把线程分为两类:
1)一类是用户线程,一类是守护线程,在默认情况下所创建的线程或者是线程池都是用户线程,所以用户线程又被称之为普通线程
2)咱们如果说想要判断当前线程是否是用户线程,就要通过Thread.isDaemon()方法来进行判断,如果返回的是true,那么是守护线程,反之则为用户线程
3)下面我们来进行判断一下线程和线程池都属于哪一种线程类型?
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Teacher{ public static void main(String[] args) { Thread thread=new Thread(new Runnable() { @Override public void run() { } }); ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(100)); threadPoolExecutor.submit(new Runnable() { @Override public void run() { System.out.println("当前线程池里面的线程是"+(Thread.currentThread().isDaemon()==true?"守护线程":"用户线程")); } }); System.out.println("main线程的线程类型是"+(Thread.currentThread().isDaemon()?"守护线程":"用户线程")); } }1)守护线程又被称之为后台线程或者是服务线程,守护线程是为用户线程服务的,当我们程序中的用户线程全部执行完之后,守护线程也会随之结束
2)守护线程的角色就类似于服务员,但是用户线程的角色就类似于顾客,当顾客全部离开之后(也就是说咱们的用户线程全部执行完之后),随之服务员(守护线程)也就没有了存在的意义,
所以说当一个程序里面的全部用户线程执行完成之后,那么无论是用户线程是守护线程都会都会随着用户线程一起结束,整个程序也会随之终止运行
创建守护线程:
可以通过Thread.setDaemon(true)方法将线程设置为守护线程
public class Teacher{ public static void main(String[] args) { Thread t1=new Thread(){ public void run(){ System.out.println("生命在于运动"); } }; t1.setDaemon(true); System.out.println("t1线程是"+(t1.isDaemon()?"守护线程":"用户线程")); t1.start(); System.out.println("main线程是"+((!Thread.currentThread().isDaemon()?"用户线程":"守护线程"))); } }将线程池设置为守护线程:
本质上是把线程池中所有的线程都设置成守护线程,这个时候我们就需要线程工厂ThreadFactory来进行设置的,因为咱们线程池中的所有线程都是通过线程工厂来进行创建的
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class Teacher{ public static void main(String[] args) throws InterruptedException { //创建线程工厂,指定线程创建方式 ThreadFactory threadFactory=new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread=new Thread(r); thread.setDaemon(true); return thread; } }; //创建线程池 ThreadPoolExecutor executor=new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>(10),threadFactory); executor.submit(new Runnable() { @Override public void run() { System.out.println("当前线程池类型"+(Thread.currentThread().isDaemon()?"守护线程":"用户线程")); } }); Thread.sleep(2000); } }注意事项:
1)守护线程的设置setDaemo(true)必须要放置在线程的start()方法之前,否则程序会发生报错,况且守护线程的定义也不会生效
2)也就是说在运行线程之前,一定要先进行确定线程的类型,并且在线程运行之后是不可以修改线程的类型的,就算设置也不会生效
3)在JAVA里面,我们在默认情况下创建的线程和线程池都是用户线程,守护线程是为用户线程进行服务的,当一个程序中的所有用户线程都执行完毕之后程序就会结束运行,程序运行结束不会管守护线程是否正在运行
下面我们来进行演示一下:
Thread t1=new Thread(()->{ for(int i=0;i<10;i++){ System.out.println(i+"正在执行"); } }); t1.setDaemon(true); t1.start(); } //从这里看出程序是不会进行打印的,因为咱们的main线程执行完毕之后,守护线程会被强制退出1)多线程不是万能良药,不是说你上了多线程,你的速度就一定可以变快,如果说你本身创建线程的时间已经超过了任务执行的时间,那么多线程不会很快的
2)多线程本身还是特别适用于那一种CPU密集型的程序,就是我们程序需要进行大量的计算,使用多线程就可以更充分地利用CPU的多核资源
十五)ThreadLocal的原理以及内存泄漏问题
ThreadLocal也叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量针对于其他线程来说隔离的,这个变量时当前线程独有的变量
线程隔离:每一个线程的变量都是独立的不会相互影响
1)如果不去看源码的话,可能会猜测ThreadLocal是这样子设计的,每一个ThreadLocal都创建一个HashMap,让线程作为Map的key,要存储的局部变量座位Map的value,这样就可以达到每一个线程的局部变量的隔离的效果了
2)但是JDK后期改变了,每一个线程都存在着一个ThreadLocalMap,这个HashMap中的每一个元素叫做Entry,key就是ThreadLocal实例本身,而value就是最终存放的数据
2.1)每一个线程内部都有一个HashMap就是叫做ThreadlocalMap
2.2)Map里面存放的每一个元素叫做entry,里面的key就叫是ThreadLocal实例和现成的变量副本
局部变量生命周期只在当前方法内,ThreadLocal生命周期在整个线程内
从上面的实例中可以看到,两个线程分别的存储了自己的变量,并且他们变量的获取不会受到影响
1)ThreadLocal引用作为ThreadLocalMap集合中的一个Entry值的key属性,虽然所有的线程的ThreadLocalMap中的key是相同的,但是每一个线程都是拥有一个ThreadLocalMap
2)不同的线程有着不同的ThreadLocalMap,每一个ThreadLocalMap都有着一个Entry,key是固定的,但是value使每一个线程独有的;
3)ThreadLocal中的set方法:
public void set(T value) { //1获取当前线程的对象 Thread t = Thread.currentThread(); //2获取线程中的属性threadLocalMap,如果threadLocalMap不为空, //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值 ThreadLocalMap map = getMap(t); //getMap方法会t.getthreadlocals,这个threadlocals是一个成员变量本身就是一个ThreadlocalMap if (map != null) map.set(this,value);//底层类似于哈希表下标的添加元素操作 else //3.如果本身ThreadLocalMap不存在,那么会直接针对当前线程创建一个ThreadLocalMap并将ThreadLocal实例作为key,线程本身要存储的值作为value createMap(t,value); } t代表当前线程,this代表当前ThreadLocal实例,firstValue是要存放的局部变量 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }1)首先获取到当前线程并根据当前线程获取到ThreadLocalMap
2)如果获取到的Map不为空,那么直接将参数设置到Map中,ThreadLocal的引用作为key
3)如果Map为空,那么先初始化Map,然后设置初始值
4)ThreadLocal的get方法:
public T get() { //1.获取当前线程实例 Thread t = Thread.currentThread(); //2.获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t); //3.如果map数据不为空, if (map != null) { //3.1、获取threalLocalMap中存储的值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked"); //取出entry T result = (T)e.value; return result; } } //4.map本身不存在,表示当前线程没有维护的ThreadLocalMap对象 //map存在,但是没有和当前ThreadLocal关联的entry return setInitialValue(); } private T setInitialValue() { T value = initialValue();//默认返回null Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
代码的具体执行流程:
1)首先获取到当前线程,根据当前线程获取到ThreadLocalMap
2)如果获取到的Map不为空,如果Map为空,直接跳转到第四步,那么就在Map中根据ThreadLocal的引用来从Map中获取到相应的Entry,如果entry为空,直接跳转到第四步
3)如果entry不为空,直接返回entry.value
4)Map为空或者是当前的Entry为空,那么直接初始化Map设置初始值
5)remove方法:删除当前线程中保存的ThreadLocal对应的实体entry
6)InitialValue初始化方法
总结:ThreadLocal适用于每一个线程需要有自己单独的实例,实例需要在多个方法中共享但是不希望被多个线程共享
ThreadLocalMap底层实现:底层是使用Entry数组来实现的
1)ThreadLocalMap的构造方法:构造函数首先会创建一个长度是16的Entry数组,Entry中的key是ThreadLocal的引用,value是要存放的本地变量,然后计算出firstkey的索引,然后创建出一个Entry对象,然后存储到数组中;
key:threadlocal1,value:"张三"
key:threadlocal2,value:"李四"
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { //这里的firstkey指的是本地的ThreadLocal变量this,firstValue指的是需要保存的线程本地变量 //1.初始化table table = new Entry[INITIAL_CAPACITY]; //2.计算所存储的下标索引 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //3.设置Map的第一个键值对 table[i] = new Entry(firstKey, firstValue); //4.设置元素个数 size = 1; //5.设置阈值 setThreshold(INITIAL_CAPACITY); }2)ThreadLocal的set方法:
private void set(ThreadLocal> key, Object value) { Entry[] tab = table;//实际存放键值对的数组 int len = tab.length;//获取到数组的长度 int i = key.threadLocalHashCode & (len-1);//计算索引 //使用线性探测法设置元素 for (Entry e = tab[i];//先取出计算到对应位置的索引的entry e != null;//如果不为空就取下一个 e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get();//取下一个进行循环,获取到环形数组的下一个索引 if (k == key) {//如果目前ThreadLocal的实例和Entry中的我key已经相等直接覆盖value e.value = value; return; } if (k == null) {//为空直接创建新的 //当前的key为空但是值不为空说明之前的ThreadLocal对象已经被回收了 replaceStaleEntry(key, value, i);//用新元素替换旧元素 return; } } //新创建一个entry放入数组中,这个情况是ThreadLocal中的key不存在还没有找到陈旧的元素,那么直接在空元素的位置重新创建一个新的key tab[i] = new Entry(key, value); int sz = ++size; //判断是否需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
对于强引用来说,即使内存不够也会抛出异常,JVM也不会回收这个对象
ThreadLocal是会造成内存溢出的问题,内存溢出和内存泄漏是两个不同的概念,内存溢出是最终导致的一个结果,内存不够用的,当程序发生OOM的时候,内存泄漏是一个问题,这个问题并不会报错,是慢慢泄露的,内存泄漏会导致内存溢出;
因为Threadlocal里面存储的数据的生命周期是和线程的生命周期和线程池的生命周期是保持一致的,如果说在整个程序的运行期间,线程和线程池都没有进行销毁的情况下,那么ThreadLocal中的保存的数据也不会销毁或者被垃圾回收机制回收,那么这个问题就是内存泄漏的问题,当程序中有大量的ThreadLocal并且ThreadLocal中记录的都是大数据的情况下,有可能会造成OutOfMemory内存溢出的问题,就是ThreadLocal里面存的东西很大很多,而存储的这些数据的生命周期是和线程和线程池绑定在一起的,因为线程或者线程池在程序运行的过程中没有得到销毁,所以此时ThreadLocal也不会销毁,那么ThreadLocal里面的值会占用内存,虽然此时ThreadLocal里面的内容已经不用了,但是线程和线程池没有挂掉,所以ThreadLocal中的内容也不会销毁;
首先ThreadLocal在栈上面有一个地址,是ThreadLocal线程的引用地址,线程引用会指向堆里面的一个线程(new Thread()对象),每一个线程都有一个ThreadLocalMap的一个字典,每一个线程都对应着一个ThreadLocalMap,ThreadLocalMap里面存放的是多个Entry,是一个桶,这个桶里面有一个key值和value值,这个key就是ThreadLocal中的key(ThreadLocal的引用),这个key是一个弱引用,进行普通的GC也会回收的,只要发生GC,就一定会把key值回收掉的,但是value再进行设计的时候没有使用弱引用,那么在进行普通垃圾回收的时候只能回收key,而这个value属于强引用,是和具体的线程和线程池绑定在一起的;
key是弱引用被回收是为了减少内存泄露,防止程序员马虎被移除的,value存的是具体的值,这个value是线程直接持有的,这个value值是被放在线程的栈里面的,不能直接回收,是线程持有的,不能直接回收,当手动调用remove才会被回收,起码key值能被回收尽量能够腾出一些地方,万一value值以后还用怎么办?以后再存还是比较浪费时间
十六)CAS在JAVA中的实现类都有哪些?如何解决AtomicXXX的ABA问题呢?
首先先说一下CAS:
AtomicInteger atomicInteger=new AtomicInteger(0);底层通过CAS实现 atomicInteger.getAndIncrement();//i++ atomicInteger.IncrementAndGet();//++i atomicInteger.getAndDcrement();//i-- atomicInteger.decrementAndGet();/--i atomicInteger.addAndGet(data);i+=data System.out.println(atomicInteger);
下面介绍一下AtomicInteger的用法:
Atomic家族出了一个AtomicStampedReference类,它在使用的时候必须要加版本号,所以AtomicStampedReference类的使用是不存在ABA问题的
public class MyThreadPool{ public static void main(String[] args) { //1.创建一个初始值是100,初始版本号是0的AtomicStampedReference的对象 AtomicStampedReferencestampedReference=new AtomicStampedReference<>(1,10); //2.获取到当前的值和版本号 int currentValue=stampedReference.getStamp(); int oldVersion= stampedReference.getReference(); System.out.println("当前的值是"+currentValue); System.out.println("当前的版本号是"+oldVersion); //3.更新当前的值并且尝试将版本号+1 int newVersion=oldVersion+1; boolean flag=stampedReference.compareAndSet(oldVersion,newVersion,currentValue,1000); System.out.println(flag); System.out.println("当前数值是"+stampedReference.getStamp()); System.out.println("当前版本是"+stampedReference.getReference()); } }十七)FutureTask
FutureTask类的作用:
1)保存callable的返回值,并且后续可以通过get方法获取
2)将这个类关联到Thread类中,让线程进行工作
package com; class Result{ public int sum; public Object locker=new Object(); } public class Solution{ public static Result result=new Result(); public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { int sum = 0; for (int i = 0; i <= 1000; i++) { sum = sum + i; } synchronized (result.locker){ result.sum=sum; result.locker.notify(); } }); t.start(); synchronized (result.locker) { if (result.sum == 0) { result.locker.wait(); } } System.out.println(result.sum); } }
Callablecallable=new Callable () {//此时的泛型参数存放线程的返回值 @Override public Integer call() throws Exception,InterruptedException,ConcurrentModificationException{ int sum=0; for(int i=0;i<=1000;i++) { sum=sum+i; } return sum; } }; //此时不可以直接把Callable接口关联到Thread中,需要一个辅助的类来包装 FutureTaskfutureTask=new FutureTask<>(callable); //当我们创建出FutureTask这个实例的时候,此时Callable大概率还没有算完的,后面的结果还不知道呢 //当后面的Callable在线程中的代码执行完了之后,就会把这个结果写到FutureTask实例中保存结果 Thread thread=new Thread (futureTask); thread.start(); Integer result = futureTask.get(); System.out.println(result); //此时会在主线程获取结果,如果FutureTask中的结果还没有生成呢 //此时就会一直进入到阻塞等待,一直等到对应的线程计算出结果后,get才会返回