千变万化的锁
-
Lock接口
-
锁的分类
-
乐观锁和悲观锁
-
可重入锁和非可重入锁,以ReentrantLock为例
-
公平锁和非公平锁
-
共享锁和排他锁:以ReentrantReadWriteLock读写锁为例
-
自旋锁和阻塞锁
-
可中断锁:顾名思义,就是可以响应中断的锁
-
锁优化
Lock接口
-
简介、地位、作用
-
为什么synchronized不够用?为什么需要Lock?
-
方法介绍
-
可见性保证
简介、地位、作用
锁是一种工具,用于控制对共享资源的访问。
Lock和synchronized,这两个是最常见的锁,它们都可以达到线程安全的目的,但是在使用上和功能上又
有较大的不同。
Lock并不是用来代替synchronizedi的,而是当使用synchronized不合适或不足以满足要求的时候,来
提供高级功能的。
为什么synchronized不够用?
-
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
-
不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是
不够的
- 无法知道是否成功获取到锁
Lock主要方法介绍
在Lock中声明了四个方法来获取锁
lock()、tryLock0、tryLock(long time,TimeUnit unit)和lockInterruptibly()
那么这四个方法有何区别呢?
Lock()
lock()就是最普通的获取锁。如果锁已被其他线程获取,则进行等待
Lock不会像synchronized一样在异常时自动释
放锁
因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定被释放
lock()方法不能被中断,这会带来很大的隐患:一旦陷入死锁
Iock()就会陷入永久等待、
tryLock()
tryLock()用来尝试获取锁,如果当前锁没有被其他线程占用
则获取成功,则返回true,否则返回false,代表获取锁失败
相比于引ock,这样的方法显然功能更强大了,我们可以根据
是否能获取到锁来决定后续程序的行为
该方法会立即返回,即便在拿不到锁时不会一直在那等
tryLock(long time, TimeUnit unit)
添加了超时时间
lockInterruptibly()
相当于tryLock(long time, TimeUnit unit)把超时时间设置为无限。在等待锁的过程中,线程可以被中断
可见性保证
-
可见性
-
happens-before
-
Lock的加解锁和synchronized有同样的内存语义,也就是说下一个线程加锁后可以看到所有前一
-
个线程解锁前发生的所有操作
锁的分类
-
这些分类,是从各种不同角度出发去看的
-
这些分类并不是互斥的,也就是多个类型可以并存:有可能一个锁同时属于两种类型
-
比如ReentrantLock既是互斥锁,又是可重入锁
为什么会诞生互斥同步锁
互斥同步锁的劣势
-
阻塞和唤醒带来的性能劣势
-
永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行
-
优先级反转
悲观锁
如果我不锁住这个资源,别人就会来争抢,就会造成数据结果错误,所以每次悲观锁为了确保结果的正
确性,会在每次获取并修改数据时,把数据锁住,让别人无法访问该数据,这样就可以确保数据内容万
无一失
Java中悲观锁的实现就是synchronized和Lock相关类
乐观锁
认为自己在处理操作的时候不会有其他线程来干扰,所以并不会锁住被操作对象
在更新的时候,去对比在我修改的期间数据有没有被其他人改变过如果没被改变过,就说明真的是只有
我自己在操作,那我就正常去修改数据
如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那我就不能继续刚才的更新
数据过程了,我会选择放弃报错、重试等策略
乐观锁的实现一般都是利用CAS算法来实现的
开销对比
-
悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对 互斥锁的开销造成影响
-
相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资 源也会越来越多
使用场景
悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自
旋等消耗,典型情况:
-
临界区有IO操作
-
临界区代码复杂或循环量大
-
临界区竞争非常激烈
乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高
ReetrantLock使用案例
普通方法1:预定电影院座位
普通方法2:打印字符串
ReentrantLock重入原理
当前线程想多次获取锁,可以多次获取,通过计数来计算获取次数。
公平锁和非公平锁
-
公平指的是按照线程请求的顺序,来分配锁;非公平指的是不完全按照请求的顺序,在一定情况 下,可以插队。
-
注意:非公平也同样不提倡“”插队”行为,这里的非公平,指的是“在合适的时机”插队,而不是盲目 插队。
-
什么是合适的时机呢?
-
实际情况并不是这样的,Java设计者这样设计的目的,是为了提高效率
-
避免唤醒带来的空档期
公平锁和非公平锁的优缺点
共享锁和排他锁
-
排他锁,又称为独占锁、独享锁
-
共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据,其他线程此时也可以获
-
取到共享锁,也可以查看但无法修改和删除数据
-
共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁
读写锁的作用
-
在没有读写锁之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一
-
定的资源:多个读操作同时进行,并没有线程安全问题
-
在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的提
-
高了程序的执行效率
读写锁规则
-
多个线程只申请读锁,都可以申请到
-
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待 释放读锁。
-
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等 待释放写锁。
-
一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现 (要么多读,要多一写)
换一种思路更容易理解:读写锁只是一把锁,可以通过两种方式锁定读锁定和写锁定。读写锁可以同时
被一个或多个线程读锁定,也可以被单一线程写锁定。但是永远不能同时对这把锁进行读锁定和写锁
定。
这里是把“获取写锁”理解为“把读写锁进行写锁定”,相当于是换了一种思路,不过原则是不变的,就是要
么是一个或多个线程同时有读锁(同时读锁定),要么是一个线程有写锁(进行写锁定),但是两者不
会同时出现
读写锁插队策略
公平锁:不允许插队
非公平锁
-
写锁可以随时插队
-
读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队
锁的升降级
-
为什么需要升降级
-
支持锁的降级,不支持升级
-
为什么不支持升级?死锁
共享锁和排它锁总结
- ReentrantReadWriteLock:实现了ReadWriteLock接口,最主要的有两个方法:readLockO和
writeLock(用来获取读锁和写锁
-
锁申请和释放策略
-
多个线程只申请读锁,都可以申请到
-
如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直
等待释放读锁。
-
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一
直等待释放写锁。
-
要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现。
-
-
插队策略:为了防止饥饿,读锁不能插队
-
升降级策略:只能降级,不能升级
-
适用场合:相比于ReentrantLock适用于一般场合ReentrantReadWriteLock适用于读多写少的情
况,合理使用可以进一步提高并发效率。
总结:要么多读,要多一写
自旋锁和阻塞锁
-
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
-
如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长
-
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的 花费可能会让系统得不偿失
-
如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求 锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁
-
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋如果在自旋完成后前面锁定同步资源的 线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开 销。这就是自旋锁。
-
阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,知道被唤醒
自旋锁的缺点
-
如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源
-
在自旋的过程中,一直消耗cpu,所以虽然自旋锁的起始开销低于悲观锁,但是随着自旋时间的增 长,开销也是线性增长的
自旋锁的适用场景
-
自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
-
另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大*(线程一旦拿到锁,很久以后 才会释放),不太合适
可中断锁
-
在Java中,synchronized就不是可中断锁,而lock是可中断锁因为tryLock(time)和 lockInterruptibly都能响应中断。
-
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁可能由于等待时间过长,线程B 不想等待了,想先处理其他事情我们可以中断它,这种就是可中断锁
锁优化
JVM对锁的优化
-
自旋锁和自适应
-
锁消除
-
锁粗化
自己优化策略
-
缩小同步代码快
-
尽量不要锁住方法
-
减少请求锁的次数
-
避免人为制造”热点“
-
锁中尽量不要再包含锁
-
选择合适的锁类型或合适的工具类