zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【并发基础】Java中线程的创建和运行以及相关源码分析

JAVA源码基础并发 分析 创建 运行 以及
2023-09-27 14:19:51 时间

目录

一、线程的创建和运行

1.1 创建和运行线程的三种方法

1.2 三者之间的继承关系

二、Thread类和Runnable接口的区别

2.1 Runnable接口可以实现线程之间资源共享,而Thread类不能

2.2 实现Runnable接口相对于继承Thread类的优点

三、实现 Runnable 接口和实现 Callable 接口的区别

四、Thread类和Runnable接口关于启动线程的源码解析

4.1 实现方法

4.2 Thread.start()方法源码分析

4.3 Runnable.run()方法源码分析

4.4 总结


一、线程的创建和运行

1.1 创建和运行线程的三种方法

Java里的程序天生就是多线程的,那么有几种启动线程的方式? 

Java 线程创建有3种方式:

  1. 继承 Thread 类并且重写 run 方法
  2. 实现 Runnable接口的 run 方法
  3. 使用 Callable接口和FutureTask类方式

1.2 三者之间的继承关系

public class Thread implements Runnable {}

Thread类也是实现的Runnable接口。

单独说下 FutureTask 的方式,这种方式的本身也是实现了Runnable 接口的 run 方法,看它的继承结构就可以知道。

前两种方式都没办法拿到任务的返回结果,但是 Futuretask 方式可以。

由此我们知道了,其实这三种创建方法的根源,都是来源于Runnable接口,这三种方法往上层追溯都能追到Runnble接口。

二、Thread类和Runnable接口的区别

2.1 Runnable接口可以实现线程之间资源共享,而Thread类不能

实际上Thread类和Runnable接口之间在使用上也是有所区别的,如果一个类继承Thread类,就不适合于多个线程共享资源,而实现了Runnable接口,则可以方便的实现资源的共享。

由上文我们就可以知道,Thread类和Runnable接口最大的区别就是继承Thread类不能资源共享,而实现Runnable接口可以资源共享。 

为什么Runnable可以共享数据:

总结起来原因就是用Runnable接口的方法可以对两个不同的Thread类的构造方法传入相同的实现Runnable接口的对象,那么这两个不同的Thread线程类本质操控的是同一个Runnable接口的实现对象了,调用的也是同一个run()方法,自然这两个线程下就实现了共享同一个Runnable实现类中的数据了。

如果两个Thread类的构造方法传入不同的Runnable接口实现类,那么两个Thread线程对象操作的不是同一个Runnable实现类,两个线程也就不能共享数据了。

2.2 实现Runnable接口相对于继承Thread类的优点

可见,实现Runnable接口相对于继承Thread类来说,有如下显著的优势:

  • 适合多个相同程序代码的线程去处理同一资源的情况。
  • 可以避免由于Java的单继承特性带来的局限。
  • 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
  • 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类

三、实现 Runnable 接口和实现 Callable 接口的区别

  1. Runnable 是自从 java1.1 就有了,而 Callable 是 1.5 之后才加上去的
  2. 实现 Callable 接口的任务线程能返回执行结果,而实现 Runnable 接口的任务线程不能返回结果
  3. Callable 接口的 call()方法允许抛出异常,而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛
  4. 加入线程池运行,Runnable 使用 ExecutorService 的 execute 方法,Callable 使用 submit 方法。注:Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞

四、Thread类和Runnable接口关于启动线程的源码解析

这里我们来讲一下Java中创建线程最经典的这两种方式,在底层源码是如何实现的。

4.1 实现方法

Java 中实现多线程有两种「基本方式」:继承 Thread 类和实现 Runnable 接口。从实现的编程手法来看,认为这是两种实现方式并无不妥。但是究其实现根源,这么讲其实并不准确。

其实多线程从根本上讲只有一种实现方式,就是实例化 Thread,并且提供其执行的 run 方法。无论你是通过继承 Thread还是实现 Runnable接口,最终都是重写或者实现了 run 方法。而你真正启动线程都是通过实例化 Thread,调用其 start 方法

来看下两种不同实现方式的例子:

1. 继承 Thread 方式:

public class MyThread extends Thread {  
    public void run() {  
        System.out.println("MyThread.run()");  
    }  
}  
MyThread myThread1 = new MyThread();  
MyThread myThread2 = new MyThread();  
myThread1.start();  
myThread2.start();  

2. 实现 Runnable 方式

public class MyThread extends OtherClass implements Runnable {  
    public void run() {  
         System.out.println("MyThread.run()");  
    }  
} 
MyThread myThread = new MyThread();  
Thread thread = new Thread(myThread);  
thread.start(); 

第一种方式中,MyThread 继承了 Thread 类,启动时调用的 start 方法,其实还是他父类 Thread 的 start 方法。并最终触发执行 Student 重写的 run 方法。

第二种方式中,MyThread 实现 Runnable 接口,将MyThread对象作为参数传递给 Thread 构造函数。接下来还是调用了 Thread 的 start 方法。最后则会触发传入的 Runnable 实现类的 run 方法。

两种方式都是创建 Thread 或者 Thread 的子类,通过 Thread 的 start 方法启动。唯一不同是第一种 run 方法实现在 Thread 子类中。第二种则是把 run 方法逻辑转移到 Runnable 的实现类中。线程启动后,第一种方式是 thread 对象运行自己的 run 方法逻辑,第二种方式则是调用 Runnable 实现的 run 方法逻辑。

相比较来说,第二种方式是更好的实践,原因如下:

  1. java 语言中只能单继承,通过实现接口的方式,可以让实现类去继承其它类。而直接继承 thread 就不能再继承其它类了;
  2. 线程控制逻辑在 Thread 类中,业务运行逻辑在 Runnable 实现类中。解耦更为彻底;
  3. 实现 Runnable 的实例,可以被多个线程共享并执行。而实现 thread 是做不到这一点的。

看到这里,你是不是很好奇,为什么程序中调用的是 Thread 的 start 方法,而不是 run 方法?为什么线程在调用 start 方法后会执行 run 方法的逻辑呢?接下来我们通过学习 start 方法的源代码来找到答案。

4.2 Thread.start()方法源码分析

Thread类的无参构造方法:

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

如果是直接创建Thread类对象,我们通过源码就能看出,传入到target是空。在这种情况下,我们需要在Thread的继承类中去覆写run()方法,这样在Thread类执行run()方法的时候,就是调用我们继承类中覆写的run()方法逻辑。

我们知道Thraed类的对象是不能直接调用run()方法的,那么它是如何调用run()方法的呢?下面我们接着来进行分析。

Thread类中start()方法源码:

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();    
    group.add(this);
    boolean started = false;
    try {        
        start0();        
        started = true;    
    } finally {
        try {
            if (!started) {                
                group.threadStartFailed(this);            
            }        
        } catch (Throwable ignore) {        
        }    
    }
}

这段代码足够简单,简单到没什么内容。主要逻辑如下:

  1. 检查线程的状态,是否可以启动;
  2. 把线程加入到线程 group 中;
  3. 调用了 start0 () 方法。

可以看到 Start 方法中最终调用的是 start0()方法,并不是 run 方法。那么我们再看 start0 方法源代码:

private native void start0();

什么也没有,因为 start0 是一个 native 方法,也称为 JNI(Java Native Interface)方法。JNI 方法是 Java和其它语言交互的方式。同样也是 Java代码和虚拟机交互的方式,虚拟机就是由 C++ 和汇编所编写。

由于 start0 是一个 native 方法,所以后面的执行会进入到 JVM 中。那么 run 方法到底是何时被调用的呢?这里似乎找不到答案了。

难道我们错过了什么?回过头来我们再看看 Start 方法的注解。其实读源代码的时候,要先读注解,否则直接进入代码逻辑,容易陷进去,出不来。原来答案就在 start 方法的注解里,我们可以看到:

/* 
* Causes this thread to begin execution; the Java Virtual Machine* calls the run method of this thread.* 
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* start method) and the other thread (which executes its
* run method).
*
*
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed execution.
*/

最关键一句: the Java Virtual Machine calls the run method of this thread。由此我们可以推断出整个执行流程如下:

start 方法调用了 start0 方法,start0 方法在 JVM 中,start0 中的逻辑会调用 run 方法。

至此,我们已经分析清楚从线程创建到 run 方法被执行的逻辑。但是通过实现 Runnbale 的方式实现多线程时,Runnable 的 run 方法是如何被调用的呢?

4.3 Runnable.run()方法源码分析

我们先从 Thread 的构造函数入手。原因是 Runnable 的实现对象通过构造函数传入 Thread。

Thread类构造方法源码:

public Thread(Runnable target) {    
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

可以看到 Runnable 实现作为 target 对象传递进来。再次调用了 init 方法,init 方法有多个重载,最终调用的是Thread类中的如下方法:

private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
    Thread parent = currentThread();
    if (g == null) {
        g = parent.getThreadGroup();
    }
    g.addUnstarted();
    this.group = g;
    this.target = target;
    this.priority = parent.getPriority();
    this.daemon = parent.isDaemon();
    setName(name);
    init2(parent);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;
    tid = nextThreadID();
}

此方法里有一行代码:

this.target = target;

 原来 target 是 Thread类的成员变量:

/* What will be run. */
private Runnable target;

此时,Thread 的 target 被设置为你实现业务逻辑的 Runnable 实现。

我们再看下Thread类的run 方法的代码:

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

看到这里是不是已经很清楚了,当你传入了 target时(target不为null),在执行Thread类的run()方法时其实会调用执行 target 的 run 方法。也就是执行你实现业务逻辑的方法,我们需要在实现Runnable接口的类中实现Runnable接口的run()方法。整体执行流程如下:

如果你是通过继承 Thread,重写 run 方法的方式实现多线程。那么在上图中的第三步执行的就是你重写的 run 方法。

我们回过头看看 Thread 类的定义:

public class Thread implements Runnable

原来 Thread 也实现了 Runnable 接口。怪不得 Thread 类的 run 方法上有 @Override 注解。所以继承 Thread类实现多线程,其实也相当于是实现 Runnable 接口的 run 方法。只不过此时,不需要再传入一个 Thread 类去启动。它自己已具备了 Thread 的功能,自己就可以运转起来。既然 Thread 类也实现了 Runnable 接口,那么 Thread 子类对象是不是也可以传入另外的 Thread 对象,让其执行自己的 run 方法呢?答案是可行的。

4.4 总结

以上对多线程的两种实现方式做了分析。在学习多线程的同时,我们也应该学习源代码中优秀的设计模式。Java 中多线程的实现采用了模板模式Thread 是模板对象,负责线程相关的逻辑,比如线程的创建、运行以及各种操作。而线程真正的业务逻辑则被剥离出来,交由 Runnable 的实现类去实现。线程操作和业务逻辑完全解耦,普通开发者只需要聚焦在业务逻辑实现

执行业务逻辑,是 Thread 对象的生命周期中的重要一环。这一步通过调用传入 Runnable 的 run 方法实现。Thread 线程整体逻辑就是一个模板,把其中一个步骤剥离出来由其他类实现,这就是模板模式。


相关文章:【并发基础】线程,进程,协程的详细解释
                  【操作系统】一篇文章带你快速搞懂用户态和内核态