知识点二:CAS,Synchronized,AQS,ReentranLock

CAS

  1. CAS 是一种乐观锁,给变量设定一个期望值,当变量和期望值相等后才能做修改,整个比较和修改的过程,是一个整体的原子化操作,保证了多线程同时修改变量时,只有一个线程可以修改成功;

  2. 操作上,使用 3 个操作数,需要读写的内存值 V,期望值 A,准备修改的新值 B,只有 V 与 A 相同时,才会更新 V 为值 B,自旋就是将这个比较修改的过程,不断尝试,直到成功或到达一个指定次数;

  3. 使用上,通常直接使用 AtomicXXX 类,内部就是 CAS 实现,性能上优于 synchronized 等悲观锁,缺点是如果是热点资源,CAS 会大量不成功,并且可能出现 ABA 问题;

	本身乐观锁就适合冲突不是很激烈的资源上,这样提高了 CAS 的成功率,如果竞争激烈的资源,适合悲观锁;
  1. ABA 问题是,如果一个线程 CAS 改变一个变量,从 A 改成 B,然后再从 B 改成 C,这两个操作都成功,但并不能说明没有其他线程对共享变量进行了修改,因为在线程 从 A 改成 B,到 从 B 改成 C 的间隙中,可能出现另一个线程将变量从 B 改成了 D,然后又从 D 改回了 B,这样从第一个线程来看,好像是整个操作过程没有其他线程改变变量值;

  2. 解决 ABA 的问题,首先看业务场景是否要避免,如果需要可以使用 AtomicStampedReference 在CAS 操作时添加一个版本号,修改时比较版本号是否一致,或者使用 synchronized 悲观锁来代替;

Synchronized

  1. 修饰在实例方法,在代码块上,保证多线程执行该代码块是串行执行,是通过加锁实现的,获取到锁的线程才允许执行,可以在对象实例,Class 对象,指定对象上进行加锁;

  2. 每个 Java 对象都有一个监视器锁对象 monitor,是由 C++ 实现,java 对象存储在堆上,其对象头中有一个锁标志的信息,记录的就是 monitor 对象的持有线程和锁计数值;

  3. 锁对象的计数,能实现 synchronized 的可重入特性,同一线程,每次加锁,计数会加 1,每次释放锁,计数会减 1,直到计数为0,就表示当前线程已释放锁;

  4. synchronized 的加锁过程是 jvm 的底层实现,c++ 代码编写,是通过锁升级的方式,并且锁升级的过程是不可逆;

	线程进入同步代码块,首先判断锁对象的锁标志是否是当前线程,即在锁对象的对象头中,判断记录的 monitor 对象的持有线程和计数值,如果是当前线程,因为可重入,计数值 +1,直接加锁成功,可以执行同步代码块
	如果不是当前线程,尝试 CAS 改变锁标志为自身线程,如果改变成功,表示加锁成功,其他线程没有持有锁,可以执行同步代码块
	如果 CAS 失败,进入短暂自旋 CAS 操作,默认是 10 次,中间成功,同样继续执行同步代码块
	如果自旋 CAS 还是无法成功,就会升级为重量级锁,保证线程安全,需要在用户态和内核态之间的切换,耗资源程度高;	
  1. 使用 synchronized 加锁的线程,在阻塞等待锁的过程,是不可中断的;

AQS

  1. 由一个状态变量 state,一系列能够更新和检查该状态变量值的操作方法,和一系列能够唤醒其他修改该状态变量的线程的方法;

  2. AQS 使用 FIFO 的队列来管理修改状态变量的线程,每个线程都尝试通过 CAS 改变状态变量,失败的线程会放在队列中排队,成功的线程就改变了 state 的值,保证其他线程都会失败排队;

  3. 成功的线程执行完成后,会改回 state 的值,会通知 FIFO 队列中的对头保存的线程, 实现一个公平锁的机制;

  4. 更多参考:https://cong-onion.cn/archives/AQS-and-CAS/;

ReentranLock

  1. ReentranLock 是基于 AQS(AbstractQueuedSynchronizer) 来实现,其中成员变量 state,为 0 就表示没有锁,其他值表示锁被持有,和相应的加锁次数;

  2. 加锁方法 lock,使用 CAS 改变 state,期望值是 0,要更新值是 1,设置成功,加锁成功,设置 AQS 的持有锁的线程为当前线程;

	if (compareAndSetState(0, 1)){
       setExclusiveOwnerThread(Thread.currentThread());}
    else{
       acquire(1);
    }
  1. 如果 CAS 改变 state 失败,会先执行 tryAcquire(1),再次尝试 CAS 改变 state,成功,则加锁成功,如果失败,执行 addWaiter(Node.EXCLUSIVE), arg) 将当前线程构造成一个排他的数据节点,放到 FIFO 的链表末尾,然后调用 acquireQueued(node),执行一个自旋(死循环),判断链表头是否是自己构造的节点,如果是,再次使用 CAS 设置是 state;
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                ...
                ...
                ...
	protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

  1. 释放锁方法 unlock,是状态变量 state 值进行减一操作,并调整 FIFO 的链表头;

Synchronized 和 Lock 的区别;

  1. synchronized 是 JVM 底层的实现,Lock 是 JDK 层面的实现;

  2. synchronized 会自动释放锁,Lock 必须手动释放锁;

  3. synchronized 只能是非公平锁,Lock 可以设置是公平锁,或者非公平锁,

  4. Lock 还可以设置为读锁,提高并发度,并且提供了非阻塞式的加锁,tryLock方法,允许设置一个超时时间来加锁;

  5. synchronized 是不可中断的,Lock 可以设置为可中断或不可中断;

  6. 建议在资源竞争不激烈的情况下,lock 更适合,因为大量使用到 CAS;

你可能感兴趣的:(知识点,数据库,redis,java,缓存,分布式)