Java中Synchronized的用法及原理

为了避免临界区的竞态条件发生(多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测),有多种手段可以达到目的:

阻塞式的解决方案:synchronized,Lock

非阻塞式的解决方案:CAS

一、基本用法

关键字synchronized的作用是实现线程间的同步。它的工作是对同步代码加锁,使得每一次只能有一个线程进入同步代码块,从而保证线程间的安全性。另外,不要错误理解为锁住了对象就能一直执行下去,时间片用完就会停止执行下去。

关键字synchronized的用法:

  • 指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。特殊的,synchronized(this)就是将当前对象作为锁。

  • 直接作用于实例方法:相当于对当前实例(拥有这个方法的对象)加锁,进入同步代码前要获得当前实例的锁。相当于在方法体前后包装了synchronized(this)

  • 直接作用于静态方法相当于对当前类加锁,进入同步代码前要获得当前类的锁。

二、synchronized的底层原理

Monitor被翻译为监视器或者管程:

每个Java对象都可以关联一个Monitor对象(操作系统的对象),如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word就会被设置指向Monitor对象的指针。

Java中Synchronized的用法及原理_第1张图片

  • 刚开始Monitor中Owner为null

  • 当Thread 2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread 2,Moniter中只能有一个Owner

  • 在Thread 2上锁的过程中,如果Thread 3,Thread 4,Thread 5也来执行synchronized(obj),就会进入EntryList BLOCKED状态

  • Thread 2执行完同步代码块中的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争时是非公平的

  • 图中WaitSet中的Thread 0,Thread 1是之前获得过锁,但是条件不满足进入waiting状态的线程,后面的wait-nofity中会分析

注意:synchronized必须是进入同一对象的monitor对象才有上述的效果。不加synchronized的对象不会关联监视器,不遵从以上规则。

什么是对象头?

对象头包括Mark Word字段和Klass字段,后者表示类信息。

Java中Synchronized的用法及原理_第2张图片

2.1 轻量级锁

如果偏向锁失败,虚拟机并不会挂起线程,它还会使用一种轻量级锁的优化手段。轻量级锁的操作也很轻便,它只是简单地将锁对象的对象头部作为指针,指向持有锁的线程栈帧中锁记录对象的地址,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,则可以顺利进入临界区。如果轻量级锁加锁失败,则表示其他线程抢先争夺到了锁,那么当前线程的锁请求就会膨胀为重量级锁。

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是synchronized

假设有两个同步方法块,利用同一个对象加锁:

static final Object obj = new Object();
public static void method1(){
    synchronized(obj){
        method2();
    }
}
public static void method2(){
    synchronized(obj){
        //同步块
    }
}
  • 首先,假设Thread-0先使用了轻量级锁,创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储对象的Mark Word。Object代表锁对象的内部结构。

Java中Synchronized的用法及原理_第3张图片

  • 让锁记录中的Object reference指向锁对象,并尝试用CAS将锁记录地址(默认后两位是00)替换Object的Mark Word,将Mark Word的值存入锁记录。

Java中Synchronized的用法及原理_第4张图片

  • 如果CAS替换成功,对象头存储了锁记录地址和状态00,表示由该线程给对象加锁,这时图示如下:此时,Object的Mark Word的状态为00,表示为轻量级锁。

Java中Synchronized的用法及原理_第5张图片

 如果CAS失败,有两种情况

  • 如果是其他线程已经持有了该Object的轻量级锁(Mark Word状态为00),此时表明有竞争,进入锁膨胀过程。

  • 如果是自己执行了synchronized的锁重入(发现Object中lock record地址指向自己线程),那么再添加一条锁记录作为重入的计数。

 Java中Synchronized的用法及原理_第6张图片

  • 当退出synchronized代码块(解锁时)如果由取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数减1。Java中Synchronized的用法及原理_第7张图片

  • 当退出synchronized代码块(解锁时)锁记录的值不为null,这时使用CAS将Mark Word的值恢复给对象头

    • 成功,则解锁成功

    • 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

2.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时有一种情况就是已经有其他线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。

  • 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

Java中Synchronized的用法及原理_第8张图片

  •  这时Thread-1加轻量级锁失败,进入锁膨胀流程
    • 即为Object对象申请Monitor,让Object的Mark Word指向重量级锁的位置(状态为10)
    • 然后当前线程Thread-1进入Monitor的EntryList Blocked阻塞状态

Java中Synchronized的用法及原理_第9张图片

当Thread-0退出同步块解锁时,使用CAS将Mark Word的值恢复给对象头,失败(对象头MarkdWord值的后两位状态是10,而不是00。这时会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置为Owner为null,唤醒EntryList中BLOCKED线程

2.3 偏向锁

在没有实际竞争的情况下,轻量级锁每次申请、释放、重入都至少需要一次CAS操作(尝试将锁对象的对象头赋值为栈帧中锁记录的地址 ),对于一些自始至终使用锁的线程都只有一个的情况下还是有很大的浪费。偏向锁就是一种针对这种情况下的优化,只需要在初始化时进行一次CAS操作。、

Java6中引入了偏向锁来作进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现锁对象的Mark Word中的线程ID是自己就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。

static final Object = new Object();
public static void m1(){
    synchronized(obj){
        //
        m2();
    }
}
public static void m2(){
    synchronized(obj){
        //
        m3();
    }
}
public static void m3(){
    synchronized(obj){
        //
    }
}

Java中Synchronized的用法及原理_第10张图片

偏向状态

Java中Synchronized的用法及原理_第11张图片

一个对象创建时:

  • 如果开启了偏向锁(默认开始),那么对象创建后,Mark Word值的后三位为101,这时它的thread、epoch、age都为0

  • 偏向锁默认是延迟的,不会在程序运行时立即生效,如果避免延迟,可以加VM参数-XX:BiasedLockingStartupDelay=0来禁用延迟

  • 如果没有开启偏向锁,那么对象创建后,Mark Word值最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值

偏向锁解锁之后,线程ID仍然存储于对象头中。

2.4 偏向锁撤销

  • 禁用偏向锁:代码运行时添加VM参数-XX:-UseBiasedBlocking禁用偏向锁,进入synchronized代码块,自动升级为轻量级锁,最后两位为0。解锁之后Mark Word恢复为原来的值。
  • 调用HashCode方法:正常状态对象一开始时没有hashCode的,第一次调用才生成。调用了对象的hashCode,但偏向锁的对象MarkWord中存储的是线程ID,如果调用hashCode会导致偏向锁被撤销。轻量级锁在锁记录中记录hashCode重量级锁会在Monitor中记录hashCode。
  • 其他线程使用:当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。
  • 测试调用wait/notify:会将轻量级锁或者偏向锁升级为重量级锁,只要重量级锁才有wait/notify机制。

2.5 批量重偏向

锁对象虽然被多个线程访问,但没有竞争(使用锁的时间错开),这时偏向线程t1的对象仍有机会重新偏向t2,而不进行锁升级,重偏向会重置对象头中的Thread ID。

当撤销偏向锁的阈值超过20次后(超过二十个初始偏向t1的对象锁,总是被其他线程例如t2在t1释放锁之后,在其他时间使用),jvm会觉得是不是偏向错了,于是会给这些对象加锁时偏向其他的加锁线程(20次之后的对象,不会升级为轻量级锁,还是偏向锁但会将对象头的ThreadID设置为其他线程ID,不偏向原来的线程)。

批量撤销:

当撤销偏向锁阈值超过40次后,jvm会觉得自己确实偏向错了,根部就不该偏向。于是整个类的所有对象都会变为不可偏向,新建的对象也是不可偏向的(Mark Word最后三位为001)。

你可能感兴趣的:(Java并发,java)