1、概念
synchronized是利用锁的机制来实现同步的。
锁机制的两个特性:
- 互斥性(原子性)
即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。
- 可见性
必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应该获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作数据从而引起不一致。
2、synchronized用法
1、同步静态方法
public synchronized static void accessResources() {
...
}
2、同步非静态方法
public synchronized void accessResources() {
...
}
3、同步代码块
synchronized(this){
...
}
synchronized(Main.class){
...
}
synchronized参数与加锁方式的区别
在静态方法中无法使用this对象作为锁对象,因为静态方法不需要实例化对象。
synchronized(this) 就是当前对象,谁实例化调用了这个代码块使用的就是它作为锁对象。同步方法与this是一样的。
在java中,每一个对象都会有一个monitor对象,这个对象其实就是java对象的锁,通常会被成为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有自己独立的对象锁,互不干扰。
synchronized(Test.class) 这个得作用等同于静态同步方法的方式,都采用的classLoader加载该class至堆中,使用的是Class对象作为锁。
在java中,针对每个类也有一个锁,可以成为“类锁”,类锁实际上是通过对象锁实现的,即类的Class对象锁,每个类只有一个Class对象,所以每个类只有一个类锁。针对每一个编写的Hello.class 都有一个Class对象对应着,Class对象中存放类的信息,是classLoader在加载class字节码文件时,动态的在堆中生成出来的字节码的Class对象,是程序共享的。
加锁过程
在java对象中,每个对象都会有一个monitor对象,监视器。Class对应着一个monitor对象。
1、某一线程占有这个对象的时候,先看monitor对象的计数器是否为0,为0表示没有其他线程占有;这个时候线程占有这个对象并且对这个对象的monitor+1;;如果不为0,表示这个线程已经被其他线程占有,这个线程进行等待。当线程释放占有权的时候,monitor-1;
2、同一线程可以对同一对象进行多次加锁,+1,+1 重入性
3、synchronized原理分析
1、线程堆栈分析
- jconsole工具
等待状态
- jstack pid 工具
pid在jconsole中的VM概要查看
结论:
互斥,排他性
2、JVM指令分析
代码块synchronized加锁分析
- javap 反编译
反编译结果输出至txt中
javap -v Test.class > test.txt
monitorenter :互斥入口
monitorexit :互斥出口
把指令monitorenter与monitorexit中间的区域锁住。
这里存在两个monitorexit出口 一个为正常,一个为异常,出现异常时,依然需要释放锁,就会走monitorexit异常出口。
结论
代码块加锁,是monitorenter与monitorexit进行的锁控制。
对静态方法方法进行synchronized加锁
对方法(不论静态还是非静态方法)的加锁是加一个flags标记 ACC_SYNCHRONIZED,说明这个方法为互斥方法,对下面的方法进行加锁
使用Test.class进行synchronized加锁
类似this,因为Test.class加载进来会产生Class对象实例,这里就是使用了共有的Class对象实例为monitor.
synchronized jdk6 之后优化
在jdk6之前synchronized为重量级锁。之后的jdk虚拟机对synchronized进行优化,加入偏向锁,轻量级锁,重量级锁(等待时间长)。
对象头与monitor
一个对象实例包含:对象头,实例变量,填充数据
- 对象头:加锁的基础
- 实例变量:私有的属性变量信息
- 填充数据:8字节填充
对象头表
hashcode的作用:对象的编码,hash值,唯一值
锁状态
无锁状态
没有加锁
偏向级锁
在对象第一次被某线程占有的时候,是否偏向锁置为1,锁标志位置为01,写入线程ID。记录当前对象被谁占有。当其他线程访问时,就会发生竞争,竞争的结果有成功失败。
偏向锁比较偏向第一次占有它的线程,多数统计下,锁竞争中很多次都会被第一次占有它的线程所占有。
竞争成功: 很多次被第一次占有它的线程获取次数比较多,还是被第一次占有它的线程获取锁,标记位不变,记录的线程ID不变
CAS算法 比较和替换算法(compare and set)
使用CAS算法 优化的偏向锁,使用时间与无锁状态的时间非常接近。
竞争不激烈的情况下使用CAS,不适合竞争激烈的时候
竞争失败:竞争失败,升级轻量级锁
轻量锁
线程有交替适用,互斥性不是很强,可以使用轻量级锁。
CAS竞争成功后,锁状态置为00
竞争失败后,升级重量级锁。
重量级锁
强互斥性,等待时间长,锁标志位置为10
锁升级的过程中,会发生用户线程和核心线程的转换,是非常耗时的,通过自旋锁
自旋锁
竞争失败后,不是马上进行锁升级,而是执行几次空循环 来等待其他线程释放锁。执行完空循环后依然没有拿到锁,则进行锁升级。
锁消除
JIT在编译过程中,取消不必要的锁。
synchronized (this) {
int i = 10;
}
在以上原子性操作时进行加锁,JIT在即时编译时,会去除不必要的锁。
锁消除 锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。 锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行