面试中最常被虐的地方必定有并发编程这块知识点,不管你是刚刚入门的大四萌新仍是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程当中的笔记和总结,并提供调试代码供你们玩耍)java
1.并发编程主要解决的问题是什么?git
2.线程建立和实现的方式有哪些?github
3.new Thread()是如何一穿三的呢?面试
请自行回顾以上问题,若是还有疑问的自行回顾上一章哦~编程
本章学习完成,你将会对线程的生命周期
有清楚的认识,而且明白不一样状态之间是如何转换的,以及对java线程状态枚举类解读。此外本章还会着重对Thread.start()
进行细致的解读和分析,同时经过案例你能够更加清晰的明白start()
和run()
之间的关系。(老规矩,熟悉这块的同窗能够选择直接点赞👍完成本章学习哦)设计模式
本章代码下载数组
相信有很多同窗看到这四个字就气不打一出来,面试老是被问及这生命周期
是啥,那生命周期
你来描述一下的,一下脾气就上来了。确实这个东西是有点烦,可是我的没什么技巧,死记硬背
是最简单最好用的方式。等到背熟了,天然而然在用到的时候就会融会贯通,甚至会衍生出本身的独有的思路。不说太多,概念性的东西背就完事儿了,上菜~bash
NEW阶段就是你new Thread()
建立线程对象时候的阶段。并发
划重点了jvm
NEW阶段下其实线程根本仍是不存在的,咱们只是建立了一个Therad对象
,就和咱们最经常使用的new
关键字是一个道理。只有当咱们真正把线程启动起来的时候,此时才会在JVM进程中把咱们的线程建立出来。
按照上一章的思路,咱们new了一个Thread对象以后就须要调用Thread.start()
来启动线程,此时线程会从NEW
阶段转换到RUNNABLE
阶段。
只有调用Thread.start()
方法才能使线程从NEW
阶段转换到RUNNABLE
阶段。
固然咱们从字面意思也能够知道此时线程是处于可执行转状态
而不是真正的执行中状态了,此时的线程只能等CPU翻牌子,翻到了他才能真正的跑起来。有些同窗可能会说要是CPU一直不翻牌子咋办?严格意义上来说,处在RUNNABLE
的线程只有两条出路,一条是线程意外退出
,还有一条是被CPU翻牌子进入RUNNING
阶段。
到这里一切看起来仍是那么简单,那么美好,new一个Thread对象,而后调用start启动起来,而后等翻CPU翻牌子以后进入RUNNING
阶段。能够打个比方,NEW
阶段的时候咱们的线程仍是宫外的一位佳人对象,调用start
方法以后就摇身一变成为宫里的一位小主了,也就是中间阶段RUNNABLE
,等到获取到CPU调度执行权的时候就晋升为得宠的娘娘
了,也就是进入了RUNNING
阶段。可想而知,此时咱们的线程娘娘
必定是比宫外的佳人要复杂的多了。
⚠️注意
有了解过这块内容的同窗看到这里可能会有疑问,java线程状态中并无这个状态,为何咱们在讲生命周期的时候会把这一状态单独拆分出来作讲解?为了章节内容的流畅性,这块内容的解释放到下一节去讲解,这边咱们仍是继续讲咱们的线程生命周期
。
好的咱们继续
这个阶段的线程已经获取到了CPU调度执行权,也就是说处于运行中状态了。
在该阶段中,线程能够向前或者向后发生转换:
1.因为CPU的调度器轮询致使该线程放弃执行,就会进入RUNNABLE
阶段。
2.线程主动调用yield
,放弃CPU执行权,就会进入RUNNABLE
阶段(这种方式并非百分百生效的,在CPU资源不紧张的时候不会生效
)。
3.调用sleep
、wait
方法,进入BLOCKED
阶段(这里讲的BLOCKED阶段和线程的BLOCKED状态须要区分开,这边讲的是一个比较广义的BLOCKED的阶段
)
4.进行某个阻塞的IO操做而进入BLOCKED
阶段
5.为了获取某个锁资源而加入到该锁到阻塞队列中而进入BLOCKED
阶段
6.线程执行完成或者调用stop
方法或者判断某个逻辑标识,直接进入TERMINATED
阶段
进入该阶段的缘由已经在RUNNING
阶段阐述过了,这里就再也不说明,这里主要介绍一下处于该阶段的线程能够如何切换。 1.直接进入TERMINATED
,好比调用stop
方法或者意外死亡(JVM Crash)
2.线程阻塞的操做结束,读取或者写入了想操做的数据进入RUNNABLE
状态
3.线程完成了指定时间的休眠,进入RUNNABLE
状态
4.Wait
状态的线程被notify
或者notifyall
唤醒,进入RUNNABLE
状态
5.获取到了锁资源,进入RUNNABLE
状态
6.线程阻塞过程被打断,好比调用interrupt
方法,进入RUNNABLE
状态
TERMINATED
状态是一个线程的最终状态,在该状态中线程不会切换到其余任何状态,线程进入TERMINATED
状态意味着线程整个生命周期结束了,进入TERMINATED
状态的方式有如下三种:
1.线程正常结束
2.线程意外结束
3.JVM Crash
面对这个问题,最直接的论证方式必定就是看代码,请同窗们先找到java.lang.Thread.State
这个枚举类。
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
复制代码
能够看到这边state
枚举类包含有6中线程状态,根听说明咱们一一来解读这六个状态。
* <li>{@link #NEW}<br>
* A thread that has not yet started is in this state.
* </li>
复制代码
源码清楚地说明了NEW
状态就是一个线程刚刚被建立,可是尚未启动地时候所处的状态
,这个和咱们上一小节中地NEW
阶段可以对应起来这里就很少说了。
* <li>{@link #RUNNABLE}<br>
* A thread executing in the Java virtual machine is in this state.
* </li>
复制代码
这段的说明意思是在java虚拟机中执行的线程所处的状态称之为RUNNABLE
。 也就是说咱们上一节中分开讲解的RUNNABLE阶段
和RUNNING阶段
在线程状态中来看统一都称之为RUNNABLE状态
,之因此咱们在生命周期划分的时候把这两个状态拆分开来看是由于这两个状态差异仍是很大的,RUNNING
状态的线程比RUNNABLE
状态的线程更加复杂一些。
* <li>{@link #BLOCKED}<br>
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
复制代码
源码中对于这三个状态对叙述是这样的
BLOCKED:等待获取监视器锁而进入阻塞队列的线程所处的状态
WAITING:无限期等待另外一线程执行特定操做的线程处于此状态。
TIMED_WAITING:等待另外一线程执行操做的线程在指定的等待时间内处于此状态。
同窗们对于BLOCKED
和WAITING
这两个状态的区别清楚吗?清楚的直接进入(4)哦~
咱们仍是经过源码中对于state
枚举值的描述来进入主题。 state中对于BLOCKED
状态的描述以下:
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED
复制代码
源码中对于BLOCKED
这一状态的叙述是这样的,线程在等待获取一个monitor lock
的时候该线程就是处于BLOCKED
。说通俗一点就是被synchronized
关键字描述的同步块或者方法,此时其余线程想要获取这个被描述的同步块或者方法的时候,这个线程就会进去等待获取monitor lock
的时候,也就是进入来BLOCKED
状态。这里的关键是monitor lock
监视锁。
唤醒方式是目标监视锁monitor lock
主动释放,这时候咱们去竞争这个监视锁并成功以后。
state中对于WAITING
状态的描述以下:
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING
复制代码
这里给咱们描述了哪些状况下咱们的线程会进入WAITING
状态,分别是调用Object.wait
、Thread.join
和LockSupport.park
三个API接口才会进入WAITING
状态,这边对于线程API不作具体阐述,后面会单独开一章来介绍,咱们这边就先不纠结。
也说明了WAITING
状态是当一个线程处于等待另外一个线程完成一个特定操做时候所处的状态,这里的关键是两个线程,目标线程等待另外一个线程完成某个动做
。
唤醒方式是等待其余线程完成本身逻辑以后,调用notify
或者notiffyall
唤醒处于WAITING
状态的线程。
到这里同窗们应该已经清楚了线程生命周期以及各个阶段之间的转换,同时明白了不一样阶段的生命周期所对应的java线程状态。接下来是本章第二段源码分析啦~
划重点啦,看累的同窗休息休息,接下来是start()源码分析
开始以前咱们先思考一下,为何start()
能启动咱们的线程,run()
不行呢?
废话不说,先上源码
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* 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 */
}
}
}
复制代码
老套路,咱们先看方法说明
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
* <p>
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* <code>start</code> method) and the other thread (which executes its
* <code>run</code> method).
* <p>
* 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()
*/
复制代码
翻译:start()
方法使线程开始运行,java虚拟机会调用run()
方法来执行线程的逻辑单元。而且线程只容许启动一次,屡次启动线程是不合法的,会抛出IllegalThreadStateException
异常。
下面咱们经过三步来解读Thread.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();
复制代码
threadStatus==0
表示NEW
状态,若是在调用Thread.start()
到时候发现threadStatus!=0
那么表示线程已经不处于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);
复制代码
翻译:通知组新线程已经被启动,须要添加到线程组中,同时减小未开始计数。 看完翻译,不知所云,咱们继续追踪add
void add(Thread t) {
synchronized (this) {
if (destroyed) {
throw new IllegalThreadStateException();
}
if (threads == null) {
threads = new Thread[4];
} else if (nthreads == threads.length) {
threads = Arrays.copyOf(threads, nthreads * 2);
}
threads[nthreads] = t;
// This is done last so it doesn't matter in case the // thread is killed nthreads++; // The thread is now a fully fledged member of the group, even // though it may, or may not, have been started yet. It will prevent // the group from being destroyed so the unstarted Threads count is // decremented. nUnstartedThreads--; } } 复制代码
add
方法咱们也分为三小步来看
第一步: 判断线程组是否已经被销毁了,若是线程组已经不存在了,那就抛出IllegalThreadStateException
异常。
if (destroyed) {
throw new IllegalThreadStateException();
}
复制代码
第二步:把线程加入到线程组数组中。
if (threads == null) {
threads = new Thread[4];
} else if (nthreads == threads.length) {
threads = Arrays.copyOf(threads, nthreads * 2);
}
threads[nthreads] = t;
// This is done last so it doesn't matter in case the // thread is killed nthreads++; 复制代码
首先判断threads
是否为空,为空则建立一个数组,初始化长度为4。若是threads
不为空,判断nthreads == threads.length
若是为true,则对threads数组进行扩容threads = Arrays.copyOf(threads, nthreads * 2)
,把入参添加到threads[]
中,对下标进行累加nthreads++
第三步:未开始线程计数减一
// The thread is now a fully fledged member of the group, even
// though it may, or may not, have been started yet. It will prevent
// the group from being destroyed so the unstarted Threads count is
// decremented.
nUnstartedThreads--;
复制代码
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 */
}
}
复制代码
started
是线程的启动状态,预设为false,启动成功再赋值为true。而后调用start0()
方法
private native void start0();
复制代码
至此咱们回想一下咱们的线程执行逻辑单元是写在run()
中的,那么全程发现没有地方在调用run()
,只有这个start0()
方法最为可疑。能够看到这个方法是一个本地方法
,在Thread.java
类定义的开头有静态块来完成注册。
/* Make sure registerNatives is the first thing <clinit> does. */
private static native void registerNatives();
static {
registerNatives();
}
复制代码
好的到此打住,以后会涉及到JVM底层的一些内容,这里咱们就不作扩展,咱们只要知道start()
内部调用调用start0()
方法,最终是在建立完咱们的线程以后,在线程内部调用来run()
方法。
咱们只要记清楚一点,真正建立线程和调用run方法的地方是由jvm来完成的,咱们这里只是做为一个启动发起方完成一个发送线程启动信号的动做就行。咱们这里再也不带你们去一堆CPP文件里面去翻云覆雨了,以避免晕车。
到这里咱们应该清楚了本节开始时候的问题了,为何start()能启动咱们的线程,run()不行呢
。相信你们已经知道了缘由了,由于run
只能做为是一个内部实现类,若是单纯调用run
方法的话,咱们只是在本线程中实现了逻辑而并无真正地开辟出一个新的线程来执行咱们的逻辑。开辟新的线程须要调用start0()
来让JVM帮咱们完成建立,因此只有调用start()
方法才能启动线程。
这里我也写了一个小例子来验证:
public static class TestStartAndRun implements Runnable{
@Override
public void run() {
System.out.println("个人名字叫"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
new Thread(new TestStartAndRun(),"start").start();
new Thread(new TestStartAndRun(),"run").run();
}
复制代码
输出:
个人名字叫start
个人名字叫main
复制代码
能够看到,咱们定义的名称叫作run的线程并无启动起来,执行Thread.run()的是咱们的main
线程,这就证实来run
不能建立线程,可是能在当前线程中执行内部类run
的逻辑单元,而且能够执行屡次。
/**
* 杯子
*/
final void Teacup(String nothing){
System.out.print("今天");
Brewing(nothing);
System.out.println(",愉快的一天开始啦!");
}
/**
* 泡点什么呢
* @param nothing
*/
void Brewing(String nothing){
}
public static void main(String[] args) {
TemplateDesignExample t1 = new TemplateDesignExample(){
@Override
void Brewing(String nothing){
System.out.print("下雨,"+nothing);
}
};
t1.Teacup("早上喝咖啡");
TemplateDesignExample t2 = new TemplateDesignExample(){
@Override
void Brewing(String nothing){
System.out.print("晴天,"+nothing);
}
};
t2.Teacup("早上喝茶");
}
复制代码
输出:
今天下雨,早上喝咖啡,愉快的一天开始啦!
今天晴天,早上喝茶,愉快的一天开始啦!
复制代码
这样不论是下雨仍是晴天,咱们均可以喝到本身想喝的,每一天均可以愉快地编码,若是能点点关注,点点赞👍,每一天都是双倍的快乐哦!
祝同窗们端午小长假快乐呀,放假也不要忘记和我一块儿学习哦~