【多线程】什么是悲观锁和乐观锁

邓敏 2022年01月30日 111次浏览

悲观锁

概念

悲观锁在已最坏的打算来考虑结果,它会在每次资源操作的同时,都需要对他进行加锁,避免其他的线程来抢占。在绝对上保证我这次执行是没有问题的。

适用场景

悲观锁适用于竞争激励的场景,例如高并发的读写操作。

典型案例

synchronized 关键字

public class TestLock implements Runnable{

    private static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread th01 = new Thread(new TestLock());
        Thread th02 = new Thread(new TestLock());
        th01.start();
        th02.start();
    }

    @Override
    public void run() {
        synchronized (object){
            System.out.println(Thread.currentThread().getName()+"获取到了锁");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"释放了锁");
    }
}

输出结果

Thread-0获取到了锁
Thread-0释放了锁
Thread-1获取到了锁
Thread-1释放了锁

可以看到
当线程Thread-0获取到锁的时候,Thread-1会陷入到阻塞状态,因为这个时候synchronized发挥的是悲观锁,所以Thread-0在执行进入到锁的同时,其他线程是无法获取到锁的状态的

Lock 接口

public class TestLock implements Runnable{

    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread th01 = new Thread(new TestLock());
        Thread th02 = new Thread(new TestLock());
        th01.start();
        th02.start();
    }

    @Override
    public void run() {
        lock.lock();
        System.out.println(Thread.currentThread().getName()+"获取到了锁");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        lock.unlock();
        System.out.println(Thread.currentThread().getName()+"释放了锁");
    }
}

Lock与synchronized的作用是一样的,都是悲观锁,但是Lock的优势要比
synchronized强。平时我们要实现一个悲观锁,推荐使用Lock来使用。具体的优势可以我这里就不再过多的阐述,后续会再会出一片这个文章来专门讲。

乐观锁

概念

乐观锁在Java的线程中,并不会真正的上锁,而是通过以判断初始值的方式来判断当前操作的线程是否有被其他线程来操作过,当乐观锁判断本次执行结束后发现初始值和当时开始执行的初始值一致的时候,那么就代表这次线程执行中,修改的值没有被其他线程修改过,如果不一致那么就是被修改过。

适用场景

乐观锁适用于读比较多,但是写操作比较少的情况,并且并发也不会很高的时候。

典型案例

原子类

AtomicInteger

public class TestLock{

    private static AtomicInteger integer = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread th01 = new Thread(()->{
            while (integer.get()<10000){
                System.out.println(Thread.currentThread().getName()+"输出"+integer.addAndGet(1));
            }
        });
        Thread th02 = new Thread(()->{
            while (integer.get()<10000){
                System.out.println(Thread.currentThread().getName()+"输出"+integer.addAndGet(1));
            }
        });
        th01.start();
        th02.start();
        th01.join();
        th02.join();
        System.out.println(integer.get());
    }
}

上面这段代码其实就是简单的实现了一个类似于i + + 的操作,但是为什么需要用到
AtomicInteger来代替i + + 来实现呢,使用i + + 岂不是更简单快捷一点的吗?其实这里用AtomicInteger的原因就是因为i + + 它不是原子操作,在多线程的环境下会出现少加的情况,但是AtomicInteger就不一样,他是基于CAS算法来实现乐观锁的,并且保证了每次在进行i + + 的情况下会得倒自己想要的值,接下来我们可以来看一下AtomicInteger的实现 + + i的流程。
image.png
在AtomicInteger的代码中,是通过调用compareAndSwapInt方法来实现上面这个流程的,这个方法用了一种比较并交换的机制(Compare And Swap),在这个方法中有几个参数:

var1:传入AtomicInteger对象
var2:AtomicInteger中变量的偏移地址
var5:修改之前的AtomicInteger中的值
var5+var4:预期结果

在compareAndSwapInt开始执行的时候,会先根据传入的AtomicInteger对象和AtomicInteger中变量的偏移地址来获取现在AtomicInteger内存中保存的正确的值,然后和修改之前获取到的AtomicInteger值比较,如果一致那么就开始执行var5+var4,获取预期结果,如果不一致,就会重头再来。

例如上面的流程,我们再用文字阐述一遍
步骤一:初始的AtomicInteger的值为0
步骤二:线程A开始执行,获取到AtomicInteger的值为0;
步骤三:线程A执行暂停,线程B开始获取AtomicInteger的值为0;
步骤四:线程B开始执行 + +
i操作,获取到值为1,并修改了AtomicInteger的值
步骤五:线程B执行完毕,切换到线程A。
步骤六:线程A开始执行 + + i操作,这个时候线程A会再获取一次AtomicInteger的值,发现AtomicInteger的值为1,与修改之前获得的AtomicInteger的值0不同了。比较失败,返回false,继续循环。直到下次只执行的AtomicInteger的值等于修改之前的值,便执行成功。