特性
原子性:要么全部成功,要么失败。这是指的再CPU调度层面,不会被中断,是作为一个步骤,不会被CPU拆解开的动作。最常见的 i++操作,就是不是原子性的,因为在CPU层面,这个i++包括3个步骤:读取、计数、赋值,在这3个步骤中,CPU时间片调度随时可能被切走。注意:volatile不具备原子性。
可见性:通过加锁的方式,synchronized修饰类或方法或对象时,如果一个线程想要执行该代码,首先要获取对象锁。如果当前有其他线程占用对象锁,那其他线程阻塞等待获取。
有序性:是指程序的执行是按着代码的先后顺序执行。volatile也保证有序性,它是通过防止指令重排序来实现的有序性。但是synchronized是通过同一时刻只会有一个线程访问资源,从而在单线程环境下保证的有序性。
可重入性:就是可以重复获取,获取对象锁后,可以进入该对象作用域范围内的其他锁同步代码块。
用法
修饰静态方法、修饰普通方法、修饰代码块。思考:锁的是什么?其实总结来说,锁的资源有2类:一个是对象,一个是类。
分类:在jdk1.6之前,只有无锁和有锁(重量级锁)两种类型。在jdk1.6之后,对synchronized进行了优化,新增了2种类型,变成4种:无锁、偏向锁、轻量级锁(自旋锁)、重量级锁。
底层实现
首先明白java对象实例在jvm内存中的结构:对象头、实例数据、对齐填充。对象头的内容包括:Mark Word 和 Class Metadata Address(类元数据,jvm是通过该指针知道此对象的类型信息)。Mark Word是存储对象的hashcode、锁信息、GC标志和分代年龄。我们申请锁、上锁、释放锁,都是通过获取对象头的Mark Word来获取锁的类型和状态,每个对象都有关联的monitor(管程或监视器锁)
方法块同步:从字节码文件中了解,synchronized锁同步方法块是用monitorenter和monitorexit指令标识实现的。当 当前线程程序指令进入monitorenter开始尝试获取对象锁monitor,如果对象锁的monitor=0,那线程就获得了对象锁,并标记Mark Word。如果对象锁的monitor>0,且Mark Word 不是自己当前线程,那就阻塞等待获取锁。如果重入,monitor 也会+1;
方法同步:是通过方法flags标志,读取运行时常量池中的 ACC_SYNCHRONIZED标志来实现的。当调用同步方法时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor, 然后再执行方法,当方法执行完成时释放monitor。
锁升级
上面提到了锁的4种状态,这4种锁状态是会升级的:无锁->偏向锁->轻量级锁->重量级锁。问题:这个锁升级的过程是不可逆的吗?
jdk1.6之前获得锁很费劲儿,每次都像操作系统申请获得锁,效率低。jdk1.6改版了,对于一些并发小,没必要每次都找OS。当初次进入synchronized修饰的方法时,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁(自旋锁),其他线程会通过自旋的形式尝试获取锁,线程不会阻塞(是积极的排队等候,一直占用CPU资源),从而提高性能。注意:自旋锁适用于线程数少,执行时间短的情况,因为自旋的时候是一直占用CPU,不会让出。
当自旋一直获取不到锁,且自旋次数>10次了,就会升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒,它是不占用CPU资源。
