【高并发】多线程创建的几种方式

前言

Java中创建线程的方式有四种:继承Thread类、实现Runnable接口、实现Callable接口、通过Executors工具类创建线程池等,作为多线程的基础来整理一下。

1、继承Thread类

说明:

Thread类本质上是实现了Runnable接口的一个实例。子类继承Thread类后需要重写run()方法,创建子类的实例后,通过start()方法启动线程。注意,Java只支持类的单继承,这种方式是不利于子类扩展的。

举例:

public class MyThread extends Thread {
    Logger logger = LoggerFactory.getLogger(MyThread.class);

    public void run() {
        for (int i = 0; i < 5; i++) {
            logger.info("线程" + Thread.currentThread().getName() + ":" + i);
        }
    }
}

@SpringBootTest
class MyThreadTest {
    @Test
    void testThread() {
        MyThread myThread1 = new MyThread();
        myThread1.start();
        MyThread myThread2 = new MyThread();
        myThread2.start();
        MyThread myThread3 = new MyThread();
        myThread3.start();
    }
}

结果从结果可知,启动线程的顺序是有序的,但是线程执行的顺序并非是有序的!!!

简化形式:

/** 使用线程Thread的匿名子类 **/
Thread thread1 = new Thread(){
    public void run() {
        for (int i = 0; i < 5; i++) {
            logger.info(Thread.currentThread().getName() + ":" + i);
        }
    }
};
thread1.start();

/** 使用Lambda表达式创建线程Thread **/
Thread thread2= new Thread(()->{
    for (int i = 0; i < 5; i++) {
        logger.info(Thread.currentThread().getName() + ":" + i);
    }
});
thread2.start();

2、实现Runnable接口

说明:

需要重写run()方法,并把实现类的实例作为参数传给Thread对象,通过start()方法启动该线程。

举例:

public class MyRunable implements Runnable{
    Logger logger = LoggerFactory.getLogger(MyRunable.class);

    @Override
    public void run() {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        logger.info("当前线程名称为:"+Thread.currentThread().getName());
    }
}
@SpringBootTest
class MyThreadTest {
    @Test
    void testThread() {
        MyRunable myRunable = new MyRunable();
        Thread t1 = new Thread(myRunable,"t1线程");
        Thread t2 = new Thread(myRunable,"t2线程");
        Thread t3 = new Thread(myRunable,"t3线程");
        t1.start();
        t2.start();
        t3.start();
    }
}

结果:

3、实现Callable接口

说明:

Callable接口在java.util.concurrent包下,重写call()方法并有返回值,可抛异常,创建实现类的实例作为FutureTask类的包装对象,使用FutureTask对象作为参数创建Thread对象,通过start()方法启动线程,而且通过FutureTask对象的get()方法可获取子线程的返回值。

举例:

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
 
public class MyCallable implements Callable<String> {
    Logger logger = LoggerFactory.getLogger(MyCallable.class);
    private int cnt = 10;

    @Override
    public String call() throws Exception {
        for (int i = cnt; i > 0; i--) {
            logger.info("商品当前剩余数量:" + cnt--);
        }
        return "商品售罄,欢迎下次抢购!";
    }
}
@SpringBootTest
class MyThreadTest {
    @Test
    void testThread() throws Exception{
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        // 这三个线程谁获得了CPU执行权,谁就执行call()方法,直到返回结果
        Thread th1 = new Thread(futureTask);
        Thread th2 = new Thread(futureTask);
        Thread th3 = new Thread(futureTask);
        th1.start();
        th2.start();
        th3.start();
        logger.info("询问-->是否完成商品售卖:" + futureTask.isDone());
        // 阻塞其他的线程,保证执行完call()并返回结果值
        logger.info(futureTask.get());
        logger.info("再次询问-->是否完成商品售卖:" + futureTask.isDone());
    }
}

结果:

最后总结一下,实现Callable接口与实现Runnable接口的区别:

  • 实现Callable接口方式的call()方法可以抛异常,而Runnable的run()方法不会抛;
  • 实现Callable接口方式在任务结束后能提供一个返回值,而Runnable没有;
  • 使用Callable会拿到一个futureTask对象,它是异步计算的结果,提供了检查运算是否结束的方法(如isCancelled()、isDone()等)。而线程属于异步计算模型,是无法从其他线程得到call()方法的返回值的,这样可以使用futureTask对象来监控目标线程执行call()方法的情况,通过futureTask对象的get()方法可以阻塞当前线程,直到call()执行完毕返回结果。

4、通过Executors工具类创建线程池

1、为什么需要线程池?

每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存等,我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。

合理利用线程池有以下三个优势:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

然而,要合理的使用线程池,了解其原理是很重很要的!

2、线程池的运行流程

当提交一个新任务到线程池时,线程池的处理流程如图:

  • 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
  • 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
  • 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给拒绝策略来处理这个任务。

3、线程池的类型

Java 5 在 java.util.concurrent 包中自带了内置的线程池,如Executors工具类,它提供了多种类型的线程池,下面看源码里怎么定义这些类型的,同时需要注意以下几点:

ThreadPoolExecutor的参数说明:

  • corePoolSize:线程池核心线程数量;
  • maximumPoolSize:线程池能容纳的最大线程数量;
  • keepAliveTime:线程保持活性的时间;
  • unit:时间单位;
  • workQueue:工作队列,是一种阻塞队列BlockingQueue。

线程池有以下六种类型:

  • newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool、newSingleThreadScheduledExecutor
public class Executors {
    // 第一种:定长线程池--拥有固定线程数量的线程池,若无任务执行,线程会一直等待
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
    }
    // 第二种:容量为1的线程池--线程池中只有一个工作线程
    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
    }
     // 第三种:可缓存的线程池 -- 被创建的线程若处于空闲状态,60s后自动销毁
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    // 第四种:可调度的线程池 -- 需要指定核心工作线程数量,用于执行调度任务
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
        // ScheduledThreadPoolExecutor默认的参数如下:
        //  super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
        //      new DelayedWorkQueue());
    }
    // 第五种:容量为1的可调度线程池 -- 只有1个工作线程执行调度任务
    public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
        return new DelegatedScheduledExecutorService
            (new ScheduledThreadPoolExecutor(1));
    }
}
  • Java8又新增了newWorkStealingPool类型的线程,它是一种具有抢占式操作的线程池,有2个方法:一个方法需要指定并行数目,另一个方法默认系统的CPU核的数量(Runtime.getRuntime().availableProcessors())。该类型的线程池属于并行线程池,与前面五种线程池不同之处在于,它不会保证任务的顺序执行,哪个任务抢占到线程就执行哪个。
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

4、线程池的使用步骤

从上面的六种线程池类型看,返回值可以分为两大类:ExecutorService接口ScheduledExecutorService接口,因此如何使用,要先了解这两个接口:

ExecutorService接口源码:

  • 该接口继承了Executor 接口,Executor里的execute()方法执行的是Runnable实例,无返回值,但使用会受到局限,可以使用submit()替代。
public interface ExecutorService extends Executor { 
    // 关闭线程池操作
    void shutdown();
    List<Runnable> shutdownNow();
    boolean isShutdown();
    boolean isTerminated();
    // 用于等待子线程结束,再继续往下执行
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
   // 单个提交任务操作,参数可以是Callable、Runnable等类型的线程,返回Future对象
   <T> Future<T> submit(Callable<T> task);
   <T> Future<T> submit(Runnable task, T result);
   Future<?> submit(Runnable task);
   // 批量提交任务操作, invokeAll会保证集合中的任务全部完成再返回,而invokeAny返回集合中的任务的一个,但不知道返回哪一个任务的Future对象
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}

ScheduledExecutorService接口源码:

  • ScheduledExecutorService接口继承了ExecutorService接口,因此ExecutorService接口里的方法均可使用,此外还额外增加了用于延迟或周期性调度任务的方法。
public interface ScheduledExecutorService extends ExecutorService {
   // 创建具有延迟的任务
    public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
    public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
    // 创建并执行具有周期频率或延迟的任务
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit);
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                 long initialDelay,
                                                 long delay,
                                                 TimeUnit unit);
}

A、线程池的创建

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService workStealingPool = Executors.newWorkStealingPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

B、向线程池提交任务

  • 创建线程时,使用Runable接口实现线程的(继承Thread类本质上属于使用了Runable接口):
// 执行任务方式一
fixedThreadPool.execute(new MyThread());
// 执行任务方式二
Future future = fixedThreadPool.submit(new MyThread());
logger.info("任务是否执行完毕:" + future.get());
  • 创建线程时,使用Callable接口实现线程的:
Future future = fixedThreadPool.submit(new MyCallable());
logger.info("获取返回值:" + future.get());

注意:

1.使用具有返回Future对象的submit()方法,在Runable接口,可以通过Future对象的get()方法判断任务是否执行完毕(返回null表示任务已执行完毕~):

2.Callable接口中的call()方法有返回值,也可以通过Future对象的get()方法去获取:

C、线程池的关闭

程序通过 main() 方法启动,当主线程退出了程序后,如果ExecutorService继续存在,程序将继续保持运行状态,而ExecutorService中存在的活动线程是不会被JVM关闭的,因此需要shutdown()或shutdownNow()进行关闭。

  • shutdown():不会立即关闭线程池,而是不再接受新提交的任务,等待线程池中没有执行完的任务线程执行任务完毕后关闭线程池;
  • shutdownNow():尝试中断所有的线程(不管是正在执行任务的线程还是已经执行完任务的线程),返回正在执行任务的列表,但无法保证正在执行任务的线程能否被终止;
  • isShutdown():只要执行了shutdown()或shutdownNow(),返回true;
  • isTerminated():当所有的任务都终止时,才返回true;

举例:

有一个短任务和一个长任务,测试关闭线程池(以newFixedThreadPool为例):

@Test
void testExecutors() throws ExecutionException, InterruptedException {
    // 短任务
    Thread shortTask = new Thread(
            () -> logger.info("shortTask正在调用线程:" + Thread.currentThread().getName())
    );
    // 长任务
    Thread longTask = new Thread(
            () -> {
                logger.info("longTask正在调用线程:" + Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(5); //休眠5秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    // 创建FixedThreadPool类型的线程池
    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 5; i++) {
        fixedThreadPool.submit(shortTask); //提交短任务
    }
    logger.info("第一次判断是否Shutdown了线程池:" + fixedThreadPool.isShutdown());
    logger.info("第一次判断是否终止了所有的任务:" + fixedThreadPool.isTerminated());

    // 继续提交长任务(模拟线程池有个正在执行任务的线程还未执行完任务)
    fixedThreadPool.submit(longTask);
    // Shutdown当前的线程池
    fixedThreadPool.shutdown();
    logger.info("第二次判断是否Shutdown了线程池:" + fixedThreadPool.isShutdown());
    logger.info("第二次判断是否终止了所有的任务:" + fixedThreadPool.isTerminated());
    // shutdown线程池后测试一下能否继续提交新的任务,结果报拒绝异常,说明无法继续提交新任务到线程池
//    fixedThreadPool.submit(shortTask);

    // 延迟5秒,模拟线程池可以执行完长任务longTask
//    TimeUnit.SECONDS.sleep(5);
    fixedThreadPool.awaitTermination(5,TimeUnit.SECONDS);
    logger.info("第三次判断是否Shutdown了线程池:" + fixedThreadPool.isShutdown());
    logger.info("第三次判断是否终止了所有的任务:" + fixedThreadPool.isTerminated());
}

结果:

遇到的问题:

在线程池shutdown()之后再往线程池加入新任务,线程池执行拒绝策略:抛异常!!

上述代码将shutdown()改为shutdownNow()再进行测试,结果如下:

  • 依旧会拒绝在线程池shutdownNow()后往线程池加入新的任务;
  • 休眠5s时,休眠状态被打断,改成休眠3s依旧是,说明了shutdownNow()尝试并成功打断了线程池中所有正在执行任务的线程(包括longTask线程,休眠线程等)

【不积硅步无以至千里,共同进步吧~】

猜你喜欢

转载自blog.csdn.net/qq_29119581/article/details/114279521