Java线程池原理 3个常用方式 7大参数 4种拒绝策略以及线程的实现方式

在讨论线程池的问题之前先说一个面试重点问题synchronized和Lock的区别:
    1 synchronized是关键字加在方法和代码块上,Lock是接口,有自己的实现类;
    2 synchronized不需要手动释放锁,当代码执行完毕的时候自动会将锁释放掉,而Lock需要有unLock的过程;
    3 synchronized是非公平锁,Lock可以是公平锁也可以是非公平锁,默认是非公平的;
    4 锁可以通过绑定多个condition,来分组唤醒线程;

1 线程的几种实现方式

    Java多线程实现方式主要有四种:继承Thread类、实现Runnable接口、实现Callable接口。

1.1 继承Thread类创建线程

public class MyThread extends Thread {  
//重写run()
   public void run() {  
       System.out.println("MyThread.run()");  
   }  
}  
//创建线程 调用start()方法启动
MyThread myThread1 = new MyThread();             myThread1.start();  
MyThread myThread2 = new MyThread();             myThread2.start();  

1.2 实现Runnable接口创建线程

public class MyThread extends OtherClass implements Runnable {  
	//重写run()方法
    public void run() {  
        System.out.println("MyThread.run()");  
    }  
}  

MyThread myThread = new MyThread();        
Thread thread = new Thread(myThread);        
thread.start();  

1.3 实现Callable接口通过FutureTask包装器来创建Thread线程

public interface Callable<V>   { 
    V call() throws Exception;   
}  
public class SomeCallable<V> extends OtherClass implements Callable<V> {
    @Override
    public V call() throws Exception {
        return null;
    }
} 
Callable<V> oneCallable = new SomeCallable<V>();   
//使用Callable<Integer>创建一个FutureTask<Integer>对象:   
FutureTask<V> oneTask = new FutureTask<V>(oneCallable);    
//注释:FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。 
//由FutureTask<Integer>创建一个Thread对象:   
Thread oneThread = new Thread(oneTask);       
oneThread.start();   

1.4 方法对比

实现Runnable接口比继承Thread类所具有的优势:
    1 适合多个相同的程序代码的线程去处理同一个资源
    2 可以避免java中的单继承的限制
    3 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
    实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的

2 线程池

2.1 优势

与各种池技术的优势基本相同。
    (1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
    (2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
    (3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。

2.2 线程池工作原理

2.3 3种实现方式

    为啥不能使用Exectors创建线程池?首先Excutors会很耗系统资源,线程最大值比较大,或者堆积的请求队列对系统的系统资源的消耗,容易出现OOM,那么如何创建呢?使用ThreadPoolExecutor。
这部分认真学习一下《并发编程的艺术》第十章

2.3.1 FixedThreadPool
2.3.2 SingleThreadExecutor
2.3.3 CachedThreadPool

2.4 7 大参数与四种拒绝策略

1 corePoolSize 线程池核心线程大小
    线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会 被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
2 maximumPoolSize 线程池最大线程数量
    一个任务被提交到线程池后,首先会缓存到工作队列(后面会介绍)中,如果工作队列满了,则会创建一个新线程,然后从工作队列中的取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize来指定。
3 keepAliveTime 空闲线程存活时间
    一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
4 unit 空间线程存活时间单位
    keepAliveTime的计量单位
5 workQueue 工作队列
    新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
    ①ArrayBlockingQueue
    基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
    ②LinkedBlockingQuene
    基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
    ③SynchronousQuene
    一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
    ④PriorityBlockingQueue
    具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
6 threadFactory 线程工
    创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
7 handler 拒绝策略
    当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
    ①CallerRunsPolicy
    该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
    ②AbortPolicy
    该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
    ③DiscardPolicy
    该策略下,直接丢弃任务,什么都不做。
    ④DiscardOldestPolicy
    该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列

2.5 合理地配置线程池

要想合理的配置线程池的大小,首先得分析任务的特性,可以从以下几个角度分析:

  1. 任务的性质:CPU密集型任务、IO密集型任务、混合型任务。

  2. 对于不同性质的任务来说,CPU密集型任务应配置尽可能小的线程,如配置CPU个数+1的线程数,IO密集型任务应配置尽可能多的线程,因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,如配置两倍CPU个数+1,而对于混合型的任务,如果可以拆分,拆分成IO密集型和CPU密集型分别处理,前提是两者运行的时间是差不多的,如果处理时间相差很大,则没必要拆分了。

  3. 任务的优先级:高、中、低。
    优先级不同的任务可以采用优先级队列来处理

  4. 任务的执行时间:长、中、短。
    执行时间不同的任务可以交给不同的线程池来处理,或者使用优先级队列,进行短作业优先

  5. 任务的依赖性:是否依赖其他系统资源,如数据库连接等。
    若任务对其他系统资源有依赖,如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值