Java创建线程的几种方式
参考博客:https://blog.csdn.net/weixin_64916311/article/details/130573871
线程的生命周期
新建状态(NEW):指的是线程已经被创建,但是还不允许分配 CPU 执行。
就绪状态(RUNNABLE): 指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
运行状态(Running):当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
阻塞状态(BLOCKED):运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如键盘输出Scanner方法调用),那么线程的状态就会转换到阻塞状态,同时释放 CPU 使用权,阻塞状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,阻塞状态结束,线程就会从阻塞状态转换到就绪状态,重新抢到CPU时间片!
** 等待状态(WAITING):**当两个线程运行时,在线程1内设置一个死循环。并在线程2内使用 join() 方法等待线程1执行完毕。此时的线程2就处于 WAITING(无限制等待)状态
指定时间等待状态(TIMED_WAITING) 指定时间等待状态用在于线程使用 sleep() 方法时,线程就处于指定时间等待状态。直到 sleep() 方法执行完毕,线程才会被唤醒
完成状态(TERMINATED)当线程执行完毕。这个线程就死亡了,也就是完成状态
死亡状态:线程执行完或者出现异常就会进入死亡状态,死亡状态的线程不会切换到其他任何状态,进入死亡状态也就意味着线程的生命周期结束了。通常线程死亡的方式有三种:
- run()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。
线程方法
1、sleep(long millis)线程休眠
线程休眠:让执行的线程暂停一段时间,进入等待状态。
static void sleep(long millis):调用此方法后,当前线程放弃 CPU 资源,在指定的时间内,sleep 所在的线程不会获得可运行的机会,此状态下的线程不会释放同步锁。
该方法更多的是用来模拟网络延迟,让多线程并发访问同一资源时的错误效果更加明显。
2.wait()线程等待
线程等待:一旦一个线程执行到wait(),就释放当前的锁。
注意:此方法必须在同步代码块或同步方法中
3、notify()/notifyAll()
唤醒:唤醒wait的一个或所有的线程,需和wait()成对使用
4、join()
联合线程:表示这个线程等待另一个线程完成后(死亡)才执行,join 方法被调用之后,线程对象处于阻塞状态。写在哪个线程中,哪个线程阻塞
这种也称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程
5、yield()
礼让线程:表示当前线程对象提示调度器自己愿意让出 CPU 资源。
调用该方法后,线程对象进入就绪状态,所以完全有可能:某个线程调用了 yield() 方法,但是线程调度器又把它调度出来重新执行。
sleep() 和 yield() 方法的区别:
①都能使当前处于运行状态的线程放弃 CPU资源,把运行的机会给其他线程
②sleep 方法会给其他线程运行的机会,但是不考虑其他线程优先级的问题;yield 方法会优先给更高优先级的线程运行机会
③调用 sleep 方法后,线程进入计时等待状态,调用 yield 方法后,线程进入就绪状态。
6、interrupt()
Thread.interrupt()方法: 作用是中断线程。将会设置该线程的中断状态位,即设置为true,中断的结果线程是死亡、还是等待新的任务或是继续运行至下一步,就取决于这个程序本身
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。
Java创建线程的三种方式
继承Thread类创建线程
-
1、定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
-
2、创建Thread子类的实例,也就是创建了线程对象
-
3、启动线程,即调用线程的start()方法
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
实现Runnable接口创建线程
-
1、定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
-
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
-
3、第三部依然是通过调用线程对象的start()方法来启动线程
public class MyThread2 implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
//或者 new Thread(new MyThread2()).start();
}
}
使用Callable和Future创建线程
通常我们都是我们使用Runnable和Thread来创建一个新的线程。但是它们有一个弊端,就是run方法是没有返回值的。但是有时候我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。这时候我们就可以使用实现callable接口的方式来创建线程。我们可以使用 ExecutorService 可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Future的get方法得到结果.
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 使得线程睡眠一秒
Thread.sleep(1000);
//返回一个2
return 2;
}
public static void main(String args[]){
ExecutorService executor = Executors.newCachedThreadPool();
Task task = new Task();
Future<Integer> result = executor.submit(task);
// 注意调用get方法会阻塞当前线程,直到得到结果。
System.out.println(result.get());
}
}
还可以使用一个类FutureTask配合实现callable接口的方式来创建线程。
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 使得线程睡眠一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
executor.submit(futureTask);
System.out.println(futureTask.get());
}
}
两种方式在使用上有一点区别。首先,调用submit方法是没有返回值的,我们不用接收返回值去调用get方法。
这里实际上是调用的submit(Runnable task)方法,而上面的Demo,调用的是submit(Callable task)方法。
然后,这里是使用FutureTask直接取get取值,而上面的Demo是通过submit方法返回的Future去取值。
在很多高并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能够在高并发环境下确保任务只执行一次。
三种方式之间的比较
-
实现接口方式的好处
- 由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。
- Runnable接口出现更符合面向对象,将线程单独进行对象的封装。
- Runnable接口出现,降低了线程对象和线程任务的耦合性。
- 如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。
-
Runnable和Callable接口的比较
- Callable接口可以获取线程运行的信息以及中止线程,Runnable只能提供基本的线程运行工作,Callable的功能更丰富一些
- Callable的call()方法允许抛出异常,Runnable的run()方法则不允许
- 当使用FutureTask.get()方法时,主线程会阻塞,因为该方法返回的是该线程的运行结果,只有等到该线程结束才可以返回结果,而该方法写在主线程中,主线程会因为该方法等待线程结束而阻塞,直到返回出了运行结果,主程序才会继续运行,所以FutureTask.get()要在不需要并发的时候去调用
实际工作创建线程的方式
private void initRqdjHandler() {
new Thread(new Runnable() {
@Override
public void run() {
// 另一个线程需要执行的内容
}
}).start();
}
创建线程池的方式
7大线程池核心参数
总体来说线程池的创建可以分为以下两类:
- 通过 ThreadPoolExecutor 手动创建线程池。
- 通过 Executors 执行器自动创建线程池。
Java开发手册建议通过ThreadPoolExecutor创建线程池,这里只介绍这一种方式
corePoolSize
线程池核心线程数量,核心线程不会被回收,即使没有任务执行,也会保持空闲状态。
maximumPoolSize
池允许最大的线程数,当线程数量达到corePoolSize,且workQueue队列塞满任务了之后,继续创建线程。
线程池大小设置策略
-
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io操作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了cpu资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
-
I/O 密集型任务(2N): 系统会用大部分的时间来处理 I/O 操作,而线程等待 I/O 操作会被阻塞,释放 cpu资源,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N
keepAliveTime
超过corePoolSize之后的“临时线程”的存活时间。
unit
keepAliveTime的单位。
workQueue
当前线程数超过corePoolSize时,新的任务会处在等待状态,并存在workQueue中,BlockingQueue是一个先进先出的阻塞式队列实现,底层实现会涉及Java并发的AQS机制
threadFactory
创建线程的工厂类,通常我们会自定一个threadFactory设置线程的名称,这样就可以知道线程是由哪个工厂类创建的,可以快速定位。
handler
线程池执行拒绝策略,当线数量达到maximumPoolSize大小,并且workQueue也已经塞满了任务的情况下,线程池会调用handler拒绝策略来处理请求。
系统默认的拒绝策略有以下几种:
- AbortPolicy:为线程池默认的拒绝策略,该策略直接抛异常处理。
- DiscardPolicy:直接抛弃不处理。
- DiscardOldestPolicy:丢弃队列中最老的任务。
- CallerRunsPolicy:将任务分配给当前执行execute方法线程来处理。
我们还可以自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可,友好的拒绝策略实现有如下:
- 将数据保存到数据,待系统空闲时再进行处理
- 将数据用日志进行记录,后由人工处理
@Component
public class CommonThreadPool {
private ThreadPoolExecutor executor;
private int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
private int maxPoolSize = Runtime.getRuntime().availableProcessors() * 4;
private void initThreadPool() {
executor = new ThreadPoolExecutor(
// 核心线程数
corePoolSize,
// 最大线程数
maxPoolSize,
// 临时线程存活时间
60,
// 存活时间的单位
TimeUnit.SECONDS,
//workQueue
new ArrayBlockingQueue<>(100),
// 创建线程的工厂类
Executors.defaultThreadFactory(),
// 拒绝策略
new ThreadPoolExecutor.AbortPolicy()
);
}
@Bean
public ThreadPoolExecutor getThreadPool() {
if (executor == null) {
initThreadPool();
}
return executor;
}
}
线程池调用:
@Autowired
private ThreadPoolExecutor poolExecutor;
private void handle() {
poolExecutor.execute(
// test方法是要执行的线程方法
() -> test()
);
}
线程池调用有submit方法和execute方法,它们的区别:
- execute只能提交Runnable类型的任务,没有返回值,而submit既能提交Runnable类型任务也能提交Callable类型任务,返回Future类型。
- execute方法提交的任务异常是直接抛出的,而submit方法是是捕获了异常的,当调用FutureTask的get方法时,才会抛出异常。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)