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资源了

自旋锁(轻量级锁):当出现其他线程竞争的时候,发现markword里面已经有其他线程id了

          首先撤销偏向锁的状态, 然后 以CAS的方式修改MarkWord 谁修改成功了就算谁的(MarkWord记着指向线程中的lockRecord指针)

          类似数据库的乐观锁(乐观锁有版本号,而自旋锁是比较操作的值,存在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放到MarkWord中,解锁一次 一个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),说明里面的线程持有这把锁

如果当前锁的状态不是0,就去比较当前线程和占用锁的线程是不是一个线程,如果是,会去增加状态变量的值

从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁

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++指针)

CAS必须是原子性的,不然if之后 V被其他线程改了咋办?

 https://www.bilibili.com/video/BV1bv411u7qX?p=15

posted @ 2020-05-01 18:47  palapala  阅读(1991)  评论(0编辑  收藏  举报