玩转java并发-从创建线程开始
创建并启动线程
- 创建一个线程,并重写其run()方法
- 启动新的线程,调用Thread的start方法,才代表派生了一个新的线程,否则Thread和其他的java对象没什么区别,start()是一个立即返回的方法,不会让线程陷入阻塞。
注意:当启动JVM时,从操作系统就会创建一个新的进程(JVM进程),JVM进程会派生或者创建其他线程,其中main方法就是主线程。JVM启动后,实际上由多个线程,至少有一个非守护线程。
public static void main(String[] args) {
//内部类
Thread t1 = new Thread() {
@Override
public void run() {
for(int i=0;i<100;i++) {
System.out.println("thread:"+i);
}
}
};
t1.start();
for(int i=0;i<100;i++) {
System.out.println("main:"+i);
}
}
//程序结果:交叉运行
线程生命周期
- NEW
- RUNNABLE
- RUNNING
- BLOCKED
- TERMINATED
线程的NEW状态
当我们new一个Thread时,只是创建了一个Thread对象。在没有调用start()之前,该线程根本不存在。这个Thread对象和普通的java对象没什么区别
线程的RUNNABLE状态
线程对象进入RUNNABLE状态必须调用start()方法,此时才是真正地在JVM创建了一个线程,线程一经启动就可以立即执行吗?答案是否定的。线程的运行与否需要听从CPU的调度,这个中间状态叫做可执行状态。此时具备可执行的条件,但是在等待CPU的调度。由于存在RUNNING状态,处于RUNNABLE状态的线程不会直接进入BLOCKED状态和TERMINATED状态。即使在线程的执行逻辑中调用了wait,sleep或者其他的block的IO操作,也必须先取得CPU的执行权。RUNNABLE状态只能进入RUNNING状态或者意外终止。
线程的RUNNING状态
一旦CPU通过轮询或者其他方式从可执行队列中选中了线程,这个线程此时才可以真正地执行自己的逻辑代码。在RUNNING状态下的线程可以进入如下的状态:
- 直接进入TERMINATED状态,比如调用JDK已经不推荐的stop方法
- 进入BLOCKED状态,调用sleep或者wait方法而加入waitSet中
- 进行某个阻塞IO的操作,比如因网络数据的读写而进入了BLOCKED状态
- 获取某个锁的资源,从而加入到该锁的阻塞队列中而进入BLOCKED状态
- 由于CPU的调度器轮询使线程放弃执行,进入RUNNABLE状态
- 线程主动放弃使用权,调用yield方法,进入RUNNABLE状态
线程的BLOCKED状态
在BLOCKED状态下的线程可以进入如下的状态转换:
- 直接进入TERMINATED状态,比如调用JDK已经不推荐的stop方法
- 线程阻塞的操作结束,比如获得了想要的数据字节,从而进入了RUNNABLE状态
- 线程完成了指定的休眠,从而进入了RUNNABLE状态
- wait中的线程被其他线程notify/notifyall唤醒,从而进入RUNNABLE状态
- 线程获取到了某个锁的资源,从而进入了RUNNABLE状态
- 线程在阻塞过程中被打断,从而进入了RUNNABLE状态
线程的TERMINATED状态
线程进入TERMINATED状态意味着线程生命周期的结束。下列情况会进入此状态:
- 线程运行正常结束,结束生命周期。
- 线程运行出错意外结束
- JVM CRASH 所有线程结束
线程start方法剖析
先看下start()的API文档解释这里说到调用start()方法后,有两个线程并发执行。我们可以这么理解:是主线程(main)调用了start()方法,然后由我们自己创建的线程调用run()方法。
public static void main(String[] args) {
//内部类
Thread t1 = new Thread("MyThread") {
@Override
public void run() {
//打印结果为:MyThread
System.out.println(Thread.currentThread().getName());
}
};
t1.start();
}
当我们直接调用Thread的run()方法,可以发现run()的调用者就为main了
public static void main(String[] args) {
//内部类
Thread t1 = new Thread("MyThread") {
@Override
public void run() {
//打印结果为:main
System.out.println(Thread.currentThread().getName());
}
};
t1.run();
}
start()源码
public synchronized void start() {
//同一个Thread不能调用start两次,否则抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
//native的C++方法,里面调用了run()
//native表示和C/C++互操作的,告诉编译器该方法在外部定义
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()和run()的区别就是:start()方法实现了多线程,run()没有实现多线程。
start()和run()之间的关系涉及到模板设计模式的思想。run()只是一个类的普通方法,程序中依然只有主线程这一线程,还是要顺序执行,还是要等run方法执行完毕之后再执行其他方法。start()真正实现了多线程,无需等run()执行完,就可以继续执行下面的代码。run()可以看作模板设计模式中用户自定义的方法,而start()中固定的部分是创建新线程等部分,run()只是提供给用户实现业务逻辑的。如果用户脱离start(),而直接运行run(),也就意味着脱离了模板,那么此时run()方法只是一个普通的方法,并没有实现多线程。
RUNNABLE接口的引入
很多地方会讲到创建线程有两种方式,一种是基础Thread,另一种是实现Runnnable接口,这种是不严谨的。在JDK中代表线程的只有Thread这个类。
创建线程只有一种方式就是构造Thread类,而实现线程的执行单元(run方法)有两种方式,第一种是重写Thread的run(),第二种是实现Runnable接口的run()方法,并且将Runnable实例用作构造Thread的参数。
public class TickRunnable implements Runnable {
private final static int max=50;
private int index=1;
@Override
public void run() {
while(index<=max) {
System.out.println("当前柜台"+Thread.currentThread().getName()+"号码"+index);
index++;
}
}
}
//将实现Runnable接口的实例用作构造Thread的参数
//实现业务逻辑和构造Thread的分离
final TickRunnable tickRunnable = new TickRunnable();
Thread TickWindow1=new Thread(tickRunnable,"一号柜台");
策略模式在Thread的应用
无论是Runnable中的run()方法,还是Thread中的run()方法,都是想将线程的控制本身和业务逻辑的运行分离开来,达到职责分明,功能单一的原则,这涉及到策略模式的设计思想。
关于策略模式,可以看这篇文章传送门
策略模式简单地说就是将代码中动态可变的部分封装成策略类(接口形式),动态注入实现类。