前言
初学者很容易看错,如果没有看到spring或者JUC源码的人肯定是不太了解的。
ThreadPoolTaskExecutor是spring core包中的,而ThreadPoolExecutor是JDK中的JUC。ThreadPoolTaskExecutor是对ThreadPoolExecutor进行了封装处理。
自己在之前写多线程代码的时候都是这么玩的executor=Executors.newCachedThreadPool();但是有一次在大量数据的时候由于入库速度远大于出库速度导致内存急剧膨胀最后悲剧了重写代码,原来spring 早就给我们做好封装了。
来看一下ThreadPoolExecutor结构,祖类都是调用Executor接口:
再来看一下ThreadPoolTaskExecutor结构,祖类都是调用Executor接口:
一、创建一个线程的三种方法
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
/**
* 方式二、实现Runnable
*/
public class RunnableTest implements Runnable {
@Override
public void run() {
System.out.println("启动一个线程");
}
public static void main(String[] args) {
Thread thread = new Thread(new RunnableTest());
thread.start();
}
}
- 通过继承 Thread 类本身;
/**
* 方式一、继承Thread
*/
public class ThreadDemo extends Thread{
@Override
public void run() {
System.out.println("启动一个线程");
}
public static void main(String[] args) {
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
}
}
- 通过 Callable 和 Future 创建线程。
/**
* 方式三、
* 通过 Callable 和 Future 创建线程
* 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
*
* 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
*
* 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
*
* 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
*/
public class CallableTest implements Callable<Integer> {
public static void main(String[] args) {
try {
CallableTest callableTest = new CallableTest();
FutureTask<Integer> futureTask = new FutureTask<>(callableTest);
for (int i=0;i<100;i++){
System.out.println(Thread.currentThread().getName()+"的循环变量i的值"+i);
if (i==20){
new Thread(futureTask,"有返回值的线程").start();
}
}
System.out.println("子线程的返回值:"+futureTask.get());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int i=0;
for (;i<100;i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
二、线程共有5大状态
①新建状态:新建线程对象,并没有调用start之前
②就绪状态:调用start方法之后就进入就绪状态,另外线程在睡眠和挂起中恢复的时候也会进入就绪状态
③运行状态:线程被设置为当前线程开始执行run方法
④阻塞状态:线程被暂停。比如调用sleep方法后
⑤死亡状态:线程执行结束 (一个线程结束的标志是:run()方法结束。)
三、Thread的几个重要方法
①start()方法:开始执行该线程
②stop()方法:强制结束该线程
③join()方法 :等待该线程结束( join() 方法主要是让调用该方法的thread完成run方法里面的东西后, 再执行join()方法后面的代码参考链接)
④sleep()方法:该线程进入等待,Thread.sleep(时间); 代表的是当前的线程休眠,在main方法中休眠的就是main方法这个线程
⑤run()方法 :直接执行该线程的run方法(线程调用start()也会执行run方法,区别是一个是由线程调度运行run 方法,一个是直接调用线程中的run方法)
注意:wait()和notify()是object中的方法,分别表示线程挂起和线程恢复 ,而notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。wait方法和notify方法的详解
wait()与sleep()的区别:wait()会释放对象锁,sleep()不会释放对象锁。所以当在一个Synchronized方法中调用sleep()时,线程虽然休眠了,但是对象的机锁没有被释放,其他线程仍然无法访问这个对象。而wait()方法则会在线程休眠的同时释放掉机锁,其他线程可以访问该对象。设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。
为什么wait和notify方法要在同步块中调用?
主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。
四、线程优先级
在java语言中,每个线程都有一个优先级,当线程调控器有机会选择新的线程时,线程的优先级越高越有可能先被选择执行。
在java里,线程的优先级可以设置1-10,数字越大代表优先级越高。默认情况下,一个线程继承它的父线程的优先级,即如果用A线程启动B线程,那么B线程的优先级就和A线程的优先级一致。也可以用setPriority方法提高或降低任一个线程的优先级。
但是,根据《JAVA核心技术》所讲的,线程的优先级是高度依赖于系统的。
举个具体的例子:
在windows系统中,有7个优先级级别,但是在java语言里是有10个优先级别的,不能做到一一对应,所以至少会有两个优先级共享相同的windows操作系统优先级。
而在Oracle为Linux提供的java虚拟机中,线程的优先级将被忽略,即所有线程具有相同的优先级。
所以,不要过度依赖优先级。
还有,并不是说A线程的优先级比B的高,A线程执行完了才会执行B才会执行,只是A线程的优先级比较高所以cpu分配的资源比较多一些。
public class myThread05 extends Thread {
public void run(){
long begin = System.currentTimeMillis();
for (int i=0;i<50000000;i++){
}
long end = System.currentTimeMillis();
long time=end-begin;
System.out.println(Thread.currentThread().getName()+ "优先级为:"+this.getPriority()+" 执行完毕,花费时间为"+ time); // getPriority()方法可以获取线程的优先级
}
public static void main(String[] args) throws InterruptedException {
myThread05 mt = new myThread05();
myThread05 mt1 = new myThread05();
myThread05 mt2 = new myThread05();
mt1.setPriority(1);//设置线程的优先级为1
mt2.setPriority(10); //设置线程的优先级10
mt.start(); //默认情况下线程的优先级为5
mt1.start();
mt2.start();
}
}
多次运行,结果也不太一样,说明不一定是优先级高就先运行,只是获得cpu资源的可能性更大。
所以不要过度依赖优先级
五、守护线程与非守护线程
用户线程即运行在前台的线程,而守护线程是运行在后台的线程。 守护线程作用是为其他前台线程的运行提供便利服务,而且仅在普通、非守护线程仍然运行时才需要,比如垃圾回收线程就是一个守护线程。当VM检测仅剩一个守护线程,而用户线程都已经退出运行时,VM就会退出,因为没有如果没有了被守护这,也就没有继续运行程序的必要了。如果有非守护线程仍然存活,VM就不会退出。
守护线程并非只有虚拟机内部提供,用户在编写程序时也可以自己设置守护线程。用户可以用Thread的setDaemon(true)方法设置当前线程为守护线程。
虽然守护线程可能非常有用,但必须小心确保其他所有非守护线程消亡时,不会由于它的终止而产生任何危害。因为你不可能知道在所有的用户线程退出运行前,守护线程是否已经完成了预期的服务任务。一旦所有的用户线程退出了,虚拟机也就退出运行了。 因此,不要在守护线程中执行业务逻辑操作(比如对数据的读写等)。、
六、ThreadPoolTaskExecutor 线程池使用
public ThreadPoolTaskExecutor taskExector() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//int i = Runtime.getRuntime().availableProcessors();//获取到服务器的cpu内核
executor.setCorePoolSize(5);//核心池大小
executor.setMaxPoolSize(15);//最大线程数
executor.setQueueCapacity(15);//队列程度
executor.setKeepAliveSeconds(1000);//线程空闲时间
executor.setThreadNamePrefix("tsak-asyn");//线程前缀名称
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//配置拒绝策略
executor.initialize();
return executor;
}
rejectedExecutionHandler字段用于配置拒绝策略,常用的拒绝策略如下:
- AbortPolicy,用于被拒绝任务的处理程序,它将抛出RejectedExecutionException。
- CallerRunsPolicy,用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
- DiscardOldestPolicy,用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
- DiscardPolicy,用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
其他说明:
- 为了实现某些特殊的业务需求,用户可以选择使用自定义策略,只需实现RejectedExecutionHandler接口即可。
- 建议配置线程前缀名称threadNamePrefix属性,出问题时可以更方便的进行排查。
- 提交任务
- 无返回值的任务使用execute(Runnable)
- 有返回值的任务使用submit(Runnable)
处理流程
- 当一个任务被提交到线程池时,首先查看线程池的核心线程是否都在执行任务,否就选择一条线程执行任务,是就执行第二步。
- 查看核心线程池是否已满,不满就创建一条线程执行任务,否则执行第三步。
- 查看任务队列是否已满,不满就将任务存储在任务队列中,否则执行第四步。
- 查看线程池是否已满,不满就创建一条线程执行任务,否则就按照策略处理无法执行的任务。
在ThreadPoolExecutor中表现为:
- 如果当前运行的线程数小于corePoolSize,那么就创建线程来执行任务(执行时需要获取全局锁)。
- 如果运行的线程大于或等于corePoolSize,那么就把task加入BlockQueue。
- 如果创建的线程数量大于BlockQueue的最大容量,那么创建新线程来执行该任务。
- 如果创建线程导致当前运行的线程数超过maximumPoolSize,就根据饱和策略来拒绝该任务。
关闭线程池
调用shutdown或者shutdownNow,两者都不会接受新的任务,而且通过调用要停止线程的interrupt方法来中断线程,有可能线程永远不会被中断,不同之处在于shutdownNow会首先将线程池的状态设置为STOP,然后尝试停止所有线程(有可能导致部分任务没有执行完)然后返回未执行任务的列表。而shutdown则只是将线程池的状态设置为shutdown,然后中断所有没有执行任务的线程,并将剩余的任务执行完。
配置线程个数
- 如果是CPU密集型任务,那么线程池的线程个数应该尽量少一些,一般为CPU的个数+1条线程。
- 如果是IO密集型任务,那么线程池的线程可以放的很大,如2*CPU的个数。
- 对于混合型任务,如果可以拆分的话,通过拆分成CPU密集型和IO密集型两种来提高执行效率;如果不能拆分的的话就可以根据实际情况来调整线程池中线程的个数。
监控线程池状态
常用状态:
- taskCount:线程需要执行的任务个数。
- completedTaskCount:线程池在运行过程中已完成的任务数。
- largestPoolSize:线程池曾经创建过的最大线程数量。
- getPoolSize获取当前线程池的线程数量。
- getActiveCount:获取活动的线程的数量
七、线程并发
synchronized
Lock
错误例子(产生了并发):这边每个线程都创建新的insertDemo对象,出现并发现象,多个线程同时进去了,因为synchronized(this)锁的是对象。如果改成synchronized(list)锁变量就不会出现并发现象了
public class RunnableDemo1 implements Runnable{
@Override
public void run() {
insertDemo demo = new insertDemo();
demo.insertDemo1(Thread.currentThread().getName());
}
}
class insertDemo{
static private List<Integer> list = new ArrayList<>();
void insertDemo1(String str){
synchronized(this){
try {
for (int i=0;i<10;i++){
System.out.println("执行");
Thread.sleep(1000);
System.out.println(str+":"+i);
list.add(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}}
}
synchronized void insertDemo2(String str){
for (int i=0;i<10;i++){
System.out.println(str+"对象2:"+i);
list.add(i);
}
}
}
class Yun{
public static void main(String[] args) {
/**
* 这边每个线程都创建新的insertDemo对象,出现并发现象,多个线程同时进去了
*/
RunnableDemo1 runnableDemo1 = new RunnableDemo1();
new Thread(runnableDemo1,"线程1").start();
new Thread(runnableDemo1,"线程2").start();
new Thread(runnableDemo1,"线程3").start();
}
}
解决了并发:
public class RunnableDemo1 implements Runnable{
private insertDemo demo;
public RunnableDemo1(insertDemo demo) {
this.demo=demo;
}
@Override
public void run() {
demo.insertDemo1(Thread.currentThread().getName());
}
}
class insertDemo{
static private List<Integer> list = new ArrayList<>();
void insertDemo1(String str){
synchronized(this){
try {
for (int i=0;i<10;i++){
System.out.println("执行");
Thread.sleep(1000);
System.out.println(str+":"+i);
list.add(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}}
}
synchronized void insertDemo2(String str){
for (int i=0;i<10;i++){
System.out.println(str+"对象2:"+i);
list.add(i);
}
}
}
class Yun{
public static void main(String[] args) {
/**
* 这样对象才是一个,锁的对象也是同一个
*/
insertDemo demo = new insertDemo();
RunnableDemo1 runnableDemo1 = new RunnableDemo1(demo);
new Thread(runnableDemo1,"线程1").start();
new Thread(runnableDemo1,"线程2").start();
new Thread(runnableDemo1,"线程3").start();
}
}