乐观锁
乐观地认为并发访问不会造成数据冲突,只在更新时检查是否有冲突。乐观锁和CAS的关系可以用“乐观锁是一种思想,CAS是一种具体的实现”来理解。
当使用CAS操作修改数据时,如果版本号不匹配或者其他线程已经修改了要操作的数据,CAS会返回失败。这时候,程序可以再次尝试CAS操作,也就是进行自旋重试,直到CAS操作成功。
因此,CAS操作已经内置了自旋重试的机制,避免了使用额外的自旋锁。
适用场景:适用于并发较低
(高并发场景每次修改了去对比,还不如让加锁阻塞排队执行)、读多写少
的场景,相信数据多数情况下不会发生冲突,只在更新时进行检查,以减少对共享资源的争用。
java中常见悲观锁实现:可以使用java.util.concurrent.atomic包中的原子类,比如AtomicInteger、AtomicLong等,来实现CAS操作。
mysql实现乐观锁:版本号、时间戳
悲观锁
悲观地认为并发访问会造成数据冲突,因此在访问共享资源之前就会进行加锁,确保同一时刻只有一个线程能够访问。
适用场景:适用于高并发
、写多
的场景,通过加锁保护共享资源,确保并发访问时不会造成数据不一致性。
java中常见悲观锁实现:synchronized 关键字、ReentrantLock(可重入锁)
mysql中实现悲观锁:SELECT ... FOR UPDATE
、 SELECT ... LOCK IN SHARE MODE
乐观锁ABA问题
ABA问题是在并发编程中一种常见的问题,特别是在使用乐观锁的情况下。它的发生主要是因为在执行CAS操作(比较并交换)时,数据的值发生了变化。
具体来说,假设有两个线程 T1 和 T2,初始时它们都读取了某个共享变量的数值为 A。然后 T1 首先将共享变量的值从 A 修改为了 B,然后再将其修改回 A,而线程 T2 在 T1 修改回 A 之前也对共享变量进行了修改(比如将 A 修改为了 C,然后再修改回 A)。在这种情况下,线程 T2 会错误地认为共享变量的值没有发生变化,因为它原始读取的值和最终值都是 A。这就是ABA问题的本质。
乐观锁通常使用CAS操作来实现,而CAS操作的基本思想是:读取当前内存值 A,比较 A 和预期值 B 是否相等,如果相等,则更新为新值 C;否则进行重试。在ABA问题中,CAS操作可能会由于共享变量的值从 A 变化为 B 再变化为 A,导致CAS操作在比较时误以为共享变量的值没有发生变化。
为了解决ABA问题,一种常见的方法是使用版本号(Version Numbering)或者时间戳(Timestamp)来标记共享变量的变化次数,每次修改共享变量时都更新版本号或时间戳,这样就能够避免因为共享变量的数值相同而导致的误判。另外,Java中的AtomicStampedReference
类可以用于解决ABA问题,它通过引入一个标记来区分不同的修改次数,从而避免了传统CAS操作可能出现的ABA问题。
悲观锁 和 乐观锁 比较
相对而言,悲观锁适用于高并发,乐观锁适用于低并发
为什么乐观锁适用于并发量低:因为并发量高的时候,cas一直失败自旋没有任何意义,损耗性能,不如让cpu干其他的或者等待
锁升级
在 Java 中,锁升级是指在同步代码块中锁的状态发生改变的过程。这个过程包括偏向锁、轻量级锁和重量级锁三种状态的切换。
锁升级是JVM自动进行管理的。
当JVM检测到多个线程对同步代码块的竞争时,会根据实际情况自动进行锁的升级。这种锁升级的机制是为了在多线程竞争情况下保证程序的安全和效率,以及在不同线程竞争程度下选择合适的锁状态,从而最大限度地提高并发性能。
偏向锁:
用于处理只有一个线程访问同步块的情况,减少不必要的竞争。
偏向锁需要在对象头中记录持有偏向锁的线程ID,避免重复的CAS操作。
适用于只有一个线程执行同步块的情况,从而减少不必要的同步操作。不用去加锁释放锁。
轻量级锁:
适用于短时间内只有少量线程竞争同步块的情况。
使用CAS操作尝试获取锁,避免了传统锁(互斥锁)的性能开销。
在少量线程竞争情况下,避免了传统锁的重量级化,提高了性能。
重量级锁:
当锁存在大量的线程竞争时会升级为重量级锁,采用传统的互斥锁实现。
保证了线程间数据同步和互斥访问,但性能开销相对较大。
偏向锁升级为轻量级锁:
当多个线程访问同步块时,偏向锁会升级为轻量级锁。这时,会使用CAS(Compare And Swap)操作来尝试获取锁,如果成功获取锁,则表示处于轻量级锁状态。
轻量级锁升级为重量级锁:
如果轻量级锁获取失败,就会升级为重量级锁。这时,会使用传统的互斥锁机制来确保线程间的互斥访问。
公平锁
公平锁则按照请求锁的顺序来获取锁,不允许插队,即等待时间最长的线程会优先获得锁。
非公平锁
非公平锁允许抢占,即允许在等待队列中的线程随机获取锁,synchronized 关键字是非公平锁
在 Java 中,ReentrantLock 是可重入锁的一种实现,通过使用 ReentrantLock 类,可以根据构造函数的不同参数选择是公平锁还是非公平锁。例如:
ReentrantLock fairLock = new ReentrantLock(true); // 创建一个公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 创建一个非公平锁
共享锁(Shared Lock):
允许多个事务同时获取对象的共享锁,可同时读取共享资源,但不允许进行修改。这种锁适用于读多写少的场景,可以提高并发读取的效率。
排他锁(Exclusive Lock):
排他锁是一种独占锁,它防止其他事务获取同一对象的共享或排他锁,确保只有一个事务能够对资源进行修改操作,其他事务需要等待该锁的释放。
可重入锁:
可重入锁是一种允许同一个线程多次获取同一把锁的锁。当线程第一次获取锁后,再次尝试获取该锁时,也会成功获取而不会被阻塞,这样可以避免死锁情况的发生。Java 中的 ReentrantLock 、synchronized 就是一种可重入锁的实现。
不可重入锁:
不可重入锁是一种不允许同一个线程多次获取同一把锁的锁。如果一个线程已经获取了该锁,再次尝试获取时会被阻塞,即使是同一个线程也不能再次获取这把锁,这可能会导致死锁情况。
单机锁:
单机锁是指在单个计算机或单个进程中控制对共享资源的访问的锁。常见的单机锁包括 synchronized 关键字、ReentrantLock 等。单机锁通常只在单个 JVM 内有效,不能跨越多个计算机或进程。 synchronized 关键字或 ReentrantLock 都是单机锁
分布式锁:
分布式锁是用于在分布式系统中控制对共享资源的访问的锁,可以跨越多个计算机或进程。分布式锁可以通过基于数据库、基于缓存(如 Redis)、基于 ZooKeeper 等方式来实现。