线程-线程池

线程池概念

池: 容器,

以数据库连接池为例:

每次连接都需要创建一个对象,用完销毁,频繁地创建销毁占用一定的开销。因此有了池的概念,就是先创建一定数量的对象放入数据库连接池有连接过来时,从池子中获取一个连接对象使用,用完之后不销毁,换回到池子中减少创建销毁的开销。

例:使用jdbc链接5000次使用时间20s,和使用阿里德鲁伊数据源连接5000次的0.6s比速度慢了很多。

为什么要使用线程池:池(容器)

每次创建线程都会占用内存空间,如果无线的创建内存就会浪废掉内存空间,验证的情况就可能导致内存溢出

cpu是有限的,一个cpu只能处理一个线程,很多线程没有执行权,那这些线程都会出去等待,会造成线程间大量的切换,导致性能变慢。因此需要线程池,创建管理线程。

线程池里的每一个线程在使用结束后不会销毁,就处于空闲状态,等待下一个任务的到来。

怎么使用线程池?java提供了有哪些线程池的实现?

  • jdk5版本java内置了线程池实现ThreadPolExecutor

  • 还提供Executors来创建不同类型的线程池。

Executors类(不推荐使用)

在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见的有四种

1.创建使用固定线程数(大小)的线程池

ExecutorService executorService = Executors.newFixedThreadPool(3);

  • 核心线程数与最大线程数一样,没有救急线程

  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

  • 适用场景:适用于任务量已知,相对耗时的任务

2.单线程化的线程池,一个单线程的线程池,只有池子里一个线程,保证所有任务按顺序执行。

ExecutorService exec =Executors.newSingleThreadExecutor();

  • 核心线程数和最大线程数都是1

  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE

  • 适用场景:适用于按照顺序执行的任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class NewSingleThreadCase {
    static int count = 0;
    static class Demo implements Runnable {
        @Override
        public void run() {
            count++;
            System.out.println(Thread.currentThread().getName() + ":" +
                    count);
        }
    }
    public static void main(String[] args) throws
            InterruptedException {
        //单个线程池,核心线程数和最大线程数都是1
        ExecutorService exec =Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            exec.execute(new Demo());
            Thread.sleep(5);
        }
        exec.shutdown();
    }
}
  1. 可缓存线程池:会根据任务自动新增或回收线程

  • 核心线程数为0

  • 最大线程数是Integer.MAX_VALUE

  • 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都

  • 必须等待一个移出操作。

  • 适用场景:适合任务数比较密集,但每个任务执行时间较短的情况

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class CachedThreadPoolCase {
    static class Demo implements Runnable {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            try {
//修改睡眠时间,模拟线程执行需要花费的时间
                Thread.sleep(100);
                System.out.println(name + "执行完了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) throws
            InterruptedException {
//创建一个缓存的线程,没有核心线程数,最大线程数为 Integer.MAX_VALUE
        ExecutorService exec =  Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            exec.execute(new Demo());
            Thread.sleep(1);
        }
        exec.shutdown();
    }
}

为什么不建议用Executors创建线程池

参考阿里开发手册《Java开发手册-嵩山版》

ThreadPoolExecutor

提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。

使用ThreadPoolExecutor的好处?

ThreadPoolExecutor准确的控制创建线程池的数量,最大等待数量,拒绝策略等。

适用场景:有定时和延迟执行的任务

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
​
public class ScheduledThreadPoolCase {
    static class Task implements Runnable {
        @Override
        public void run() {
            try {
                String name =
                        Thread.currentThread().getName();
                System.out.println(name + ", 开始:" + newDate());
                Thread.sleep(1000);
                System.out.println(name + ", 结束:" + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
​
    public static void main(String[] args) throws
            InterruptedException {
//按照周期执行的线程池,核心线程数为2,最大线程数为 Integer.MAX_VALUE
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
        System.out.println("程序开始:" + new Date());
/**
 * schedule 提交任务到线程池中
 * 第一个参数:提交的任务
 * 第二个参数:任务执行的延迟时间
 * 第三个参数:时间单位
 */
        scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);
        scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);
        Thread.sleep(5000);
// 关闭线程池
        scheduledThreadPool.shutdown();
    }
}

ThreadPoolExecutor的(核心参数)

参考ThreadPoolExecutor这个类的7个参数的构造方法

  • corePoolSize 核心线程数目(核心池子大小),创建线程池后,默认线程是0(先不创建线程),有任务来创建线程 ,之后就不销毁了存在线程池。也可以调用perstartAllCoreThreads()或者perstartCoreThreads()方法进行预创建线程。

  • maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目)

  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放

  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等

  • workQueue(等待队列) - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  • threadFactory 线程工厂 - 线程对象的创建,例如设置线程名字、是否是守护线程等

  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,就触发拒绝策略

执行原理

1.任务在提交的时候,先判断核心线程数是否已满,未满则添加到工作线程执行任务。

2.如果核心线程数满了,则判断阻塞队列是否已满,未满,则进入阻塞队列。

3.如果阻塞队列也满了,判断当前线程总数是否小于最大线程数,小于,则创建救急线程,也就是临时线程去执行任务,如果核心和临时线程执行完了,回去阻塞队列中找是否有要被执行的任务,有则去执行。

4.如果所有线程都在忙(核心线程+临时线程),则使用拒绝策略处理。

拒绝策略

1.AbortPolicy:直接抛出异常,默认策略;

2.CallerRunsPolicy:用调用者所在的线程来执行任务(main);

3.DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;

4.DiscardPolicy:直接丢弃任务;

execute 与 submit 的区别

执行任务除了可以使用 execute 方法还可以使用 submit 方法。它们的主要区别

是:execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返

回值的场景。

线程池中常见的阻塞队列

workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会

创建救急线程执行任务

常用的有:

1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。

2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。

ArrayBlockingQueueLinkedBlockingQueue区别

LinkedBlockingQueueArrayBlockingQueue
默认无界,支持有界强制有界
底层是列表底层是数组
是懒惰的,创建节点的时候添加数据提前初始化 Node 数组
入队会生成新 NodeNode需要是提前创建好的
两把锁(头尾)一把锁

左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方 式

LinkedBlockingQueue读和写各有一把锁,性能相对较好

ArrayBlockingQueue只有一把锁,读和写公用,性能相对于 LinkedBlockingQueue差一些

确定核心线程数

执行线程池执行任务的类型:

IO密集型任务:文件读写,DB读写,网络请求

推荐:核心线程数大小设置为2N+1 (N为计算机的CPU核数)

CPU密集型任务 :计算型代码、Bitmap转换、Gson转换等

推荐:核心线程数大小设置为N+1 (N为计算机的CPU核数)

java代码查看cpu核数:

 public static void main(String[] args) {
        System.out.println(Runtime.getRuntime().availableProcessors());
    }

① 高并发、任务执行时间短 -->( CPU核数+1 ),减少线程上下文的切换

② 并发不高、任务执行时间长

IO密集型的任务 --> (CPU核数 * 2 + 1)

计算密集型任务 --> ( CPU核数+1 )

③ 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整

体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器

是第二步。

CountDownLatch

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值

  • await() 用来等待计数归零

  • countDown() 用来让计数减一

关闭线程池

关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现。

shutdownNow:对正在执行的任务全部发出 interrupt(),停止执行,对还未开

始执行的任务全部取消,并且返回还没开始的任务列表。

shutdown:当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会

去强制终止已经提交或者正在执行中的任务。

ThreadLocal类

ThreadLocal(线程变量)是多线程中用于解决线程安全的一个操作类,他会为每个线程分配一个单独只属于当前线程的线程副本,从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享。

例:

使用jdbc操作数据库时,会将每一个线程的Connection放入各自的threadlocal中,从而保证每个线程都在各自的connection上进行数据库操作,避免a线程关闭b线程。

基本使用:

set(value)设置值,

get()获取设置的值

remove()清除值

ThreadLocal的实现原理

ThreadLocal本质就是一个线程内部的存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离。

在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象value,放入当前线程的 ThreadLocalMap 集合中

当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值

当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

ThreadLocal内存泄漏问题:

是应为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC回收,释放key,此时ThreadLocalMap中key=null,而value还强引用着,不会被回收,只有当前线程结束才会被回收。

解决:

在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,需要我们每次使用完 ThreadLocal 都调用它的 remove()方法清除数据。

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值