Java并发学习之四种线程创建方式的实现与对比

栏目: 编程语言 · Java · 发布时间: 6年前

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

线程创建的几种方式

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

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

  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方式实现定时任务的方式,也是一种新的创建线程的方式,这里也没有多说,后续将有一篇专门说明定时任务的博文介绍其用法

IV. 其他

声明

尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见识有限,如有问题,请不吝指正,感激

扫描关注,java分享

Java并发学习之四种线程创建方式的实现与对比


以上所述就是小编给大家介绍的《Java并发学习之四种线程创建方式的实现与对比》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms

Algorithms

Robert Sedgewick、Kevin Wayne / Addison-Wesley Professional / 2011-3-19 / USD 89.99

Essential Information about Algorithms and Data Structures A Classic Reference The latest version of Sedgewick,s best-selling series, reflecting an indispensable body of knowledge developed over the ......一起来看看 《Algorithms》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具