乐观锁 VS 悲观锁

一、简介

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

二、悲观锁的实现

乐观锁 VS 悲观锁
对象锁:synchronized

锁在某一个实例对象上,如果该类是单例,那么该锁也具有全局锁的概念。

synchronized是对类的当前实例(当前对象)进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”, 类的两个不同实例就没有这种约束了。

import java.util.concurrent.TimeUnit;
​
public class Test1 {
​
    public synchronized void start() {
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ",start");
    }
​
    public synchronized void end() {
        System.out.println(Thread.currentThread().getName() + ",end");
    }
​
    public static void main(String args[]) {
        Test1 test = new Test1();
        new Thread(test::start, "线程A").start();
        new Thread(test::end, "线程B").start();
    }
}

执行结果:

线程A,start 线程B,end

类锁:static synchronized

该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。

public synchronized void start() {
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ",start");
}
​
public synchronized void end() {
    System.out.println(Thread.currentThread().getName() + ",end");
}
​
public static void main(String args[]) {
    StaticTest a = new StaticTest();
    StaticTest b = new StaticTest();
    new Thread(a::start, "线程A").start();
    new Thread(b::end, "线程c").start();
}

执行结果:

线程A,end 线程c,start

更改start、end方法为static后,执行结果:

线程c,start 线程A,end

synchronized+static synchronized

public class StaticTest {
​
    public synchronized void start() {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ",start");
    }
​
    public static synchronized void end() {
        System.out.println(Thread.currentThread().getName() + ",end");
    }
​
​
    public static void main(String args[]) {
        StaticTest a = new StaticTest();
        StaticTest b = new StaticTest();
        new Thread(() -> a.start(), "线程c").start();
        new Thread(() -> a.end(), "线程A").start();
    }
}

执行结果:

线程A,end 线程c,start

synchronized 与static synchronized 相当于两帮派,各自管各自,相互之间就无约束了,可以被同时访问。

三、乐观锁的实现

乐观锁 VS 悲观锁

以上两图来自美团技术团队

CAS算法
CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
乐观锁 VS 悲观锁

这就容易引起ABA问题
一个线程X1从内存位置V中取出A,这时候另一个线程Y1也从内存中取出A,并且Y1进行了一些操作变成了B,然后Y1又将V位置的数据变成A,这时候线程X1进行CAS操作发现内存中仍然是A,然后X1操作成功。尽管线程X1的CAS操作成功,但是不代表这个过程就是没有问题的。

解决方案CAS类似于乐观锁,即每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。因此解决方案也可以跟乐观锁一样:使用版本号机制,如手动增加版本号字段

上一篇:给大伙讲讲并发编程里的悲观锁和乐观锁


下一篇:[转载]JavaEE学习篇之——网络传输数据中的密码学知识以及Tomcat中配置数字证书EE