多线程详解(1)--Runnable接口和Thread类

913 阅读6分钟

1、Runnalbe接口和Thread类有什么关系

通过源代码可以发现Thread类实现了Runnable接口,并且重写了Runnable接口中唯一定义的run() 方法

public interface Runnable {
    public abstract void run();
}


public class Thread implements Runnable {
    ...
}

在Java中,interface定义的是一种规范,如果某个类实现了这个这个接口,那么他一定要(或者在子类中)去实现这个interface中所规定的方法,也就是去重写这个方法。

Runnable接口定义了Java中所有的线程类都应该具有的方法,即run() 方法。也就是如果要实现一个线程类,必须要继承Runnable接口,并重写其中的run() 方法。但是这个方法是无返回的,如果当前线程执行完毕之后需要返回一些内容给请求方,可以使用Callable接口,这个后面会详细探讨,Callable接口的实现类最终也会被包装成Runnable接口实现类的一个属性。

Thread类实现了Runnable接口,对run方法进行了重写,同时支持了和线程相关的其他内容,比如线程的启动、结束、状态管理等方法。我们通过实现Runnable接口所定义的线程类只能通过Thread类才可以成功启动一个线程进行执行。Thread类是Java提供给编程人员启动一个线程的入口,只有通过Thread类的start方法,才可以成功启动一个新的线程。

所以,Runnable接口和Thread类的关系就是,Runnable接口定义了Java中线程类的实现规范,Thread类实现了Runnable接口,并提供了关于线程创建、启动、停止等相关的方法。

2、创建、启动线程的两种方式

线程的创建本质上线程类是要实现Runnable接口,并重写run方法,启动只能通过Thread类的start方法进行启动。

那么我们可以得到两种创建线程的方式:

  • 实现Runnable接口,并将这个实现类的实例作为参数传递给Thread类的构造方法中
  • 继承Thread类,间接的实现Runnable接口,重写run方法
// 方式1:通过实现Runnable接口
public class MyThread implements Runnable {
    ...
        
    @Override
    public void run() {
        ...
    }
}
Thread thread = new Thread(new MyThread());
thread.start();

// 也可以使用表达式+匿名类的形式
new Thread((() -> {
    System.out.println(Thread.currentThread().getName());
})).start();


// 方式2: 通过继承的方式来定义一个线程类
public class MyThread extends Thread {
    @Override
    public void run() {
        ...
    }
}
Thread thread = new MyThread();
thread.start();

方式2的创建启动方式很容易理解,我们创建的MyThread类是Thread类的子类,在调用的时候其实调用的是父类的start方法,进而完成的线程的启动。

方式1的创建启动方式稍微特殊一点,我们传入的是一个Runnable接口的实现类,那么Thread类是如何保证线程启动后,执行的是我们传入的实现类的run方法的呢,原因就在于线程的启动创建的过程中。

3、线程的创建启动过程

首先看一下线程的创建部分的代码,Thread类有个多个构造方法,主要是由于传入参数的不同,但是最终调用的都是init方法,我们这里主要看一下init方法即可。

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

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
    init(g, target, name, stackSize, null, true);
}
// 所有构造方法最终都会调用到这个方法上
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
	// 完成线程名字的赋值
    this.name = name;
	
    // 获取父线程并处理安全相关的内容,暂时可以不看
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    ...
        
    // 完成线程属性的赋值    
    this.group = g;
    this.daemon = parent.isDaemon();           // 是否是守护线程
    this.priority = parent.getPriority();      // 优先级
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();  // 类加载器
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =                           // 访问控制Context
        acc != null ? acc : AccessController.getContext();
    this.target = target;                                         // 传入的Runnable接口实现类
    setPriority(priority);
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);  // 继承ThreadLocalMap
    
    // 线程堆栈的大小,如果为0则表示忽略(有的虚拟机会忽略这个参数)
    this.stackSize = stackSize;

    // 设置线程id
    tid = nextThreadID();
}

其参数的主要内容为:

  • g:线程group相关内容
  • target:target实例的run方法将在新创建的线程中被调用
  • name:线程名字
  • stackSize:新线程的堆栈的大小,如果为0则表示忽略(有的虚拟机会忽略这个参数)
  • acc:访问控制相关
  • inheritThreadLocals:是否继承父类的threadLocals,为true则表示继承

通过上面的代码可以看到,通过方式1的形式创建一个线程的时候,我们编写的Runnable接口的实现类作为一个参数值保存在了Thread类的target属性中,同时启动一个线程的时候,是通过Thread类的start方法进行启动的,整个的调用过程是Thread.start() --> Thread.start0() --> Thread.run() ,那我们接下来主要看下这三段代码的具体内容。

public synchronized void start() {
	// ... 省略一些判断的代码
    boolean started = false;
    try {
        start0();    // 重点:在start方法中又去调用了start0方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            
        }
    }
}

// start0是一个native方法,也就是一个本地方法,是由Java虚拟机实现的
// 这个方法会启动一个新的线程,并在这个新线程中调用run方法
private native void start0();

// Thread类中重写的run方法,这个run方法真正的去调用了target中的run方法
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

根据上面的代码就可以明白方式1的启动中是如何调用到我们自己写的MyThread类的run方法了。

其实这个可以看成是一种静态代理的模式,Thread类在执行run方法前,完成参数的赋值,对parent线程的继承以及创建出一个新的线程等工作。我们编写的Runnable实现类就作为被代理的对象,在Thread的run方法中被真正的进行调用。

同理,方式2中的启动逻辑,通过重写了run方法,使得可以在Thread类的启动链里面直接执行到我们编写的线程run方法逻辑。

总结

  • Runnable 接口定义了线程类所应该具有的规范,即实现run方法
  • Thread 类实现了Runnable接口,并实现了线程启动的相关逻辑
  • 线程创建启动有两种方式:1、参数传入Runnable接口的实现类; 2、继承Thread类并重写run方法
  • 线程启动的方法调用链:Thread.start() -> Thread.start0() -> Thread.run() -> target.run()