前言:
此刻兄弟们对“java线程项目”大致比较注意,咱们都需要知道一些“java线程项目”的相关知识。那么小编也在网摘上搜集了一些有关“java线程项目””的相关资讯,希望姐妹们能喜欢,同学们一起来了解一下吧!在 Java 中,可以利用线程做很多事情,创建多个线程来高效的完成任务,例如 Tomcat 用多个线程来接收和处理请求,我们也可以创建多个线程来批量处理数据,原本串行执行的任务变成并行执行,充分利用 CPU 多个核的性能。
我们可以用这样的方式创建线程执行并发任务:
for (int i = 0; i < 任务数量; i++) { Thread thread = new Thread(任务); thread.start();}复制代码
这样子确实能够并发完成任务,但是也带了问题:创建的线程数量不可控制,一个任务就创建一个线程,当任务量庞大的时候,会带来极大的内存开销,反复创建、销毁线程。
通过使用线程池的方式,就可以集中管理线程资源,线程池能够给我们带来如下优点:
复用线程。利用一定数量的线程,反复执行任务,而不用频繁的创建、销毁线程。控制了资源的总量,合理利用 CPU 和内存。由于复用线程,CPU 和 内存相较于创建多个线程来说占用更低。统一管理资源,统一停止线程。相同任务类型的线程被统一管理起来,能够在某些情况统一的停止这些任务。
⭐ 在实际开发中,如果需要创建超过五个线程执行类似的任务,就可以考虑使用线程池了。
创建线程池
我们已经知道了线程池的优点和强大了,现在再介绍一下线程池要怎么去创建,怎么去使用。
线程池创建线程的规则
首先,线程池有几个核心属性,分别是:
corePoolSize,核心线程数量,线程池的线程数量会维持在这个数字上maximumPoolSize,最大线程数量,创建的线程数量不会超过这个数字keepAliveTime,线程存活时间,超过核心线程数的线程,如果空闲时间超过指定时间,就会被回收任务队列,用于接收、存储待执行的任务,当线程空闲下来时,会从任务队列中取出任务并执行 直接交接队列(SynchronousQueue),队列大小为零,新任务直接开始运行,不会等待 有界队列(ArrayBlockQueue),任务队列是有限的 无界队列(LinkedBlockQueue),任务队列是无限的,理论上可以添加任意数量的任务
线程池创建线程的规则是这样的:
如果当前线程数<核心线程数当前线程数 < 核心线程数当前线程数<核心线程数,则接受新任务就创建新线程如果核心线程数≤当前线程数<最大线程数核心线程数 \le 当前线程数 < 最大线程数核心线程数≤当前线程数<最大线程数,则接收新任务时,将新任务加入到任务队列中如果任务队列满了,并且 当前线程数<最大线程数当前线程数 < 最大线程数当前线程数<最大线程数,则创建新的线程执行任务如果任务队列满了,并且 当前线程数=最大线程数当前线程数 = 最大线程数当前线程数=最大线程数,那么拒绝该任务并抛出拒绝任务异常RejectedExecutionException 使用 Executors 创建线程池
上文已经提到了线程池的几个核心属性以及创建线程的规则,其实这些核心属性是创建线程池对象的参数,主要来自于ThreadPoolExecutor类的构造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) 复制代码
我们可以利用这个方法来创建一个线程池:
public static void main(String[] args) { // 创建一个线程池 ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); // 创建 10 个任务,打印线程名 for (int i = 0; i < 10; i++) { // ThreadPoolExecutor 的 execute 方法可以接收并执行任务 // 接收 Runnable 接口实现类,Runnable 是一个方法接口,因此可以使用 Lambda 表达式 pool.execute(()-> { System.out.println(Thread.currentThread().getName()); }); } }复制代码
输出结果:
pool-1-thread-1pool-1-thread-4pool-1-thread-3pool-1-thread-3pool-1-thread-3pool-1-thread-3pool-1-thread-3pool-1-thread-2pool-1-thread-5pool-1-thread-1复制代码
阿里手册里面推荐我们给一个线程池指定一个线程工厂,从而另线程池创建的每个线程的名字都有意义。在使用这个方法创建线程的时候,可以这样创建,以指定具体的线程名:
// 创建一个线程工厂,实现接口 ThreadFactorypublic class MyThreadPoolFactory implements ThreadFactory { private final AtomicInteger threadNum = new AtomicInteger(1); private final String prefixName; public MyThreadPoolFactory(String prefixName) { this.prefixName = prefixName; } @Override public Thread newThread(Runnable r) { Thread t = new Thread(r, prefixName + threadNum.getAndIncrement()); return t; } }// 修改上面的例子,创建线程池的语句修改为:ThreadPoolExecutor pool = new ThreadPoolExecutor(5, 5, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new MyThreadPoolFactory("我的线程"));// 再次运行查看结果复制代码
运行结果:
我的线程2我的线程5我的线程3我的线程1我的线程3我的线程5我的线程2我的线程4我的线程3我的线程1复制代码
也可以参考Java线程池中设置线程名称三种方式 - 屠城校尉杜 - 博客园 (cnblogs.com),用现有的工具类创建。
我们也可以使用 Executor 类快速创建各种类型的线程池,可以创建如下类型的线程池:
线程类型
说明
固定数量的线程池
创建一个线程池,其核心线程数和最大线程数都相同
单线程线程池
创建一个线程池,其最多只有一个线程
缓存线程池
任务队列使用直接交接队列
最大线程数为整形最大值
定时任务线程池
可以定时执行任务或者周期执行任务
我们来看看 Executor 的用法,首先是看看固定数量的线程池的创建方法:
public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("固定线程池")); for (int i = 0; i < 10; i++) { pool.submit(() -> { System.out.println(Thread.currentThread().getName()); }); } }// 运行结果如下:固定线程池3固定线程池5固定线程池5固定线程池4固定线程池1固定线程池2固定线程池1固定线程池4固定线程池3固定线程池5// 方法实际调用情况,还是用到了我们上面提到的方法:public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }复制代码
此外,其他类型的线程池创建方法也大同小异,具体可以看看下面的Executors的相关方法,使用起来还是很简单的。
// 创建一个缓存线程池public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory); }// 创建一个单线程线程池public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }// 创建一个定时任务线程池,这里其实还是调用了 new ThreadPoolExecutor(),可以进源码查看,任务队列使用延时队列public static ScheduledExecutorService newScheduledThreadPool( int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); }public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue(), threadFactory); }// ScheduledThreadPoolExecutor 的 schedule 方法可以指定任务在一定时间后开始运行,运行结束后任务结束// scheduleAtFixedRate 方法则可以让一个任务以指定的周期运行,运行结束后会在指定时间后继续运行复制代码推荐的方法
阿里的Java开发手册是推荐我们使用 ThreadPoolExecutor 来创建线程池,并且指定线程工厂从而让每个线程都有自己的具体名字的。因此,在实际开发过程中,我们可以尽量使用 ThreadPoolExecutor 创建线程池。
指定合适的线程数量
创建线程池指定合适的线程数量对于应用的性能也很有影响,线程数量分配少了,则性能有限,线程数量分配多了,则浪费了资源。但是要如何确定线程需要的数量呢?
如果是CPU密集型的应用,例如加密、哈希计算、大量计算的任务,可以考虑使用 1-2 被CPU核心数量的线程数。如果是IO密集型的应用,例如需要反复读写磁盘的任务,则可以指定尽可能多倍于CPU核心数的线程数,因为大多数后线程其实是在等待磁盘 IO 的,可以让其他线程使用资源。 还有一条经验法则,可以利用这条公式设置线程数:线程数=CPU核心数×(1+平均等待时间平均工作时间)线程数=CPU核心数 \times (1 + \frac{平均等待时间}{平均工作时间})线程数=CPU核心数×(1+平均工作时间平均等待时间) 总而言之,线程数的设置没有一个通用的规则,而是根据具体的应用场景不断调试出合适的数量。 停止线程池
在使用线程池的过程中,我们也并不是想要让线程池一直运行下去的,有时候可能根据业务需要,例如项目需要紧急停止、用户需要暂停等等,我们需要让一个正在运行并且正在执行任务的线程池停止下来。 那么,具体可以怎么停止一个线程池呢?可以看看下面的代码:
public class ThreadPoolDemo implements Runnable { @Override public void run() { try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(5, new MyThreadPoolFactory("演示停止线程池")); for (int i = 0; i < 50; i++) { pool.submit(new ThreadPoolDemo()); } // 查看线程池是否处于 shutdown 状态 System.out.println("线程池 shutdown:" + pool.isShutdown()); System.out.println("停止线程池"); pool.shutdown(); // 线程池已经进入 shutdown 状态,但是应用还在运行,线程池中的任务还在执行 System.out.println("线程池 shutdown:" + pool.isShutdown()); // 提交新任务会报异常 //pool.submit(new ThreadPoolDemo()); // 如果想要立即停止线程池,可以使用 shutdownNow,会返回线程池中未执行完毕的任务列表 // List<Runnable> runnables = pool.shutdownNow(); // 查看线程池是否已经终止 System.out.println("线程池 terminated:" + pool.isTerminated()); // 等待线程池中的任务运行完毕,主线程阻塞指定时间 pool.awaitTermination(1, TimeUnit.MINUTES); System.out.println("线程池 terminated:" + pool.isTerminated()); }}// 打印结果:线程池 shutdown:false停止线程池线程池 shutdown:true线程池 terminated:false线程池 terminated:true复制代码
上面的例子已经演示了如何停止一个正在运行的线程池,现在总结一下上面相关方法的区别:
方法
说明
shutdown
停止线程池,线程池并不会马上停止。拒绝接受新任务,如果提交新任务会抛出异常
shutdownNow
立即停止线程池,正在执行任务的线程会收到中断通知。返回任务队列中的任务
isShutdown
线程池是否收到了 shutdown 指令
isTerminated
线程池是否已经停止
awaitTerminated
调用该方法的线程阻塞住,等待指定时间,运行结果如下:
如果所有任务执行完毕,返回 true。
超过了等待时间,返回false。
当前线程被中断,抛出中断异常
为线程池指定拒绝策略
线程池在以下这些情况会拒绝任务:
线程池的状态是 shutdown,此时提交任务会执行拒绝策略线程池的任务队列已满,并且线程数量已经到达最大线程数,此时提交任务会执行拒绝策略
可以指定的拒绝策略如下,具体可以查看接口RejectedExecutionHandler 的实现类:
策略
说明
AbortPolicy
拒绝任务并直接抛出异常,默认策略
DiscardPolicy
默默丢弃任务,不抛出异常
CallerRunsPolicy
拒绝任务,并让提交任务的线程运行被拒绝的任务
DiscardOldestPolicy
丢弃最早提交的任务,然后重新尝试接收该任务
可以在 ThreadPoolExecutor 的这个构造方法中指定拒绝策略:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);// 例如 new ThreadPoolExecutor(2,5,1,...,new ThreadPoolExecutor.CallerRunsPolicy());复制代码线程池的状态
从上文中也可以看出,线程池有着各种状态,或者可以提交并执行任务,或者不再接受任务但是仍然执行运行中的任务,或者直接中断执行的任务…… 下面是线程池的各种状态:
状态
说明
RUNNING
线程池创建后的状态,接收并执行任务
SHUTDOWN
拒绝接收新任务,等待任务执行完成,shutdown() 执行后的状态
STOP
拒绝接收新任务,中断执行中的任务,返回任务队列中的任务,shutdownNow() 执行后的状态
TIDYING
任务已经停止,没有工作线程,执行线程池的 terminated() 方法
TERMINATED
线程池已经停止
标签: #java线程项目