JUC(四):线程基本方法

线程的常用方法

Thread 类 API:

方法 说明
public void start() 启动一个新线程,Java虚拟机调用此线程的 run 方法
public void run() 线程启动后调用该方法
public void setName(String name) 给当前线程取名字
public void getName() 获取当前线程的名字
线程存在默认名称:子线程是 Thread-索引,主线程是 main
public static Thread currentThread() 获取当前线程对象,代码在哪个线程中执行
public static void sleep(long time) 让当前线程休眠多少毫秒再继续执行
Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争
public static native void yield() 提示线程调度器让出当前线程对 CPU 的使用
public final int getPriority() 返回此线程的优先级
public final void setPriority(int priority) 更改此线程的优先级,常用 1 5 10
public void interrupt() 中断这个线程,异常处理机制
public static boolean interrupted() 判断当前线程是否被打断,清除打断标记
public boolean isInterrupted() 判断当前线程是否被打断,不清除打断标记
public final void join() 等待这个线程结束
public final void join(long millis) 等待这个线程死亡 millis 毫秒,0 意味着永远等待
public final native boolean isAlive() 线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on) 将此线程标记为守护线程或用户线程

1.run 和start

run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行

start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码,所以调用start方法才是开启一个线程,然后线程会自动执行run方法。

2.sleep和yield

sleep(线程睡眠):

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
  • sleep() 方法的过程中,线程不会释放对象锁
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU
  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

yield(线程让步):

  • 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用
  • 具体的实现依赖于操作系统的任务调度器
  • 会放弃 CPU 资源,锁资源不会释放

3.join

join方法:等待其他线程终止,在当前线程中调用一个线程的 join() 方法,则当前线程转为阻塞状态,等到另一个线程结束,当前线程再由阻塞状态变为就绪状态,等待 cpu 的调度。

原理:调用者轮询检查线程 alive 状态,t1.join() 等价于:

1
2
3
4
5
6
public final synchronized void join(long millis) throws InterruptedException {
// 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
while (isAlive()) {
wait(0);
}
}
  • join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁

线程同步:

  • join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行
    • 需要外部共享变量,不符合面向对象封装的思想
    • 必须等待线程结束,不能配合线程池使用
  • Future 实现(同步):get() 方法阻塞等待执行结果
    • main 线程接收结果
    • get 方法是让调用线程同步等待
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
t1.join();//不等待线程执行结束,输出的10
System.out.println(r);
}
}

4.interrupt

interrupt():中断线程

public void interrupt():中断这个线程,异常处理机制

public static boolean interrupted():判断当前线程是否被中断,中断返回 true,清除中断标记,连续调用两次一定返回 false

public boolean isInterrupted():判断当前线程是否被中断,不清除中断标记中断一个线程,其本意是给这个线程一个通知信号,会影响这个线程内部的一个中断标识位。这个线程本身并不会因此而改变状态(如阻塞,终止等)。

  1. 调用 interrupt()方法并不会中断一个正在运行的线程。也就是说处于 Running 状态的线程并不会因为被中断而被终止,仅仅改变了内部维护的中断标识位而已。

  2. 若调用 sleep()而使线程处于 TIMED-WATING 状态,这时调用 interrupt()方法,会抛出InterruptedException中断异常,从而使线程提前结束 TIMED-WATING 状态。

  3. 许多声明抛出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),抛出异常前,都会清除中断标识位,所以抛出异常后,调用 isInterrupted()方法将会返回 false。

  4. 中断状态是线程固有的一个标识位,可以通过此标识位安全的终止线程。比如,你想终止一个线程 thread 的时候,可以调用 thread.interrupt()方法,在线程的 run 方法内部可以根据 thread.isInterrupted()的值来优雅的终止线程。

    ⚠️中断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(注意中断不是停止

  • sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空中断状态(false)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(()->{
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }, "t1");
    t1.start();
    Thread.sleep(500);
    t1.interrupt();
    System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false
    }
  • 中断一个正常运行的线程:不会清空中断状态(true)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static void main(String[] args) throws Exception {
    Thread t2 = new Thread(()->{
    while(true) {
    Thread current = Thread.currentThread();
    boolean interrupted = current.isInterrupted();
    if(interrupted) {
    System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true
    break;
    }
    }
    }, "t2");
    t2.start();
    Thread.sleep(500);
    t2.interrupt();
    }

5.daemon

public final void setDaemon(boolean on):如果是 true ,将此线程标记为守护线程

线程启动前调用此方法:

1
2
3
4
5
6
7
8
9
Thread t = new Thread() {
@Override
public void run() {
System.out.println("running");
}
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();

用户线程:平常创建的普通线程

守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示

说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去

常见的守护线程:

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

object类下提供的线程方法

  1. notify() 方法:唤醒在此对象监视器上等待的单个线程。
  2. notifyAll() 方法:唤醒在此对象监视器上等待的所有线程。
  3. wait() 方法:线程从处于等待状态,直到其他线程调用此对象的notify() 方法和notifyAll() 方法。
  4. wait(long timeout) 方法:线程从处于等待状态,直到其他线程调用此对象的notify() 方法和notifyAll() 方法,或者超过指定的时间量。时间精度是毫秒级。