synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重
入锁。
synchronize锁的作用范围
- 作用于方法时候锁的是对象
- 作用于静态方法的时候锁的是类
synchronized的原理
Synchronized用的锁是存在java的对象头里面的。
一个对象被new出来之后包含四个部分
- 对象头
- 类型指针
- 实例数据
- 对齐填充
对象头Mark Word:存储了对象的hashCode、GC信息、锁信息三部分。这部分占8字节。每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的。
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。
当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,在达到了自旋次数的限制之后,就会导致轻量级锁膨胀为重量级锁。
锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的.那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。有了自适应自旋锁.随着程序运行和性能监控信息的不断完善.虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下, JVM的即时编译器JIT检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁的时间。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显式使用锁,但是我们在使用一些JDK的内置API时 ,如StringBuffer、Vector、HashTable等 ,这个时候会存在隐形的加锁操作。比如
StringBuffer的append()方法,Vector的add()方法:
public void test(){
Vector<Integer> vector = new Vector<Integer>();
for(int i=0;i<10;i++){
vector.add(i);
}
System.out.println(vector);
}
在运行这段代码时, JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。当然我们也可以 禁止编译器进行即时编译,添加启动参数:-XX:-EliminateLocks
这个时候就不会发生锁消除啦。
在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步,这样做的目的是为了使需要同步的操作量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。如上面实例: vector每次add的时候都需要加锁操作, JVM检测到对同一个对象( vector )连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀
以下图片摘自黑马教程
对于一个Java对象都有一个对象头,对象头中有一个MarkWord变量,在一个线程在对一个对象使用synchronized的时候,会先看这个对象的MarkWord有没有被其他线程引用,没有的话,在jdk1.6的时候引入了偏向锁,这个时候就会在对象头的这个变量上写入线程id,这样就不会对MarkWord进行cas操作(将对象头改成新的锁记录的地址),避免了资源的开销。当然,如果这个对象已经被其他线程锁住了,那么就会进行cas操作将对象头的markWord改成新的锁记录的地址。
什么是MarkWord呢?其实就是一个记录了对象各种信息的数据结构,里面记录了hash值,对象 的分代年龄,对象是否启用偏向锁等,当然还有没有用到的一些位,
我们编写代码来对对象头的信息进行测试验证
这里我们用到了工具库所以要引入相关的依赖
<!--查看对象头工具-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
编写代码进行测试
package com.dongmu.test;
import org.openjdk.jol.info.ClassLayout;
/**
* 测试对象头的信息
*/
public class Test18 {
public static void main(String[] args) {
Bird bird = new Bird();
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
synchronized (bird){
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
}
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
}
}
class Bird{
}
打印结果
这里的对象头的信息我们需要这样子看
所以上面的我们打印结果是
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
00000000 00000000 00000000 10010000 01000000 01111111 11110101 00001000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
默认是开启了延迟开启偏向锁的,所以由于这里存在延迟开启偏向锁,所以对象的对象头的最后三位是001。我们也可以手动修改JVM的启动参数-XX:BiasedLockingStartupDelay=0
来不延迟开启偏向锁。当然,偏向锁的存在是为了减少线程上下文切换的开销,如果我们的线程本身就会存在很多的冲突那么我们也可关闭偏向锁来提高性能。使用-XX:-UseBiasedLocking
来关闭偏向锁。这个时候就会直接加上轻量级锁。最后两位所标志位是00.
如果上面我们在new了一个对象之后 ,调用对象的hashcode就会导致对象的偏向锁被禁用。这是因为在偏向锁的状态下线程id就已经占用了54位,对象头的MarkWord根本无法存下hashcode了。
什么是锁记录呢?每一个线程的栈帧都会包含一个锁记录的结构,内部可以用来存储锁对象的MarkWord。
当然,还存在其他导致轻量级锁失效的场景,就是多个线程获取一个对象的锁,如果没有发生获取锁冲突的时候就会将对象锁从偏向锁转化成轻量级锁,,如果存在冲突的话就不是轻量级锁这么简单了。
package com.dongmu.test;
import org.openjdk.jol.info.ClassLayout;
/**
* 测试对象头的信息
*/
public class Test18 {
public static void main(String[] args) throws InterruptedException {
Bird bird = new Bird();
new Thread(()->{
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
synchronized (bird){
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
}
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
synchronized (Test18.class){
Test18.class.notify();
}
},"t1").start();
new Thread(()->{
synchronized (Test18.class){
try {
Test18.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
synchronized (bird){
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
}
System.out.println(ClassLayout.parseInstance(bird).toPrintable());
},"t2").start();
}
}
class Bird{
}
上面代码中进行wait和notify的操作就是为了便于调试,避免发生锁冲突可能出现的导致重量级锁的发生。
当然,如果对象调用了wait和notify方法会导致对象重量级锁的的发生。
对象的偏向锁偏向了某一线程之后,还是可以偏向其他线程的,当撤销偏向锁的次数超过20次之后,JVM就会觉得偏向有问题,于是在给对象枷锁的时候重新偏向枷锁的线程。
测试代码:
private static void testRevokeBiasLock(){
Vector<Bird> vector = new Vector<>(30);
new Thread(()->{
for (int i = 0; i < 30; i++) {
Bird bird = new Bird();
vector.add(bird);
synchronized (bird){
System.out.println("线程t1打印的对象,索引是"+i+ClassLayout.parseInstance(bird).toPrintable());
}
}
synchronized (vector){
vector.notify();
}
},"t1").start();
new Thread(()->{
synchronized (vector){
try {
vector.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
for (int i = 0; i < 30; i++) {
Bird bird = vector.get(i);
synchronized (bird){
System.out.println("线程t2打印的对象,索引是"+i+ClassLayout.parseInstance(bird).toPrintable());
}
}
},"t2").start();
}
打印的结果看前几个部分
第一个线程
第1个:00000000 00000000 00000010 10001011 11100101 00100110 01101000 00000101
第10个:0000000 00000000 00000010 10001011 11100101 00100110 01101000 00000101
第22个:0000000 00000000 00000010 10001011 11100101 00100110 01101000 00000101
第二个线程
第1个:00000000 00000000 00000000 10110000 00111111 11001111 11101111 10010000
第10个:00000000 00000000 00000000 10110000 00111111 11001111 11101111 10010000
第22个:00000000 00000000 00000000 10001011 11100101 00100110 11000001 00000101
从上面的结果中可以看到对象的锁状态发生了重新的偏向,其他的都是轻量级锁,而后面发生了撤销偏向锁,所以后面对象锁状态变成了原来的偏向锁的状态。
另外,在撤销偏向锁的次数超过40次之后,jvm就会认为自己真的偏向错了,直接将这个类的所有对象设置为不可偏向的状态,新建的对象也是不可偏向的。