线程池
现有问题
通过线程的学习,我们可以建立多个线程在同一时间完成不同的任务,线程在执行完线程任务之后会进入到等到销毁的状态,在这个过程中会继续消耗内存。
在我们现在简单的几个示例对此并没有太多的感觉,但是在实际开发中我们的开发人员较多,每个人写少量的线程,最后的线程总数就会很大,会消耗大量内存,导致我们的程序卡顿。而且gc频繁的回收也会较大的影响我们系统的运行。
解决方案
为了解决这个问题,我们使用了线程池
线程池作用
线程池的主要作用是:管理线程,如创建线程、使用线程、销毁线程等。
线程池本质
线程池的本质就是容纳线程的容器
线程池使用步骤
- 线程池
- 上传线程任务
- 关闭线程池
线程池提供的方法
- void shutdown():关闭线程池,这个方法将会在线程池内所有线程任务都被执行完比之后才会关闭线程池,虽然这个方法是写在主线程中的,但是主线程代码在执行过程中会跳过它执行它后面的代码,等到线程池可以被关闭的时候再执行这行代码。
- boolean isShutdown():判断线程池是否关闭
- Future<?> submit(Runnable task):给线程池提交线程任务
- Future<T> submit(Callable<T> task):给线程池提交线程任务
Executors
线程池的创建较为繁琐,如:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
参数名 | 参数含义 |
---|---|
corePoolSize | 核心线程数 |
maximumPoolSize | 最大线程数 |
keepAliveTime | 线程执行完线程任务之后保持活跃的时间 |
unit | 时间单位 |
workQueue | 存储线程任务的队列 |
线程池五参的构造函数已经是参数最少的构造函数了,所以线程池的创建极为繁琐,所以Java给我们提供了一个专门用于创建线程池的工具类——Executors。
作用
用于创建线程池
提供的方法
static ExecutorService newFixedThreadPool(int nThreads);
用于创建固定线程池,线程池的线程数固定,即最大线程数等于核心线程数。如果我们上传的线程任务数量大于固定线程池的线程数量,线程池会先给每个线程分配一个线程任务,如果某个线程执行完线程任务,线程池将会再给它分配一个任务,知道线程任务队列中的任务全部执行结束为止。
static ExecutorService newSingleThreadExecutor();
创建单例线程池,线程池的线程数量为1,其中的线程将会逐个执行线程任务队列中的线程任务,直到所有的线程任务执行完毕。
static ExecutorService newCachedThreadPool();
创建可变线程,创建的线程池核心线程数是0,最大数量是int的最大取值,这个线程池中线程的数量将会随着线程任务的多少自动创建线程,如果某个线程在执行完线程任务之后的六十秒都没有执行新的线程任务,这个线程将会被回收。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
创建一个调度线程池,线程池中的核心线程数就是传入的参数,最大线程数在理论上是int的最大值。调度线程池最大的作用就是能够周期性的执行任务。
static ScheduledExecutorService newSingleThreadScheduledExecutor();
创建一个单例调度线程池,其中只有一条线程,可以周期性的执行线程任务。
static ExecutorService newWorkStealingPool();
创建抢占线程池,java的初衷是想让这个线程池中如果任务数大于线程数量时,每个线程先占一个任务,如果剩余任务没有线程数多,则由剩余的线程共同执行这些任务,但是这个逻辑并没有实现,所以这个一般不用。
Callable
创建线程任务的一种方法,使用Callable创建线程任务的优点在于可以获取到线程任务执行结束之后的返回值。
Callable是一个接口,其中有一个方法call,创建线程任务可以实现这个接口,然后重写call方法,在线程启动之后将会执行Callable对象中的call方法。
注:Callable只能用于线程池中的线程。
Future
获取线程任务执行之后的返回值。
Lock
用于替换synchronized,在需要上锁的代码之前加 Lock对象.lock();在代码之后加Lock对象.unLock()解锁。
lock本身用的较少,主要是用于读写锁。
读写锁
读写锁的接口是ReadWriteLock,提供了一个readLock方法和一个writeLock方法,用于获取读锁和写锁。
这个接口有一个子类ReentrantReadWriteLock,可以直接创建对象,并且通过这个对象获取读锁和写锁。
读写锁的规则是读读不互斥,读写互斥,写写互斥。
假如我们现在的类有一个属性,有两个方法,一个方法用于获取属性值,一个方法用于修改属性值。
在我们实际生活中,一个属性的值应该是可以被多人查看的,这样不会对数据造成影响。但是一个人在查看属性值的同时不能有别人修改属性值,否则读到的属性值可能是错误的,当然,多人同时修改属性值肯定不行,这个我们之前用银行的例子已经解释过了。
读写锁就完美实现了这个功能,我们给获取属性值的方法加读锁,给修改属性值的方法加写锁。那么在一个线程在修改数据的时候其他人不能修改数据或读取数据,但是如果没有线程在修改数据,那么所有需要读取数据的线程可以同时读取这个数据,大大提高了代码的运行效率。
线程池实践
创建一个拥有两个线程的固定线程池,传入三个线程任务,看一下其执行流程。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务1启动");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("任务1结束");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务2启动");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("任务2结束");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务3启动");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("任务3结束");
}
});
executorService.shutdown();
System.out.println("主线程在executorService.shutdown();之后的代码");
}
}
通过结果我们可以看到,我们将三个任务传入线程池之后,线程池中的两个线程开始执行任务1任务2,与此同时,主线程跳过了shutdown方法,开始执行其后面的代码。在线程池中的线程将任务1和任务2执行结束之后,其中的一条线程开始执行任务3,任务3执行结束之后所有的线程任务都执行完毕,主线程调用shutdown方法关闭线程池。