Java 线程常用 API

1,013 阅读6分钟

一、Java 线程常用方法

方法名static功能说明注意
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 调度的机率
getState()获取线程状态Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted()判断线程是否被打断不会清除 打断标记
isAlive()判断线程是否存活(还没有运行完毕)
interrupt()打断线程如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出InterruptedException,并清除 打断标记 ; 如果打断的正在运行的线程,则会设置 打断标记park 的线程被打断,也会设置 打断标记
interrupted()static判断当前线程是否被打断会清除 打断标记
currentThread()static获取正在执行的线程
sleep(long n)static让当前执行的线程休眠 n 毫秒, 休眠时让出 cpu 的时间片给其它 线程
yield()static提示线程调度器让出当前线程对 CPU的使用主要是为了测试和调试

二、方法对比

2.1 run 和 start 方法

当创建一个线程后,直接调用 run 方法会出现什么现象呢?其实答案很简单,就是和我们创建一个普通类调用成员方法差不多,此时并没有开启一个新的线程来运行,线程调用的方法仍然实在 main 线程运行的。线程调用了 start 方法后,才是真正地开启了一个线程,但注意的是不能多次调用这个方法。

/**
 * start 和 run 方法
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test4")
public class Test4 {

    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
                // 读取一个 mp4 文件,需要一定的时间
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };

        // 直接调用 t1.run() 并没有开启线程, 此时是同步调用
        t1.run();
        
        // t1.start();
        log.debug("do other things...");
    }
}

输出

16:27:12.321 c.Test4 [main] - running...
16:27:12.334 c.FileReader [main] - read [316 01.冒泡、选择、堆排序.flv] start ...
16:27:13.646 c.FileReader [main] - read [316 01.冒泡、选择、堆排序.flv] end ... cost: 1312 ms
16:27:13.646 c.Test4 [main] - do other things...

从输出可以看出,结果还是同步的。只有 t1 调用 start 方法,结果才是异步的。

2.2 sleep 和 yield 方法

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)。调用 yield 让当前线程从 Running 进入 Runnable 状态,但还是有机会获得 CPU 的调度;
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException;
  3. 睡眠结束后的线程未必会立刻得到执行;
  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。
/**
 * sleep 的第 1 点演示
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test6")
public class Test6 {

    public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.start();
        log.debug("t1 state: {}", t1.getState());

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 state: {}", t1.getState());
    }
}

/**
 * sleep 的第 2 点演示
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test7")
public class Test7 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("enter sleep...");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    log.debug("wake up...");
                    e.printStackTrace();
                }
            }
        };
        t1.start();

        Thread.sleep(1000);
        // 打断正在睡眠的线程
        log.debug("interrupt...");
        t1.interrupt();
    }
}

/**
 * sleep 的第 4 点演示:直观的线程睡眠方法的使用
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test8")
public class Test8 {

    public static void main(String[] args) throws InterruptedException {
        log.debug("enter");
        TimeUnit.SECONDS.sleep(1);
        log.debug("end");
    }
}

依次输出的结果

# Test6
16:38:15.018 c.Test6 [main] - t1 state: RUNNABLE
16:38:15.522 c.Test6 [main] - t1 state: TIMED_WAITING

# Test7
16:41:02.340 c.Test7 [t1] - enter sleep...
16:41:03.336 c.Test7 [main] - interrupt...
16:41:03.336 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.Test7$1.run(Test7.java:18)
    
# Test8
16:41:50.976 c.Test8 [main] - enter
16:41:51.981 c.Test8 [main] - end

这两个方法可以让出 CPU 的使用权,当某个线程的工作繁忙时,可以让出使用权给其他线程,避免 CPU 一直被占用。

2.3 join 方法

下面的代码执行,打印 r 是 0,而我们想要的结果是 10。这时,join 方法就派上用场了,它的作用参考表格.

/**
 * join 方法
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test10")
public class Test10 {
    static int r = 0;

    public static void main(String[] args) throws InterruptedException {
        test1();
    }

    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            sleep(1);
            log.debug("结束");
            r = 10;
        },"t1");

        t1.start();
        // t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
}

输出

# 没有使用 join 方法
16:48:18.471 c.Test10 [main] - 开始
16:48:18.477 c.Test10 [t1] - 开始
16:48:18.475 c.Test10 [main] - 结果为:0
16:48:18.479 c.Test10 [main] - 结束
16:48:19.480 c.Test10 [t1] - 结束

# 使用了 join 方法
16:48:36.553 c.Test10 [main] - 开始
16:48:36.558 c.Test10 [t1] - 开始
16:48:37.560 c.Test10 [t1] - 结束
16:48:37.560 c.Test10 [main] - 结果为:10
16:48:37.562 c.Test10 [main] - 结束

小思考:之所以不用 sleep 方法来替代 join,是因为我们事前根本就不能知道 t1 的任务执行的时间,所以也就不知道究竟要等多久。join方法还有一个重载的方法,该方法的作用是等待超过的时间就不等了,CPU 执行其他的线程。

2.4 interrupt 方法

sleep,wait,join这几个方法都会让线程进入阻塞状态,interrupt 方法打断调用了这三个方法的线程,会清空打断状态(把打断状态赋值为 false),以 sleep 为例:

/**
 * interrupt 方法打断 wait, join, sleep
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test11")
public class Test11 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                // wait, join, sleep 的线程被打断会清除打断标记
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");

        t1.start();
        Thread.sleep(1000);
        log.debug("interrupt");
        t1.interrupt();
        log.debug("打断标记:{}", t1.isInterrupted());
    }
}

输出

16:58:46.903 c.Test11 [t1] - sleep...
16:58:47.899 c.Test11 [main] - interrupt
16:58:47.899 c.Test11 [main] - 打断标记:false
16:58:47.902 c.Test11 [main] - 线程状态:TERMINATED
java.lang.InterruptedException: sleep interrupted
	at java.base/java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.Test11.lambda$main$0(Test11.java:17)
	at java.base/java.lang.Thread.run(Thread.java:834)

如果是打断正常运行的线程,不会清空打断状态。

/**
 * interrupted 方法打断正常执行的线程
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test12")
public class Test12 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while(true) {
                boolean interrupted = Thread.currentThread().isInterrupted();
                if(interrupted) {
                    log.debug("被打断了, 退出循环");
                    break;
                }
            }
        }, "t1");
        t1.start();

        Thread.sleep(1000);
        log.debug("interrupt");
        // 被打断的线程并不会终止运行,可以通过打断标记让它自己终止
        t1.interrupt();
    }
}

输出

17:02:56.077 c.Test12 [main] - interrupt
17:02:56.082 c.Test12 [t1] - 被打断了, 退出循环

使用 interrupted 方法我们可以优雅地终止一个线程,优雅是指被打断的线程可以“料理后事”。

/**
 * 两阶段终止模式
 * @author 落霞不孤
 */
@Slf4j(topic = "c.TestTwoPhaseTermination")
public class TestTwoPhaseTermination {
    public static void main(String[] args) throws InterruptedException {
        TPTInterrupt t = new TPTInterrupt();
        t.start();

        Thread.sleep(3500);
        log.debug("stop");
        t.stop();
    }
}

@Slf4j(topic = "c.TPTInterrupt")
class TPTInterrupt {
    private Thread monitor;

    public void start(){
        monitor = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    Thread.sleep(1000); // 情况 1
                    log.debug("将结果保存"); // 情况 2
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 重新设置打断标记
                    current.interrupt();
                }

            }
        },"监控线程");
        monitor.start();
    }

    public void stop() {
        monitor.interrupt();
    }
}

输出

17:17:15.115 c.TPTInterrupt [监控线程] - 将结果保存
17:17:16.118 c.TPTInterrupt [监控线程] - 将结果保存
17:17:17.119 c.TPTInterrupt [监控线程] - 将结果保存
17:17:17.612 c.TestTwoPhaseTermination [main] - stop
17:17:17.613 c.TPTInterrupt [监控线程] - 料理后事

打断 park 线程,不会清空打断状态:

/**
 * interrupted 方法打断 park 线程
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test14")
public class Test14 {

    private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            // park 方法的作用就是暂停线程的运行
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();

        sleep(1);
        // 打断 park 的线程,让它继续向下执行
        t1.interrupt();
    }

    // 如果打断标记已经是 true, 则 park 会失效
    private static void test4() {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                log.debug("park...");
                LockSupport.park();
                log.debug("打断状态:{}", Thread.interrupted());
                LockSupport.park();
                log.debug("unpark...");
            }
        });
        t1.start();

        sleep(1);
        t1.interrupt();
    }

    public static void main(String[] args) throws InterruptedException {
        test3();
    }
}

输出

17:15:38.744 c.Test14 [t1] - park...
17:15:39.738 c.Test14 [t1] - unpark...
17:15:39.738 c.Test14 [t1] - 打断状态:true

如果打断标记已经是 true, 则 park 会失效。这时候线程会继续执行下去,可以使用 Thread.interrupted() 清除打断状态。调用 test4() 方法: 输出

17:18:28.569 c.Test14 [Thread-0] - park...
17:18:29.566 c.Test14 [Thread-0] - 打断状态:true

三、主线程和守护线程

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

/**
 * 守护线程
 * @author 落霞不孤
 */
@Slf4j(topic = "c.Test15")
public class Test15 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("t1 开始运行");
            Sleeper.sleep(2);
            log.debug("t1 运行结束");
        }, "daemon");

        // 设置线程为守护线程
        log.debug("开始运行");
        t1.setDaemon(true);
        t1.start();

        Sleeper.sleep(1);
        log.debug("运行结束");
    }
}

输出

19:40:53.777 c.Test15 [main] - 开始运行
19:40:53.780 c.Test15 [daemon] - t1 开始运行
19:40:54.781 c.Test15 [main] - 运行结束

注意

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

四、总结

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。

方法名static功能说明
stop()停止线程运行
suspend()挂起(暂停)线程运行
resume()恢复线程运行

通过这节课的学习,我对线程常用的 API 有了简单的认识。运用这些方法,也能解决一些简单的同步和异步的问题。