Java 线程:创建、属性以及状态控制

前言


本文将对 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 方法的时候实际调用 targetrun 方法。

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 的相关逻辑。Thread2Thread1run() 方法中调用了 join() 方法,这样 Thread1 就获取到了 Thread2lock 对象的锁。

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 用来结束逻辑执行。

总结

线程使用时要特别注意同步、锁的使用。由于多个线程不易管理,实际使用时一般用 线程池 进行处理。后面讲对线程池原理进行记录。

参考资料

线程的状态控制