1.线程组
1.1创建线程组的方法
public class xianchengzu {public static void main(String[] args) {ThreadGroup group = new ThreadGroup("group");// 创建线程组时指定父线程组ThreadGroup parent = new ThreadGroup("parent");ThreadGroup child = new ThreadGroup(parent, "child");// 向线程组中添加线程Thread thread1 = new Thread(group,"thread1"){public void run(){System.out.println("我是" + group.getName() + "线程组的一个线程,名称为:"+ this.getName());}};thread1.start();// 通过runnable创建线程Thread thread2 = new Thread(group,new run());thread2.start();}
}class run implements Runnable{public void run(){System.out.println("我是实现runnable接口的线程");}
}
1.2线程组常用方法
// 获取线程组名称
String name = group.getName();// 获取线程组中活跃线程的估计数
int activeCount = group.activeCount();// 获取线程组中活跃子线程组的估计数
int activeGroupCount = group.activeGroupCount();// 中断线程组中所有线程
group.interrupt();// 设置线程组中所有线程的优先级
group.setMaxPriority(Thread.NORM_PRIORITY);// 枚举线程组中的线程
Thread[] threads = new Thread[group.activeCount()];
group.enumerate(threads); // 将线程组中的线程复制到数组中
2.线程池
线程池是一个包含了能提供相同功能的多个线程的集合。
2.1ThreadPoolExecutor
ava 线程池的核心实现是ThreadPoolExecutor
,其构造方法包含以下核心参数:
public ThreadPoolExecutor(int corePoolSize, // 核心线程数int maximumPoolSize, // 最大线程数long keepAliveTime, // 非核心线程空闲超时时间TimeUnit unit, // 超时时间单位BlockingQueue<Runnable> workQueue,// 任务阻塞队列ThreadFactory threadFactory, // 线程工厂RejectedExecutionHandler handler // 拒绝策略
)
各参数的含义:
- corePoolSize:线程池长期保持的线程数,即使线程空闲也不会销毁
- maximumPoolSize:线程池允许创建的最大线程数
- keepAliveTime:当线程数超过核心线程数时,多余线程的空闲存活时间
- workQueue:用于存放等待执行的任务的阻塞队列
- threadFactory:用于创建新线程的工厂
- handler:当任务无法被处理时的拒绝策略
public class xianchengchi {public static void main(String[] args) {ThreadPoolExecutor pool01 = new ThreadPoolExecutor(3,5,500,TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));for(int i=0;i<10;++i){DoWork doWork = new DoWork(i);pool01.execute(doWork);System.out.println("我是线程池,当前池中的线程数为:"+pool01.getActiveCount()+ " 线程池的总容量为:"+pool01.getPoolSize());}for(int i=0;i<8;++i){System.out.println("我是01线程池,目前池内线程总数为:"+ pool01.getPoolSize() + " 等待队列中的任务为:"+pool01.getQueue().size());try{Thread.currentThread().sleep(100);}catch (InterruptedException e){e.printStackTrace();}}pool01.shutdown();}
}class DoWork implements Runnable {private int i;public DoWork(int i) {this.i = i;}@Overridepublic void run() {System.out.println("我是" + i + "号任务,我的任务执行完了");try{Thread.currentThread().sleep(100);}catch (InterruptedException e){e.printStackTrace();}}
}
上面代码创建了一个线程池pool01,核心线程数为3,最大线程数为5,空闲存活时间为500毫秒,等待队列为5。
为线程池添加10个线程,最初,线程池的核心线程数从0到3,此时新加入的任务会被放入等待队列中,如果等待队列也满了,才会继续增加线程池的线程数,如果线程池达到最大的线程数时继续加入线程,则会调用拒绝策略。
下面是一次运行的结果。
我是0号任务,我的任务执行完了
我是线程池,当前池中的线程数为:1 线程池的总容量为:1
我是线程池,当前池中的线程数为:2 线程池的总容量为:2
我是线程池,当前池中的线程数为:3 线程池的总容量为:3
我是1号任务,我的任务执行完了
我是线程池,当前池中的线程数为:3 线程池的总容量为:3
我是线程池,当前池中的线程数为:3 线程池的总容量为:3
我是2号任务,我的任务执行完了
我是线程池,当前池中的线程数为:3 线程池的总容量为:3
我是线程池,当前池中的线程数为:3 线程池的总容量为:3
我是线程池,当前池中的线程数为:3 线程池的总容量为:3
我是线程池,当前池中的线程数为:4 线程池的总容量为:4
我是8号任务,我的任务执行完了
我是线程池,当前池中的线程数为:5 线程池的总容量为:5
我是9号任务,我的任务执行完了
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:5
我是4号任务,我的任务执行完了
我是3号任务,我的任务执行完了
我是5号任务,我的任务执行完了
我是6号任务,我的任务执行完了
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:1
我是7号任务,我的任务执行完了
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:0
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:0
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:0
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:0
我是01线程池,目前池内线程总数为:5 等待队列中的任务为:0
我是01线程池,目前池内线程总数为:3 等待队列中的任务为:0进程已结束,退出代码为 0
2.2四种常用方法
除了前面介绍的通过ThreadPoolExecutor构建方法创建的线程池外,还有四种简化的创建线程池的方法,这几种方法不需要设置大量的参数,或者当不清楚怎么设置参数时可以参考这些方法。
2.2.1FixedThreadPool(固定大小的线程池)
- 创建方式:通过
Executors.newFixedThreadPool(int nThreads)
方法创建,参数nThreads
指定线程池中线程的数量。 - 核心参数特点:
corePoolSize
和maximumPoolSize
都等于创建时指定的线程数量,即线程池中的线程数量固定不变。keepAliveTime
为 0,因为线程不会被销毁,始终保持在池中。- 使用
LinkedBlockingQueue
作为任务队列,容量为Integer.MAX_VALUE
,理论上可以存放无限多的任务。
- 工作原理:当提交任务时,如果有空闲线程,就直接使用空闲线程执行任务;如果没有空闲线程,任务会被放入任务队列等待,直到有线程空闲。
- 适用场景:适用于执行长期的、有稳定并发需求的任务,比如服务器端的请求处理,因为固定数量的线程可以保证系统资源的稳定消耗,避免因线程数量波动带来的性能问题。
2.2.2CachedThreadPool(可缓存的线程池)
- 创建方式:通过
Executors.newCachedThreadPool()
方法创建。 - 核心参数特点:
corePoolSize
为 0,maximumPoolSize
为Integer.MAX_VALUE
,即线程池中的线程数量可以根据任务的多少动态调整。keepAliveTime
为 60L,单位是TimeUnit.SECONDS
,即非核心线程如果闲置 60 秒,就会被销毁。- 使用
SynchronousQueue
作为任务队列,这个队列不存储任务,每个插入操作必须等待另一个线程的移除操作。
- 工作原理:当提交任务时,如果有空闲线程,就直接使用空闲线程执行任务;如果没有空闲线程,就创建新的线程来执行任务。当线程空闲时间超过 60 秒,就会被销毁,所以线程池中的线程数量会根据任务的提交情况动态增减。
- 适用场景:适用于执行大量的短期异步任务,比如异步日志记录、异步数据处理等场景。由于它可以快速创建和销毁线程,能很好地应对突发的大量任务请求。
2.2.3SingleThreadExecutor(单线程的线程池)
- 创建方式:通过
Executors.newSingleThreadExecutor()
方法创建。 - 核心参数特点:
corePoolSize
和maximumPoolSize
都为 1,即线程池中始终只有一个线程。keepAliveTime
为 0,线程不会被销毁。- 使用
LinkedBlockingQueue
作为任务队列,容量为Integer.MAX_VALUE
。
- 工作原理:所有提交的任务都会按照顺序依次由这个唯一的线程来执行,前一个任务执行完后,才会执行下一个任务,保证了任务的串行执行。
- 适用场景:适用于需要保证任务顺序执行,或者任务之间有依赖关系的场景,比如数据库的单线程操作,确保数据操作的一致性和顺序性 。
2.2.4ScheduledThreadPool(支持定时及周期性任务执行的线程池)
- 创建方式:通过
Executors.newScheduledThreadPool(int corePoolSize)
方法创建,参数corePoolSize
指定线程池中的核心线程数量。 - 核心参数特点:
corePoolSize
是创建时指定的核心线程数量,maximumPoolSize
为Integer.MAX_VALUE
。keepAliveTime
为 0,核心线程不会被销毁。- 使用
DelayedWorkQueue
作为任务队列,用于存储延迟执行的任务。
- 工作原理:除了具备普通线程池提交任务执行的功能外,还支持定时执行任务(如延迟一定时间后执行)和周期性执行任务(如每隔固定时间执行一次)。
- 适用场景:适用于需要定时执行或者周期性执行的任务,比如定时备份数据、定时发送邮件、周期性的系统状态检查等场景。
3.线程的异常处理
使用Thread类的线程或是实现了Runnable接口的线程需要在 run 方法中使用try-catch捕获错误,在主程序中捕获会失效,但是实现callable接口的线程可以在主程序中捕获错误。
原因是在Thread类中有一个接口 UncaughtExceptionHandler,它包含一个uncaughtException方法,该方法原意是专门对原本线程中没有捕获成功的异常进行最终捕获处理,同时,当线程未捕获异常而进入该方法时,所有抛出的异常都会被java虚拟机所忽略,即不再对外抛出。
由于java的Thread类默认使用uncaughtException进行空处理,而java虚拟机又会忽略该方法之后的抛出异常,因此我们经常看到的结果是内部线程发生异常时,在外层线程看来,是既不能成功对内部线程的异常进行catch,也不能获取其详细信息。
根本原因是:子线程和主线程是完全独立的执行流(拥有各自的调用栈),子线程中抛出的异常无法直接 “渗透” 到主线程的调用栈中。
具体来说:
- 当我们通过
thread.start()
启动子线程时,子线程的run()
方法会在一个全新的调用栈中执行(与主线程的调用栈完全分离)。 - 如果
run()
方法中抛出未捕获的异常(比如RuntimeException
),这个异常只会在子线程的调用栈中传播,不会影响主线程的执行流程。 - 主线程的
try-catch
只能捕获主线程自身调用栈中产生的异常,无法 “跨线程” 捕获子线程的异常。
为什么 Callable 的异常可以在主程序捕获?
Callable
与Runnable
的核心区别在于:Callable
的异常会被线程池 “封装并保存”,通过Future
对象传递给主线程。
具体流程:
Callable
的call()
方法允许声明抛出异常(包括受检异常),当它抛出异常时,线程池会捕获这个异常,并将其封装到Future
对象中。- 主线程调用
future.get()
获取结果时,如果Callable
执行中抛出了异常,get()
方法会将封装的异常以ExecutionException
的形式抛出(ExecutionException
的getCause()
方法可以获取原始异常)。 - 因此,主线程可以通过
try-catch
捕获ExecutionException
,从而间接获取Callable
中的异常。
1. 使用Runnable
接口的线程异常处理示例
public class RunnableExceptionExample {public static void main(String[] args) {Runnable runnable = () -> {// 模拟抛出异常throw new RuntimeException("Runnable中的异常");};Thread thread = new Thread(runnable);try {// 这里直接在main方法中捕获,捕获不到异常thread.start(); } catch (Exception e) {System.out.println("在main方法中捕获Runnable异常: " + e.getMessage());}}
}
上述代码中,在main
方法里直接捕获Runnable
线程执行时抛出的异常是捕获不到的。如果要处理Runnable
线程中的异常,需要在run
方法内部进行捕获,修改代码如下:
public class RunnableExceptionHandleInRunExample {public static void main(String[] args) {Runnable runnable = () -> {try {// 模拟抛出异常throw new RuntimeException("Runnable中的异常"); } catch (Exception e) {System.out.println("在Runnable的run方法中捕获异常: " + e.getMessage());}};Thread thread = new Thread(runnable);thread.start(); }
}
2. 使用Callable
接口的线程异常处理示例
public class FutureCatch {public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<String> stringCallable = new subCallable();FutureTask<String> futureTask = new FutureTask<String>(stringCallable);Thread thread = new Thread(futureTask);thread.start();try{System.out.println(futureTask.get());}catch(Exception e){System.out.println("返回字符串失败了,下标有问题");}}
}class subCallable implements Callable<String> {public String call() throws Exception{String str = "我是测试字符串";String subStr = str.substring(6,10);return subStr;}
}
4.多线程的安全关闭
多线程的关闭设计其他线程的运行和稳定,所以一个线程并不是调用一个关闭的方法就能马上停止的,要考虑线程使用的资源是否已经释放、线程是否需要在退出服务前通知其它线程或进行一些准备工作。
public class CloseThread {public static void main(String[] args) throws InterruptedException, ExecutionException {Thread clost = new Thread(new waittoclose());clost.start();try{// 让主线程休眠Thread.sleep(2000);}catch (InterruptedException e){e.printStackTrace();}// 调用方法后isInterrupted() 变为true,并且抛出InterruptedExceptionclost.interrupt();}
}class waittoclose implements Runnable{@Overridepublic void run() {while(Thread.currentThread().isInterrupted()==false){try{System.out.println(Thread.currentThread().getName()+"正在运行");Thread.sleep(200);}catch(InterruptedException e){System.out.println("资源正在释放");System.out.println("线程关闭中");Thread.currentThread().interrupt();}}}
}
当线程在执行Thread.sleep()
时,如果收到中断信号(其他线程调用了它的 interrupt()
方法),会发生两件事:
sleep()
会立即抛出InterruptedException
异常,提前结束休眠- 自动清除线程的中断状态(将中断标记设为
false
)
在Runnable接口中,try模块的Thread.sleep(200)在察觉到interrupt后,会按照上述抛出异常和清除中断状态,之后catch模板捕获异常,并进行善后操作,最后还要关闭一次线程,这样线程才能实现安全关闭。
5.自定义多线程异常处理
这里给一个简单的自定义异常:
public class ExceptionHandle implements Thread.UncaughtExceptionHandler {@Overridepublic void uncaughtException(Thread t, Throwable e) {System.out.println(t.getName() + "抛出了异常信息:" + e.toString());}
}
- 实现了
Thread.UncaughtExceptionHandler
接口 - 当线程抛出未捕获异常时,JVM会自动调用此方法
public class CatchExceptionFactory implements ThreadFactory {@Overridepublic Thread newThread(Runnable r) {Thread t = new Thread(r);t.setUncaughtExceptionHandler(new ExceptionHandle()); // 关键设置return t;}
}
- 实现了
ThreadFactory
接口 - 为每个新创建的线程设置自定义异常处理器
class divide implements Runnable {@Overridepublic void run() {int a = 30;int b = 3;int step = 1;while(b >= 0) {System.out.println(a + "与" + b + "相除的结果为:");System.out.println(a / b); // 当b=0时这里会抛出ArithmeticExceptionb--;}System.out.println("结束运算");}
}
- 包含一个会引发除零异常的任务
- 当b递减到0时,
a/b
会抛出ArithmeticException
public class ExceptionTest {public static void main(String[] args) {// 使用自定义线程工厂创建线程池ExecutorService executorService = Executors.newCachedThreadPool(new CatchExceptionFactory());// 提交任务executorService.execute(new divide());}
}
运行结果(此时主程序并没有停止):
30与3相除的结果为:
10
30与2相除的结果为:
15
30与1相除的结果为:
30
30与0相除的结果为:
Thread-0抛出了异常信息:java.lang.ArithmeticException: / by zero