乐观锁 分为三个阶段:数据读取、写入校验、数据写入。
假设数据一般情况下不会造成冲突,只有在数据进行提交更新时,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回错误信息,让用户决定如何去做。fail-fast
机制。
悲观锁 正如其名,它指对数据被外界(可能是本机的其他事务,也可能是来自其它服务器的事务处理)的修改持保守态度。在整个数据处理过程中,将数据处于锁定状态。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。如果加锁的时间过长,其他用户长时间无法访问,影响程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是长事务而言,这样的开销往往无法承受。
分布式锁 分布式集群中,对锁接口QPS
性能要求很高,单台服务器满足不了要求,可以考虑将锁服务部署在独立的分布式系统中,比如借助分布式缓存来实现。
可重入锁 可重入锁,也叫做递归锁,是指在同一个线程在调外层方法获取锁的时候,再进入内层方法会自动获取锁。ReentrantLock
和synchronized
都是 可重入锁。可重入锁的一个好处是可一定程度避免死锁。
这对于设计复杂的程序或库来说是非常重要的。考虑下面这种情况:
language-java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class SomeClass { private final Lock lock = new ReentrantLock(); public void methodA() { lock.lock(); try { // 一些代码 methodB(); // 在 methodA 中调用 methodB // 更多代码 } finally { lock.unlock(); } } public void methodB() { lock.lock(); try { // 一些代码 } finally { lock.unlock(); } } }
在这个例子中,methodA()
和 methodB()
都使用了同一把锁。如果 ReentrantLock
不是可重入的,那么当线程在 methodA()
中已经获取了锁后,再次尝试在 methodB()
中获取锁时,就会导致死锁,因为锁已经被同一个线程持有,而不是其他线程。
因此,”可重入”锁允许在同一线程中多次获取同一把锁,这样就能避免死锁,并且简化了程序设计,因为无需担心方法之间的调用顺序是否会导致死锁。
尽管在您提出的例子中看起来似乎并没有太大意义,但在实际的程序设计中,”可重入”锁确实是一个非常重要的概念。
自旋锁 自旋锁是一种基于忙等待(busy-waiting)的锁,它在获取锁时会不断地循环尝试获取锁,而不是让线程进入睡眠状态。自旋锁的主要特点是在锁被其他线程占用时,当前线程会不断地尝试获取锁,直到获取到锁为止,而不会释放 CPU 控制权。
自旋锁适用于以下情况:
锁被持有的时间短
:如果锁被持有的时间很短,那么等待锁的线程不需要进入睡眠状态,使用自旋锁可以避免线程切换的开销
。
多核处理器:在多核处理器上,一个线程在自旋等待锁的同时,其他线程可以继续执行,因此自旋锁在多核处理器上能够充分利用 CPU 时间,提高并发性能。
高并发场景:在高并发的情况下,锁的竞争可能会很激烈,自旋锁可以减少线程的阻塞时间,提高系统的响应速度。
然而,自旋锁也有一些缺点:
等待时间过长可能会浪费 CPU 资源:如果锁的竞争很激烈,导致线程不断自旋等待锁的释放,可能会浪费大量的 CPU 时间。
不适用于长时间持有锁的情况:如果锁被持有的时间较长,自旋锁会导致其他线程长时间等待,影响系统的响应性能。
因此,在使用自旋锁时需要权衡利弊,根据具体的场景来决定是否使用自旋锁。通常情况下,自旋锁适用于锁被持有时间短、锁的竞争不激烈的情况下,能够有效提高并发性能。
当然可以。以下是一个简单的示例,演示了如何使用自旋锁来保护一个共享资源:
language-java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.util.concurrent.atomic.AtomicBoolean; public class SpinLock { private AtomicBoolean locked = new AtomicBoolean(false); public void lock() { // 不断尝试获取锁,直到成功为止 while (!locked.compareAndSet(false, true)) { // 自旋等待,不做其他事情,持续尝试获取锁 } } public void unlock() { // 释放锁 locked.set(false); } }
在这个示例中,SpinLock
类实现了一个自旋锁。AtomicBoolean
类被用作锁状态的标记,初始时为 false
表示未锁定状态。lock()
方法使用了自旋等待的方式来尝试获取锁,它会不断地尝试将 locked
的值从 false
设置为 true
,直到成功获取到锁为止。在 unlock()
方法中,锁会被释放,将 locked
的值重新设置为 false
。
以下是一个使用 SpinLock
的示例:
language-java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class SpinLockExample { private static int counter = 0; private static SpinLock spinLock = new SpinLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { spinLock.lock(); counter++; spinLock.unlock(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { spinLock.lock(); counter++; spinLock.unlock(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Counter: " + counter); } }
在这个示例中,两个线程 t1
和 t2
分别对 counter
执行了 1000 次增加操作,每次增加操作都在获取自旋锁后执行,并在执行完毕后释放自旋锁。最后输出 counter
的值,由于自旋锁的保护,counter
的增加操作是线程安全的。
独享锁 独享锁是指该锁一次只能被一个线程所持有。
共享锁 共享锁是指该锁可被多个线程所持有。ReentrantReadWriteLock
,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的。独享锁与共享锁也是通过AQS
(AbstractQueuedSynchronizer
)来实现的,通过实现不同的方法,来实现独享或者共享。
互斥锁 独享锁/共享锁就是一种广义的说法,互斥锁/读写锁指具体的实现。
读写锁 读写锁在Java
中的具体实现就是ReentrantReadWriteLock
阻塞锁 阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争进入运行状态。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()/notify(),LockSupport.park()/unpark()
公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁
非公平锁 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
可能造成优先级反转或者饥饿现象。对于Java ReentrantLock
而言,通过构造函数 ReentrantLock(boolean fair) 指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。对于Synchronized
而言,也是一种非公平锁。
分段锁 分段锁其实是一种锁的设计,目的是细化锁的粒度,并不是具体的一种锁,对于ConcurrentHashMap
而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
ConcurrentHashMap
中的分段锁称为Segment
,它即类似于HashMap
(JDK7
中HashMap
的实现)的结构,即内部拥有一个Entry
数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock
(Segment
继承了ReentrantLock)
。
当需要put
元素的时候,并不是对整个HashMap
加锁,而是先通过hashcode
知道要放在哪一个分段中,然后对这个分段加锁,所以当多线程put时,只要不是放在同一个分段中,可支持并行插入。
对象锁 一个线程可以多次对同一个对象上锁。对于每一个对象,java
虚拟机维护一个加锁计数器,线程每获得一次该对象,计数器就加1
,每释放一次,计数器就减 1
,当计数器值为0
时,锁就被完全释放了。
synchronized
修饰非静态方法、同步代码块的synchronized (this)
、synchronized
(非this
对象),锁的是对象,线程想要执行对应同步代码,需要获得对象锁。
使用 synchronized 加锁 this 时,只有同一个对象会使用同一把锁,不同对象之间的锁是不同的
。
当需要同步访问对象的实例方法或实例变量时,应该使用 this 作为 synchronized 的参数。 例如,在一个多线程环境中,多个线程需要同时访问对象的实例方法或实例变量时,可以使用 synchronized(this) 来确保线程安全。
language-java 1 2 3 4 5 6 7 public void synchronizedBlock() { synchronized (this) { // 同步代码块 } }
类锁 synchronized
修饰静态方法或者同步代码块的synchronized
(类.class)
,线程想要执行对应同步代码,需要获得类锁。使用 synchronized 加锁 class 时,无论共享一个对象还是创建多个对象,它们用的都是同一把锁
。 当需要同步访问类的静态方法或静态变量时,应该使用 MyClass.class 作为 synchronized 的参数。 例如,在一个多线程环境中,多个线程需要同时访问类的静态方法或静态变量时,可以使用 synchronized(MyClass.class) 来确保线程安全。
language-java 1 2 3 4 5 6 7 public static void synchronizedStaticBlock() { synchronized (MyClass.class) { // 同步代码块 } }
锁升级 锁升级是Java虚拟机中的一种优化策略
,它是针对 synchronized 关键字进行的优化,并不是像 ReentrantLock 这样的锁类库,可以在代码逻辑中直接使用的锁。
(无锁、偏向锁、轻量级锁、重量级锁)是指在Java虚拟机中对锁的状态进行优化和调整的过程。这些状态反映了对象的锁定状态以及锁的竞争情况。这种锁升级的目的是为了在不同情况下提供不同的锁定机制,以减少锁的竞争、提高性能。
下面是对这几种锁状态的简要介绍:
无锁状态 :
当线程尝试获取锁时,对象的锁状态为无锁状态,表示该对象没有被任何线程锁定。
在无锁状态下,线程会通过CAS(Compare and Swap)等原子操作尝试直接修改对象的指针或标记位,来尝试获取锁。
偏向锁 :
当只有一个线程访问同步块时,对象的锁状态会升级为偏向锁状态。
偏向锁会将线程的ID记录在对象头中,表示该线程拥有对象的偏向锁。当其他线程尝试获取锁时,会检查偏向锁的线程ID是否与当前线程ID相同,如果相同则表示获取成功。
轻量级锁 :
当有多个线程竞争同步块时,对象的锁状态会升级为轻量级锁状态。
轻量级锁使用CAS操作来避免传统的互斥量操作,尝试在用户态下通过自旋来获取锁。如果自旋获取锁失败,则升级为重量级锁。
重量级锁 :
当轻量级锁竞争失败时,对象的锁状态会升级为重量级锁状态。
重量级锁会使得竞争失败的线程进入阻塞状态,从而让出CPU资源,减少竞争。
锁升级是Java虚拟机对锁状态的动态调整过程,旨在根据实际的锁竞争情况和线程行为来选择最适合的锁策略,以提高程序的并发性能。