常见锁策略
乐观锁VS悲观锁
乐观锁:预测锁冲突的概率不高;
悲观锁:预测锁冲突的概率较高;
读写锁VS普通互斥锁
读写锁
加读锁
加写锁
同时读不会有线程安全问题,同时写、同时读和写都存在线程安全问题,读写锁对读和写区分加锁,相较于普通的互斥锁就会少了很多锁竞争,优化了效率。
普通互斥锁:例如synchronized,当两个线程竞争一把锁,就会产生等待。
重量级锁VS轻量级锁
重量级锁:加锁解锁开销比较大(进入内核态的加锁逻辑);
轻量级锁:加锁解锁开销较小(纯用户态的加锁逻辑)。
自旋锁VS挂起等待锁
自旋锁:是轻量级锁的一种典型实现;(同时也是一个乐观锁)
优点:持续等待,一旦锁被释放,第一时间就能获取锁
缺点:CPU占用率较高
挂起等待锁:是重量级锁的一种典型实现
优点:不占用资源,资源可以得到合理利用
缺点:获取锁时间不及时
公平锁VS非公平锁
公平锁:遵守先来后到获取锁;(实现公平锁需要引入额外的开销用来记录线程加锁的顺序)
非公平锁:不遵守先来后到,随机抢占。
可重入锁VS不可重入锁
可重入锁:针对同一个线程连续加锁两次不会死锁;
不可重入锁:针对同一个线程连续加锁两次会死锁。
synchronized
既是乐观锁也是悲观锁;
既是轻量级锁也是重量级锁;
乐观锁部分是基于自旋锁实现的,悲观锁部分是基于挂起等待锁实现的;
以上三种状态是根据锁竞争环境来看,初始时锁竞争不激烈则都是前者状态,如果锁竞争激烈就变为后者的状态。
不是读写锁,是普通互斥锁;
非公平锁;
可重入锁。(synchronized关键字锁都是可重入的)
CAS(compare and swap)
如何操作
内存中原数据、旧的预期值、需要修改的新值;
比较旧的预期值与原数据是否相等;
若相等则把需要修改的新值与原数据进行交换;
返回操作是否成功。
应用场景
实现原子类:
CAS通过比较交换进行循环,需需要使用轻量级锁就能完成多线程的自增工作,实现一个原子类。
实现自旋锁:
判断CAS是否进行交换,当返回操作为false时,会进行循环,立即再次发起判断
CAS的ABA问题
ABA问题:在CAS中,进行比较的时候,寄存器和内存的值相同,但是无法判断内存的值是始终没变还是变了又变回来了。(极端情况下出现的bug:付费时网络卡顿多操作两次,此时就会有两个线程进行扣钱,基于CAS来扣钱,此时另外一个人转账同样的钱,扣完款账户余额没有发生变化,因此会继续扣钱,导致一次付费扣两份的钱。)
解决办法:对内存数据变化进行记录,记录修改次数或上次修改时间,CAS与修改次数或上次修改时间进行比较就不会相同。
synchronized原理
加锁具体过程(锁升级/锁膨胀)
根据具体依次进行升级:
偏向锁(无竞争)
不是真正加锁,只是设置了一个状态。
类似于懒汉模式,必要的时候再加锁;如果不是发生锁竞争,就没必要真加锁。
轻量级锁(有竞争)
重量级锁(竞争激烈)
锁消除
JVM自动判定,如果代码不必加锁就会自动把锁去掉;
只有一个线程或者多个线程不修改一个变量,此时也会将锁去掉;
锁消除是一种编译优化行为,不是一定准确,因此不确定时不一定会消除。
锁粗化
锁的粒度:synchronized对应的代码块少(粒度细),多(粒度粗);
锁粗化:细粒度加锁->粗粒度加锁;
加锁解锁加锁解锁->加锁解锁;(加锁里面的代码变多了,对同一个对象加锁才能粗化)
锁粗化避免频繁加锁解锁形成的锁竞争。
JUC
Callable接口
类似于Runnable;
Runnable描述任务不带返回值,Callable描述任务带返回值;
Callable 通常需要搭配 FutureTask 来使用;
FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 执行完毕时间并不确定,FutureTask 负责这个等待结果出来的工作.
ReentrantLock
lock():加锁,获取不到一直等待;
unlock():解锁,容易遗忘;
相对于synchronized的优势:
tryLock():超时等待,等待一定的时间就放弃;
可以实现公平锁,进行简单传参就变为公平锁:
![](https://i-blog.csdnimg.cn/blog_migrate/39ac88f99aabbc7904bdfaf3bff7e83d.png)
synchronized是搭配wait/notify实现等待通知机制,唤醒是随机唤醒一个线程;reentrantLock搭配Condition类实现,唤醒操作时可以指定唤醒哪个线程的。
原子类
Java中封装好的,可以直接使用;
使用场景多为多线程计数,可以通过原子变量进行累加。
线程池(前面写道过)
Semaphore信号量
信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器;
信号量是更广义的锁,所示特殊的信号量(可用资源只有1的信号量);
当需求中有多个可用资源就可以使用信号量。
CountDownLatch
构造 CountDownLatch 实例, 初始化 10就表示有 10 个任务需要完成;
每个任务执行完毕, 调用 latch.countDown();
在 CountDownLatch 内部的计数器同时自减;
主线程中使用 latch.await(); 阻塞等待所有任务执行完毕。(相当于计数器为 0)