相关文章参考:

概念

乐观锁

乐观锁不会上锁,只是在执行更新的时候判断一下在此期间别人是否修改了数据:如果别人修改了数据则放弃操作,否则执行操作。

乐观锁本身不会上锁。但是可以与加锁操作合作实现一些业务需求

悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

实现

悲观锁的实现

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题(线程获得锁的顺序不当时),影响代码的正常运行

乐观锁的实现

CAS算法

CAS 的全称是 Compare And Swap(比较与交换)

  1. CAS操作包括了3个操作数:
    1. 需要读写的内存位置(V)
    2. 进行比较的预期值(A)
    3. 拟写入的新值(B)
  2. 操作逻辑:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。(通常情况CAS是自旋的,操作不成功会一直重试)
  3. CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。(其原子性由硬件层面保证)
    示例(java7):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    public class Test {

    //value1:线程不安全
    private static int value1 = 0;
    //value2:使用乐观锁
    private static AtomicInteger value2 = new AtomicInteger(0);
    //value3:使用悲观锁
    private static int value3 = 0;

    private static synchronized void increaseValue3() {
    value3++;
    }

    public static void main(String[] args) throws Exception {
    //开启1000个线程,并执行自增操作
    for (int i = 0; i < 1000; ++i) {
    new Thread(new Runnable() {
    @Override
    public void run() {
    try {
    Thread.sleep(100);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    value1++;
    value2.getAndIncrement();
    increaseValue3();
    }
    }).start();
    }
    //查看活跃线程 ,因守护线程的原因[基于工具问题windows:idea run 启动用 >2,debug 用>1]
    while (Thread.activeCount() > 2) {
    //Thread.currentThread().getThreadGroup().list();
    Thread.yield();//让出cpu
    }

    //打印结果
    Thread.sleep(1000);
    System.out.println("线程不安全:" + value1);
    System.out.println("乐观锁(AtomicInteger):" + value2);
    System.out.println("悲观锁(synchronized):" + value3);
    }
    }

    因为版本太老,分析自行看第一条借鉴文章,总的来说就是乐观与悲观锁都保证了线程安全

版本号机制

  • 版本号机制的基本思路是在数据中增加一个字段version,表示该数据的版本号,每当数据被修改,版本号加1。
  • 常见的思路就是当版本号已经被A更新而B访问原来的版本号,就不会进行操作

乐观锁和悲观锁优缺点和适用场景

  1. 功能限制 与悲观锁相比,乐观锁适用的场景受到了更多的限制,无论是CAS还是版本号机制。
    • 例如,CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS是无能为力的,而synchronized则可以通过对整个代码块加锁来处理。
    • 再比如版本号机制,如果query的时候是针对表1,而update的时候是针对表2,也很难通过简单的版本号来实现乐观锁。
  2. 竞争激烈程度 如果悲观锁和乐观锁都可以使用,那么选择就要考虑竞争的激烈程度:
    • 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
    • 当竞争激烈(出现并发冲突的概率大)时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费CPU资源。

CAS的一些缺点

ABA问题

  • 假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:
    1
    2
    3
    4
    (1)线程1读取内存中数据为A;
    (2)线程2将该数据修改为B;
    (3)线程2将该数据修改为A;
    (4)线程1对数据进行CAS操作
  • 在第(4)步中,由于内存中数据仍然为A,因此CAS操作成功,但实际上该数据已经被线程2修改过了。这就是ABA问题。
  • 但是在某些场景下,ABA却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化。
  • 方案:比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1

高竞争下的开销问题

  • 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。
    • 针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。
    • 当然,更重要的是避免在高竞争环境下使用乐观锁。

功能限制

  • CAS的功能是比较受限的,例如CAS只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:
    1. 原子性不一定能保证线程安全,例如在Java中需要与volatile配合来保证线程安全;
    2. 当涉及到多个变量(内存值)时,CAS也无能为力
    3. 除此之外,CAS的实现需要硬件层面处理器的支持,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受到限制。