Java--并发(二)------Java 线程创建的四种方式

本文详细介绍了Java中创建线程的四种方法,包括继承Thread类、实现Runnable接口、使用Callable和Future,以及使用线程池如Executor框架。深入解析了ThreadPoolExecutor和ScheduledThreadPoolExecutor的使用和区别,提供了丰富的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、继承Thread类的方式

  • 创建一个继承于Thread类的子类
  • 重写Thread类中的run(),run()方法也称为线程执行体
  • 创建Thread子类的实例,也就是创建了线程对象
  • 启动线程,即调用线程的start()方法
public class TestThread extends Thread {

    @Override
    public void run() {
        System.out.println("线程开始执行了");
    }

    public static void main(String[] args){
        Thread t = new TestThread();
        t.start();
    }
}

2、实现Runnable接口创建线程

  • 创建一个实现Runnable接口的类
  • 实现Runnable接口中的抽象方法:run(),将创建的线程要执行的操作声明在此方法中
  • 创建Runnable接口实现类的实例对象
  • 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 调用线程对象的start()方法来启动线程
public class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("线程开始执行了");
    }
}


public class TestMain{

    public static void main(String[] args){  
        Thread t = new Thread(new TestThread());
        t.start();
    }
}

3、使用Callable和Future创建线程

从Java5开始,Java提供了Callable接口,和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

call()方法可以有返回值
call()方法可以声明抛出异常

  • 创建Callable接口的实现类,实现call() 方法

  • 创建Callable实现类实例,通过FutureTask类来包装Callable对象,该对象封装了Callable对象的call()方法的返回值。

  • 将创建的FutureTask对象作为target参数传入,创建Thread线程实例并启动新线程。

  • 调用FutureTask对象的get方法获取返回值。

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableThreadTest implements Callable<Integer>
{
    @Override
    public Integer call() throws Exception
    {
        int i = 0;
        System.out.println("线程开始执行了");
        return i;
    }
    
    public static void main(String[] args)
    {
        CallableThreadTest ctt = new CallableThreadTest ();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
		new Thread(ft ).start();
        String s = ft .get();
        System.out.println(s);
    }
}

4、使用线程池例如用Executor框架

JDK1.5开始引入了Executor框架用于管理线程和调度线程,可以参考Executor框架的两级调度模型

4.1、使用线程池的好处

降低资源消耗。

每次new Thread()和destroy一个Thread都会耗费一定的系统资源,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度。

当任务到达时,任务可以不需要的等到线程创建就能立即执行。

提高线程的可管理性。

线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

4.2、Executor框架的结构
在这里插入图片描述

Executor是一个顶级的接口,接口只有execute一个方法

ExecutorService是Executor的子接口,拥有更多需要用的的方法规范

ExecutorService拥有两个重要实现类ThreadPoolExecutor和ScheuledThreadPoolExecutor

ScheduledExecutorService实现了ExecutorService接口

ScheduledThreadPoolExecutor实际是继承了ThreadPoolExecutor并实现ScheduledExecutorService

Executor框架主要成员的介绍:

Executor接口:Executor框架的基础,将任务的提交和任务的执行分离。

ThreadPoolExecutor类:Java线程池的核心实现类,用来执行提交的任务。

ScheduledThreadPoolExecutor类:Executor框架的另一个关键类,可以在给定的延迟后执行命令,或者定期执行命令。

还有Future接口、FutureTask类、Runnable接口、Callable接口。

4.3、ThreadPoolExecutor详解

线程池实现类ThreadPoolExecutor是Executor 框架最核心的类

创建线程的两种方式

1、通过构造方法实现(推荐)

在这里插入图片描述

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

1.1、参数解释

参数解释备注
int corePoolSize该线程池中核心线程数最大值线程池新建线程时,如果当前线程总数小于corePoolSize,
则新建的是核心线程,如果超过corePoolSize,
则新建的是非核心线程,核心线程默认情况下会一直存活在线程池中,
即使这个核心线程啥也不干(闲置状态)。
如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,
那么核心线程如果不干活(闲置状态)的话,
超过一定时间(时长下面参数决定),就会被销毁掉。
int maximumPoolSize该线程池中线程总数最大值线程总数 = 核心线程数 + 非核心线程数。
long keepAliveTime该线程池中非核心线程闲置超时时长一个非核心线程,如果闲置状态的时长
超过这个参数所设定的时长,就会被销毁掉,
如果设置allowCoreThreadTimeOut = true,
则会作用于核心线程。
BlockingQueue workQueue该线程池中的任务队列:维护着等待执行的Runnable对象当所有的核心线程都在干活时,
新添加的任务会被添加到这个队列中等待处理,
如果队列满了,则新建非核心线程执行任务。
常用的workQueue类型有四种
ThreadFactory threadFactory创建线程的方式这是一个接口,在new操作的时候需要实现他的Thread ,
即newThread(Runnable r)方法
RejectedExecutionHandler handler当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理jdk1.5提供了四种饱和策略

四种workQueue类型

SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

四种饱和策略

AbortPolicy 默认。直接抛异常。

CallerRunsPolicy 只用调用者所在的线程执行任务,重试添加当前的任务,它会自动重复调用execute()方法

DiscardOldestPolicy 丢弃任务队列中最久的任务。

DiscardPolicy 丢弃当前任务。

2、通过Executor 框架的工具类Executors(不推荐)

1、在《阿里巴巴Java开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。
2、《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

2.1、FixedThreadPool

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

特点:

1、适用固定线程数的线程池,适用于为了满足资源管理的需求,而限制当前线程数的应用场景。或者是适用于负载比较重的服务器。

2、FixedThreadPool使用无界队列 LinkedBlockingQueue(队列的容量为Intger.MAX_VALUE)作为线程池的工作队列,可能堆积大量的请求,从而导致OOM。

2.2、SingleThreadExecutor

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

特点:

1、适用单个线程的线程池,适用于需要保证各个任务顺序执行,而且在任意时刻,不会有多个活动线程的应用场景。

2、SingleThreadExecutor使用无界队列作为线程池的工作队列会对线程池带来的影响与FixedThreadPool相同。

2.3、CachedThreadPool

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

特点

1、根据需要创建新线程的线程池,适用于执行很多短期异步任务的小程序,或者是负载比较轻的服务器。

2、如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新的线程。极端情况下,这样会导致耗尽cpu和内存资源。

3、ThreadPoolExecutor使用案例demo

import java.util.concurrent.*;
 
public class TreadTest{
    public static void main(String[] args) {
        //创建一个线程池对象
        /**
         * 参数信息:
         * int corePoolSize     核心线程大小
         * int maximumPoolSize  线程池最大容量大小
         * long keepAliveTime   线程空闲时,线程存活的时间
         * TimeUnit unit        时间单位
         * BlockingQueue<Runnable> workQueue  任务队列。一个阻塞队列
         */
        ThreadPoolExecutor pool = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10));
     
        //执行任务
        for (int i = 0; i < 10; i++) {
            int index = i;
            pool.execute( ()-> System.out.println("i:"+index+"execute!"));
        }
        //关闭线程池
        pool.shutdown();
    }
}

5、ScheduledThreadPoolExecutor详解

ScheduledThreadPoolExecutor主要用来在给定的延迟后运行任务,或者定期执行任务。

5.1、与ThreadPoolExecutor的区别

使用DelayQueue作为阻塞队列

获取任务的方式不同

执行周期任务后,添加了额外的处理

5.2、ScheduledThreadPoolExecutor执行周期任务的步骤

1、线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前系统的时间;

DelayQueue中封装的PriorityQueue,会对这些任务按time进行排序,时间小的先被执行。如果time相同,则按seq进行排序,序号较小的先执行。(即如果任务的time相同,则先提交的先被执行)

2、线程1执行这个ScheduledFutureTask;

ScheduledFutureTask中的三个重要的成员变量

long time: 表示这个任务将要被执行的具体时间。当time大于等于当前系统的时间,表明该任务已到期,可以执行。
long sequenceNumber: 表示这个任务被添加到ScheduledThreadPoolExecutor的序号。
long period: 表示任务执行的时间间隔

3、线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间;

4、线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

5.3、ScheduledThreadPoolExecutor和Timer和Quartz的比较

Timer只有一个执行线程,因此长时间运行的任务可以延迟其他任务

TimerTask中抛出的运行时异常会导致计划任务将不再运行

ScheduledThreadExecutor不仅可以捕获运行时异常,还可以处理异常,其他任务的允许不会停止。

ScheduledThreadPoolExecutor可以配置任意数量的线程,不会导致长时间允许一个任务而延迟其它任务。

PS:相对于两者,一般定时任务还是使用Quartz居多。

5.4、ScheduledThreadPoolExecutor使用案例

import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class TreadTest {
 
	private static class TaskDemo implements Runnable {
		private String TAG = "";
		public TestTask(String tag) {
			TAG = tag;
		}
 
		@Override
		public void run() {
			System.out.println(TAG + "\t" + System.currentTimeMillis());
		}
	}
 
	public static void main(String[] args) {
	
		ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(2);
		TaskDemo task1 = new TaskDemo ("第一个任务");
		TaskDemo task2= new TaskDemo ("第二个任务");
 		 // 延迟10秒执行
		threadPoolExecutor .schedule(task1, 10, TimeUnit.SECONDS);
		 // 延迟20秒执行
		threadPoolExecutor .schedule(task12, 20, TimeUnit.SECONDS);
	}
}

如果你觉得本篇文章对你有所帮助的话,麻烦请点击头像右边的关注按钮,谢谢!

技术在交流中进步,知识在分享中传播

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值