创建线程的方式实现与对比

线程创建的几种方式

在并发编程中,最基本的就是创建线程了,那么一般的创建姿势是怎样的,又都有些什么区别

一般来讲线程创建有四种方式:

  1. 继承Thread
  2. 实现Runnable接口
  3. 实现Callable接口,结合 FutureTask使用
  4. 利用该线程池ExecutorService、Callable、Future来实现

所以本篇博文从布局来讲,分为两部分

  1. 实例演示四种使用方式
  2. 对比分析四种使用方式的异同,以及适合的应用场景

I. 实例演示

目标: 创建两个线程并发实现从1-1000的累加

1. 继承Thread实现线程创建

实现逻辑如下

public class AddThread extends Thread {

    private int start, end;

    private int sum = 0;

    public AddThread(String name, int start, int end) {
        super(name);
        this.start = start;
        this.end = end;
    }
    public void run() {
        System.out.println("Thread-" + getName() + " 开始执行!");
        for (int i = start; i <= end; i ++) {
            sum += i;
        }
        System.out.println("Thread-" + getName() + " 执行完毕! sum=" + sum);
    }

    public static void main(String[] args) throws InterruptedException {
        int start = 0, mid = 500, end = 1000;

        AddThread thread1 = new AddThread("线程1", start, mid);
        AddThread thread2 = new AddThread("线程2", mid + 1, end);

        thread1.start();
        thread2.start();

        // 确保两个线程执行完毕
        thread1.join();
        thread2.join();

        int sum = thread1.sum + thread2.sum;
        System.out.println("ans: " + sum);
    }
}

输出结果

Thread-线程1 开始执行!
Thread-线程2 开始执行!
Thread-线程1 执行完毕! sum=125250
Thread-线程2 执行完毕! sum=375250
ans: 500500

一般实现步骤:

  • 继承 Thread
  • 覆盖 run() 方法
  • 直接调用 Thread#start() 执行

逻辑比较清晰,只需要注意覆盖的是run方法,而不是start方法

2. 实现Runnable接口方式创建线程

public class AddRun implements Runnable {

    private int start, end;
    private int sum = 0;

    public AddRun(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 开始执行!");
        for(int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + " 执行完毕! sum=" + sum);
    }


    public static void main(String[] args) throws InterruptedException {
        int start = 0, mid = 500, end = 1000;
        AddRun run1 = new AddRun(start, mid);
        AddRun run2 = new AddRun(mid + 1, end);
        Thread thread1 = new Thread(run1, "线程1");
        Thread thread2 = new Thread(run2, "线程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        int sum = run1.sum + run2.sum;
        System.out.println("ans: " + sum);
    }
}

输出结果

线程2 开始执行!
线程1 开始执行!
线程2 执行完毕! sum=375250
线程1 执行完毕! sum=125250
ans: 500500

一般实现步骤:

  • 实现Runnable接口
  • 获取实现Runnable接口的实例,作为参数,创建Thread实例
  • 执行 Thread#start() 启动线程

说明

相比于继承Thread,这里是实现一个接口,最终依然是借助 Thread#start()来启动线程

然后就有个疑问:

两者是否有本质上的区别,在实际项目中如何抉择?

3. 实现Callable接口,结合FutureTask创建线程

Callable接口相比于Runnable接口而言,会有个返回值,那么如何利用这个返回值呢?

demo如下

public class AddCall implements Callable<Integer> {

    private int start, end;

    public AddCall(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        System.out.println(Thread.currentThread().getName() + " 开始执行!");
        for (int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + " 执行完毕! sum=" + sum);
        return sum;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        int start = 0, mid = 500, end = 1000;
        FutureTask<Integer> future1 = new FutureTask<>(new AddCall(start, mid));
        FutureTask<Integer> future2 = new FutureTask<>(new AddCall(mid + 1, end));

        Thread thread1 = new Thread(future1, "线程1");
        Thread thread2 = new Thread(future2, "线程2");

        thread1.start();
        thread2.start();

        int sum1 = future1.get();
        int sum2 = future2.get();
        System.out.println("ans: " + (sum1 + sum2));
    }
}

输出结果

线程2 开始执行!
线程1 开始执行!
线程2 执行完毕! sum=375250
线程1 执行完毕! sum=125250
ans: 500500

一般实现步骤:

  • 实现Callable接口
  • Callable的实现类为参数,创建FutureTask实例
  • FutureTask作为Thread的参数,创建Thread实例
  • 通过 Thread#start 启动线程
  • 通过 FutreTask#get() 阻塞获取线程的返回值

说明

Callable接口相比Runnable而言,会有结果返回,因此会由FutrueTask进行封装,以期待获取线程执行后的结果;

最终线程的启动都是依赖Thread#start

4. 线程池方式创建

demo如下,创建固定大小的线程池,提交Callable任务,利用Future获取返回的值

public class AddPool implements Callable<Integer> {
    private int start, end;

    public AddPool(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        System.out.println(Thread.currentThread().getName() + " 开始执行!");
        for (int i = start; i <= end; i++) {
            sum += i;
        }
        System.out.println(Thread.currentThread().getName() + " 执行完毕! sum=" + sum);
        return sum;
    }

    public static void main(String[] arg) throws ExecutionException, InterruptedException {
        int start=0, mid=500, end=1000;
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Integer> future1 = executorService.submit(new AddPool(start, mid));
        Future<Integer> future2 = executorService.submit(new AddPool(mid+1, end));

        int sum = future1.get() + future2.get();
        System.out.println("sum: " + sum);
    }
}

输出结果

pool-1-thread-1 开始执行!
pool-1-thread-2 开始执行!
pool-1-thread-1 执行完毕! sum=125250
pool-1-thread-2 执行完毕! sum=375250
sum: 500500

一般实现逻辑:

  • 创建线程池(可以利用JDK的Executors,也可自己实现)
  • 创建Callable 或 Runnable任务,提交到线程池
  • 通过返回的 Future#get 获取返回的结果

II. 对比分析

1. 分类

上面虽然说是有四种方式,但实际而言,主要划分为两类

  • 继承Thread类,覆盖run方法填写业务逻辑
  • 实现Callable或Runnable接口,然后通过Thread或线程池来启动线程

此外,还有一种利用Fork/Join框架来实现并发的方式,后续专门说明,此处先略过

2. 区分说明

继承和实现接口的区别

先把线程池的方式拎出来单独说,这里主要对比Thread, Callable, Runnable三中方式的区别

个人理解,线程的这两种方式的区别也就只有继承和实现接口的本质区别:

一个是继承Thread类,可以直接调用实例的 start()方法来启动线程;另一个是实现接口,需要借助 Thread#start()来启动线程

继承因为java语言的限制,当你的任务需要继承一个自定义的类时,会有缺陷;而实现接口却没有这个限制


至于网上很多地方说的实现Runnable接口更利于资源共享什么的,比如下面这种作为对比的

public class ShareTest {
    private static class MyRun implements Runnable {
        private volatile AtomicInteger ato = new AtomicInteger(5);
        @Override
        public void run() {
            while (true) {
                int tmp = ato.decrementAndGet();
                System.out.println(Thread.currentThread() + " : " + tmp);
                if (tmp <= 0) {
                    break;
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        MyRun run = new MyRun();
        Thread thread1 = new Thread(run, "线程1");
        Thread thread2 = new Thread(run, "线程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println("over");
    }
}

输出结果

Thread[线程1,5,main] : 4
Thread[线程2,5,main] : 3
Thread[线程1,5,main] : 2
Thread[线程2,5,main] : 1
Thread[线程1,5,main] : 0
Thread[线程2,5,main] : -1
over

MyRun实现Runnable接口,然后创建一个实例,将这个实例作为多个Thread的参数构造Thread类,然后启动线程,发现这几个线程共享了 MyRun#ato 变量

然而上面这个实现接口改成继承Thread,其他都不变,也没啥两样

public class ShareTest {

    private static class MyRun extends Thread {
        private volatile AtomicInteger ato = new AtomicInteger(5);
        @Override
        public void run() {
            while (true) {
                int tmp = ato.decrementAndGet();
                System.out.println(Thread.currentThread() + " : " + tmp);
                if (tmp <= 0) {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRun run = new MyRun();
        Thread thread1 = new Thread(run, "线程1");
        Thread thread2 = new Thread(run, "线程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println("over");
    }
}

输出如下

Thread[线程1,5,main] : 4
Thread[线程2,5,main] : 3
Thread[线程1,5,main] : 2
Thread[线程1,5,main] : 0
Thread[线程2,5,main] : 1
Thread[线程2,5,main] : -1
over

上面除了说明使用Runnable更利于资源共享神马的,其实并没有之外,还有一个比较有意思的,为什么会输出-1?

如果我这个任务是售票的话,妥妥的就超卖了,这个问题留待后续详解


Runnable, Callable两种区别

这两个就比较明显了,最根本的就是

  • Runnable 无返回结果
  • Callable 有返回结果

从根源出发,就直接导致使用姿势上的区别

举个形象的例子说明两种方式的区别:

小明家今儿没米了,小明要吃饭怎么办?

小明他妈对小明说,去你大爷家吃饭吧,至于小明到底吃没吃着,小妈他妈就不管了,这就是Runnable方式;

小明他妈一想,这一家子都要吃饭,我先炒个菜,让小明去大爷家借点米来,所以就等着小明拿米回来开锅,这就是Callable方式

1.Runnable

Runnable不关心返回,所以任务自己默默的执行就可以了,也不用告诉我完成没有,我不care,您自己随便玩,所以一般使用就是

new Thread(new Runnable() { public void run() {...} }).start()

换成JDK8的 lambda表达式就更简单了 new Thread(() -> {}).start();

2.Callable

相比而言,callbale就悲催一点,没法这么随意了,因为要等待返回的结果,但是这个线程的状态我又控制不了,怎么办?借助FutrueTask来玩,所以一般可以看到使用方式如下:

FutureTask<Object> future = new FutureTask<>(() -> null);
new Thread(future).start();
Object obj = future.get(); // 这里会阻塞,直到线程返回值

Thread启动和线程池启动方式

这个就高端了,线程池一听就感觉厉害了,前面的四中方式有三种都是Thread#start()来启动线程,这也是我们最常用的方式,这里单独说一下线程池的使用姿势

  • 首先是创建一个线程池
  • 利用 ExecutorService#submit()提交线程
  • Future<Object> 接收返回
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> future1 = executorService.submit(()-> 10);
int ans = future1.get();

说明,这里提交线程之后,并不表示线程立马就要执行,也不表示一定可以执行(这个留待后续线程池的学习中探讨)


III. 小结

四种创建方式

  1. 继承Thread类,覆盖run方法,调用 Thread#start启动
  2. 实现Runnable接口,创建实例,作为Thread构造参数传入,调用 Thread#start启动 new Thread(() -> {}).start()
  3. 实现Callable接口,创建实例,作为FutureTask<>构造参数创建FutureTask对象,将FutureTask对象作为Thread构造参数传入,调用 Thread#start启动 FutureTask<Object> future = new FutureTask<>(() -> null); new Thread(future).start(); Object obj = future.get(); // 这里会阻塞,直到线程返回值
  4. 创建一个线程池,利用 ExecutorService#submit()提交线程,Future<Object> 接收返回 ExecutorService executorService = Executors.newFixedThreadPool(2); Future<Integer> future1 = executorService.submit(()-> 10); int ans = future1.get();

区别与应用场景

  • 继承和实现接口的方式唯一区别就是继承和实现的区别,不存在共享变量的问题
  • 需要获取返回结果时,结合 FutureTask和Callable来实现
  • Thread和Runnable的两种方式,原则上想怎么用都可以,个人也没啥好推荐的,随意使用
  • 线程池需要注意线程复用时,对ThreadLocal中变量的串用问题(本篇没有涉及,等待后续补上)

注意

  • 利用线程池创建线程,实际上依然是借助的Runnable或者Callable,能否算一种新的方式纯看个人理解
  • 采用Timer方式实现定时任务的方式,也是一种新的创建线程的方式,这里也没有多说。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
是的,通过实现Callable接口也可以创建线程。在Java中,实现Callable接口是创建线程的另一种方式,相比较于实现Runnable接口,Callable接口可以返回线程的执行结果,同时也可以抛出异常。创建线程的步骤如下: 1. 定义一个类,实现Callable接口,并重写call()方法。 2. 在call()方法中编写线程的代码逻辑。 3. 创建该类的实例。 4. 创建FutureTask对象,将该类的实例作为参数传入FutureTask的构造方法中。 5. 创建Thread对象,将FutureTask对象作为参数传入Thread的构造方法中。 6. 调用Thread对象的start()方法启动线程。 下面是一个示例代码: ``` import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class MyCallable implements Callable<String> { public String call() throws Exception { // 线程执行的代码逻辑 return "Hello, World!"; } } // 创建线程 MyCallable callable = new MyCallable(); FutureTask<String> futureTask = new FutureTask<>(callable); Thread thread = new Thread(futureTask); thread.start(); // 获取线程的执行结果 String result = futureTask.get(); System.out.println(result); ``` 在上面的示例中,我们定义了一个名为MyCallable的类,实现了Callable接口,并重写了call()方法。在call()方法中,我们编写了线程的代码逻辑,并返回了一个字符串"Hello, World!"。 然后,我们创建了该类的实例,并将该实例作为参数传递给FutureTask的构造方法中。然后,我们创建了Thread对象,并将FutureTask对象作为参数传递给Thread的构造方法中。最后,我们调用Thread对象的start()方法启动线程。 需要注意的是,在使用Callable接口创建线程时,需要将Callable接口的泛型参数指定为线程执行结果的类型。在本例中,我们指定了泛型参数为String类型。同时,为了获取线程的执行结果,我们使用了FutureTask类来包装Callable对象,并在最后调用了FutureTask的get()方法获取线程的执行结果。 总之,实现Callable接口也是一种创建线程方式,可以通过该方式获取线程的执行结果,同时也可以抛出异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值