一、概念
首先来看几个基本概念:
程序
在计算机科学中,程序是为了执行特定任务或解决特定问题而编写的一组指令或代码。这些指令通常由高级编程语言编写,如Java、Python、C++等,然后经过编译器或解释器转换为机器可以理解的代码。
进程
运行中的程序。它是系统进行资源分配的基本单位,也是操作系统结构的基础。从广义上讲,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。而在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程则是线程的容器。
线程
操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。它被包含在进程之中,是进程中的一条执行路径。线程在程序里是一个独立的执行流,由一组机器指令、数据和栈组成,能够在线程创建后独立运行。
线程可以分为多种类型,如内核线程和用户线程。内核线程由操作系统内核进行调度和管理,而用户线程则是由用户程序自己创建和管理。在某些操作系统中,还存在混合调度的情况,即线程既可以由内核调度,也可以由用户程序自行调度。
多线程
指从软件或者硬件上实现多个线程并发执行的技术。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。(反过来说,如果硬件上只有一个cpu,那么基于它的多线程就只是看起来像同时执行而已)
优点:提高CPU的利用率,可以同时处理多个任务,提高程序的运行效率;当一个线程由于某种原因被阻塞时,其他线程可以继续执行,从而提高了响应速度;对于需要同时处理多个任务的程序,使用多线程可以简化程序结构,使程序更容易理解和修改。
缺点:多个线程可能会竞争同一资源,导致数据混乱,即所谓的“线程安全”问题;由于线程是并发运行的,导致调试多线程程序非常困难;过多的线程会消耗大量的系统资源,如果不合理控制线程的数量,可能会导致系统性能降低;线程之间的切换需要时间,如果不恰当地使用多线程,可能会引起频繁的上下文切换,反而降低系统效率。
并发、并行、串行
并发:是指在一个时间段内,有多个程序或任务同时处于启动到运行完毕之间的状态,但任意一个时刻点上只有一个程序或任务在处理机上运行。并发强调的是多个任务在宏观上同时发生,但在微观上,由于处理器资源的限制,它们仍然是顺序执行的。并发编程的目标在于充分利用处理器的每一个核,以达到最高的处理性能。
并行:是指多个任务或事件在同一时刻发生,即它们在同一时间段内同时开始并同时结束。在操作系统中,这表现为多个程序段同时在系统中运行,这些程序的执行在时间上是重叠的。并行要求有足够的处理器资源来同时处理多个任务,每个处理器可以运行一个程序。
串行:与并发和并行相对,它指的是在通信或数据处理中,每次只处理一个任务或传输一个数据单元。在串行通信中,计算机总线或其他数据通道上,每次传输一个位元数据,并连续进行以上单次过程。串行方式通常用于长距离通信或大多数计算机网络,因为在这些情况下,电缆和同步化使得并行通信的实际应用面临困难。
从概念上来看,并发和并行的主要区别在于时间上的重叠:并发在宏观上同时发生,但微观上仍然是顺序执行;而并行则是真正的同时执行,没有任何时间上的重叠。另外,并发通常是在同一实体(如单个处理器)上发生的,而并行则可能涉及多个不同的实体(如多个处理器)。而串行则与这两者完全不同,它强调的是任务或数据传输的顺序性。
二、线程的生命周期
(了解Java中线程的生命周期,为后续的多线程开发奠定基础)
在Java中,线程的生命周期是一个重要的概念,它描述了线程从创建到销毁的整个过程。这个过程包括几个关键的状态转换,每个状态都代表了线程在执行过程中的不同阶段。
线程的生命周期主要包括以下几个阶段:
- 新建状态:当一个Thread类或其子类的对象被声明及创建时,新创建的线程对象处于新建状态。在这个阶段,线程还没有开始执行,只是被创建出来,等待进一步的启动操作。
- 就绪状态:当处于新建状态的线程调用start()方法开始运行时,它会进入就绪状态。在这个阶段,线程已经具备了运行的条件,只是还没有被分配到CPU资源。线程会进入线程队列等待CPU分配时间片执行。
- 运行状态:当就绪的线程被调度并获得CPU资源时,它会进入运行状态。在这个阶段,线程开始执行其任务,即运行run()方法中的代码。
- 阻塞状态:在某些特殊情况下,线程可能会进入阻塞状态。这通常发生在线程执行I/O操作、等待获取同步锁,或者调用某些导致线程暂停的方法(如sleep()、wait()等)时。在阻塞状态下,线程会暂停执行,并释放CPU资源,直到某个条件满足后才会重新进入就绪状态。
- 死亡状态:线程在完成其任务、被强制终止,或者因为出现异常而结束时,会进入死亡状态。在这个阶段,线程已经停止执行,其占用的资源会被释放。
三、Java中创建线程的几种方法
继承Thead类、实现Runnable接口、实现Callable接口
继承Thread类:
通过继承Thread类并重写其run()方法,可以创建新的线程。
缺点:受限单继承,即:java中只能继承一个类。
示例:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Thread running: " + i);
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
实现Runnable接口:
通过实现Runnable接口并重写其run()方法,可以创建新的线程。这种方式比继承Thread类更灵活,因为Java不支持多重继承,但可以实现多个接口。
示例:
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Runnable running: " + i);
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
(说明:Runnable不等于线程,表示可运行,可作为创建线程时的参数。)
实现Callable接口与Future、FutureTask:
Callable接口与Runnable接口类似,但Callable可以返回执行结果,并且可以声明抛出异常。Future用于获取Callable执行后的结果。FutureTask是Future接口的一个实现类,它方便把Callable转换成Future和Runnable。
示例:
import java.util.concurrent.*;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
return sum;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Integer> future = executorService.submit(new MyCallable());
Integer result = future.get(); // 获取执行结果
System.out.println("Result: " + result);
executorService.shutdown();
}
}
基于这三种基础方式的扩展与简化:
除了上述三种常见的创建线程的方式,Java中还有一些其他实现多线程的方法,但它们通常是基于上述方式的扩展或简化。以下是几种额外的方式:
使用线程池(ExecutorService)
Java的java.util.concurrent
包提供了线程池的实现,如ExecutorService
。线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的ThreadFactory
创建一个新线程。
示例代码:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建一个固定大小的线程池
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker); // 提交任务到线程池执行
}
executor.shutdown(); // 关闭线程池,不再接受新任务
while (!executor.isTerminated()) {
} // 等待所有任务完成
System.out.println("Finished all threads");
}
}
class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start. Command = " + command);
processCommand();
System.out.println(Thread.currentThread().getName() + " End.");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用Lambda表达式简化Runnable
从Java 8开始,可以使用Lambda表达式来更简洁地创建Runnable对象,而无需显式创建匿名内部类。
示例代码:
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Lambda thread running: " + i);
}
});
thread.start();
使用匿名内部类
匿名内部类也是一种创建线程的常见方式,它允许你快速实现一个接口或继承一个类而无需定义一个新的命名类。这种方式与实现Runnable接口类似,但语法上更加简洁。
示例代码:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("Anonymous class thread running: " + i);
}
}
});
thread.start();
这些额外的实现方式通常是为了简化代码或提高性能而存在的,但它们的核心原理仍然是基于Java中的多线程基础机制。在选择使用哪种方式时,应根据项目的具体需求、代码的可读性和维护性等因素进行综合考虑。
四、Java中线程常用的方法
Java线程中常用的方法主要包括以下几种,它们各自在线程的生命周期和操作中扮演着重要的角色:
start():
- 功能:启动线程。调用线程的start()方法将导致此线程开始执行;Java虚拟机调用此线程的run()方法。
- 注意:start()方法并不是直接运行线程的代码,而是告诉JVM去启动一个新的线程,并执行该线程的run()方法中的代码。
run():
- 功能:线程的线程体。当线程开始执行后,它会调用run()方法中的代码。
- 注意:不能直接通过线程对象来调用run()方法,因为这并不会产生一个新的线程,而只是普通的方法调用。
getName():
- 功能:获取线程的名称。
- 返回值:返回此线程的名称。
setPriority(int newPriority):
- 功能:设置线程的优先级。
- 参数:newPriority是线程的新优先级。
- 注意:Java线程的优先级是一个提示给调度器的,并不保证优先级高的线程一定优先执行。
getPriority():
- 功能:返回线程的优先级。
join():
- 功能:等待该线程终止。调用该方法的线程将等待被调用的线程执行完毕。
- 注意:如果当前线程是A,它调用了线程B的join()方法,那么线程A就会进入阻塞状态,直到线程B运行结束,线程A才从join()方法返回,继续执行后续代码。
sleep(long millis):
- 功能:使当前正在执行的线程以指定的毫秒数暂停(临时停止执行)。
- 参数:millis是要线程睡眠的毫秒数。
- 注意:sleep()是Thread类中的静态方法,它不会释放锁。
interrupt():
- 功能:中断线程。
- 注意:中断线程不会立即停止线程的执行,而是改变线程的中断状态。被中断的线程可以通过检查自身的中断状态来决定如何响应中断请求。
isInterrupted() 和 static interrupted():
- 功能:这两个方法都用于检测线程的中断状态。
- 区别:isInterrupted()是实例方法,它返回线程的中断状态并清除中断状态;而interrupted()是静态方法,它返回当前线程的中断状态并清除中断状态。
yield():
- 功能:提示调度器该线程愿意放弃当前对CPU资源的占用,以使其他线程得到执行的机会。
- 注意:yield()方法只是建议性的,调度器可能会忽略这个提示。
除了上述方法外,Java线程中还有许多其他方法和工具类用于线程同步、通信和协作,如wait()
, notify()
, notifyAll()
, synchronized
关键字,以及java.util.concurrent
包中的各种工具类。这些方法和工具类共同构成了Java线程编程的丰富生态系统。
注意:wait(),notify(),notifyAll()这些方法并非是线程类特有的,它们是Object类中定义的方法,所以它们是每一个java对象中都有的方法。
wait(), notify() 和 notifyAll()
是 Java 中 Object 类中的三个方法,它们主要用于多线程之间的同步和通信。这三个方法都是 Java 线程同步机制中的关键部分,与 synchronized 关键字一起使用,可以实现线程间的协调与合作。下面是对这三个方法的详细介绍:
wait() 方法
wait()
方法用于让当前线程等待,直到其他线程调用该对象的 notify()
或 notifyAll()
方法。调用 wait()
方法的线程必须持有该对象的监视器(即已经获取了该对象的锁)。当线程调用某个对象的 wait()
方法时,它会释放该对象的监视器,并进入等待状态,直到其他线程调用该对象的 notify()
或 notifyAll()
方法将其唤醒。
wait()
方法有三种重载形式:
wait()
:无限期地等待,直到被其他线程唤醒。wait(long timeout)
:等待指定的毫秒数,或者直到被其他线程唤醒。wait(long timeout, int nanos)
:等待指定的毫秒数和纳秒数,或者直到被其他线程唤醒。
notify() 方法
notify()
方法用于唤醒在此对象监视器上等待的单个线程。如果有多个线程在此对象上等待,则选择其中一个唤醒,选择是任意性的。调用 notify()
方法的线程也必须持有该对象的监视器。
notifyAll() 方法
notifyAll()
方法用于唤醒在此对象监视器上等待的所有线程。调用 notifyAll()
方法的线程也必须持有该对象的监视器。
使用注意事项
wait()
,notify()
, 和notifyAll()
必须在synchronized
代码块或方法中使用,否则会导致IllegalMonitorStateException
异常。- 调用
wait()
,notify()
, 或notifyAll()
的线程必须拥有对象的监视器(即必须持有对象的锁)。 wait()
会释放对象的监视器,而notify()
和notifyAll()
不会释放监视器,它们只是在唤醒等待的线程后,允许这些线程重新竞争监视器的所有权。wait()
,notify()
, 和notifyAll()
都是与对象的监视器相关的,而不是与线程相关的。这意味着不同的线程可以在不同的监视器上调用这些方法。
示例:
下面是一个简单的示例,演示了如何使用 wait()
和 notify()
方法实现生产者-消费者问题:
public class ProducerConsumer {
private final Object lock = new Object();
private int buffer = 0;
private boolean isFull = false;
public void producer() {
synchronized (lock) {
while (isFull) {
try {
lock.wait(); // 生产者等待,直到缓冲区不满
} catch (InterruptedException e) {
e.printStackTrace();
}
}
buffer = produce(); // 假设produce()方法产生一个新的值
isFull = true;
lock.notify(); // 唤醒等待的消费者线程
}
}
public void consumer() {
synchronized (lock) {
while (!isFull) {
try {
lock.wait(); // 消费者等待,直到缓冲区不空
} catch (InterruptedException e) {
e.printStackTrace();
}
}
consume(buffer); // 假设consume()方法消费缓冲区中的值
isFull = false;
lock.notify(); // 唤醒等待的生产者线程
}
}
// 假设的produce和consume方法,这里只是示意
private int produce() {
// ... 生产逻辑
return 0;
}
private void consume(int value) {
// ... 消费逻辑
}
}
在这个示例中,生产者和消费者线程通过 wait()
和 notify()
方法在缓冲区满或空时进行协调。注意,这些操作都是在持有 lock
对象的监视器的情况下进行的,以确保线程安全。
五、线程安全
jvm内存划分
在讲线程安全之前,先简单了解一下jvm中的内存区域是如何划分的。
在JVM(Java虚拟机)中,内存被划分为几个不同的区域,每个区域都有其特定的用途和生命周期。以下是JVM内存划分的详细介绍:
- 方法区(Method Area):
- 也被称为静态区,它主要存储了类的元数据信息、常量、静态变量、即时编译器编译后的代码等数据。
- 方法区中的数据是线程共享的,在JVM启动时创建,并且在JVM关闭时销毁。
- 在Java 8及以后的版本中,方法区被元空间(Metaspace)所替代。
- 堆(Heap):
- 堆是JVM中最大的一块内存区域,它用于存放所有对象实例和数组。
- 堆是线程共享的,所有的线程都可以访问堆中的任何对象。
- 当创建一个对象时,它会在堆上分配内存。这个内存分配是通过使用
new
关键字完成的。 - 堆通常会被进一步细分为新生代(Young Generation)和老年代(Old Generation),其中新生代又包含Eden区、From Survivor区和To Survivor区,这是为了优化垃圾回收的性能。
- 栈(Stack,也叫虚拟机栈):
- 每个线程在创建时都会创建一个虚拟机栈,用于存储线程执行方法时的局部变量表、操作数栈、动态链接、方法出口等信息。
- 每当一个方法被调用时,都会在该线程的虚拟机栈中创建一个新的栈帧(Stack Frame),用于存储该方法的局部变量等信息。
- 当方法执行完成后,对应的栈帧会被销毁,释放内存。
- 本地方法栈(Native Method Stack):
- 与虚拟机栈类似,但它是为执行本地方法(通常是native方法)服务的。
- 如果JVM使用的是JNI(Java Native Interface)来调用本地方法,那么这些本地方法就会运行在本地方法栈中。
- 程序计数器(Program Counter Register):
- 是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。
- 每个线程都有一个私有的程序计数器,用于记录线程当前执行的字节码指令的地址。
- 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值则是undefined。
- 运行时常量池(Runtime Constant Pool):
- 是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
- 它在类加载后进入方法区,支持动态扩展。
- 除了编译期产生的常量,运行时常量池还可以动态地接收并存储由
String
类的intern()
方法生成的字符串。
这些内存区域共同构成了JVM的内存模型,使得Java程序能够正常运行。同时,JVM还提供了垃圾回收机制来自动管理堆内存中的对象,防止内存泄漏和溢出等问题。
注:这里只对java内存模型做简单了解,主要用于了解哪些内存区域是多线程间共享(堆、方法区)的,哪些是线程私有(栈、程序计数器)的,而线程安全问题,只会发生在对共享空间数据的操作中。
出现线程安全问题的原因
- 多个线程访问和修改同一变量,导致数据不一致;
- 线程的执行顺序不确定,导致代码的执行结果不可预测;
- 某些操作不是原子的,即它们包含多个步骤,如果在这个过程中被其他线程打断,可能导致数据损坏;
- 内存可见性问题,即一个线程对共享变量的修改,另一个线程可能看不到;
- 指令重排序引起的安全问题,即编译器或处理器为了优化性能,可能改变指令的执行顺序。
分析一下,是否会出现线程安全问题的条件:
- 是否具备多线程环境;
- 是否共享数据;(读写)
- 是否有多条语句操作共享数据;
- 是否共享数据;(读写)
变量对线程安全的影响:
- 静态变量:在方法区,共享,存在线程安全问题;
- 实例变量:在堆中,共享,存在线程安全问题;
- 局部变量:在栈中,线程私有,不存在线程安全问题;
- 实例变量:在堆中,共享,存在线程安全问题;
常用的线程安全解决方案
- synchronized关键字:synchronized是Java中最基本的线程同步机制,它可以用来修饰方法或代码块。当一个线程进入一个synchronized方法或代码块时,它会获取一个锁,其他尝试进入该方法或代码块的线程将会被阻塞,直到锁被释放。这样就保证了在同一时刻只有一个线程能够执行某个方法或代码块,从而避免了并发访问共享资源导致的线程安全问题。
- 加锁,如:Lock接口和ReentrantLock类:除了synchronized关键字外,Java还提供了Lock接口和ReentrantLock类等更灵活的锁机制。这些锁机制提供了更细粒度的控制,可以显式地获取和释放锁,还支持中断等待的线程,以及尝试获取锁等高级功能。
- volatile关键字:volatile关键字可以用来修饰变量,它保证了被修饰的变量的可见性和有序性。当一个线程修改了一个volatile变量的值,其他线程能够立即看到这个修改。但是需要注意的是,volatile并不能保证复合操作的原子性,因此在使用时需要注意。
- 避免共享状态:如果可能的话,尽量避免在多个线程之间共享状态。例如,可以使用局部变量或线程局部变量来存储数据,这样每个线程都有自己的数据副本,就不会出现线程安全问题。
- 使用线程安全的集合类:Java提供了一些线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等。这些集合类内部实现了适当的同步机制,可以在多线程环境下安全地使用。
总的来说,解决Java中的线程安全问题需要综合考虑多种因素,包括并发访问的模式、共享资源的性质以及性能需求等。在选择解决方案时,需要根据具体情况进行权衡和选择。
关于同步的进一步说明
同步是一种用于控制多个线程访问共享资源的方式,以确保它们按照预定的顺序或条件进行访问,从而避免数据不一致、脏读或死锁等问题。同步机制在并发编程中扮演着至关重要的角色,它使得多个线程可以安全地协作,共同完成任务。
(如上面提供的解决方案中,synchronized、lock都是同步的实现方式)
下面对同步的实现方式做下详细介绍:
synchronized关键字
synchronized
是Java提供的一种内置锁机制,用于控制对共享资源的访问。它可以在方法级别或代码块级别上应用。
方法级别
当一个方法被synchronized
修饰时,它表示该方法在同一时刻只能被一个线程访问。
public synchronized void synchronizedMethod() {
// 方法体
}
这里的锁对象是实例对象本身(对于非静态方法)或类的Class对象(对于静态方法)。
代码块级别
synchronized
还可以用于代码块,这样可以将锁限定在更小的代码范围内。
public void someMethod() {
synchronized(this) {
// 同步代码块
}
}
在上面的例子中,this
作为锁对象。你也可以使用其他对象作为锁,只要确保所有需要同步的线程都使用相同的锁对象。
volatile关键字
volatile
关键字用于确保变量的可见性和有序性,但它不保证复合操作的原子性。当一个变量被声明为volatile
时,它会告诉JVM这个变量可能会被多个线程同时访问,并且每次使用这个变量时都应该从主内存中读取它的值,而不是使用线程本地缓存中的值。
public volatile int sharedCount = 0;
尽管volatile
可以提供一定的线程安全,但它通常用于标记那些被多个线程共享但不涉及复杂线程间交互的变量。
java.util.concurrent.locks包中的锁
Java的java.util.concurrent.locks
包提供了更灵活和强大的锁机制。
ReentrantLock
ReentrantLock
是一个可重入的互斥锁,与synchronized
相比,它提供了更多的功能,如可中断的获取锁尝试、定时获取锁尝试以及锁的可视化监控。
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁
try {
// 访问共享资源
} finally {
lock.unlock(); // 释放锁
}
Condition
Condition
是与ReentrantLock
配合使用的,它允许线程在某个特定条件满足时才能继续执行。
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while (!someCondition) {
condition.await(); // 等待条件成立
}
// 执行条件成立后的操作
} finally {
lock.unlock();
}
使用局部变量和ThreadLocal
当每个线程都需要访问它自己的数据副本时,可以使用局部变量或ThreadLocal
。ThreadLocal
为每个使用该变量的线程提供独立的变量副本。
ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal.set(123); // 线程设置自己的值
Integer value = threadLocal.get(); // 线程获取自己的值
使用并发容器和工具类
Java的java.util.concurrent
包提供了许多并发容器和工具类,如ConcurrentHashMap
、CopyOnWriteArrayList
、CountDownLatch
、CyclicBarrier
和Semaphore
等,这些工具类内部实现了适当的同步机制,可以简化并发编程的复杂性。
注意事项
- 过度同步可能导致性能下降,因此应该仔细评估同步的范围和粒度。
- 死锁是同步编程中常见的问题,应该避免嵌套锁、循环等待等情况。
- 在使用锁时,要注意锁的释放顺序,避免在异常情况下锁没有被正确释放。
- 使用
volatile
时要确保操作的原子性,对于复合操作可能需要额外的同步机制。
通过合理选择和使用这些同步机制,我们就大概率可以编写出安全、高效且易于维护的并发程序。
六、线程池
线程,是一种宝贵的资源,我们不应该在多线程编程中通过不断创建新线程的方式来处理我们的任务,线程的创建、销毁,都会过度的消耗系统资源,导致系统负载高,进一步影响服务性能和用户体验,所以,出现了线程池。
定义
线程池是一个存放线程的池子,池子里存放了多个可以复用的线程。创建一定数量的线程并放在池中,当需要使用线程时,直接从池中获取,使用完毕后再放回池中,而不是每次都创建新的线程。这样可以大大减少创建和销毁线程的次数,提高系统的性能。
线程池的优点:
- 资源的有效使用:创建和销毁线程涉及到与操作系统的交互,这需要一定的时间和资源。线程池通过复用线程,大大减少了这种开销。
- 限制线程数量:如果没有线程池,每个并发任务都可能创建一个新线程,这可能导致系统中线程数量过多,消耗大量内存和CPU资源,甚至可能导致系统崩溃。线程池可以限制系统中活动线程的数量,有效防止这种情况的发生。
- 内核线程管理:在底层操作系统中,每个Java线程实际上都是一个内核线程或轻量级进程。线程池通过限制活动线程的数量,可以减少操作系统的负载,提高系统的响应速度。
使用方法
Executor框架
Executor框架是Java中的一个重要的并发编程工具,它提供了一套灵活且强大的线程池管理机制,使得开发者能够更方便地控制和管理线程的创建、执行和销毁过程。以下是Executor框架的基本使用方法:
一、创建线程池
Executor框架提供了多种线程池的创建方式,以适应不同的应用场景。例如:
newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池,当提交的任务数超过线程池大小时,多余的任务会被放在队列中等待执行。newCachedThreadPool()
:创建一个可缓存的线程池,如果线程池当前大小超过处理需要,可回收空闲线程,若可回收的线程数已经到达最大空闲数,后续空闲线程不会被立即回收,而是在一个可接受的超时时间后才会被终止。newSingleThreadExecutor()
:创建一个单线程的Executor,它保证所有任务按照某一顺序在一个线程中执行。
二、提交任务
创建好线程池后,可以通过execute()
方法或submit()
方法提交任务到线程池执行。
execute(Runnable command)
:接收一个Runnable对象作为参数,用于执行没有返回值的任务。submit(Callable<T> task)
:接收一个Callable对象作为参数,用于执行有返回值的任务,并返回一个Future对象,用于获取任务的执行结果。
三、关闭线程池
当不再需要线程池时,应该关闭它,以释放资源。可以通过调用线程池的shutdown()
或shutdownNow()
方法来关闭线程池。这两个方法的区别在于,shutdown()
会等待所有已提交的任务执行完毕后再关闭线程池,而shutdownNow()
会尝试停止所有正在执行的任务,并返回那些尚未开始执行的任务列表。
四、获取任务执行结果
对于通过submit()
方法提交的有返回值的任务,可以通过返回的Future对象获取任务的执行结果。调用Future对象的get()
方法会阻塞当前线程,直到任务执行完成并返回结果。如果任务尚未完成,get()
方法会一直等待。
需要注意的是,使用Executor框架时,应该避免过度提交任务,以免导致线程池阻塞或系统崩溃。同时,也要合理处理任务执行过程中可能出现的异常,避免影响线程池的正常运行。
总的来说,Executor框架为Java的并发编程提供了强大的支持,通过合理使用线程池和任务提交机制,可以大大提高系统的性能和响应速度。
示例:
下面是一个使用FixedThreadPool
的简单示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
Runnable task = new Runnable() {
public void run() {
// 执行任务的具体逻辑
System.out.println("Executing task in thread: " + Thread.currentThread().getName());
}
};
executor.execute(task);
}
// 关闭线程池(不再接受新任务,但已提交的任务会继续执行)
executor.shutdown();
}
}
在这个例子中,我们首先通过Executors.newFixedThreadPool(5)
创建了一个固定大小为5的线程池。然后,我们创建了一个Runnable
任务,并通过executor.execute(task)
将任务提交到线程池。最后,通过executor.shutdown()
关闭线程池。
ThreadPoolExecutor
ThreadPoolExecutor
是Java中用于创建和管理线程池的核心类,它位于java.util.concurrent
包中。这个类提供了丰富的配置选项,允许开发者根据应用程序的具体需求来创建和定制线程池的行为。
ThreadPoolExecutor的构造
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
参数说明:
- corePoolSize:线程池的基本大小,即线程池中保持的线程数量,即使在线程空闲时,线程池也不会释放这些线程。除非设置了
allowCoreThreadTimeOut
。 - maximumPoolSize:线程池允许的最大线程数。当队列满了,并且已创建的线程数小于maximumPoolSize,则线程池会再创建新的线程执行任务。
- keepAliveTime:当线程数大于核心线程数时,这是多余的空闲线程在终止前等待新任务的最长时间。
- unit:
keepAliveTime
参数的时间单位,可以是TimeUnit
枚举中的任意一个。 - workQueue:用于保存等待执行的任务的阻塞队列。这个队列很重要,它必须是有界的,当队列满了,会创建新的线程(如果当前线程数小于maximumPoolSize),否则根据拒绝策略来处理新任务。
- threadFactory:用于创建新线程的线程工厂,可以自定义线程创建方式。
- handler:当队列和线程池都满了,用于处理新提交任务的拒绝策略。
ThreadPoolExecutor的工作顺序
线程池的工作顺序为:corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略。也就是说,当有新任务提交时,线程池会首先尝试使用核心线程来执行,如果核心线程都在忙,那么任务会被放入队列中等待。如果队列也满了,线程池会尝试创建新的线程(但不超过maximumPoolSize)来执行任务。如果线程池中的线程数已经达到了maximumPoolSize,并且队列也满了,那么就会根据设定的拒绝策略来处理新提交的任务。
ThreadPoolExecutor的使用方法
使用ThreadPoolExecutor
创建线程池时,需要合理设置上述参数,以满足应用程序的需求。例如:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // workQueue
Executors.defaultThreadFactory(), // threadFactory
new ThreadPoolExecutor.DiscardOldestPolicy() // handler
);
在这个例子中,我们创建了一个线程池,其核心线程数为5,最大线程数为10,多余的线程在空闲60秒后会被终止。任务队列是一个容量为100的ArrayBlockingQueue。当队列和线程池都满了时,会采用DiscardOldestPolicy
策略,即丢弃队列中最老的任务,并尝试再次提交当前任务。
线程池使用的注意事项
- 确定线程池大小:线程池的大小应根据程序的性质和目标来确定。如果线程池太小,可能导致任务无法及时执行;如果线程池太大,可能会浪费系统资源。
- 避免过度提交任务:向线程池提交过多的任务可能导致线程池阻塞或系统崩溃。可以通过控制任务队列的大小来限制向线程池提交的任务数量。
- 合理处理异常:当线程池中的任务抛出异常时,应合理处理这些异常,避免影响线程池的正常运行。
- 优雅地关闭线程池:在不再需要线程池时,应优雅地关闭它,确保所有已提交的任务都已执行完毕,同时释放线程池占用的资源。
七、经典案例
龟兔赛跑
在这个例子中,我们将创建两个线程分别代表乌龟和兔子,它们将进行赛跑。为了模拟兔子的懒惰和乌龟的坚持,兔子线程会在赛跑过程中睡眠一段时间。
public class Race {
private static final int DISTANCE = 100;
static class Turtle extends Thread {
@Override
public void run() {
for (int i = 0; i < DISTANCE; i++) {
System.out.println("Turtle moves forward.");
try {
Thread.sleep(100); // 模拟乌龟慢跑
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Turtle wins the race!");
}
}
static class Rabbit extends Thread {
@Override
public void run() {
for (int i = 0; i < DISTANCE; i++) {
System.out.println("Rabbit moves forward quickly.");
if (i == DISTANCE / 4) {
try {
System.out.println("Rabbit takes a nap...");
Thread.sleep(5000); // 模拟兔子睡觉
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
Thread.sleep(100); // 模拟兔子快跑
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("Rabbit wakes up and finishes the race!");
}
}
public static void main(String[] args) {
Turtle turtle = new Turtle();
Rabbit rabbit = new Rabbit();
turtle.start();
rabbit.start();
}
}
银行转账
在这个例子中,我们将模拟两个账户之间的转账操作,这通常需要在多线程环境下进行,以确保转账的原子性。
public class BankAccount {
private double balance;
public BankAccount(double balance) {
this.balance = balance;
}
public synchronized void deposit(double amount) {
balance += amount;
System.out.println("Deposited: " + amount);
}
public synchronized void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrawn: " + amount);
} else {
System.out.println("Insufficient funds!");
}
}
public double getBalance() {
return balance;
}
}
public class TransferTask implements Runnable {
private BankAccount fromAccount;
private BankAccount toAccount;
private double amount;
public TransferTask(BankAccount fromAccount, BankAccount toAccount, double amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run() {
fromAccount.withdraw(amount);
toAccount.deposit(amount);
}
}
public class BankDemo {
public static void main(String[] args) {
BankAccount accountA = new BankAccount(1000);
BankAccount accountB = new BankAccount(500);
Thread transferThread = new Thread(new TransferTask(accountA, accountB, 200));
transferThread.start();
}
}
在这个例子中,BankAccount
类提供了存款和取款的方法,并且都是同步的,以确保在同一时间只有一个线程可以执行这些方法。TransferTask
类实现了 Runnable
接口,它将在一个新的线程中执行转账操作。
生产者消费者模型
生产者消费者模型是一个经典的并发编程问题,其中生产者线程产生数据并放入缓冲区,消费者线程从缓冲区中取出数据并处理。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ProducerConsumerDemo {
static class Producer extends Thread {
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("Producer produced: " + i);
queue.put(i); // 生产数据并放入队列,如果队列满则阻塞
Thread.sleep(100); // 模拟生产数据的耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
static class Consumer extends Thread {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer data = queue.take(); // 从队列中取出数据,如果队列空则阻塞
System.out.println("Consumer consumed: " + data);
Thread.sleep(200); // 模拟消费数据的耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5); // 创建一个容量为5的阻塞队列
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
producer.start();
consumer.start();
}
}
在这个例子中,Producer
类和 Consumer
类分别继承自 Thread
,并且都有一个 BlockingQueue
类型的字段来作为数据的共享缓冲区。生产者线程将生成的数据通过 put
方法放入队列,如果队列已满则线程会被阻塞,直到队列中有空闲位置。消费者线程通过 take
方法从队列中取出数据,如果队列为空则线程会被阻塞,直到队列中有新的数据。
注意,这个示例中的 Consumer
类使用了一个无限循环来不断地从队列中取出数据。在实际应用中,你可能需要添加一些逻辑来优雅地停止消费者线程,比如设置一个标志位或者使用中断来停止循环。
此外,LinkedBlockingQueue
的构造函数接受一个整数参数,用于指定队列的容量。在这个例子中,我们将其设置为5,这意味着队列中最多可以存放5个元素。当队列满时,生产者线程会被阻塞,直到消费者线程从队列中取走一些元素。同样地,当队列空时,消费者线程会被阻塞,直到生产者线程向队列中放入新的元素。这种机制确保了生产者和消费者之间的同步,避免了数据竞争和不一致的问题。
八、工作中遇到过的一个小坑
(贴这里多少有点勉强,不过,冲着学习+分享的态度,但愿能帮助减少一次事故发生,少罚钱!!!)
项目背景说明:
spring项目;
controller接收用户请求,controller中调用service方法,处理业务逻辑;
service调用某个工具类处理具体的业务逻辑,处理完成后返回;
controller响应请求;
很普通的一个处理流程,但是由于调用方并不关心service层的处理逻辑,也不需要获取执行结果,所以自然而然的就想到了异步处理的方式,通过EnableAsync启用异步支持+使用一个@Async注解就可以轻松实现。
@Configuration
@EnableAsync
public class SpringAsyncConfig {
// ...
}
@Service
public class AsyncService {
@Async
public void asyncMethod() {
// 执行异步任务
}
@Async
public Future<String> asyncMethodWithReturnType() {
// 执行异步任务并返回结果
return new AsyncResult<>("异步结果");
}
}
怎么样,是不是很方便:)
问题就在这!
默认情况下,Spring会使用SimpleAsyncTaskExecutor
执行器来执行异步任务,但它是单线程的。
所以,在生产环境下,你可能需要配置一个线程池执行器,比如ThreadPoolTaskExecutor
,来更好地管理异步任务的执行。
@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("Async-");
executor.initialize();
return executor;
}
}
另外一个需要注意的:如果在异步方法中使用事务,你需要特别注意事务的传播行为。默认情况下,@Async
方法会在一个独立的线程中运行,这可能导致事务上下文不会正确传播。如果需要保持事务的一致性,你可能需要使用@Transactional
注解的propagation
属性进行配置。
建议:在使用@Async
注解时,务必仔细阅读文档,理解其工作原理,并根据实际需求进行配置和使用。
(其实都懂,但是往往出现问题的时候才会去查看源码,吃一堑长一智吧!!!)