Cas与同步与锁:无锁编程

2021/12/14

CAS概念:compare-and-swap原子操作

CAS到底是什么,为什么出现?很多文章都说的稀里糊涂,Wiki百科的解释反而是最清晰的

In computer science, compare-and-swap (CAS) is an atomic instruction used in multithreading to achieve synchronization. It compares the contents of a memory location with a given value and, only if they are the same, modifies the contents of that memory location to a new given value. This is done as a single atomic operation. The atomicity guarantees that the new value is calculated based on up-to-date information; if the value had been updated by another thread in the meantime, the write would fail.

CAS是一条原子操作指令,用于在多线程编程中实现同步,CAS本身就是为了多线程同步而出现的,对于单线程编程没有任何意义。在同步上,CAS的立足点是共享变量,依赖硬件指令,保证了比较+修改的原子性,每次利用CAS写的时候,都要确保之前GET值未被修改,否则更新失败,该操作可以保证写操作都是基于最新的值计算而来,避免了GET->SET之间线程被被打断而引发的风险。

AtomicBoolean如何利用CAS实现无锁同步

CAS本身的定位是原子操作,不具备锁或者synchronization的能力,但它是构建锁的一种非常好的工具,compare-and-swap的原子性为实现synchronization提供了契机:多个线程利用CAS更新一个值时,只有一个会成功,成功代表已获取锁,失败的代表竞争失败,失败的线程可以选择自旋等待或者睡眠等待,这就是锁的概念。可以参考利用AtomicBoolean实现的一个无锁并发编程:先看下在没有锁的情况下会有什么表现:

 <!--无锁编程:多线程 CPU忙等待-->
static volatile boolean condition=false;
  @Override
    public void run() {
        if (!condition) {
         <!--这里有漏洞可以钻,切走-->
           condition = true;
         <!--临界操作-->
         condition = false;
        } }

多线程执行如上逻辑时,会存在问题,如果线程A恰好在执行condition = true这条操作前之前被切走了,还没来得及更新condition,那么其他线程也同样可以满足 if (!condition) ,从而进入临界区,出现多个线程访问临界区从情况。究其原因就是对于condition 的get、compare、set是分开的,如若借助CAS就可以避免上述问题,原子类就是利用CAS实现的类,通过这种类可以修复上述风险:

static  AtomicBoolean  condition = new AtomicBoolean(false);
  @Override
public void run() {
<!--默认false,谁设置为true谁获得成功-->
    if (!condition.getAndSet(true)) {
       <!--临界资源操作-->
       ...           
       <!--恢复-->
       condition.set(false)
    }  
}

AtomicBoolean的getAndSet保证了GET->SET的原子性,中间没有中断,因而不会存在多个线程同时满足 if (!condition.getAndSet(false, true))的情况,一定存在且只存在一个线程在某个时间点满足条件。而AtomicBoolean如何实现的呢?其实就是利用CAS+自旋完成的

//AtomicBoolean.java
private static final long VALUE;
<!--CAS必须跟volatile一起使用,否则由于内存一致性的问题存在,可能导致多个线程同时获取锁-->
private volatile int value;
public final boolean getAndSet(boolean var1) {
    boolean var2;
    do {
        var2 = this.get();
    } while(!this.compareAndSet(var2, var1));
   return var2;
}

compareAndSet通过Java中的Unsafe类实现,核心原则就是只有一个线程能够成功!

    public final boolean compareAndSet(boolean expect, boolean update) {
        return U.compareAndSwapInt(this, VALUE,
                                   (expect ? 1 : 0),
                                   (update ? 1 : 0));
    }

Unsafe底层在不同平台实现各不相同,不需要过多关心。综上所述,CAS其实只能提供原子操作能力,需要CAS配合自旋能才能达到类似synchronization同步锁的目的,不过这种锁是忙等待,如果临界区执行比较耗时,其他任务会一直轮训等待,可能会造成CPU负担过重,不过是睡眠还是自旋这个要有权衡,因为线程的睡眠唤醒也是有成本的。

Java框中的AbstractQueuedSynchronizer[AQS队列同步器]框架:有锁同步[睡眠与唤起]

AbstractQueuedSynchronizer[队列同步器]是并发包的核心,或者说抽象队列同步器就是锁的本体。ReentrantLock、Semaphore(做流量控制)等都是借助AQS模板实现的,而AQS也是借助CAS与同步队列实现的,AQS会把请求获取锁失败的线程放入一个队列的尾部,然后睡眠。加锁仍旧是借助CAS完成,CAS保证只会有一个线程获取锁成功,失败的就进入睡眠,下面借助ReentrantLock【可重入的互斥锁】来看下AbstractQueuedSynchronizer框架的实现,ReentrantLock是一种常用的Java锁,支持公平与非公平两种模式,公平锁与非公平锁的区别是是否一直遵守先来后到,公平锁:直接进入等待队列,先进入先唤醒,而非公平锁支持先来一次抢断,抢断不成功,再退化成排队。实现如下:

非公平锁类似于强制加塞+交警执法,加塞成功,直接抢断,加塞失败,被罚到后面排队

首先看下ReentrantLock的构造函数,很明显可以看出,ReentrantLock默认无参构造方法实现的是非公平锁

public ReentrantLock() {
    sync = new NonfairSync();
}

如果需要使用公平锁,则需要使用有参构造函数

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

NonfairSync与FairSync均继承自ReentrantLock中的静态内部类Sync类,结构

image.png

加锁流程 – 公平锁 即使锁可用,也要看看有没有已经在等待的线程]

ReentrantLock在使用时候,一般是

reentrantLock.lock();
<!--临界代码-->
...
reentrantLock.unLock();

先看下公平锁lock的实现

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

acquire调用的其实是父类AbstractQueuedSynchronizer的acquire方法,acquire进一步调用子类tryAcquire以及自身的acquireQueued,如果无法获取锁,并且满足某些条件则进入睡眠

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果tryAcquire直接就获取成功的话,是无需睡眠的,tryAcquire逻辑如下

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            
		<!--  获取用于同步的state   private volatile int state; 在AbstractQueuedSynchronizer定义-->
            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;
    }
} compareAndSetState将value的值从0更新成1,获得锁,同时将自己设置成占有锁的对象。这个时候,如果有另一个线程进来会怎样,hasQueuedPredecessors仍旧满足条件,但是compareAndSetState会无法满足,从而进入另一半流程,先判断是不是当前线程重新申请锁

if (current == getExclusiveOwnerThread()) 

这部分是可重入锁的原理部分,即一个已经获取锁的线程,重新申请锁,如果是这种情况,则直接更新state即可,也看做锁获取成功,否则认为锁获取失败,假设不是同一个线程,后续如何处理呢?流程会重新回到父类的模板:

   public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

继续走addWaiter 与acquireQueued流程,创建一个enqueues node,再加入队列,然后就可以中断

private Node addWaiter(Node mode) {
    Node node = new Node(mode);
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {//尾部插入,头部唤起,先进先出
        	//给定对象的指定偏移地址的地方设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
        	<!--为什么不用赋值?防止切换么?  设置node的前一个是oldTail,可能吧,不让队列设置被中断-->
            U.putObject(node, Node.PREV, oldTail);
            if (compareAndSetTail(oldTail, node)) { //   U.compareAndSwapObject(this, TAIL, expect, update); tail 变量赋值,保证尾部正确性,尾巴是oldTail,新尾巴是node,有且仅有一个线程符合要求
           <!-- 进入锁,有保证,可以赋值-->  
                oldTail.next = node;
                return node;
            }
        } else {
        <! 构建队列,第一个H=T使用来辅助的吗?感觉无实质意义-->
            initializeSyncQueue();
        }
    }
} 第一次进来调用initializeSyncQueue,初始化SyncQueue,利用U.compareAndSwapObject设置Head,并且tail = h,

private final void initializeSyncQueue() {
    Node h;
    if (U.compareAndSwapObject(this, HEAD, null, (h = new Node())))
        tail = h;
}

之后回到for(;;),将该线程新建的Node加入到该队列,这里核心的同步全部依靠Unsafe类的compareAndSet来实现。 for + compareAndSetTail构成自旋,组要保证只有compareAndSetTail【更新多线程共用的对象都要用CAS】,这保证只有一个线程更新tail成功,这里重点是tail,而不是tail内部的值,将其加入到队列之后,即可返回,其他线程可进入addWaiter流程,本线程acquireQueued流程继续,

final boolean acquireQueued(final Node node, int arg) {
    try {
        boolean interrupted = false;
        <!--注意这里有for (;;),for (;;)的目的是防止临界状态的时候,线程未被唤醒-->
        for (;;) {
        		<!--此时node已经加入队列尾部-->
            final Node p = node.predecessor();
            <!--判断当前加入的是不是第一个,如果是,再次尝试获取一次,如果获取tryAcquire成功,说明正好锁被释放了,可以直接获取 这个时候还没睡眠 ,如果是一个等待线程都没有,那么另一个执行线程不会唤醒水,如果有等待线程才存在唤醒,这里有个标志就是shouldParkAfterFailedAcquire设置的signal-->
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                return interrupted;
            }
            <!--存在执行到这里,另一个释放的临界点-->
            <!--如果node不是第一个节点,那自己基本上要睡眠的  shouldParkAfterFailedAcquire 找到一个可以唤起自己的前驱节点这里其实就是之前第一个 ,第一次shouldParkAfterFailedAcquire设置了signal,返回了false,所以会从新进入for (;;),如果tryAcquire成功,说明释放了,如果失败,也已经加入等待队列,不会被遗漏。-->
         
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } catch (Throwable t) {
        cancelAcquire(node);
        throw t;
    }
}

注意,这里有个for循环,第一次shouldParkAfterFailedAcquire设置了signal,返回了false,所以会从新进入for (;;),如果tryAcquire成功,说明释放了,如果失败,也已经加入等待队列,主要是防止临界点的遗漏。如果加入队列后,还是没有获取到锁,那么parkAndCheckInterrupt会利用LockSupport.park挂起,最终其实是Unsafe的park函数,LockSupport(提供park/unpark操作,睡眠与唤起),是AQS框架中另一个重要类,跟提供CAS的Unsafa共同构建了该体系。

    private final boolean parkAndCheckInterrupt() {
   	 <!--安全挂起-->
        LockSupport.park(this);
        <!--唤醒后判断是否是被中断过,正常不会被中断-->
        return Thread.interrupted();
    }

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker); //设置block,可能给外部看状态用的
    U.park(false, 0L);   //开始阻塞,等待该线程的unpark函数被调用
    setBlocker(t, null);//清理
}

Thread.interrupted()是用来判断是否被设置中断运行,如果被打断过,返回true,结果就是不用处理该任务了,需要终结,否则还是要处理的。从上面的流程可以看出,公平锁是在AQS的基础上实现的,AQS定义了锁的基本框架与功能:

  • CAS的能力
  • acquire :获取锁
  • tryAcquire :尝试获取,试试能不能一次性成功
  • release:释放锁
  • tryRelease:尝试释放
  • Node head、 Node tail队列 :线程等待队列模型,这里的队列对应是Thread等待队列

备注:对于已经获取到锁的线程,后续的操作就不需要任何同步处理,因为就它自己能操作其他的都无法通过CAS更新,那后续也就无需CAS更新,直接赋值即可。

加锁流程 -非公平锁 [如果锁可用,上来就抢]

   static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;


 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()) {
		                int nextc = c + acquires;
		                if (nextc < 0) // overflow
		                    throw new Error("Maximum lock count exceeded");
		                setState(nextc);
		                return true;
		            }
		            return false;
		        }

可以看到非公平锁在当下可用的时候,首先调用CAS尝试将state从0改为1,如果能改成功则表示能够直接获取到锁,那就可以将exclusiveOwnerThread设置为当前线程,不需要公平锁的判断是否有等待队列的操作,如果获取不到,则走后续acquire流程,而公平锁则要看自己是不是第一个,如果是才会去获取,这里之后的流程两者一致都是要睡眠等待唤起。

唤起流程

唤起其实是调用的unlock,无论是否是公平锁,这里的处理逻辑都一样

public void unlock() {
    sync.release(1);
}

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
         <!--   **这里在之前设置过等待信号量  pred.compareAndSetWaitStatus(ws, Node.SIGNAL)**;-->
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease的实现在ReetrantLock中,因为ReetrantLock是可重入锁,所以要看看是不是已经到了最外层的锁,只有到了最外层,才算真正的release:

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }	

之后选择Head之后第一个线程Node进行唤起即可

   private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            node.compareAndSetWaitStatus(ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }        可以看到最终调用的是 LockSupport.unpark(s.thread)将线程唤起。到这里ReentrantLock基本用法的分析就结束了,可以看到它基本是依靠Unsafe的CAS操作+LockSupport的park/unpark实现了锁同步。从上述分析也可以窥探AbstractQueuedSynchronizer框架的一部分,AbstractQueuedSynchronizer实现了线程队列与唤起的基本框架,将lock/unlock的能力交给外部进行定制,只需要实现AbstractQueuedSynchronizer定制的模板,就可以获得不同的锁,但是核心的阻塞/唤起框架已经定了:靠Node队列+CAS更新操作+Unsafe的睡眠/唤起能力实现。	 

ReentrantLock的Condition

ReentrantLock的Condition用来处理生产者-消费者(producer-consumer)问题,即有界缓冲区(bounded-buffer问题,一个是生产者,一个是消费者,除了临界资源的使用,还牵扯缓冲区空与满的处理,如果没有Condition,则只有临界区的概念,producer-consumer模型就不够清晰,这个模型会根据身份与缓存的状态,选择性睡眠与唤醒,而ReentrantLock是无差别的。

  • 当缓冲区已经满了,生产者还想放入新的数据,生产者应该休眠,等待消费者从缓冲区中取走数据后再唤醒它。
  • 当缓冲区已经空了,消费者还想去取消息,可以让消费者进行休眠,待生产者放数据再唤醒它。

很明显这个时候,单纯靠ReentrantLock处理是不友善的

class BoundedBuffer {
   final Lock lock = new ReentrantLock();//锁
   final Condition notFull  = lock.newCondition();//写条件 
   final Condition notEmpty = lock.newCondition();//读条件 

   final Object[] items = new Object[100];//缓存队列
   int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)//如果队列满了 
         notFull.await();//阻塞写线程
       items[putptr] = x;//赋值 
       if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
       ++count;//个数++
       notEmpty.signal();//唤醒读线程
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)//如果队列为空
         notEmpty.await();//阻塞读线程
       Object x = items[takeptr];//取值 
       if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0
       --count;//个数--
       notFull.signal();//唤醒写线程
       return x;
     } finally {
       lock.unlock();
     }
   } 
 }
 

notify() 方法随机唤醒对象的等待池中的一个线程,进入锁池;notifyAll() 唤醒对象的等待池中的所有线程,进入锁池。

在Condition上调用 await() 方法使线程等待,其他线程调用signal() 或 signalAll() 方法唤醒等待的线程 wait一般用于Synchronized中,而await只能用于ReentrantLock锁中

1.Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。 不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。

2.Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition

synchronized锁的实现

多个线程访问同一段代码的时候,如果需要互斥访问某些资源,则需要同步 ,如果只是一个线程访问,无需

synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是 可重入 的

monitorenter

monitorexit

synchronized与Lock的区别

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现; 2)当synchronized块结束时,会自动释放锁,lock一般需要在finally中自己释放。synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁; 3)lock等待锁过程中可以用interrupt来终端等待,而synchronized只能等待锁的释放,不能响应中断。 4)lock可以通过trylock来知道有没有获取锁,而synchronized不能; 5. 当synchronized块执行时,只能使用非公平锁,无法实现公平锁,而lock可以通过new ReentrantLock(true)设置为公平锁,从而在某些场景下提高效率。

6、LLock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离) 7、synchronized 锁类型 可重入 不可中断 非公平 而 lock 是: 可重入 可判断 可公平(两者皆可) 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

CAS的ABA问题

AtomicInteger中volatile value作用 但是解决不了i++类原子操作的问题

volatile 可以保证可见性

:启动两个线程,一个线程修改static 变量,另一个线程读取该变量,看看volatile变量的作用

public class VolatileTest {
    final static int COUNT = 5;
    static int value = 0;
    public static void main(String[] args) {
        new Thread(() -> {
            int tmp = value;
            while (tmp < COUNT) {
            <!--读取并使用-->
                if (value != tmp) {
                    tmp = value;
                    System.out.print("\n R "+value + " "+tmp);
                }
            }
        }, "R").start();
        new Thread(() -> {
            while (value < COUNT) {
                value ++;
                System.out.print("\n W "+value);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                }
            }
        }, "W").start();
    }
}

输出可能是如下情况:

 W 1
 R 1 1
 W 2
 W 3
 W 4
 W 5
 结束

可以看到写线程已经将静态变量value更新成了5,但是R线程中看到的value依旧是1,所以R线程可能就在那个地方死循环了,为value加上volatile之后呢?

    static volatile  int value = 0;

之后输出会变成理想情况:

 W 1
 R 1 1
 W 2
 R 2 2
 W 3
 R 3 3
 W 4
 R 4 4
 W 5
 R 5 5
Process finished with exit code 0

所以volatile修饰的变量其可见性会很时时,一个线程修改后,另一个线程会再次用的时候会立即可见。

防止指令重排

public class NoVisibility { private static boolean ready = false; private static int number = 0;

private static class ReaderThread extends Thread {
    @Override
    public void run() {
        while (!ready) {
            Thread.yield(); //交出CPU让其它线程工作
        }
        System.out.println(number);
    }
}

public static void main(String[] args) {
    new ReaderThread().start();
    number = 42;
    ready = true;
} }

在单一线程中,只要重排序不会影响到程序的执行结果,那么就不能保证其中的操作一定按照程序写定的顺序执行,即使重排序可能会对其它线程产生明显的影响。

synchroniz关键字也能保证可见性

即当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。

无锁编程与乐观锁

CAS:Compare and Swap,比较再交换,从硬件上说,是一条指令,执行两个动作,为什么要这样做:因为有时候需要这样来达到及时更新,从而用于无锁编程。,所以CAS背后其实透露的是一种无锁算法。

CAS到底是什么?是一种操作还是一种思想,还是一个指令

CAS算法

,。

无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

compareAndSwapInt本身是原子操作,不阻塞,本身没有自旋属性,需要外部添加do while才能达到自旋的作用

AtomicBoolean这些类只是提供原子操作的类,本身不算锁,而且本身的初衷是无锁编程,不存在GET,

// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
   int v;
   do {
       v = getIntVolatile(o, offset);
   } while (!compareAndSwapInt(o, offset, v, v + delta));
   return v;
}
 
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
  try {
    valueOffset = unsafe.objectFieldOffset
        (AtomicReference.class.getDeclaredField("value"));
  } catch (Exception ex) { throw new Error(ex); }
}

private volatile V value;

/**
 * Creates a new AtomicReference with the given initial value.
 *
 * @param initialValue the initial value
 */
public AtomicReference(V initialValue) {
    value = initialValue;
}

/**
 * Creates a new AtomicReference with null initial value.
 */
public AtomicReference() {
}

getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。 整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。CPU操作成功会里用MESI内存一致模型,让其同步,

后续JDK通过CPU的cmpxchg指令,去比较寄存器中的 A 和 内存中的值 V。如果相等,就把要写入的新值 B 存入内存中。如果不相等,就将内存值 V 赋值给寄存器中的值 A。然后通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。

System.out.print("普通原子类无法解决ABA问题: ");
        System.out.println(atomicReference.compareAndSet("A", "C") + "\t" + atomicReference.get());
        System.out.print("版本号的原子类解决ABA问题: ");

synchronized锁原理

Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,

Search

    Table of Contents