关于Java多线程 线程池应用的思考与整理 ThreadPoolExecutor类

关于线程

线程创建方式

继承Thread类

可以通过创建一个继承Thread的类,通过实现其run方法
a.一个线程调用 两次start()方法将会抛出线程状态异常,也就是的start()只可以被调用一次
b.native生明的方法只有方法名,没有方法体。是本地方法,不是抽象方法,而是调用c语言方法
registerNative()方法包含了所有与线程相关的操作系统方法
c. run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)

public class ThreadDemo extends Thread {
	public ThreadDemo() {
		
	}
	public void run() {
		for(int i=0;i<10;i++) {
			System.out.println(Thread.currentThread()+":"+i);
		}
	}
	public static void main(String[] args) {
		ThreadDemo thread1=new ThreadDemo();
		ThreadDemo thread2=new ThreadDemo();
		thread1.start();
		thread2.start();
	}
}

覆写Runnable()接口实现多线程

a.覆写Runnable接口实现多线程可以避免单继承局限
b.当子类实现Runnable接口,此时子类和Thread的代理模式(子类负责真是业务的操作,thread负责资源调度与线程创建辅助真实业务。

   public class MyThread implements Runnable{
	public static int count=20;
	public void run() {
		while(count>0) {
			try {
				Thread.sleep(200);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName()+"-当前剩余票数:"+count--);
		}
	}
	public static void main(String[] args) {
		MyThread Thread=new MyThread();
		Thread mThread1=new Thread(Thread,"线程-1");
		Thread mThread2=new Thread(Thread,"线程-2");
		Thread mThread3=new Thread(Thread,"线程-3");
		mThread1.start();
		mThread2.start();
		myThread3.start();
	}
}

继承Thread和实现Runnable接口的区别
a.实现Runnable接口避免多继承局限
b.实现Runnable()可以更好的体现共享的概念

覆写Callable接口实现多线程(JDK1.5)

a.核心方法叫call()方法,有返回值
b.有返回值

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class MyThread implements Callable<String> {
	private int count = 30;
 
	@Override
	public String call() throws Exception {
		for (int i = count; i > 0; i--) {
//			Thread.yield();
			System.out.println(Thread.currentThread().getName()+"当前票数:" + i);
		}
		return "sale out";
	} 
 
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		Callable<String> callable  =new MyThread();
		FutureTask <String>futureTask=new FutureTask<>(callable);
		Thread mThread=new Thread(futureTask);
		Thread mThread2=new Thread(futureTask);
		Thread mThread3=new Thread(futureTask);
//		mThread.setName("hhh");
		mThread.start();
		mThread2.start();
		mThread3.start();
		System.out.println("result:" + futureTask.get());
	}
}

线程池实现(推荐)

关于阿里开发手册1.4的一些内容
【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯

public class TimerTaskThread extends Thread { 
public TimerTaskThread() { 
	super.setName("TimerTaskThread");
		...	
	} 
}

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决
资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或
者“过度切换”的问题。

我们也看到了,在阿里1.4的JAVA开发手册中明确规定了不允许使用线程池以外的形式去创建线程,我们在生产中创建线程一般也只使用这一种方式。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样
的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

一般情况我们生产中使用ThreadPoolExecutor,但是为了了解ThreadPoolExecutor,我们先列举一下Executors的三种使用情况
此段参考了,感谢

https://blog.csdn.net/weixin_41891854/article/details/81265772

我们先看下线程池的定义

/**
 * Creates a thread pool that reuses a fixed number of threads
 * operating off a shared unbounded queue.  At any point, at most
 * {@code nThreads} threads will be active processing tasks.
 * If additional tasks are submitted when all threads are active,
 * they will wait in the queue until a thread is available.
 * If any thread terminates due to a failure during execution
 * prior to shutdown, a new one will take its place if needed to
 * execute subsequent tasks.  The threads in the pool will exist
 * until it is explicitly {@link ExecutorService#shutdown shutdown}.
 *
 * @param nThreads the number of threads in the pool
 * @return the newly created thread pool
 * @throws IllegalArgumentException if {@code nThreads <= 0}
 */
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

简要说明,FixedThreadPool,也就是可重用固定线程数的线程池。 它corePoolSize和 maximumPoolSize是一样的。并且他的keepAliveTime=0, 也就是当线程池中的线程数大于corePoolSize, 多余的空闲线程会被***立即***终止。

它的基本执行过程如下
1, 如果当前运行的线程数少于corePoolSize, 会立刻创建新线程执行任务。
2,当线程数到达corePoolSize后,将任务加入到LinkedBlockingQueue中。
3,当线程执行完任务后,会循环从LinkedBlockingQueue中获取任务来执行。

FixedThreadPool使用了LinkedBlockingQueue, 也就是无界队列(队列最大可容纳Integer.MAX_VALUE), 因此会造成以下影响:
a, 线程池线程数到达corePoolSize后,任务会被存放在LinkedBlockingQueue中
b, 因为无界队列,运行中(未调用shutdown()或者shutdownNow()方法)的不会拒绝任务(队列无界,可以放入"无限"任务)

FixThreadPool(int n); 固定大小的线程池

使用于为了满足资源管理需求而需要限制当前线程数量的场合。使用于负载比较重的服务器。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Test {
	public static void main(String[] args) {
		ExecutorService ex=Executors.newFixedThreadPool(5);
		
		for(int i=0;i<5;i++) {
			ex.submit(new Runnable() {
				
				@Override
				public void run() {
					for(int j=0;j<10;j++) {
						System.out.println(Thread.currentThread().getName()+j);
					}
				}
			});
		}
		ex.shutdown();
	}	
}

实现

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

corePoolSize与maximumPoolSize相等,即其线程全为核心线程,是一个固定大小的线程池,是其优势;
keepAliveTime = 0 该参数默认对核心线程无效,而FixedThreadPool全部为核心线程; workQueue
为LinkedBlockingQueue(无界阻塞队列),队列最大值为Integer.MAX_VALUE。如果任务提交速度持续大余任务处理速度,会造成队列大量阻塞。因为队列很大,很有可能在拒绝策略前,内存溢出。是其劣势;
FixedThreadPool的任务执行是无序的;

适用场景:可用于Web服务瞬时削峰,但需注意长时间持续高峰情况造成的队列阻塞。

SingleThreadPoolExecutor :单线程池

需要保证顺序执行各个任务的场景

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Test {
	public static void main(String[] args) {
		ExecutorService ex=Executors.newSingleThreadExecutor();
		
		for(int i=0;i<5;i++) {
			ex.submit(new Runnable() {
				
				@Override
				public void run() {
					for(int j=0;j<10;j++) {
						System.out.println(Thread.currentThread().getName()+j);
					}
					
				}
			});
		}
		ex.shutdown();
	}	
}

实现

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

咋一瞅,不就是newFixedThreadPool(1)吗?定眼一看,这里多了一层FinalizableDelegatedExecutorService包装,这一层有什么用呢,写个dome来解释一下:

    public static void main(String[] args) {
        ExecutorService fixedExecutorService = Executors.newFixedThreadPool(1);
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) fixedExecutorService;
        System.out.println(threadPoolExecutor.getMaximumPoolSize());
        threadPoolExecutor.setCorePoolSize(8);
        
        ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
//      运行时异常 java.lang.ClassCastException
//      ThreadPoolExecutor threadPoolExecutor2 = (ThreadPoolExecutor) singleExecutorService;
    }

对比可以看出,FixedThreadPool可以向下转型为ThreadPoolExecutor,并对其线程池进行配置,而SingleThreadExecutor被包装后,无法成功向下转型。因此,SingleThreadExecutor被定以后,无法修改,做到了真正的Single。

CachedThreadPool(); 缓存线程池

当提交任务速度高于线程池中任务处理速度时,缓存线程池会不断的创建线程
适用于提交短期的异步小程序,以及负载较轻的服务器

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class Test {
	public static void main(String[] args) {
		ExecutorService ex=Executors.newCachedThreadPool();
		
		for(int i=0;i<5;i++) {
			ex.submit(new Runnable() {
				
				@Override
				public void run() {
					for(int j=0;j<10;j++) {
						System.out.println(Thread.currentThread().getName()+j);
					}
				}
			});
		}
		ex.shutdown();
	}	
}

实现

 public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
keepAliveTime = 60s,线程空闲60s后自动结束。 workQueue 为 SynchronousQueue
同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;

适用场景:快速处理大量耗时较短的任务,如Netty的NIO接受请求时,可使用CachedThreadPool。

线程之间状态

在这里插入图片描述

ThreadPoolExecutor 生产中创建线程

参考文章

https://www.jianshu.com/p/f030aa5d7a28

我们先看一下ThreadPoolExecutor的构造方法
在这里插入图片描述

 public ThreadPoolExecutor(int corePoolSize, // 1
                              int maximumPoolSize,  // 2
                              long keepAliveTime,  // 3
                              TimeUnit unit,  // 4
                              BlockingQueue<Runnable> workQueue, // 5
                              ThreadFactory threadFactory,  // 6
                              RejectedExecutionHandler handler ) { //7
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
序号名称类型含义
1corePoolSizeint核心线程池大小
2maximumPoolSizeint最大线程池大小
3keepAliveTimelong线程最大空闲时间
4unitTimeUnit时间单位
5workQueueBlockingQueue线程等待队列
6threadFactoryThreadFactory线程创建工厂
7handlerRejectedExecutionHandler拒绝策略

深入内容可以学习一下该博主的其他两篇

https://www.jianshu.com/p/c41e942bcd64
https://www.jianshu.com/p/23cb8b903d2c
队列模式

在编写自定义线程池之前,我们先了解一下几种常用的队列模式

直接提交 SynchronousQueue

它将任务直接提交给线程而不保存它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 SynchronousQueue线程安全的Queue,可以存放若干任务(但当前只允许有且只有一个任务在等待),其中每个插入操作必须等待另一个线程的对应移除操作,也就是说A任务进入队列,B任务必须等A任务被移除之后才能进入队列,否则执行异常策略。你来一个我扔一个,所以说SynchronousQueue没有任何内部容量。

   比如:核心线程数为2,最大线程数为3;使用SynchronousQueue。

   当前有2个核心线程在运行,又来了个A任务,两个核心线程没有执行完当前任务,根据如果运行的线程等于或多于 corePoolSize,

  则 Executor 始终首选将请求加入队列,而不添加新的线程。所以A任务被添加到队列,此时的队列是SynchronousQueue,

  当前不存在可用于立即运行任务的线程,因此会构造一个新的线程,此时又来了个B任务,两个核心线程还没有执行完。

   新创建的线程正在执行A任务,所以B任务进入Queue后,最大线程数为3,发现没地方仍了。就只能执行异常策略(RejectedExecutionException)。

代码示例:

package zmx.jdkthreadpool.test;
 
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class PoolForSynchronousQueueTest {
	
	public static void main(String[] args) {
	      
	        ThreadPoolExecutor pool =  new ThreadPoolExecutor(2, 3, 3L, 
		                    TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadPoolExecutor.AbortPolicy());
	        for (int i = 0; i < 4; i++) {
	            pool.execute(new Runnable() {
	                @Override
	                public void run() {
		                	for (int i = 0; i < 10; i++) {
		    	                System.out.println(Thread.currentThread().getName() + " 执行 - " + i);
		    		        }
		                    System.out.println("run");
		                }
		            });
		    }
		    pool.shutdown();
   }
}
// 在添加第4个任务的时候,程序抛出 java.util.concurrent.RejectedExecutionException</span>

运行结果:

pool-1-thread-2 执行 - 0
run
pool-1-thread-1 执行 - 0
run
pool-1-thread-3 执行 - 0
run
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task zmx.jdkthreadpool.test.PoolForSynchronousQueueTest$1@bda96b rejected from java.util.concurrent.ThreadPoolExecutor@4e4b9101[Running, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]
 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2048)
 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:821)
 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1372)
 at zmx.jdkthreadpool.test.PoolForSynchronousQueueTest.main(PoolForSynchronousQueueTest.java:14)
无界队列 如LinkedBlockingQueue

使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有核心线程都在忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就没意义了。)也就不会有新线程被创建,都在那等着排队呢。如果未指定容量,则它等于 Integer.MAX_VALUE。如果设置了Queue预定义容量,则当核心线程忙碌时,新任务会在队列中等待,直到超过预定义容量(新任务没地方放了),才会执行异常策略。你来一个我接一个,直到我容不下你了。FIFO,先进先出。

    比如:核心线程数为2,最大线程数为3;使用LinkedBlockingQueue(1),设置容量为1。

    当前有2个核心线程在运行,又来了个A任务,两个核心线程没有执行完当前任务,根据如果运行的线程等于或多于 corePoolSize,

    则 Executor 始终首选将请求加入队列,而不添加新的线程。所以A任务被添加到队列,此时的队列是LinkedBlockingQueue,

    此时又来了个B任务,两个核心线程没有执行完当前任务,A任务在队列中等待,队列已满。所以根据如果无法将请求加入队列,则创建新的线程,

     B任务被新创建的线程所执行,此时又来个C任务,此时maximumPoolSize已满,队列已满,只能执行异常策略(RejectedExecutionException)。

代码示例:

package zmx.jdkthreadpool.test;
 
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class PoolForLinkedBlockingQueueTest {
	
	
	public static void main(String[] args) {
	      
        ThreadPoolExecutor pool =  new ThreadPoolExecutor(2, 3, 3L, 
	                            TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1),  
	                            new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 5; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
	                	for (int i = 0; i < 10; i++) {
	    	                System.out.println(Thread.currentThread().getName() + " 执行 - " + i);
	    		        }
	                    System.out.println("run");
	                }
	            });
	    }
	    pool.shutdown();
    }
}

运行结果:

pool-1-thread-1 执行 - 0
pool-1-thread-2 执行 - 0
run
run
pool-1-thread-2 执行 - 0
run
pool-1-thread-3 执行 - 0
run
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task zmx.jdkthreadpool.test.PoolForLinkedBlockingQueueTest$1@7d557ee8 rejected from java.util.concurrent.ThreadPoolExecutor@2a97cec[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2048)
 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:821)
 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1372)
 at zmx.jdkthreadpool.test.PoolForLinkedBlockingQueueTest.main(PoolForLinkedBlockingQueueTest.java:16)

无界的话要防止任务增长的速度远远超过处理任务的速度。

// 在添加第5个任务的时候,程序抛出 java.util.concurrent.RejectedExecutionException

有界队列 如ArrayBlockingQueue

操作模式跟LinkedBlockingQueue查不多,只不过必须为其设置容量。所以叫有界队列。new ArrayBlockingQueue(Integer.MAX_VALUE) 跟 new LinkedBlockingQueue(Integer.MAX_VALUE)效果一样。LinkedBlockingQueue 底层是链表结构,ArrayBlockingQueue 底层是数组结构。你来一个我接一个,直到我容不下你了。FIFO,先进先出。

代码示例:

package zmx.jdkthreadpool.test;
 
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class PoolForArrayBlockingQueueTest {
	
	public static void main(String[] args) {
	      
        ThreadPoolExecutor pool =  new ThreadPoolExecutor(2, 3, 3L, 
	                            TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1),  
	                            new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 5; i++) {
            pool.execute(new Runnable() {
                @Override
                public void run() {
	                	for (int i = 0; i < 10; i++) {
	    	                System.out.println(Thread.currentThread().getName() + " 执行 - " + i);
	    		        }
	                    System.out.println("run");
	                }
	            });
	    }
	    pool.shutdown();
    }
}
// 在添加第5个任务的时候,程序抛出 java.util.concurrent.RejectedExecutionException</span>

运行结果:

pool-1-thread-1 执行 - 0
run
pool-1-thread-1 执行 - 0
run
pool-1-thread-3 执行 - 0
run
pool-1-thread-2 执行 - 0

run
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task zmx.jdkthreadpool.test.PoolForArrayBlockingQueueTest$1@6c3b0b1e rejected from java.util.concurrent.ThreadPoolExecutor@7d6ac92e[Running, pool size = 3, active threads = 3, queued tasks = 1, completed tasks = 0]
 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2048)
 at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:821)
 at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1372)
 at zmx.jdkthreadpool.test.PoolForArrayBlockingQueueTest.main(PoolForArrayBlockingQueueTest.java:15)
关于队列的总结

使用无界队列,要防止任务增长的速度远远超过处理任务的速度,控制不好可能导致的结果就是内存溢出。

使用有界队列,关键在于调节线程数和Queue大小 ,线程数多,队列容量少,资源浪费。线程数少,队列容量多,性能低,还可能导致内存溢出。

关于keepAliveTime

JDK解释:当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。

也就是说啊,线程池中当前的空闲线程服务完某任务后的存活时间。如果时间足够长,那么可能会服务其它任务。

这里的空闲线程包不包括后来新创建的服务线程呢?我的理解是包括的。

关于handler有四个选择

ThreadPoolExecutor.AbortPolicy() 抛出java.util.concurrent.RejectedExecutionException异常。

ThreadPoolExecutor.CallerRunsPolicy() 重试添加当前的任务,他会自动重复调用execute()方法。

ThreadPoolExecutor.DiscardOldestPolicy() 丢弃任务,但是不抛弃异常。当缓冲队列和线程池满时,其它任务将被丢弃。

ThreadPoolExecutor.DiscardPolicy() 当新任务被线程池拒绝时,首先会丢弃缓冲队列最前面的任务,然后将被拒绝的任务添加到末尾。

自定义线程池

为了更好的理解线程池工作的原理,我们手动实现一个线程池
在自定义线程池我们先了解一下常见的几种队列模式

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException, IOException {
        int corePoolSize = 2;
        int maximumPoolSize = 4;
        long keepAliveTime = 10;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        ThreadFactory threadFactory = new NameTreadFactory();
        RejectedExecutionHandler handler = new MyIgnorePolicy();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue, threadFactory, handler);
        executor.prestartAllCoreThreads(); // 预启动所有核心线程
        
        for (int i = 1; i <= 10; i++) {
            MyTask task = new MyTask(String.valueOf(i));
            executor.execute(task);
        }

        System.in.read(); //阻塞主线程
    }

    static class NameTreadFactory implements ThreadFactory {

        private final AtomicInteger mThreadNum = new AtomicInteger(1);

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "my-thread-" + mThreadNum.getAndIncrement());
            System.out.println(t.getName() + " has been created");
            return t;
        }
    }

    public static class MyIgnorePolicy implements RejectedExecutionHandler {

        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            doLog(r, e);
        }

        private void doLog(Runnable r, ThreadPoolExecutor e) {
            // 可做日志记录等
            System.err.println( r.toString() + " rejected");
//          System.out.println("completedTaskCount: " + e.getCompletedTaskCount());
        }
    }

    static class MyTask implements Runnable {
        private String name;

        public MyTask(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                System.out.println(this.toString() + " is running!");
                Thread.sleep(3000); //让任务执行慢点
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "MyTask [name=" + name + "]";
        }
    }
}

输出结果如下:
在这里插入图片描述

其中线程线程1-4先占满了核心线程和最大线程数量,然后4、5线程进入等待队列,7-10线程被直接忽略拒绝执行,等1-4线程中有线程执行完后通知4、5线程继续执行。

线程定时任务的实现 ScheduledExecutorService

这个还有一种特殊的实现类 ScheduledExecutorService

ScheduledExecutorService 和 Timer 的区别
Timer的内部只有一个线程,如果有多个任务的话就会顺序执行,这样我们的延迟时间和循环时间就会出现问题。

ScheduledExecutorService是线程池,所以就不会出现这个情况,在对延迟任务和循环任务要求严格的时候,就需要考虑使用ScheduledExecutorService了。

ScheduledExecutorService的用法主要有三个:

public class MainActivity extends AppCompatActivity {
 
    // 通过静态方法创建ScheduledExecutorService的实例
    private ScheduledExecutorService mScheduledExecutorService = Executors.newScheduledThreadPool(4);
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // 延时任务
        mScheduledExecutorService.schedule(threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                Log.e("lzp", "first task");
            }
        }), 1, TimeUnit.SECONDS);
 
        // 循环任务,按照上一次任务的发起时间计算下一次任务的开始时间
        mScheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                Log.e("lzp", "first:" + System.currentTimeMillis() / 1000);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 1, 1, TimeUnit.SECONDS);
 
        // 循环任务,以上一次任务的结束时间计算下一次任务的开始时间
        mScheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                Log.e("lzp", "scheduleWithFixedDelay:" + System.currentTimeMillis() / 1000);
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 1, 1, TimeUnit.SECONDS);
 
 
    }
}

四个参数分别为:定时任务,初始化时间,间隔时间,时间类型
注:TimeUnit.SECONDS 表示S TimeUnit.MILLISECONDS 表示ms 底层实现都是用的Thread.sleep

ThreadPoolExecutor执行原理

读懂ThreadPoolExecutor执行原理,需要先掌握其状态控制的方式,因为使用了大量位运算,读起来有点吃力,所以单独用一篇文章分析。以下是ThreadPoolExecutor状态控制的主要变量和方法:

//原子状态控制数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//29比特位
private static final int COUNT_BITS = Integer.SIZE - 3;
//实际容量 2^29-1
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
// runState存储在高位中
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl 打包和解压ctl

// 解压runState
private static int runStateOf(int c)     { return c & ~CAPACITY; }
// 解压workerCount
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 打包ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }

线程池使用一个AtomicInteger的ctl变量将 workerCount(工作线程数量)和 runState(运行状态)两个字段压缩在一起 ,这种做法在在java源码里经常有出现,如在 ReentrantReadWriteLock 里就将一个int分成高16位和底16位,分别表示读锁状态和写锁状态。ThreadPoolExecutor里也是使用了同样的思想,表现得更加复杂。

ThreadPoolExecutor用3个比特位表示runState, 29个比特位表示workerCount。因此这里需要特别说明的是:

确切的说,当最大线程数量配置为Integer.MXA_VAULE时,ThreadPoolExecutor的线程最大数量依然是2^29-1。

目前来看这是完全够用的,但随着计算机的不断发展,真的到了不够用的时候可以改变为AtomicLong。这如同32位系统时间戳会在2038年01月19日03时14分07秒耗尽一样,当以后我们的系统线程能够超过2^29-1时,这些代码就需要调整了。对于未来,无限可能。

思考一下为什么是29:3呢?
这是因为我们的运营状态有5种,向上取2次方数,2^3 = 8。所以必须要3个比特位来表示各种状态。

状态解释
RUNNING运行态,可处理新任务并执行队列中的任务
SHUTDOW关闭态,不接受新任务,但处理队列中的任务
STOP停止态,不接受新任务,不处理队列中任务,且打断运行中任务
TIDYING整理态,所有任务已经结束,workerCount = 0 ,将执行terminated()方法
TERMINATED结束态,terminated() 方法已完成

整个ctl的状态,会在线程池的不同运行阶段进行CAS转换。

ThreadPoolExecutor 结束线程的方式

线程中断机制 interrupt

总所周知,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。因为它们太暴力了,是不安全的,这种暴力中断线程是一种不安全的操作,举个栗子来说明其可能造成的问题:

public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        StopThread thread = new StopThread();
        thread.start();
        // 休眠1秒,确保线程进入运行
        Thread.sleep(1000);
        // 暂停线程
        thread.stop();
//      thread.interrupt();
        // 确保线程已经销毁
        while (thread.isAlive()) { }
        // 输出结果
        thread.print();
    }

    private static class StopThread extends Thread {

        private int x = 0; private int y = 0;

        @Override
        public void run() {
            // 这是一个同步原子操作
            synchronized (this) {
                ++x;
                try {
                    // 休眠3秒,模拟耗时操作
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ++y;
            }
        }

        public void print() {
            System.out.println("x=" + x + " y=" + y);
        }
    }
}

上述代码中,run方法里是一个同步的原子操作,x和y必须要共同增加,然而这里如果调用thread.stop()方法强制中断线程,输出如下:

x=1 y=0

没有异常,也破坏了我们的预期。如果这种问题出现在我们的程序中,会引发难以预期的异常。因此这种不安全的方式很早就被废弃了。取而代之的是interrupt(),上述代码如果采用thread.interrupt()方法,输出结果如下:

x=1 y=1
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at ThreadTest$StopThread.run(ThreadTest.java:28)

x=1,y=1 这个结果是符合我们的预期,同时还抛出了个异常,这个异常下文详说。

interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。

interrupt() 并不能真正的中断线程,这点要谨记。需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就需要这样做:

在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
在调用阻塞方法时正确处理InterruptedException异常。(例如:catch异常后就结束线程。)
先看下 Thread 类 interrupt 相关的几个方法:

   // 核心 interrupt 方法
   public void interrupt() {
        if (this != Thread.currentThread()) // 非本线程,需要检查权限
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // 仅仅设置interrupt标志位
                b.interrupt(this);    // 调用如 I/O 操作定义的中断方法
                return;
            }
        }
        interrupt0();
    }
    // 静态方法,这个方法有点坑,调用该方法调用后会清除中断状态。
    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
    // 这个方法不会清除中断状态
    public boolean isInterrupted() {
        return isInterrupted(false);
    }
   // 上面两个方法会调用这个本地方法,参数代表是否清除中断状态
   private native boolean isInterrupted(boolean ClearInterrupted);

首先讲 interrupt() 方法:

interrupt 中断操作时,非自身打断需要先检测是否有中断权限,这由jvm的安全机制配置;
如果线程处于sleep, wait, join 等状态,那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常;
如果线程处于I/O阻塞状态,将会抛出ClosedByInterruptException(IOException的子类)异常;
如果线程在Selector上被阻塞,select方法将立即返回;
如果非以上情况,将直接标记 interrupt 状态;
注意:interrupt 操作不会打断所有阻塞,只有上述阻塞情况才在jvm的打断范围内,如处于锁阻塞的线程,不会受 interrupt 中断;

阻塞情况下中断,抛出异常后线程恢复非中断状态,即 interrupted = false

public class ThreadTest {
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("1"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable{
        String name;
        
        public Task(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("thread has been interrupt!");
            }
            System.out.println("isInterrupted: " + Thread.currentThread().isInterrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

输出:

thread has been interrupt!
isInterrupted: false
task 1 is over

调用Thread.interrupted() 方法后线程恢复非中断状态

public class ThreadTest {
    
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Task("1"));
        t.start();
        t.interrupt();
    }

    static class Task implements Runnable{
        String name;
        
        public Task(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            System.out.println("first :" + Thread.interrupted());
            System.out.println("second:" + Thread.interrupted());
            System.out.println("task " + name + " is over");
        }
    }
}

输出结果:

first :true
second:false
task 1 is over

上述两种隐含的状态恢复操作,是符合常理的,因为线程标记为中断后,用户没有真正中断线程,必然将其恢复为false。理论上Thread.interrupted()调用后,如果已中断,应该执行退出操作,不会重复调用。

优雅的关闭线程

平时开发中,大家更多的关注的是线程池的创建、任务的提交和执行。往往会忽略线程池的关闭,甚至忘记调用shutdown()方法,导致内存溢出。大多知道需要调用shutdown()关闭线程池,也少研究其真正的关闭过程。

首先看源码中的一句注释:

A pool that is no longer referenced in a program and has no remaining threads will be shutdown automatically.
如果程序中不再持有线程池的引用,并且线程池中没有线程时,线程池将会自动关闭。

线程池自动关闭的两个条件:1、线程池的引用不可达;2、线程池中没有线程;

这里对于条件2解释一下,线程池中没有线程是指线程池中的所有线程都已运行完自动消亡。然而我们常用的FixedThreadPool的核心线程没有超时策略,所以并不会自动关闭。

展示两种不同线程池 不关闭 的情况:

1、FixedThreadPool 示例

public static void main(String[] args) {
    while(true) {
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        executorService.execute(() -> System.out.println("running"));
        executorService = null;
    }
}

输出结果:

running
......
running
Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
    at java.lang.Thread.start0(Native Method)
    at java.lang.Thread.start(Thread.java:714)
    at java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:950)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1357)
    at test.PoolTest.main(PoolTest.java:29)

因为FixedThreadPool的核心线程不会自动超时关闭,使用时必须在适当的时候调用shutdown()方法。
2、 CachedThreadPool 示例

public static void main(String[] args) {
    while(true) {
        // 默认keepAliveTime为 60s
        ExecutorService executorService = Executors.newCachedThreadPool(); 
        ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
        // 为了更好的模拟,动态修改为1纳秒
        threadPoolExecutor.setKeepAliveTime(1, TimeUnit.NANOSECONDS);
        threadPoolExecutor.execute(() -> System.out.println("running"));
    }
}

输出结果:

running
running
running
running
running
......

CachedThreadPool 的线程 keepAliveTime 默认为 60s ,核心线程数量为 0 ,所以不会有核心线程存活阻止线程池自动关闭。 详见 线程池之ThreadPoolExecutor构造 ,为了更快的模拟,构造后将 keepAliveTime 修改为1纳秒,相当于线程执行完马上会消亡,所以线程池可以被回收。实际开发中,如果CachedThreadPool 确实忘记关闭,在一定时间后是可以被回收的。但仍然建议显示关闭。

然而,线程池关闭的意义不仅仅在于结束线程执行,避免内存溢出,因为大多使用的场景并非上述示例那样 朝生夕死。线程池一般是持续工作的全局场景,如数据库连接池。

本文更多要讨论的是当线程池调用shutdown方法后,会经历些什么?思考一下几个问题:

是否可以继续接受新任务?继续提交新任务会怎样?
等待队列里的任务是否还会执行?
正在执行的任务是否会立即中断?
问题1:是否可以继续接受新任务?继续提交新任务会怎样?

public static void main(String[] args) {
    ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
    executor.execute(() -> System.out.println("before shutdown"));
    executor.shutdown();
    executor.execute(() -> System.out.println("after shutdown"));
}

输出结果如下:

before shutdown
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task PoolTest$$Lambda$2/142257191@3e3abc88 rejected from java.util.concurrent.ThreadPoolExecutor@6ce253f1[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
    at PoolTest.main(PoolTest.java:12)

当线程池关闭后,继续提交新任务会抛出异常。这句话也不够准确,不一定是抛出异常,而是执行拒绝策略,默认的拒绝策略是抛出异常。可参见 线程池之ThreadPoolExecutor构造 里面自定义线程池的例子,自定义了忽略策略,但被拒绝时并没有抛出异常。

问题2:等待队列里的任务是否还会执行?

public class WaitqueueTest {
    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
        for(int i = 1; i <= 100 ; i++){
            workQueue.add(new Task(String.valueOf(i)));
        }
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
        executor.execute(new Task("0"));
        executor.shutdown();
        System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
    }
    
    static class Task implements Runnable{
        String name;
        
        public Task(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            for(int i = 1; i <= 10; i++){
                System.out.println("task " + name + " is running");
            }
            System.out.println("task " + name + " is over");
        }
    }
}

这个demo解释一下,我们用LinkedBlockingQueue构造了一个线程池,在线程池启动前,我们先将工作队列填充100个任务,然后执行task 0 后立即shutdown()线程池,来验证线程池关闭队列的任务运行状态。
输出结果如下:

......
task 0 is running
task 0 is over
workQueue size = 100 after shutdown //表示线程池关闭后,队列任然有100个任务
task 1 is running
......
task 100 is running
task 100 is over

从结果中我们可以看到,线程池虽然关闭,但是队列中的任务任然继续执行,所以用 shutdown()方式关闭线程池时需要考虑是否是你想要的效果。

如果你希望线程池中的等待队列中的任务不继续执行,可以使用shutdownNow()方法,将上述代码进行调整,如下:

public class WaitqueueTest {
    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
        for(int i = 1; i <= 100 ; i++){
            workQueue.add(new Task(String.valueOf(i)));
        }
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, workQueue);
        executor.execute(new Task("0"));
        // shutdownNow有返回值,返回被抛弃的任务list
        List<Runnable> dropList = executor.shutdownNow();
        System.out.println("workQueue size = " + workQueue.size() + " after shutdown");
        System.out.println("dropList size = " + dropList.size());
    }
    
    static class Task implements Runnable{
        String name;
        
        public Task(String name) {
            this.name = name;
        }
        
        @Override
        public void run() {
            for(int i = 1; i <= 10; i++){
                System.out.println("task " + name + " is running");
            }
            System.out.println("task " + name + " is over");
        }
    }
}

输出结果如下:

task 0 is running
workQueue size = 0 after shutdown
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
task 0 is running
dropList size = 100
task 0 is over

从上述输出可以看到,只有任务0执行完毕,其他任务都被drop掉了,dropList的size为100。通过dropList我们可以对未处理的任务进行进一步的处理,如log记录,转发等;

问题3:正在执行的任务是否会立即中断?

要验证这个问题,需要对线程的 interrupt 方法有一定了解。

关于 interrupt 方法:
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。
所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
而 Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
具体来说,当对一个线程,调用 interrupt() 时,
① 如果线程处于被阻塞状态(例如处于sleep, wait, join 等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
② 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。
interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。也就是说,一个线程如果有被中断的需求,那么就可以这样做。
① 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。
② 在调用阻塞方法时正确处理InterruptedException异常。(例如,catch异常后就结束线程。)

public class InteruptTest {

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
        executor.execute(new Task("0"));
        Thread.sleep(1);
        executor.shutdown();
        System.out.println("executor has been shutdown");
    }

    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            
            for (int i = 1; i <= 100 && !Thread.interrupted(); i++) {
                Thread.yield();
                System.out.println("task " + name + " is running, round " + i);
            }
            
        }
    }
}

输出结果如下:

task 0 is running, round 1
task 0 is running, round 2
task 0 is running, round 3
......
task 0 is running, round 28
executor has been shutdown
......
task 0 is running, round 99
task 0 is running, round 100

为了体现在任务执行中打断,在主线程进行短暂 sleep , task 中 调用 Thread.yield() ,出让时间片。从结果中可以看到,线程池被关闭后,正则运行的任务没有被 interrupt。说明shutdown()方法不会 interrupt 运行中线程。再将其改修改为shutdownNow() 后输出结果如下:

task 0 is running, round 1
task 0 is running, round 2
......
task 0 is running, round 56
task 0 is running, round 57
task 0 is running, round 58
task 0 is running, round 59
executor has been shutdown

修改为shutdownNow() 后,task任务没有执行完,执行到中间的时候就被 interrupt 后没有继续执行了。

总结,想要正确的关闭线程池,并不是简单的调用shutdown方法那么简单,要考虑到应用场景的需求,如何拒绝新来的请求任务?如何处理等待队列中的任务?如何处理正在执行的任务?想好这几个问题,在确定如何优雅而正确的关闭线程池。
PS:线程被 interrupt 后,需要再run方法中单独处理 interrupted 状态,interrupt 更类似一个标志位,不会直接打断线程的执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值