一、线程池的作用
一般情况下线程运行就死亡了,后面如果有新任务就必须创建新的线程,如果有大量的任务就需要创建大量线程,会降低服务器的性能,造成资源的浪费。
线程池的作用是:首先会在池中分配一定数量的线程,线程使用完后会回到池中,等待下一个任务,线程资源就得到回收利用,减少服务器资源的消耗,提高了性能。
二、线程池的API
- Executor接口
- ExecutorService接口
- AbstractExecutorService抽象类
- ThreadPoolExecutor线程池类
- Executors工具类
三、Executors工具类
主要方法:
- ExecutorService newCachedThreadPool()
返回线程数量无限的线程池 - ExecutorService newFixedThreadPool(int size)
返回固定长度的线程池,好处是可以控制并发量 - ExecutorService newSingleThreadExecutor()
返回单一线程的线程池 - ScheduledExecutorService newScheduledThreadPool(int size)
返回可以调度的线程池
/**
* 测试长度不限的线程池
*/
public static void testCachedThreadPool(){
//获得长度不限的线程池
ExecutorService pool = Executors.newCachedThreadPool();
//测试执行100个任务
for(int i = 0;i < 100;i++){
final int n = i;
//使用线程池启动线程
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了! i --> "+n);
});
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//关闭线程池
pool.shutdown();
}
/**
* 测试长度固定的线程池
*/
public static void testFixedThreadPool(){
//固定长度线程池
ExecutorService pool = Executors.newFixedThreadPool(20);
for(int i = 0;i < 100;i++){
final int n = i;
//使用线程池启动线程
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了! i --> "+n);
});
}
//关闭线程池
pool.shutdown();
}
/**
* 测试长度单一的线程池
*/
public static void testSingleThreadPool(){
//单一长度线程池
ExecutorService pool = Executors.newSingleThreadExecutor();
for(int i = 0;i < 100;i++){
final int n = i;
//使用线程池启动线程
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了! i --> "+n);
});
}
//关闭线程池
pool.shutdown();
}
/**
* 测试可以调度的线程池
*/
public static void testScheduledThreadPool(){
//可调度线程池
ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
//执行线程,1、线程任务 2、初始的延迟数 3、周期数 4、时间单位
pool.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName()+"执行了,时间:"+new Date());
}, 2, 1, TimeUnit.SECONDS);
}
四、自定义线程池的配置
ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue)
参数说明:
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数
- keepAliveTime 临时的线程能够存活的时间
- unit 时间单位
- workQueue 用于保存任务的阻塞队列
优化配置:
- 核心线程数大于或等于CPU内核的数量(如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1,如果是IO密集型任务,参考值可以设置为2*NCPU)获得CPU的内核数:Runtime.getRuntime().availableProcessors()
- 最大线程数和核心线程数配置一样,性能比较高,因为避免临时线程的创建和销毁
- 如果临时线程比较多,可以将存活时间配置稍微长点,减少临时线程的创建和销毁
- 阻塞队列LinkedBlockingQueue的性能比较高
//cpu内核数
public static final int CPU_NUM = Runtime.getRuntime().availableProcessors();
/**
* 自定义配置线程池
*/
public static void testThreadPool(){
//配置线程池
ExecutorService pool = new ThreadPoolExecutor(CPU_NUM,CPU_NUM * 4 + 20,
0,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>());
for(int i = 0;i < 100;i++){
final int n = i;
//使用线程池启动线程
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+"执行了! i --> "+n);
});
}
//关闭线程池
pool.shutdown();
}
五、乐观锁和悲观锁
5.1 乐观锁和悲观锁的区别
- 悲观锁
想法悲观,认为当前的资源存在竞争,所以每次获得资源时都会上锁,阻塞住其它线程。
数据库中的行锁、表锁、读锁、写锁都属于悲观锁,Java的synchronized和ReentrantLock也属于悲观锁。
悲观锁会降低系统性能和吞吐量,提高数据的安全性,适用于多写少读的场景。 - 乐观锁
想法乐观,认为当前的资源不存在竞争,所以每次获得资源时都不上锁
乐观锁执行效率高,有利于提高系统吞吐量,适用于多读少写的场景。
5.2 乐观锁的实现
乐观锁常用的两种实现方式:
- 版本号机制
利用版本号version记录数据被修改的次数,当数据被修改时,version加一。当线程要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前version值相等时才更新,否则重试更新操作,直到更新成功。 - CAS算法
Compare and Swap(比较与交换)
CAS涉及三个操作数:- 读写变量的内存位置
- 预期值
- 写入的新值
CAS实现过程是:先判断内存位置上的值是否和预期值一致,如果一致就修改为新值,否则不执行。
5.3 原子类
下面代码,启动10000个线程中对count执行自增,执行多次后发现,最后count的值出现9999或9997的结果。
static int count = 0;
public static void main(String[] args) {
for(int i = 0;i < 10000;i++){
new Thread(()->{count++;}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count = " + count);
}
原因分析:
count++的操作分为三步:
- 读取count的值
- 计算count+1的值
- 把新值存入count
假设count值为100,两个线程A和B都执行了操作1,再同时执行操作2,A先进行操作3,这时count值为101,B再执行操作3,之前B读取的值是100,执行完操作3后B的结果还是101,这样数据出现了问题。因为上面的操作不是原子的,可以分开执行。
java.util.concurrent包中的原子类就应用了CAS机制,以AtomicInteger(原子整数)为例。使用AtomicInteger的自增方法incrementAndGet无论执行多少次,结果都是10000
static AtomicInteger at = new AtomicInteger(0);
public static void main(String[] args) {
for(int i = 0;i < 10000;i++){
new Thread(()->{at.incrementAndGet();}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count = " + at.get());
}
AtomicInteger的incrementAndGet操作就是:通过内存偏移量offset将数据取出,和expected期待值进行比较,如果相等就代表此过程中没有其他线程修改此值,那么就把存储中的值修改掉,否则就放弃修改。