1.线程定义,线程与进程的区别
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
进程是资源分配的基本单位,它拥有自己独立的地址空间和各种资源;而线程是处理机调度的基本单位,它只能和其他线程共享进程的资源,而本身并不拥有资源。
线程的上下文切换速度比进程快。
2.创建线程
(1)继承Thread类
通过继承Thread类并重写其run()
方法,可以创建新的线程,之后创建该类的实例并调用其start()
方法来启动线程。
class MyThread extends Thread {
public void run() {
System.out.println("线程运行中");
}
}
public class Test {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
(2) 实现Runnable接口
实现Runnable接口并重写其run()方法,然后将Runnable实例传递给Thread类的构造器来创建线程。这种方法比继承Thread类更加灵活,因为Java不支持多重继承,但可以实现多个接口。
class MyRunnable implements Runnable {
public void run() {
System.out.println("线程运行中");
}
}
public class Test {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
(3)实现Callable与Future、FutureTask 间接创建线程
虽然这不是直接创建线程的方法,但它与线程执行密切相关,特别是在需要获取线程执行结果时。Callable接口类似于Runnable,但它可以返回一个结果,并且可以抛出异常。Future用于表示异步计算的结果,而FutureTask是Future的一个实现,它同时实现了Runnable接口,因此可以包装一个Callable或Runnable对象。
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
return 123;
}
}
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(new MyCallable());
System.out.println(future.get()); // 输出 123
executor.shutdown();
}
}
(4)使用Executor框架(线程池)
这不是直接创建单个线程的方法,是Java中管理线程的高级方法,包括Executors工厂类提供的各种线程池实现。这些线程池可以管理一组线程,用于异步执行提交给它们的任务。
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker);
}
executor.shutdown();
3.线程的七种状态
线程的七种状态详细描述了线程在其生命周期中的不同阶段
(1) 新建状态(NEW)
当线程对象被创建时,它处于新建状态,此时,线程还没有开始执行,即没有调用线程的start()
方法。在这个状态下,线程只是一个普通的Java对象,占用内存空间但还没有被分配CPU时间。
(2)就绪状态(READY/RUNNABLE)
或者称为可运行状态。当线程被启动(即调用了start()
方法)后,会进入就绪状态。在这个状态下,线程已经准备好了所有需要的资源,等待操作系统的调度,以获取CPU时间并执行。线程在等待CPU时间的过程中,可能会与其他线程共享处理器的执行时间,具体取决于操作系统的调度算法。
(3)运行状态(RUNNING)
当线程获得CPU时间后,会进入运行状态,执行其run()
方法中的代码。在这个状态下,线程正在占用CPU并执行任务。需要注意的是,由于操作系统的调度,线程的运行状态可能是间歇性的,即线程可能在多次CPU时间片内完成其执行。
(4)阻塞状态(BLOCKED)
当线程需要等待某些条件满足时(如等待获取某个锁),它会进入阻塞状态。在这个状态下,线程会暂停执行,并释放CPU资源,直到等待的条件满足后(如锁被释放),线程才会重新进入就绪状态等待执行。
(5)等待状态(WAITING)
线程处于等待状态时,它正在等待其他线程执行某些操作来唤醒它。这通常涉及到线程间的协作,如通过调用Object类的wait()
方法或Thread类的join()
方法进入等待状态。线程在等待状态下不会占用CPU资源,直到它被其他线程显式唤醒(如通过调用notify()
或notifyAll()
方法)。
(6)超时等待状态(TIMED_WAITING)
类似于等待状态,但超时等待状态有一个指定的等待时间。当线程等待某个条件或某个时间到来时,如果时间到了而条件仍未满足,线程将自动醒来并重新进入就绪状态。这通常通过调用Thread类的sleep(long millis)
方法或Object类的wait(long timeout)
方法实现。
(7)终止状态(TERMINATED)
当线程完成了其run()
方法的执行,或者因为异常而终止时,它会进入终止状态。在这个状态下,线程不再执行任何代码,并释放所有已持有的资源,终止状态的线程不能被重新启动。
4.线程之间的通信方式
(1)Volatile共享内存
由于线程共享同一进程的地址空间,直接访问共享数据是线程间通信的主要方式。需要使用同步原语(如互斥锁)来保护共享数据,避免竞争条件。
(2)线程间消息队列
类似于进程间的消息队列,线程间的消息队列可以用于在线程之间传递消息和数据,通常用于生产者-消费者模式中。
(3)条件变量
条件变量与互斥锁结合使用,允许线程在特定条件满足时等待或唤醒其他线程。它们非常适合用于实现复杂的线程同步机制。
(4)信号量
信号量是一种计数器,用于控制对共享资源的访问,可以限制同时访问资源的线程数量,非常适合用于实现资源池和限流机制。
5.线程五种调用方法
在Java多线程编程中,休眠、中断、等待、唤醒和让步等方法是线程控制的重要机制。
(1)休眠(Sleep)
休眠是指让当前线程暂停执行一段时间,时间结束后线程自动恢复执行。休眠期间线程不会释放锁(如果有的话),休眠可以被中断异常(InterruptedException)打断。
public static void sleep(long millis) throws InterruptedException:使当前正在执行的线程以指定的毫秒数暂停执行(毫秒数参数)。
public static void sleep(long millis, int nanos) throws InterruptedException:使当前正在执行的线程暂停执行指定的毫秒数和纳秒数。
(2)中断(Interrupt)
中断是线程间的一种协作机制,用于通知一个线程应该停止它正在执行的任务。中断是一种协作机制,线程需要定期检查中断状态并据此作出响应,中断状态可以被清除。
public void interrupt():中断线程。
public boolean isInterrupted():检查线程是否已经被中断。
public static boolean interrupted():检查当前线程是否已经被中断,并清除中断状态。
(3)等待(Wait)
等待是指让当前线程进入等待(阻塞)状态,直到其他线程调用该对象的notify()
或notifyAll()
方法,或者等待时间结束。等待必须在同步代码块或同步方法中调用,且调用线程必须持有该对象的锁,等待期间线程会释放锁。
public final void wait():让当前线程无限期等待,直到被其他线程唤醒。
public final void wait(long timeout):让当前线程等待指定的毫秒数,或者直到被其他线程唤醒。
public final void wait(long timeout, int nanos):让当前线程等待指定的毫秒数和纳秒数,或者直到被其他线程唤醒。
(4)唤醒(Notify/NotifyAll)
唤醒是指唤醒正在等待(阻塞)的线程。唤醒操作也必须在同步代码块或同步方法中调用,且调用线程必须持有该对象的锁,唤醒操作不会立即执行等待线程,而是将其状态从等待变为就绪,等待CPU调度执行。
public final void notify():唤醒在此对象监视器上等待的单个线程。
public final void notifyAll():唤醒在此对象监视器上等待的所有线程。
(5)让步(Yield)
让步是指让当前线程暂停执行,将执行机会让给同优先级的线程或更高优先级的线程。
public static void yield():暂停当前正在执行的线程对象,并执行其他线程。
让步是线程调度的一个提示,但具体是否执行取决于JVM的实现,让步不会使线程进入阻塞状态,而是让出CPU时间片。
6.使用线程池
6.1.最常用的创建线程池的方式是使用ThreadPoolExecutor,是Java中最原始的创建线程池的方式,它包含了7个参数可供设置。
1.核心线程数:corePoolSize,线程池中的常驻线程数
2.最大线程数:maximumPoolSize,线程池中允许的最大线程数
3.非核心线程的存活时间:keepAliveTime
4.存活时间的时间单位:unit
5.任务队列:workQueue,用于存放待执行的任务
6.线程工厂:threadFactory,用于创建新线程
7.拒绝策略:handler,当任务队列已满且线程池中的线程数达到最大线程数时,对于新提交的任务将采取的拒绝策略。
ExecutorService executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
10, // keepAliveTime
TimeUnit.SECONDS, // unit
new LinkedBlockingQueue<>(5), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.CallerRunsPolicy() // handler
);
6.2.线程池处理流程:
线程池执行任务->检查核心线程池是否已满->检查任务队列是否已满->检查最大线程数是否达到->根据拒绝策略处理任务
6.3.四种常见拒绝策略
(1)中止策略(AbortPolicy)
默认策略,当任务添加到线程池中被拒绝时,它将抛出RejectedExecutionException异常。这种策略适用于那些不能接受新任务提交的场景,或者希望在任务被拒绝时及时获得反馈的应用。通过抛出异常,可以促使调用者关注到问题的发生,并进行相应的处理。
(2)调用者运行(CallerRunsPolicy)
任务添加到线程池中被拒绝时,会在线程池当前正在运行的线程中(即调用execute方法的线程)直接运行被拒绝的任务。如果线程池已经关闭,则任务将被丢弃。通过将任务回退给调用者线程执行,可以避免因为线程池拒绝任务而导致的业务中断。
(3)丢弃策略(DiscardPolicy)
当任务添加到线程池中被拒绝时,线程池将静默地丢弃被拒绝的任务,既不抛出异常也不执行该任务。
(4)弃老策略(DiscardOldestPolicy)
当任务添加到线程池中被拒绝时,线程池会尝试丢弃等待队列中最旧的未处理任务(即等待时间最长的任务),然后尝试将新任务添加到队列中,如果线程池已经关闭,则任务将被丢弃。注意:这种策略可能会导致一些已经等待了很久的任务被丢弃,从而影响到业务逻辑的准确性。
6.4.四种常见线程池创建方式
(1)FixedThreadPool(定长线程池)
线程池中的线程数量固定,即使有空闲线程,也不会被回收,如果所有线程都在忙,新任务会等待空闲线程。
ExecutorService executor = Executors.newFixedThreadPool(int nThreads);
(2)CachedThreadPool(可缓存线程池)
线程池中的线程数量不固定,可以根据需要自动调整。当有新任务提交时,如果线程池中有空闲线程,则使用空闲线程执行;如果没有空闲线程,则创建新线程执行。当线程空闲超过一定时间(默认为60秒),线程将被回收。
ExecutorService executor = Executors.newCachedThreadPool();
(3)ScheduledThreadPool(定时线程池)
支持定时及周期性任务执行。线程池中的线程数量固定,但可以根据需要调整。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(int corePoolSize);
(4)SingleThreadExecutor(单线程化线程池)
线程池中只有一个线程,所有任务按照提交顺序执行。如果任务执行过程中发生异常,则线程池会创建一个新线程来执行后续任务。
ExecutorService executor = Executors.newSingleThreadExecutor();
6.5 线程池五种队列
(1)LinkedBlockingQueue
基于链表的阻塞队列,默认情况下是无界队列(但如果指定了容量,也可以作为有界队列使用),队列容量理论上是无限的,但受限于JVM的内存。需注意内存溢出的风险
(2)ArrayBlockingQueue
基于数组的阻塞队列,有界队列。它确保了所有线程可以安全地访问队列,如果队列已满,则线程将会阻塞直到有空间可用。
(3)SynchronousQueue
无缓存的阻塞队列,不存储数据。每个插入操作必须等待一个相应的删除操作,反之亦然。它直接进行数据的交换,即一个线程尝试向队列中添加元素时,必须有另一个线程正在等待接收这个元素。
(4)PriorityBlockingQueue
基于优先级的阻塞队列,无界队列(但也可以指定容量),队列中的元素会根据其优先级进行排序,优先级高的元素会先被取出执行。
(5)DelayQueue
延时队列,只有在特定时间后才能被获取的元素存在,元素通常在添加到DelayQueue时被“推迟”,直到其指定的延迟时间过去后才会被移除。
参考:https://nbsaas.blog.csdn.net/article/details/139858398
参考:https://www.bilibili.com/video/BV1HP411t7Lq?p=29&vd_source=ea0f09fc475a05984bf43c87396aa0f8