synchronized作为保证线程安全的常规手段之一,其可以修饰实例方法、静态方法和代码块 修饰实例方法,锁住的是实例对象this 修饰静态方法,锁住的是类对象 修饰代码块,锁住的是括号内的对象
每一个对象都关联一个监视器锁(monitor),当monitor被占用时就会处于锁定状态。
在Java虚拟机(HotSpot)种,monitor是由ObjectMonitor实现的,内部主要有_count、_owner、_EntryList、_WaitSet属性 _count:为线程进入加锁代码的次数 _owner:为持有锁的线程,即持有ObjectMonitor对象的线程 _EntryList:是想要持有锁的线程的集合(保存ObjectWaiter,每个等待锁的线程都会封装成ObjectWaiter对象) _WaitSet:是加锁对象调用wait()方法后,等待被唤醒的线程的集合
【多线程访问同步代码】
·首先会进入_EntryList,当线程获取到对象(objectref)的monitor后,会把monitor的owner变量设置为当前线程,同事count变量加1 ·若线程调用了wait()方法,将释放当前持有的monitor,owner变量回复为null,count自减1,同时该线程进入_WaitSet种等待唤醒 ·当线程执行完毕,释放monitor(锁)并复位变量的值,以便其他的线程获取monitor
2.2.1.【修饰代码块】:
public class SynchronizeTest { public void method(){ synchronized (this) { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "+++++++" + i); } } } }【反编译class文件】:
synchronized修饰代码块,在进入代码块前,JVM会先执行monitorenter指令,试图获取objectref(锁对象)锁对的moniotr的所有权: · 如果目标锁对象的monitor的count为0,声明此对象没有被其他线程持有,此时把monitor中的owner变量设置为当前线程,同时count加1; 如果monitor的count不为0,判断持有的线程是否是当前线程(owner是否指向当前线程): 如果是,则计数器count再次加1(锁的可重入性) 如果不是,声明monitor不是当前线程持有,则当前线程阻塞等待,直至monitor被释放 · 同步代码执行完后遇到monitorexit指令,执行指令后线程将释放monitor(锁)并设置计算器count为0,这样其他线程才有机会持有锁 可以看见,还有一个monitorexit指令,那是为了保证monitor的释放,也就是同步代码出现异常,也能保证锁的正常释放
2.2.1.【修饰同步方法】:
public class SynchronizeTest { public synchronized void method() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "+++++++" + i); } } }【反编译class文件】:
synchronized修饰同步方法,当方法调用时,将检查方法的ACC_SYNCHRONIZED访问标识是否被设置 · 如果设置了,该线程就先持有monitor,然后执行同步方法,最后再方法完成(无论正常或异常结束)时释放monitor · 所以,同步方法通过ACC_SYNCHRONIZED标识来获取锁
一个对象由三部分组成:对象头,实例数据、数据填充;锁状态标识储存再对象头的Mark Word里
【Mark Word再对象头的布局】:
在32为的虚拟机中:
在64为的虚拟机中: 可见,不同的锁状态由对应的锁标识:
Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了偏向锁和轻量级锁,对synchronized锁进行了优化 Java6之后,锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,随着竞争J情况逐渐升级
偏向锁是Java 6 新添的内容,并且jdk默认启动的选项,可以通过-XX:-UseBiasedLocking 来关闭偏向锁。 另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。
【加锁过程】:
如果JVM支持偏向锁,那么在分配对象时,分配一个可偏向而未偏向的对象(Mark Word的后三位为101,并且线程ID字段初始为0) 如果JVM关闭偏向锁,那么在分配对象时,分配一个无锁不可偏向的对象(Mark Word的后三位为001,前25bit为hashcode值) 可见,无锁和偏向锁是互斥的,也就是不能从无锁升级到偏向锁
【线程访问同步代码时】:先检查Mark Word是否为可偏向状态,即是否为偏向锁为1,锁标识位位01; · 若为可偏向状态,则检查Mark Word的线程ID是否为当前线程ID 若不是,将通过CAS来尝试将对象头的Thread ID设置为当前线程ID 如果设置成功,则获得锁,那么线程再次进入和退出同步代码块时,就不需要CAS来获得锁,只是检查Mark Word的线程是否指向当前线程 如果设置失败,声明存在锁的竞争,那么将执行偏向锁的撤销操作,将偏向锁升级为轻量级锁 如果为当前线程ID,则直接执行同步代码 · ·【无锁➡轻量级锁】 · 若不是可偏向状态,即对象为无锁不可偏向的对象,则JVM首先将在当前线程的栈帧中建立一个锁记录(Lock Record)空间,用于储存锁对象目前Mark Word的拷贝(官方称之为Displaced Mark Word); 然后线程尝试CAS将对象头中的Mark Word替换为指向锁记录的指针 如果替换成功,表示竞争到锁,则将锁标识位变为00(表示处于轻量级锁状态),然后执行同步代码 如果替换失败,则判断当前对象的Mark Word是否指向当前线程栈帧的所记录 如果指向,则说明当前线程已经拥有这个对象锁,则可以直接指向同步代码 如果不指向,则说明锁被其他线程竞争持有,当前线程便尝试通过自选来获取锁。当线程的自选次数达到阈值,轻量级锁膨胀为重量级锁
上文提到,如果CAS失败,则会进行偏向锁的撤销操作 偏向锁的撤销操作需要在全局检查点(global safepoint)执行,在全局检查点上没有线程执行字节码。 【撤销过程】: 通过Mark Word 中已存在的Thread ID找到成功获取偏向锁的那个线程 · 如果该线程拥有锁,则将偏向锁升级为轻量级锁: 在该线程的栈帧中补充锁记录(Lock Record),然后将被获取了偏向锁对象的Mark Word更新为指向这个所记录的指针 · 如果该线程不拥有锁(同步代码执行完,锁释放了) 如果允许重偏向,那么将Mark Word的线程ID重置为0(还是偏向锁) 如果不允许重偏向,那么将Mark Word设置为无锁状态,即后两位为01(无锁不可偏向状态)
上文提到,线程尝试CAS将对象头中的Mark Word替换为指向锁记录的指针,如果失败,便尝试自选来获取锁 当竞争线程的自旋次数达到阈值,轻量级锁就会膨胀为重量级锁
轻量级锁几所时,如果对象的Mark Word仍指向线程的锁记录,会使用CAS操作,将Dispalced Mark Word替换到对象头 如果成功,则表示没有竞争发生,释放成功 如果失败,表示当前锁存在竞争,锁会膨胀为重量级锁 · 【疑问】:什么时候会对轻量级锁解锁???我也不知道
方正我也看不明白,java6之前synchronized是重量级的锁操作
在无锁状态下,Mark Word中可以存储对象的identity hash code值 当对象的hashCode()方法,并将该值存储到Mark Word中 后续如果该对象的hashCode()方法再次被调用则不会再通过JVM进行计算得到,而是直接从Mark Word中获取 只有这样才能保证多次获取到的identity hash code的值是相同的 · 在HotSpot, 调用hashCode(), 或者System.identityHashCode() 方法会导致对象撤销偏向锁
【疑问】:
不是引入了偏向锁么,为什么synchronized反编译后还会有monitorenter指令,这个不是获取重量级锁monitor的么? 那么执行代码块之前是走monitorenter的流程还是偏向锁的流程?求dalao解答。
【参考】: 深入理解Java并发之synchronized实现原理 三面阿里,最后问了Synchronized底层原理 JVM锁简介:偏向锁、轻量级锁和重量级锁 深入分析synchronized原理和锁膨胀过程(二) 深入分析synchronized的实现原理 偏向锁与hashcode能共存吗? Java中的偏向锁,轻量级锁, 重量级锁解析
以上为个人理解,不对之处欢迎指出。