互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;
同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。
相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。
不会出现死锁
注意活锁和饥饿问题
无锁方案的实现原理
硬件支持-CPU为了解决并发问题,提供了CAS指令。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的。
CAS的使用一般都伴随着自旋
ABA问题
解决方案
理论上增加时间戳或者版本号都可以实现,JDK采用的是使用版本号。
单纯的累加操作,使用累加器相对原子化的基本类型,性能更高
TODO实操
22 | Executor与线程池:如何创建正确的线程池?线程池是一种生产者 - 消费者模式
线程池的使用方式 是 生产者
线程池本身是消费者
ThreadPoolExecutorThreadPoolExecutor( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize 核心池大小
表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。
当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
核心线程会一直存活,即使没有任务需要执行
设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
maximumPoolSize 最大池
表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。
keepAliveTime & unit 空闲线程存活的时间
上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。
workQueue 工作队列
工作队列,当线程核心池已满,接下来来的任务就会被放到工作队列(阻塞队列)
最好不要用无界队列,一旦业务量增加很容易OOM
threadFactory
通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。(PS:一般给线程池名字加前缀用这个方法)
handler 拒绝策略
当工作队列和线程池都满了,那么此时提交任务,线程池就会拒绝接收
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException
DiscardPolicy:直接丢弃任务,没有任何异常抛出
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
实现 RejectedExecutionHandler 接口 自定义拒绝策略,如果处理的任务不允许丢失,则可以和降级策略配合使用。
可以放数据库,放mq,redis,本地文件都可以,具体要看实际需求。
重点
注意事项
不要使用无界工作队列 最好不要使用 无界队列 因为业务量的突然增加很容易导致OOM
默认拒绝策略要慎重使用 实际开发任务中可以配合MQ和服务降级处理