目录
锁的类型
悲观锁 vs 乐观锁:
- 悲观锁:悲观锁的思想是在操作数据之前先获取锁,认为数据在操作期间可能会被其他事务修改,因此需要先加锁保护数据。
- 常见的悲观锁实现包括数据库中的行级锁、表级锁、页级锁等,以及Java中的 synchronized 关键字、ReentrantLock 等。
- 悲观锁的特点是对数据的并发访问持保守态度,可能会导致大量的锁竞争和资源浪费。
- 乐观锁:乐观锁的思想是假设数据在操作期间不会被其他事务修改,因此不加锁直接进行操作,但在提交事务时会检查数据是否被其他事务修改过。
- 常见的乐观锁实现包括数据库中的版本号控制(如MVCC机制)、CAS(Compare and Swap)算法等。
- 乐观锁的特点是减少了锁竞争和资源浪费,但可能会增加冲突处理的复杂度。
间隙锁
在数据库中,间隙锁(Gap Lock)是一种用于锁定一段范围的记录之间的空间(间隙)的锁。当使用间隙锁时,数据库系统会锁定指定范围内的记录之间的空间,防止其他事务在该范围内插入新记录,从而确保了范围查询的一致性。
具体来说,间隙锁有以下特点和作用:
-
范围锁:间隙锁是一种范围锁,它锁定了一段记录之间的空间,而不是具体的记录。这意味着在该间隙范围内的任何记录都无法插入或更新,直到锁释放。
-
防止幻读:间隙锁的一个主要作用是防止幻读(Phantom Read)。幻读是指在一个事务中,同样的查询条件在不同的时间点会返回不同数量的记录。通过使用间隙锁,可以阻止其他事务在指定范围内插入新的记录,从而避免了幻读的问题。
-
范围查询一致性:当一个事务执行范围查询时,间隙锁可以确保查询结果的一致性,即在查询过程中其他事务无法在查询范围内插入新的记录,从而保证了查询结果的可靠性。
需要注意的是,间隙锁可能会引发死锁问题,特别是在并发量较大的情况下。因此,在使用间隙锁时,需要注意合理设计事务和锁的范围,以避免死锁的发生。
间隙锁常用于数据库中的并发控制。在数据库中,间隙锁通常由数据库引擎自动管理,不需要手动操作。
synchronized
synchronized的使用
synchronized锁的的内容应该是变化的量。
锁定块
//synchronized 关键字,上锁后执行后面的代码
public class T5_Synchronized {
public void m(){
synchronized (this) {
cnt--;
System.out.println( Thread.currentThread().getName() + " count = " + cnt );
}
}
}
锁定方法
//synchronized 关键字,上锁后执行后面的代码
public class T6_Synchronized {
//等价于synchronized(T6_Synchronized.class)
public synchronized void m(){
System.out.println( Thread.currentThread().getName() + " count = " + cnt );
}
}
synchronized原理
- 互斥性: synchronized提供了互斥性,即一次只允许一个线程进入被Syn锁住的代码块或方法。当一个线程获取到Syn锁时,其他线程会被阻塞,直到该线程释放锁。
- 可见性: synchronized还提供了可见性,即当一个线程修改了被synchronized保护的共享变量时,修改对其他线程是可见的。这是因为当一个线程释放Syn锁时,会强制刷新缓存,使得其他线程可以看到最新的共享变量值。
- 内存模型屏障: 在Java内存模型中,synchronized还会通过内存模型屏障来确保代码的顺序性。在获取和释放锁的过程中,会插入相应的内存屏障指令,以防止编译器或处理器对代码进行重排序,确保代码执行顺序符合预期。
当一个线程获取某个类的Syn锁时,发生以下情况:
- 获取锁: 线程尝试获取该类的Syn锁。如果锁可用,线程将立即获得锁,并继续执行同步代码块或方法;如果锁不可用,则线程进入阻塞状态,直到锁可用为止。
- 锁竞争: 如果多个线程同时竞争同一个类的Syn锁,只有一个线程能够获取锁,其他线程将被阻塞在锁获取的过程中,直到锁被释放。
- 执行同步代码: 一旦线程获取到锁,它就可以执行同步代码块或方法。在执行同步代码期间,其他线程无法访问被synchronized保护的共享资源。
- 释放锁:当线程执行完同步代码块或方法后,会释放锁。这样其他等待的线程就有机会获取锁,并执行同步代码。
总的来说,当一个线程锁住某个类时,它会尝试获取该类的Syn锁,如果成功获取锁,则可以执行同步代码,否则会被阻塞直到获取到锁为止。一旦线程执行完同步代码,它会释放锁,让其他等待的线程有机会获取锁。
ReentrantLock
(可重入锁)
ReentrantLock
是Java中的一种独占锁(也称为互斥锁),它可以用于实现对共享资源的线程安全访问。与使用synchronized
关键字实现的隐式锁相比,ReentrantLock
提供了更多的灵活性和功能。
主要特点包括:
-
可重入性:与
synchronized
类似,ReentrantLock
是可重入的,同一个线程可以多次获取同一把锁而不会导致死锁。 -
公平性:
ReentrantLock
可以选择是否公平地获取锁。在公平模式下,锁将按照线程的请求顺序分配。在非公平模式下,锁将会尝试立即获取锁,而不管其他线程是否在等待。 -
等待可中断:
ReentrantLock
支持在等待锁的过程中响应中断。线程可以在等待锁时被其他线程中断,而不是一直等待下去。 -
超时获取锁:
ReentrantLock
支持尝试在一定时间内获取锁,如果获取不到则放弃。 -
条件变量:
ReentrantLock
提供了Condition
接口,可以通过newCondition()
方法创建条件对象,用于线程间的协调和通信。 -
释放:使用
ReentrantLock
时需要确保在finally块中释放锁,以防止发生死锁。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final Lock lock = new ReentrantLock();
public void doSomething() {
lock.lock(); // 获取锁
try {
// 执行需要同步的代码块
} finally {
lock.unlock(); // 释放锁
}
}
}
注意:
在try
块中避免使用return
语句,以免出现意外的逻辑错误。
如果在try
块中使用了return
语句,而在finally
块中释放锁,那么会发生以下情况:
- return语句在finally块之前执行:在
try
块中的return
语句执行时,会将结果返回给调用者,但此时finally
块尚未执行。如果在finally
块中释放锁,那么在return
语句执行完毕后才会执行finally
块中的释放锁操作。public class Example { private final Lock lock = new ReentrantLock(); public int getValue() { lock.lock(); // 获取锁 try { return 10; } finally { lock.unlock(); // 释放锁 } } }
- 在finally块中return:在
finally
块中使用return
语句,会导致在try
块中的return
语句失效,实际返回的结果是在finally
块中的return
语句返回的结果。public class Example { private final Lock lock = new ReentrantLock(); public int getValue() { lock.lock(); // 获取锁 try { return 10; } finally { lock.unlock(); // 释放锁 return 20; // 这个return语句会覆盖try块中的return语句 } } }
锁升级
锁升级是指锁的级别在运行时由低级别的锁升级为高级别的锁,通常是为了提高并发性能。在 Java 中,synchronized 关键字就涉及了锁升级的问题。
synchronized 关键字和锁升级:
在 Java 中,synchronized 关键字可以应用于不同级别的锁,包括偏向锁、轻量级锁和重量级锁。锁的级别会根据竞争情况和线程执行状态进行自动升级。
-
偏向锁(Biased Locking): 当一个线程首次进入同步代码块时,会尝试获取偏向锁。如果没有竞争,该线程会获得锁,并将对象头的标记位设置为偏向锁。如果有其他线程尝试获取锁,偏向锁会升级为轻量级锁。
-
轻量级锁(Lightweight Locking): 当有多个线程竞争同一个锁时,锁会升级为轻量级锁。轻量级锁使用 CAS 操作来实现线程之间的互斥,避免了传统的互斥量的开销。如果竞争激烈或者 CAS 操作失败,锁会进一步升级为重量级锁。
-
重量级锁(Heavyweight Locking): 当多个线程竞争同一个锁,并且竞争激烈时,锁会升级为重量级锁。重量级锁会让其他线程进入阻塞状态,直到持有锁的线程释放锁。
锁升级的过程是为了尽可能地减少锁竞争,提高程序的并发性能。
分布式锁
分布式锁是一种用于解决分布式系统中多个进程或者线程访问共享资源的同步问题的锁。
实现分布式锁需要解决以下几个关键问题:
- 锁的唯一性: 确保同一时间只有一个客户端能够获取到锁。
- 锁的可重入性: 同一个客户端能够多次获取同一把锁而不会被阻塞。
- 锁的失效: 确保锁在一定时间内没有被释放时,能够自动释放,避免死锁。
- 锁的高可用性: 即使部分节点宕机,整个分布式锁仍然能够正常工作。
在设计一个分布式锁时,需要考虑以下问题:
-
锁的实现方式: 分布式锁的实现方式有很多种,包括基于数据库、基于缓存、基于 ZooKeeper 等。需要选择合适的实现方式,并根据实际情况考虑锁的性能、可靠性和成本等因素。
-
锁的粒度: 锁的粒度可以是针对整个资源、针对部分资源或者针对单个操作。需要根据业务需求和系统架构来确定锁的粒度。
-
锁的可重入性: 分布式锁是否支持可重入性是一个重要的考虑因素。如果业务逻辑中存在递归调用或者嵌套调用的情况,那么锁必须支持可重入性。
-
锁的超时和自动释放: 分布式锁需要考虑锁的超时和自动释放机制,避免因为死锁或者其他原因导致锁无法释放而造成系统的阻塞。
-
锁的容错和高可用: 分布式系统中锁的容错和高可用性是非常重要的,需要考虑在节点故障或者网络分区的情况下锁的可用性和一致性。
辨析
synchronized:
- 关键字。
- Java 中最基本的线程同步机制之一。
- 通过 JVM 实现,无需额外的 API 调用。
- 不能中断一个正在试图获得锁的线程。
- 不支持超时等待。
- 性能较低,对于大规模并发访问同步代码块时,性能可能会有所下降。
- 适用于简单的线程同步需求,且锁的范围较小的情况。
Lock:
- 接口,位于
java.util.concurrent.locks
包中。 - 提供了更灵活的线程同步机制,具有更多的功能。
- 支持中断等待线程、支持超时等待。
- 可以获取锁的持有情况,可以尝试获取锁而不会一直等待。
- 可以创建公平锁(按照等待时间或线程优先级来获取锁)。
- 可以与条件变量一起使用。
- 适用于对线程同步需求较为复杂、灵活性较高的情况。
ReentrantLock:
Lock
接口的实现类之一,也位于java.util.concurrent.locks
包中。- 是可重入锁,支持同一个线程重复获取锁,避免死锁。
- 提供了更多的功能,如公平锁、可中断锁等。
- 需要显式地进行锁的获取和释放,相对于
synchronized
更为灵活,但也更容易出错。 - 适用于对锁的粒度要求较高,需要更精细的控制的情况。
常见的应用场景:
-
synchronized
:适用于简单的线程同步需求,例如对实例方法或代码块进行同步,保护共享资源的读写等。 -
Lock
:适用于对线程同步需求较为复杂的情况,例如需要支持中断、超时等待、公平锁等功能的场景。 -
ReentrantLock
:适用于需要可重入锁和更高级功能的情况,例如需要在同一个线程中多次获取同一个锁的情况,或需要公平锁、可中断锁等功能的场景。