前言
Java提供了各式各样的锁,在我们日常开发的过程中,为了保证我们的业务逻辑在多线程并发的场景下的正确性、安全性、唯一性,通常会使用锁机制来对业务进行限制,因为每个锁的特性不同,在不同的业务场景下使用不同的锁,才能发挥出它的作用,从而展现出更高的效率,除了JDK自带的锁,还有外部锁包括redis的分布式锁,针对不同的业务场景,应该如何来使用锁的类型,让我一起来看下。
锁的分类
大致分为乐观锁、悲观锁,只是从从广义的看来,细致的分类的话就有无锁、偏向锁、轻量级锁、重量级锁、公平、非公平性锁、可重入、不可重入锁、独享锁、共享锁,这么多的锁类型,用法也是不一样的。
乐观锁
首先来看乐观锁,乐观锁是一种无锁的状态,实现方式就是我们最常采用的CAS算法,CAS的全称是compare and swap(比较并且交换),主要就是维护了3个变量,内存值、预期值、更新值,通过循环去比较内存值和预期值是否相等,如果相等,就把内存值赋到更新值上去,适应于多读的场景。
存在问题
- ABA的问题,一个比较明显的漏洞,存在预期值可能在中途被更改,更改后又更改回来,这个是后CAS操作就会误认为预期值没有被更改,从而进行CAS操作的时候仍然能够操作成功。解决办法,就是在更新变量前加入version版本号概念,每次比较的时候需要去判断版本号是否一致。
- 同时只能保证一个共享变量的原子操作,多个共享变量被操作时,CAS就无法保证原子性了,需要通过悲观锁保证原子性。
- CAS循环比较,存在CPU的性能问题。
应用业务场景
保证接口幂等性的场景下,保证接口不会重复更新数据,在请求更新接口的时候,通过在数据库增加版本字段,更新的时候需要判断版本号是否一致。
数据库层面
update table set version =version+1 where id =#{id} and version =1
代码层面,可以使用基于CAS实现的原子类。
//乐观锁调用方式
AtomicInteger atomicInteger =new AtomicInteger();
atomicInteger.incrementAndGet(); //自增1
悲观锁
多个线程去尝试获得同步的资源,保证同时只允许一个线程通过锁获取到资源,其余的线程需要等待获取到同步资源的线程执行完成后,释放锁才能获取到同步资源,保证写数据时数据的正确性。
ReentrantLock
ReentrantLock,基于AQS队列的同步锁,之前关于AQS队列的文章AQS为什么使用双向FIFO队列 - 掘金 这里又涉及到一个公平和非公平性锁的概念,让我们看源码进行分析。
非公平锁
公平锁
hasQueuedPredecessors方法主要用来判断线程需不需要排队,ReentrantLock底层是FIFO双向队列,需要判断队列中有没有线程节点在排队,这也是和非公平锁的区别,公平锁需判断是否排队,排队返回ture,没有排队返回false优先执行。
存在的问题
- ReentrantLock默认是非公平锁,如果需要使用公平锁需要手动开启,可能是考虑到性能的原因,所以默认是非公平锁。
- 需要在try catch后面执行finally,将锁释放掉,否则当前线程一直获取到锁,其他线程一直处于等待状态,无法获取到锁。
- ReentrantLock,进行一次枷锁操作,只能进行一次释放,不能多次释放,否则会报错
- try里面进行加锁操作,会导致还没加锁成功,就进行finally释放锁,导致try里面其他业务的报错,被加锁报错的异常覆盖掉,无法排查问题。
应用业务场景
加锁排队进行add操作,同时只允许一个线程获取到锁进行业务操作。
/**
* 可重入锁、公平锁
*/
private Lock lock = new ReentrantLock(true);
/**
* 判断是否请求过
*/
public void add (String key) {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
//执行业务逻辑
} finally {
lock.unlock();
}
} else {
log.info("获取锁失败!直接返回!");
}
} catch (Exception e) {
log.warn("获取锁异常! key={} errMsg:" + e.getMessage(), key, e);
}
}
Sychronized
基于JVM的锁,通过monitor enter 和monitor exit这两个JVM命令完成加锁, 从主内存中获取数据,共享变量需要在操作后从本地内存在刷回主内存。 可以作用于锁对象和class类。
存在问题
- 锁的的颗粒度较大
- 没有公平锁,所有的线程竞争锁,先到先得,一些需要排队处理的场景无法进行实现。
- 锁不能进行手动释放,需要的等待当前线程执行完业务后,自己释放锁,其他线程才能获取锁。
- 不能设置超时时间,意味当前获取锁在进行io操作的时时候,必须执行完才能释放锁,否则将一直占用锁资源。
- 无法中断正在获取锁的线程,不够灵活。
应用业务场景
最常见的就是在单例模式中加入sychronized锁,防止对象重复创建,包括在双亲委派机制里面ClassLoader类的loadClass方法里面,通过对需要进行加载的类的对象进行上锁,业务代码中一般不会去用到sychronized锁,主要取决于它的灵活性实在太低。
Double check 加锁
public class Singleton{
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton newInstance(){
//double check 校验对象实例是否已经存在
if(singleton==null){
//对对象加锁,当前只允许一个线程对象实例进行加载
synchronized(Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
ClassLoader顶级类加载机制加锁
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
//.....
}
}
读写锁
ReentrantReadWriteLock
顾名思义读锁和写锁结合,ReentrantReadWriteLock源码ReadLock 和WriteLock作为内部类,也是通过Sync类和AQS队列实现。 但是读锁和写锁的加锁方式有些不同,读锁是共享锁,因为涉及到读读不互斥,写锁作为独占锁,需要在触发写的逻辑的时候,做到写写互斥。
那么读写会是怎样的呢,答案是互斥的,读锁获取锁,写锁也要获取锁,因为写锁是独占你的,所以这样就会造成死锁。
public class ReadWriteLockTest {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void methodReadLock() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":读锁获取成功");
Thread.sleep(1000);
//尝试获取写锁
System.out.println(Thread.currentThread().getName() + ":写锁尝试获取");
writeLock.lock();
System.out.println(Thread.currentThread().getName() + ":写锁获取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println("读锁释放");
}
}
private static void methodWriteLock() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":写锁获取成功");
System.out.println(Thread.currentThread().getName() + ":读锁尝试获取");
readLock.lock();
System.out.println(Thread.currentThread().getName() + ":读锁获取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println("写锁释放");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(() -> methodReadLock(), "线程一");
Thread thread2 = new Thread(() -> methodWriteLock(), "线程二");
thread1.start();
thread2.start();
}
}
[image:A8B0FDD4-656B-4298-A29C-027C3DBA827F-42190-00003BFD17BFAD1A/6C98BB3D-C19C-4960-8053-050C10FD16EC.png]
改下代码,线程一和线程二都获取读锁,最后都获取成功,做到了读读不互斥。
public class ReadWriteLockTest {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void methodReadLock() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":读锁获取成功");
Thread.sleep(1000);
//尝试获取写锁
// System.out.println(Thread.currentThread().getName() + ":写锁尝试获取");
// writeLock.lock();
// System.out.println(Thread.currentThread().getName() + ":写锁获取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(Thread.currentThread().getName()+"读锁释放");
}
}
private static void methodWriteLock() {
// writeLock.lock();
try {
// System.out.println(Thread.currentThread().getName() + ":写锁获取成功");
System.out.println(Thread.currentThread().getName() + ":读锁尝试获取");
readLock.tryLock();
System.out.println(Thread.currentThread().getName() + ":读锁获取成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
// writeLock.unlock();
// System.out.println("写锁释放");
readLock.unlock();
System.out.println(Thread.currentThread().getName()+"读锁释放");
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(() -> methodReadLock(), "线程一");
Thread thread2 = new Thread(() -> methodWriteLock(), "线程二");
thread1.start();
thread2.start();
}
}
存在问题
- 读写交替的场景需要注意,除读读不互斥,读写、写读、写写都是互斥的。
- 一些场景容易造成死锁。
- 获取读锁的速度会比写锁高,在多写的业务场景下不适合。
总结
通过对我们日常开发中使用的一些锁的总结,让我们很清楚的认识到,锁的优缺点,在开发过程中的某些业务场景,就可以避开这些出现的问题,减少我们出现bug的几率,当然在大部分微服务业务的开发场景下,因为是跨服务的,还是使用分布式锁比较多,比如redis、redission、ZooKeeper的分布式锁,这些锁都会在后面专门结合公司的分布式锁的使用场景,整理出来分享出来,如有什么地方说的有误,欢迎大家在评论区留言。