摘要:无论它由子类覆写提供还是由对象提供,方法最终都会新建一个线程来执行这个方法。这种方法看上去好像复杂了好多,但其实就是通过新建类的对象来创建线程。总结在中,创建一个线程,有且仅有一种方式创建一个类实例,并调用它的方法。
前言
系列文章目录
谈到线程同步与通信,线程本身的概念是绕不开的,而进程和线程的概念已经是老生常谈的话题了,一些基本的概念本文就不再讨论了,本篇仅仅致力于通过源码,了解线程的构造与启动,从而更深入的了解线程。
本文源码基于jdk1.8 。
阅读完本文,你应当有能力回答以下常见面试题:
创建线程有哪几种方式?
如何启动一个线程?
线程的run方法和start方法有什么区别?
Runnale接口我们看Thread类的定义知道,它实现了Runable接口
public class Thread implements Runnable { ... }
而Runnable接口的定义如下:
@FunctionalInterface public interface Runnable { public abstract void run(); }
它只有一个抽象方法run。同时,该接口还被@FunctionalInterface注解标注,说明它是一个函数式接口(@FunctionalInterface是java 1.8版本之后引入的)。这意味着我们可以使用Lambda表达式来创建Runnable接口的实例,这个我们到后面再举例。
线程创建在java中,创建一个线程,有且仅有一种方式:
创建一个Thread类实例,并调用它的start方法。
这写在了java语言规范中(参见The Java Language Specification, Java SE 8 Edition, P659,chapter17):
Threads are represented by the Thread class. The only way for a user to create a thread is to create an object of this class; each thread is associated with such an object. A thread will start when the start() method is invoked on the corresponding Thread object.构造函数
要创建一个Thread类的实例自然要通过构造函数,Thread的public构造函数有8个之多,但是他们本质上都调用了同一个init函数:
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); } public Thread(String name) { init(null, null, name, 0); } public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } public Thread(Runnable target, String name) { init(null, target, name, 0); } public Thread(ThreadGroup group, Runnable target) { init(group, target, "Thread-" + nextThreadNum(), 0); } public Thread(ThreadGroup group, String name) { init(group, null, name, 0); } public Thread(ThreadGroup group, Runnable target, String name) { init(group, target, name, 0); } public Thread(ThreadGroup group, Runnable target, String name, long stackSize) { init(group, target, name, stackSize); }
可见,这八个public类型的构造函数只不过是给init的方法的四个参数分别赋不同的值, 这四个参数分别是:
ThreadGroup g(线程组)
Runnable target (Runnable 对象)
String name (线程的名字)
long stackSize (为线程分配的栈的大小,若为0则表示忽略这个参数)
而init方法又调用了另一个init方法,设置了AccessController,以及inheritThreadLocals参数:
/** * Initializes a Thread with the current AccessControlContext. * @see #init(ThreadGroup,Runnable,String,long,AccessControlContext,boolean) */ private void init(ThreadGroup g, Runnable target, String name, long stackSize) { init(g, target, name, stackSize, null, true); } //上面那个init方法最终调用了下面这个方法: /** * Initializes a Thread. * * @param g the Thread group * @param target the object whose run() method gets called * @param name the name of the new Thread * @param stackSize the desired stack size for the new thread, or * zero to indicate that this parameter is to be ignored. * @param acc the AccessControlContext to inherit, or * AccessController.getContext() if null * @param inheritThreadLocals if {@code true}, inherit initial values for * inheritable thread-locals from the constructing thread */ private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { ... }
init方法中有一些关于线程组和访问控制上下文的设置,这里我们暂时就不深入讨论了。
所以综上来看,我们最常用的也就两个参数:
Runnable target (Runnable 对象)
String name (线程的名字)
而对于线程的名字,其默认值为"Thread-" + nextThreadNum(), nextThreadNum方法又是什么呢:
/* For autonumbering anonymous threads. */ private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; }
可见,它就是一个简单的递增计数器,所以如果创建线程时没有指定线程名,那线程名就会是:
Thread-0, Thread-1, Thread-2, Thread-3, ...
至此,我们看到,虽然Thread类的构造函数有这么多,但对我们来说真正重要的参数只有一个:
Runnable target (Runnable 对象)
所以创建一个线程实例最重要的是要传入一个Runnable类型对象。
既然是Runnable类型,那么这个target必然是实现了Runnable接口的,也就是说该对象一定覆写了run方法。
我们知道,Thread类本身也实现了Runnable接口,所以它必然也覆写了run方法,我们先来看看它的run方法:
@Override public void run() { if (target != null) { target.run(); } }
可以看到,这个run方法仅仅是调用了target对象的run方法,如果我们在线程构造时没有传入target(例如调用了无参构造函数),那么这个run方法就什么也不会做。
启动线程线程对象创建完了之后,接下来就是启动一个线程,在java中,启动一个线程必须调用线程的start方法:
/** * Causes this thread to begin execution; the Java Virtual Machine * calls therun
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. * * @exception IllegalThreadStateException if the thread was already * started. * @see #run() * @see #stop() */ public synchronized void start() { /** * This method is not invoked for the main method thread or "system" * group threads created/set up by the VM. Any new functionality added * to this method in the future may have to also be added to the VM. * * A zero status value corresponds to state "NEW". */ if (threadStatus != 0) throw new IllegalThreadStateException(); /* Notify the group that this thread is about to be started * so that it can be added to the group"s list of threads * and the group"s unstarted count can be decremented. */ group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { /* do nothing. If start0 threw a Throwable then it will be passed up the call stack */ } } } private native void start0()
这个方法本质是调用了native的start0()方法,但是它的注释部分说明一些很重要的信息:
这个方法使得线程开始执行,并由JVM来执行这个线程的run方法,结果就是有两个线程在并发执行,一个是当前线程,也就是调用了Thread#start方法的线程,另一个线程就是当前thread对象代表的线程,它执行了run方法。
也就是说,这个Thread类实例代表的线程最终会执行它的run方法,而上面的分析中我们知道,它的run做的事就是调用Runnable对象的run方法,如果Runnable对象为null, 就啥也不做:
@Override public void run() { if (target != null) { target.run(); } }
有的同学就要问了,绕了一大圈,忙了大半天,最后不就是为了执行target对象的run方法吗?为什么我们不直接调用target的run方法?这一层层的调用究竟是为了啥? 答案是:
为了使用多线程 !
我们知道,Thread类从定义上看就是个普通的java类,是什么魔法让它从一个普通的java类晋升为一个可以代表线程的类呢?是native方法!
如果我们直接调用target对象的run方法,或者Thread类的run方法,那就是一个普通调用,因为run方法就是普普通通的类方法,与我们平时调用的其他类方法没有什么不同,这并不会产生多线程。
但是,如果我们调用了start方法,由于它内部使用了native方法来启动线程,它将导致一个新的线程被创建出来, 而我们的Thread实例, 就代表了这个新创建出来的线程, 并且由这个新创建出来的线程来执行Thread实例的run方法。
实战说了这么多理论的东西,下面让我们通过一个实战来加深理解。java官方文档给我们提供了两种创建线程的方法.
方法1:继承Thread类,覆写run方法首先我们自定义一个继承自Thread的类,并覆写run方法:
public class CustomizedThread extends Thread { public void run() { System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我是定义在CustomizedThread类中的run方法。"); } }
然后我们创建类的实例,并调用start方法启动这个线程:
public class CustomizedThreadTest { public static void main(String[] args) { System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里"); CustomizedThread myThread = new CustomizedThread(); myThread.start(); } }
执行结果:
[main线程]: 我在main方法里 [Thread-0线程]: 我是定义在CustomizedThread类中的run方法。
可见,这里有两个线程,一个是main线程,它执行了main方法,一个是Thread-0线程,它是我们自定义的线程,它执行了run方法。
如果我们不通过start方法来运行线程会有什么不同呢:
public class CustomizedThreadTest { public static void main(String[] args) { System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里"); CustomizedThread myThread = new CustomizedThread(); //myThread.start(); myThread.run(); } }
这里我们直接调用自定义线程的run方法,看看结果有什么不同:
[main线程]: 我在main方法里 [main线程]: 我是定义在CustomizedThread类中的run方法。
可见,这次只有一个main线程,由main线程执行了我们自定义线程类的run方法,并没有新的线程产生。 其实这个时候,CustomizedThread的run方法就是一个普普通通的类的普普通通的方法,与我们平时定义的方法并没有什么特别之处。
有的同学要问了,上面不是说创建一个线程最重要的是传入一个Runnable对象吗? 我没有看到Runnable对象啊? 别急,我们来分析一下:
首先,我们的CustomizedThread继承自Thread类,则我们会调用父类的无参构造函数:
public Thread() { init(null, null, "Thread-" + nextThreadNum(), 0); }
这个构造函数中,target对象为null;
然后,我们使用了myThread.start(),因为我们在子类中没有定义start方法,所以,这个方法来自父类,而Thread类的start方法的作用我们已经讲过,它将新建一个线程,并调用它的run方法,这个新建的线程的抽象代表就是我们的CustomizedThread,所以它的(CustomizedThread的)run方法将会被调用。
那么,如果我们的子类没有覆写run方法呢?,那自然是继承Thread类自己的run方法了:
@Override public void run() { if (target != null) { target.run(); } }
而Thread类的run方法调用的又是target对象的run方法,而target对象现在为null, 所以这个方法啥也不做。
所以到这里我们就很清晰了,创建一个线程最重要的是定义一个run方法,这个run方法要么通过继承Thread类的子类覆写,要么通过直接构造Thread类时传入一个Runnable的target对象。无论它由子类覆写提供还是由target对象提供,start方法最终都会新建一个线程来执行这个run方法。
方法2:通过Runnable接口创建线程类我们先来看官方的例子:
class PrimeRun implements Runnable { long minPrime; PrimeRun(long minPrime) { this.minPrime = minPrime; } public void run() { // compute primes larger than minPrime . . . } } //The following code would then create a thread and start it running: PrimeRun p = new PrimeRun(143); new Thread(p).start();
这个例子中首先定义了一个PrimeRun类实现了Runnable接口,接着实例化出一个对象p,并将这个对象作为参数传递给Thread类的构造方法。
这种方法看上去好像复杂了好多,但其实就是通过新建Thread类的对象来创建线程。它本质上就是传递一个Runnable对象给Thread的构造函数,所以我们完全可以用匿名类,又因为Runnable是一个函数接口,所以上面的代码完全可以被简写,我们来看一个例子:
public class CustomizedThreadTest { public static void main(String[] args) { System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里"); Thread myThread = new Thread(() -> System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我是传递给Thread类的Runnable对象的run方法")); myThread.start(); } }
代码输出:
[main线程]: 我在main方法里 [Thread-0线程]: 我是传递给Thread类的Runnable对象的run方法
这里,myThread是我们new出来的Thread类的实例,我们调用了Thread类的构造函数:
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
传入了一个Runnable对象,这个Runnable对象由lambda表达式表示。我们最后调用了 myThread.start()来启动这个线程,通过上一节的分析我们知道,start方法会调用run方法,而thread类的run方法最终会调用target对象的run方法,而target对象的run方法就是我们传进来的lambda表达式。上面这个例子其实等效于下面这种写法:
public class CustomizedThreadTest { public static void main(String[] args) { System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我在main方法里"); Thread myThread = new Thread(new Runnable() { @Override public void run() { System.out.println("[" + Thread.currentThread().getName() + "线程]: " + "我是传递给Thread类的Runnable对象的run方法"); } }); myThread.start(); } }
可见函数式接口和lambda表达式使我们的书写变得简洁多了。
总结在java中,创建一个线程,有且仅有一种方式:
创建一个Thread类实例,并调用它的start方法。
创建一个Thread类的实例最重要的是定义一个run方法,这个run方法说明了这个线程具体要做什么事情。有两种方式定义一个run方法:
继承Thread类,覆写run方法
实现Runnale接口,将它作为target参数传递给Thread类构造函数
启动一个线程一定要调用该线程的start方法,否则,并不会创建出新的线程来。
(完)
查看更多系列文章:系列文章目录
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/76743.html
摘要:为了避免一篇文章的篇幅过长,于是一些比较大的主题就都分成几篇来讲了,这篇文章是笔者所有文章的目录,将会持续更新,以给大家一个查看系列文章的入口。 前言 大家好,笔者是今年才开始写博客的,写作的初衷主要是想记录和分享自己的学习经历。因为写作的时候发现,为了弄懂一个知识,不得不先去了解另外一些知识,这样以来,为了说明一个问题,就要把一系列知识都了解一遍,写出来的文章就特别长。 为了避免一篇...
摘要:如果线程还存活,线程就无限期等待,并让出监视器锁,进入状态。当线程从状态被唤醒后通过,或者是假唤醒将继续竞争监视器锁,当成功获得监视器锁后,他将从调用的地方恢复,继续运行。 前言 系列文章目录 上一篇我们讨论了线程的创建,本篇我们来聊一聊线程的状态转换以及常用的几个比较重要的方法。 本篇依然是通过源码分析来了解这些知识。 本文源码基于jdk1.8 。 阅读完本文,你应当有能力回答以...
摘要:线程池为线程生命周期的开销和资源不足问题提供了解决方案。状态说明线程池处于状态,不接收新任务,不处理已提交的任务,并且会中断正在处理的任务。线程池中允许的最大线程数。线程池的饱和策略。 线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runna...
阅读 1751·2021-09-27 14:02
阅读 3100·2021-09-27 13:36
阅读 1046·2019-08-30 12:46
阅读 1834·2019-08-30 10:51
阅读 3571·2019-08-29 17:02
阅读 940·2019-08-29 16:38
阅读 1846·2019-08-29 16:37
阅读 3003·2019-08-26 10:32