一、线程的创建
1.Oracle 官网描述
There are two ways to create a new thread of execution.
One is to declare a class to be a subclass of Thread
. This subclass should override the run
method of class Thread
. An instance of the subclass can then be allocated and started.
The other way to create a thread is to declare a class that implements the Runnable
interface. That class then implements the run
method. An instance of the class can then be allocated, passed as an argument when creating Thread
, and started.
有两种方式可以创建新的执行线程。一种方法是定义一个 Thread 类的子类,该子类重写 Thread 类的 run 方法。然后可以分配并启动子类的实例。创建线程的另一种方法是定义一个实现 Runnable 接口的类,然后可以分配该类的实例,在创建 Thread 时将其作为参数传递并启动。
1.1 继承 Thread 类
public class ThreadStyle extends Thread {
@Override
public void run() {
System.out.println("用 Thread 方式创建线程");
}
public static void main(String[] args) {
new ThreadStyle().start();
}
}
定义一个类继承 Thread 类,然后重写 run 方法,直接通过该类的实例启动线程,输出结果为:用 Thread 方式创建线程。
1.2 实现 Runnable 接口
public class RunnableStyle implements Runnable {
@Override
public void run() {
System.out.println("用 Runnable 方式实现线程");
}
public static void main(String[] args) {
new Thread(new RunnableStyle()).start();
}
}
定义一个类实现 Runnable 接口并重写 run 方法,通过将该 Runnable 实例传入 Thread 类参数中启动线程,输出结果为:用 Runnable 方式实现线程。
1.3 两种方法对比
准确的讲,创建线程都是通过构造 Thread 类这一种方式实现,实现线程的执行单元有两种方式。
public class Thread implements Runnable {
/** 省略 */
private Runnable target;
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
- 方法一(继承 Thread 类):Thread 类本身也是实现了 Runnable 接口然后重写 run 方法的,当通过继承 Thread 类创建线程时,Thread 类的整个 run 方法都会被重写。
- 方法二(实现 Runnable 接口):Thread 类中有一个名为 target 的 Runnable 变量,在 Thread(Runnable target) 构造方法中传入 Runnable 实例会初始化 target 属性,通过该方法创建线程,最终只会调用 target.run() 方法,并不会重写整个 run 方法。
通过方法二创建线程的方式其实更好:
- 实现 Runnable 接口的方式与 Thread 类解耦。
- 接口可以多实现,继承 Thread 类的方式限制了可扩展性。
- 继承 Thread 类的话,每次新建线程都会去创建一个独立的线程,开销大,不适合资源共享。实现 Runnable 接口的话,则很容易的实现资源共享,而且可以利用线程池等工具,大大减小创建线程和销毁线程带来的损耗。
同时使用这两种方法创建线程:
先通过在 Thread 类构造器中传入匿名内部类(Runnable 实例)的方式创建线程,然后在此基础上重写了 Thread 类的 run 方法,最终输出:使用 Thread 方式创建。因为传入 Runnable 实例创建线程是调用 run 方法中的 target.run() 执行的,但是后面重写了 run 方法,导致此方法失效。
public class BothRunnableAndThread {
public static void main(String[] args) {
new Thread(() -> System.out.println("使用 Runnable 方式创建")) {
// 重写了 run 方法,覆盖了 run 里的三行代码
// runnable 传入进去却没有运行
@Override
public void run() {
System.out.println("使用 Thread 方式创建");
}
}.start();
}
}
创建线程的方式通常分为两类,除此之外,通过实现 Callable 接口、使用线程池等方式也可以创建线程,但是本质上都是继承 Thread 类或实现 Runnable 接口这两种方法。
1.4 线程初始化
不管通过哪种方式创建线程,都会调用线程初始化函数 init,可以通过不同的构造函数初始化线程的部分参数。如下 init 函数所示,一个新构造的线程对象是由其父线程来进行空间分配的,而子线程继承了父线程的 daemon、priority 等属性,同时还会分配一个唯一的 id 来标识这个线程。
public class Thread implements Runnable {
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) {
// 省略部分代码
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// 当前线程就是该线程的父线程
Thread parent = currentThread();
// 复制父线程的 daemon 和 priority 属性
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// 复制父线程的 inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 分配线程 ID
tid = nextThreadID();
}
}
2.实现 Callable 接口
public class CallableDemo implements Callable {
@Override
public String call() {
return "hncboy";
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask ft = new FutureTask<>(new CallableStyle());
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}
3.使用线程池
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(new TaskThread());
}
private static class TaskThread implements Runnable {
@Override
public void run() {
System.out.println("hncboy");
}
}
}
4.使用定时器
public class TimerTaskDemo {
public static void main(String[] args) {
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("hncboy");
}
}, 1000, 1000);
}
}
二、线程的启动
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()。
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 方法
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
};
位于 /src/hotspot/share/prims/jvm.cpp 的 JVM_StartThread 方法中有段注释
// 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。
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 类的一个基本方法
public class Thread implements Runnable {
/** 省略 */
@Override
public void run() {
if (target != null) {
target.run();
}
}
}
3.比较两方法
输出:main 和 Thread-0
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 循环停止,子线程运行结束。循环只进行了两次。
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.3 失败案例
使用 volatile 的局限性,当线程陷入阻塞时,使用 volatile 修饰的变量无法停止线程。
通过生产者消费者例子来演示阻塞情况下 volatile 的局限性,定义一个生产者类实现 Runnable 接口重写 run 方法,在 run 中当 volatile 修饰的 canceled 变量为 false 时,生产者通过 BlockingQueue 的 put 方法不断添加数据,当阻塞队列到达上限时,put 方法会阻塞。定义一个消费者类,通过 needMoreCount 方法判断消费者是否结束消费。
在主函数中初始化一个长度为 10 的阻塞队列,构建生产者和消费者实例,当消费者结束消费时,将生产者的 canceled 属性值改为 true,但是此时生产者仍然在运行,因为生产者线程阻塞在 put 方法。这就是 volatile 标志位的局限性了。
public class WrongVolatileDemo {
public static void main(String[] args) throws InterruptedException {
// 定义容量为 10 的阻塞队列
BlockingQueue 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 storage;
public Producer(BlockingQueue 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 storage;
public Consumer(BlockingQueue storage) {
this.storage = storage;
}
public BlockingQueue 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() 方法就能判断线程是否被中断。
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 方法会响应中断。
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();
}
}
运行结果如下:
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.3 interrupt 相关方法
3.4.1 interrupt()
设置中断标记,最终调用 native 的 interrupt0() 方法设置中断标记。
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 方法,找到该方法代码。
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 的代码。
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。
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() 在哪个线程里被调用,就返回哪个线程的中断标志。
public boolean isInterrupted() {
return isInterrupted(false);
}
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
private native boolean isInterrupted(boolean ClearInterrupted);
3.4.2 综合例子
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.");
}
}
运行结果:
isInterrupted: true
isInterrupted: false
isInterrupted: false
isInterrupted: true
Main thread is over.
3.4 能响应中断的部分方法
有些阻塞方法是不可中断的,例如 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.5 InterruptedException 异常处理
3.5.1 传递中断
当在 run 中调用了一个有异常的方法时,该异常应该在方法中用 throws 声明,传递到 run 方法,而不是在方法中捕获,此时可能会造成不可预料的结果。throwInMethod2() 为正确做法,throwInMethod1() 为错误做法。
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.5.2 重新设置中断状态
因为阻塞抛出 InterruptedException 异常后,会清除中断状态。可以在 catch 子语句中调用 Thread.currentThread().interrupt() 方法来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断。
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();
}
}
}
《Java 并发编程的艺术》
深入理解Thread.run()底层实现
Thread.interrupt()相关源码分析