同步容器
1、同步容器的思路:隐藏状态、对象访问加锁
2、同步容器单个操作都是线程安全的,但是复合操作不是线程安全的,需要进行客户端枷锁来解决,即对容器自身加锁。同步容器
3、同步修改异常和迭代器。当使用iterator来迭代容器的时候,如果其他线程同时修改了这个容器,那么在调用hasNext,next等方法的时候会抛出这个异常,如果不是使用迭代器来迭代就不会出现这个问题。为了防止抛出这个异常,有2个方法,一个是在迭代期间对容器加锁,一个是复制一个副本到线程中。
并发容器
1、ConcurrenthashMap 是用来解决同步hashmap在并发环境下访问性能的低下而设计的。主要采用锁分段技术,细化加锁粒度,降低锁竞争。
2、CopyOnWriteArrayList、CopyOnWriteArraySet 采用CopyOnWrite的机制,读写分离的思路,当向容器里添加元素时copy一份,在新的容器里添加元素,最后在将原来的引用指向新的容器。也就是说容器一旦发布出去后,就是一个事实不可变的额,这就可以实现并发读的使用不需要加锁。添加的时候需要加锁,所以他的使用场景是读远远大于写的场景,比如事件通知系统、黑名单管理服务。并且需要注意,写时复制会占用更多的内容,可能会触发大量的youngGC和fullGC,并且只能保证最终一致性。
3、阻塞队列和生产者消费者模式,提供了put、take方法和超时的offer和poll方法,主要功能在于解耦。优先队列和同步队列
同步工具
同步工具的作用是可以根据自身的状态来协调不同线程的控制流。blockingqueue(阻塞队列)、semaphore(信号量)、barrier(栅栏)、latch(闭锁)。这些工具都分装了一些状态,并可以根据这些状态来决定执行同步工具的线程是等待还是执行
1、闭锁。初始化一个数量,countDown方法将数量减1,await方法将当前线程在闭锁上等待,当数量为0的时候锁打开。a、如果主线程await,多个子线程countdown,那么可以实现当子线程都全部就绪通知主线程继续。b、使用二元闭锁,如果主线程countdown,子线程await,可以实现当主线程相关工作准备就绪后通知所有子线程同时开始运行
2、FutureTask
也可以做闭锁,是当前线程等待执行callable的线程返回结果,否则一直等待
3、信号量 semaphore 用来控制同时访问某个资源的线程数量,初始化一个数量,acquire申请一个许可,release释放一个许可,如果没有许可可以用accquire将阻塞
4、栅栏
任务取消
0、可以使用自定义变量作为中断标志,然后轮询检查这个状态,问题是有可能在处理过程中发生阻塞后不能及时或者永远不能检查这个标志,导致线程不能退出,任务不能结束。
1、使用中断,中断是线程之间的一种协作机制
interrupt 中断一个线程,设置他的中断标志位
isInterrupted 返回目标线程的中断标志位
interrupted 清除当前线程的中断标志位
JVM在什么时候感知到中断标志为的变化?会不会和自定义变量一样发生严重延迟问题?
JVM不能保证阻塞的方法检测中断的速度,但在实际情况中响应速度是非常快的!!
JVM的关闭
jvm的关闭分为正常关闭和强行关闭。正常关闭包括所有线程(非守护)任务都执行完成并退出、使用System.exit、或者使用ctrl-c。强行关闭包括使用Runtime.halt(),在操作系统中使用kill -9
正常关闭中,jvm首先调用注册的关闭钩子 shutdown hook
饥饿死锁
一个任务等待另外一个任务完成,但是另外一个任务又在等第一个任务完成才能完成。
比如:在单线程线程池中,一个在线程池中的任务把另一个任务提交到当前线程池中,但是提交不进去阻塞,这样就发生了饥饿死锁
线程池
ThreadPoolExecutor 线程池,所有其他的线程池都是根据这个来配置的。
线程池本质上就是利用一些基础构建工具来实现的一种工具而已。线程池中包含两个数据结构:任务队列、工作者队列,提交的任务都是先放在任务队列中(同步队列除外),任务队列容纳worker对象。处理的时候线程不断的从任务队列中去取任务来执行。
线程池的5大参数:coreSize、maxSize、keepAlive、workqueue、threadFactory、饱和策略
其中coreSize、maxSize、keepAlive负责控制线程的创建和销毁
新请求到来:
当前线程数< coreSize ,创建新的线程来处理对象,即使还有其他的线程处于空闲状态
coreSize<当前线程数<maxSize , 仅仅当任务队列满的情况下才会创建新线程去处理
coreSize的含义是线程池的目标大小,线程池觉得coreSize这么多的线程已经可以处理queueSize中的所有任务了,所以如果线程数为coreSize,但是任务队列还没有满的时候是不会创建新线程的。
一个新任务来了,线程池有三种处理方式:1、新建线程立即处理,2、存到任务队列中,3、拒绝处理。新建线程有2种情况,一种是核心线程未满,一种是核心线程满了但未达到最大线程大小并且任务队列满了。
新建线程是怎么过程?
提交一个任务后,内部会将任务封装到Worker里面,然后把worker存到内部一个hashset里面(works),work是一个线程对象,他run里主要的逻辑就是不断的从任务队列中取任务来执行,这是一个无限循环。
线程退出的过程是怎么过程?
1、在超过keepAlive 且curSize>coreSize 情况下退出(正常退出)
2、线程抛出异常退出(非正常退出)
在这两种情况下分别做什么处理?如果线程池所有的线程都抛异常退出,那么是不是线程池就没有线程了?
如果是抛异常非正常退出,那么会在补一个线程进去
如果是正常退出,1、如果不允许核心线程超时,那么最少保留的线程就是coreSize,如果允许核心线程超时并且这是任务队列还有任务,那么最少保留一个线程,如果当前线程退出后小于最小线程数,那么会补一个线程,否则不会补。
我们知道线程池在构造完成后,可以通过setter来改变他的参数,如 coreSize、maxSize等等,如果对于SingleThread 单线程池来说在构造完成后,我们去改变他的线程数,不就可以让他变成非单线程池了么?单线程的语义在于让所有任务串行执行,那么这样一来不就打破原有的语义了么?
通过线程封闭来实现不可修改,即把ThreadPoolExecutor 实例作为另外一个类 FinalizableDelegatedExecutorService 的实例属性,对外是没有setter方法可以设置的。
newFixedThreadPool(1) 和 newSingleThreadExecutor() 都是一个线程,他们一样吗??
不一样,newSingleThreadExecutor 可以保证构造完成之后参数不会被修改,保证单线程串行执行语义
如果newSingleThreadExecutor中唯一的一个线程挂了之后怎么办?
和上边一样,单线程池coreSize是1,如果这个线程挂了,会在补充一个线程进去,从而保证始终只有一个线程在运行。
同样在线程空闲时间超过了keepAlive的情况下,1、coreSize<curSize<maxSize 线程就会退出,2、curSize<coreSize 线程不同退出,但这是如果实现的?
worker唯一的逻辑就是不停的从任务队列中取任务,执行任务,任务队列本身是个blockingqueue,当从blockingqueue中取任务的时候,poll 可以有超时返回的特性,take 具有阻塞的特性,正式利用这两个特性来实现两种情况下的处理。对于第一种情况,取任务的时候使用poll,超时的时间就是keepAlive,如果在keepAlive内没有渠道任务返回null,那么线程退出循环,这样就实现了在超过keepAlive的情况下线程退出。对于第二种情况,取任务的时候采用take的方式,一直阻塞直到有任务,这样即使超时也不会退出。
在默认情况下,新创建的线程池是没有线程的,只有请求到来的时候才会创建线程。也可以配置预分配coreSize的线程preStartCoreThreads()
加入coreSize=0,任务队列长度是10,那么只有当任务队列满的时候才开始新建线程,才开始执行任务。
线程池中使用的任务队列有3中:有界、无界、同步移交
当有界队列满的时候,会采取饱和策略,jdk提供了4中饱和策略:终止策略、调用者运行、抛弃、抛弃旧的。
几种常见的线程池内部实现。
FixedThreadPool:coreSize=maxSize,存活时间=0,任务队列使用的是无界队列 LinkedBlockingQueue。
CachedThreadPool:coreSize=0,maxSize=Integer.MAX_VALUE,keepAlive=60s,使用同步队列
SingleThread:coreSize=maxSize=1,keepAlive=1ms,使用无界队列LinkedBlockingQueue