多线程是Java中非常重要的一个特性,允许程序同时执行多个任务,从而提高程序的执行效率和响应速度。Java多线程主要涉及以下几个方面:
进程的概念
进程是系统进行资源分配和调度的一个独立单元,它是操作系统结构的基础。每个进程都有自己的独立内存空间和系统资源,进程之间的通信需要通过特定的机制(如管道、消息队列、共享内存等)来实现。进程是资源分配的最小单位,一个进程可以包含多个线程。
线程的基本概念
线程是操作系统中能够独立执行代码的最小单位,它是进程的一个实体,是CPU调度和分派的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可与同属一个进程的其他线程共享进程所拥有的全部资源。以下是线程的一些基本概念:
并发执行
线程允许多个任务(或代码段)在同一时间内并发执行,即使它们在同一处理器上运行。这通过CPU的时间片轮转、中断等技术实现,使得每个线程在极短的时间内交替执行,从而给用户一种同时运行多个任务的感觉。
共享资源
线程之间可以共享进程所拥有的资源,如内存空间、文件描述符等。这使得线程之间的通信和数据共享变得相对简单和高效。
独立调度
线程作为CPU调度的基本单位,可以被独立地调度和执行。这意味着操作系统可以根据需要,单独地暂停、恢复或终止线程的执行,而不需要影响进程中的其他线程。
状态转换
线程在其生命周期中会经历不同的状态转换,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和终止(Terminated)等状态。这些状态的转换由线程自身的执行和操作系统的调度策略共同决定。
同步与互斥
由于多个线程可能会同时访问共享资源,因此需要通过同步和互斥机制来避免数据竞争和保证数据的一致性。同步机制用于协调线程之间的执行顺序,而互斥机制则用于防止多个线程同时访问同一资源。
实现方式
线程的实现方式可以分为用户级线程和内核级线程。用户级线程由用户空间的线程库提供,其调度和管理不依赖于操作系统内核;而内核级线程则是由操作系统内核直接管理和调度的。现代操作系统通常将两者结合起来,提供更为灵活和高效的线程支持。
线程的使用也需要注意同步和互斥问题,以避免出现数据竞争、死锁等并发问题。
线程的创建方式
线程的创建方式主要有以下几种:
继承Thread类:
定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就是线程需要执行的任务。
创建Thread子类的实例,即创建了线程对象。
调用线程对象的start()方法来启动该线程。
优点:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获取当前线程。
缺点:因为线程类已经继承了Thread类,Java语言是单继承的,所以不能再继承其他父类。
// 继承Thread类创建线程
class MyThread extends Thread {
@Override
public void run() {
System.out.println("通过继承Thread类创建的线程正在执行...");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
在这个例子中,我们创建了一个MyThread类,它继承自Thread类,并重写了run方法。
然后,在main方法中创建了MyThread的一个实例,并调用其start方法来启动线程。
实现Runnable接口:
定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法同样是线程的执行体。
创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象。
调用线程对象的start()方法来启动线程。
优点:线程类只是实现了Runnable接口,还可以继承其他类,且多个线程可以共享一个target对象,适合多个相同线程处理同一份资源的情况。
缺点:编程稍微复杂一些,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
// 实现Runnable接口创建线程
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("通过实现Runnable接口创建的线程正在执行...");
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
/*在这个例子中,我们创建了一个MyRunnable类,
它实现了Runnable接口,并重写了run方法。
然后,在main方法中,我们创建了Thread类的一个实例,
将MyRunnable的实例作为参数传递给Thread的构造方法,
并调用Thread实例的start方法来启动线程。*/
实现Callable接口:
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
优点:call()方法可以有返回值,可以声明抛出异常。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 实现Callable接口创建线程
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "通过实现Callable接口创建的线程正在执行,并返回结果...";
}
}
public class Main {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
Thread thread = new Thread(futureTask);
thread.start(); // 启动线程
try {
// 获取线程执行结果
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
/*在这个例子中,我们创建了一个MyCallable类,
它实现了Callable接口,并重写了call方法。
call方法有一个返回值,并且可以声明抛出异常。
然后,我们创建了FutureTask类的一个实例来包装MyCallable实例,
并将FutureTask实例作为参数传递给Thread的构造方法。
最后,我们调用了Thread实例的start方法来启动线程,
并通过futureTask.get()方法获取线程执行的结果。*/
通过线程池创建线程:
使用ExecutorService框架来创建线程池,如Executors.newFixedThreadPool(int nThreads)或Executors.newCachedThreadPool()等。
线程池可以提高响应速度、降低资源消耗,并便于线程管理。
以上四种方式中,继承Thread类和使用Runnable接口是最基本的两种方式,而实现Callable接口则提供了更多的功能(如返回值和异常处理)。通过线程池创建线程则是一种更高级的线程管理方式,它能够提高程序的性能和可维护性。在实际开发中,应根据具体需求选择合适的线程创建方式。
线程的控制
线程的控制主要涉及线程的启动、暂停、恢复、终止等操作。以下是一些常见的线程控制方法:
1. 启动线程
- sleep()方法:可以使当前线程暂停执行指定的时间(以毫秒为单位)。当指定的时间过去后,线程将自动恢复执行。注意,sleep()方法不会释放锁。
- wait()和notify()/notifyAll()方法:这些方法用于线程间的通信。一个线程可以在某个对象的监视器上调用wait()方法,从而释放该对象的锁并进入等待状态。其他线程可以在同一个监视器上调用notify()或notifyAll()方法来唤醒一个或所有等待的线程。
- join()方法:可以使当前线程等待另一个线程完成其执行。调用线程(当前线程)将被阻塞,直到被join()的线程执行完毕。
- yield()方法:提示调度器当前线程愿意放弃当前使用的处理器。但是,调度器可以忽略这个提示。
2. 终止线程
- 正常终止:线程执行完其run()方法中的代码后自然终止。
- 使用共享变量:在线程中设置一个共享变量(如布尔类型的flag),当需要终止线程时,将该变量设置为特定值(如false),并在run()方法中使用循环检查该变量。如果变量表示需要终止线程,则线程可以退出循环并终止执行。
- 中断线程:可以通过调用线程的interrupt()方法来请求中断线程。线程的中断状态将被设置为true。线程应定期检查其中断状态(例如,在循环中),并在接收到中断请求时执行清理操作并退出。可以使用Thread.interrupted()或Thread.currentThread().isInterrupted()来检查中断状态。
注意事项
- 避免使用stop()和destroy()方法:这些方法已经被废弃,因为它们可能会导致线程留下未受管理的资源(如文件描述符、数据库连接等)。
- 线程间的同步:当多个线程需要访问共享资源时,必须采取适当的同步措施来避免数据不一致性和竞争条件。
- 线程安全:在设计多线程程序时,应考虑线程安全问题,确保在并发环境下程序的正确性和稳定性。
线程的同步
线程的同步是并发编程中的一个重要概念,它用于控制多个线程对共享资源的访问,以确保线程之间以有序、协调的方式进行工作,从而避免数据不一致性、竞争条件和其他并发问题。
在Java中,实现线程同步主要有以下几种方式:
1. synchronized关键字
synchronized关键字是Java提供的一种内置同步机制。它可以用于方法或代码块上,以确保在同一时刻只有一个线程可以执行该段代码。
同步方法:在方法声明中加上synchronized关键字,则该方法在同一时刻只能被一个线程访问。
public synchronized void syncMethod() {
// 同步代码块
}
同步代码块:使用synchronized关键字修饰一个代码块,可以指定一个锁对象。
只有持有该锁对象的线程才能执行该代码块。
java
public void method() {
synchronized(lockObject) {
// 同步代码块
}
}
2. Lock接口
从Java 5开始,java.util.concurrent.locks包中提供了显式的锁机制,允许更灵活的结构。Lock接口提供了比synchronized关键字更广泛的锁定操作。
ReentrantLock:是Lock接口的一个实现,它支持一个与之关联的条件对象(Condition),可以用来实现更复杂的线程同步控制。
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
try {
// 同步代码块
} finally {
lock.unlock(); // 解锁
}
3. volatile关键字
volatile关键字用于修饰变量,确保变量对所有线程的可见性。当一个变量被声明为volatile时,任何对该变量的写操作都将立即被其他线程所感知,这有助于避免指令重排序导致的可见性问题。但请注意,volatile并不能保证原子性。
4. 使用并发工具类
Java并发包java.util.concurrent还提供了许多并发工具类,如CountDownLatch、CyclicBarrier、Semaphore等,这些工具类可以用于实现复杂的线程同步控制。
注意事项
- 避免过度同步:过度同步会导致性能下降,因为线程在访问共享资源时可能需要等待。
- 理解死锁:死锁是多个线程相互等待对方释放资源而无法继续执行的情况。在设计同步机制时,应避免死锁的发生。
- 合理使用锁:锁的粒度应该适中,既不要太大导致性能下降,也不要太小导致频繁的锁竞争。
- 考虑线程安全类:Java中有些类已经是线程安全的,如Vector、Hashtable等,但在并发环境下推荐使用java.util.concurrent包中的并发集合,如ConcurrentHashMap等。
高级多线程工具
高级多线程工具在Java中主要通过java.util.concurrent包提供。这个包是Java并发框架的核心,包含了一系列用于实现多线程并发编程的工具和类。以下是一些高级多线程工具的分类和简介:
1. 线程池
ExecutorService是Java提供的线程池框架,它使得并发编程更加简单和高效。通过线程池,你可以控制同时执行的线程数量,避免创建大量线程导致的系统资源耗尽。ExecutorService提供了一系列工厂方法来创建不同类型的线程池,如:
- Executors.newSingleThreadExecutor():创建一个单线程的线程池。
- Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程池中的线程数量超过了处理任务所需要的线程,那么它就会回收空闲(60秒不执行任务)的线程,当任务增加时,它可以智能地添加新线程来处理任务。
- Executors.newFixedThreadPool(int nThreads):创建一个固定大小的线程池。
- Executors.newScheduledThreadPool(int corePoolSize):创建一个支持定时及周期性任务执行的线程池。
2. 并发集合
Java并发包提供了多种并发集合,这些集合是线程安全的,适用于高并发环境下的数据结构操作。例如:
- ConcurrentHashMap:线程安全的HashMap实现。
- CopyOnWriteArrayList:一个线程安全的ArrayList实现,适用于读多写少的并发场景。
- BlockingQueue:支持两个附加操作的队列,这些操作是:在元素可用之前阻塞的检索操作,以及在没有剩余空间时阻塞的插入操作。
3. 同步器(Synchronizers)
同步器用于管理多个线程之间的协作,包括等待/通知机制、屏障、信号量等。Java并发包提供了以下几种同步器:
- CountDownLatch:允许一个或多个线程等待其他线程完成操作。
- CyclicBarrier:让一组线程互相等待,直到所有线程都达到某个公共屏障点(common barrier point)。
- Semaphore:管理一组许可(permits),允许多个线程同时访问某个特定资源。
- Exchanger:用于在两个线程之间进行数据交换的同步辅助类。
4. 并发工具类
除了上述的线程池、并发集合和同步器之外,Java并发包还提供了一些其他并发工具类,如ForkJoinPool,它用于将可以并行计算的大任务分割成若干小任务,以便使用多核处理器并行处理。
使用建议
- 合理选择线程池:根据任务的性质选择合适的线程池,如IO密集型任务适合使用缓存线程池,CPU密集型任务适合使用固定大小的线程池。
- 优先考虑并发集合:在需要线程安全的数据结构时,优先考虑使用并发集合而不是对集合进行外部同步。
- 合理使用同步器:根据需求选择合适的同步器,确保线程之间的正确协作。
- 注意线程安全问题:在使用多线程时,要注意避免共享资源的不当访问,确保数据的一致性和完整性。
线程池
线程池(Thread Pool)是一种基于池化技术的多线程管理机制,它维护了一定数量的工作线程,这些线程可以并发地执行任务。线程池通过复用线程来减少线程创建和销毁的开销,提高系统的响应速度和吞吐量。
在Java中,线程池主要通过java.util.concurrent包下的ExecutorService接口实现。ExecutorService提供了一系列用于线程池管理的方法,如提交任务、关闭线程池等。
线程池的主要优点
- 降低资源消耗:通过复用线程,避免了频繁创建和销毁线程所带来的性能开销。
- 提高响应速度:当任务到达时,可以立即复用线程池中的空闲线程,无需等待新线程的创建。
- 提高线程的可管理性:线程池可以统一管理多个线程,包括线程的创建、启动、执行、监控和销毁等。
线程池的关键参数
- 核心线程数:线程池维护线程的最少数量,即使这些线程处于空闲状态,线程池也不会销毁它们。
- 最大线程数:线程池中允许的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会尝试创建新的线程来执行任务。
- 非核心线程空闲存活时间:当线程数大于核心线程数时,这是多余空闲线程在终止前等待新任务的最长时间。
- 时间单位:keepAliveTime参数的时间单位,如TimeUnit.SECONDS。
- 任务队列:用于保存等待执行的任务的阻塞队列。
线程池的主要类型
Java的Executors工厂类提供了多种线程池的创建方式:
- SingleThreadExecutor:单线程线程池,只有一个线程来执行任务,适用于需要顺序执行的任务。
- FixedThreadPool:固定大小的线程池,可以指定线程池的大小,适用于负载较重的服务器。
- CachedThreadPool:可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,可以智能地添加新线程来处理任务。ScheduledThreadPool:支持定时及周期性任务执行的线程池。
使用线程池的步骤
- 创建线程池:通过Executors工厂类创建线程池。
- 提交任务:通过线程池的submit方法提交任务,该方法返回一个Future对象,可以用来检查任务是否执行完成,以及获取任务的返回值。
- 关闭线程池:当所有任务执行完毕后,调用线程池的shutdown方法来关闭线程池,或者调用shutdownNow方法来尝试立即停止所有正在执行的任务并关闭线程池。
注意事项
- 避免创建过多线程:过多的线程会消耗系统资源,并可能导致上下文切换的开销增加。
- 合理设置线程池参数:根据应用的实际需求合理设置线程池的核心线程数、最大线程数、任务队列等参数。
- 正确处理异常:任务执行过程中可能会抛出异常,需要在任务内部进行适当的异常处理,或者通过Future对象来捕获和处理异常。
- 优雅关闭线程池:在应用程序关闭时,需要优雅地关闭线程池,等待所有任务执行完成后再关闭线程池,避免数据丢失或任务未完成的问题。