多线程基础

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

多线程向来是面试的重灾区,它真的很难。一般来说,普通的开发人员很少有机会能直接写多线程,一般都是使用封装好的工具类或者JDK提供的API,但并不意味着我们可以不用了解多线程。

学习多线程的好处至少有两点:

  • 应付面试
  • 深入理解并发编程,帮助我们更好地使用JDK提供的锁及JUC工具包

今天我们再来强化一下多线程的基础知识。

线程类与任务类

我在初学多线程时,也感到云里雾里,后来才发现自己一开始就把一些概念搞错了!很多博客或者视频每次都会说“实现”多线程的方式有3种:Thread、Runnable和Callable,其实我个人是不太认同这种说法的。Thread是线程类,另外两个是任务类,不是一类事物啊!

也即是说,JDK看似提供了很多和多线程相关的类,实际上有且仅有Thread类能通过start0()方法向操作系统申请线程资源(本地方法)。

线程池只是对Thread的复用,两者本质上可以归为一类,这里暂且不讨论

另外,JVM的线程和操作系统的线程是一一对应的,当我们new一个Thread对象并调用start()方法后,JVM的Thread对象就与系统底层的一个线程资源绑定了。

而Runnable和Callable并不会产生线程,仅仅用于包裹待执行的任务。如果没有线程或线程池去执行,它们只不过是一坨普通的代码。

为了让Thread和Runnable/Callable产生关联,通常需要我们手动进行“组合”(Thread只能接收Runnable,Callable要使用线程池,后面再介绍):

new Thread(() -> {
    // 把任务包裹在Runnable任务类中,通过构造参数传入Thread
    System.out.println("待执行任务");
}).start();

但实际开发中,大家肯定也见过这种写法:

new Thread() {
    @Override
    public void run() {
        // 直接重写Thread的run(),把待执行的任务放里面
        System.out.println("待执行任务");
    }
}.start();

注意,上面这种写法可不是传入Runnable/Callable,而是采用匿名类的方式重写了Thread自己的run()。

总结一下,要想让多线程执行一个任务,大致有以下几种做法:

  • 线程与任务不分离
    • 重写Thread的run(),把任务直接塞到Thread内部,执行路径是:JVM线程-->Thread#run()
  • 线程与任务分离(推荐)
    • 把任务塞到Runnable,丢入Thread里,执行路径是:JVM线程-->Thread#run()-->target.run()
    • 把任务塞到Runnable/Callable,丢到线程池里,屏蔽内部细节,省心省力

注意,当Thread#start()向操作系统申请线程后,线程的执行入口始终是Thread#run(),而不是Runnable/Callable的run()

到这里,我们替大家扫清了一些繁杂的概念,只需记住Java创建多线程有且仅有一种方式:Thread!无论是直接通过new Thread().start(),或是通过线程池,底层其实都是Thread在向操作系统申请资源,而且新线程启动后,会找到原来的Thread,从它的run()方法开始执行。

任务代码是如何被执行到的?

上面提到过,如果需要多线程帮我们执行任务,一般有3种方式:

  • 重写Thread#run()
  • 将任务包装成Runnable,丢入Thread
  • 将任务包装成Runnable/Callable,丢入线程池

那么,为什么我把代码放在这些指定的地方,线程就能执行到呢?

其实代码的执行顺序,说到底也是人为设计、编排的,只不过Thread、线程池等设计得更为精妙。整个过程就像一条流水线,从上游到下游,我们只要把商品放入指定的地方,最终就会被封口、打包并装箱。

这里主要介绍方式1、2的原理,线程池的执行原理后面会介绍。Thread是如何执行任务的呢?关键在于Thread#run():

Thread#start()的作用是向操作系统申请线程资源,当操作系统分配好线程资源,并且当前线程得到CPU执行权时,执行的入口一定是Thread#run()。

那么线程执行run()时,会发生什么呢?

  • 如果我们重写了Thread#run(),就会执行我们重写的方法(任务和线程不分离)
  • 如果我们没有重写Thread#run()
    • target为null,run()为空,直接结束
    • target不为null,就会执行target的run()。而target就是我们通过Thread构造器传入的Runnable对象

所以,如果不重写Thread#run(),就一定要传入任务类(target),否则新建的Thread无任务可执行,就浪费了

最后,再来回顾一下让线程执行指定任务的几种常见方式:

public class AsyncAndWaitTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式1:重写Thread#run()
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "========>正在执行");
            }
        };
        thread.start();

        // 方式2:构造方法传入Runnable实例
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "========>正在执行");
        }).start();

        // 方式3:线程池 + Callable/Runnable,这里以Callable为例
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<String> submit = executorService.submit(() -> {
            System.out.println(Thread.currentThread().getName() + "========>正在执行");
            Thread.sleep(3 * 1000L);
            return "success";
        });
        String result = submit.get();
        System.out.println("result=======>" + result);
        // 关闭线程池
        executorService.shutdown();
    }
}

除了上面3种利用多线程执行任务的方式,我们再另外介绍一种初学者可能觉得有点绕、但源码里经常会见到的写法,算是Runnable的变种写法

把new Thread().start()隐藏到某个类的内部

重点关注Worker到底是什么,以及begin()内部做了什么。不熟悉的同学不妨自己写一下,琢磨一下。我们马上会在JDK的某个类中看到类似的写法!

总之,在我心里Thread的Level要比Runnbale、Callable高一级。看到很多人把它们混在一起,不禁想起一句话:

萧某大好男儿,竟和你这种人齐名! --- 乔峰

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值