并发编程之线程的创建和启动
一、线程创建
1.1. 实现Runnable
接口
实现Runnable
接口,重写run
方法,实现Runnable
接口的实现类的实例对象作为Thread
构造函数的target
:
public class CreateThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ": 通过Runnable创建线程......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread createThread = new Thread(new CreateThread(), "子线程");
createThread.start();
System.out.println(Thread.currentThread().getName() + "主线程......");
}
}
我们看一下Runnable
接口:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
我们可以看到Runnable
被FunctionInterface
接口,说明使用lamdba
的写法去实现线程。很简洁。
public class CreateThread {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + "主线程......");
new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ": 通过Runnable创建线程......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "子线程1").start();
}
}
1.2. 继承Thread
类
继承Thread
类,重写run
方法。其实Thread
也是实现了Runnable
接口,里面有很多native
方法。后面会分析。
public class CreateThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ": 通过Runnable创建线程......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread createThread = new CreateThread1();
createThread.setName("子线程");
createThread.start();
System.out.println(Thread.currentThread().getName() + "主线程......");
}
}
我们简单看见一下Thread
里面的run
方法:
@Override
public void run() {
if (target != null) {
target.run();
}
}
这个里面的target
其实就是我们传入的Runnable
,这也是为啥我们可以实现Runnable
接口的run
方法,这也是就是继承Thread
(把run
重写)和实现Runnable
(调用target.run()
方法)的区别。
更值得我们注意的是run
方法是异常的处理和抛出的,这意味的子线程发生异常,主线程是无法捕获到的(但是具体还是有处理的方法的,日后介绍,挖个坑,日后填)。
1.3. 总结
- 实现
Runnable
接口更好 -
Runnable
方便配合线程池使用 -
Thread
线程执行和线程创建无法解耦 -
Thread
继承之后无法继承其他线程,限制扩展性
最后再说一下 :创建线程我们可以有两种方法实现Runnable
和继承Thread
,但是本质来说创建线程只有一种(构造Thread
类),run
方法有两种传入Runnable
通过target.run()
调用和重写run()
方法。
二、线程的启动
我们从上面的看到,线程的启动涉及到start()
和run()
两种方法。
我们先看看start()
方法:
/**
* 1. start方法将导致当前线程开始执行。由JVM调用当前线程的run方法
* 2. 结果是两个线程同时运行:当前线程(从对start方法的调用返回)和另一个线程(执行其run方法)
* 3. 不允许多次启动线程, 线程一旦完成执行就不会重新启动
*/
public synchronized void start() {
/**
* 对于VM创建/设置的主方法线程或“系统”组线程,不调用此方法。
* 将来添加到此方法的任何新功能可能也必须添加到VM中.
*
* threadStatus = 0 意味着线程处于初始状态
*/
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) {
/* 不做处理。如果start0抛出了一个Throwable,那么它将被传递到调用堆栈上 */
}
}
}
private native void start0();
从上面代码的注释我们可以看到:
-
start()
方法被synchronized
进行修饰,为同步方法,这样避免了多次调用问题; - 使用
threadStatus
(此变量被volatile
修饰)记录线程状态;多次调用会抛出异常; - 这方法会重写的
run
方法被虚拟机调用,是子线程执行的run
方法。
上面已经介绍了start
和run
方法,接着我们写一个例子看看两个的区别:
public static void main(String[] args) {
Thread one = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程1");
Thread two = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "启动");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "线程2");
one.start();
two.run();
System.out.println("主线程启动");
}
执行结果也很明显,调用run
会阻塞主线程,start
是异步的。