在Java中,synchronized
关键字是用来控制线程同步的。就是在多线程的环境下,控制synchronized
代码段不被多个线程同时执行。java
那么synchronized具体是怎么作到线程同步的呢?还有锁升级过程的过程是怎样的的?咱们来探讨一下。linux
咱们先来了看下若是多线程间竞争共享资源,不采起措施会出现什么状况:安全
public class TestSync implements Runnable { private int count = 100; public static void main(String[] args) { TestSync ts = new TestSync(); Thread t1 = new Thread(ts, "线程1"); Thread t2 = new Thread(ts, "线程2"); Thread t3 = new Thread(ts, "线程3"); t1.start(); t2.start(); t3.start(); } @Override public void run() { while (true) { if (count > 0) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } else { break; } } } }复制代码
线程2将count减到了97,线程三、线程1在某一刻也作了count--,可是结果却也是97,说明他们在作count--的时候并不知道有别的线程也操做了count。多线程
这个问题,相信你们都知道加synchronized能够解决。jvm
对run方法做以下修改:ide
@Overridepublic void run() { while (true) { synchronized (this) { if (count > 0) { count--; System.out.println(Thread.currentThread().getName() + " count = " + count); } else { break; } } } }复制代码
执行count--有条不紊,不会出现不安全的问题。工具
所以,在代码层面,加关键字synchronized
能解决上述线程安全问题。oop
若是使用IDEA的话,这里推荐安装一个jclasslib Bytecode viewer
,这个插件能够很方便的看程序字节码执行指令:布局
咱们来看一下刚才的程序字节码指令:测试
实际上synchronized
的实现从字节码层面来看,就是monitorenter
和monitorexit
指令,这两个就能够实现synchronized了。
「monitorenter」:
Java对象天生就是一个Monitor,当monitor被占用,它就处于锁定的状态。
每一个对象都与一个监视器关联。且只有在有线程持有的状况下,监视器才被锁定。
执行monitorenter
的线程尝试得到monitor的全部权:
「monitorexit」:
一个或多个MonitorExit
指令可与Monitorenter
指令一块儿使用,它们共同实现同步语句。
尽管能够将monitorenter
和monitorexit
指令用于提供等效的锁定语义,但它们并未用于同步方法的实现中。
JVM在完成monitorexit
时的处理方式分为正常退出和出现异常时退出:
monitorexit
,这个指令是athrow
。monitorexit
。简单的加锁解锁过程
所以,执行同步代码块后首先要执行monitorenter
指令,退出的时候monitorexit
指令。
public static void main(String[] args) { Object o = new Object(); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.out.println(ClassLayout.parseInstance(o).toPrintable()); } }复制代码
执行结果:
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 20 (11100101 00000001 00000000 00100000) (536871397) 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) 08 f3 7f 02 (00001000 11110011 01111111 00000010) (41939720) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) e5 01 00 20 (11100101 00000001 00000000 00100000) (536871397) 12 4 (loss due to the next object alignment) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total复制代码
没有加synchronized的时候,对象头信息的值为01 00 00 00
,加了锁以后,对象头变了08 f3 7f 02
,说明synchronized会修改对象的头新信息,对象头在Hotspot里面叫作markword
。
一个对象的markword
里面有很是重要的信息,其中最重要的就是锁synchronized
。(markword里还有GC的信息,还有hashcode的信息。)
「Hotspot实现的JVM在64位机的markword信息」:
markword信息
在JDK早期的时候,synchronized的底层实现是重量级的,所谓重量级,就是它直接去找操做系统去申请锁,它的效率是很低的。
JDK后来对synchronized锁进行了优化,这样才有了锁升级
的概念。
锁升级
的过程大体是这样的:
new -> 「偏向锁」 -> 「轻量级锁 (自旋锁)」-> 「重量级锁」
synchronized优化的过程和markword息息相关。
用markword中最低的三位表明锁状态,其中1位是偏向锁位,最后两位是普通锁位。
锁 = 0 01 无锁态
❝注意:若是偏向锁打开,默认是匿名偏向状态
❞
❝001 + hashcode
❞
00 -> 轻量级锁
默认状况,偏向锁有个时延,默认是4秒
why? 由于JVM虚拟机本身有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道确定会有竞争,若是使用偏向锁,就会形成偏向锁不断的进行锁撤销和锁升级的操做,效率较低。
能够用BiasedLockingStartupDelay参数设置是否启动偏向锁(=0,当即启动偏向锁):
-XX:BiasedLockingStartupDelay=0复制代码
锁升级过程:new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock
打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101
上偏向锁,指的就是,把markword的线程ID改成本身线程ID的过程。
偏向锁不可重偏向、批量偏向、批量撤销
撤销偏向锁,升级为轻量级锁
线程在本身的线程栈生成LockRecord ,用CAS操做将markword设置为指向本身这个线程的LR的指针,设置成功者获得锁
竞争加重:有线程超过10次自旋, (-XX:PreBlockSpin参数可调),或者自旋线程数超过CPU核数的一半, JDK 1.6以后,加入自适应自旋 Adapative Self Spinning ,JVM本身控制。
升级重量级锁:向操做系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操做系统的调度,而后再映射回用户空间。
总结一下,锁升级的过程大概是这样的:
锁升级过程
自旋是消耗CPU资源的,若是锁的时间长,或者自旋线程多,CPU会被大量消耗。
重量级锁有等待队列,全部拿不到锁的进入等待队列,不须要消耗CPU资源
不必定,在明确知道会有多线程竞争的状况下,偏向锁确定会涉及锁撤销,这时候直接使用自旋锁。
JVM启动过程,会有不少线程竞争(明确),因此默认状况启动时不打开偏向锁,过一段儿时间再打开。
在硬件层面,锁实际上是执行了lock cmpxchg xx
指令。
synchronized在字节码层面:
若是锁的是方法,jvm会加一个synchronized修饰符;
若是是同步代码快,就是用monitorenter和monitorexit指令。
当jvm看到了synchronized修饰符
或者monitorenter和monitorexit
的时候,对应的就是C++调用操做系统提供的同步机制。
CPU级别是使用lock
指令来实现的。
❝好比,咱们要在synchronized某一块内存上设置一个数i,把i的值从0变成1,这个过程放在CPU执行可能会有好几条指令或者不能同步(速度太快),因此须要有个lock指令。
cmpxchg前面若是加了一个lock的话,后面的指令执行过程当中对这块区域进行锁定,只有这条指令能够修改,其余指令是不能操做的。
❞
Java对象头 「markword」
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:
❝Java对象头是实现synchronized的锁对象的基础,通常而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。
❞
一个同步工具,也能够描述为一种同步机制。
为何每一个对象均可以成为锁呢? 由于每一个 Java Object 在 JVM 内部都有一个 native 的 C++ 对象 oop/oopDesc 与之对应,而对应的 oop/oopDesc 都会存在一个markOop 对象头,而这个对象头是存储锁的位置,里面还有对象监视器,即ObjectMonitor,因此这也是为何每一个对象都能成为锁的缘由之一。
synchronized的锁是进行过优化的,引入了偏向锁、轻量级锁;锁的级别从低到高逐步升级, 无锁->偏向锁->轻量级锁->重量级锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程在进入和退出同步块时不须要进行CAS操做来加锁和解锁,只需简单地测试一下对象头的markword里是否存储着指向当前线程的偏向锁。
开启:「-XX:BiasedLockingStartupDelay=0」
自旋等待的时间或者次数是有一个限度的,若是自旋超过了定义的时间仍然没有获取到锁,则该线程应该被挂起。
JDK1.6中-XX:+UseSpinning开启; -XX:PreBlockSpin=10 为自旋次数; JDK1.7后,去掉此参数,由jvm控制。
重量级锁经过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操做系统的Mutex Lock实现,操做系统实现线程之间的切换须要从用户态到内核态的切换,切换成本很是高。