JAVA锁机制

锁的基本介绍

什么是锁?

  • 在计算机科学中,锁(Lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。

  • 锁通常需要硬件支持才能有效实施。这种支持通常采用一个或多个原子指令的形式来实现。这些指令允许单个进程测试锁是否空闲,如果空闲,则通过单个原子操作获取锁。

  • 锁有几个重要的概念:

    • 锁粒度:是衡量锁保护的数据量大小,通常选择粗粒度的锁(锁的数量少,每个锁保护大量的数据),在当单进程访问受保护的数据时锁开销小,但是当多个进程同时访问时性能很差。因为单个锁保护的数据更多,也就导致了更多的锁竞争。相反,使用细粒度的锁(锁数量多,每个锁保护少量的数据)增加了锁的开销但是减少了锁竞争。例如数据库中,锁的粒度有表锁、页锁、行锁、字段锁、字段的一部分锁。
    • 锁开销(lock overhead):锁占用内存空间、CPU初始化和销毁锁、获取和释放锁的时间。程序使用的锁越多,发生锁竞争的可能性就越小。
    • 锁竞争(lock contention):一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。锁粒度越小,发生锁竞争的可能性就越小。
    • 死锁(deadlock):至少两个任务中的每一个都等待另一个任务持有的锁的情况。
    • 重入锁:一个线程在拥有了当前资源的锁之后,可以再次拿到该锁而不被阻塞。
    • 锁消除:在编译期间利用“逃逸分析技术”分析出那些不存在竞争却加了锁的代码的锁失效。这样就减少了锁的请求与释放操作,因为锁的请求与释放都会消耗系统资源。(默认开启)
      • 另逃逸分析技术,还会将确定不会发生逃逸的对象放在栈内存中,而不是堆内存中,所以说,并不是所有的对象都存在堆内存中的。
    • 锁偏向:当第一个线程请求时,会判断锁的对象头里的Threadld字段的值,如果为空,则让该线程持有偏向锁,并将Threadld的值置为当前线程ID。当前线程再次进入时,如果线程ID与Threadld的值相等,则该线程就不会再重复获取锁了。因为锁的请求与释放是要消耗资源的。如果有其他线程也来请求该锁,则锁升级为轻量级锁。
    • 锁粗化:在编译期间将相邻的同步代码块合并成一个大同步块。这样做可以减少反复申请和释放同一个锁对象导致的系统开销。(默认开启)

锁的分类

img

乐观锁和悲观锁

  • 判断条件:线程是否锁住同步资源

  • 概述:乐观锁与悲观锁是一种广义上的概念,体现了看待线程同步的不同角度。

  • 乐观锁:认为自己在使用数据时不会有别的线程修改数据,所以 不会加锁 ;只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(比如抛出错误或者自动重试等);乐观锁在java中通过无锁编程实现,最常采用的方法是CAS算法、JAVA原子类中的递增操作就通过CAS自选实现。

  • 悲观锁:认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会 先加锁 ,确保数据不会被别的线程修改。

  • 特点:

    • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
    • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
  • 使用方式:

    //悲观锁的调用
    //synchronized方式
    public synchronized void test() {
        /*
        ...  操作资源
        */
    }
    //ReentrantLock方式
    //需要保证多个线程使用的是同一个锁
    private ReentrantLock lock = new ReentrantLock();
    public void modifyPublicResources() {
        lock.lock();
        /*
        ...  操作资源
        */
        lock.unlock();
    }
    //乐观锁的调用
    //AtomicInteger通过CAS实现。更多关于CAS的内容在文章后半部分的无锁编程部分
    private AtomicInteger atomicInteger = new AtomicInteger();
    atomicInetger.incrementAndGet();
    

自旋锁和适应性自旋锁

  • 概述:例如重量级锁这种完全的锁是通过阻塞或唤醒一个JAVA线程来实现,而阻塞或唤醒一个JAVA线程需要操作系统切换CPU的状态,这种状态切换需要耗费一定的处理器资源(时间)。如果同步代码块中的内容过于简单,状态转换消耗的时间可能比执行用户代码的时间还要长,这是不划算的。所以就提出是否有不切换CPU状态就可以实现锁的功能,这在计算机发展的早期或许很难办到,但是在今天,几乎所有物理机的处理器都是多核处理器。因此我们可以让两个或以上的线程同时并行执行,让请求锁的线程不放弃CPU的执行时间,使其空转来等待持有锁的线程释放锁。无论是自旋锁还是适应性自旋锁,都是通过这种方式实现。

  • 自旋锁:线程在没有获得锁时,不是被直接挂起,而是执行一个空循环(自旋)。自旋锁本身是有缺点的,它并不能替代阻塞;自旋等待虽然避免了线程切换的开销,但它要占用处理器的时间。如果锁被占用的时间很短,自旋锁等待的效果将非常优秀;如果长时间没有获得锁而导致CPU空转浪费的资源超过切换状态浪费的资源,那么这是得不偿失的,因此 当自旋超过一定次数时(默认10次),就应当挂起线程。

  • 自适应自旋锁:是对自旋锁的一种优化。 其自选次数不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 如果因为虚拟机认为,既然上次自旋期间成功拿到了锁,那么后面的自选会有很大几率拿到锁,进而允许该线程等待相对更长的时间(也就是自旋的次数更多)。相反,如果对于某个锁,很少有自选能够成功获得,那么下一次也不大可能获得,因此会减少自选次数,甚至不进行自旋,以节约处理器资源。(默认开启)

公平锁和非公平锁

  • 判断条件:多个线程竞争锁时是否需要排队

  • 公平锁:是指多个线程按照申请锁的顺序来获取,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

    • 优点:等待锁的线程不会饿死。
    • 缺点:整体吞吐效率相对较低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
  • 非公平锁:多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁;

    • 优点:减少唤起线程的开销,整体吞吐效率高,因为线程有一定几率不阻塞直接获得锁,CPU不必唤醒所有线程。
    • 有可能出现后申请锁的线程先获取锁的场景,导致处于等待队列的线程可能会饿死也等不到。哎!太惨了!
  • 通过ReentrantLock的源码分析:

    //ReentrantLock
    public class ReentrantLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = 7373984872572414699L;
        /** Synchronizer providing all implementation mechanics */
        private final Sync sync;
    
        //内部类,其继承AQS,添加锁和释放锁的大部分操作都在这里实现
        /**...*/
        abstract static class Sync extends AbstractQueueStnchronizer {...}
    
        /**...*/
        static final class NonfairSync extends Sync {...}
    
        /**...*/
        static final class FairSync extends Sync {...}
    
        /**...*/
        public ReentrantLock() {
            //ReentrantLock默认非公平锁,可以通过构造器指定公平锁
            sync = new NonfairSync();
        }
    
        /**...*/
        public ReentrantLock (boolean fair) {
    		sync = fair ? new FairSync() : new NonFairSync();
        }
    }
    //公平锁
    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;
    }
    //非公平锁
    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)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    
    /**...*/
    public final boolean hasQueuePredecessors() {
        //...
        Node t = tail;   //Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
    }
    /*
    从源码可以看出,公平锁和非公平的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuePredecessors(),看代码可以知道该方法主要的作用是判断当前线程是否位于同步队列中的第一个,如果是则返回true,否则返回false。
    */
    

可重入锁和非可重入锁

  • 判断条件:一个线程中的多个流程是否能获取同一把锁

  • 可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。java中ReentrantLock和Synchronized都是可重入锁,可重入锁的一个优点是可一定程度上避免死锁。

  • 非可重入锁:也叫做自旋锁,线程在外层方法获取锁之后,要想获取内层方法的锁,必须要先释放外层函数的锁。

  • 代码演示:

    public class Main {
    	public synchronized void fun1() {
            //可重入锁在获取当前方法的锁之后可以自动获得fun2()的锁
            //非可重入锁需要释放当前的锁才能获取fun2()的锁,实际上这样会出现死锁,因为没有当前的锁则无法进入当前方法,不释放又获取不到锁
            fun2();
        }
    
        public synchronized void fun2() {
            /*
            ...
            */
        }
    }
    
  • 源码分析:

    /*
    ReentrantLock和NonReentrantLock都继承父类AQS,其父类AQS中维护了一个同步状态status来计数重入次数,status初始为0.
    */
    //可重入锁
    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)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
    
    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;
    }
    //非可重入锁
    protected boolean tryAcquire(int acquires) {
        if (this.compareAndSetState(0, 1)) {
            this.owner = Thread.currentThread();
            return true;
        } else {
            return false;
        }
    }
    
    protected boolean tryRelease(int releases) {
        if (Thread.currentThread() != this.owner()) {
            throw new IllegalMonitorStateException();
        } else {
            this.owner = null;
            this.setState(0);
            return true;
        }
    }
    

独享锁和共享锁

  • 判断条件:多个线程是否能共享一把锁

  • 独享锁:也叫排他锁,指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排他锁的线程即能读数据又能修改数据。(ReentrantLock、Synchronized)

  • 共享锁:指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排他锁。获得共享锁的线程只能读数据,不能修改数据。(ReadWriteLock)

  • 源码分析:

    /*
    独享锁与共享锁也是通过AQS来实现的,通过不同的方法,来实现独享或者共享。例如下面的ReentrantReadWriteLock,其分为读锁和写锁两部分并且都是通过内部的Sync实现。
    */
    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
        private static final long serialVersionUID = -6992448646407690164L;
        /** Inner class providing readlock */
        private final ReentrantReadWriteLock.ReadLock readerLock;
        /** Inner class providing writelock */
        private final ReentrantReadWriteLock.WriteLock writerLock;
        /** Performs all synchronization mechanics */
        final Sync sync;
    
        /**...*/
        public ReentrantReadWriteLock() {
            this(false);
        }
    
        /**...*/
        public ReentrantReadWriteLock(boolean fair) {
            sync = fair ? new FairSync(); new NonfairSync();
            readerLock = new ReadLock(this);
            writeLock = new WriteLock(this);
        }
    
        public ReentrantReadWriteLock.WriteLock writeLock() {
            return writerLock;
        }
        public ReentrantReadWriteLock.ReadLock readLock() {
            return readerLock;
        }
    
    }
    
    public static class ReadLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -5992448646407690164L;
        private final Sync sync;
    
        /**...*/
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }
    
    public static class WriteLock implements Lock, java.io.Serializable {
        private static final long serialVersionUID = -4992448646407690164L;
        private final Sync sync;
    
        /**...*/
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
    }
    /*
    虽然读锁和写锁的主体都是Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock并发性相比一般的互斥锁有了很大提升。
    */
    /*
    读锁和写锁的具体加锁方式,在之前提及AQS的时候,我们讲述了通过state字段(int类型,32位)来表述有多少个线程持有锁。在独享所中,这个值为0或1(如果是重入锁则是重入的次数),在共享锁中为持有锁的数量。但是在ReentrantReadWriteLock中有读锁和写锁两把锁,使用一个整型变量state来表示有点...
    但是还是离谱的实现了,没错! 它通过将整型变量按位分割,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。这个state可太惨了!
    */
    //写锁的加锁方式
    protected final boolean tryAcquire(int acquires) {
    	Thread current = Thread.currentThread();
    	int c = getState(); // 取到当前锁的个数
    	int w = exclusiveCount(c); // 取写锁的个数w,exclusiveCount()方法的原理是取低16位的最大值(就是全是1,也不能这么说,实现方式是还是用的32位的数,只不过低16位为1,高16位为0)与参数做与运算。
    	if (c != 0) { // 判断是否有线程持有了锁,如果已经有线程持有了锁(c!=0)
        // (Note: if c != 0 and w == 0 then shared count != 0)
    		if (w == 0 || current != getExclusiveOwnerThread()) // 如果写线程数(w)为0(换言之存在读锁) 或者持有锁的线程不是当前线程就返回失败
                /*
                做这个操作的原因是确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被获取;并且写锁一旦被获取,其他的读锁请求均被阻塞。
                */
    			return false;
    		if (w + exclusiveCount(acquires) > MAX_COUNT)    // 如果写入锁的数量大于最大数(65535,2的16次方-1)就抛出一个Error。
          throw new Error("Maximum lock count exceeded");
    		// Reentrant acquire
        setState(c + acquires);
        return true;
      }
      if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) // 如果当且写线程数为0(这是读线程也是0,因为上面的c!=0已经过滤过了),并且当前线程需要阻塞那么就返回失败;或者如果通过CAS增加写线程数失败也返回失败。
    		return false;
    	setExclusiveOwnerThread(current); // 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
    	return true;
    }
    //这里说一下写锁的释放,写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁被释放。然后等待的读写线程才能够继续访问读写锁,同时前次写线程的修改对后续的读写线程可见。
    //读锁的枷锁方式
    protected final int tryAcquireShared(int unused) {
        Thread current = Thread.currentThread();
        int c = getState();
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return -1;                                   // 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
        int r = sharedCount(c);
        if (!readerShouldBlock() &&
            r < MAX_COUNT &&
            compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        return fullTryAcquireShared(current);
    }
    //读锁的每次释放(线程安全,可能有多个线程同时释放读锁)均减少读状态,减少的值是'1<<16'。所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥。
    /*
    虽然ReentrantLock有公平锁和非公平锁两种,但是他们添加的都是独享锁。当一个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁柱,那么当前线程在使用CAS更新state成功后就会成功抢占该资源。而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以可以确定ReentrantLock无论是读锁还是写锁,添加的锁都是独享锁。
    */
    

java对象结构

  • 对象头

    • Mark Word:存储对象运行时状态信息(HashCOde,分代年龄和锁标志位信息,都与对象自身定义无关)

      32位Mark Word概览:

      锁状态 25 bit 4 bit 1 bit 2 bit
      23 bit 2 bit 是否偏向锁 锁标志位
      无锁 对象的HashCode 分代年龄 0 01
      偏向锁 线程ID Epoch 分代年龄 1 01
      轻量级锁 指向栈中锁记录的指针 00
      重量级锁 指向重量级锁的指针 10
      GC标记 11
    • Class Point:指针,指向当前对象类型所在方法区中的类型数据

  • 实例数据(成员变量和方法)

  • 填充字节(对象大小必须是8bit的倍数,填充字节就是为了满足这一条件而存在)

synchronized(悲观锁)

  • javac编译后会发现其依赖monitorenter和monitorexit来实现线程同步

  • Monitor:管程/监视器,通常被描述为一个对象。每一个java对象就有一把看不见的锁,称为内部锁或者Monitor锁。Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

  • 简述流程:

    • 一个线程A从Entry Set进入Monitor
    • 其因为一些IO条件需要暂时让出Monitor
    • 线程A进入Wait Set
    • 线程B进入Monitor
    • 线程B执行完成
    • 线程B通过notify的形式唤醒线程A并退出Monitor
    • 线程A执行完成并退出
  • image-20201210132142032

  • synchronized可能存在的性能问题

    • 由于synchronized依赖monitorenter和monitorexit实现,而monitor依赖操作系统的mutex lock来实现的,java线程实际上是对操作系统的映射,所以每当挂起或者唤醒一个线程,都要切换操作系统内核态,而这对系统的性能产生很严重的影响

    • 因此用java6开始,synchronized进行了优化,引入了偏向锁、轻量级锁,所以锁总共有四种状态(四种状态的锁只能升级不能降级),即

      • 无锁:没有对资源进行锁定,所有线程都可以对同一资源进行访问

        • 若想不加锁而实现多线程访问的限制,比如有多个线程修改同一个值,我们只需要一个线程修改成功,其他修改失败的线程不断重试,直到修改成功。
        • 我们可以通过CAS(Compare and Swap)来实现,CAS在操作系统中通过一条指令来实现,所以其能够保证原子性;这就是无锁编程的方式之一
      • 偏向锁

        • 设计构想:如果不想通过monitor或者CAS的方式来实现对资源加锁,而是采用资源识别线程的方式来实现加锁同样的效果,则可以更加节省资源。
        • 具体步骤:
          • 在对象头中检查锁标志位是否为01
          • 若不是,则不可能是偏向锁
          • 若是,则检查前一位是否为1
          • 若不是,则为无锁
          • 若是,则为偏向锁,此时读取前23bit的值(即线程ID)
          • 通过线程ID来确认当前想要获得对象锁的线程是否是指定线程
          • 如果此时有多个线程正在竞争锁,那么偏向锁自动升级为轻量级锁
      • 轻量级锁

        • 具体步骤:
          • 在对象头中检查锁标志位为00,则其为轻量级锁
          • 此时线程在自己的虚拟机栈中开辟一块叫做Lock Record的空间(存放对象头中Mark Word的副本以及owner指针)
          • 线程通过CAS尝试获取锁,获得后则复制该对象头中Mark Word到Lock Record中,并且将Lock Record中的owner指针指向该对象
          • 对象头中的Mark Word中前30 bit生成一个指针,指向线程虚拟机栈中的Lock Record,此时便实现了线程和对象锁的相互绑定,并知道了对方的存在
          • 当其他线程想要获取锁时则自旋等待
        • 自旋
          • 什么是自旋:自旋就是线程自动的进行轮询,每过一段时间就检查锁是否被释放,如果释放则获取,如果没有释放则进行下一轮循环
          • 自旋和被操作系统挂起的区别:当锁很快被释放时,自旋不需要进行系统中断和现场恢复,所以其效率更高(但是由于自旋相当于CPU在空转,如果长时间自旋将会浪费CPU资源)
          • 适应性自旋:为了减少自旋过程中的CPU资源浪费,自旋时间不再是固定的,而是由上一次在 同一个锁上的自旋时间 以及 锁状态 这两个条件来决定。
          • 假如自旋等待的轻量级锁超过一个,或者自旋次数超过n次,轻量级锁则升级为重量级锁
      • 重量级锁

        • 假如锁标志位为10,即锁为重量级锁
        • 则会通过Monitor来对线程进行控制,此时将会完全锁定资源

无锁编程

  • 特点:

    • 优点:难度相对较高,控制更加复杂更加容易出错
    • 缺点:因为不需要对资源进行锁定,所以就不需要调用操作系统底层的同步原语(比如mutex);这样就能大幅度减少操作系统内核态、用户态的切换,能够极大提升多线程并发的性能
  • 互斥锁(悲观锁):(这玩意不是无锁编程,写在这里只是为了对比)

    • 特点:悲观(如果不严格同步线程调用,那么一定会产生异常)
    • 会将资源锁定,只供一个线程调用,而阻塞其他线程
    • 如果大部分操作都是读操作,那么将会浪费大量的资源
  • CAS(Compare And Swap)(乐观锁):

    • 特点:乐观(当线程需要修改共享资源的对象时,总是会乐观的认为对象状态值没有被其他线程修改过,而是每次自己都会主动尝试去compare状态值)

    • CAS执行原理:

      • 给资源设置一个状态值(默认状态下为0即代表资源空闲)
      • 当线程读取到资源状态值为0时会各自产生两个值
        • old value(之前读到的资源状态值)
        • new value(想要将资源对象的状态值更新后的值)
      • 当多个线程读取到资源状态值为0后,其old value都为0,new value都为1
      • 当一个线程获得时间片后,其会将old value与资源状态值进行compare,发现一致时进行swap,将状态值改为new value的值
      • 此时资源对象的状态值已经改变,其他线程进行compare时将会发现状态值与预期的old value不一致,则放弃swap 操作(实际操作时,会让其进入自旋状态,即不断的重试CAS操作,而不是放弃;为了防止无限制的自旋会产生死锁,通常为配置自选次数)
    • CAS代码演示:

      //这段代码只是演示CAS的实现原理,并不能拿来用
      int cas(long *addr, long oldValue, long newValue) {
          /* Executes atomically. */
          if (*addr != oldValue)
              return 0;
          *addr = newValue;
          return 1;
      }
      
    • CAS实例代码:

      /*
       *使用3条线程,将一个值,从0累加到1000
       */
      import java.util.concurrent.atomic.AtomicInteger;
      
      public class Main{
          //AtomicInteger即是底层通过CAS实现同步的计数器
          static AtomicInteger num = new AtomicInteger(0);
      
          public static void main(String[] args) {
              for (int i = 0; i < 3; i++) {
                  Thread t = new Thread(new Runnable) {
                      @Override
                      public void run() {
                          //synchronized(this) {   //这里是互斥锁的方式
                          	//CAS 
                              while(num。get() < 1000) {
                                  System.out.println("thread name:" + Thread.currentThread().getName() + ": " + num.incrementAndGet());
                              }
                          //}
                      }
                  }
              }
          }
      }
      /*
       *AtomicInteger的incrementAndGet()通过unsafe的CAS来实现无锁编程,CAS可以通过启动参数配置自选次数,不配置默认为10次
       */
      // AtomicInteger 自增方法
      

    public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    // JDK 8 ---- Unsafe.java
    public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
    }

    //OpenJDK 8 --- Unsafe.java
    public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do { //循环获取给定对象o的偏移量储的值v,然后判断内存值是否等于v。
    v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta)); //如果相等,则将内存值设置为v+delta,否则,返false,继续循环进行重试,直到设置成功并返回旧值。
    return v;
    //整个比较和更新的操作封装在compareAndSwapInt()中,在JNI里是借助一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
    //后续jdk铜鼓哟cpu的cmpxchg指令,去比较寄存器中的值A和内存中的值V。如果相等,就把要写入的新值B写入内存。如果不相等,就将内存值V赋值给寄存器中的值A。然后通过java代码中的while循环再次调用cmpxchg执行进行重试,直到设置成功为止。
    //同时,CAS并不是完美无缺的,他也存在一些问题,比如ABA问题,循环时间长开销大,只能保证一个共享变量的原子操作等。这里不做详述。
    }

    
    - 关于unsafe的简单功能介绍:
    
    - CAS
    - Class相关:动态创建类(普通类或匿名类)/获取field的内存地址偏移量/监测、确保类初始化
    - 对象操作:获取对象成员属性在内存偏移量/非常规对象实例化/存储、获取指定偏移地址的变量值(包含延迟生效、volatile语义)
    - 内存操作:分配、拷贝、扩充、释放堆外内存/设置、获得给定地址中的值
    - 数组相关:返回数组元素内存大小/返回数组首元素偏移地址
    - 内存屏障:禁止load、store重排序
    - 系统相关:返回内存页大小/返回系统指针大小
    - 线程调度:线程挂起、恢复/获取、释放锁

Q.E.D.


If you don't come, I will snow.