synchronized 关键字
特点
- 在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
- 当我们调用某对象的synchronized方法时,就获取了该对象的同步锁
- 不同线程对同步锁的访问是互斥的
规则
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程
- 对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
- 仍然可以访问“该对象”的非同步代码块
- 对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞
用法
修饰代码块
同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。(实例锁)
synchronized代码块可以更精确的控制冲突限制访问区域
1 | class Test |
修饰方法
- synchronized修饰方法,作用范围为整个函数
- synchronized关键字不能被继承,子类重写父类synchronized方法,想要同步需要添加synchronized关键字,或者调用父类同步方法
- 定义接口方法时不能使用synchronized关键字
- 构造方法不能使用synchronized关键字,可以用synchronized代码块来进行同步
用法:
1 | public synchronized void method() |
修饰静态方法
synchronized修饰的静态方法锁定的是这个类的所有对象(全局锁)
用法:
1 | public synchronized static void method() { |
实例锁
锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
实例锁对应的就是synchronized关键字。
全局锁
该锁针对的是对象对应的Class实例,而Class实例存于永久,无论实例多少个对象,那么线程都共享该锁。
全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)
示例:
1 | pulbic class Something { |
假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况。
- x.isSyncA()与x.isSyncB()
不能同时被访问。isSyncA() 和 isSyncB() 都是访问同一个对象(对象x)的同步锁
- x.isSyncA()与y.isSyncA()
可以同时被访问。因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。
x.cSyncA()与y.cSyncB()
不能被同时访问。因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。
x.isSyncA()与Something.cSyncA()
可以被同时访问。因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。
修饰类
synchronized作用于类时,是给这个类加锁,类中所有对象用的是同一把锁
用法:
1 | class ClassName { |
底层原理
字节码
synchronized 语句
对于synchronized
语句当Java源代码被javac
编译成bytecode
的时候,会在同步块的入口位置和退出位置分别插入monitorenter
和monitorexit
字节码指令,这两个指令通过一个reference类型的参数来指明要锁定和解锁的对象
monitorenter
监视器准入指令
每个对象有一个监视器锁(monitor
)。当monitor
被占用时就会处于锁定状态,线程执行monitorenter
指令时尝试获取monitor的所有权,过程如下:
- 如果
monitor
的进入数为0,则该线程进入monitor
,然后将进入数设置为1,该线程即为monitor
的所有者。 - 如果线程已经占有该
monitor
,只是重新进入,则进入monitor
的进入数加1. - 如果其他线程已经占用了
monitor
,则该线程进入阻塞状态,直到monitor
的进入数为0,再重新尝试获取monitor
的所有权。
monitorexit
监视器释放指令
- 执行
monitorexit
的线程必须是object
所对应的monitor
的所有者。 - 指令执行时,
monitor
的进入数减1,如果减1后进入数为0,那线程退出monitor
,不再是这个monitor
的所有者。其他被这个monitor
阻塞的线程可以尝试去获取这个monitor
的所有权。
monitorenter
和monitorexit
依赖于底层操作系统的Mutex Lock,而使用互斥锁需要将当前线程挂起,并从用户态切换到内核态来执行,代价高昂
synchronized 方法
synchronized
方法被翻译成普通的方法调用和返回指令如:invokevirtual
、areturn
指令,在VM
字节码层面并没有任何特别的指令来实现被synchronized
修饰的方法,而是在Class文件的方法表中将该方法的access_flags
字段中的synchronized
标志位置1,表示该方法是同步方法并使调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象
对象头
HotSpot
虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
HotSpot
虚拟机的对象头(Object Header)包括两部分信息:
第一部分”Mark Word
“:用于存储对象自身的运行时数据, 如哈希码(HashCode
)、GC
分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等.
第二部分”Klass Pointer
“:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )
32位HotSpot
虚拟机对象头(Mark Word
):
锁优化
适应性自旋
使用互斥同步,挂起线程和恢复线程的操作需要转入内核态完成,对系统的并发性能影响较大。可以使请求锁的线程等待,不放弃处理器的执行时间,执行忙循环(自旋),看持有锁的线程是否很快释放锁;当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态
如果锁被占用的时间很长,自旋的线程将白白消耗处理器资源,带来性能上的浪费,自旋有一定的限定次数
自适应自旋锁: 自旋时间不固定,由前一次在同一个锁上的自旋时间和锁的拥有者的状态来决定
锁粗化
减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁
锁消除
锁消除指虚拟机JIT
编译器在运行时,对一些代码上要求同步,但检测到不可能存在共享数据竞争的锁进行消除
依据于运行时JIT
编译器的逃逸分析技术
轻量级锁
在进入同步块的时候,如果对象没有被锁定,在当前线程的栈帧中建立一个锁记录的空间(Monitor Record列表),用于存储对象的Mark Word(称为Displaced Mark Word)。虚拟机使用CAS操作将对象的Mark Word更新为指向Monitor Record的指针,更新成功则线程拥有该对象的锁;更新失败,如果对象Mark Word不是指向当前线程的栈帧,则膨胀为重量级锁,Mark Word存储的就是指向重量级锁的指针
Monitor Record 结构:
Owner
:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;EntryQ
: 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。RcThis
: 表示blocked或waiting在该monitor record上的所有线程的个数。Nest
: 用来实现重入锁的计数。HashCode
: 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。Candidate
: 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。
偏向锁
为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟
偏向锁,轻量级锁的状态转换关系:
不同锁的比较:
参考资料
《深入理解Java虚拟机》
http://www.importnew.com/21866.html