并发编程----乐观锁、悲观锁、可重入锁…..

作为一个Java开发多年的人来说,肯定多多少少熟悉一些锁,或者听过一些锁。今天就来做一个锁相关总结。

关于乐观锁、悲观锁、可重入锁...._乐观锁

需要高清图,进入公众号联系我

悲观锁和乐观锁

悲观锁

顾名思义,他就是很悲观,把事情都想的最坏,是指该锁只能被一个线程锁持有,如果A线程获取到锁了,这时候线程B想获取锁只能排队等待线程A释放。

在数据库中这样操作:

select user_name,user_pwd from t_user for update;
乐观锁

顾名思义,乐观,人乐观就是什么事都想得开,闯到桥头自然直。乐观锁就是我都觉得他们都没有拿到锁,只有我拿到锁了,最后再去问问这个锁真的是我获取的吗?是就把事情给干了。

典型的代表:CAS=Compare and Swap 先比较哈,资源是不是我之前看到的那个,是那我就把他换成我的。不是就算了。

在Java中java.util.concurrent.atomic包下面的原子变量就是使用了乐观锁的一种实现方式CAS实现。

通常都是 使用version、时间戳等来比较是否已被其他线程修改过。

update t_user set name="Java后端技术全栈" where t_version=1
使用悲观锁还是使用乐观锁?

在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。

响应效率

如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。

冲突频率

如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。
重试代价

如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。

乐观锁如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。

公平锁和非公平锁

公平锁

顾名思义,是公平的,先来先得,FIFO;必须遵守排队规则。不能僭越。多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。


ReentrantLock中默认使用的非公平锁,但是可以在构建ReentrantLock实例时候指定为公平锁。

ReentrantLock fairSyncLock = new ReentrantLock(true);

假设线程 A 已经持有了锁,这时候线程 B 请求该锁将会被挂起,当线程 A 释放锁后,假如当前有线程 C 也需要获取该锁,那么在公平锁模式下,获取锁和释放锁的步骤为:

  1. 线程A获取锁--->线程A释放锁

  2. 线程B获取锁--->线程B释放锁;

  3. 线程C获取锁--->线程释放锁;

优点

所有的线程都能得到资源,不会饿死在队列中。

缺点

吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,CPU唤醒阻塞线程的开销会很大。

非公平锁

顾名思义,老子才不管你们谁先排队的,也就是平时大家在生活中很讨厌的。生活中排队的很多,上车排队、坐电梯排队、超市结账付款排队等等。但是不是每个人都会遵守规则站着排队,这就对站着排队的人来说就不公平了。等抢不到后再去乖乖排队。

多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。


上面说过在ReentrantLock中默认使用的非公平锁,两种方式

ReentrantLock fairSyncLock = new ReentrantLock(false);

或者

ReentrantLock fairSyncLock = new ReentrantLock();

都可以实现非公平锁。

优点

可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤起线程的数量。

缺点

大家可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

独享锁和共享锁

独享锁

独享锁也叫排他锁/互斥锁,是指该锁一次只能被一个线程锁持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排他锁的线程既能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排他锁。获得共享锁的线程只能读数据,不能修改数据。

对于ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。

  1. 读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。

  2. 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

可重入锁

若当前线程执行中已经获取了锁,如果再次获取该锁时,就会获取不到被阻塞。

public class RentrantLockDemo {
    public synchronized void test(){
        System.out.println("test");
    }

    public synchronized void test1(){
        System.out.println("test1");
        test();
    }

    public static void main(String[] args) {
        RentrantLockDemo rentrantLockDemo = new RentrantLockDemo();
        //线程1
        new Thread(() -> rentrantLockDemo.test1()).start();
    }
}

当一个线程执行test1()方法的时候,需要获取rentrantLockDemo的对象锁,在test1方法汇总又会调用test方法,但是test()的调用是需要获取对象锁的。

可重入锁也叫递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。

关于锁是如何实现可重入的,请参考前面的文章。

推荐一起看一下文章

快速掌握并发编程---深入学习Condition

快速掌握并发编程---细说ReentrantLock和AQS

快速掌握并发编程---synchronized篇(上)

快速掌握并发编程---synchronized篇(下)


老铁,点个在看是最大的鼓励