zoukankan      html  css  js  c++  java
  • Synchronized和Lock的实现原理和锁升级

    Synchronized底层实现

    1)先在Idea下载一个ByteCode插件来观察java经过编译之后的字节码

    public class TestSync {
        synchronized void m() {
    
        }
    
        void n() {
            synchronized (this) {//monitorenter 
    
            } //monitorexit
        }
    
        public static void main(String[] args) {
    
        }
    }

    然后idea—view—showByteCode

    这是我们n方法的字节码 为synchronized关键字会在同步块前后增加monitorenter monitorexit指令

    在虚拟机规范对monitorenter和monitorexit的行为描述中,有两点需要注意。

    首先synchronized同步块对同一线程来说是可重入的,不会出现自己把自己锁死的问题

    其次,同步块在已进入的线程执行完成之前,会阻塞后面其他线程的进入

    还有:方法及的同步是隐式的,即无须通过字节码指令来控制,它实现在方法的调用和返回操作之中。虚拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。

    2)JVM层面

    所谓给对象上锁,就是对象头上产生了变化,锁信息就是存在MarkWord上面,加锁就是修改MarkWord

    用JOL工具观察内存布局:(JOL就是maven里面的一个jar包 可以用来观察java内存的布局和大小)

    观察Synchronized锁升级的过程,只需要观察对象MarkWord的变化就行(最后两个字节是锁标志位)

    C C++调用了操作系统提供的同步机制

    3)OS和硬件层面

    X86 : lock cmpxchg / xxx 实现CAS操作的最终指令  (lock后面的指令执行的过程中 区域被lock锁定,只有我这个指令能执行)

    https://blog.csdn.net/21aspnet/article/details/88571740

    总结:synchronized是基于jvm底层实现的数据同步 加锁解锁过程由JVM自动控制,

    Synchronized锁升级的过程

    先说下什么是重量级锁:JDK早其sysnchronized叫是重量级锁,申请资源必须通过kernel,需要从用户空间切换到内核空间(从用户态向内核态调用)拿到锁,然后把状态返回给用户空间—惊动操作系统老大

        用户空间做一些比较关键的事情 需要通过老大(OS)来做,读写网络,写硬盘,比较敏感的操作必须通过操作系统进行,可以保证操作系统比较健壮!

    偏向锁和自旋锁都不需要惊动操作系统老大。

    重量级锁:JDK早其sysnchronized叫是重量级锁,申请资源必须通过kernel,需要从用户空间切换到内核空间(从用户态向内核态调用)—惊动操作系统老大

         当竞争的线程特别多时,自旋锁就不适用了(一个线程运行,剩下的都在自旋) 

         重量锁:其他线程都进队列等着(等待队列),不需要在那里转,占用CPU资源了

    自旋锁(轻量级锁):当多个线程竞争时(竞争的线程不多),以CAS的方式修改MarkWord 谁修改成功了就算谁的

              类似数据库的乐观锁(乐观锁有版本号,而自旋锁是比较操作的值,存在ABA问题)

             指向线程栈中的LockRecord,记录了线程被锁住多少次(Syn是可重入锁)

    偏向锁:偏向锁是有偏向的,偏向于某个线程,不需要惊动操作系统,把字节的线程Id记到MarkWord里面

             在JDK类库中,大多数只在一个线程里面运行(比如StringBuffer)为了一个线程还要惊动操作系统;比较浪费

        偏向锁连CAS都不做了(消除数据在无竞争情况下的同步原语)

        凡是有人第一次得到这把锁的时候(把线程的Id放到MarkWord里面),

    自旋锁什么时候升级成重量级锁

    JDK1.6之前:在某一个线程自旋次数超过十次就会升级成重量级锁

    JDK1.6之后:自适应自旋,JDK根据线程运行情况自己判断

    锁升级的过程

    普通对象和匿名偏向的区别:因为JVM启动4s之后才会启动偏向锁,(利用4s钟的时间判断 需不需要启动偏向锁,如果JVM能确定会有多个线程争抢某些对象 则不需要启动偏向锁)

      所以在程序前4s new出来的对象是普通对象

      4s之后new出现的对象是匿名偏向(没有偏向任何人)

        public static void main(String[] args) throws InterruptedException {
            Object o = new Object();
            String s = ClassLayout.parseInstance(o).toPrintable();
            System.out.println(s);
            TimeUnit.SECONDS.sleep(5);
            Object o2 = new Object();
            String s2 = ClassLayout.parseInstance(o2).toPrintable();
            System.out.println(s2);
        }

    这是测试程序,观察最后两位字节

    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    java.lang.Object object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    View Code

    Synchronize可重入锁

    可重入锁的意思是我锁了一个对象之后,该线程又申请了一把锁,发现当前持有这把锁的就是我自己这个线程,然后就继续执行就可以了

    每个线程,想上自旋锁的过程中,会在线程栈里面生成一个LR对象和锁住的对象关联(Lock Record 锁记录),往对象MarkWord设的是锁记录(LR)的指针,

    第二个线程持有这把锁了 再加Synchronize的过程中,会在线程里再次生成一个Lock Record,解锁一次 一个Lock Record弹出就行了

    Synchronize必须是可重入的,不然子类实现父类没有办法实现。

    Lock的底层实现原理AQS

    synchronized是基于jvm底层实现的数据同步 加锁解锁过程由JVM自动控制,lock是基于Java编写,主要通过硬件依赖CPU指令实现数据同步,与底层的JVM无关。

    java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReenTrantLock、ReadWriteLock

    其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类(简称AQS),实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。

     AQS的核心是一个volatile修饰的state以及监控这个state的双向链表,链表的节点里面装的是线程Thread,当一个Node拿到这把锁 也就是拿到这个state,并且改了值之后(以CAS的方式从0改到1),说明里面的线程持有这把锁

    lock的Lock方法会调用acquire(int arg)去获得锁

            final void lock() {
                acquire(1);
            }
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //如果得不到这把锁 就跑队列里面等着
                selfInterrupt();
        }

    以下是tryAcquire(arg)的实现

     /**
             * Performs non-fair tryLock.  tryAcquire is implemented in
             * subclasses, but both need nonfair try for trylock method.
             */
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    if (compareAndSetState(0, acquires)) { //如果state等于0,用CAS的方式
                        setExclusiveOwnerThread(current); //把当前线程设为独占这个state的线程,说明得到了这把锁(这把锁是互斥的 别人在来的时候 看到是1)
                        return true;
                    }
                }
                else if (current == getExclusiveOwnerThread()) {//如果当前线程就是独占state的线程
                    int nextc = c + acquires;   //直接相加 表示可重入
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }

     以下是AQS的类图

    所谓CAS就是 Compare And Set 

    cas(V,Expected,NewValue) 当前线程想改V这个值的 期望值(当前线程认为你原来应该有的值) 

    if(V=E) V=New otherwise try again or fail

    CAS的操作是CPU的原语支持(Unsafe=C C++指针)

  • 相关阅读:
    jsp页面跳转的路径问题
    Hibernate简单的保存操作
    Hibernate中如何完成持久化类和数据库映射文件
    spring中IOC的简单使用
    对称的二叉树 --剑指offer
    二叉树的下一个结点 --剑指offer
    删除链表中重复的结点 --剑指offer
    链表中环的入口结点 --剑指offer
    字符流中第一个不重复的字符 --剑指offer
    表示数值的字符串 --剑指offer
  • 原文地址:https://www.cnblogs.com/ssskkk/p/12814323.html
Copyright © 2011-2022 走看看