深入浅出Java线程基础

1,260 阅读8分钟

不得不说,JavaSE阶段除了面向对象,最难的就是多线程。

多线程仿佛天生就是个难题,入门难不说,而且根本看不到技术的上限。最为关键的是,面试必考!像淘宝京东这样的亿万高并发系统都有它的身影。另外,多线程和系统底层关系密切,不要说非科班选手,就是正儿八经学了四年C++的人,也经常在面试中被多线程一掌拍死。

本篇文章将会和大家一起梳理关于多线程的几个小细节,并试图给出“轻松一点”的答案及概念,帮助非科班的朋友更好更快地把握多线程学习要点。

内容介绍:

  • 线程、进程概念与区别
  • 创建多线程的2种常见方式
  • Thread源码浅析
  • 继承Thread VS 实现Runnable

线程、进程概念与区别

进程

当我们双击桌面的图标时,系统会将对应的程序加载进内存,程序将会占用一部分内存用以执行操作。进入到内存的程序即为进程(一个应用程序可以同时运行多个进程)。当使用任务管理器关闭程序时(比如QQ),系统又会将程序从内存中清除,此时进程结束。

可以理解为:进程指的是占用一定内存的程序。当内存中的程序被清除,进程即结束。

图片.png 图片.png

一个应用程序可以同时运行多个进程:

图片.png

线程

线程是进程中的一个执行单元,负责当前进程中程序的执行。一个进程中至少有一个线程。

区别

进程是资源分配的单位,线程是执行单位。早期操作系统没有线程,只有进程。但是进程非常“重”,进程间切换成本高。为了降低并发导致的进程切换成本,提出了线程。一个进程可以拥有多个线程。尽量让线程间进行切换,线程不拥有资源(或者说是很少的必要的资源)。

多个线程抢占CPU执行权:

图片.png

需要注意的是,Java本身并不能创造线程,因为线程其实是操作系统的一种资源,它由操作系统管理。我们一般说“Java支持多线程”,指的就是Java可以调用系统资源创建多线程。

图片.png

单线程方法调用链(main):

图片.png

开启多线程:

图片.png

创建多线程的2种常见方式

Java中有两种最常用的创建多线程的方式(线程池和Callable下次介绍)。

继承Thread类,重写run()方法

public class ThreadDemo1 extends Thread {
    public static void main(String[] args) {
        // ThreadDemo1继承了Thread类,并重写run()
        ThreadDemo1 t = new ThreadDemo1();
        // 开启线程:t线程得到CPU执行权后会执行run()中的代码
        t.start();
    }

    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

实现Runnable接口,实现run()方法

public class ThreadDemo2 implements Runnable{
    public static void main(String[] args) {
        // ThreadDemo2实现Runnable接口,并实现run()
        ThreadDemo2 target = new ThreadDemo2();
        // 调用Thread构造方法,传入TreadDemo2的实例对象,创建线程对象
        Thread t = new Thread(target);
        // 开启线程:t线程得到CPU执行权后会执行run()中的代码
        t.start();
    }

    public void run() {
        System.out.println("Thread is running");
    }
}

图片.png

上面两段代码,相信大家早已烂熟于心,就不再赘述。这里要提一点:很多初学者,在学习多线程时被反复强调“实际编程往往只用实现Runnable接口的方式”,久而久之,便觉得Thread类干脆没啥用了,只要有Runnable接口就行。

实在本末倒置,好生糊涂!其实Thread类才是最重要的,它才是多线程的核心。

Thread源码浅析

Runnable里面仅定义了一个抽象方法run():

图片.png

从程序运行上来看,这个接口基本没什么卵用。之所以搞出Runnable接口,目的有两个:

  • 限定Thread构造方法的形参类型(针对方式2说的)
  • 将run()向上抽取,做成抽象方法,让实现类去重写(为什么?)

为了更好地理解上面两句话,先来观察Thread类的源码(截取部分):

public class Thread implements Runnable {
    /* What will be run. */
    private Runnable target;
   
    // 构造方法
    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    
    // 构造方法
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

在理解上面源码后,我们开个上帝视角重新看一下Java创建多线程的2种方式:

图片.png

图片.png 你以为方式2中t.start()开启线程后直接调用本类的run()?NO!它要曲线救国!

现在,大家应该对平时习以为常的这两段代码有了更深刻的理解。但还是不够。我们还没明白上面那两句话到底是什么意思?

  • “限定Thread构造方法的形参类型”

其实这句话,是针对创建多线程的方式2说的。方式2需要我们在创建Thread实例时传入Runnable的实现类对象:

// ThreadDemo2实现Runnable接口,并实现run()
 ThreadDemo2 target = new ThreadDemo2();
 // 调用Thread构造方法,传入TreadDemo2的实例对象,创建线程对象
 Thread t = new Thread(target);

为什么要传Runnable实现类对象?因为对于方式2而言,要执行的代码并不在Thread线程本身,而是在Runnable的实现类中,所以必须传入一个对象告诉线程去哪执行。而Thread的有参构造方法的形参类型是Runnable:

 // 构造方法
 public Thread(Runnable target) {
     init(null, target, "Thread-" + nextThreadNum(), 0);
 }

所以方式2要求我们写的类必须implements Runnable,这就是“限定Thread构造方法的形参类型”这句话的背后含义:

Thread的有参构造函数只允许接受Runnable的实现类对象(包括Thread子类对象,因为观察源码,我们发现Thread也实现了Runnable)

图片.png 把Thread类看做大水桶,但是入口很严格,由Runnable把持。如果我们的类没有实现Runnable,就无法“塞进”Thread中。

  • 将run()向上抽取,做成抽象方法,强制实现类去重写(为什么?)

为什么要把run向上抽取做成抽象方法呢?这其实是由Runnable、Thread以及他们各自的实现类及子类的继承体系决定的:

图片.png

仔细观察,会有以下发现:

  • 一个线程执行,总是从start()开始,因为它才是开启线程的钥匙。线程开始后会自动调用Thread的run()
  • run()的本质,只是为了“包裹”需要线程执行的代码块

我们实际编码时,工作量只有黄色虚线框内的代码,也就是编写Thread子类或者Runnable实现类。

虽然看似有很多run(),但是线程被start()“唤醒”后,只会去调用Thread的run(),这个run()可能来自Thread类(方式2),也可能来自Thread的子类对象(方式1)。换言之,Thread类(及其子类)是线程运行的入口!没了Thread,Runnable及其实现类就是摆设。

图片.png

Thread类及其子类永远是入口,方式2写在Runnable实现类中代码之所以能被执行到,仅仅是因为Thread的run()中调用了target.run()。

继承Thread VS 实现Runnable

文章开头已经说了,实际编程往往选择实现Runnable的方式创建多线程。为什么?其实也有点“解耦”的味道的在里面。编程界有句老话:“没有什么问题是引入第三方解决不了的”,而实现Runnable的方式,把原本线程类中的“待执行代码”挪到了Runnable实现类中,硬生生整出了“第三方”。

实现Runnable的好处恰恰在于“执行者”与“被执行者”被分离了。反观继承Thread这种方式虽然便捷,但是线程和待运行的代码在同一个类中,无法做到资源独立,也就无法共享。

图片.png

注意,左边继承Thread方式并没有做到资源共享,因为每个子类对象都有各自的一份run(),各玩各的互不影响。

小结

  • Runnable是函数式接口。它的功能是

    • 为了规范Thread有参构造的传值类型
    • 将run()向上抽取,做成抽象方法,让实现类去实现
  • Thread是多线程的命脉,是入口,没有它多线程无从谈起。不论是方式1还是方式2,切入点都是Thread的run(),然后去执行其中代码,只是方式2更加曲折一些,最终又绕回到Runnable实现类的run()

  • 实现Runnable的方式更常用,因为它分离了线程与资源。实际编程往往只是把Runnable写成匿名对象,不会去另外写一个类。每New一个Thread就塞一个Runnable,所以也谈不上共享。

方式1(继承Thread类的变种写法,用匿名对象方式,无法共享资源):
new Thread(){
    @Override
    public void run() {
        System.out.println("The code waiting for Thread1");
    }
}.start();
        
方式2(Runnable匿名对象,只能当前线程用一次,无法共享资源):
new Thread(new Runnable() {
    public void run() {
        System.out.println("The code waiting for Thread2");
    }
}).start();
        
方式3(Runnable实现,可以多个线程共享):
Runnable r = new Runnable() {
    System.out.println("The code waiting for Threads");
};
Thread t1  = new Thread(r);
Thread t2  = new Thread(r);
Thread t3  = new Thread(r);
Thread t4  = new Thread(r);
Thread t5  = new Thread(r);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();

思考题

最后留一个思考题,猜猜看打印的是什么?:

new Thread(new Runnable() {
    public void run() {
        System.out.println("Runnable's run method is running");
    }
}){
    @Override
    public void run() {
        System.out.println("Thread's run method is running");
    }
}.start();

我是bravo1988,下次见。

よろしく・つづく

我昨晚梦见你了.gif

往期文章:

漫画:从JVM锁扯到Redis分布式锁

公司禁止JOIN查询怎么办?

深入浅出Java注解

Tomcat外传:孤独的小猫咪