Java线程池

什么是线程?

线程是指进程中的一个执行流程,一个进程可以运行多个线程。比如java.exe进程可以运行很多线程。线程总是输入某个进程,进程中的多个线程共享进程的内存。

线程的创建/获取方式

1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
4.线程池获取

实现Callable接口和实现Runnable接口有什么不同?

相同点:

两者都是接口;
两者都可用来编写多线程程序;
两者都需要调用Thread.start()启动线程;

不同点:

两者最大的不同点是:实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果;
Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛;
Callable接口的应用场景一般就在于批处理业务,比如转账的时候,需要给一会返回结果的状态码回来,代表本次操作成功还是失败

注意点:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!

Callable的使用
1.FutureTask类,他实现了Runnable接口,并且还需要传递一个实现Callable接口的类作为构造函数

// FutureTask:实现了Runnable接口,构造函数又需要传入 Callable接口
// 这里通过了FutureTask接触了Callable接口
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());

2.然后在用Thread进行实例化,传入实现Runnable接口的FutureTask的类

Thread t1 = new Thread(futureTask, "aaa");
t1.start();

3.最后通过 FutureTask.get() 获取到返回值

// 输出FutureTask的返回值
System.out.println("result FutureTask " + futureTask.get());

线程的生命周期

1.新建:刚使用new方法,new出来的线程;

2.就绪:调用线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢到CPU资源,谁开始执行;

3.运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;

4.阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;

5.销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;
在这里插入图片描述

什么是线程池?

线程池中,当需要使用线程时,会从线程池中获取一个空闲线程,线程完成工作时,不会直接关闭线程,而是将这个线程退回到池子,方便其它人使用。

简而言之,使用线程池后,原来创建线程变成了从线程池获得空闲线程,关闭线程变成了向线程池归还线程。

线程池的好处

1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的性能消耗。

2.提高响应速度。当任务到达时,任务可以不需要等待线程创建,可以直接执行。

3.提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的执行流程

七大参数

1.corePoolSize:核心线程数,线程池中的常驻核心线程数
在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中

2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
相当有扩容后的线程数,这个线程池能容纳的最多线程数

3.keepAliveTime:多余的空闲线程存活时间
当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余的空闲线程会被销毁,直到只剩下corePoolSize个线程为止
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用

4.unit:keepAliveTime的单位

5.workQueue:任务队列,被提交的但未被执行的任务(类似于银行里面的候客区)
LinkedBlockingQueue:链表阻塞队列
SynchronousBlockingQueue:同步阻塞队列

6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可

7.handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝请求执行的Runnable的策略

拒绝策略

以下所有拒绝策略都实现了RejectedExecutionHandler接口

1.AbortPolicy:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行

2.DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案

3.CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者

4.DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务

执行流程

在这里插入图片描述
在这里插入图片描述
1.在创建了线程池后,等待提交过来的任务请求

2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断

- 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
- 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行

3.当一个线程完成任务时,它会从队列中取下一个任务来执行

4.当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:

- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小

线程池中线程如何复用?

什么是线程复用?

在线程池中,通过同一个线程去执行不同的任务,这就是线程复用。

假设现在有 100 个任务,我们创建一个固定线程的线程池(FixedThreadPool),核心线程数和最大线程数都是 3,那么当这个 100 个任务执行完,都只会使用三个线程。

线程复用原理

在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。

通过取 Worker 的 firstTask 或者 getTask 方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。

线程池接口类

Executor是一个顶级接口,它里面只声明一个方法:execute(Runnable command),用来执行传进去的任务。
ExecutorService接口继承了Executor接口,并声明了一些方法:submit、shutdown、invokeAll等。
AbstractExecutorService抽象类实现了ExecutorService接口,基本实现了ExecutorService接口的所有方法。
ThreadPoolExecutor继承了类AbstractExecutorService。
在这里插入图片描述

Executors框架提供的各种类型的线程池

[ newSingleThreadExecutor()方法 ]:单线程线程池

这类线程池内部只有一个核心线程,以无界队列方式来执行该线程。

这使得这些任务之间不需要处理线程同步的问题,它确保所有的任务都在同一个线程中按顺序中执行,并且可以在任意给定的时间不会有多个线程是活动的。

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		ExecutorService mSingleThreadPool = Executors.newSingleThreadExecutor();     
        for(int i = 0;i < 7;i++) {
        	final int number = i;
        	mSingleThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("现在的时间:"+System.currentTimeMillis()+"第"+number+"个线程");
	                try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					
				}
			});
        	
        }
 
	}
 
}

[ newCachedThreadPool()方法 ]:可缓存线程池

该方法返回一个可根据实际情况调整线程数量的线程池,线程池的线程数量不确定,但若有空闲线程可以复用,则会有优先使用而可以复用线程。若所有线程均在工作,又有新的任务提交,则会创建新的现场处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

其最大线程数为Integer.MAX_VALUE,这个数是很大的,一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。但是线程池中的空闲线程都有超时限制,这个超时时长是60秒,超过60秒闲置线程就会被回收。调用execute将重用以前构造的线程(如果线程可用)。这类线程池比较适合执行大量的耗时较少的任务,当整个线程池都处于闲置状态时,线程池中的线程都会超时被停止。

public class PoolExecutorTest {
 
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		ExecutorService mCachelThreadPool = Executors.newCachedThreadPool();
		
		for(int i = 0;i < 7;i++ ) {
			final int index = i;
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			mCachelThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("第" +index +"个线程" +Thread.currentThread().getName()); 
				}
			});
			
		}
		
 
	}
 
}

[ newFixedThreadPool()方法 ]:定长线程池

该方法返回一个固定线程数量的线程池。

当有一个新的任务提交时,线程池中若有空闲线程,则立即处理。若无空闲线程,则将提交的任务存入到池队列(没有大小限制)中

当线程处于空闲状态时,它们并不会被回收,除非线程池被关闭了。

由于newFixedThreadPool只有核心线程并且这些核心线程不会被回收,这样它更加快速的响应外界的请求。

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		//设置最大线程数5个
		ExecutorService mFixedThreadPool = Executors.newFixedThreadPool(5);
		
		for(int i = 0;i < 7;i++ ) {
			final int index = i;
			mFixedThreadPool.execute(new Runnable() {
				
				@Override
				public void run() {
					System.out.println("时间是:"+System.currentTimeMillis()+"第" +index +"个线程" +Thread.currentThread().getName()); 
					try {
						Thread.sleep(2000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}	
				 }
			});
			
		}
		
 
	}
 
}

[ newScheduledThreadPool()方法 ]:周期性执行任务的线程池

ScheduleExecutorService接口在ExecutorService接口之上扩张了在给定时间执行某任务的功能,如:在某个固定的延时之后执行,或者周期性执行某个任务。

创建一个线程池,它的核心线程数量是固定的,而非核心线程数是没有限制的,并且当非核心线程闲置时会被立即回收,它可安排给定延迟后运行命令或者定期地执行。这类线程池主要用于执行定时任务和具有固定周期的重复任务

public class PoolExecutorTest {
 
	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		
		//设置池中核心数量是2
        ScheduledExecutorService mScheduledThreadPool = Executors.newScheduledThreadPool(2);  
        System.out.println("现在的时间:"+System.currentTimeMillis());
        mScheduledThreadPool.scheduleAtFixedRate(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				System.out.println("现在的时间:"+System.currentTimeMillis());
				
			}
		}, 2, 3,TimeUnit.SECONDS);//这里设置延迟2秒后每3秒执行一次
		
 
	}
 
}

为什么不用默认创建的线程池?

  • 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
    • 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
  • 线程池不允许使用Executors去创建,而是通过ThreadToolExecutors的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
    • Executors返回的线程池对象弊端如下:
      • FixedThreadPool和SingleThreadPool:
        • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
      • CacheThreadPool和ScheduledThreadPool
        • 运行的请求队列长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页