@[多线程]
程序运行取决于CPU的执行
多线程的目的就是为了提高CPU的使用率
线程生命周期
1、new Thread()的方法新建一个线程,在线程创建完成之后,
线程就进入了就绪(Runnable)状态,进入抢占CPU资源的状态
2、线程抢到了CPU的执行权之后,线程就进入了运行状态(Running)
3、该线程的任务执行完成之后(非常态的调用的stop()方法)后,线程就进入了死亡状态。
4、以下几种情况的时候,容易造成线程阻塞:
1)线程主动调用了sleep()方法时,线程会进入则阻塞状态。
2)线程主动调用了阻塞时的IO方法时,该方法有一个返回参数,当参数返回之前,线程会进入阻塞状态
3)线程进入正在等待某个通知时,会进入阻塞状态
sleep()和wait()
调用对应的notify/signal方法(唤醒)
yield 让出一下CPU
java 线程模型
用户级线程 ULT
不需要用户态、内核态切换,速度快;
内核无感知,线程阻塞则进程阻塞
内核级线程 KLT
有内核上维护了线程表,具有并行处理能力
java线程创建是依赖于系统内核,通过JVM调用系统库创建内核线程, 内核线程于java-Thread是1:1的映射关系。
创建线程池?
----》减少线程创建、消亡所带来的开销,
且Java线程依赖于内核线程,创建线程需要进行操作系统状态切换
===》重用线程
--》线程池就是一个线程缓存,负责对线程进行统一分配、调优和监控。
什么时候用?
单个任务处理时间比较短
需要处理的任务数量很大
线程池优势?
重用存在的线程,减少线程创建、消亡的开销,提高性能。
提高响应速度,当任务到达时,任务可以无需等待线程创建就立即执行。
提高线程的可管理性,可统一分配、调优和监控。
线程池的五种状态
运行(RUNNING)----》接受任务,执行队列中的任务
关闭(SHUTDOWN)------》不接受新任务,但执行队列中的任务,执行钩子函数onShutdown()
停止(STOP)------>不接受新任务,也不执行队列中任务,打断正在执行的work线程
TIDYING------》当所有的任务均中断,work数量为0了,线程将逐渐收紧为TIDYING,
然后执行termnated()钩子函数
完全终止(termnated)----》当termnated钩子函数执行完毕,将转变状态为TERMnated
可以通过二进制的高3为判断是哪个状态。
Executor线程池超类接口。
线程池ThreadPoolExecutor
概念:事先创建若干个可执行的线程放入一个池(容器) 中,
需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,
从而减少创建和销毁线程对象的开销
(降低资源消耗,提高响应速度,提高线程可管理性)
Executors工具类中常用线程池
(1)newSingleThreadExecutor 多任务串行执行场景
创建一个单线程的线程池。
核心线程数=最大线程数=1
(2)newFixedThreadPool 执行长期任务,性能较好
创建固定大小的线程池,每提交一个任务创建一个线程,直到达到线程池最大
核心线程数 最大线程数 --》自定义
--》适用于负载较重的场景,对当前线程数量进行限制。
(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
(3)newCachedThreadPool 适用于执行很多短期异步的小程序或负载量小的任务
创建一个可缓存的线程池,大小依赖操作系统(JVM)能创建的最大线程数
核心线程数=0 最大线程数 无界 -》根据需要创建
--》适用于负载较轻的场景,执行短期异步任务。
(时间短,结束快,不会cpu过度切换。)
(4)newScheduledThreadPool
创建一个大小无限的线程池,支持定时/周期性执行任务需求。
内部实现均使用了ThreadPoolExecutor实现;
四个构造方法。
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
}
线程池参数:
courePoolSize
指定线程池线程核数。默认情况下,线程池中的线程数为0,
当有任务来之后,就会创建一个线程去执行任务。
当线程池的线程数达到corePoolSize后,就会把达到的任务放到缓存队列当中。
maximumPoolSize
线程池中最大线程数量
cpu密集型=cpu核数+1
IO密集型=CPU核数 / (1 - 阻塞系数) 阻塞系数在 0.8 ~ 0.9之间
keepAliveTime
线程池中的线程没有任务执行时最多或保留多久时间会终止。
默认情况下,只有当线程池中的线程数大于corePoolSize时,
keepAliveTime才会起作用,即超过corePoolSize的空闲线程,
在多长的时间内,会被销毁。
unit
参数keepAliveTime的时间单位
workQueue 推荐有界---无界一旦阻塞,占用内存资源。
一个阻塞任务队列,用来存储等待执行的任务。3种选择:
ArrayBlockingQueue; //使用较少 ,遵循FIFO原则
LinkedBlockingQueue; //经常使用,遵循FIFO原则
SynchronousQueue; //经常使用
PriorityBlockingQueue 优先级由任务的Comparator决定
threadFactory
线程工厂,主要用来创建线程,一般选择默认即可
handler
拒绝策略(都作为静态内部类在ThreadPoolExcutor中进行实现),
当任务太多时,如何拒绝任务,如下取值:
ThreadPoolExecutor.AbortPolicy
(默认)直接丢弃任务,抛出RejectedExecutionException异常,阻止系统工作
ThreadPoolExecutor.DiscardPolicy
丢弃任务,不予任何处理,不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy
丢弃最老的一个任务,即队列最前面的任务,
然后重新尝试执行任务,并重复此过程
ThreadPoolExecutor.CallerRunsPolicy
由调用线程处理该任务
为什么要加(阻塞)队列
1、因线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(非线程池缺点第 3 点)
2、创建线程池的消耗较高。(非线程池缺点第 1 点)
3、线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲
为什么先判断队列再最大线程数,而不是先最大再队列?
具体的源码实现有关。
当需要创建线程时,都会调用addWorker()方法,
会调用mainLock.lock()方法来获取全局锁,而获取锁就会造成一定的资源争抢。
前者呢 ,第1步判断核心线程数时要获取全局锁,第2步判断最大线程数时,又要获取全局锁,
这样相比于先判断任务队列是否已满,再判断最大线程数,就可能会多出一次获取全局锁的过程。
==结论:为了尽可能的避免因为获取全局锁而造成资源的争抢
LinkedBlockingQueue的吞吐量比ArrayBlockingQueue的吞吐量要高。
前者是基于链表实现的,后者是基于数组实现的,正常情况下,不应该是数组的性能要高于链表吗?
两个阻塞队列的源码才发现,
前者读和写操作使用了两个锁,takeLock和putLock,读写操作不会造成资源的争抢,
后者读和写使用的是同一把锁,读写操作存在锁的竞争。
线程池工作原理
线程池静态创建
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit. MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程池动态创建(阿里禁用Executors静态工厂构建线程池)
Executors是jdk提供的创建线程池的工厂类,默认4种,无需重构
ExecutorService executor = Executors.newCachedThreadPool();
(推荐)自定义调用ThreadPoolExecutor创建线程池
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
Executors的各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor: (底层实现队列无界)
主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM.
2)newCachedThreadPool和newScheduledThreadPool: (最大无界)
主要问题是线程数最多数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM.
线程池参数配置
1.任务的性质:CPU密集型任务(Ncpu+1个线程),IO密集型任务(2xNcpu)和混合型任务。
2.任务的优先级:高,中和低。
3.任务的执行时间:长,中和短。
4.任务的依赖性:是否依赖其他系统资源,如数据库连接。
countDownLatch
存在于java.util.cucurrent包下。
countDownLatch是在java1.5被引入,
一起被引入的工具类还有CyclicBarrier、Semaphore、concurrentHashMap和BlockingQueue。
**使一个线程等待其他线程各自执行完毕后再执行。**
是通过一个计数器来实现的,计数器的初始值是线程的数量。
每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,
表示所有线程都执行完毕,然后在闭锁上等待(阻塞队列)的线程就可以恢复工作
CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。 原子性的 。
await();//阻塞当前线程,将当前线程加入阻塞队列。
await(long timeout, TimeUnit unit);
//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,
countDown(); //对计数器进行递减1操作,当计数器递减至0时,
当前线程会去唤醒阻塞队列里的所有线程。
*CountDownLatch和CyclicBarrier区别:
1.countDownLatch是一个计数器,线程完成一个记录一个,
计数器递减,只能只用一次
2.CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,
然后继续执行,计数器递增,提供reset功能,可以多次使用
ThreadLocal
提供了线程本地变量,可保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,
每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。
核心机制:
每个Thread线程内部都有一个Map (ThreadLocalMap)
Map里面存储线程本地对象(key)和线程的变量副本(value)
内部类由由ThreadLocal维护的,负责向map获取和设置线程的变量值。
ThreadLocalMap
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,
用独立的方式实现了Map的功能,其内部的Entry也独立实现。
**Key是弱引用类型的**,Value并非弱引用(弱引用,生命周期只能**存活到下次GC前**)
==》就会有个问题:如果key被回收了,就存在一个null-value键值对,
这个value既无法被访问到,同时如果线程生命周期很长(比如线程池里),
那么这些null key的强引用关系:
Thread --> ThreadLocalMap-->Entry-->null--Value导致Value不会回收,造成**内存泄漏。**
===》解决:当调用set、get、remove方法的时候会去扫描key为null的Entry并清除(Entry=null)。
但是这个并不是100%保证不出问题,如果这个Entry过期了,
但是线程没有调用set、get或者remove,这个null key的Entry依然会存在,依然是内存泄漏了。
所以还是要规范,不用了就调用remove清除。
Hash冲突怎么解决:
非链表的方式,而是采用线性探测的方式。
据初始key的hashcode值确定元素在table数组中的位置,
如果发现这个位置上已经有其他key值的元素被占用,
则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
==》步长加1或减1,寻找下一个相邻的位置
---》大量不同ThreadLocal对象放入map中时发生冲突或二次冲突--》低效率。
从ThreadLocal的set方法说起,set是用来设置想要在线程本地的数据,可以看到先拿到当前线程,然后获取当前线程的ThreadLocalMap,如果map不存在先创建map,然后设置本地变量值。
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 尝试获取当前线程内部的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// map不为空,就正常set值
if (map != null)
map.set(this, value);
else
// 否则就初始化Map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
// 可以看出,ThreadLocalMap是存储在线程对象里的
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// new个ThreadLocalMap,key和value分别为当前ThreadLocal对象已经传入的值
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
阻塞
高并发
1.限流(接口)
**(1)漏桶算法**
短时间内有大量突发请求时,输出造成资源浪费。
(2)令牌桶算法
解决(1)的问题。允许平滑突发限流,平滑预热限流
2.分布式限流
redis 计数器 限流
百万查询优化
1.请求合并:类似于转化为批量查询--RequestQueue存储每个请求的唯一id
并发协同工具:new CountDownLatch(THREAD_NUM)
作用:利用countDownLatch.await() 阻塞
eg. 有1000个用户(多线程)查询商品,countDownLatch.await() 阻塞,等待countDownLatch为0 代表所有线程都start ,再运行后续代码