乐观锁/悲观锁
乐观锁:一般情况下不会产生并发冲突,所以在数据提交更新的时候,才会正式对数据是否并发冲突进行检测,如果发现并发信息了,则让用户返回错误的信息,让用户决定如何去做。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次拿数据的时候就会上锁,这样别人想拿到这个数据就会阻塞,直到它拿到锁。
synchronized刚开始使用乐观锁策略,当发现锁竞争比较频繁的时候会自动切换成悲观锁策略。
乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 "版本号" 来解决.
假设我们需要多线程修改 "用户账户余额".。
设当前余额为 100. 引入一个版本号 version, 初始值为 1。
并且我们规定 "提交版本必须大于记录 当前版本才能执行更新余额"
1) 线程 1此时准备将其读出( version=1, balance=100 )线程 2 也读入此信息(version=1, balance=100 )
2) 线程 1 操作的过程中并从其帐户余额中扣除 50,线程 2 从其帐户余额中扣除 20
3) 线程 1 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=50 ),写回到内存中
4) 线程 2完成了操作,也将版本号加1( version=2 )试图向内存中提交数据( balance=80 ),但此时比对版本发现,操作员 2 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败
读写锁 (readers-writer lock)
在执行加锁操作时,需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥
一个线程对于数据的访问主要就是读数据和写数据
*两个线程都只是读一个数据并没有线程安全问题,直接一同读取就行
*两个线程同时写一个数据,存在线程安全问题
*一个线程读另外一个线程写,也存在线程安全问题
读写操作就是把读操作和写操作区别对待,Java标准库中提供了ReentrantReadwriteLock类,用来实现读写操作。
ReentrantReadwriteLock.ReadLock类表示一个读锁,这个对象提供了lock/unlock方法进行加锁解锁。
ReentrantReadwriteLock.writeLock类表示一个写锁,同样也提供了lock/unlock方法进行加锁解锁。
读写锁适合于频繁读不频繁写的场景中。
重量级锁/轻量级锁
锁的核心特性“原子性”,这样的机制追根溯源是CPU这样的硬件提供的
*CPU提供了“原子操作指令”
*操作系统寄语CPU的原子指令,实现了mutex互斥锁
*JVM基于操作系统提供的互斥锁,实现了synchronized和ReentrantLock等关键字和类。
理解用户态 vs 内核态 想象去银行办业务. 在窗口外, 自己做, 这是用户态. 用户态的时间成本是比较可控的. 在窗口内, 工作人员做, 这是内核态. 内核态的时间成本是不太可控的. 如果办业务的时候反复和工作人员沟通, 还需要重新排队, 这时效率是很低的.
重量级锁:加锁机制重度依赖操作系统提供了mutex
*大量的内核态用户态切换
*很容易引发线程的调度
(这俩操作成本高)
轻量级锁:加锁机制尽可能不使用mutex,而是尽量在用户态代码完成,实在不行才使用mutex
*少量的内核态用户态切换
*不太容易引发线程调度
synchronized最初是一个轻量级锁,如果冲突比较严重,会变为重量级锁
自旋锁(Spin Lock)
如果获取锁失败,立即再尝试获取锁,无线循环,直到获取到锁为止,第一次获取失败,第二次的尝试会再极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁。
自旋锁是一种典型的轻量级锁
优点:没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁
缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗CPU资源,这个时候可以选用挂起等待锁(挂起等待锁是不消耗CPU的)
synchronized中的轻量级锁策略大概率就是通过自旋锁的方式实现的。
公平锁/非公平锁
公平锁:遵循先来后到原则
非公平锁:不遵守先来后到,谁先抢到是谁的
*操作系统内部线程调度就是随机的,不做任何限制,锁就是非公平的,如果要实现公平锁就需要依赖额外的数据结构来记录线程的先后顺序。
*公平于非公平锁没有好坏之分,关键看适用场景。
可重入锁/不可重入锁
可重入锁:允许同一个线程多次获取同一把锁,Java中只要以Reentrant开头的锁都是可重入锁,而且jdk提供的所有现成的Lock实现类,包括synchronized关键字都是可重入锁
但是Linux系统提供的mutex是不可重入锁
死锁(不可重入锁):
//第一次加锁成功,
lock();
//第二次加锁被占用,阻塞等待
lock();
优化过程
锁消除
编译器+JVM判断锁是否可以消除,如果可以就直接消除。
锁消除?(有些应用程序代码中,用到了synchronized,但是没有在多线程环境下)
例如:StringBiffer in=new StringBuffer();
in.append("a");
in.append("b");
in.append("c");
此时每个append的调用都会设计加锁和解锁,但如果只是在单线程中执行这个代码,这些加锁解锁操作没有必要,会浪费资源。
锁粗化
一段逻辑中如果的多次出现加锁解锁,编译器+JVM会自动进行锁的粗化
实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁. 但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释 放锁