当线程释放锁,JMM会把线程对应的本地的内存中的共享变量刷新到内存中
当线程获取锁,JMM会帮其他线程中对应的本地的内存中的共享变量设置未无效,从而监视器保护的临界区的代码必须从内存中读取共享变量。(临界区为锁之间的代码)
synchronized特性
- 原子性:在一个段内,要么全部执行,并且不会被其他线程干扰
- 可见性:当一个线程获取了锁时,其他线程对该锁的状态时可见性的,并且当释放锁后,资源会同步到内存中,其他线程对其又是可见性
- 有序性:在一个段内,重排是没有影响,如果多个线程重排是有影响,synchronized保证了在同一时刻只能有一个线程操作。
- 重入性:一个线程获取锁,其他锁不能在获取,但是这个线程再次获取锁,依旧能够获取锁并进入,不过其中的标识会+1。
synchronized锁的底层实现
Java对象模型
jvm加载一个类,会创建一个instanceKlass,保存在方法区中,new时创建一个对象,对象包含两个部分,对象头和实例数据部分。
对象头包含两个部分:
- Mark word一些运行时数据,包括hash code,锁状态标识,线程持有的锁等信息。
- klass Point是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例
对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。
Monitor
为了解决线程的安全问题,Java提供了同步机制,互斥锁机制。这个机制保证了同一时刻只有一个线程访问共享资源。这个机制的保障来源于Monitor。
每个对象内置了一个Monitor对象,Monitor相当于一个许可证,拿个这个monitor就可以进入操作,否则进行等待。
ObjectMonitor对象结构:
- _owner: 持有ObjectMonitor对象的线程
- _WaitSet: 持有锁的线程调用Wait方法会加入到WaitSet队列中
- _EntryList:存放于等待锁状态的线程队列
- _recursions:锁的重入次数
synchronized 的原理
对方法和类,方法中会添加一个标识flags,类的话在进入添加一个monitorEnter和monitorExit,进入时,会对标识+1,退出时对标识-1。其他获取锁时,会去竞争该资源,如果已经有了标识,则会进行等待。
同步代码块
在代码快中进入时,添加monitorEnter,标识进入了同步代码块中,其它线程进入时,进行等待,在代码块退出时,添加了monitorExit,其它线程可以进入到同步代码块中
同步代码块字节码
方法同步
synchronized方法则会被编译成普通的方法调用和返回指令,如:invokevirtual、areturn指令;
在JVM字节码层面并没有任何特别的指令来实现方法的同步,而是在Class文件的方法表中,将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法,并使用调用该方法所属的类实例对象,或该方法所属的Class在JVM的内部表示对象Klass做为锁对象。
锁的类型
- 不需要monitor entry和monitor exist
- 锁粗化:当编译器有代码连续多次加锁释放锁时会合并为一个锁
- 偏向锁:如果大概只有一个线程加锁会给这个线程维护一个bias偏好,后面加锁基于bias不需要cas
- 轻量级锁:当偏向锁加锁失败,mark word有一个轻量级的指针来直接指向持有锁的线程然后判断是不是自己加的锁
- 重量级锁:当获取锁时锁被其他线程占用则升级为重量级锁
- 自适应自旋锁:当线程获取锁失败后进入自旋状态去检查count变量值,不进行线程上下文切换因为锁等待时间会很短,而不是直接进入到entrylist中(从用户态到内核态)默认开启,自旋次数10次,jdk1.6之后加入了自适应的自旋锁通过上次自旋获取锁的时间和次数来解决
当一个线程获取锁,则会进入到偏向锁,第二线程竞争锁时,则锁进行膨胀为轻量级锁,当多个线程进行竞争是,则会进入到重量级锁。在后续jdk中,偏向锁禁用掉了或者是被废弃了。
wait和notify机制
**wait()**最终调用ObjectMonitor的wait方法,则会将当前线程封装成ObjectMonitor的对象node,通过ObjectMonitor::AddWaiter方法,将其添加到_WaitSet方法中。最后调用ObjectMonitor::exit方法放弃当前CPU,底层是调用park线程挂起。
notify方法,如果_WaitSet为空,则立刻返回,如果_WaitSet有值,将一个node取出放入到ObjectMonitor的_EntryList中,这样就可让他们竞争锁资源,这里会是进入到自旋中。