乐观锁,悲观锁介绍及常见锁算法

锁类型

悲观锁

事事皆总作最坏的打算,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

事事皆总作最好的打算,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中 java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

应用场景

乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

常见锁算法

乐观锁常见算法

CAS算法

cas 锁(compare and swap) 对比交换

  • CAS算法涉及到三个操作数
    • 需要读写的内存值V
    • 进行比较的值 A
    • 拟写入的新值 B
      当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试
  • 问题
    • ABA 问题:
      如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
      JDK 1.5 以后的 AtomicStampedReference类就提供了此种能力,其中的 compareAndSet方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
    • 循环时间长开销大:
      自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率
    • 只能保证一个共享变量的原子操作:
      CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了 AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用 AtomicReference类把多个共享变量合并成一个共享变量来操作。

版本号算法

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

##常见悲观锁

Java Synchronized

synchronized是Java中的关键字,是一种同步锁。可修饰实例方法,静态方法,代码块。

ADD

什么是自旋锁?

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。 即乐观锁的一种实现。

自旋锁存在的问题

  • 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  • 无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

  • 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  • 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁的变种

MCS锁

MCS自旋锁是一种基於单向链表的高性能、公平的自旋锁,申请加锁的线程只需要在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。


import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class MCSLockV2 {
    /**
     * MCS锁节点
     */
    public static class MCSNodeV2 {
        /**
         * 后继节点
         */
        volatile MCSNodeV2 next;
        /**
         * 默认状态为等待锁
         */
        volatile boolean blocked = true;
    }

    /**
     * 线程到节点的映射
     */
    private ThreadLocal<MCSNodeV2> currentThreadNode = new ThreadLocal<>();
    /**
     * 指向最后一个申请锁的MCSNode
     */
    volatile MCSNodeV2 queue;

    /**
     * 原子更新器
     */
    private static final AtomicReferenceFieldUpdater UPDATER = AtomicReferenceFieldUpdater.newUpdater(
            MCSLockV2.class,
            MCSLockV2.MCSNodeV2.class,
            "queue");

    /**
     * MCS获取锁操作
     */
    public void lock() {
        MCSNodeV2 cNode = currentThreadNode.get();
        if (cNode == null) {
            System.out.println(String.format("Thread %s 初始化 ", Thread.currentThread().getId()));
            // 初始化节点对象
            cNode = new MCSNodeV2();
            currentThreadNode.set(cNode);
        }
        // 将当前申请锁的线程置为queue并返回旧值
        MCSNodeV2 mcsNodeV2 = (MCSNodeV2) UPDATER.getAndSet(this, cNode); // step 1
        if (mcsNodeV2 != null) {
            // 形成链表结构(单向)
            mcsNodeV2.next = cNode; // step 2
            // 当前线程处于等待状态时自旋(MCSNode的blocked初始化为true)
            // 等待前驱节点主动通知,即将blocked设置为false,表示当前线程可以获取到锁
            while (cNode.blocked) {

            }
        } else {
            // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己为非阻塞 - 表示已经加锁成功
            cNode.blocked = false;
        }
    }

    /**
     * MCS释放锁操作
     */
    public void unlock() {
        // 获取当前线程对应的节点
        MCSNodeV2 cNode = currentThreadNode.get();

        if (cNode == null || cNode.blocked) {
            // 当前线程对应存在节点
            // 并且
            // 锁拥有者进行释放锁才有意义 - 当blocked未true时,表示此线程处于等待状态中,并没有获取到锁,因此没有权利释放锁
            return;
        }

        if (cNode.next == null && !UPDATER.compareAndSet(this, cNode, null)) {
            // 没有后继节点的情况,将queue置为空
            // 如果CAS操作失败了表示突然有节点排在自己后面了,可能还不知道是谁,下面是等待后续者
            // 这里之所以要忙等是因为上述的lock操作中step 1执行完后,step 2可能还没执行完
            while (cNode.next == null) {

            }
        }

        if (cNode.next != null) {
            // 通知后继节点可以获取锁
            cNode.next.blocked = false;

            // 将当前节点从链表中断开,方便对当前节点进行GC
            cNode.next = null; // for GC
        }

        // 清空当前线程对应的节点信息
        currentThreadNode.remove();

    }

    /**
     * 测试用例
     *
     * @param args
     */
    public static void main(String[] args) {

        final MCSLockV2 lock = new MCSLockV2();

        for (int i = 1; i <= 10; i++) {
            new Thread(generateTask(lock, String.valueOf(i))).start();
        }

    }

    private static Runnable generateTask(final MCSLockV2 lock, final String taskId) {
        return () -> {
            lock.lock();
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
            lock.unlock();
        };
    }
}
  • MCS锁的节点对象需要有两个状态,next用来维护单向链表的结构,blocked用来表示节点的状态,true表示处于自旋中;false表示加锁成功
  • MCS锁的节点状态blocked的改变是由其前驱节点触发改变的
  • 加锁时会更新链表的末节点并完成链表结构的维护
  • 释放锁的时候由于链表结构建立的时滞(getAndSet原子方法和链表建立整体而言并非原子性),可能存在多线程的干扰,需要使用忙等待保证链表结构就绪

CLH锁

同MCS自旋锁一样,CLH也是一种基於单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。


import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

public class CLHLockV2 {
    /**
     * CLH锁节点状态 - 每个希望获取锁的线程都被封装为一个节点对象
     */
    private static class CLHNodeV2 {

        /**
         * 默认状态为true - 即处于等待状态或者加锁成功(换言之,即此节点处于有效的一种状态)
         */
        volatile boolean active = true;

    }

    /**
     * 隐式链表最末等待节点
     */
    private volatile CLHNodeV2 tail = null;
    /**
     * 线程对应CLH节点映射
     */
    private ThreadLocal<CLHNodeV2> currentThreadCacheNode = new ThreadLocal<>();

    /**
     * 原子更新器
     */
    private static final AtomicReferenceFieldUpdater<CLHLockV2, CLHNodeV2> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLockV2.class, CLHNodeV2.class, "tail");

    /**
     * CLH加锁
     */
    public void lock() {
        CLHNodeV2 cNode = currentThreadCacheNode.get();
        if (cNode == null) {
            cNode = new CLHNodeV2();
            currentThreadCacheNode.set(cNode);
        }
        // 通过这个操作完成隐式链表的维护,后继节点只需要在前驱节点的locked状态上自旋
        CLHNodeV2 clhNodeV2 = UPDATER.getAndSet(this, cNode);
        if (clhNodeV2 != null) {
            // 自旋等待前驱节点状态变更 - unlock中进行变更
            while (clhNodeV2.active) {
                //等待unlock 时被释放
            }
            /**
             * 因为unlock 后 当前线程已经结束 ,所以显示的是 阻塞线程的的下一线程的ID
             *
             */
            System.out.println(String.format("释放锁%s成功", Thread.currentThread().getId()));
        }
        // 没有前驱节点表示可以直接获取到锁,由于默认获取锁状态为true,此时可以什么操作都不执行
        // 能够执行到这里表示已经成功获取到了锁
    }

    /**
     * CLH释放锁
     */
    public void unlock() {
        CLHNodeV2 cNode = currentThreadCacheNode.get();
        // 只有持有锁的线程才能够释放
        if (cNode == null || !cNode.active) {
            return;
        }
        // 从映射关系中移除当前线程对应的节点
        currentThreadCacheNode.remove();
        // 尝试将tail从currentThread变更为null,因此当tail不为currentThread时表示还有线程在等待加锁
        if (!UPDATER.compareAndSet(this, cNode, null)) {
            // 不仅只有当前线程,还有后续节点线程的情况 - 将当前线程的锁状态置为false,因此其后继节点的lock自旋操作可以退出
            cNode.active = false;
        }
    }

    /**
     * 用例
     *
     * @param args
     */
    public static void main(String[] args) {
        final CLHLockV2 lock = new CLHLockV2();
        for (int i = 1; i <= 10; i++) {
            new Thread(generateTask(lock, String.valueOf(i))).start();
        }
    }

    private static Runnable generateTask(final CLHLockV2 lock, final String taskId) {
        return () -> {
            lock.lock();

            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }

            System.out.println(String.format("Thread %s Completed %s ", Thread.currentThread().getId(), taskId));
            lock.unlock();
        };
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章