## 线程池
普通线程执行完,就会进入TERMINATED销毁掉,而线程池就是创建一个缓冲池存放线程,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等候下次任务来临,这使得线程池比手动创建线程有着更多的优势:
* 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
* 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
* 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM
* 节省cpu切换线程的时间成本(需要保持当前执行线程的现场,并恢复要执行线程的现场)。
* 提供更强大的功能,延时定时线程池。(Timer vs ScheduledThreadPoolExecutor)
## 锁
#### 乐观锁/悲观锁
乐观锁:很乐观的认为每次读取数据的时候总是没有人动过的,所以不去加锁。但是在更新的时候回去对比一下原来的值,看看有没有被别人改过。适用于读多写少的场景
java的atomic属于乐观锁实现,即CAS
悲观锁:在每次读取的时候都认为其他人会修改数据,所以读取数据的时候也加锁,这样别人想拿的时候就会阻塞,知道这个线程释放锁,会影响并发性能。适合写操作较多的场景。
synchronized是悲观锁
#### 独享锁/共享锁
独享锁(ReentrantLock):只能被一个线程持有。与悲观锁接近。
共享锁(ReadWriteLock): read共享,write独享。多个线程都可拿到锁。
#### 分段锁
HashMap 是线程不安全的,在多线程环境下,使用HashMap进行put操作时,可能会引起死循环,导致cpu接近100%,所以在并发情况下不能使用HashMap。于是有了HashTable,HashTable线程是安全的,但是策略不高明,将get/put等所有相关操作都弄成了synchronized。
ConcurrentHashMap 使用分段锁,将数据分成一段一段存储,每一段一把锁,每一个segment元素存储的是HashEntry数组+链表,这个和HashMap数据存储结构一样。当访问其中一一段数据被某个线程加锁的时候,其他段的数据也能被访问,这样就试得ConcurrentHashMap不仅保证了线程安全,还提高了性能。
ps:
在1.8之后抛弃了原来的Segment分段锁,而采用是CAS+synchronized来保证安全性。
#### 可重入锁(必须同线程内)
获取到锁之后,如果同步块内需要再次获取到同一把锁,直接放行,而不是等待。其意义在于防止死锁。synchronized和ReentrantLock都是可重入。
实现原理:为每个锁关联一个请求计数器和他占有的线程。如果同一线程请求这个锁,计数器将递增,线程退出同步模块,计数器值将递减。直到计数器为0.
场景: 父类和子类的锁重入(调用super方法),以及多个加锁方法的嵌套调用。
#### 公平锁/非公平锁
常见于AQS,公平锁就是在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,获取当前线程是等待队列的第一个,就占有锁,否则就加入等待队列,知道FIFO的规则从列队中取到自己。
非公平锁与公平锁类似,只是放入列队前先判断当前锁释放被线程持有。如果锁空闲,那么他可以直接抢占,而不需要判断列队中是否有等待线程,所有锁被占用才会进入排队。
优缺点
公平锁的优点是等待锁的线程不会饿死,进入列队排队,迟早会轮到,缺点是整体的吞吐效率相对非公平要低,等待列队中第一个除第一个线程其他线程都会阻塞,CPU唤醒阻塞线程开销较大。公平锁使用 new ReentrantLock(true)。
非公平锁性能高于公平锁,但因为线程有纪律不发生阻塞直接获得锁。ReentrantLock默认使用非公平锁就是基于性能考量。但是非公平锁的缺点是可能导致列队中的线程拿不到锁,一直被排队饿死。
#### 锁升级
java中每个对象都可作为锁,锁有四种级别,按照量级分为:无锁,偏向锁,轻量锁,重量锁。
如何理解呢?A占了锁,B就是阻塞等待。但是在操作系统中,阻塞就要存储当前的线程状态,唤醒就需要再恢复,这个过程要消耗时间。
如果A使用锁的时间小于B被阻塞和挂起的执行时间,我们认为将B挂起不合算。于是出现自旋锁,当锁被其他线程占用时,当前线程不会被挂起,二十在不停的试图获取锁(可以理解为不停的循环)
##### 锁升级举例
例如公司只有一个会议室(共享资源)
偏向锁: 前期公司只有一个团队,那么开会不需要预约,直接使用
轻量级锁: 业务扩展为2个团队,开会时,两者竞争,在一个团队在开会时候,另外一个会不停的进行询问(自旋锁)
重量级锁: A开会时间比较长,B会被挂起。
#### 互斥锁/读写锁
典型的互斥锁: synchronized, ReentrantLock, 读写锁: ReadWriteLock。
互斥锁属于独享锁,独写锁的写数据独享锁,读属于共享锁。
## Fork/Join 概念
ForkJoin是由JDK1.7后提供多线程并发处理框架。ForkJoinPool由Java大师DougLea主持编写,处理逻辑分为两部。
1, 任务分割:fork(分叉),先把任务分成足够小的子任务,如果子任务比较大的话,还要切割。
2,结果合并:join,分割后的子任务被多个线程执行后,在合并结果,得到最终的完整输出。
## 并发容器选择
* 案例一:电商网站中记录一次活动下的各个商品售卖的数量
场景分析: 需要频繁的按照商品的id做get和set,但是商品的id的数量相对稳定,不会频繁的增删。
初级方案:选用HashMap,key为商品id,value为商品购买的次数。每次下单取出次数,增加后再写入。
问题:HashMap线程不安全!在多次商品的id写入后,如果发生扩容,在jdk1.7之前,在并发场景下HashMap会出来死循环,从而导致CPU使用率居高不下,jdk8修复了HashMap扩容导致的死循环问题,但是在高并发场景下,依然会有数据丢失以及不准确的情况出现。
选型:Hashtable不推荐,锁太重。选ConcurrentHashMap确保高并发下的线程安全性。
* 案例二:在一次活动下,为每个用户记录流浪商品的历史跟次数
场景分析:每个用户各自浏览的商品量级非常大,并且每次访问都要更新次数,频繁读写。
初级方案:为确保线程安全,采用上面的思路,ConcurrentHashMap
问题:ConcurrentHashMap内部机制在处理数据量大时,会把链表转换为红黑树。而红黑树在高并发下,删除和插入的过程中有个平衡过程,会牵扯到大量节点,因此竞争锁资源的代价相对较高
选型:用跳表,ConcurrentSkipListMap 将key分层,逐个切段,增删效率高于ConcurrentHashMap
结论: 如果对数据有强一致要求,使用HashTable;在大部分场景通常都是弱一致性的情况下,ConcurrentHashMap即可;如果数据量级很高,且存在大量的数据增删改操作,则可以考虑使用ConcurrentSkipListMap(跳表);
* 案例三:在活动中,创建一个用户列表,记录冻结用户。一旦冻结,不允许下单抢购,但是可以流浪。
场景分析:违规被冻结的用户不会太多,但绝大多数非冻结的用户每次抢单豆要去查一下这个表。低写,高读。
初级方案:ArrayList记录要冻结的用户id。
问题:ArrayList对冻结用户的id插入和读取操作在高并发下,会线程不安全。Vector可以做到线程安全,但并发性能太差,锁台中。
选型:综合业务场景,选CopyOnWriteArrayList,会占空间,但是也仅仅发生在添加新冻结用户的时候。绝大多数访问在非冻结用户的读取和比对上,不会阻塞。
## 上下文切换
### 锁竞争
类锁 < 静态锁 < 方法锁 < 代码块锁,能共享锁的帝防尽量不要独享。
### wait/notify
wait 让出锁
notify 唤醒其他线程
yield 让出cpu,但不让出锁
### 死锁
死锁排查工具
* jps + jstack pid
* jconsole
* jvisualvm
### 饥饿线程
产生原因:
高优先级线程吞噬所有低优先级cpu时间
锁始终被其他线程抢占
解决方案:
保证资源充足
避免持有锁的线程长时间执行,设置一定的推出机制
在高风险地方使用公平锁
java锁
最新推荐文章于 2024-08-29 18:44:48 发布