likes
comments
collection
share

Java并发实战(2)

作者站长头像
站长
· 阅读数 51

并发实战1

线程通知与等待

在编程中,线程通知与等待是一种常见的用于线程间通信的机制。这种机制允许一个线程(通知者)向另一个线程(等待者)发送信号,告知某个事件已经发生,从而让等待者线程能够继续执行。


在Java中,线程的等待和通知机制主要通过Object类的wait()notify()notifyAll()方法来实现。这些方法需要与同步块(synchronized block)一起使用,因为它们依赖于对象的监视器锁(monitor lock)。

  • 等待(wait):当线程调用一个对象的wait()方法时,它会释放该对象的监视器锁,并进入该对象的等待集(wait set),直到被另一个线程通过调用该对象的notify()notifyAll()方法唤醒。
  • 通知(notify):当线程调用一个对象的notify()方法时,它会随机唤醒该对象等待集中的一个等待线程。如果调用notifyAll(),则唤醒所有等待的线程。

线程通知与等待的经典应用——生产者与消费者

public class ProducerAndConsumer {
    //生产者消费者模式
    public static void main(String[] args) {
        //创建一个共享的资源
        Resource r = new Resource();
        //创建生产者和消费者线程
        Thread t0 = new Thread(new Producer(r));
        Thread t1 = new Thread(new Consumer(r));
        //启动线程
        t0.start();
        t1.start();
    }

    //共享资源
    static class Resource {
        private int count = 0;


        //生产者生产资源
        public synchronized void produce() {
            while (count!=0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            count++;
            System.out.println(Thread.currentThread().getName() + "生产者生产资源" + count);
            this.notify();
        }

        //消费者消费资源
        public synchronized void consume() {
            while (count==0) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            count--;
            System.out.println(Thread.currentThread().getName() + "消费者消费资源" + count);
            this.notify();
        }
    }

    //生产者
    static class Producer implements Runnable {
        private Resource r;

        public Producer(Resource r) {
            this.r = r;
        }

        @Override
        public void run() {
            while (true) {
                r.produce();
            }
        }
    }

    //消费者
    static class Consumer implements Runnable {
        private Resource r;

        public Consumer(Resource r) {
            this.r = r;
        }

        @Override
        public void run() {
            while (true) {
                r.consume();
            }
        }
    }
}
//代码有误请指正

等待线程执行完成

常见的面试题:“如何让线程顺序执行?”。最常用的方法便是使用joinjoin 是一个在多线程编程中常用的方法,主要用于控制线程的执行顺序。示例代码如下

public class ThreadJoin {

    public static void main(String[] args) {
        System.out.println("主线程开始执行");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程开始执行");
                try {
                    Thread.sleep(2000); // 让子线程休眠2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程执行完毕");
            }
        });

        thread.start(); // 启动子线程

        try {
            thread.join(); // 主线程等待子线程结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主线程确认子线程已执行完毕,继续执行");
    }
}

Java并发实战(2)

代码解释:

  1. 首先创建一个匿名内部类实现 Runnable 接口,并定义了子线程要执行的任务。

  2. 使用这个 Runnable 对象创建了一个 Thread 对象。

  3. 调用 start() 方法启动子线程。

  4. 在主线程中调用 join() 方法,这会让主线程阻塞,直到子线程执行完毕。

  5. 子线程执行完后,主线程继续执行。

扩展:

除此之外,还有什么让线程顺序执行的方法?

  1. 使用单线程线程池 (ExecutorService): 如果你使用 Executors.newSingleThreadExecutor() 创建一个单线程的线程池,提交给这个线程池的任务会按照它们被提交的顺序执行,因为线程池中只有一个工作线程。
  2. 使用 CountDownLatchCountDownLatch 是一个同步辅助工具类,它允许一个或多个线程等待其他线程完成操作。你可以设置一个计数器,当计数器递减到零时,所有等待的线程都会被释放。例如,T1 可以减少计数器,然后 T2 等待计数器为零才开始执行。
  3. 使用 CyclicBarrierCyclicBarrier 类似于 CountDownLatch,但它支持在所有参与线程到达屏障点后重新开始,而不是一次性使用。所有线程必须到达屏障点,然后它们可以一起继续执行。
  4. 使用 SemaphoreReentrantLock 结合条件变量 (Condition): 你可以使用锁和条件变量来控制线程的执行顺序。一个线程锁定后可以调用 Condition.await() 来等待,直到另一个线程调用 Condition.signal()Condition.signalAll()
  5. 使用 FutureCallable: 如果线程执行的结果需要返回,可以使用 FutureCallable。通过 ExecutorService.submit(Callable<T> task) 提交任务并获取 Future,然后调用 get() 方法来等待结果,这实际上也实现了线程的同步。
  6. 使用 ForkJoinPoolRecursiveActionRecursiveTaskForkJoinPool 是一种工作窃取(Work Stealing)算法的线程池,适合处理大量细粒度的可分解任务。通过 ForkJoinPool 提交 RecursiveActionRecursiveTask,可以实现任务的有序执行。

在后续将详细展开

线程睡眠

sleep 方法是 Java 的 Thread 类中的一个静态方法,用于使当前正在执行的线程暂停执行指定的时间。这个方法通常用于控制线程的执行速度,避免 CPU 过度消耗,或者用于定时任务和延迟执行等场景。

使用 sleep 方法时需要注意以下几点:

  • 中断响应:如果在 sleep 方法执行期间,当前线程被中断,sleep 方法会抛出一个 InterruptedException 异常。因此,在调用 sleep 方法时,通常需要捕获这个异常,并检查线程的中断状态。
  • 线程调度sleep 方法并不会改变当前线程的状态为等待状态(如在锁等待队列中),而是直接将线程置于阻塞状态,线程调度器会在此期间选择其他线程执行。
  • 不精确性:实际的暂停时间可能比指定的时间长,这是因为线程调度的不确定性和系统负载的影响。此外,如果 sleep 方法被中断,那么剩余的睡眠时间将被忽略。
  • 不要在 synchronized 块中使用:避免在持有锁的 synchronized 块或方法中使用 sleep 方法,否则可能会导致死锁,因为线程在睡眠期间仍然持有锁,阻止其他线程访问共享资源。
public class ThreadSleep {
    //举一个例子,线程在睡眠时拥有的监视器资源不会被释放
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        //创建线程A
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                //获取独占锁
                lock.lock();
                try {
                    System.out.println("child threadA is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadA is in awaked");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放锁
                    lock.unlock();
                }
            }
        });
        //创建线程B
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                //获取独占锁
                lock.lock();
                try {
                    System.out.println("child threadB is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadB is in awaked");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放锁
                    lock.unlock();
                }
            }
        });
        //启动线程
        threadA.start();
        threadB.start();

    }

}

Java并发实战(2)

这段代码执行的结果要么执行完A再执行B或者执行完B再执行A,线程之间不会交叉执行。说明在睡眠的时候,线程的资源并不会释放。

放弃CPU使用权

yield 是 Java 中的一个关键字,用于线程调度,它属于 Thread 类的一个静态方法。yield 方法的作用是请求当前运行的线程放弃当前处理器的使用权,以便让具有相同优先级的其他线程有机会运行。但是,这种放弃并不是强制性的,JVM 可能会忽略这个请求,继续执行当前线程。

yield 方法的主要特点和注意事项如下:

  1. 非阻塞:与 sleep, wait, join 等方法不同,yield 不会导致当前线程进入阻塞状态。它只是建议线程调度器重新考虑线程的执行顺序。
  2. 仅影响同优先级的线程yield 方法只会让出 CPU 给优先级相同的其他线程。如果当前线程是唯一一个具有该优先级的线程,或者没有其他线程准备运行,那么 yield 方法可能不会产生任何效果。
  3. 调度不确定性yield 方法的效果取决于具体的线程调度策略和操作系统。即使调用了 yield,也不能保证其他线程会立即得到执行。
  4. 过度使用可能导致性能下降:频繁调用 yield 方法可能会导致线程切换过于频繁,增加上下文切换的开销,从而降低程序的整体性能。
  5. 一般用于测试和调试:在实际生产环境中,yield 方法的使用并不常见,因为它对线程调度的控制能力有限。它更多地用于测试和调试,帮助理解线程调度的行为。

下面是一个简单的示例,展示 yield 方法的使用:

public class ThreadYield {
    public static void main(String[] args) {
        // 创建一个新的线程t1
        Thread t1 = new Thread(() -> {
            // 循环10次
            for (int i = 0; i < 10; i++) {
                // 打印线程名和循环次数
                System.out.println("线程 1: " + i);
                // 如果循环次数是偶数,那么让出CPU执行权
                if (i % 2 == 0) {
                    Thread.yield(); // 请求线程调度器重新考虑执行顺序
                }
            }
        }, "线程1");

        // 创建一个新的线程t2
        Thread t2 = new Thread(() -> {
            // 循环10次
            for (int i = 0; i < 10; i++) {
                // 打印线程名和循环次数
                System.out.println("线程 2: " + i);
            }
        }, "线程2");

        // 启动线程t1和t2
        t1.start();
        t2.start();
    }
}

可以看到,线程1的两行不会执行在一起,除非线程2全部执行完。

线程中断

在 Java 中,interruptThread 类的一个方法,用于中断一个线程。当一个线程被中断时,它会被标记为“中断状态”,并且如果该线程正在等待、睡眠或执行其他阻塞操作(如 wait(), sleep(), join() 或 I/O 操作),它会抛出一个 InterruptedException

以下是 interrupt 方法的一些关键点:

  1. 中断状态:每个线程都有一个内部标志,称为中断状态。调用 interrupt 方法会设置该线程的中断状态。你可以通过 Thread.isInterrupted()Thread.interrupted() 方法检查这个状态。
  2. 异常抛出:如果线程在调用 interrupt 时正在执行一个会响应中断的操作(如 sleep(), wait(), join() 或某些 I/O 操作),它会抛出一个 InterruptedException。线程应该捕获这个异常并采取适当的措施。
  3. 资源清理:在响应中断时,线程应该释放任何持有的资源,并进行必要的清理操作,以防止资源泄漏。
  4. 重置中断状态Thread.interrupted() 方法不仅检查中断状态,还会清除中断状态。因此,如果线程捕获了 InterruptedException 并希望检查中断状态,应该先使用 Thread.isInterrupted()
  5. 循环中断检查:在长时间运行的循环中,线程应该定期检查中断状态,以确保能够及时响应中断请求。

下面是一个简单的示例,展示了如何使用 interrupt 方法:

public class ThreadInterrupt {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("线程被中断");
                    return;
                }
                try {
                    Thread.sleep(1000);
                    System.out.println("线程仍在运行...");
                } catch (InterruptedException e) {
                    System.out.println("捕获到 InterruptedException");
                    Thread.currentThread().interrupt(); // 重新设置中断状态
                    return;
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt(); // 中断线程
    }
}

在这个例子中,主线程启动了一个新线程,该线程无限循环并每隔一秒打印一条消息。主线程在等待三秒后调用 t.interrupt() 来中断新线程。新线程在检测到中断状态后退出循环。注意,如果线程在 Thread.sleep() 中被中断,它会抛出 InterruptedException,这时线程应该捕获异常并根据需要处理。

转载自:https://juejin.cn/post/7381396879057092649
评论
请登录