不得不说,JavaSE阶段除了面向对象,最难的就是多线程。
多线程仿佛天生就是个难题,入门难不说,而且根本看不到技术的上限。最为关键的是,面试必考!像淘宝京东这样的亿万高并发系统都有它的身影。另外,多线程和系统底层关系密切,不要说非科班选手,就是正儿八经学了四年C++的人,也经常在面试中被多线程一掌拍死。
本篇文章将会和大家一起梳理关于多线程的几个小细节,并试图给出“轻松一点”的答案及概念,帮助非科班的朋友更好更快地把握多线程学习要点。
内容介绍:
- 线程、进程概念与区别
- 创建多线程的2种常见方式
- Thread源码浅析
- 继承Thread VS 实现Runnable
线程、进程概念与区别
进程
当我们双击桌面的图标时,系统会将对应的程序加载进内存,程序将会占用一部分内存用以执行操作。进入到内存的程序即为进程(一个应用程序可以同时运行多个进程)。当使用任务管理器关闭程序时(比如QQ),系统又会将程序从内存中清除,此时进程结束。
可以理解为:进程指的是占用一定内存的程序。当内存中的程序被清除,进程即结束。
一个应用程序可以同时运行多个进程:
线程
线程是进程中的一个执行单元,负责当前进程中程序的执行。一个进程中至少有一个线程。
区别
进程是资源分配的单位,线程是执行单位。早期操作系统没有线程,只有进程。但是进程非常“重”,进程间切换成本高。为了降低并发导致的进程切换成本,提出了线程。一个进程可以拥有多个线程。尽量让线程间进行切换,线程不拥有资源(或者说是很少的必要的资源)。
多个线程抢占CPU执行权:
需要注意的是,Java本身并不能创造线程,因为线程其实是操作系统的一种资源,它由操作系统管理。我们一般说“Java支持多线程”,指的就是Java可以调用系统资源创建多线程。
单线程方法调用链(main):
开启多线程:
创建多线程的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");
}
}
上面两段代码,相信大家早已烂熟于心,就不再赘述。这里要提一点:很多初学者,在学习多线程时被反复强调“实际编程往往只用实现Runnable接口的方式”,久而久之,便觉得Thread类干脆没啥用了,只要有Runnable接口就行。
实在本末倒置,好生糊涂!其实Thread类才是最重要的,它才是多线程的核心。
Thread源码浅析
Runnable里面仅定义了一个抽象方法run():
从程序运行上来看,这个接口基本没什么卵用。之所以搞出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种方式:
你以为方式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)
把Thread类看做大水桶,但是入口很严格,由Runnable把持。如果我们的类没有实现Runnable,就无法“塞进”Thread中。
- 将run()向上抽取,做成抽象方法,强制实现类去重写(为什么?)
为什么要把run向上抽取做成抽象方法呢?这其实是由Runnable、Thread以及他们各自的实现类及子类的继承体系决定的:
仔细观察,会有以下发现:
- 一个线程执行,总是从start()开始,因为它才是开启线程的钥匙。线程开始后会自动调用Thread的run()
- run()的本质,只是为了“包裹”需要线程执行的代码块
我们实际编码时,工作量只有黄色虚线框内的代码,也就是编写Thread子类或者Runnable实现类。
虽然看似有很多run(),但是线程被start()“唤醒”后,只会去调用Thread的run(),这个run()可能来自Thread类(方式2),也可能来自Thread的子类对象(方式1)。换言之,Thread类(及其子类)是线程运行的入口!没了Thread,Runnable及其实现类就是摆设。
Thread类及其子类永远是入口,方式2写在Runnable实现类中代码之所以能被执行到,仅仅是因为Thread的run()中调用了target.run()。
继承Thread VS 实现Runnable
文章开头已经说了,实际编程往往选择实现Runnable的方式创建多线程。为什么?其实也有点“解耦”的味道的在里面。编程界有句老话:“没有什么问题是引入第三方解决不了的”,而实现Runnable的方式,把原本线程类中的“待执行代码”挪到了Runnable实现类中,硬生生整出了“第三方”。
实现Runnable的好处恰恰在于“执行者”与“被执行者”被分离了。反观继承Thread这种方式虽然便捷,但是线程和待运行的代码在同一个类中,无法做到资源独立,也就无法共享。
注意,左边继承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,下次见。
よろしく・つづく
往期文章: