前言
本文将对 Java 线程 Thread 进行学习和总结,以下是概览:
一、Thread 创建
线程的创建主要依靠实现 Runnable
接口。调用 start()
方法使线程进入就绪状态,等待 CPU
调度,然后 run ()
方法由 JVM
调用。
1.1 实现 Runnable
public interface Runnable {
public abstract void run();
}
例子
public class TestThread implements Runnable{
@Override
public void run() {
System.out.println("TestThread Running");
}
}
使用
public static void main(String[] args) {
// 创建线程
TestThread testThread = new TestThread();
// 将 Runnable 对象作为参数传递给 Thread
Thread thread = new Thread(testThread);
thread.start();
}
为什么不能直接创建 TestThread
对象并调用 run()
方法呢,因为这样只是普通地调用了对象的方法,并没有经历创建线程的过程。
所以还是需要利用 Java 为我们写好的 Thread
类对线程对象(实现了 Runnable
的对象)进行一次包装,然后由 Thread
类替我们完成创建并执行线程的过程。
1.2 继承 Thread
Thread
类本身实现了 Runnable
接口,内部又包装了一个 Thread
对象 target
,执行 run
方法的时候实际调用 target
的 run
方法。
public class Thread implements Runnable {
private Runnable target;
...
public void run() {
if (target != null) {
target.run();
}
}
}
所以本质上还是实现了 Runnable
接口实现线程的创建。
二、部分属性
Thread
类的部分属性:
// 对象锁,用于使当前线程占有 CPU
private final Object lock = new Object();
// 该值不等于0,说明线程存活,尚不知原理
private volatile long nativePeer;
// 线程名称
private volatile String name;
// 线程优先级
private int priority;
// 是否守护线程
private boolean daemon = false;
接下来将逐个分析。
2.1 lock 锁对象
lock
对象用于加锁和使调用者线程进入阻塞状态。
阻塞
调用 join
方法会使当前线程获取 lock
对象的锁,然后 lock
对象执行 wait
方法进入阻塞状态,实现阻塞当前线程的效果。
public final void join() throws InterruptedException {
// 传入 0 表示无限阻塞
join(0);
}
public final void join(long millis) throws InterruptedException {
synchronized(lock) { // 同步锁
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) { // 等于 0 无限阻塞
while (isAlive()) {
lock.wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
lock.wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
}
比如现在有个线程 Thread1
,在运行过程中需要执行 Thread2
的相关逻辑。Thread2
在 Thread1
的 run()
方法中调用了 join()
方法,这样 Thread1
就获取到了 Thread2
中 lock
对象的锁。
lock
对象循环调用 wait()
方法进行阻塞。因为这时是处于 Thread1
的运行环境下,并且由 synchronized 获取了 monitor(相当于同步锁),所以 Thread1
进入了阻塞状态。
Java Object wait() 方法
注意:
当前线程必须是此对象的监视器所有者,否则还是会发生 IllegalMonitorStateException 异常。
如果当前线程在等待之前或在等待时被任何线程中断,则会抛出 InterruptedException 异常。
解除阻塞
解除阻塞有两种方式,但不限于这两种。一种是 超时 自动释放,另一种是由 JVM 释放。
-
join()
方法传入millis (毫秒数)
,在时间达到之后跳出循环,lock
对象不再加锁。这样Thread1
持有的lock
对象不再阻塞,由此该线程回到就绪状态。 - 加入线程执行完毕,JVM 释放锁。线程死亡时 JVM 会调用
lock
对象的notify_all()
方法来释放所有锁。
以下代码摘自 Thread.join的作用和原理 中的 hotspot 虚拟机源码:
void JavaThread::exit(bool destroy_vm, ExitType exit_type) {
assert(this == JavaThread::current(), "thread consistency check");
...
// Notify waiters on thread object. This has to be done after exit() is called
// on the thread (if the thread is the last thread in a daemon ThreadGroup the
// group should have the destroyed bit set before waiters are notified).
ensure_join(this);
assert(!this->has_pending_exception(), "ensure_join should have cleared");
...
可以看到线程退出方法 exit()
调用了 ensure_join(this)
释放锁:
static void ensure_join(JavaThread* thread) {
// We do not need to grap the Threads_lock, since we are operating on ourself.
Handle threadObj(thread, thread->threadObj());
assert(threadObj.not_null(), "java thread object must exist");
ObjectLocker lock(threadObj, thread);
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
// Thread is exiting. So set thread_status field in java.lang.Thread class to TERMINATED.
java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED);
// Clear the native thread instance - this makes isAlive return false and allows the join()
// to complete once we've done the notify_all below
//这里是清除native线程,这个操作会导致isAlive()方法返回false
java_lang_Thread::set_thread(threadObj(), NULL);
lock.notify_all(thread);//注意这里
// Ignore pending exception (ThreadDeath), since we are exiting anyway
thread->clear_pending_exception();
}
这里的 lock
对象大概就是 Thread
中的同步锁对象了,调用 Java Object notifyAll() 方法 用来唤醒所有持有该对象锁的线程。
2.2 其它参数
- nativePeer: 判断线程是否存活依靠此字段,可能在线程销毁的时候把该值置为 0。
public final boolean isAlive() {
return nativePeer != 0;
}
- priority: 优先级,默认
NORM_PRIORITY=5
。最小MIN_PRIORITY = 1
,最大MAX_PRIORITY = 10
。
设置该参数的作用在于希望较高优先级的线程可以先行执行,但实际可能并非总是如此。
优先级和操作系统及虚拟机版本相关。
优先级只是代表告知了 「线程调度器」该线程的重要度有多大。如果有大量线程都被堵塞,都在等候运
行,调试程序会首先运行具有最高优先级的那个线程。然而,这并不表示优先级较低的线程不会运行(换言之,不会因为存在优先级而导致死锁)。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。
- daemon: 是否为守护线程。守护线程就像一个卫士,追随用户线程。当用户线程销毁时,守护线程也就没有了意义,可能会被随时回收。
参考:什么是守护线程?
三、Thread 状态
3.1 线程的几种状态
有关线程状态,大概有两种说法。
- 一种是概括性的五种状态:
- 创建状态 已经创建出对象,并未调用 start;
- 就绪状态 调用 start 方法之后,并未开始运行;
- 运行状态 被 cpu 执行,run 方法被调用;
- 阻塞状态 在运行过程中被暂停,调用 wait() 或被别的线程 join() 等;
- 死亡状态 run 方法执行结束,或者调用 stop() 结束运行。
- 另一种是根据源码的六种状态:
- NEW 刚 new 出来;
- RUNNABLE 运行中;
- BLOCKED 等待同步锁;
- WAITING 阻塞状态,wait() 或被 join();
- TIMED_WAITING 阻塞状态,限时;
- TERMINATED 被终止。
个人感觉没有必要纠结孰对孰错,这是两个层面上的理解。前者更倾向于总览,后者更为细化。
3.2 线程状态控制
线程调度
简单地说就是设置优先级,使 JVM 进行协调。避免多个线程抢夺有限资源造成的死机或者崩溃。上文已经提到过,最低优先级 MIN_PRIORITY =1
、最高优先级 MAX_PRIORITY = 10
,默认优先级 NORM_PRIORITY=5
。
为了控制线程的运行策略,Java定义了线程调度器来监控系统中处于就绪状态的所有线程。线程调度器按照线程的优先级决定那个线程投入处理器运行。在多个线程处于就绪状态的条件下,具有高优先级的线程会在低优先级线程之前得到执行。
守护线程
特殊的低优先级守护(Daemon)线程,它是为系统中的其它对象或线程服务。
典型的守护线程例子是JVM中的系统资源自动回收线程,它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。
当所有用户线程销毁后,也不会产生垃圾了。守护线程会随着 JVM 销毁。
线程分组
Java定义了在多线程运行系统中的线程组(ThreadGroup)对象,用于实现按照特定功能对线程进行集中式分组管理。
用户创建的每个线程均属于某线程组,这个线程组可以在线程创建时指定,也可以不指定线程组以使该线程处于默认的线程组之中。但是,一旦线程加入某线程组,该线程就一直存在于该线程组中直至线程死亡,不能在中途改变线程所属的线程组。
与线程类似,可以针对线程组对象进行线程组的调度、状态管理以及优先级设置等。在对线程组进行管理过程中,加入到某线程组中的所有线程均被看作统一的对象。
四、Thread 其它
4.1 线程同步
- synchronized: 依赖 JVM 实现,通过锁住 对象、方法、类 等实现线程同步;
- volatile: 特殊域变量,使用该关键字修饰对象,保证其 可见性。使其无论在哪个线程读取时都从内存重新读取,而不是在 CPU 缓存中读取;
- 重入锁: ReentrantLock 可重入,内含 CAS+AQS 机制。CAS 保证数据准确性,AQS 保证顺序。
- ThreadLocal: 为每一条线程创建数据副本,这样各个线程间处理的数据互不影响。因为处理的不是同一数据,要注意数据的一致性。
4.2 终止线程
终止线程三种方法:
- 设置 flag 标记:为 false 时结束代码运行,JVM 会回收掉线程;
- stop 方法(不推荐):强行结束线程,但是可能会因马上释放锁造成数据产生误差;
- interrupt 方法:使线程进入中断状态,代码逻辑中 catch InterruptedException 用来结束逻辑执行。
总结
线程使用时要特别注意同步、锁的使用。由于多个线程不易管理,实际使用时一般用 线程池 进行处理。后面讲对线程池原理进行记录。
参考资料
线程的状态控制