锁的类型
悲观锁 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语句}} }
辨析
synchronized:
- 关键字。
- Java 中最基本的线程同步机制之一。
- 通过 JVM 实现,无需额外的 API 调用。
- 不能中断一个正在试图获得锁的线程。
- 不支持超时等待。
- 性能较低,对于大规模并发访问同步代码块时,性能可能会有所下降。
- 适用于简单的线程同步需求,且锁的范围较小的情况。
Lock:
- 接口,位于
java.util.concurrent.locks
包中。 - 提供了更灵活的线程同步机制,具有更多的功能。
- 支持中断等待线程、支持超时等待。
- 可以获取锁的持有情况,可以尝试获取锁而不会一直等待。
- 可以创建公平锁(按照等待时间或线程优先级来获取锁)。
- 可以与条件变量一起使用。
- 适用于对线程同步需求较为复杂、灵活性较高的情况。
ReentrantLock:
Lock
接口的实现类之一,也位于java.util.concurrent.locks
包中。- 是可重入锁,支持同一个线程重复获取锁,避免死锁。
- 提供了更多的功能,如公平锁、可中断锁等。
- 需要显式地进行锁的获取和释放,相对于
synchronized
更为灵活,但也更容易出错。 - 适用于对锁的粒度要求较高,需要更精细的控制的情况。
常见的应用场景:
-
synchronized
:适用于简单的线程同步需求,例如对实例方法或代码块进行同步,保护共享资源的读写等。 -
Lock
:适用于对线程同步需求较为复杂的情况,例如需要支持中断、超时等待、公平锁等功能的场景。 -
ReentrantLock
:适用于需要可重入锁和更高级功能的情况,例如需要在同一个线程中多次获取同一个锁的情况,或需要公平锁、可中断锁等功能的场景。