Java高并发入门详细讲解
上期回顾及问题总结
上次说了创建线程的两种常用方式,第三种方式在后面的更新中会讲解到。这里对于上一节的内容做个回顾。
- 在上一节中说到了创建多线程的问题,分析了Thread的源码,提出了三种的线程实现的方式。
- 简单的分析了共享资源锁竞争的问题,同样也引出了锁竞争的问题。
这次我们就来结合实践详细的说说关于创建线程的其他的注意点。
入门例子
首先你是一个非常爱音乐的人,在平时你也喜欢听歌。如果你想在看新闻的时候也想听歌,这样的话让我们用Java代码来实现一下。
public class TryConcurrency {
public static void main(String[] args) {
browsNews();
enjoyMusic();
}
/**
* 听音乐
*/
private static void enjoyMusic() {
for (;;){
System.out.println("this is good Music");
sleep(1);
}
}
/**
* 看新闻
*/
private static void browsNews() {
for (; ; ) {
System.out.println("this is good News");
sleep(1);
}
}
private static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
this is good News
this is good News
this is good News
this is good News
this is good News
this is good News
以上是我们输出的结果,看样子并不是我们想要的结果,并没有实现一遍听歌一边看新闻的功能。这里我们将以上的代码做一个优化,当然这里使用的Java8编程。如果对Java8不是很熟悉的人可以熟悉一下Java8的新特性。
如果我们想实现一边听歌一边看新闻的话就要做如下的改变
public static void main(String[] args) {
new Thread(()->{
enjoyMusic();
}).start();
browsNews();
}
使用我们上节课说过的多线程的知识来解决这个问题。
This is good Music
this is good News
this is good News
this is good Music
this is good News
this is good Music
this is good News
生命周期分析
这样我们就会看到如上的结果,在一边听歌的同时一边看看新闻。既然引入了多线程的概念,那么我们就来看看多线程的生命周期有哪些,从上节课的分析中我们可以知道,在使用多线程的时候,需要调用start方法
start方法
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 */
}
}
}
生命周期调用关系图
在调用了start方法之后又会发生什么样的变化,那么我们就来看看线程的生命周期
关系说明
这张图是从一本书上截图的,从图中可以看到线程的五种状态,首先是new,也就是实例化一个线程,通过start方法的调用进入到了Runnable状态,当然在这个状态值能保证它是可是执行的,也就是说当它拿到资源之后立即可以执行,Running状态,在这个状态中表示线程在实现,通过调用yield,表示谦让的意思。将资源让出来之后原本处于Runnable状态的线程就会有获取到资源进入到Running状态,如果在Runnable状态或者在Running状态的线程调用了sleep方法或者是wait方法就会进入到Blocked(阻塞)状态,进入阻塞状态的线程有两种结果,一种是资源不够用了直接stop,另一种是通过调用notify或者notifyAll方法唤醒执行。而正常执行结束的线程或者是被Stop的线程就会进入到Terminated状态(最终状态)在这个状态的线程将不会被切换到任何的状态,这也意味着线程的整个生命周期的结束。而以下的情况将会导致线程进入到最终状态,其实在之前也提到了
- 线程正常运行结束生命周期
- 线程出现意外
- JVM Crash导致所有的线程都结束
上面我们提到了Start方法源码,这里我们分析完线程的生命周期之后再次的进入到start源码中,其实在start源码中做的最主要的一件事情就是调用我们start0方法,这个方法是JNI方法,也就是在Java编程中调用底层C语言或者C++语言的代码。当我们执行start方法的时候就会执行run方法,从而执行strat0方法。而在上一节中我们提到了Thread的源码
Thread源码实例分析
首先Thread被构造后的new状态,这个是时候threadStatus这个内部属性为0,如果两次启动Thread,就会出现IllegalThreadStateException异常,线程启动之后就被加入到一个ThreadGroup线程组中,当一个线程结束,也就是到了Treminated状态,再次调用start方法是不允许的。也就是说了Treminated状态是没有办法会到Runnable/Running状态的。这个逻辑我们使用代码来模拟一下
第一种情况
@Test
public void testMain(){
Thread thread = new Thread(){
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
thread.start();
}
第二种情况
@Test
public void testMain1() throws InterruptedException {
Thread thread =new Thread(){
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
TimeUnit.SECONDS.sleep(3);
thread.start();
}
总结
以上两段代码都会引起IllegalThreadStateException异常,但是两个异常抛出却是有着本质的区别,第一个是重复启动,只是第二次启动不允许,但是此时线程属于运行状态,第二次企图重新激活也抛出了非法状态的异常,此时没有线程,因为该线程的生命周期已经被终结。到这里我们就看到了整个线程的生命周期