前面文章讲述了线程的部分基本知识,这篇是对线程的深入学习,包含线程池,实现框架等。
1.学习如何使用Executor框架创建线程池。
2.并发工具类如CountDownLatch、CyclicBarrier等。
3.线程安全和并发集合:
4.学习如何使用Java提供的线程安全的集合类,如ConcurrentHashMap、CopyOnWriteArrayList等。
原子类和CAS操作:
线程池简单来讲就是管理线程的一个池子。
主要有四种,都是通过工具类execute创建出来的
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory
threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
线程池特点:
工作流程:
使用场景:
FixedThreadPool:适用于处理Cpu密集型的任务,确保CPU在长期被保护的情况下,尽可能少的分配线程,即使用执行长期的任务。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory){
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
(BlockingQueue<Runnable>) new SynchronizedQueue<Runnable>(),threadFactory);
}
线程池特点:
工作流程:
适用场景为并发量大的大量短期任务。
public static ExecutorService newSingleThreadPool(ThreadFactory threadFactory){
return Executors.newSingleThreadExecutor(threadFactory);
}
线程池特点:
工作流程:
适用场景:适用执行串行的任务场景,一个接一个的执行
public ScheduledThreadPoolExecutor(int corePoolSize)
{ super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
线程池特点:
工作机制:
使用场景:
周期性执行任务的场景,需要限制最大的线程数量的场景。
线程池有7大参数重点关注:corePoolSize、maximumPoolSize,keepAliveTime、unit、workQueud、threadFactory、handler
- ArrayBlockingQueue FIFO有界阻塞队列基于数组的有界阻塞队列,特点FIFO(先进先出)。 当线程池中已经存在最大数量的线程时候,再请求新的任务,这时就会将任务加入工作队列的队尾,一旦有空闲线程,就会取出队头执行任务。因为是基于数组的有界阻塞队列,所以可以避免系统资源的耗尽。
那么如果出现有界队列已满,最大数量的所有线程都处于执行状态,这时又有新的任务请求,怎么办呢? 这时候会采用Handler拒绝策略
2.LinkedBlockingQueue FIFO无限队列 基于链表的无界阻塞队列,默认最大容量Integer.MAX_VALUE( ),可认为是无限队列,特点FIFO。
关于maximumPoolSize参数在工作队列为LinkedBlockingQueue时候,是否起作用这个问题,我们需要视情况而定!
情况①:如果指定了工作队列大小,比如core=2,max=3,workQueue=2,任务数task=5,这种情况的最大线程数量的限制是有效的。
情况②:如果工作队列大小默认,这时maximumPoolSize不起作用,因为新请求的任务一直可以加到队列中。
3.PriorityBlockingQueue VIP 优先级无界阻塞队列,前面两种工作队列特点都是FIFO,而优先级阻塞队列可以通过参数Comparator实现对任务进行排序,不按照FIFO执行。
4、SynchronousQueue不缓存任务的阻塞队列 不缓存任务的阻塞队列,它实际上不是真正的队列,因为它没有提供存储任务的空间。生产者一个任务请求到来,会直接执行,也就是说这种队列在消费者充足的情况下更加适合。因为这种队列没有存储能力,所以只有当另一个线程(消费者)准备好工作,put(入队)和take(出队)方法才不会是阻塞状态。
以上四种工作队列,跟线程池结合就是一种生产者-消费者
设计模式。生产者把新任务加入工作队列,消费者从队列取出任务消费,BlockingQueue可以使用任意数量的生产者和消费者,这样实现了解耦,简化了设计。
原文链接:https://blog.csdn.net/ZGL_cyy/article/details/118230264
守护线程(daemon Thread)再java中主要分为两类,:用户线程,守护线程
所谓守护是指再程序运行中提供的一种通用服务的线程,比如垃圾回收线程,并且这种线程并不属于线程中不可获缺的部分,因此所有的非守护线程结束的时候,程序也就终止了, 同时也会杀死进程中所有的守护线程,反而言之就是存在非守护线程那么程序就不会终止。用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。
因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
(1)thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
可分为:CPU密集型任务,IO密集型任务,混合型任务
任务的执行时长。
任务是否有依赖——依赖其他系统资源,如数据库连接等。
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。
因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
IO密集型任务
可以使用稍大的线程池,一般为2CPU核心数+1。
因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失
依赖其他资源
如某个任务依赖数据库的连接返回的结果,这时候等待的时间越长,则CPU空闲的时间越长,那么线程数量应设置得越大,才能更好的利用CPU。
借鉴别人的文章 对线程池大小的估算公式:
最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 ) CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
可以出一个结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
原文链接:https://blog.csdn.net/ZGL_cyy/article/details/118230264