java创建线程的4种方式

2024-11-18 0 313

在Java中,创建线程有多种方式,每种方式都有其适用场景和优缺点。以下是四种常见的创建线程的方式:

一、方式一:继承Thread类

这是最直观的方式,通过创建一个新的类继承Thread类,并重写其run方法。

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("数据:" + i);
        }
    }
}

public class Demo01 {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        MyThread thread2 = new MyThread();
        thread2.start();
        for (int i = 10; i < 100; i++) {
            System.err.println("Main:" + i);
        }
    }
}

优点:简单直观。

缺点:Java不支持多重继承,因此如果一个类已经继承了另一个类,那么它就不能再继承Thread类。

 二、方式二:实现Runnable接口创建线程目标类

这种方式更加灵活,因为它允许类继承其他类的同时实现Runnable接口。

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 {
    public static void main(String[] args) {
        A a = new A();
        new Thread(a).start();
        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);
        }
    }
}

优点:避免了Java单继承的限制,可以更容易地扩展功能。

缺点:需要手动管理线程的生命周期。

 三、使用Callable和FutureTask创建线程

Callable接口与Runnable类似,但它可以返回值并抛出异常。

class MyCall implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 200;
    }
}

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);
    }
}

优点:可以返回值和抛出异常。

缺点:使用起来比Runnable复杂,需要配合FutureTask使用。

java创建线程的4种方式

 

通过FutureTask类和Callable接口的联合使用可以创建能够获取异步执行结果的线程,具体步骤如下:

(1) 创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。

(2) 使用Callable实现类的实例构造一个FutureTask实例。

(3) 使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。

(4) 调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。

(5) 调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果。

按照以上步骤,通过Callable接口和Future接口相结合创建多线程,实例如下:

创建一个Callable接口的实现类:

public class CallableTaskDemo implements Callable {
    // 编写好异步执行的具体逻辑,可以有返回值。
    // (Runnable接口中的run()方法是没有返回值得,Callable接口的call()方法有返回值)
    @Override
    public Long call() throws Exception {
        Long startTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName()+" 线程开始运行");
        Thread.sleep(1000);

        for(int i=0;i<100000000;i++){
            int j = i*10000;
        }

        Long endTime = System.currentTimeMillis();
        Long used = endTime-startTime;
        System.out.println(Thread.currentThread().getName()+" 线程结束运行");
        return used;
    }
}

 在这个例子中有两个线程:一个是执行main()方法的主线程,叫作main;另一个是main线程通过thread.start()方法启动的业务线程,叫作callableTaskThread。该线程是一个包含FutureTask任务作为target的Thread线程。

public class CreateDemo {
    public static void main(String[] args) throws InterruptedException {
        CallableTaskDemo callableTaskDemo = new CallableTaskDemo();
        FutureTask<Long> futureTask = new FutureTask<Long>(callableTaskDemo);
        Thread thread = new Thread(futureTask,"callableTaskThread");
        thread.start();

        Thread.sleep(500);

        System.out.println("main线程执行一会");
        for(int i=0;i<100000000/2;i++){
            int j = i*10000;
        }
        // 获取并发任务的执行结果
        try {
            System.out.println(thread.getName()+" 线程占用时间:"+futureTask.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

 

main线程通过thread.start()启动callableTaskThread线程之后,会继续自己的事情,callableTaskThread线程开始并发执行。

callableTaskThread线程首先执行的是thread.run()方法,然后在其中会执行到其target(futureTask任务)的run()方法;接着在这个futureTask.run()方法中会执行futureTask的callable成员的call()方法,这里的callable成员(ReturnableTask实例)是通过FutureTask构造器在初始化时传递进来的、自定义的Callable实现类的实例。

说明:
Callable 接口:Callable 是一个泛型接口,call() 方法中定义了线程的任务,并返回一个结果。
Future 接口:Future 表示一个异步计算的结果,使用 get() 方法可以获取线程执行的结果。
ExecutorService:通常与 ExecutorService 结合使用,可以更好地管理线程池和任务。
优点:
返回结果:Callable 可以返回执行结果,而 Runnable 不能。
异常处理:Callable 可以抛出受检查的异常,便于处理复杂任务中的错误。
线程池支持:与 ExecutorService 结合,可以实现灵活的线程管理。
缺点:
相对复杂:相比 Runnable 和 Thread,Callable 和 Future 的使用稍微复杂一些,特别是涉及到线程池的管理。

 四、使用线程池

线程池是一种更加高效的方式来管理线程,它可以复用线程,减少创建和销毁线程的开销。

第一种:使用自带的API实现

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秒,就销毁线程。

代码演示: 

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创建线程的4种方式

 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接口

 java创建线程的4种方式

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();
        }
 
    }
}

总结:

每种创建线程的方式都有其适用场景。在实际开发中,我们通常会根据需求选择最合适的方式。例如,对于简单的后台任务,可以使用Runnable接口;对于需要返回结果的任务,可以使用Callable接口;而对于需要频繁创建和销毁线程的场景,使用线程池是更好的选择。

平台声明:以上文章转载于《CSDN》,文章全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,仅作参考。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/m0_56341622/article/details/143865368

遇见资源网 JAVA java创建线程的4种方式 http://www.ox520.com/157393.html

常见问题

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务