volatile、synchronized、ReentrantLock与CAS

部分摘自:

https://blog.csdn.net/zxh476771756/article/details/78685581

https://blog.csdn.net/tiandao321/article/details/80811103

一、JVM内存模型:

  ==JVM将内存组织为主内存和工作内存两个部分。==

  主内存主要包括本地方法区和堆。

  每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和CPU工作的高速缓存区)。

  1. ==所有的变量都存储在主内存中(虚拟机内存的一部分)==,对于所有线程都是共享的。
  2. 每个线程都有自己的工作内存,==工作内存中保存的是该线程使用到的变量副本(该副本就是主内存中该变量的一份拷贝)==,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
  3. ==线程之间无法直接访问对方的工作内存中的变量==,线程间变量的传递均需要通过主内存来完成。

在这里插入图片描述

Java内存模型(JMM)规定了jvm有主内存,主内存是多个线程共享的。当new一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本。

所以可能会存在这种情况:当线程2更改了xxx变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2已经对xxx变量的更改,因此还会一直循环下去。

二、volatile关键字

==java中volatile解决了可见性问题==

  volatile就是表示某人或某物是不稳定的、易变的

1、volatile保证内存可见性。

    可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

    ==普通变量与volatile变量的区别是:==  

    ==volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。==

2、能禁止指令重排序

所以volatile能在一定程度上保证有序性。

    1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

    2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

  例1:

//x、y为非volatile变量

//flag为volatile变量

x = 2;        //语句1

y = 0;        //语句2

flag = true;  //语句3

x = 4;         //语句4

y = -1;       //语句5

  由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

  并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

  例2:

//线程1:

context = loadContext();   //语句1

inited = true;             //语句2

//线程2:

while(!inited ){

  sleep()

}

doSomethingwithconfig(context);

  而有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。

  这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

3、不能保证原子性

==volatile无法保证对变量的任何操作都是原子性的==,比如i++。

三、synchronized关键字

  synchronized:关键词,它依赖于JVM,保证了同一时刻只能有一个线程作用对象作用范围内进行操作。

1、内存可见性:

  同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的。

  在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁

2、操作的原子性:

  原子性提供了程序的互斥操作,同一时刻只能有一个线程能对某块代码进行操作。

  • synchronized修饰的代码块,作用于 java中volatile解决了可见性问题。
  • synchronized修饰的方法,作用于 java中volatile解决了可见性问题。
  • synchronized修饰的静态方法,作用于这个类的所有对象
  • synchronized修饰的,作用于这个类的所有对象

3、有序性

  java用synchronized关键字做为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。

synchronized(锁){

临界区代码

}

  一个线程执行临界区代码过程如下: 
    1 获得同步锁 
    2 清空工作内存 
    3 从主存拷贝变量副本到工作内存 
    4 对这些变量计算 
    5 将变量从工作内存写回到主存 
    6 释放锁

  可见,synchronized既保证了多线程的并发有序性,又保证了多线程的内存可见性

四、Synchronized和volatile的比较

1)Synchronized保证内存可见性和操作的原子性,Volatile只能保证内存可见性。

2)volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。)

3)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化)

4)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符

五、ReentrantLock

非公平锁和公平锁的两处不同:

  • 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。

  • 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

1、ReentrantLock介绍

ReentrantLock是JDK1.5引入的,它拥有与synchronized相同的并发性和内存语义,并提供了超出synchonized的其他高级功能(例如,中断锁等候、条件变量等),并且使用ReentrantLock比synchronized能获得更好的可伸缩性。

ReentrantLock的实现基于AQS(AbstractQueuedSynchronizer)和LockSupport
AQS主要利用硬件原语指令(CAS compare-and-swap),来实现轻量级多线程同步机制,并且不会引起CPU上文切换和调度,同时提供内存可见性和原子化更新保证(线程安全的三要素:原子性、可见性、顺序性)。

AQS的本质上是一个同步器/阻塞锁的基础框架,其作用主要是提供加锁、释放锁,并在内部维护一个FIFO等待队列,用于存储由于锁竞争而阻塞的线程。

2、关键代码分析

2.1.关键字段

AQS使用链表作为队列,使用volatile变量state,作为锁状态标识位。
    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;//等待队列的头

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;//等待队列的尾

    /**
     * The synchronization state.
     */
    private volatile int state;//原子性的锁状态位,ReentrantLock对该字段的调用是通过原子操作compareAndSetState进行的

    /**
     * Atomically sets synchronization state to the given updated
     * value if the current state value equals the expected value.
     * This operation has memory semantics of a <tt>volatile</tt> read
     * and write.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that the actual
     *         value was not equal to the expected value.
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

2.2.ReentrantLock的公平锁与非公平锁

从ReentrantLock的构造子可以看到,ReentrantLock提供两种锁:公平锁和非公平锁,其内部实现了两种同步器NonfairSync、FairSync派生自AQS,主要才采用了模板方法模式,主要重写了AQS的tryAcquire、lock方法,如下图。

在这里插入图片描述

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

2.3.获取锁操作

 public void lock() {
        sync.lock();
 }

由于NonfairSync、FairSync分别实现了lock方法,我们将分别进行探讨:

NonfairSync.lock()分析

(1)通过原子的比较并设置操作,如果成功设置,说明锁是空闲的,当前线程获得锁,并把当前线程设置为锁拥有者;
(2)否则,调用acquire方法;

package java.util.concurrent.locks.ReentrantLock;
final void lock() {
            if (compareAndSetState(0, 1))//表示如果当前state=0,那么设置state=1,并返回true;否则返回false。由于未等待,所以线程不需加入到等待队列
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
}
 
 package java.util.concurrent.locks.AbstractOwnableSynchronizer  //AbstractOwnableSynchronizer是AQS的父类
 protected final void setExclusiveOwnerThread(Thread t) {
            exclusiveOwnerThread = t;
}

2) acquire方法分析

(1)如果tryAcquire尝试以独占的方式获得锁失败,那么就把当前线程封装为一个Node,加入到等待队列中;如果加入队列成功,接下来检查当前线程的节点是否应该等待(挂起),如果当前线程所处节点的前一节点的等待状态小于0,则通过LockSupport挂起当前线程;无论线程是否被挂起,或者挂起后被激活,都应该返回当前线程的中断状态,如果处于中断状态,需要中断当前线程。

package java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
         if (!tryAcquire(arg) &&
              acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
             selfInterrupt();
}

 
  protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
 }

3) nonfairTryAcquire分析
(1)==如果锁状态空闲(state=0),且通过原子的比较并设置操作,那么当前线程获得锁,并把当前线程设置为锁拥有者==;
(2)如果锁状态空闲,且原子的比较并设置操作失败,那么返回false,说明尝试获得锁失败;
(3)否则,检查当前线程与锁拥有者线程是否相等(表示一个线程已经获得该锁,再次要求该锁,这种情况叫可重入锁),如果相等,维护锁状态,并返回true;
(4)如果不是以上情况,说明锁已经被其他的线程持有,直接返回false;

final boolean nonfairTryAcquire(int acquires) {  
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  //表示一个线程已经获得该锁,再次要求该锁(重入锁的由来),为状态位加acquires
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
 }

4) addWaiter分析

(1)如果tail节点不为null,说明队列不为空,则把新节点加入到tail的后面,返回当前节点,否则进入enq进行处理(2);

(2)如果tail节点为null,说明队列为空,需要建立一个虚拟的头节点,并把封装了当前线程的节点设置为尾节点;另外一种情况的发生,是由于在(1)中的compareAndSetTail可能会出现失败,这里enq采用for的无限循环,是要保证当前线程能够正确进入等待队列;

addWaiter

package java.util.concurrent.locks.AbstractQueuedSynchronizer
   private Node addWaiter(Node mode) {
         Node node = new Node(Thread.currentThread(), mode);
         // Try the fast path of enq; backup to full enq on failure
         Node pred = tail;
         if (pred != null) {  //如果当前队列不是空队列,则把新节点加入到tail的后面,返回当前节点,否则进入enq进行处理。
              node.prev = pred;
              if (compareAndSetTail(pred, node)) {
                  pred.next = node;
                  return node;
              }
         }
         enq(node);
         return node;
     }

enq

package java.util.concurrent.locks.AbstractQueuedSynchronizer
 private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // tail节点为空,说明是空队列,初始化头节点,如果成功,返回头节点
                Node h = new Node(); // Dummy header
                h.next = node;
                node.prev = h;
                if (compareAndSetHead(h)) {
                    tail = node;
                    return h;
                }
            }
            else {   //
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }

5) acquireQueued分析

(1)如果当前节点是队列的头结点(如果第一个节点是虚拟节点,那么第二个节点实际上就是头结点了),就尝试在此获取锁tryAcquire(arg)。如果成功就将头结点设置为当前节点(不管第一个结点是否是虚拟节点),返回中断状态。否则进行(2)shouldParkAfterFailedAcquire。

(2)检测当前节点是否应该park()-"挂起的意思",如果应该park()就挂起当前线程并且返回当前线程中断状态。进行操作(1)。

final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (RuntimeException ex) {
            cancelAcquire(node);
            throw ex;
        }
     } 

6) shouldParkAfterFailedAcquire分析

(1)如果前一个节点的等待状态waitStatus<0,也就是前面的节点还没有获得到锁,那么返回true,表示当前节点(线程)就应该park()了。否则进行(2)。

(2)如果前一个节点的等待状态waitStatus>0,也就是前一个节点被CANCELLED了,那么就将前一个节点去掉,递归此操作直到所有前一个节点的waitStatus<=0,进行(4)。否则进行(3)。

(3)前一个节点等待状态waitStatus=0,修改前一个节点状态位为SINGAL,表示后面有节点等待你处理,需要根据它的等待状态来决定是否该park()。进行(4)。

(4)返回false,表示线程不应该park()。

注意:一个Node节点可包含以下状态以及模式:

        /** waitStatus value to indicate thread has cancelled */    取消
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */   信号等待(在AQS中,是通过LockSupport进行线程间信号交互的) 
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */       条件等待    
        static final int CONDITION = -2;
        /** Marker to indicate a node is waiting in shared mode */  共享模式
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */           独占模式
        static final Node EXCLUSIVE = null;
   /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
 private static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

FairSync.lock()分析

==公平锁相对与非公平锁,在锁的获取实现上,差别只在FairSync提供自己的tryAcquire()的方法实现==,代码如下:

(1)如果锁状态为0,等待队列为空,或者给定的线程在队列的头部,那么该线程获得锁;
(2)如果当前线程与锁持有者线程相等,这种情况属于锁重入,锁状态加上请求数;
(3)以上两种情况都不是,返回false,说明尝试获得锁失败;

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (isFirst(current) &&
                    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;
        }


    final boolean isFirst(Thread current) {
        Node h, s;
        return ((h = head) == null ||
                ((s = h.next) != null && s.thread == current) ||
                fullIsFirst(current)); //头为null,头的下个节点不是空且该节点的线程与当前线程是相等的,
    }

    final boolean fullIsFirst(Thread current) {
        // same idea as fullGetFirstQueuedThread
        Node h, s;
        Thread firstThread = null;//如果头不为空,且头的下个节点也不为空,且该节点的上一个节点是头节点,且该节点的线程不为null
        if (((h = head) != null && (s = h.next) != null &&
             s.prev == head && (firstThread = s.thread) != null))
            return firstThread == current;
        Node t = tail;
        while (t != null && t != head) {
            Thread tt = t.thread;
            if (tt != null)
                firstThread = tt;
            t = t.prev;
        }
        return firstThread == current || firstThread == null;
    }

六、Synchronized与ReentrantLock对比

1、可重入性:

从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。==两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁==。

2、锁的实现

对于==Synchronized==来说,它是java语言的关键字,是原生语法层面的互斥,需要==jvm==实现。而==ReentrantLock==它是JDK 1.5之后提供的==API层面==的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成

3、性能的区别:

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从==Synchronized引入了偏向锁,轻量级锁(自旋锁)==后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

4、功能区别:

便利性:很明显==Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放==,而==ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁==。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

5、ReenTrantLock独有的能力:

  1. ==ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁==。所谓的公平锁就是先等待的线程先获得锁。
  2. ==ReenTrantLock提供了一个Condition(条件)类==,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  3. ==ReenTrantLock提供了一种能够中断等待锁的线程的机制==,通过lock.lockInterruptibly()来实现这个机制。

七、CAS (Compare-and-Swap) 比较并替换--处理器指令

1、CAS指令

CAS指令需要有3个操作数,分别是==内存位置==(在Java中可以简单理解为变量的内存地址,用V表示)、==旧的预期值==(用A表示)和==新值==(用B表示)。

CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作。

2、Unsafe类

在jdk1.5之后,Java程序中才可以使用CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt和compareAndSwapLong等几个方法包装提供,虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程,或者可以认为是无条件关联进去了。

  由于==Unsafe类不是提供给用户程序调用的类==(Unsafe.getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问它),因此,如果不采用反射手段,我们只能通过其他的Java API来间接使用它。

2.1、AtomicInteger

2.1.1、unsafe实例

如==java.util.concurrent包==里面的整数原子类==AtomicInteger==,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。

 // setup to use Unsafe.compareAndSwapInt for updates

  //unsafe实例采用Unsafe类中静态方法getUnsafe()得到,但是这个方法如果我们写的时候调用会报错,
  //因为这个方法在调用时会判断类加载器,我们的代码是没有“受信任”的,而在jdk源码中调用是没有任何问题的

    private static final Unsafe unsafe = Unsafe.getUnsafe();

    private static final long valueOffset;

 

    static {

      try {

        valueOffset = unsafe.objectFieldOffset

            (AtomicInteger.class.getDeclaredField("value"));

      } catch (Exception ex) { throw new Error(ex); }

    }

    private volatile int value;//volatile关键字保证了在多线程中value的值是可见的,任何一个线程修改了value值,会将其立即写回内存当中

2.1.2、getAndIncrement 方法,该方法的作用相当于i++操作

==getAndIncrement==的功能为:
i与current比较,如果相等则把i的值变为next;
这时候可以保证在int next = current + 1;与if();之间不会被其他线程抢占(因为i的值在这段时间内没有变),如果被抢占则会做自旋操作。这就在某种程度上可以实现原子性操作。

这是一种不加锁而实现操作原子化的一种巧妙的编程方式,不仅在java的jvm种,甚至在操作系统的底层并发实现机制中也有CAS的大量应用。

    /**
     * Atomically increments by one the current value.
     *
     * @return the previous value
     */
    public final int getAndIncrement() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return current;
        }
    }

3、CAS缺点

ABA问题:

  比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,然后线程1操作成功。尽管线程1的CAS操作成功,但可能存在潜藏的问题。如下所示:

img

  现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:

  head.compareAndSet(A,B);

  在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:

      img

  此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:

     img

  其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。

  从Java1.5开始JDK的atomic包里提供了一个类==AtomicStampedReference来解决ABA问题==。这个类的==compareAndSet==方法作用是==首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值==。

    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference  预期引用值
     * @param newReference 更新后的引用
     * @param expectedStamp 预期标志
     * @param newStamp 更新后的标志
     * @return true if successful
     */
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

  实际应用代码:

private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);

.......................
atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);

循环时间长开销大:

  ==自旋CAS(不成功,就一直循环执行,直到成功==)如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作:

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是==对多个共享变量操作时,循环CAS就无法保证操作的原子性==。

  这个时候就==可以用锁==,或者==有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作==。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了==AtomicReference类来保证引用对象之间的原子性==,你可以==把多个变量放在一个对象里来进行CAS操作==。

猜你喜欢

转载自www.cnblogs.com/amunamuna/p/11088374.html