美文网首页Android进阶之路Android技术知识
Android高工进阶必会技能——synchronized锁升级

Android高工进阶必会技能——synchronized锁升级

作者: 码农的地中海 | 来源:发表于2022-07-08 16:59 被阅读0次

    背景

    synchronized是Java实现同步的一种机制,它属于Java中关键字,是一种jvm级别的锁。synchronized锁的创建和释放是此关键字控制的代码的开始和结束位置,锁是有jvm控制的创建和释放的,正是因为这一点,synchronized锁不需要手动释放,哪怕是代码出现异常,jvm也能自动释放锁。同时jvm也记录的使用锁的线程,以及哪些线程出现了死锁这非常有利于我们排查问题。


    a2013fee9a75ea3f6c22481a2c4276b7.jpeg

    使用优缺点

    优点

    1.synchronized所不用手动释放锁,即便抛出异常jvm也是让线程自动释放锁

    2.当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源

    缺点

    1.使用synchronized如果其中一个线程不释放锁,那么其他需要获取锁的线程会一直等待下去,等待的线程不能中途中断,直到使用完释放或者出现异常jvm会让线程自动释放锁

    2.也无法通过投票得到锁,如果不想等下去,也就没法得到锁

    3.同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况
      4.在激烈争用情况下更佳的性能,也即是如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作,性能比较低。因为当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息,这些虽然对调试非常有价值,因为它们能标识死锁或者其他异常行为的来源,但是这势必会增大资源的消耗和耗时增加。

    Synchronized锁升级、降级原理

    多线程中锁的升级

    synchronized锁升级原理:在锁对象的对象头里面有一个threadid字段,在第一次访问的时候threadid为空,jvm 让其持有偏向锁,并将threadid 设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致。

    如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized 锁的升级。

    锁的升级的目的

    锁升级是为了减低了锁带来的性能消耗。在Java 6之后优化 synchronized的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。

    多线程中锁的降级

    我注意到有的观点认为Java不会进行锁降级。实际上据我所知,锁降级确实是会发生的。

    具体的触发时机:在全局安全点(safepoint)中,执行清理任务的时候会触发尝试降级锁。
    当锁降级时,主要进行了以下操作:

    • 恢复锁对象的markword对象头;
    • 重置ObjectMonitor,然后将该ObjectMonitor放入全局空闲列表,等待后续使用。

    拓展知识

    Java对象在堆内存的构成

    在JVM中,对象在堆内存中分为三块区域:

    (1)对象头

    对象头相当于对象的元数据信息,对象头由两部分组成:

    (a)Mark Word(标记字段)

    存储对象的HashCode、分代年龄和锁标志位信息,在运行期间Mark Word里存储的数据结构会随着锁标志位的变化而变化,Mark Word的结构图如下,图摘自链接3:

    image.png

    Mark Word结构图

    上面提到了Mark Word被设计成一个非固定结构,在运行期间会随着锁标志位的变化而变化,上图中一个锁标志位所在的一行数据结构就对应一种Mark Word结构。

    (b)Klass Pointer(类型指针)

    对象指向它的类元数据的指针,JVM通过这个指针来确定对象是哪个类的实例。

    (2)实例数据

    这部分主要存放类的数据信息和父类信息。

    (3)填充数据

    JVM要求对象的起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

    synchronized锁升级流程

    synchronized锁/重量级锁

    这里主要介绍一下通常说的synchronized锁或者重量级锁的底层实现原理

    Monitor对象

    我们经常说synchronized关键字获得的是一个对象锁,那这个对象锁到底是什么?

    每一个对象的对象头会关联一个Monitor对象,这个Monitor对象的实现底层是用C++写的,对应在虚拟机里的ObjectMonitor.hpp文件中。

    Monitor对象由以下3部分组成:

    (1)EntryList队列

    当多个线程同时访问一个Monitor对象时,这些线程会先被放进EntryList队列,此时这些线程处于Blocked状态;

    (2)Owner

    当一个线程获取到了这个Monitor对象时,Owner会指向这个线程,当线程释放掉了Monitor对象时,Owner会置为null;

    (3)WaitSet队列

    当线程调用wait方法时,当前线程会释放对象锁,同时该线程进入WaitSet队列。

    Monitor对象还有一个计数器count的概念,这个count是属于Monitor对象的,而不属于某个获得了Monitor对象的线程,当Monitor对象被某个线程获取时,++count,当Monitor对象被某个线程释放时,--count。

    同步代码块和同步方法

    synchronized关键字可以修饰方法,也可以修饰代码块,二者底层的实现稍有不同。

    (1)同步代码块
    public void method(){        synchronized(new Object()){          do something...        }    }
    
    • 当进入method方法的synchronized代码块时,通过monitorenter指令获得Monitor对象的所有权,此时count+1,Monitor对象的owner指向当前线程;
    • 如果当前线程已经是Monitor对象的owner了,再次进入synchronized代码块时,会将count+1;
    • 当线程执行完synchronized代码块里的内容后,会执行monitorexit,对应的count-1,直到count为0时,才认为Monitor对象不再被线程占有,其他线程才可以尝试获取Monitor对象。
    (2)同步方法

    当线程调用到方法时,会判断一个标志位:ACC_SYNCHRONIZED。当方法是同步方法时,会有这个标志位,ACC_SYNCHRONIZED会去隐式调用那两个指令:monitorenter和monitorexit去获得和释放Monitor对象。

    归根到底,synchronized关键字还是看哪个线程获得了对象对应的Monitor对象。

    锁升级过程

    JDK1.6之前,synchronized的实现涉及到操作系统实现线程之间的切换时需要从用户态切换为核心态,这是很消耗资源的,这也是早期synchronized锁称为“重量级”锁的原因,jdk1.6之后对synchronized锁进行了优化,引入了偏向锁和轻量级锁的概念,即synchronized锁有具体4种状态,这几个状态会随着竞争程度逐渐升级,就是锁升级。

    synchronized锁的4种状态

    synchronized锁有无锁、偏向锁、轻量级锁和重量级锁4种状态,在对象头的Mark Word里有展示,锁状态不同,Mark Word的结构也不同。

    (1)无锁

    很好理解,就是不存在竞争,线程没有获取synchronized锁的状态。

    (2)偏向锁

    即偏向第一个拿到锁的线程,锁会在对象头的Mark Word通过CAS(Compare And Swap)记录获得锁的线程id,同时将Mark Word里的锁状态置为偏向锁,是否为偏向锁的位也置为1,当下一次还是这个线程获取锁时就不需要通过CAS。

    如果其他的线程尝试通过CAS获取锁(即想将对象头的Mark Word中的线程ID改成自己的)会获取失败,此时锁由偏向锁升级为轻量级锁。

    (3)轻量级锁

    JVM会给线程的栈帧中创建一个锁记录(Lock Record)的空间,将对象头的Mark Word拷贝到Lock Record中,并尝试通过CAS把原对象头的Mark Word中指向锁记录的指针指向当前线程中的锁记录,如果成功,表示线程拿到了锁。如果失败,则进行自旋(自旋锁),自旋超过一定次数时升级为重量级锁,这时该线程会被内核挂起。

    (4)自旋锁

    轻量级锁升级为重量级锁之前,线程执行monitorenter指令进入Monitor对象的EntryList队列,此时会通过自旋尝试获得锁,如果自旋次数超过了一定阈值(默认10),才会升级为重量级锁,等待线程被唤起。

    线程等待唤起的过程涉及到Linux系统用户态和内核态的切换,这个过程是很消耗资源的,自选锁的引入正是为了解决这个问题,先不让线程立马进入阻塞状态,而是先给个机会自旋等待一下。

    (5)重量级锁

    在2中已经介绍,就是通常说的synchronized重量级锁。

    锁升级过程

    锁升级的顺序为:

    无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,且锁升级的顺序是不可逆的。

    线程第一次获取锁获时锁的状态为偏向锁,如果下次还是这个线程获取锁,则锁的状态不变,否则会升级为CAS轻量级锁;如果还有线程竞争获取锁,如果线程获取到了轻量级锁没啥事了,如果没获取到会自旋,自旋期间获取到了锁没啥事,超过了10次还没获取到锁,锁就升级为重量级的锁,此时如果其他线程没获取到重量级锁,就会被阻塞等待唤起,此时效率就低了。

    具体顺序如图所示:

    image.png

    锁升级过程

    synchronized锁降级流程

    锁降级概念

    锁降级:当前线程获得写锁,没有释放写锁的情况下再去获得读锁,然后释放写锁,这个过程就是锁降级
    (当前线程持有锁的状态由写锁降到读锁就是锁降级)

    使用场景:当多线程情况下,更新完数据要立刻查询刚更新完的数据
    (因更新完数据释放写锁后还持有读锁,所有线程要获得写锁都要等待读锁释放,这时持有读锁的线程可以查到刚更新完的数据)

    弊端:适合读多写少的场景,如果锁降级的同时设置成了非公平锁可能会导致写锁很长时间获得不到

    ReentrantReadWriteLock

    ReentrantReadWriteLock支持锁降级,但是不支持锁升级
    下面代码说明ReentrantReadWriteLock锁降级的使用

    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class ReentrantReadWriteLockDemo {
    
        //默认是非公平锁
        //ReentrantReadWriteLock 支持锁降级 不支持锁升级
        ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock(true);
        //读锁
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        //写锁
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
        
        private  int i=0;
        
        public static void main(String[] args) {
        
            //锁升级  当前线程持有读锁,然后获得写锁,将读锁释放,这样就完成了锁升级
            //锁降级  当前线程持有写锁,然后获得读锁,将写锁释放,这样就完成了锁降级
    
    //锁降级
    //        writeLock.lock();
    //        System.out.println("获得写锁");
    //        readLock.lock();
    //        System.out.println("获得读锁");
    //        writeLock.unlock();
    //        System.out.println("获得释放写锁");
    //        readLock.unlock();
    //        System.out.println("释放读锁");
    
    //锁升级 (ReentrantReadWriteLock 不支持锁升级)在持有读锁情况下获得写锁会阻塞,要等待读锁释放
    //        readLock.lock();
    //        System.out.println("获得读锁");
    //        writeLock.lock();
    //        System.out.println("获得写锁");
    //        readLock.unlock();
    //        System.out.println("释放读锁");
    //        writeLock.unlock();
    //        System.out.println("获得释放写锁");
            //为了处理敏感数据才会使用锁降级
            ReentrantReadWriteLockDemo myCountDownLatch=new ReentrantReadWriteLockDemo();
            for(int i=0;i<5;i++){
                new Thread(()->{
                    myCountDownLatch.doSomething();
                }).start();
            }
        }
    
        public void doSomething(){
            try{
                writeLock.lock();
                i++;
                writeLock.lock();
            }finally {
                writeLock.unlock();
            }
            try {
                //模拟复杂业务
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        
            try{
                //如果每次更新后的数据都要查询
                //数据比较敏感可以使用所降级
                //writeLock.lock();
                System.out.println(i);
            }finally {
                writeLock.unlock();
            }
        }
    }
    

    ReentrantReadWriteLock 浅析

    ReentrantReadWriteLock 什么情况下能获得锁

    ReentrantReadWriteLock 有两把锁,一个是读锁一个是写锁
    当A线程获得到写锁没有释放时,其他线程想获得读锁只能阻塞,然而这时A线程可以再次获得写锁和读锁。

    当A线程获得到读锁没有释放时,其他线程也能获得读锁,这时A线程和其他线程想获得写锁都要阻塞。

    ReentrantReadWriteLock 读锁和写锁重入次数计算

    ReentrantReadWriteLock 类中有一个Sync静态内部类,Sync类中代码说明将一个int 32位的数拆分成两个无符号位的short类型,其中这个数的低16位表示写锁个数,高16位表示读锁个数,所以得出读锁或写锁的重入次数最大是65535次
    Sync代码如下

      abstract static class Sync extends AbstractQueuedSynchronizer {
            private static final long serialVersionUID = 6317671515068378041L;
            /*
             * Read vs write count extraction constants and functions.
             * Lock state is logically divided into two unsigned shorts:
             * The lower one representing the exclusive (writer) lock hold count,
             * and the upper the shared (reader) hold count.
             */
            static final int SHARED_SHIFT   = 16;
            //1左移16位 是 65536  => 0000 0000 0000 0001 0000 0000 0000 0000
            static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
            //1左移16位 是 65535 ⇒ 0000 0000 0000 0000 1111 1111 1111 1111
            static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
            static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
            /** Returns the number of shared holds represented in count  */
            //无符号右移16位  返回读锁的个数  
            static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
            /** Returns the number of exclusive holds represented in count  */
            //int类型数据 与 0000 0000 0000 0000 1111 1111 1111 1111
            //返回写锁的个数
            static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
            }
    

    ReentrantReadWriteLock 读锁上锁代码分析

    简化代码执行流程,代码如下

    
     public void lock() {
          //获得读锁
          //这里传入的值为1,因为如果当前线程持有读锁这里重入次数要加1或得到锁重入次数初始化为1
          //这里会调用抽象类AbstractQueuedSynchronizer中的方法
          sync.acquireShared(1);
     }
    
     public final void acquireShared(int arg) {
            //尝试去获得锁,如果没有获得到锁返回值会小于0
            //调用ReentrantReadWriteLock中重写的方法
            if (tryAcquireShared(arg) < 0)
                //当获得到锁没有获得到执行此方法
                doAcquireShared(arg);
     }
    
    //来到ReentrantReadWriteLock中
    protected final int tryAcquireShared(int unused) {
        /*
         * Walkthrough:
         * 1. If write lock held by another thread, fail.
         * 2. Otherwise, this thread is eligible for
         *    lock wrt state, so ask if it should block
         *    because of queue policy. If not, try
         *    to grant by CASing state and updating count.
         *    Note that step does not check for reentrant
         *    acquires, which is postponed to full version
         *    to avoid having to check hold count in
         *    the more typical non-reentrant case.
         * 3. If step 2 fails either because thread
         *    apparently not eligible or CAS fails or count
         *    saturated, chain to version with full retry loop.
         */
        Thread current = Thread.currentThread();
        //这个变量中存储着,读锁的重入次数和写锁的重入次数
        int c = getState();
        //如果有线程持有写锁并且不是自己持有的则直接返回-1 说明没有获得到锁
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return -1;
        //获得读锁个数
        int r = sharedCount(c);
        //判断是否应该阻塞,因为这里分析的是读锁,来到ReentrantReadWriteLock 的ReadLock内部类的readerShouldBlock方法中
        if (!readerShouldBlock() &&  
            //判断重入次数是否小于65536
            r < MAX_COUNT &&
            //使用cas将读锁重入次数加1  
            //SHARED_UNIT是65536,因为读锁重入次数是高16位所以这里要加65536
            compareAndSetState(c, c + SHARED_UNIT)) {
            //读锁的重入次数为0,则本次获得读锁的是第一个
            if (r == 0) {
                //将持有锁的线程设置成为当前线程
                firstReader = current;
                //设置读锁的重入次数为1
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                //如果已经是当前线程持有锁了,则直接重入次数+1
                firstReaderHoldCount++;
            } else {
                //rh中存储着当前线程的id,和一个count
                HoldCounter rh = cachedHoldCounter;
                //如果当前变量没有缓存或 ThreadLocal中的线程id和当前线程的id不同
                //这里不能使用thread.getId() 是因为getId()方法不是final的
                if (rh == null || rh.tid != getThreadId(current))
                    //readHolds是会在ReentrantReadWriteLock 的内部类Sync中初始化(在Sync构造方法中初始化的)
                    //readHolds创建时候通过ThreadLocal将HoldCounter存到当前线程中
                    //HoldCounter中存了当先线程的pid和一个count
                    //获得到rh 然后给cachedHoldCounter 用来缓存
                    cachedHoldCounter = rh = readHolds.get();
                //第一次获取HoldCounter的时候,rh的count肯定是0,获得了之后就会存到缓存中
                //意思是如果缓存中没有,count肯定是0
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return 1;
        }
        //重新循环去获得锁,但是如果重入次数超过65535则会抛出异常
        return fullTryAcquireShared(current);
    }
    
    //当获得到锁没有获得到执行此方法
    private void doAcquireShared(int arg) {
            //Node.SHARED是一个空节点
            //创建一个节点,节点中存着当前线程,然后将这个节点通过cas添加到一个双向链表中
            //1 如果队列中没有一个节点,这是会将队列中的头结点和尾节点都设置添加的节点
            //2 如果队列中有节点,则通过cas将添加的节点设置成尾节点
            //返回添加到队列中的节点
            final Node node = addWaiter(Node.SHARED);
            boolean failed = true;
            try {
                boolean interrupted = false;
                for (;;) {
                    //找到当前节点的上一个节点
                    final Node p = node.predecessor();
                    //如果上一个节点是第一个节点
                    if (p == head) {
                        //尝试去获得读锁
                        int r = tryAcquireShared(arg);
                        //获得到了读锁
                        if (r >= 0) {                    
                            //将头节点设置成为自己,然后调用LockSupport.unpark(s.thread) 执行本线程
                            setHeadAndPropagate(node, r);
                            p.next = null; // help GC
                            if (interrupted)
                                selfInterrupt();
                            failed = false;
                            return;
                        }
                    }
                    //判断本节点和上一个节点的状态
                    if (shouldParkAfterFailedAcquire(p, node) &&
                        //在这里执行LockSupport.park(this); 阻塞当前线程
                        parkAndCheckInterrupt())
                        interrupted = true;
                }
         } finally {
                //如果当前Node存储的线程还没有执行LockSupport.unpark(s.thread) 则执行cancelAcquire
                if (failed)
                    cancelAcquire(node);
         }
    }
    

    总结

    所谓的锁升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 监测到不同的竞争状况是,会自动切换到不同的锁实现。这种切换就是锁的升级、降级。 更多Android核心技术进阶学习,可进入或嘚。

    相关文章

      网友评论

        本文标题:Android高工进阶必会技能——synchronized锁升级

        本文链接:https://www.haomeiwen.com/subject/wbevbrtx.html