一、Java中锁分类
二、乐观锁与悲观锁
1 乐观锁
1.1乐观锁的CAS实现
import java.util.concurrent.atomic.AtomicInteger;
public class OptimisticLock {
private AtomicInteger counter;
public OptimisticLock() {
this.counter = new AtomicInteger(0);
}
public void increment() {
int oldValue = counter.get();
int newValue = oldValue + 1;
// CAS自循环机制,当旧值和当前值不等就重新获取当前值进行重新计算,直到值相等跳出循环
while (!counter.compareAndSet(oldValue, newValue)) {
oldValue = counter.get();
newValue = oldValue + 1;
}
}
public int getCounterValue() {
return counter.get();
}
}
在这个案例中,OptimisticLock类用一个AtomicInteger来表示一个计数器。在increment方法中,我们首先获取当前计数器的旧值,然后计算新的值。然后使用compareAndSet方法比较旧值和当前值,如果相等则更新为新值。如果不相等,则说明其他线程已经修改了计数器的值,需要重新获取旧值并重新计算新值,直到成功更新为止。
这里使用了AtomicInteger类的compareAndSet方法来实现CAS操作,保证了原子性和线程安全性。这个方法返回一个布尔值,表示是否成功更新。
这个例子展示了CAS的典型应用场景,即乐观锁的实现。通过CAS操作,我们可以在无锁的情况下实现并发控制,避免了传统锁的开销和线程阻塞。
1.2 乐观锁的版本号实现
public class OptimisticLockExample {
private int value;
private int version;
public OptimisticLockExample() {
this.value = 0;
this.version = 0;
}
public void increment() {
int currentVersion = this.version;
int newValue = this.value + 1;
// 模拟更新数据前的一些其它操作
// 检查版本号是否发生变化
if (this.version == currentVersion) {
this.value = newValue;
this.version++;
} else {
// 版本号发生变化,说明数据已经被其他线程修改,抛出异常或进行其他处理
throw new OptimisticLockException("Concurrent modification detected.");
}
}
public int getValue() {
return this.value;
}
public int getVersion() {
return this.version;
}
public static void main(String[] args) {
OptimisticLockExample example = new OptimisticLockExample();
// 创建并启动多个线程对value进行增加操作
for (int i = 0; i < 100; i++) {
new Thread(() -> {
example.increment();
}).start();
}
// 等待所有线程执行完毕
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value: " + example.getValue());
System.out.println("Final version: " + example.getVersion());
}
}
class OptimisticLockException extends RuntimeException {
public OptimisticLockException(String message) {
super(message);
}
}
通过版本号机制可解决上面CAS实现可能导致的ABA问题
2 悲观锁
1.2 悲观锁的synchronized使用示例
public class PessimisticLockExample {
private int value;
public synchronized void increment() {
// 模拟更新数据前的一些其它操作
this.value++;
}
public int getValue() {
return this.value;
}
public static void main(String[] args) {
PessimisticLockExample example = new PessimisticLockExample();
// 创建并启动多个线程对value进行增加操作
for (int i = 0; i < 100; i++) {
new Thread(() -> {
example.increment();
}).start();
}
// 等待所有线程执行完毕
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value: " + example.getValue());
}
}
在上述代码中,PessimisticLockExample类表示一个使用悲观锁控制并发的例子。increment方法使用synchronized关键字修饰,这样在进入该方法前会对示例对象进行加锁,保证同一时间只有一个线程可以访问该方法。在进行自增操作之前,可以进行一些其它操作。最后,对value进行自增操作。
在main方法中,我们创建了多个线程对value进行增加操作。最后,输出最终的value值。
需要注意的是,使用悲观锁的代价较高,因为每次访问共享资源都需要获取锁。在实际应用中,需要根据具体需求和实际情况选择合适的锁策略。
2 乐观锁和悲观锁的区别
区分 | 乐观锁 | 悲观锁 |
---|---|---|
定义 | 态度乐观,认为别的线程不会同时修改数据,所以不会上锁,但是更新前还是会通过比较的方式判断是否有过更新 | 态度悲观,认为别人一定会修改数据,所以上锁,别的线程想拿数据就会自旋或阻塞 |
实现 | 可以使用 版本号机制和CAS算法 实现(比较和替换是一个原子操作,自旋操作,不断重试) | 如 Synchronized,ReentrantLock 等 |
场景 | 写多读少,冲突多,这种场景如果是乐观锁的话会不断retry反而增加开销 | 读多写少,冲突少,可以省去锁竞争的开销,增加系统吞吐量 |
三、Synchronized
定义: synchronized 是Java的一个关键字,一种悲观锁,自jdk1.6后进行了优化,不在像之前那么重量
特点:
- 悲观锁,可重入锁
- 每个对象锁只能分配给一个线程
- 可自动释放锁
- 根据锁竞争的激烈程度进行升级
示例:
// 互斥锁,可重入锁,
synchronized (this){
System.out.println("只有一个线程能抢占");
}
原理: 根据锁竞争的激烈程度存在一个锁升级的过程,由无锁——》偏向锁——》轻量锁——》重量锁
偏向锁
- 检查到没有人获取到偏向锁的时候,将线程ID存到对象头的MarkWord中,相同线程再次获取锁只需要比较线程id即可
轻量级锁
- 当有线程已经获取锁(偏向锁),其他线程也想获取的时候,偏向锁转为轻量级锁,将MarkWord存到获得锁的线程栈桢中,原MarkWord的位置存一个指向锁记录的指针,锁记录中也会存一个指向锁对象的指针,这样线程和锁对象都存在指向对方的指针,线程就获取了锁。其他线程进行CAS自旋,自旋一定次数获取到锁,那就是获取到轻量级锁,未获取到,就进入阻塞,升级为重量级锁
重量级锁
- 多个线程竞争轻量锁,一个线程获取锁,其他线程自旋无果进入阻塞状态,等待获取锁的线程释放锁之后唤醒阻塞的线程,阻塞——》唤醒,内核态——》用户态的切换,所以是重量级锁
注:对象头中存在MarkWord 和 Class MetaData Address共12个字节
MarkWord: 哈希码,GC分代年龄,锁状态,线程持有的锁,线程ID
Class MetaData Address: 存储对象类型指针,指向类元数据
synchronized 锁哪里比较合适,可以锁对象(对象相同被锁),锁类(任何此类对象都会被锁)
四、ReentranLock
定义: 实现lock接口的可重入锁、悲观锁
特点:
- 读/读、读/写、写/写 操作都不能同时发生,如果读能够以共享锁的方式进行那么会进一步提升性能
- 有两种模式,公平模式与非公平模式,默认是公平模式
- 需要手动释放锁,可重入
- 可通过tryLock方法确定锁是否可用
方法:
方法 | 释义 |
---|---|
boolean tryLock() | 如果资源未被任何其他线程持有,则调用 tryLock() 返回 true,并且持有计数加 1。如果资源不是空闲的,则该方法返回 false,线程不会被阻塞,而是退出 |
boolean tryLock(long timeout, TimeUnit timeUnit) | 线程在退出前等待方法参数定义的一定时间段来获取资源的锁 |
void lock() | 调用 lock() 方法将保持计数加 1,如果共享资源最初是空闲的,则将锁分配给线程,如果锁不可用就阻塞直到锁释放 |
lockInterruptibly() | 如果资源空闲,则此方法获取锁,同时允许线程在获取资源时被其他线程中断。意思是如果当前线程正在等待锁,但是其他线程请求锁,那么当前线程将被中断并抛出异常 |
void unlock() | 调用unlock()方法将持有计数减1。当这个计数达到零时,资源被释放 |
getHoldCount() | 此方法返回资源上持有的锁的数量 |
isHeldByCurrentThread() | 如果当前线程持有资源的锁,则此方法返回 true |
示例:
// 排他锁,可重入锁,参数为TRUE,表示是公平锁模式
ReentrantLock reentrantLock = new ReentrantLock(true);
reentrantLock.lock();
try {
System.out.println("不允许 读读 ");
}catch (Exception e){
e.printStackTrace();
}finally {
// 必须释放锁
reentrantLock.unlock();
}
五、ReentrantReadWriteLock
ReentrantReadWriteLock 读写锁(乐观锁): Reentrant(可重入)Read(读)Write(写)Lock(锁)。
定义: 实现lock接口的读写锁,乐观锁
特点:
- 读写锁,可以分别获取读锁或写锁。也就是说将数据的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。
- 读锁使用共享模式;写锁使用独占模式;读锁可以在没有写锁的时候被多个线程同时持有;获得写锁的线程可获得读锁
- 需要手动释放锁,可重入
- 可通过tryLock方法确定锁是否可用
- 有两种模式,公平模式与非公平模式,默认是公平模式
// 读写锁,可重入锁,参数为TRUE,表示是公平锁模式
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(true);
try {
reentrantReadWriteLock.writeLock().lock();
System.out.println("相比ReentrantLock增加了读的性能");
}catch (Exception e){
e.printStackTrace();
}finally {
reentrantReadWriteLock.writeLock().unlock();
}
注: synchronized 在执行完可以自动释放,而ReentranLock 必须手动释放,为保证不会死锁,要放到finally中,但是更加灵活
五、问题
1. Synchronized和Reentrantlock区别?
区别项 | Synchronized | Reentrantlock |
---|---|---|
锁释放 | 自动释放 | 手动释放 |
灵活性 | 当前类,方法使用 | 可以跨类,跨方法使用 |
可重入性 | 可重入 | 可重入,需要手动释放加锁次数 |
响应中断 | 不可响应中断(获取不到锁就一直等待) | 可中断通过tryLock检测 |
公平锁 | 不支持 | 支持 |
适用范围 | 可加在对象,类,代码块上 | 只能加在代码块上 |
参考博客:
http://t.csdn.cn/LO8Bs
https://blog.csdn.net/m0_50370837/article/details/124471888