Java并发实战(2)
并发实战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();
}
}
}
}
//代码有误请指正
等待线程执行完成
常见的面试题:“如何让线程顺序执行?”。最常用的方法便是使用join
。join
是一个在多线程编程中常用的方法,主要用于控制线程的执行顺序。示例代码如下
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("主线程确认子线程已执行完毕,继续执行");
}
}
代码解释:
-
首先创建一个匿名内部类实现
Runnable
接口,并定义了子线程要执行的任务。 -
使用这个
Runnable
对象创建了一个Thread
对象。 -
调用
start()
方法启动子线程。 -
在主线程中调用
join()
方法,这会让主线程阻塞,直到子线程执行完毕。 -
子线程执行完后,主线程继续执行。
扩展:
除此之外,还有什么让线程顺序执行的方法?
- 使用单线程线程池 (
ExecutorService
): 如果你使用Executors.newSingleThreadExecutor()
创建一个单线程的线程池,提交给这个线程池的任务会按照它们被提交的顺序执行,因为线程池中只有一个工作线程。- 使用
CountDownLatch
:CountDownLatch
是一个同步辅助工具类,它允许一个或多个线程等待其他线程完成操作。你可以设置一个计数器,当计数器递减到零时,所有等待的线程都会被释放。例如,T1
可以减少计数器,然后T2
等待计数器为零才开始执行。- 使用
CyclicBarrier
:CyclicBarrier
类似于CountDownLatch
,但它支持在所有参与线程到达屏障点后重新开始,而不是一次性使用。所有线程必须到达屏障点,然后它们可以一起继续执行。- 使用
Semaphore
或ReentrantLock
结合条件变量 (Condition
): 你可以使用锁和条件变量来控制线程的执行顺序。一个线程锁定后可以调用Condition.await()
来等待,直到另一个线程调用Condition.signal()
或Condition.signalAll()
。- 使用
Future
和Callable
: 如果线程执行的结果需要返回,可以使用Future
和Callable
。通过ExecutorService.submit(Callable<T> task)
提交任务并获取Future
,然后调用get()
方法来等待结果,这实际上也实现了线程的同步。- 使用
ForkJoinPool
和RecursiveAction
或RecursiveTask
:ForkJoinPool
是一种工作窃取(Work Stealing)算法的线程池,适合处理大量细粒度的可分解任务。通过ForkJoinPool
提交RecursiveAction
或RecursiveTask
,可以实现任务的有序执行。在后续将详细展开
线程睡眠
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();
}
}
这段代码执行的结果要么执行完A再执行B或者执行完B再执行A,线程之间不会交叉执行。说明在睡眠的时候,线程的资源并不会释放。
放弃CPU使用权
yield
是 Java 中的一个关键字,用于线程调度,它属于 Thread
类的一个静态方法。yield
方法的作用是请求当前运行的线程放弃当前处理器的使用权,以便让具有相同优先级的其他线程有机会运行。但是,这种放弃并不是强制性的,JVM 可能会忽略这个请求,继续执行当前线程。
yield
方法的主要特点和注意事项如下:
- 非阻塞:与
sleep
,wait
,join
等方法不同,yield
不会导致当前线程进入阻塞状态。它只是建议线程调度器重新考虑线程的执行顺序。 - 仅影响同优先级的线程:
yield
方法只会让出 CPU 给优先级相同的其他线程。如果当前线程是唯一一个具有该优先级的线程,或者没有其他线程准备运行,那么yield
方法可能不会产生任何效果。 - 调度不确定性:
yield
方法的效果取决于具体的线程调度策略和操作系统。即使调用了yield
,也不能保证其他线程会立即得到执行。 - 过度使用可能导致性能下降:频繁调用
yield
方法可能会导致线程切换过于频繁,增加上下文切换的开销,从而降低程序的整体性能。 - 一般用于测试和调试:在实际生产环境中,
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 中,interrupt
是 Thread
类的一个方法,用于中断一个线程。当一个线程被中断时,它会被标记为“中断状态”,并且如果该线程正在等待、睡眠或执行其他阻塞操作(如 wait()
, sleep()
, join()
或 I/O 操作),它会抛出一个 InterruptedException
。
以下是 interrupt
方法的一些关键点:
- 中断状态:每个线程都有一个内部标志,称为中断状态。调用
interrupt
方法会设置该线程的中断状态。你可以通过Thread.isInterrupted()
或Thread.interrupted()
方法检查这个状态。 - 异常抛出:如果线程在调用
interrupt
时正在执行一个会响应中断的操作(如sleep()
,wait()
,join()
或某些 I/O 操作),它会抛出一个InterruptedException
。线程应该捕获这个异常并采取适当的措施。 - 资源清理:在响应中断时,线程应该释放任何持有的资源,并进行必要的清理操作,以防止资源泄漏。
- 重置中断状态:
Thread.interrupted()
方法不仅检查中断状态,还会清除中断状态。因此,如果线程捕获了InterruptedException
并希望检查中断状态,应该先使用Thread.isInterrupted()
。 - 循环中断检查:在长时间运行的循环中,线程应该定期检查中断状态,以确保能够及时响应中断请求。
下面是一个简单的示例,展示了如何使用 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