java.util.concurrent包系列文章
JUC—ThreadLocal源码解析(JDK13)
JUC—ThreadPoolExecutor线程池源码解析(JDK13)
JUC—各种锁(JDK13)
JUC—原子类Atomic*.java源码解析(JDK13)
JUC—CAS源码解析(JDK13)
JUC—ConcurrentHashMap源码解析(JDK13)
JUC—CopyOnWriteArrayList源码解析(JDK13)
JUC—并发队列源码解析(JDK13)
JUC—多线程下控制并发流程(JDK13)
JUC—AbstractQueuedSynchronizer解析(JDK13)
本篇偏概念性,附带部分源码。
常用的锁
常用的2种加锁方式synchronized和Lock。
synchronized的缺点
- 效率低,试图获得锁时不能设定超时,不能中断一个正在试图获得锁的线程
- 不够灵活,加锁和释放的时机单一,每个锁仅有单一的条件(某个对象)
- 无法知道是否成功获取到锁
Lock作为synchronized的一种补充,他们都是可重入锁。
Lock的主要方法
- lock():获取锁,如果锁被其他线程获取,则等待。lock()不会像synchronized一样在异常时自动释放锁,必须在finally中释放锁。lock()方法不能被中断,一旦陷入死锁,lock()就会陷入永久等待
- tryLock():尝试获取锁,如果当前锁没有被其他线程占用,则获取成功,则返回true,否则返回false,代表锁获取失败。不等待,立刻返回
- tryLock(long time,TimeUnit unit):超时就放弃
- lockInterruptibly():相当于tryLock(long time,TimeUnitunit)把超时时间设置为无限。在等待锁的过程中可以被中断。 unlock():必须在finally中释放锁
锁的分类
互斥同步锁(悲观锁),
会锁住被操作的对象
缺点
- 阻塞和唤醒带来的性能劣势
- 可能会陷入永久阻塞,死锁
例子
- Java中悲观锁的实现就是synchronized和Lock相关类
- 数据库中selec for update也是悲观锁
适合于并发写很多的情况,适用于临界区持锁有时间比较长的情况,可以避免大量的无用自旋操作
非互斥同步锁(乐观锁)
不会锁住被操作的对象,在更新时会去检查我修改期间数据有没有被其他线程修改过,如果没有被修改过,就正常修改数据,如果发现数据被修改过,就会选择放弃,重试,报错等策略。乐观锁的实现一般都是使用CAS实现的。
缺点
- 可能会导致大量的无用的自旋操作,消耗CPU资源
例子
- Java中乐观锁的实现就是原子类和并发容器等
- 数据库中version来控制更新就是乐观锁
适合于并发写入少,读很多的情况,不加锁能让读取性能大幅提高
可重入锁
同一个线程可以多次获取同一把锁,synchronized和ReentrantLock都是可重入锁。state次数+1。
公平锁与非公平锁
公平是指按照线程请求的顺序来分配锁,非公平是指不完全按照请求的顺序分配锁,在一定情况下,可以插队。设计非公平锁是为了提高效率,避免唤醒线程带来的空档期。把已经挂起的线程唤醒的那段时间是有开销的。如果是公平的,这段时间谁都没办法拿到锁。ReentrantLock默认非公平锁。
- tryLock():本身是自带插队熟悉的。即时已经等待队列中已经有其他线程了。
公平锁在获取锁之前会判断有没有线程在队列中
非公平锁不管有没有线程在队列中都直接尝试去获取锁
共享锁和排他锁
- 排他锁:又叫独占锁,独享锁。获取锁之后可以做修改查询。
- 共享锁:又叫读锁。可以查看但是不能修改数据。
ReentrantReadWriteLock
读写锁的规则
- 多个线程可以同时获取读锁
- 如果一个先获得了读锁,其他线程无法申请写锁,会等待。
- 如果一个线程获取了写锁,其他线程都不能获取写,读锁。
- 要么一个多个读。要么一个写。多读一写。
读写锁插队策略
公平锁:不允许插队
非公平锁:
- 写锁随时插队读
- 锁仅在等待队列头节点不是想获取写锁的线程的时候可以插队
公平锁的情况下读写都需要判断队列是否有等待线程
非公平锁
写线程不关心等待队列是否有线程在等待,可以插队。
读线程需要判断等待队列头结点是否是排他锁(读锁)。
锁的升降级
支持锁的降级不支持升级,线程拿到写锁正在执行,中途需要执行某一个读锁锁定的方法,是可以的。不用先释放写锁,再去尝试获取这个读锁。提高了整体效率。在持有写锁的同时获取读锁。
如果支持升级的话容易造成死锁。两个线程都持有读锁,同时又都升级写锁,都要等待对方释放读锁,就会造成死锁。
自旋锁和阻塞锁
- 自旋锁:阻塞和唤醒一个线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。让线程自旋,如果在自旋完成后前面锁定同步资源的线程 已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。但是如果长时间自旋,也会耗费CPU资源。原子类Atomic*,就是利用无限循环加上CAS实现自旋。
- 阻塞锁:如果没拿到锁,会直接把线程阻塞,直到被唤醒