- 继承Thread类创建线程类
- 通过Runnable接口创建线程类
- 通过Callable和Future创建线程
1、继承Thread类创建线程类
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
- 创建Thread类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
csharp
public class ThreadSimple extends Thread {
int i = 0;
//重写run方法,run方法的方法体就是现场执行体
public void run() {
for (; i < 100; i++) {
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
if (i == 20) {
new ThreadSimple().start();
}
}
}
}
2、通过Runnable接口创建线程类
- 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
csharp
public class ThreadSimple implements Runnable {
private int i;
public void run() {
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20) {
ThreadSimple threadSimple = new ThreadSimple();
new Thread(threadSimple, "新线程1").start();
}
}
}
}
3、通过Callable和Future创建线程
具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。
csharp
public class ThreadSimple implements Callable<Integer> {
public static void main(String[] args) {
Callable<Integer> myCallable = new ThreadSimple(); // 创建myCallable对象
FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask来包装myCallable对象
for (int i = 0; i < 31; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 30) {
Thread thread = new Thread(ft); //FutureTask对象作为Thread对象的target创建新的线程
thread.start(); //线程进入到就绪状态
}
}
System.out.println("主线程for循环执行完毕..");
try {
int sum = ft.get(); //取得新创建的新线程中的call()方法返回的结果
System.out.println("sum = " + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
private int i = 0;
// 与run()方法不同的是,call()方法具有返回值
@Override
public Integer call() {
int sum = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
sum += i;
}
return sum;
}
}
线程的启动
1.start() 方法
1.1 方法含义
启动新线程:通知 JVM 在有空闲的情况下启动线程,本质是请求 JVM 来运行我们的线程,线程何时运行由线程调度器来确定。该线程启动的同时会启动两个线程:第一个是用来执行 start 方法的父线程或主线程,第二个是被创建的子线程。
准备工作:让线程处于就绪状态(已经获得了除 CPU 以外的其他资源,如已经设置了上下文,线程状态,栈等),做完准备工作后,才能被 JVM 或操作系统调度到执行状态获取 CPU 资源,然后才会执行 run 方法。
重复调用 start() :抛出异常 Exception in thread "main" java.lang.IllegalThreadStateException。一旦线程 start 以后,就从 NEW 状态进入到其他状态,比如 RUNNABLE,只有处于 NEW 状态的线程才能调用 start() 方法。
1.2 原理分析
通过 threadStatus 属性来判断是否重复启动并抛出异常,实际的启动方法是 native 方法 start0()。
csharp
public class Thread implements Runnable {
/**
* 线程状态,初始化为 0,表示还未启
*/
private volatile int threadStatus = 0;
public synchronized void start() {
// 判断线程的状态,也就是判断是否启动,重复启动时抛出 IllegalThreadStateException
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) {}
}
}
private native void start0();
}
通过 /src/share/native/java/lang/Thread.c 可知,start0() 方法对应 JVM_StartThread 方法
arduino
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
};
位于 /src/hotspot/share/prims/jvm.cpp 的 JVM_StartThread 方法中有段注释
less
// Since JDK 5 the java.lang.Thread threadStatus is used to prevent
// re-starting an already started thread, so we should usually find
// that the JavaThread is null. However for a JNI attached thread
// there is a small window between the Thread object being created
// (with its JavaThread set) and the update to its threadStatus, so we
// have to check for this
该段注释说自从 JDK5 后 使用 Thread 类的 threadStatus 属性去方式线程重复启动,接下来看下 /src/share/vm/runtime/thread.cpp 中的 start 方法,该方法中判断如果该线程是 Java 线程,则将该线程的状态改为 RUNNABLE。
arduino
void Thread::start(Thread* thread) {
trace("start", thread);
if (!DisableStartThread) {
if (thread->is_Java_thread()) {
// 这里调用 set_thread_status 方法将线程的状态修改为 RUNNALBE
java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),
java_lang_Thread::RUNNABLE);
}
os::start_thread(thread);
}
}
2.run() 方法
run() 只是 Thread 类的一个基本方法
typescript
public class Thread implements Runnable {
/** 省略 */
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
3.比较两方法
输出:main 和 Thread-0
scss
public class StartAndRunMethod {
public static void main(String[] args) {
Runnable runnable = () -> System.out.println(Thread.currentThread().getName());
runnable.run();
new Thread(runnable).start();
}
}
调用 start 方法才是真正意义上启动了一个线程,会经历线程的各个生命周期,如果直接调用 run 方法,则只是普通的调用方法,不会通过子线程去调用。
线程的终止
1.过期的 suspend()、resume()、stop()
这三个方法已经被废除,通过查看 Oracle 官方文档 可以得知。使用 stop() 方法停止线程会释放线程的所有 monitor,该方法在终止一个线程时不会保证线程的资源正常释放,并且抛出 ThreadDeath 异常,通常是没有给予线程完成资源释放工作的机会,因此会导致程序出现数据不同步。suspend() 方法则容易造成死锁,该方法在调用后,线程不会释放已经占有的资源(比如锁),而是占有着资源进入挂起状态。resume() 必须和 suspend() 一起使用,当要恢复目标线程的线程在调用 resume 之前尝试锁定这个 monitor,此时就会导致死锁。
2.volatile 标志位
通过 volatile 修饰的共享变量可以进行线程的终止。
2.1 成功案例
子线程每隔 1 秒输出:持续运行。主线程在 2 秒后将 stop 置为 true,此时子线程 while 循环停止,子线程运行结束。循环只进行了两次。
arduino
public class RightVolatileDemo {
private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (!stop) {
System.out.println("持续运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
TimeUnit.SECONDS.sleep(2);
stop = true;
}
}
运行结果如下:
持续运行
持续运行
2.2 失败案例
使用 volatile 的局限性,当线程陷入阻塞时,使用 volatile 修饰的变量无法停止线程。
通过生产者消费者例子来演示阻塞情况下 volatile 的局限性,定义一个生产者类实现 Runnable 接口重写 run 方法,在 run 中当 volatile 修饰的 canceled 变量为 false 时,生产者通过 BlockingQueue 的 put 方法不断添加数据,当阻塞队列到达上限时,put 方法会阻塞。定义一个消费者类,通过 needMoreCount 方法判断消费者是否结束消费。
在主函数中初始化一个长度为 10 的阻塞队列,构建生产者和消费者实例,当消费者结束消费时,将生产者的 canceled 属性值改为 true,但是此时生产者仍然在运行,因为生产者线程阻塞在 put 方法。这就是 volatile 标志位的局限性了。
csharp
public class WrongVolatileDemo {
public static void main(String[] args) throws InterruptedException {
// 定义容量为 10 的阻塞队列
BlockingQueue<Integer> storage = new ArrayBlockingQueue<>(10);
// 启动生产者线程
Thread producerThread = new Thread(new Producer(storage));
producerThread.start();
Thread.sleep(1000);
// 启动消费者
Consumer consumer = new Consumer(storage);
while (consumer.needMoreCount()) {
System.out.println("消费者消费:" + consumer.getStorage().take());
Thread.sleep(100);
}
System.out.println("消费者消费完全结束");
// 此时生产者不应该继续生产
Producer.canceled = true;
}
/**
* 生产者
*/
private static class Producer implements Runnable {
static volatile boolean canceled = false;
private BlockingQueue<Integer> storage;
public Producer(BlockingQueue<Integer> storage) {
this.storage = storage;
}
@Override
public void run() {
int count = 1;
try {
while (!canceled) {
// 如果队列满的话,put 方法会阻塞当前线程
storage.put(count);
System.out.println("生产者生产:" + count);
count++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("生产者停止运行");
}
}
}
/**
* 消费者
*/
private static class Consumer {
private BlockingQueue<Integer> storage;
public Consumer(BlockingQueue<Integer> storage) {
this.storage = storage;
}
public BlockingQueue<Integer> getStorage() {
return storage;
}
public boolean needMoreCount() {
return Math.random() < 0.95;
}
}
}
3.interrupt 方法
interrupt 翻译为中断,中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt() 方法对其进行中断操作。
举几个例子来演示 interrupt 的不同用法。
3.1 不带阻塞的中断
该例子是最简单的中断,thread 线程启动后,休眠 1ms 再调用该对象的 interrupt 方法,此时线程中正在执行的循环检测到 Thread.currentThread().isInterrupted() 为 true 结束循环,输出 count 变量的值。
当线程调用自身的 interrupt 方法时,会将中断标记设置为 ture,线程内部循环会通过检查自身是否被中断来结束循环,而 线程内部的 isInterrupted() 方法就能判断线程是否被中断。
arduino
public class InterruptThreadWithoutSleep {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
int count = 0;
// 检查自身是否被中断来结束循环
while (!Thread.currentThread().isInterrupted()) {
count++;
}
System.out.println(count);
});
thread.start();
Thread.sleep(1);
// 设置中断标记
thread.interrupt();
}
}
3.2 带有阻塞的中断
该例子演示带有 sleep 阻塞的中断方法使用。sleep 方法使用需要抛出 InterruptedException,说明该方法可以响应 interrupt 中断。在线程启动后,该线程会休眠 1s,而主线程在休眠 100ms 后会调用中断方法,此时该线程是处于阻塞状态,在阻塞状态下响应到中断,sleep 方法会抛出 InterruptedException ,但是在抛出该异常前,JVM 会先将该线程的中断标识位清除,然后才抛出 InterruptedException,此时调用 isInterrupted() 方法将会返回 false。
如果在执行过程中,每次循环都会调用 sleep 方法,那么其实可以不需要每次迭代都通过 isInterrupted() 方法检查中断,因为 sleep 方法会响应中断。
arduino
public class InterruptThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("中断标记:" + Thread.currentThread().isInterrupted());
}
});
thread.start();
Thread.sleep(100);
thread.interrupt();
}
}
运行结果如下:
bash
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.hncboy.interrupt.InterruptThreadWithSleep.lambda$main$0(InterruptThreadWithSleep.java:15)
at java.lang.Thread.run(Thread.java:748)
中断标记:false
3.4 interrupt 相关方法
3.4.1 interrupt()
设置中断标记,最终调用 native 的 interrupt0() 方法设置中断标记。
scss
public void interrupt() {
if (this != Thread.currentThread())
// 权限检查
checkAccess();
synchronized (blockerLock) {
// IO 读写相关
Interruptible b = blocker;
if (b != null) {
interrupt0();
b.interrupt(this);
return;
}
}
// 该方法一定会执行
interrupt0();
}
private native void interrupt0();
找到 interrupt0 方法对应的 JVM_Interrupt 方法,找到该方法代码。
scss
JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_Interrupt");
// Ensure that the C++ Thread and OSThread structures aren't freed before we operate
oop java_thread = JNIHandles::resolve_non_null(jthread);
MutexLockerEx ml(thread->threadObj() == java_thread ? NULL : Threads_lock);
// We need to re-resolve the java_thread, since a GC might have happened during the
// acquire of the lock
JavaThread* thr = java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread));
if (thr != NULL) {
Thread::interrupt(thr);
}
JVM_END
找到关键方法 Thread::interrupt 的代码。
arduino
void Thread::interrupt(Thread* thread) {
trace("interrupt", thread);
debug_only(check_for_dangling_thread_pointer(thread);)
os::interrupt(thread);
}
找到关键方法 os::interrupt 的代码,此时找到了设置中断标记的方法,Java 中的每个线程都与操作系统的线程一一对应,一个 osthread 就对应 Java 中的一个线程,如果 osthread 没有被设置为中断,则设置中断标记为 true。
scss
void os::interrupt(Thread* thread) {
assert(Thread::current() == thread || Threads_lock->owned_by_self(),
"possibility of dangling Thread pointer");
OSThread* osthread = thread->osthread();
// 如果线程没有被中断
if (!osthread->interrupted()) {
// 设置中断标记为 true
osthread->set_interrupted(true);
// More than one thread can get here with the same value of osthread,
// resulting in multiple notifications. We do, however, want the store
// to interrupted() to be visible to other threads before we execute unpark().
OrderAccess::fence();
ParkEvent * const slp = thread->_SleepEvent ;
if (slp != NULL) slp->unpark() ;
}
// For JSR166. Unpark even if interrupt status already was set
if (thread->is_Java_thread())
((JavaThread*)thread)->parker()->unpark();
ParkEvent * ev = thread->_ParkEvent ;
if (ev != NULL) ev->unpark() ;
}
3.4.2 isInterrupted() 和 interrupted()
返回线程的中断状态。interrupted 为静态方法,两个方法都调用了 isInterrupted 方法,不过传入的参数不一样,true 表示清除中断状态,false 表示不清除。
Thread.interrupted() 在哪个线程里被调用,就返回哪个线程的中断标志。
java
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
3.4.3 综合例子
arduino
public class InterruptComprehensive {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {});
// 启动线程
thread.start();
// 设置中断标志
thread.interrupt();
// 获取中断标志,被中断了返回 true
System.out.println("isInterrupted: " + thread.isInterrupted());
// 获取中断标志并重置,interrupted 静态方法调用的是执行它的线程,也就是 main 线程,返回 false
System.out.println("isInterrupted: " + thread.interrupted());
// 获取中断标志并重置,Main 函数没有没有被中断,返回 false
System.out.println("isInterrupted: " + Thread.interrupted());
// 获取中断标志,中断标记没有被清除,返回 true
System.out.println("isInterrupted: " + thread.isInterrupted());
thread.join();
System.out.println("Main thread is over.");
}
}
运行结果:
vbnet
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
Main thread is over.
3.5 能响应中断的部分方法
有些阻塞方法是不可中断的,例如 I/O 阻塞和 synchronized 阻塞,需要针对某一些锁或某一些 I/O 给出特定的方案。
- Object.wait()/wait(long)/wait(long, int)
- Thread.sleep(long)/sleep(long, int)
- Thread.join()/join(long)/join(long, int)
- java.util.concurrent.BlockingQueue.take()/put(E)
- java.util.concurrent.locks.Lock.lockInterruptibly()
- java.util.concurrent.CountDownLatch.await()
- java.util.concurrent.CyclicBarrier.await()
- java.util.concurrent.Exchanger.exchange(V)
- java.nio.channels.InterruptibleChannel 相关方法
- java.nio.channels.Selector 相关方法
3.6 InterruptedException 异常处理
3.6.1 传递中断
当在 run 中调用了一个有异常的方法时,该异常应该在方法中用 throws 声明,传递到 run 方法,而不是在方法中捕获,此时可能会造成不可预料的结果。throwInMethod2() 为正确做法,throwInMethod1() 为错误做法。
csharp
public class HandleInterruptedException implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new HandleInterruptedException());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
@Override
public void run() {
while (true) {
System.out.println("work");
try {
throwInMethod2();
} catch (InterruptedException e) {
System.out.println("保存日志、停止程序");
e.printStackTrace();
}
}
/*while (true) {
System.out.println("go");
throwInMethod1();
}*/
}
private void throwInMethod2() throws InterruptedException {
Thread.sleep(2000);
}
private void throwInMethod1() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.6.2 重新设置中断状态
因为阻塞抛出 InterruptedException 异常后,会清除中断状态。可以在 catch 子语句中调用 Thread.currentThread().interrupt() 方法来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断。
csharp
public class HandleInterruptedException2 implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new HandleInterruptedException2());
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
@Override
public void run() {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("发生中断,程序运行结束");
break;
}
System.out.println("work");
reInterrupt();
}
}
private void reInterrupt() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
}
拓展
实现线程只有一种方式
关于这个问题,我们先不聚焦为什么说创建线程只有一种方式,先认为有两种创建线程的方式,而其他的创建方式,比如线程池或是定时器,它们仅仅是在 new Thread() 外做了一层封装,如果我们把这些都叫作一种新的方式,那么创建线程的方式便会千变万化、层出不穷,比如 JDK 更新了,它可能会多出几个类,会把 new Thread() 重新封装,表面上看又会是一种新的实现线程的方式,透过现象看本质,打开封装后,会发现它们最终都是基于 Runnable 接口或继承 Thread 类实现的。
接下来,我们进行更深层次的探讨,为什么说这两种方式本质上是一种呢?
typescript
@Override
public void run() {
if (target != null) {
target.run();
}
}
首先,启动线程需要调用 start() 方法,而 start() 方法最终还会调用 run() 方法,我们先来看看第一种方式中 run() 方法究竟是怎么实现的,可以看出 run() 方法的代码非常短小精悍,第 1 行代码 if (target != null) ,判断 target 是否等于 null,如果不等于 null,就执行第 2 行代码 target.run(),而 target 实际上就是一个 Runnable,即使用 Runnable 接口实现线程时传给Thread类的对象。
然后,我们来看第二种方式,也就是继承 Thread 方式,实际上,继承 Thread 类之后,会把上述的 run() 方法重写,重写后 run() 方法里直接就是所需要执行的任务,但它最终还是需要调用 thread.start() 方法来启动线程,而 start() 方法最终也会调用这个已经被重写的 run() 方法来执行它的任务,这时我们就可以彻底明白了,事实上创建线程只有一种方式,就是构造一个 Thread 类,这是创建线程的唯一方式。
我们上面已经了解了两种创建线程方式本质上是一样的,它们的不同点仅仅在于实现线程运行内容的不同,那么运行内容来自于哪里呢?
运行内容主要来自于两个地方,要么来自于 target,要么来自于重写的 run() 方法,在此基础上我们进行拓展,可以这样描述:本质上,实现线程只有一种方式,而要想实现线程执行的内容,却有两种方式,也就是可以通过 实现 Runnable 接口的方式,或是继承 Thread 类重写 run() 方法的方式,把我们想要执行的代码传入,让线程去执行,在此基础上,如果我们还想有更多实现线程的方式,比如线程池和 Timer 定时器,只需要在此基础上进行封装即可。
实现 Runnable 接口比继承 Thread 类实现线程要好
下面我们来对刚才说的两种实现线程内容的方式进行对比,也就是为什么说实现 Runnable 接口比继承 Thread 类实现线程要好?好在哪里呢?
首先,我们从代码的架构考虑,实际上,Runnable 里只有一个 run() 方法,它定义了需要执行的内容,在这种情况下,实现了 Runnable 与 Thread 类的解耦,Thread 类负责线程启动和属性设置等内容,权责分明。
第二点就是在某些情况下可以提高性能,使用继承 Thread 类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了 Thread 类的类,如果此时执行的内容比较少,比如只是在 run() 方法里简单打印一行文字,那么它所带来的开销并不大,相比于整个线程从开始创建到执行完毕被销毁,这一系列的操作比 run() 方法打印文字本身带来的开销要大得多,相当于捡了芝麻丢了西瓜,得不偿失。如果我们使用实现 Runnable 接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
第三点好处在于 Java 语言不支持双继承,如果我们的类一旦继承了 Thread 类,那么它后续就没有办法再继承其他的类,这样一来,如果未来这个类需要继承其他类实现一些功能上的拓展,它就没有办法做到了,相当于限制了代码未来的可拓展性。
综上所述,我们应该优先选择通过实现 Runnable 接口的方式来创建线程。
好啦,本课时的全部内容就讲完了,在这一课时我们主要学习了 通过 Runnable 接口和继承 Thread 类等几种方式创建线程,又详细分析了为什么说本质上只有一种实现线程的方式,以及实现 Runnable 接口究竟比继承 Thread 类实现线程好在哪里?学习完本课时相信你一定对创建线程有了更深入的理解。