JUC并发编程第四节:线程常用方法

start 与 run

调用run方法

public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };

        t1.run();
        log.debug("do other things...");
    }

输出:

19:39:14 [main] c.TestStart - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...

程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的

调用 start

public static void main(String[] args) {
        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                log.debug("running...");
                FileReader.read(Constants.MP4_FULL_PATH);
            }
        };

        t1.start();
        log.debug("do other things...");
    }

输出

19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms

程序在 t1 线程运行, FileReader.read() 方法调用是异步的

小结

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

  2. start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码(start方法只能调用一次)

    说明:线程控制资源类

  3. run() 方法中的异常不能抛出,只能 try/catch

    • 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
    • 异常不能跨线程传播回 main() 中,因此必须在本地进行处理

sleep 与 yieId

调用sleep方法

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();
    }

输出:

16:18:03.496 c.Test7 [t1] - enter sleep...
16:18:04.499 c.Test7 [main] - interrupt...
16:18:04.499 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.Test7$1.run(Test7.java:14)

推荐使用TimeUnit来设置线程睡眠时间可读性更好

TimeUnit.SECONDS.sleep(1);

调用yieId方法

public static void main(String[] args) {
        Runnable task1 = () -> {
            int count = 0;
            for (;;) {
                System.out.println("---->1 " + count++);
            }
        };
        Runnable task2 = () -> {
            int count = 0;
            for (;;) {
                Thread.yield();
                System.out.println("              ---->2 " + count++);
            }
        };
        Thread t1 = new Thread(task1, "t1");
        Thread t2 = new Thread(task2, "t2");
        //设置线程优先级
//        t1.setPriority(Thread.MIN_PRIORITY);
//        t2.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
    }

线程优先级

线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
不能依靠线程优先级来判断线程执行的先后顺序
设置线程优先级,最小是1,最大是10,默认是5

t1.setPriority(Thread.MIN_PRIORITY); //1
t2.setPriority(Thread.MAX_PRIORITY); //10

小结

sleep:

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

yield:

  • 调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用
  • 调用 yield 会让当前线程从 Running 进入 Runnable 状态(就绪状态),然后调度执行其他线程。(如果没有其他线程可以运行,任务调度器还是会把时间片分给 yieId 线程。)
  • 具体的实现依赖于操作系统的任务调度器
  • 会放弃 CPU 资源,锁资源不会释放

join方法

为什么需要join方法

不使用join方法

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("结束");
    }

结果:

17:15:14.079 c.Test10 [main] - 开始
17:15:14.176 c.Test10 [t1] - 开始
17:15:14.176 c.Test10 [main] - 结果为:0
17:15:14.179 c.Test10 [main] - 结束
17:15:15.181 c.Test10 [t1] - 结束

分析

  • 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
  • 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0

将代码注释打开
运行结果:

17:23:35.557 c.Test10 [main] - 开始
17:23:35.649 c.Test10 [t1] - 开始
17:23:36.662 c.Test10 [t1] - 结束
17:23:36.662 c.Test10 [main] - 结果为:10
17:23:36.665 c.Test10 [main] - 结束

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步
    在这里插入图片描述

等待多个结果

问,下面代码 cost 大约多少秒?

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test2();
}
private static void test2() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    Thread t2 = new Thread(() -> {
        sleep(2);
        r2 = 20;
    });
    long start = System.currentTimeMillis();
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

分析如下

  • 第一个 join:等待 t1 时, t2 并没有停止, 而在运行
  • 第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s

如果颠倒两个 join 呢?
最终都是输出

17:35:36.412 c.Test10 [main] - r1: 10 r2: 20 cost: 2009

在这里插入图片描述

有时效的join

等够时间

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(1);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

输出:

17:39:08.615 c.Test10 [main] - r1: 10 r2: 0 cost: 1013

没等够时间

static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
    test3();
}
public static void test3() throws InterruptedException {
    Thread t1 = new Thread(() -> {
        sleep(2);
        r1 = 10;
    });
    long start = System.currentTimeMillis();
    t1.start();
    // 线程执行结束会导致 join 结束
    t1.join(1500);
    long end = System.currentTimeMillis();
    log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}

输出:

17:40:28.601 c.Test10 [main] - r1: 0 r2: 0 cost: 1503

原理

public final void join():等待这个线程结束

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

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

  • 当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕

线程同步:

  • join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行
    • 需要外部共享变量,不符合面向对象封装的思想
    • 必须等待线程结束,不能配合线程池使用
  • Future 实现(同步):get() 方法阻塞等待执行结果
    • main 线程接收结果
    • get 方法是让调用线程同步等待

使用Future实现同步

Future 接口可以通过调用其 get() 方法来实现同步。这个方法会阻塞当前线程,直到异步任务完成并返回结果。

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建一个线程池
        ExecutorService executor = Executors.newFixedThreadPool(1);

        // 提交一个异步任务
        Future<Integer> futureResult = executor.submit(() -> {
            // 模拟一个耗时计算任务
            Thread.sleep(1000);
            return 42;
        });

        // 等待任务完成并获取结果
        int result = futureResult.get();
        System.out.println("Result: " + result);

        // 关闭线程池
        executor.shutdown();
    }
}

在这个示例中,创建了一个线程池,并使用 executor.submit() 方法提交了一个异步任务。然后,调用了 futureResult.get() 方法来等待任务完成,并获取结果。由于调用了 get() 方法,主线程会阻塞直到异步任务完成,因此这是一种同步的方式来执行异步任务。

需要注意的是,使用 Future 的同步方式可能会阻塞当前线程,降低程序的并发性能。因此,应该根据实际情况选择是否使用同步方式来执行异步任务。

interrupt方法

打断线程

interrupt 打断线程有两种情况,如下:

  • 如果一个线程在在运行中被打断,打断标记会被置为 true 。
  • 如果是打断因sleep wait join 方法而被阻塞的线程,会将打断标记置为 false 。

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

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

public boolean isInterrupted():判断当前线程是否被打断,不清除打断标记

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

  • sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)
public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("sleep...");
            try {
                Thread.sleep(5000); // wait, join
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1");

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

输出:

21:20:23.812 c.Test11 [t1] - sleep...
21:20:24.814 c.Test11 [main] - interrupt
21:20:24.814 c.Test11 [main] - 打断标记:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.Test11.lambda$main$0(Test11.java:12)
	at java.lang.Thread.run(Thread.java:750)
  • 打断正常运行的线程:不会清空打断状态(true)
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();
        log.debug("打断标记:{}", t1.isInterrupted());
    }

输出:

21:26:36.806 c.Test12 [main] - interrupt
21:26:36.811 c.Test12 [t1] - 被打断了, 退出循环
21:26:36.811 c.Test12 [main] - 打断标记:true

打断 park
park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)

private static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            LockSupport.park();
            log.debug("unpark...");
            log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();

        sleep(1);
        t1.interrupt();

    }

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

输出:

23:37:18.177 c.Test14 [t1] - park...
23:37:19.176 c.Test14 [t1] - unpark...
23:37:19.176 c.Test14 [t1] - 打断状态:true

如果打断标记已经是 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.currentThread().isInterrupted());
            }
        });
        t1.start();
        sleep(1);
        t1.interrupt();
    }
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

输出:

23:46:09.784 c.Test14 [Thread-0] - park...
23:46:10.795 c.Test14 [Thread-0] - 打断状态:true
23:46:10.799 c.Test14 [Thread-0] - park...
23:46:10.799 c.Test14 [Thread-0] - 打断状态:true
23:46:10.799 c.Test14 [Thread-0] - park...
23:46:10.799 c.Test14 [Thread-0] - 打断状态:true
23:46:10.799 c.Test14 [Thread-0] - park...
23:46:10.799 c.Test14 [Thread-0] - 打断状态:true
23:46:10.800 c.Test14 [Thread-0] - park...
23:46:10.800 c.Test14 [Thread-0] - 打断状态:true

可以修改获取打断状态方法,使用 Thread.interrupted(),获取打断状态后清除打断标记,当清除打断标记后,LockSupport.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());
            }
        });
        t1.start();
        sleep(1);
        t1.interrupt();
    }
    public static void main(String[] args) throws InterruptedException {
        test4();
    }

输出:

23:48:36.016 c.Test14 [Thread-0] - park...
23:48:37.022 c.Test14 [Thread-0] - 打断状态:true
23:48:37.026 c.Test14 [Thread-0] - park...

LockSupport 类在 同步 → park-un 详解

isInterrupted() 与 interrupted() 比较,如下:
首先,isInterrupted 是实例方法,interrupted 是静态方法,它们的用处都是查看当前打断的状态,但是 isInterrupted 方法查看线程的时候,不会将打断标记清空,也就是置为 false,interrupted 查看线程打断状态后,会将打断标志置为 false,也就是清空打断标记,简单来说,interrupt() 方法类似于 setter 设置中断值,isInterrupted() 类似于 getter 获取中断值,interrupted() 类似于 getter + setter 先获取中断值,然后清除标志。

/**
 * 测试 isInterrupted 与 interrupted
 */
@Slf4j(topic = "c.Code_14_Test")
public class Code_14_Test {

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

        Thread t1 = new Thread(() -> {
            log.info("park");
            LockSupport.park();
            log.info("unpark");
//            log.info("打断标记为:{}", Thread.currentThread().isInterrupted());
            log.info("打断标记为:{}", Thread.interrupted());

            // 使用 Thread.currentThread().isInterrupted() 查看打断标记为 true, LockSupport.park() 失效
            /**
             * 执行结果:
             * 11:54:17 [t1] c.Code_14_Test - park
             * 11:54:18 [t1] c.Code_14_Test - unpark
             * 11:54:18 [t1] c.Code_14_Test - 打断标记为:true
             * 11:54:18 [t1] c.Code_14_Test - unpark
             */
            // 使用 Thread.interrupted() 查看打断标记为 true, 然后清空打断标记为 false, LockSupport.park() 不失效
            /**
             * 执行结果:
             * 11:58:12 [t1] c.Code_14_Test - park
             * 11:58:13 [t1] c.Code_14_Test - unpark
             * 11:58:13 [t1] c.Code_14_Test - 打断标记为:true
             */
            LockSupport.park();
            log.info("unpark");

        }, "t1");

        t1.start();
        Thread.sleep(1000); // 主线程休眠 1 秒
        t1.interrupt();
    }
}

终止模式之两阶段终止模式

终止模式之两阶段终止模式:Two Phase Termination

目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器

错误思想:

  • 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
  • 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止

两阶段终止模式图示:

在这里插入图片描述
打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法:

/**
 * 使用 interrupt 进行两阶段终止模式
 */
@Slf4j(topic = "c.Code_13_Test")
public class Code_13_Test {

    public static void main(String[] args) throws InterruptedException {
        TwoParseTermination twoParseTermination = new TwoParseTermination();
        twoParseTermination.start();
        Thread.sleep(3500);
        twoParseTermination.stop();
    }

}

@Slf4j(topic = "c.TwoParseTermination")
class TwoParseTermination {

    private Thread monitor;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread thread = Thread.currentThread();
                if(thread.isInterrupted()) { // 调用 isInterrupted 不会清除标记
                    log.info("料理后事 ...");
                    break;
                } else {
                    try {
                        Thread.sleep(1000); // 睡眠
                        log.info("执行监控的功能 ..."); // 在此被打断不会异常
                    } catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑
                        log.info("设置打断标记 ...");
                        // 重新设置打断标记,打断 sleep 会清除打断状态
                        thread.interrupt();
                        e.printStackTrace();
                    }
                }
            }
        }, "monitor");
        monitor.start();
    }

    // 终止线程
    public void stop() {
        monitor.interrupt();
    }
}

输出:

23:27:16.170 c.TwoParseTermination [monitor] - 执行监控的功能 ...
23:27:17.177 c.TwoParseTermination [monitor] - 执行监控的功能 ...
23:27:18.193 c.TwoParseTermination [monitor] - 执行监控的功能 ...
23:27:18.665 c.TwoParseTermination [monitor] - 设置打断标记 ...
23:27:18.666 c.TwoParseTermination [monitor] - 料理后事 ...
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.test.TwoParseTermination.lambda$start$0(Test13.java:30)
	at java.lang.Thread.run(Thread.java:750)

过时方法

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

  • public final void stop():停止线程运行

    废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面

  • public final void suspend()挂起(暂停)线程运行

    废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁

  • public final void resume():恢复线程运行

主线程和守护线程

默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。普通线程t1可以调用 t1.setDeamon(true); 方法变成守护线程。

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

线程启动前调用此方法:

	public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    break;
                }
            }
            log.debug("t1 结束");
        }, "t1");
        t1.setDaemon(true);
        t1.start();

        Thread.sleep(1000);
        log.debug("main 结束");
    }

输出:
只有main结束,没有t1结束

00:00:18.316 c.Test15 [main] - main 结束

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

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

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

常见的守护线程:

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

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)将此线程标记为守护线程或用户线程
  • 18
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值