java中创建多线程的4种方式

目录

一、继承 Thread 类创建线程

步骤

示例代码

原理

二、实现 Runnable 接口创建线程

步骤

示例代码

原理

三、实现 Callable 接口创建线程

步骤

示例代码

原理

与Runnable接口相比的不同之处

四、使用线程池创建线程

步骤

示例代码(使用 Executors.newFixedThreadPool)

原理

线程池的优势

自定义线程池

五、总结


        在 Java 编程中,多线程是一项非常重要的技术,它能够充分利用计算机的多核处理器资源,提高程序的执行效率和响应性。本文将详细介绍 Java 中创建多线程的四种方式,包括继承 Thread 类、实现 Runnable 接口、实现 Callable 接口以及使用线程池,并对每种方式的原理、代码示例和适用场景进行深入剖析。

一、继承 Thread 类创建线程

步骤

  • 创建一个类继承自 Thread 类。
  • 重写 run 方法,在 run 方法中定义线程要执行的任务。
  • 创建该类的实例,然后调用 start 方法启动线程。

示例代码

class MyThread extends Thread{

    // Ctrl + o
    // 展示所有的可以重写的方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("数据:"+i);
        }
    }
}
public class Demo01 {

    /**
     *  第一种方案,继承Thread类  重写run方法 实现
     * @param args
     */
    public static void main(String[] args) {

        // 在Main方法中,启动了一个子线程,子线程什么时候工作
        MyThread thread = new MyThread();
        thread.start();// 启动一个线程,调用start方法,不要调用run方法

        // 一个线程类,是可以创建多个不同的子线程的
        MyThread thread2 = new MyThread();
        thread2.start();// 启动一个线程,调用start方法,不要调用run方法

        // 主线程,直接运行代码   会出现子线程和主线程抢占资源的情况
        for (int i = 10; i < 100; i++) {

            System.err.println("Main:"+i);
        }

    }
}

原理

        当调用 start 方法时,会在新的线程中执行 run 方法。需要注意的是,start 方法只是启动线程,不会立即执行 run 方法。线程要等待获取 CPU 资源后才会执行 run 方法,而且在执行过程中可能会被其他线程抢占 CPU 资源。

二、实现 Runnable 接口创建线程

步骤

  • 创建一个类实现 Runnable 接口。
  • 实现 run 方法,在 run 方法中定义线程要执行的任务。
  • 创建 Runnable 接口实现类的实例,将其作为参数传递给 Thread 类的构造函数,然后调用 start 方法启动线程。

示例代码

class A implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
public class Demo03 {

    /**
     *  多线程创建的第二种方式,使用 Runnable接口
     *  该接口还需要传递给Thread类才能启动,否则自己启动不了
     *
     *  两种方式:推荐使用第二种
     *   1、Thread类是一个线程类,它只需要管理好线程就行了,不需要管业务怎么写
     *   2、具体的业务可以交给Runnable接口实现
     *   3、java是单继承的,继承了Thread,就无法继承别的类了,但是可以实现多个接口。
     */
    public static void main(String[] args) {
        A a = new A();

        new Thread(a).start();

        // Runnable接口本身就是一个函数式接口,就可以使用lambda表达式,代码可以简化为如下:
        new Thread( ()-> {
            for (int i = 0; i < 1000; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }).start();

        for (int i = 0; i < 1000; i++) {
            System.err.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

原理

        Runnable 接口定义了一个无返回值的 run 方法,用于包含线程要执行的代码。Thread 类的构造函数可以接收一个 Runnable 接口的实现对象,当调用 Thread 的 start 方法时,会在新的线程中执行 Runnable 对象的 run 方法。这种方式比继承 Thread 类更灵活,因为 Java 是单继承的,如果一个类已经继承了其他类,就不能再继承 Thread 类了,但是可以实现 Runnable 接口来实现多线程。

三、实现 Callable 接口创建线程

步骤

  • 创建一个类实现 Callable 接口,该接口是一个函数式接口,有一个泛型参数,用于指定返回值类型。
  • 实现 call 方法,在 call 方法中定义线程要执行的任务,并返回一个结果。
  • 创建 Callable 接口实现类的实例,将其包装在一个 FutureTask 对象中,FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 接口继承了 Runnable 和 Future 接口。
  • 将 FutureTask 对象作为参数传递给 Thread 类的构造函数,然后调用 start 方法启动线程。可以通过 FutureTask 的 get 方法获取 call 方法的返回结果。

示例代码

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

class MyCall implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        return 200;
    }
}

class MyRun implements Runnable{

    @Override
    public void run() {
        System.out.println("我是子线程....");
    }
}
public class Demo08 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCall());
        new Thread(futureTask,"计算线程").start();
        Integer i = futureTask.get();
        System.out.println(i);
        // --------------------------------------
        new Thread(new MyRun()).start();
        // ------------------使用callable 模拟 子线程进行大量计算并返回结果------------------
        FutureTask<Integer> f1 = new FutureTask<>(
                ()->{
                    System.out.println(Thread.currentThread().getName()+"  come in callable");
                    TimeUnit.SECONDS.sleep(4);

                    return 1024;
                }
        );

        FutureTask<Integer> f2 = new FutureTask<>(
                ()->{
                    System.out.println(Thread.currentThread().getName()+"  come in callable");
                    TimeUnit.SECONDS.sleep(4);
                    return 2048;
                }
        );

        new Thread(f1,"线程一:").start();
        new Thread(f2,"线程二:").start();

        while(!f1.isDone()){
            System.out.println("f1  wait中.....");
        }
        while(!f2.isDone()){
            System.out.println("f2  wait中.....");
        }
        // 其实 get 获取不到值会一直阻塞,直到获取到值为止
        int a = f1.get();
        int b = f2.get();
        System.out.println(a+b);


    }
}

原理

        Callable 接口与 Runnable 接口类似,但是 Callable 接口的 call 方法可以有返回值,并且可以抛出异常。FutureTask 用于包装 Callable 对象,它可以在未来某个时刻获取 call 方法的返回结果。通过这种方式,可以实现有返回值的多线程任务。

与Runnable接口相比的不同之处

(1)是否有返回值

(2)是否抛异常
(3)落地方法不一样,一个是run,一个是call

四、使用线程池创建线程

步骤

  • 通过 Executors 工具类的静态方法(如 newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutor)创建一个线程池对象,或者直接使用 ThreadPoolExecutor 类来创建自定义的线程池。
  • 创建 Runnable 或 Callable 接口实现类的实例,作为任务提交给线程池。对于 Runnable 任务,可以使用 execute 方法提交;对于 Callable 任务,可以使用 submit 方法提交。

示例代码(使用 Executors.newFixedThreadPool

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {

    public static void main(String[] args) {

//       ExecutorService threadPool =  Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
//       ExecutorService threadPool =  Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口
       ExecutorService threadPool =  Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口

        //10个顾客请求
        try {
            for (int i = 1; i <=10; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+"\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

原理

        线程池用于管理和复用线程。当提交一个任务到线程池时,线程池会根据自身的状态和配置来决定如何处理任务。如果线程池中有空闲线程,就会将任务分配给空闲线程执行;如果没有空闲线程且线程数量未达到最大限制,就会创建新的线程来执行任务;如果线程数量达到最大限制且任务队列已满,会根据线程池的拒绝策略来处理任务。这样可以有效地控制线程的数量,提高系统的性能和资源利用率,减少线程创建和销毁的开销。

线程池的优势

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用;控制最大并发数;管理线程。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。

第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。

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

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。

经常使用的线程池做法

1、Executors.newFixedThreadPool(int)

执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程

newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

2、Executors.newSingleThreadExecutor()

一个任务一个任务的执行,一池一线程。

newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue

3、Executors.newCachedThreadPool()

执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强。

newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

自定义线程池


虽然根据API 我们能很轻松的使用到线程池,但是在实际开发中我们经常自定义线程池,怎么做呢?

参数说明

1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间   
当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时, 多余线程会被销毁直到只剩下corePoolSize个线程为止。
4、unit:keepAliveTime的单位 
5、workQueue:任务队列,被提交但尚未被执行的任务  就是我们之前讲的阻塞队列
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
请求执行的runnable的策略

线程池的拒绝策略

AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,俗称从哪儿来到哪儿去。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。

以上内置拒绝策略均实现了RejectedExecutionHandle接口

1、在创建了线程池后,线程池中的线程数为零。

2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

        2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

        2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

        2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

        2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:

        如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

        所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

 示例代码

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
/**
 * 线程池
 * Arrays
 * Collections
 * Executors
 */
public class MyThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(
                2,
                5,
                2L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(),
                //new ThreadPoolExecutor.AbortPolicy()
                //new ThreadPoolExecutor.CallerRunsPolicy()
                //new ThreadPoolExecutor.DiscardOldestPolicy()
                new ThreadPoolExecutor.DiscardOldestPolicy()
        );
        //10个顾客请求
        try {
            for (int i = 1; i <= 10; i++) {
                threadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 办理业务");
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }
}

五、总结

        Java 提供了多种创建多线程的方式,每种方式都有其特点和适用场景。继承 Thread 类简单直接,适用于简单的线程任务;实现 Runnable 接口更加灵活,适合在已有类层次结构中使用多线程;实现 Callable 接口可用于需要获取线程执行结果的场景;使用线程池则可以高效地管理和复用线程,适用于需要大量线程处理任务的情况,并且可以通过合理配置线程池参数来优化系统性能。在实际开发中,需要根据具体的需求和场景选择合适的多线程创建方式,以充分发挥多线程编程的优势,提高程序的质量和效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天冬忘忧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值