重新认识多线程编程04 线程使用相关

之前我们讨论了线程对象的一些属性和一些方法,和线程状态怎么理解,应该如何理解同步锁,接下来我们接着讨论,线程其他常用的一些操作,包括 打断线程,让出时间片,一些过期的方法,为什么不建议使用等等操作。

一、认识join方法

join() 方法是 Thread 类的一个方法,它允许一个线程等待另一个线程的结束。具体来说,如果在一个线程对象上调用了 join() 方法,那么当前线程将被阻塞,直到被调用 join() 方法的线程执行完成。


用法和语法

public final void join() throws InterruptedException

精确到毫秒

public final synchronized void join(long millis) throws InterruptedException

精确到纳秒
public final synchronized void join(long millis, int nanos) throws InterruptedException


join(): 调用此方法会使当前线程等待被调用对象(即调用 join() 的线程)执行完成。如果被调用对象已经执行完成,则立即返回。

join(long millis): 等待被调用对象执行完成的最长时间为 millis 毫秒。如果在指定时间内被调用对象执行完成,则立即返回;否则,超时后返回。

join(long millis, int nanos): 等待被调用对象执行完成的最长时间为 millis 毫秒加上 nanos 纳秒。这个方法提供了更高精度的等待时间控制。

工作原理

当一个线程 A 调用另一个线程 B 的 join() 方法时,线程 A 将被阻塞,直到线程 B 完成其执行。在这个过程中,线程 A 处于阻塞状态,不会消耗 CPU 资源,直到线程 B 执行完成或超时(如果使用了带有超时参数的 join() 方法)。

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread started: " + Thread.currentThread().getName());
        try {
            Thread.sleep(2000); // 模拟耗时操作
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted");
        }
        System.out.println("Thread ended: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();

        thread1.start();
        thread2.start();

        try {
            thread1.join(); // 等待thread1执行完毕
            System.out.println("Thread 1 joined");
            thread2.join(); // 等待thread2执行完毕
            System.out.println("Thread 2 joined");
        } catch (InterruptedException e) {
            System.out.println("Main thread interrupted");
        }

        System.out.println("Main thread finished");
    }
}

在上面的示例中,主线程启动了两个新线程 thread1thread2。然后,主线程调用了这两个线程的 join() 方法,分别等待它们执行完成。只有当 thread1thread2 执行完成后,主线程才会继续执行,并输出 "Main thread finished"

注意事项

  • 在使用 join() 方法时要注意可能的线程死锁情况。例如,如果线程 A 等待线程 B,同时线程 B 也在等待线程 A,就会导致死锁。

  • 调用 join() 方法会导致当前线程进入阻塞状态,因此应当谨慎使用,以免影响程序的响应性和性能。

join() 方法在多线程编程中非常有用,可以用于线程间的协调与同步,确保各个线程按照预期顺序执行或完成。

底层原理

join() 方法底层使用了 wait() 方法 和  notify 和 notifyAll 方法。这是因为 join() 方法要求当前线程等待被调用线程执行完成,直到被调用线程结束或超时。

wait() 方法的作用和用法
  • wait(): wait() 方法是 Object 类的一个方法,它使当前线程等待,直到另一个线程调用相同对象的 notify()notifyAll() 方法唤醒该线程,或者指定的时间到期。

  • notify()notifyAll(): 这两个方法用于唤醒处于等待状态的线程,notify() 唤醒单个等待线程,而 notifyAll() 唤醒所有等待线程。

 下面看join 源码

    // millis  超时时间

    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) {
            while (isAlive()) {
                wait(0);
            }
        } else {
        // 如果超时时间大于零  循环问 线程是否存活 如果存活则继续调用等待方法 同时修改超时时间 逐渐缩小 缩小到0之后 停止等待
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
解释下join()方法底层到底做了什么

首先当前线程调用用了 一个线程的join方法  ,首先会获得被等待线程的同步锁,上面的源码可以看到join方法 是被 synchronized 关键字修饰的,然后通过wait() 方法 使当前线程进入等待池 等待唤醒状态。

当被等待的线程执行完毕后,将会调用notify 方法唤醒等待池中的线程,让其重新竞争执行。

(这步唤醒操作  是 jvm内部代码执行的  也就是通过c++代码去唤醒的,我们是无感的)

 

二、认识yield()方法

yield() 方法是一个静态方法,它属于 Thread 类。它的作用是让出当前线程的CPU使用权,让系统调度器重新选择具有相同优先级的其他线程来执行。

yield() 方法的作用和用法

  1. 让出CPU资源: 当一个线程调用 yield() 方法时,它会让出其CPU执行的时间片,允许其他具有相同优先级的线程运行。这并不意味着该线程不再运行,只是它会暂时让出执行权,让其他线程有机会运行。

  2. 无法控制具体效果: 调用 yield() 方法并不会保证当前线程一定会让出CPU资源,因为这取决于系统调度器的实现。有些操作系统和虚拟机可能会忽略 yield() 请求。

  3. 用于调试和优化: yield() 方法通常用于调试和优化多线程程序,可以在某些情况下改善线程的执行效率和响应性,但是通常不建议在生产代码中频繁使用,因为它可能会带来不可预测的结果和性能损失。


public class YieldExample {
    public static void main(String[] args) {
        Thread producer = new ProducerThread();
        Thread consumer = new ConsumerThread();

        producer.start();
        consumer.start();
    }
}

class ProducerThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Producing item " + i);
            // 生产完毕后,让出CPU执行权
            Thread.yield();
        }
    }
}

class ConsumerThread extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Consuming item " + i);
            // 消费完毕后,让出CPU执行权
            Thread.yield();
        }
    }
}

在上述示例中,ProducerThreadConsumerThread 分别是生产者和消费者线程。它们在执行每个生产或消费操作后都调用 yield() 方法,以示意让出CPU执行权,让其他线程有机会运行。

这里可以结合线程的优先级  因为比较简单就描述一下

我们可以通过给线程设置不同的优先级  然后 通过yield()方法去让出执行权 来验证优先级使用效果

ps:  具体的线程调度 还是依赖操作系统的  也就说 优先级只是建议值 出现预期外的效果也是正常的

注意事项

  • yield() 方法的具体效果和行为依赖于操作系统和Java虚拟机的实现。
  • 不建议在需要确定的线程调度情况下依赖 yield(),因为它并不保证线程一定会让出CPU。
  • 在大多数情况下,应当依赖于更可控的线程同步和调度机制,如 wait()notify()notifyAll() 等来实现线程之间的协作和控制。

三、 如何优雅的停止线程的运行

认识打断方法

interrupt() 方法用于中断线程,即设置线程的中断状态为 true。它不会立即停止线程,而是给线程发送一个中断信号,线程可以在合适的时候处理这个信号。

如果线程在调用 wait()sleep() 或者 join() 等方法时被阻塞,那么调用 interrupt() 方法会使线程抛出 InterruptedException,并清除中断状态。

如果线程在运行中没有被阻塞,调用 interrupt() 方法仅仅设置线程的中断状态为 true。

isInterrupted()方法用于判断当前线程是否被中断。

如果调用该方法时线程的中断状态为 true,则返回 true,并清除中断状态。

如果调用该方法时线程的中断状态为 false,则返回 false。

 // 使用改变线程打断状态的方式 优雅打断线程执行 并且使用二次打断的方式 防止打断睡眠后 打断标记重置的问题  
 public static void main(String[] args) {

        Thread thread = new Thread(() -> {
            while (true) {
                // 检测线程是否被打断  同时重置打断标记
                if (Thread.interrupted()) {
                    System.out.println("线程被中断");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("线程正在运行");
                } catch (InterruptedException e) {
                    System.out.println("线程睡眠被中断");
                    // 二次打断  上面一次打断了睡眠 打断标记重置
                    //   此时线程将继续运行  为了可以正确打断线程执行 手动继续打断一次
                    Thread.currentThread().interrupt();
                }
            }
        });
        thread.start();

        try {
            TimeUnit.SECONDS.sleep(5);
            // 线程打断
            thread.interrupt();
            TimeUnit.SECONDS.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

---------------------------------------------------------------------------------------------------------------------------------

线程正在运行
线程正在运行
线程正在运行
线程正在运行
线程睡眠被中断
线程被中断

---------------------------------------------------------------------------------------------------------------------------------

上面介绍了 使用Thread 的打断标记 实现优雅打断线程执行的问题除了上面的这种方式

下面还有不推荐的方式 可以实现打断线程需求

使用Thread.stop()方法(已过时)

该方法的作用是直接停止线程运行,这种方式会造成非常严重的后果,即线程没有执行完成任务就被强行终止,在生产环境下,就是丢失了正在处理的数据,这是一个不能被接受的后果,因此已经被弃用了。

而第一种方式,将线程停止的时机交给了 线程的创建者来管理,显然线程的创建者可以决定它什么时候可以被安全打断,非常的合理。

使用自定义标记

下面展示了使用自定义标记的方式,可以理解成为,Thread线程内部的打断标记的手动替代版本,使用此种方式 需要结合 volatile关键字 强制将标记放入主存中,而不是工作内存中,这样不同的线程间 保证了可见性,因此 其他线程可以感知 主线程修改了标记,从而正确的停止工作。

public class TestThread03 {
    public static volatile boolean isStop = false;
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
                while (true){
                    if (isStop){
                        System.out.println("线程停止");
                        break;
                    }
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("线程执行中");
                }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        TestThread03.isStop = true;
        TimeUnit.SECONDS.sleep(100);
    }
}

好接下来我们简单补充一下 wait()方法  以及 notify 和 notifyAll

由于上次我们已经对锁对象 也就是监视器对象有了一个简单的理解,即它的结构 包括阻塞队列、等待池以及锁拥有者。

而调用了 wait() 方法的线程 将会进入等待池中阻塞,需要等待超时或者通知 才可以唤醒

而通知的方法就是  其他线程 调用了  notify 和 notifyAll 唤醒一个 或全部唤醒。

未完待续

接下来我们会进行 线程调度实战,手写几个线程调度的例子,来真实讲解这几个 可以让线程阻塞的方法,典型就是 生产者 与  消费者的模型。我们下期再见。吼吼吼。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值