一. 按照性质
1. 公平锁/非公平锁
公平锁是指等待时间最长的会先获得锁,等待时间短的后获得锁;而非公平锁则不一定,一般情况下,非公平锁的吞吐率会高于公平锁。
2. 乐观锁/悲观锁
悲观锁是指对于同一数据的并发操作,总是悲观的认为数据会被改变,因此每次操作数据时一定会给数据上锁;
乐观锁是指对于同一数据的并发操作,乐观锁会认为数据不太可能被修改,因此不会加锁,但是乐观锁也会采用自己的同步方式来保证数据被修改时能做出正确的反应,通常是通过采用版本号的方式实现,即:给数据赋予一个版本号字段,每次数据被修改版本号就加一,然后乐观锁在使用时会先读取数据,然后在使用数据时判断读取到的版本号与最新版本号是否一致,如果一致则使用数据,否则认为数据是过期数据,需要重新读取。
悲观锁适合写操作多的数据,乐观锁适合读操作多的数据。
3. 独享锁/共享锁
独享锁:同一时刻只能被一个线程持有。
共享锁:同一时刻能被多个线程持有。
常见的例子是ReentrantReadWriteLock,它的读锁是共享锁,允许多个线程同时读数据,写锁是独享锁,只有一个线程修改完毕,其他线程才能修改或读取。
二. 按照实现原理
1. 自旋锁/互斥锁
自旋锁和互斥锁有点类似,二者的区别:
假设现在有一个锁L,一个CPU C1用来运行线程T1,另一个CPU C2用来运行线程T2,这时T1持有L,但T2尝试去获取L
互斥锁:发现L被T1占有,因此C2会使T2睡眠,并切换线程(通常这个操作的代价都比较大,这也是在许多情况下互斥锁的效率不如自旋锁的主要原因),C2可能会去运行T3;
自旋锁:不断的循环查看T1是否释放了L,这期间C2无法做别的事,在循环了N(默认是10)次后,会采用和互斥锁同样的方式挂起线程T2去做别的事;
自适应自旋锁(JDk1.6):会记录之前在得到锁之前自旋的次数R,如果R很小,那么他会倾向于认为这次能比较快的获得锁,就会把N设置的稍大一点;而如果R很大,那么它会倾向于认为不会很快获得锁,因此会把N设置的较小,甚至为0,即很快就挂起线程T2。
因此可以看到,只有当CPU核数较高,且锁的平均占用时间较短的情况下自旋锁才会有更好的表现。
2.锁粗化/锁消除
这算是两种锁优化的方式,
锁粗化:虽然说加锁块应该尽量小,以使其他线程能尽快获取到锁,但是频繁的加锁和解锁反而会增加资源的开销,因此如果一个操作是对同一个对象反复加锁解锁或是锁操作在循环体内部,那么就可以考虑合并这些锁为一个。
锁消除:如果可以确定一个资源只会被一个线程访问,那么就可以取消对该资源的加锁操作来降低开销(由虚拟机即时编译器完成)。
3. 偏向锁/轻量级锁/重量级锁
特指synchronized中锁的状态(JDK1.5):
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
如果一个线程当前持有偏向锁,然后该锁被另一个线程所争用,那么偏向锁就会升级为轻量级锁,等待获取锁的线程会采用自旋的方式等待锁,不会阻塞。
如果一个请求轻量级锁的线程自旋了太久,那么轻量级锁就会膨胀为重量级锁,相应的性能也会降低
4. CAS算法
compare and swap——比较交换技术,是一种通过无锁来实现“有锁”的算法,其原理为:
当线程A尝试去修改B的值时首先它会有一个期待的B的值,假设为H,然后当它真正去修改B的值时,如果发现B确实等于L,就进行修改,否则修改失败并在之后不断重新尝试直到修改成功(自JD1.5之后JVM在底层硬件上对CAS提供了支持)。
CAS最大的优点首先天生避免死锁,因为就没有锁,其次因为他没有争用开销和线程调度的开销因此性能非常出色。
参考:
[1] 线程安全与锁优化:https://blog.csdn.net/sinat_33087001/article/details/77644441#t27
[2] java锁有哪些种类:https://blog.csdn.net/nalanmingdian/article/details/77800355
[3] 自旋锁和互斥锁区别 --- 经典:https://www.sohu.com/a/118199639_464041
网友评论