线程-Thread

70 阅读6分钟

1、进程与线程

  • 进程是资源分配的最小单位,线程是任务执行的最小单位
    • 运行一个 Java 服务,就是开启了一个 JVM 进程,在 JVM 进程中可以创建多个线程,由线程来执行具体的代码

2、构造方法

  • Thread 有多种构造方法,本质都是执行 init 方法,方法会做一些判断和初始化一些变量
private void init(
  // 线程组
  ThreadGroup g,
  // 执行的任务
  Runnable target,
  // 线程名称,默认"Thread-自增数"
  String name,
  // 当前线程的栈大小,默认0,有些 JVM 会忽略掉这个参数
  long stackSize,
  AccessControlContext acc,
  // 是否从创建线程的线程中继承 ThreadLocal
  boolean inheritThreadLocals) {
  
  // 检查线程名称
  if (name == null) {
    throw new NullPointerException("name cannot be null");
  }

  // 设置线程名称
  this.name = name;

  // 获取创建线程的线程-父线程
  Thread parent = currentThread();
  
  // 获取安全管理器
  SecurityManager security = System.getSecurityManager();
  
  // 设置线程组,如果未指定线程组,会先从安全管理器中获取,如果安全管理器中获取不到,会从父线程中获取
  if (g == null) {
    /* Determine if it's an applet or not */

    /* If there is a security manager, ask the security manager
               what to do. */
    if (security != null) {
      g = security.getThreadGroup();
    }

    /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
    if (g == null) {
      g = parent.getThreadGroup();
    }
  }

  /* checkAccess regardless of whether or not threadgroup is explicitly passed in. */
  g.checkAccess();

  /*
    * Do we have the required permissions?
  */
  if (security != null) {
    if (isCCLOverridden(getClass())) {
      security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
    }
  }

  // 记录线程组未启动线程数
  g.addUnstarted();

  // 设置线程组
  this.group = g;
  
  // 设置是否未守护线程,和父线程一样
  this.daemon = parent.isDaemon();
  
  // 设置优先级,和父线程一样
  this.priority = parent.getPriority();
  
  if (security == null || isCCLOverridden(parent.getClass()))
    this.contextClassLoader = parent.getContextClassLoader();
  else
    this.contextClassLoader = parent.contextClassLoader;
  this.inheritedAccessControlContext =
    acc != null ? acc : AccessController.getContext();
  
  // 设置执行任务
  this.target = target;

  // 处理优先级
  setPriority(priority);
  
  // 继承父线程的 ThreadLocal
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  
  /* Stash the specified stack size in case the VM cares */
  // 设置栈大小
  this.stackSize = stackSize;

  /* Set thread ID */
  // 设置id---自增
  tid = nextThreadID();
}

3、常见方法

方法名功能说明注意
start()启动一个新线程,在新的线程运行 run 方法中的代码start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException
run()新线程启动后会调用的方法如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
join()等待线程运行结束
join(long n)等待线程运行结束,最多等待 n 毫秒
getId()获取线程长整型的 idid 唯一
getName()获取线程名
setName(String)修改线程名
getPriority()获取线程优先级
setPriority(int)修改线程优先级java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率
1、线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
2、如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断是否被打断不会清除打断标记
isAlive()线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park 的线程被打断,也会设置打断标记
interrupted()判断当前线程是否被打断会清除打断标记
currentThread()获取当前正在执行的线程
sleep(long n)让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程1、调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
2、其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
3、睡眠结束后的线程未必会立刻得到执行(CPU 的时间片还没分给它)
yield()提示线程调度器让出当前线程对CPU的使用主要是为了测试和调试
1、调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2、具体的实现依赖于操作系统的任务调度器
stop()停止线程运行过时,不推荐
suspend()挂起(暂停)线程运行过时,不推荐
resume()恢复线程运行过时,不推荐

(1)start

  • 启动线程,执行 run() 方法
  • 一个线程只能被启动一次
  • 该方法只是让线程进入可执行的状态,真正执行 run() 方法需要等待 cpu 分配时间片
public synchronized void start() {
  /**
   * This method is not invoked for the main method thread or "system"
   * group threads created/set up by the VM. Any new functionality added
   * to this method in the future may have to also be added to the VM.
   *
   * A zero status value corresponds to state "NEW".
   */
  // 检查线程状态
  if (threadStatus != 0)
    throw new IllegalThreadStateException();

  /* Notify the group that this thread is about to be started
   * so that it can be added to the group's list of threads
   * and the group's unstarted count can be decremented. */
  // 将当前线程加入线程组
  group.add(this);

  boolean started = false;
  try {
    // 启动线程
    start0();
    started = true;
  } finally {
    try {
      if (!started) {
        // 通知线程组启动失败
        group.threadStartFailed(this);
      }
    } catch (Throwable ignore) {
      /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
    }
  }
}

private native void start0();

(2)join

  • 等待线程运行结束,可以一直等待或带超时时间的等待,通过 wait / notify 实现
  • 如果线程是存活状态(并且没有超过超时时间),会调用 wait 方法,等待被 notify,循环执行
  • 如果其他线程打断了该线程,会抛出 InterruptedException
public final synchronized void join(long millis) throws InterruptedException {
  // 调用方法时的时间
  long base = System.currentTimeMillis();
  // 已经等待的时间
  long now = 0;

  // 判断等待超时时间
  if (millis < 0) {
    throw new IllegalArgumentException("timeout value is negative");
  }

  if (millis == 0) {
    // 如果线程还是存活状态,一直调用 wait 方法
    while (isAlive()) {
      wait(0);
    }
  } else {
    // 如果线程还是存活状态,并且需要等待的时间未小于0,一直调用 wait 方法
    while (isAlive()) {
      // 计算还需要等待的时间
      long delay = millis - now;
      if (delay <= 0) {
        // 等待时间小于0直接退出
        break;
      }
      wait(delay);
      // 更新已经等待时间
      now = System.currentTimeMillis() - base;
    }
  }
}

(3)interrupt

  • 打断线程,如果线程在执行可中断 IO 操作时,先设置打断标记再通知阻塞者中断,否则直接设置打断标记
  • 若线程阻塞在 Object 类的 wait 方法、Thread 类自己的 join 和 sleep 方法调用上,那么中断状态会被清除,线程会收到 InterruptedException
  • 若线程阻塞在 InterruptibleChannel 的 IO 操作上,那么通道会被关闭,中断状态会被置位,线程会收到 ClosedByInterruptException
  • 若线程阻塞在 Selector 上,那么中断状态会被置位,select操作会立即返回,返回值可能是一个非零值
  • 其他情况下中断状态都会被置位
public void interrupt() {
  if (this != Thread.currentThread())
    checkAccess();

  // 获取阻塞者锁
  synchronized (blockerLock) {
    // 线程在执行可中断IO操作时阻塞该线程的对象
    Interruptible b = blocker;
    if (b != null) {
      // 如果阻塞者不为空设置打断标记
      interrupt0(); // Just to set the interrupt flag
      // 调用阻塞者打断方法,中断IO
      b.interrupt(this);
      return;
    }
  }
  // 设置打断标记
  interrupt0();
}

4、线程状态

(1)操作系统层面

  • 初始状态
    • 仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 可运行状态
    • (就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
  • 运行状态
    • 指获取了 CPU 时间片运行中的状态
    • 当 CPU 时间片用完,会从运行状态转换至可运行状态,会导致线程的上下文切换
  • 阻塞状态
    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入阻塞状态
    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至可运行状态
    • 与可运行状态的区别是,对阻塞状态的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 终止状态
    • 表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

(2)Java层面

  • NEW(尚未启动)
    • 线程刚被创建,但是还没有调用 start() 方法
  • RUNNABLE(可运行状态)
    • 调用了 start() 方法之后
    • Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的可运行状态、运行状态和阻塞状态(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  • BLOCKED (阻塞状态)
    • 等待 monitor 锁进入同步代码块/方法
  • WAITING(等待状态)
    • 处于等待状态的线程正在等待另一个线程执行特定操作
    • Object 的 wait() 方法,没设置超时时间
    • Thread 的 join() 方法,没设置超时时间
    • LockSupport 的 park() 方法
  • TIMED_WAITING (具有指定等待时间的等待状态)
    • Thread 的 sleep() 方法
    • Object 的 wait() 方法,设置了超时时间
    • Thread 的 join() 方法,设置了超时时间
    • LockSupport 的 parkNanos() 方法
    • LockSupport 的 parkUntil() 方法
  • TERMINATED (终止状态)
    • 线程代码运行结束

(3)线程状态转换

线程状态流转.png

5、用户线程与守护线程

  • 默认情况下,JVM 需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要用户线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

  • 平时创建的线程就是用户线程,可调用 setDaemon() 方法,参数传 true 设置为守护线程

6、线程运行

  • 每个线程启动后,虚拟机就会为其分配一块栈内存和程序计数器。
      • 每个栈由多个栈帧(Frame)组成
        • 栈针对应着每次方法的调用
        • 栈针包括局部变量表,返回地址,锁记录,操作数栈
    • 程序计数器
      • 选取下一条需要执行的字节码指令
    • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

7、线程上下文切换

因为一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • 上下文切换频繁发生会影响性能

8、活跃性

(1)死锁

  • 死锁是指多个进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象(互相挂起等待),若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

  • 死锁产生的必要条件

    • 互斥条件
      • 该资源任意一个时刻只由一个线程占用
    • 请求与保持条件
      • 一个进程因请求资源而阻塞时,对已获得的资源保持不放
    • 不剥夺条件
      • 线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
    • 循环等待条件
      • 若干进程之间形成一种头尾相接的循环等待资源关系
  • 死锁定位

    • 通过 jps 命令找到当前 JVM 的进程 id

      • admin@admindeMacBook-Pro fighting % jps
        4880 RemoteMavenServer36
        627 
        4887 RemoteMavenServer36
        6776 Jps
        4888 RemoteMavenServer36
        6493 Launcher
        6494 DeadlockDemo
        
    • 通过 jstack <进程 id>,查看堆栈信息,分析死锁

      • Found one Java-level deadlock:
        =============================
        "Thread-1":
          waiting to lock monitor 0x00007fe951030aa8 (object 0x00000007957784c0, a java.lang.Object),
          which is held by "Thread-0"
        "Thread-0":
          waiting to lock monitor 0x00007fe95102e218 (object 0x00000007957784d0, a java.lang.Object),
          which is held by "Thread-1"
        
        Java stack information for the threads listed above:
        ===================================================
        "Thread-1":
                at com.jelly.fighting.jdk.concurrent.DeadlockDemo.lambda$main$1(DeadlockDemo.java:28)
                - waiting to lock <0x00000007957784c0> (a java.lang.Object)
                - locked <0x00000007957784d0> (a java.lang.Object)
                at com.jelly.fighting.jdk.concurrent.DeadlockDemo$$Lambda$2/990368553.run(Unknown Source)
                at java.lang.Thread.run(Thread.java:748)
        "Thread-0":
                at com.jelly.fighting.jdk.concurrent.DeadlockDemo.lambda$main$0(DeadlockDemo.java:18)
                - waiting to lock <0x00000007957784d0> (a java.lang.Object)
                - locked <0x00000007957784c0> (a java.lang.Object)
                at com.jelly.fighting.jdk.concurrent.DeadlockDemo$$Lambda$1/2003749087.run(Unknown Source)
                at java.lang.Thread.run(Thread.java:748)
        
        Found 1 deadlock.
        
  • 避免死锁

    • 固定顺序加锁
      • 线程 1 和线程 2 都按照 lock1、lock2 的顺序加锁
    • 增加超时时间
      • 当过了超时时间之后自动释放锁资源,但是可能会出现逻辑未执行完,锁已经释放,发生并发冲突问题
    • 释放锁资源
      • 线程 1 尝试获取 lock1 成功后,再尝试获取 lock2 ,如果获取 lock2 失败,就释放 lock1,但是可能会导致活锁

(2)活锁

  • 多个线程互相改变对方的结束条件,虽然没有阻塞,但是也无法结束
  • 避免活锁
    • 交错执行,增加随机睡眠时间

(3)饥饿

  • 因为某种原因,线程始终得不到 CPU 调度,无法执行
  • 解决饥饿
    • 合理安排任务线程的优先级
    • 使用公平锁