2.2、多线程(中)
2.2.4、为什么启动线程不能直接调用run()方法?调用两次start()方法会有什么后果?
答:
- 在Java中,启动线程不能直接调用run()方法的原因是,run()方法是线程的执行体,通过调用start()方法来启动线程可以创建一个新的线程并使其运行。如果直接调用run()方法,则会在当前线程中按顺序执行run()方法的代码,而不会创建新的线程。
当调用start()方法时,会在新的线程中调用run()方法,这样可以并发地执行多个线程,提高程序的执行效率。另外,调用start()方法还会执行一些必要的准备工作,包括分配新的线程栈、初始化线程的状态等。 - 在Java中,如果尝试调用一个已经启动过的线程的start()方法,会抛出一个IllegalThreadStateException异常。这是因为线程的生命周期在调用start()方法后进入就绪状态,当线程被调度后才会进入运行状态。一旦线程进入运行状态,就不能再次调用start()方法。
如果尝试调用两次start()方法,第二次调用将会抛出IllegalThreadStateException异常,并且不会创建新的线程。这是出于线程安全和语义的考虑,避免在同一个线程中重复启动线程,导致不确定的行为和结果。因此,应该确保每个线程只调用一次start()方法,在需要重新启动线程时,应该创建一个新的线程对象来执行任务。
扩展:
Java中,线程共7种状态,但在Java API中,线程的状态只被定义成了6个枚举值,没有Running,因为Running是线程在操作系统中的状态。
- 新建(NEW):线程对象已经创建好了,但是还没有调用start()方法启动。
- 就绪(RUNNABLE):当线程对象调用start()方法后,线程进入就绪状态,表示已经准备好执行,只等待操作系统分配执行时间。
- 运行(RUNNING):当就绪状态的线程对象被操作系统调度后,就进入运行状态,开始执行run()方法中的任务。
- 阻塞(BLOCKED):当线程执行过程中遇到阻塞操作时(如IO操作、等待锁资源),线程进入阻塞状态,暂时释放CPU资源,等待阻塞条件满足后再进入就绪状态。
- 等待(WAITING):当线程调用wait()方法时,线程进入等待状态,直到其他线程调用notify()或notifyAll()方法唤醒该线程,才能进入就绪状态。
- 超时等待(TIMED WAITING):当线程调用带有超时时间的wait()、sleep()、join()方法时,线程进入超时等待状态,指定时间过后自动进入就绪状态。
- 终止(TERMINATED):当线程的run()方法执行完毕或因异常退出时,线程进入终止状态,不再执行任何任务。
2.2.5、谈谈你对Java线程5种状态流转原理的理解
答:
- 新建状态(New): 当一个线程对象被创建后,它处于新建状态。此时,线程已经分配内存,并被系统初始化。但还没有开始执行,也没有分配到CPU资源。
- 就绪状态(Runnable): 当线程对象调用start()方法之后,线程就进入就绪状态。此时,线程已经被系统分配到了CPU资源,但还没有开始执行。处于就绪状态的线程会进入线程调度器的等待队列中,等待系统调度以获得CPU资源。
- 运行状态(Running): 当线程被线程调度器选中,从就绪状态进入运行状态时,线程开始执行指定的代码逻辑。处于运行状态的线程会使用CPU资源,执行自己的逻辑。
- 阻塞状态(Blocked): 当线程在运行过程中,发生阻塞操作时,线程会进入阻塞状态。阻塞操作可能是因为等待某个资源的释放、等待输入输出等。处于阻塞状态的线程会释放CPU资源,并不会参与线程调度。
- 终止状态(Terminated): 当线程执行完成或发生异常时,线程会进入终止状态。线程执行完成后会释放所有的资源,并且不能再次进入其他状态。终止的线程对象可以被垃圾回收器回收。
2.2.6、谈谈你对线程池的理解
答:
线程池是Java中用于管理和复用线程的机制。它由线程池管理器、工作队列和线程组成。主要目的是避免频繁地创建和销毁线程,从而提高系统的性能和资源利用率。线程池会在程序启动时创建一定数量的线程,并将它们保存在线程池中,当有任务到来时,线程池会从线程池中获取一个空闲的线程来执行任务,当任务执行完成后,线程会返回线程池并等待下一个任务的到来。这样可以避免线程的频繁创建和销毁,减少了系统开销,提高了线程的复用性和并发效率。
2.2.7、Java有哪些实现线程池的方式?
答:
Java中有几种方式可以实现线程池:
ThreadPoolExecutor
类:这是Java提供的内置线程池实现。通过创建ThreadPoolExecutor
对象,可以自定义线程池的参数,如核心线程数、最大线程数、非核心线程存活时间等。这也是最常用的实现线程池的方式
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个线程池,其中包含2个核心线程,最多允许4个线程同时执行任务
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, 4, 10,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// 提交任务给线程池
executor.execute(new MyTask("Task 1"));
executor.execute(new MyTask("Task 2"));
executor.execute(new MyTask("Task 3"));
// 关闭线程池
executor.shutdown();
}
static class MyTask implements Runnable {
private String name;
public MyTask(String name) {
this.name = name;
}
public void run() {
System.out.println("Task " + name + " is running.");
}
}
}
在上面的示例中,我们创建了一个ThreadPoolExecutor对象,并通过构造函数设置了以下参数:
核心线程数为2
最大线程数为4
空闲线程的存活时间为10秒
使用有界的阻塞队列(ArrayBlockingQueue)来存储等待执行的任务,队列的容量为10
然后,我们使用execute()方法将3个MyTask任务提交给线程池执行。最后,我们调用shutdown()方法关闭线程池。
在MyTask类中,我们实现了Runnable接口,并重写了run()方法,该方法定义了任务的具体逻辑。在本例中,run()方法简单地打印出任务的名称。
这个示例展示了如何使用ThreadPoolExecutor类手动创建和配置一个线程池。通过设置不同的参数,可以根据需求来调整线程池的行为。
Executors
工厂类:Java提供了Executors
工厂类来创建不同类型的线程池,如FixedThreadPool
固定大小线程池、CachedThreadPool
缓冲线程池、ScheduledThreadPool
定时线程池等。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorsExample {
public static void main(String[] args) {
// 创建固定大小线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务给线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("Task-" + i);
executor.execute(worker);
}
// 关闭线程池
executor.shutdown();
while (!executor.isTerminated()) {
// 等待所有任务完成
}
System.out.println("Finished all tasks");
}
}
class WorkerThread implements Runnable {
private String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Task = " + taskName);
// 执行具体任务逻辑
System.out.println(Thread.currentThread().getName() + " End.");
}
}
ForkJoinPool
类:这是Java提供的一个特殊的线程池实现,用于支持并行计算。ForkJoinPool
基于工作窃取算法,可以将一个任务拆分成多个子任务并行执行。
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class ForkJoinPoolExample {
public static void main(String[] args) {
// 创建ForkJoinPool
ForkJoinPool pool = ForkJoinPool.commonPool();
// 提交任务给ForkJoinPool
pool.invoke(new MyTask(0, 10));
// 关闭ForkJoinPool
pool.shutdown();
}
}
class MyTask extends RecursiveAction {
private int start;
private int end;
public MyTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
if (end - start <= 2) {
// 执行具体任务逻辑
System.out.println(Thread.currentThread().getName() + " Start. Range = " + start + "-" + end);
} else {
int mid = (start + end) / 2;
MyTask leftTask = new MyTask(start, mid);
MyTask rightTask = new MyTask(mid + 1, end);
invokeAll(leftTask, rightTask);
}
}
}
开发时根据具体需求选择适合的方式来创建和管理线程池即可。
2.2.8、线程池是如何回收线程的?
答:
由于非核心线程是临时增加的,所以当任务处理完成后,工作线程处于空闲状态的时候,就需要回收非核心线程。
因为所有工作线程都从阻塞队列中获取要执行的任务,所以只要在一定时间内,阻塞队列中没有任何可以处理的任务,那这个线程就结束了。这个功能是通过阻塞队列里面的poll()方法来完成的。poll()方法提供了超时时间和超时时间单位两个参数,当超过指定时间没有获取到任务的时候,poll()方法返回null,从而终止当前线程,完成线程回收。
标签:状态,调用,编程,start,MyTask,线程,2.2,多线程,方法 From: https://blog.csdn.net/weixin_61769871/article/details/142031290扩展:默认情况下,线程池只会回收非核心线程,如果希望回收核心线程,可以设置allowCoreThreadTimeOut属性为true。不过一般情况我们不会回收核心线程,因为线程池本身就实现了线程的复用,而且核心线程在没有任务要处理的时候处于阻塞状态,并没有占用CPU资源。