这些分类是从不同的角度出发去看的,并不互斥,也就是多个类型可以并存。比如ReentrantLock既是互斥锁又是可重入锁。
乐观锁和悲观锁
-
互斥同步锁的劣势
- 阻塞和唤醒带来的性能劣势:比如操作系统用户态和内核态的切换,上下文的切换。而乐观锁不需要把线程挂起,自然就没有这些影响。
- 永久阻塞:如果持有锁的线程被永久阻塞,那么等待该线程释放锁的那几个线程将永远也得不到执行。
- 优先级反转
-
什么是乐观锁和悲观锁
- 悲观锁:它认为如果不锁住资源,其他线程就会来抢占,就会造成数据错误,所以为了保证结果正确性,会在每次获取修改数据时,把数据锁住,让别人无法访问该数据。如synchronized和lock接口
- 乐观锁:它认为自己在处理操作的时候不会有其他线程来干扰,所以不会锁住被操作对象,在更新的时候会判断自己修改数据的时候,有没有被其他线程修改过,没有就正常修改;如果数据和开始拿到的不一样了,就说明有其他线程修改过,这时可以选择放弃、报错、重试等策略来应对。乐观锁一般都是采用CAS算法来实现的。
-
典型例子
- 悲观锁:synchronized和lock
- 乐观锁:原子类和并发容器
-
开销对比
- 悲观锁的原始开销要高于乐观锁,但特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响。
- 虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多。
-
使用场景
- 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
- 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高
- 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗
可重入锁和非可重入锁,已ReentrantLock为例(重点)
- 可重入性质
- 什么是可重入
- 同一个线程可以多次获取同一把锁
- 好处
- 避免死锁
- 提升封装性
- 什么是可重入
- 一些方法介绍
- getHoldCount:获得当前已经被重入的数量
- isHeldByCurrentThread:判断锁是否被当前线程持有
- getQueueLength:返回当前正在等待这把锁的队列有多长
公平锁和非公平锁
默认是非公平的。
- 什么是公平和非公平
- 公平是指按照线程请求的顺序来分配锁;非公平是指不完全按照请求的顺序,在一定的情况下,可以插队。
- 为什么要有非公平
- 避免唤醒带来的空档期,提高效率
- 特例
- tryLock()不遵守设定的规则。当有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使在它之前已经有其他线程在等待队列里了。
- 对比公平和非公平的优缺点
共享锁和排它锁
-
排它锁,又称为独占锁、独享锁
-
共享锁,又称为读锁,获得
共享锁
之后,可以查看但无法修改和删除
数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据 -
共享锁和排它锁的典型是
读写锁ReentrantReadWriteLock
,其中读锁是共享锁,写锁是独享锁。这里我们用ReentrantReadWriteLock为例。 -
读写锁的作用
- 不使用读写锁的话,虽然可以保证线程安全,但是也浪费了一定的资源:多个读操作同时进行并没有线程安全问题,不需要锁住。
- 读写锁的原理:在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果
没有写锁的情况下,读是无阻塞的
,提高了程序的执行效率
-
读写的规则
- 多个线程只申请读锁,都可以申请到; 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁; 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
- 一句话总结:要么多读,要么一写
- 另一种思路:读写锁只是一把锁,可以通过两种方式锁定:读锁定和写锁定。读写锁可以被一个或者多个线程读锁定,也可以被单一线程写锁定。但是不能同时对这把锁进行读锁定和写锁定。
-
读锁和写锁的交互方式
- 读锁插队策略(默认非公平,公平不能插队)
- 写锁可以随时插队,但如果写锁已经被获取时不可以插队。
- 读锁仅在等待队列头结点不是想获取写锁的线程的时候可以插队。
- 锁的升降级
- 如果持有写锁,想要进行读操作。可以直接把锁降级到读锁,提高转换效率
- 支持锁降级,不支持锁升级。(若支持升级,会有多个读锁均想获得写锁,造成死锁)
- 读锁插队策略(默认非公平,公平不能插队)
自旋锁和阻塞锁
- 概念
- 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
- 如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长,为了这一小段时间去切换线程,线程挂起和恢复线程的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让当前请求线程不放弃CPU执行时间,进行自旋,如果在自旋完成后,锁定的同步资源已经释放了锁,那当前线程就不需要阻塞而是直接获取同步资源,避免切换线程开销,这就是自旋锁。阻塞锁就是没拿到锁就阻塞,直到被唤醒。
- 缺点
- 如果锁被占用时间很长,那么自旋只会浪费处理器资源。
- 原理
- 自旋锁的实现原理是CAS
- 适用场景
- 自旋锁一般用于多核服务器,在并发度不是很高的情况下,比阻塞锁效率高
- 自旋锁适用于临界区比较短小的情况。
可中断锁
- synchronized是不可中断锁,Lock是可中断锁,因为tryLock和lockInterruptibly都可以响应中断。
- 线程A获取锁,B正在等待该锁,此时B不想等待了,可以中断等待,去执行其他任务。,这就是可中断锁。
锁的优化
-
JVM虚拟机对锁的优化
- 自旋锁和自适应
- 锁消除
- 访问的数据是方法内部的,不会有外部线程访问到,就可以不需要加锁
- 锁粗化
- 一连串的加锁减锁都是对一个对象的,就把锁定范围扩大,减少加锁减锁的次数。
-
代码优化
- 缩小同步代码块
- 尽量不要锁住方法
- 减少请求锁的次数
- 避免认为制造“热点”,即故意多次访问共享数据。
- 锁中尽量不要再包含锁
- 选择合适的工具类