线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以有多个线程,它们共享进程的资源,如内存空间、文件句柄等。线程相较于进程,具有更小的资源开销,创建和切换线程的速度也更快。
线程的故事
有一天,一个程序员在开发一个应用程序,这个应用程序需要处理大量的任务。程序员决定使用多线程来提高程序的执行效率。
于是,程序员创建了一个线程池,线程池中有很多线程。当有新任务到来时,线程池中的一个空闲线程会被分配任务去执行。在执行过程中,线程可能会遇到一些阻塞操作,如等待文件读写、等待网络请求等。此时,线程会进入阻塞状态,线程池会将这个线程置换出去,让其他线程继续执行任务。当阻塞操作完成后,线程会重新回到线程池中,等待分配新任务。
有一次,线程池中的线程都在忙碌,但应用程序仍然有大量任务需要处理。这时,线程池决定扩容,增加更多的线程。新来的线程迅速地接管了任务,提高了程序的处理能力。
另一次,线程池中的线程数量过多,导致系统资源紧张。这时,线程池决定缩容,减少一些线程。被减少的线程会完成当前任务后,自动退出。
通过这个故事,我们可以了解到线程的基本概念和作用。在实际开发中,我们需要根据应用程序的需求和系统资源情况,合理地使用多线程来提高程序的执行效率。
线程的状态
线程在执行过程中会经历不同的状态:
新建(New):
当用new
关键字创建一个线程对象时,线程处于新建状态。此时,线程对象已经创建,但线程还没有开始执行。
Thread thread = new Thread();
就绪(Runnable):
当线程对象调用了start()
方法后,线程进入可运行状态。此时,线程已经准备好执行,等待系统分配资源。
thread.start();
运行(Running):
当线程获得系统资源后,线程开始执行run()
方法中的代码,此时线程处于运行状态。
public void run() {
// 线程执行的代码
}
阻塞(Blocked):
线程在运行过程中,可能会因为某些原因暂时无法继续执行,如等待 I/O 操作完成、等待获取锁等。此时,线程进入阻塞状态。
代码语言:javascript复制synchronized (lock) {
// 等待获取锁
}
阻塞的三种分类:
等待阻塞(Waiting for I/O):
线程在等待 I/O 操作完成,如等待文件读写、网络请求等。此时,线程会被挂起,不占用 CPU 资源。当 I/O 操作完成后,线程会重新进入可运行状态。
代码语言:javascript复制FileInputStream fis = new FileInputStream("file.txt");
int data = fis.read(); // 等待文件读取完成
同步阻塞(Synchronization Blocked):
线程在等待获取锁。当一个线程试图访问被synchronized
关键字修饰的同步代码块时,它需要获取锁。如果锁已经被其他线程持有,当前线程会被阻塞,等待锁被释放。
synchronized (lock) {
// 等待获取锁
}
其他阻塞(Other Blocked):
线程在等待某些系统资源,如等待操作系统分配内存、等待线程调度等。此时,线程会被挂起,不占用 CPU 资源。当系统资源可用时,线程会重新进入可运行状态。
代码语言:javascript复制Thread.sleep(1000); // 等待1秒
了解这三种阻塞情况有助于我们更好地理解多线程编程中的问题和解决方案。在实际开发中,我们需要根据具体需求合理地控制线程状态,以实现高效的并发编程。同时,要注意避免死锁、资源竞争等问题。
等待(Waiting):
线程在运行过程中,主动调用了wait()
、join()
或park()
方法,暂时放弃 CPU 资源,进入等待状态。
lock.wait();
超时等待(Timed Waiting):
线程在等待状态的基础上,设置了等待超时时间。
代码语言:javascript复制lock.wait(timeout);
终止(Terminated):
线程执行完run()
方法中的代码,或者因为异常而终止,线程进入终止状态。
线程状态之间的转换关系如下:
- 新建 -> 可运行:调用
start()
方法 - 可运行 -> 运行:获得系统资源
- 运行 -> 阻塞:等待 I/O 操作完成、等待获取锁等
- 运行 -> 等待:调用
wait()
、join()
或park()
方法 - 运行 -> 超时等待:调用带有超时参数的
wait()
方法 - 阻塞、等待、超时等待 -> 运行:获得锁、I/O 操作完成、超时等待结束等
- 运行 -> 终止:执行完
run()
方法或发生异常
了解线程的状态有助于我们更好地理解多线程编程中的问题和解决方案。在实际开发中,我们需要根据具体需求合理地控制线程状态,以实现高效的并发编程。
线程调整优先级
在 Java 中,我们可以通过调整线程的优先级来影响线程调度。线程优先级是一个整数值,范围在 1(最低优先级)到 10(最高优先级)之间。默认情况下,新创建的线程优先级与其父线程相同。这些优先级常量分别由 Thread
类中的 MAX_PRIORITY
、NORM_PRIORITY
和 MIN_PRIORITY
定义。
Thread.MAX_PRIORITY
(10):表示线程的最高优先级。当一个线程的优先级设置为最高优先级时,它具有更高的概率被调度执行。然而,这并不意味着最高优先级的线程总是优先执行。线程调度仍然取决于操作系统和 JVM 的实现。
Thread.NORM_PRIORITY
(5):表示线程的默认优先级。当创建一个新线程时,如果没有显式设置优先级,那么它将继承父线程的优先级。默认优先级适用于大多数线程,它不会导致线程饥饿,也不会导致过分的线程切换开销。
Thread.MIN_PRIORITY
(1):表示线程的最低优先级。当一个线程的优先级设置为最低优先级时,它具有较低的概率被调度执行。这可以用于确保低优先级线程不会影响到高优先级线程的执行。
Thread thread = new Thread(() -> {
// 线程执行的代码
});
// 设置线程优先级
thread.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
thread.setPriority(Thread.NORM_PRIORITY); // 设置为默认优先级
thread.setPriority(Thread.MIN_PRIORITY); // 设置为最低优先级
需要注意的是,线程优先级并不能保证线程按照预期的顺序执行。线程调度仍然取决于操作系统和 JVM 的实现。此外,过分依赖线程优先级可能导致程序难以维护和理解。在实际开发中,我们应该根据应用程序的需求和系统资源情况,合理地设置线程优先级,以实现高效的并发编程。同时,要注意避免死锁、资源竞争等问题。
线程调度策略
线程调度是操作系统用来决定哪个线程应该获得处理器资源的过程。线程调度策略会影响程序的执行效率和响应时间。
常见的线程调度策略:
- 协同式多线程(Cooperative Multithreading):协同式多线程是一种非抢占式的线程调度策略。在这种策略中,线程需要主动地让出处理器资源,以便其他线程可以执行。这种调度策略的优点是实现简单,但缺点是可能导致线程饥饿(一个线程长时间得不到执行)。
- 抢占式多线程(Preemptive Multithreading):抢占式多线程是一种抢占式的线程调度策略。在这种策略中,操作系统可以在任何时候暂停一个正在执行的线程,将处理器资源分配给其他线程。这种调度策略可以避免线程饥饿,但实现相对复杂。
- 优先级调度(Priority Scheduling):优先级调度是一种基于线程优先级的调度策略。线程可以被分配一个优先级,优先级较高的线程更有可能获得处理器资源。优先级调度可以确保重要的线程优先执行,但可能导致低优先级线程饥饿。
- 时间片轮转调度(Round-Robin Scheduling):时间片轮转调度是一种将处理器资源分配给线程的公平策略。每个线程都有一个时间片,当时间片用完时,线程会被挂起,让其他线程执行。这种调度策略可以确保每个线程都有机会执行,但可能导致线程切换频繁,增加上下文切换开销。
- 最高响应比优先调度(Highest Response Ratio Next, HRRN):最高响应比优先调度是一种既考虑线程等待时间又考虑线程优先级的调度策略。线程的响应比定义为(等待时间 服务时间)/ 服务时间,响应比较高的线程更有可能获得处理器资源。这种调度策略可以在保证公平性的同时,尽量减少线程的等待时间。