1.目录&前言
前言:
在日常开发中,我们会经常使用到多线程,比如WEB服务中的tomcat服务器,其核心组件之一正是使用多线程去处理并发请求。本文将以讲解源码的方式深入了解多线程,主要包括多线程的创建、执行机制等。
由于篇幅有限,因此多线程的创建方式将分为上下两部分进行讲解,此篇为上部分,主要讲解的是没有返回值的多线程。
下部分为有返回值的多线程Java源码学习之高并发编程基础——多线程的创建(下)
目录:
2.什么是多线程
在计算机中,运行的基本单位是进程,而线程则是比进程更小的资源,线程存在于进程中,共享进程中的数据;不同进程之间的数据是相互隔离的,它们共享计算机的内存等资源。
随着电脑硬件的发展,CPU从最开始的单核心变成多核心,多核心的CPU是多线程的实现基础,在Java中,运行一个程序即表示运行一个JVM进程,JVM进程可以创建多个线程。
可以简单想象为,多线程就是一台电脑中多开的应用程序,运行中的电脑就是进程。
3.源码分析
本章节将结合Java面向对象特去讲解多线程的创建方式、以及其执行机制,对于笔者来说,以前背八股文的时候,总有那么一道问题:创建多线程的方式。答案是记住了,但是并没有深入我的脑子里,因为了解一件事情最好去理解它的底层逻辑而非记住表面。
3.1 Thread类源码剖析
对于创建多线程的方式,网上许多文章说有继承Thread、实现Runnable接口、使用FutureTask或线程池之类的,但是笔者想说,在Java中实现多线程编程,归根到底只有一个方式——创建一个Thread实例对象。
笔者为什么这么说呢?让我们先看看Thread类,官方对它的解释是Thread是程序中的执行线程,Java 虚拟机允许应用程序同时运行多个执行线程。也就是说,Thread类才是实实在在的多线程执行载体。
JVM建议启动一个线程的方式是调用其start()方法,从以下的伪代码可以看出,start()方法被synchronized关键字修饰,也就是说在同一时刻start()方法只能执行一次 ,直到它结束为止。
在其start()方法里面,调用了native的方法start0(),它的底层是C语言写的一个方法,目的就是在JVM进程中创建一个新的线程(或许不应该描述为创建新的线程,应该是创建一个更小的、独立的运行单位)去调用当前Thread实例的run()方法。
public class Thread implements Runnable {
private Runnable target; // 这个是多线程的目标对象
private Thread(Runnable target){ // 构造函数
this.targett = target;
}
// 启动一个线程的方法,调用其start()方法
public synchronized void start() {
if (threadStatus != 0){
throw new IllegalThreadStateException();
}
boolean started = false;
try {
start0();
started = true;
} finally {
....
}
}
private native void start0(); // 底层是C方法,主要是调用下面的run方法
@Override
public void run() {
// 当多线程的目标对象不是null的时候才调用它的run方法实现
// 属于自己的业务luoj
if (target != null) {
target.run();
}
}
}
在Thread#run()方法中,逻辑一目了然,就是当(Runnable) target成员属性不为空时,调用其run()方法。那么这个Runnable和其run()方法充当着什么角色呢?
3.2 Runnable接口源码剖析
Runnable是一个线程执行逻辑的接口,它定义了抽象的run()方法,也就是子类实现此接口时,必须重写run方法,注意它没有返回值。
@FunctionalInterface
public interface Runnable {
// 无返回值的run方法
public abstract void run();
}
而Thread正是实现了Runnable接口,在其重写的run()方法中正是尝试调用其成员属性(Runnable)target的run方法。
3.3 实现多线程的方式
介绍到这里,我们知道JVM中创建更小的执行单元(即线程)是通过Thread#start()完成的,并且在其start()方法被调用后,实际上会执行Thread#run()方法,在这里面也是委托给其(Runnable) target实例调用。
3.3.1 继承Thread的方式实现多线程
也就是说,我们可以通过继承Thread的方式去实现多线程编程。
如下图所示,静态类MyThread继承并重写了run()方法,在主线程的main函数中的for循环一共创建了三个实例并调用其start()方法,此时JVM中便会为这三个Thread实例分配运行单元并调用其run()方法。
public class MutipleThreadTest1 {
public static void main(String[] args) {
for (int i=0;i<3;i++){
new MyThread().start();
}
}
static class MyThread extends Thread{
@Override
public void run(){
System.out.println("当前线程:"+Thread.currentThread()+"正在工作");
}
}
}
----------------输出结果--------------------------
当前线程:Thread[Thread-0,5,main]正在工作
当前线程:Thread[Thread-2,5,main]正在工作
当前线程:Thread[Thread-1,5,main]正在工作
----------------输出结果--------------------------
在MyThread的run()方法中,逻辑很简单,就是输出一行日志。其中用到了Thread.currentThread()方法,它的作用是返回当前执行线程的名称。
从输出结果看,for循环中的确一共创建了三个线程,所有的线程都完成了各自的业务逻辑,打印出了包含当前线程名称的调试结果。
3.3.2 实际Runnable接口的方式实现多线程
观察Thread#run()方法,其实也是调用成员属性(Runnable) target的run方法,那么我们可以通过实例化一个Runnable对象、实现run方法去实现多线程。
先回顾Thread的run()方法,它里面逻辑相当简单就是当target不为null时就调用其重写Runnable接口的run()方法,再结合Thread的构造函数,其实我们可以将一个Runnable实例当作构造函数入参去创建一个新的Thread实例,因为启动一个新线程的方式是通过调用Thread#start()方法,而不是直接运行run()方法。
直接运行run方法并没有通过C方法的调度是无法真正启动一个线程的,只是当作一个普通的类实例方法调用。
在以下伪代码中,创建了一个实现了Runnable接口的静态类MyRunnable,它重写了run方法,而在main函数中,直接将MyRunnable实例作为入参调用Thread的构造函数,当启动线程的时候,就会进入到重写方法,输出如下结果。
public class MutipleThreadTest2 {
public static void main(String[] args) {
for (int i=0;i<3;i++){
new Thread(new MyRunnable()).start();
}
}
static class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("当前线程:"+Thread.currentThread()+"正在工作");
}
}
}
----------------输出结果--------------------------
当前线程:Thread[Thread-1,5,main]正在工作
当前线程:Thread[Thread-2,5,main]正在工作
当前线程:Thread[Thread-0,5,main]正在工作
----------------输出结果--------------------------
输出结果和继承Thread实现的多线程一模一样,均看到的确在for循环中创建了三个线程,并都在其run方法中打印了包含每个线程自己名称的调试结果。
3.3.3 推荐哪种方式实现多线程编程?
官方推荐的是通过创建Runnable实例的方式。
因为从Java面向对象的思想来说,对一个类子类化的时候,其实就是对父类的某些功能(方法)做修改或增强。但是在介绍Thread#run()方法的时候我们知道了,实际的线程业务逻辑执行是通过Thread的(Runnable) target成员变量执行的。
也就是说我们可以直接通过设置Thread实例的target属性即可完成同样的操作,子类化的操作显然显得有点冗余,除非你需要改造Thread类中的其他方法(但是往往一般只用到run方法)。
其次,Java中的类是单继承、多实现的机制,如果通过继承Thread的方式去实现多线程,则当前类的扩展能力就受到了限制。
此外,实现Runnable接口的方式更能体现数据与操作隔离,这是什么意思?
以商品秒杀服务为例,商品数量有限并且对所有用户是可见的,继承Thread和实现Runnable接口实现的多线程抢购行为封装到run方法中。
假设多线程代表着不同的用户,那么不同实现方式的伪代码如下所示。均在重写的run方法中进行“抢购”行为、减去商品数量。
但是可以看到,实现Runnable接口的方式,只需要new一个实例出来即可通过创建多个Thread实例共享其数据,实际上一个业务服务的代码也是封装到一个Java类去。
而继承Thread类的方式,由于各个实例对象之间数据隔离,因此必须通过构造函数等方式将共享的商品数量变量传入。也就是说,这种方式下其实数据和操作并没有隔离开,因为总是需要通过设置属性值的方式告诉线程我能读到的值是多少。
public class MutipleThreadTest3 {
public static void main(String[] args) {
GoodsServiceRunnable goodsServiceRunnable = new GoodsServiceRunnable();
AtomicInteger goodsNum = new AtomicInteger(5);
for (int i=0;i<10;i++){
new GoodsServiceThread(goodsNum).start();
new Thread(goodsServiceRunnable).start();
}
}
static class GoodsServiceRunnable implements Runnable {
private volatile AtomicInteger goodsNum = new AtomicInteger(5);
@Override
public void run() {
System.out.println("当前(实现Runnable)线程:" + Thread.currentThread() +
"正在抢购商品,数量剩下:" + (goodsNum.decrementAndGet()));
}
}
static class GoodsServiceThread extends Thread {
private AtomicInteger goodsNum;
public GoodsServiceThread(AtomicInteger goodsNum) {
this.goodsNum = goodsNum;
}
@Override
public void run() {
System.out.println("当前(继承Thread)线程:" + Thread.currentThread() +
"正在抢购商品,数量剩下:" + (goodsNum.decrementAndGet()));
}
}
}
--------------------输出结果-------------------------
当前(继承Thread)线程:Thread[Thread-0,5,main]正在抢购商品,数量剩下:4
当前(继承Thread)线程:Thread[Thread-2,5,main]正在抢购商品,数量剩下:3
当前(实现Runnable)线程:Thread[Thread-1,5,main]正在抢购商品,数量剩下:4
当前(实现Runnable)线程:Thread[Thread-3,5,main]正在抢购商品,数量剩下:3
当前(继承Thread)线程:Thread[Thread-6,5,main]正在抢购商品,数量剩下:2
当前(继承Thread)线程:Thread[Thread-4,5,main]正在抢购商品,数量剩下:1
当前(实现Runnable)线程:Thread[Thread-7,5,main]正在抢购商品,数量剩下:2
当前(继承Thread)线程:Thread[Thread-8,5,main]正在抢购商品,数量剩下:0
当前(实现Runnable)线程:Thread[Thread-5,5,main]正在抢购商品,数量剩下:1
当前(实现Runnable)线程:Thread[Thread-9,5,main]正在抢购商品,数量剩下:0
--------------------输出结果-------------------------
实现Runnable接口的方式,更能体现数据和操作的隔离,因为在for循环中,创建的Thread实例接收的Runnable实例都是同一个的,不像继承Thread一样,每次都要将商品数量传入。
最后,还和其他方式实现的多线程编程有关。本文讲解的继承Thread类、实际Runnable接口所重写的run方法都是没有返回值,那么有返回值的多线程编程又是怎么样的?
由于篇幅原因,有返回值的多线程编程将在下部分文章讲解,但是我们可以提前知道,它的实现也是借助于Runnable接口,后面要讲的线程池也是依赖于Runnable接口
3.4 使用到的设计模式
看到这里,相信大家也猜到了这是什么的设计模式,没错,通过实现Runnable接口实现多线程编程的方式,其实是一种代理模式,Thread并不负责执行多线程的业务逻辑,而是通过其目标属性target去执行的。即Thread在这里充当一个代理对象的角色,(Runnable) target就是目标对象的角色。
4. 总结
实现多线程的方式(无返回值)有两种,继承Thread类、实例化实现Runnable接口的对象将其设置到Thread实例,并都重写run方法。
前者执行run方法是属于继承了父类,子类调用父类方法过程中若有子类重写的则优先调用子类的。后者则是利用代理模式,只要设置一个目标,让代理对象(Thread实例)帮我们调用执行run方法。
推荐实例化实现Runnable接口的对象的方式去实现多线程编程,因为这种方式扩展性比继承来说更高、其他的多线程实现方式(有返回值的以及线程池)也依赖于Runnable接口。
但无论通过哪种方式创建多线程,本质上都是通过一个Thread实例调用其start()方法、进而调用对应的C方法,通知JVM创建更小的执行单元去执行其run()方法。
下部分的Java源码学习之高并发编程基础——多线程的创建(下) ,将还是以类和接口源码解读的方式去讲解能获取返回值的多线程编程的方式——FutureTask。