锁类型讲解
锁类型讲解
一、锁导图
对于锁的分类,需要从不同角度进行考察,这些分类并不是互斥的,也就是多个类型可以并存;有可能一个锁同时属于两种类型。比如ReentrantLock既是互斥锁,又是可重入锁。



二、乐观锁和悲观锁
1、为什么会诞生非互斥同步锁(乐观锁)
互斥同步锁(悲观锁)的劣势:
1)阻塞和唤醒带来的性能劣势;2)永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环、死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程,将永远也得不到执行;3)优先级反转:就是设置的优先级别毫无用处。
java中悲观锁的实现就是synchronized和Lock相关类。
2、乐观锁介绍
1)认为自己在处理操作的时候,不会有其他线程来干扰,所以并不会锁住被操作对象;
2)在更新的时候,去对比在我修改的期间数据有没有被其他人改变过;如果没被改变过,就说明真的是只有自己在操作,那就正常去修改数据;
3)如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过数据,那就不能继续刚才的更新数据过程,我会选择放弃、报错、重试等策略;
4)乐观锁的实现一般都是利用CAS算法来实现的。
乐观锁的典型例子就是原子类、并发容器等。


3、开销对比
1)悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响。
2)相反,虽然乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多。
4、两种锁各自的使用场景
1)悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗,典型情况:
a.临界区有IO操作;b.临界区代码复杂或者循环量大;c.临界区竞争非常激烈
2)乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提高。
三、可重入锁和非可重入锁
1、什么是可重入?
可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。synchronized和ReentrantLock都是可重入锁。
2、好处
1)避免死锁;2)提升封装性;
3、可重入锁简单测试
public class GetHoldCount { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.lock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); lock.unlock(); System.out.println(lock.getHoldCount()); } }
运行结果
0
1
2
3
2
1
0
4、源码层面进行对比

四、公平锁和非公平锁
1、什么是公平和非公平
1)公平指的是按照线程请求的顺序,来分配锁;非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队。
2、为什么要有非公平锁
1)为了提高效率;2)避免唤醒带来的空档期
ReentrantLock lock = new ReentrantLock(true); // 公平锁
ReentrantLock lock = new ReentrantLock(false); // 非公平锁
3、对比公平和非公平的优缺点
优势
劣势
公平锁
各线程公平平等,每个线程在等待一段时间后,总有执行的机会
更慢、吞吐量更小
不公平锁
更快,吞吐量更大
有可能产生线程饥饿,也就是某些线程在长时间内,始终得不到执行。
4、源码分析

五、共享锁和排他锁
以ReentrantReadWriteLock读写锁为例
1、什么是共享锁和排它锁
1)排它锁,又称为独占锁、独享锁;
2)共享锁,又称为读锁,获得共享锁之后,可以查看,但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据。
3)共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁。
2、读写规则
1)多个线程只申请读锁,都可以申请到;
2)如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
3)如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
4)总结:两者不会同时出现(要么多读,要么一写)。
3、代码示例
public class ReadWriteTest { private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); private static void read() { readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了读锁,正在读取"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "释放了读锁"); readLock.unlock(); } } private static void write() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到了写锁,正在写入"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "释放了写锁"); writeLock.unlock(); } } public static void main(String[] args) { new Thread(() -> read(), "Thread1").start(); new Thread(() -> read(), "Thread2").start(); new Thread(() -> write(), "Thread3").start(); new Thread(() -> write(), "Thread4").start(); } }
运行结果:

4、读锁插队策略
1)公平锁:不允许插队 2)非公平锁:a:写锁可以随时插队 b:读锁仅在等待队列头节点不是想获得写锁的线程的时候可以插队。
5、读写锁,只能降级,不能升级
降级可以提高效率,升级会导致阻塞
6、适用场景
ReentrantReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发效率。
六、自旋锁和阻塞锁
1、自旋锁理解
1)阻塞或唤醒一个java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。
2)如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。
3)在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
4)如果物理机有多个处理器,能够让两个或以上的线程同事并行执行,我们就可以让后面那个请求锁的线程不放弃cpu的执行时间,看看持有锁的线程是否很快就会释放锁。
5)而为了让当前线程"稍等一下",我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销,这就是自旋锁。
6)阻塞锁和自旋锁相反,阻塞锁如果遇到没拿到锁的情况,会直接把线程阻塞,直到被唤醒。
2、原理
AtomicInteger的实现:自旋锁的实现原理是CAS。
AtomicInteger中调用unsafe进行自增操作的源码中do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环,直到修改成功。
3、代码示例
public class SpinLock { private AtomicReference
运行结果:

4、自旋锁的适用场景
1)自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高。
2)另外,自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不适合的。






