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
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