Merge pull request #22 from HuaHero/HuaHero-patch-20

Update Java 并发.md
This commit is contained in:
HuaHero 2023-11-09 15:54:05 +08:00 committed by GitHub
commit c14d40f13c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -11,6 +11,7 @@
* [Daemon](#daemon)
* [sleep()](#sleep)
* [yield()](#yield)
* [线程通信方式](#线程通信方式)
* [三、中断](#三中断)
* [InterruptedException](#interruptedexception)
* [interrupted()](#interrupted)
@ -209,24 +210,31 @@ public void run() {
Thread.yield();
}
```
### 线程通信方式来自https://www.nowcoder.com/exam/interview/75826387/test?paperId=50270024
在Java中,常用的线程通信方式有两种,分别是利用**Monitor实现**线程通信、利用**Condition实现**线程通信。
线程同步是线程通信的前提,所以究竟采用哪种方式实现通信,取决于线程同步的方式。 如果是采用**synchronized**关键字进行同步,则需要依赖**Monitor**(同步监视器)实现线程通信,Monitor就是锁对象。在synchronized同步模式下,锁对象可以是任意的类型,所以通信方法自然就被定义在Object类中了,这些方法包括wt()、notify()、notifyAll()。一个线程通过Monitor调用wt()时,它就会释放锁并在此等待。当其他线程通过Monitor调用notify()时,则会唤醒在此等待的一个线程。当其他线程通过Monitor调用notifyAll()时,则会唤醒在此等待的所有线程。
JDK1.5新增了Lock接口及其实现类,提供了更为灵活的同步方式。如果是采用**Lock对象**进行同步,则需要依赖**Condition**实现线程通信,Condition对象是由Lock对象创建出来的,它依赖于Lock对象。Condition对象中定义的通信方法,与Object类中的通信方法类似,它包括awt()、signal()、signalAll()。通过名字就能看出它们的含义了,当通过Condition调用awt()时当前线程释放锁并等待,当通过Condition调用signal()时唤醒一个等待的线程,当通过Condition调用signalAll()时则唤醒所有等待的线程。
加分回答 线程同步是基于同步队列实现的,而线程通信是基于等待队列实现的。当调用等待方法时,即将当前线程加入等待队列。当调用通知方法时,即将等待队列中的一个或多个线程转移回同步队列。因为synchronized只有一个Monitor,所以它就只有一个等待队列。而Lock对象可以创建出多个Condition,所以它拥有多个等待队列。多个等待队列带来了极大的灵活性,所以基于Condition的通信方式更为推荐。 比如,在实现生产消费模型时,生产者要通知消费者、消费者要通知生产者。相反,不应该出现生产者通知生产者、消费者通知消费者这样的情况。如果使用synchronized实现这个模型,由于它只有一个等待队列,所以只能把生产者和消费者加入同一个队列,这就会导致生产者通知生产者、消费者通知消费者的情况出现。采用Lock实现这个模型时,由于它有多个等待队列,可以有效地将这两个角色区分开,就能避免出现这样的问题。
**关于线程池**
【线程池的意义】
### **关于线程池**
--【线程池的意义】
线程池可以有效地管理线程:它可以**管理线程的数量**,可以避免无节制的创建线程,导致超出系统负荷直至崩溃。它还可以**让线程复用**,可以大大地减少创建和销毁线程所带来的开销。
【**线程池的核心参数**】
--【**线程池的核心参数**】
线程池需要依赖一些参数来控制任务的执行流程,其中最重要的参数有corePoolSize核心线程数、workQueue等待队列、maxinumPoolSize最大线程数、handler拒绝策略、keepAliveTime空闲线程存活时间
【线程池的运作步骤】
--【线程池的运作步骤】
当我们向线程池提交一个任务之后,线程池按照如下步骤处理这个任务:
1. 判断线程数是否达到corePoolSize,若没有则新建线程执行该任务,否则进入下一步。
2. 判断等待队列是否已满,若没有则将任务放入等待队列,否则进入下一步。
3. 判断线程数是否达到maxinumPoolSize,如果没有则新建线程执行任务,否则进入下一步。
4. 采用初始化线程池时指定的拒绝策略,拒绝执行该任务。
5. 新建的线程处理完当前任务后,不会立刻关闭,而是继续处理等待队列中的任务。如果线程的空闲时间达到了keepAliveTime,则线程池会销毁一部分线程,将线程数量收缩至corePoolSize。 第2步中的队列可以有界也可以无界。若指定了无界的队列,则线程池永远无法进入第3步,相当于废弃了maxinumPoolSize参数。这种用法是十分危险的,如果任务在队列中产生大量的堆积,就很容易造成内存溢出。
【线程池创建工具--建议用**ThreadPoolExecutor**】
--【线程池创建工具--建议用**ThreadPoolExecutor**】
JDK为我们提供了一个名为Executors的线程池的创建工具,该工具创建出来的就是带有无界队列的线程池,所以一般在工作中我们是不建议使用这个类来创建线程池的。 第4步中的拒绝策略主要有4个让调用者自己执行任务、直接抛出异常、丢弃任务不做任何处理、删除队列中最老的任务并把当前任务加入队列。这4个拒绝策略分别对应着RejectedExecutionHandler接口的4个实现类,我们也可以基于这个接口实现自己的拒绝策略。 在Java中,线程池的实际类型为ThreadPoolExecutor,它提供了线程池的常规用法。该类还有一个子类,名为ScheduledThreadPoolExecutor,它对定时任务提供了支持。在子类中,我们可以周期性地重复执行某个任务,也可以延迟若干时间再执行某个任务。
在Java中正常创建线程池应该使用ThreadPoolExecutor类。虽然Executors类可以方便地创建线程池但它使用的默认参数可能不适合所有情况。例如Executors.newFixedThreadPool方法创建的线程池默认使用无界队列可能会导致内存溢出。此外Executors类隐藏了线程池的细节使得无法对其进行更灵活的配置和优化。因此建议使用ThreadPoolExecutor类手动创建线程池并根据具体情况进行参数配置。
【线程池的生命周期状态】 线程池的生命周期包含5个状态RUNNING、SHUTDOWN、STOP、TIDING、TERMINATED。这5种状态的状态值分别是-1、0、1、2、3。在线程池的生命周期中,它的状态只能由小到大迁移,是不可逆的。 1. RUNNING表示线程池正在运行。 2. SHUTDOWN执行shutdown()时进入该状态,此时队列不会清空,线程池会等待任务执行完毕。 3. STOP执行shutdownNow()时进入该状态,此时现线程池会清空队列,不再等待任务的执行。 4. TIDING当线程池及队列为空时进入该状态,此时线程池会执行钩子函数,目前该函数是一个空的实现。 5. TERMINATED钩子函数执行完毕后,线程进入该状态,表示线程池已经死亡。
--【线程池的生命周期状态】 线程池的生命周期包含5个状态RUNNING、SHUTDOWN、STOP、TIDING、TERMINATED。这5种状态的状态值分别是-1、0、1、2、3。在线程池的生命周期中,它的状态只能由小到大迁移,是不可逆的。 1. RUNNING表示线程池正在运行。 2. SHUTDOWN执行shutdown()时进入该状态,此时队列不会清空,线程池会等待任务执行完毕。 3. STOP执行shutdownNow()时进入该状态,此时现线程池会清空队列,不再等待任务的执行。 4. TIDING当线程池及队列为空时进入该状态,此时线程池会执行钩子函数,目前该函数是一个空的实现。 5. TERMINATED钩子函数执行完毕后,线程进入该状态,表示线程池已经死亡。
来自https://www.nowcoder.com/exam/interview/75826387/test?paperId=50270024
## 三、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。