synchronized关键字原理

1 篇文章 0 订阅

synchronized三种使用方式

一、synchronized有三种方式可以来加锁,对象锁,类锁和方法锁

1、修饰代码块,对指定的对象(synchronized括号内的对象)进行加锁,
2、修饰静态方法,对当前class对象进行加锁
3、修饰实例方法,对实例方法进行加锁

二、实现原理

synchronized的底层是使用操作系统的mutex lock(互斥锁)来实现的,synchronized锁住的是对象头。

  • 当修饰代码块。同步代码块时是通过monitorenter和monitorexit两条指令来完成,它们分别位于同步的开始和结束的位置。monitor位于对象头中,每个对象都有一个monitor与之关联,当对象的monitor被持有后,它将处于锁定的状态。
  • 在执行monitorenter操作的时候,首先会去获取该对象的锁,如果该对象未被锁定或者已经持有该对象的锁,我们会获取当前对象的锁并把该对象的锁计数+1。相反的如果我们执行monitorexit操作时,该对象的锁会-1直到为0时释放该对象的锁。如果获取锁失败,当前的线程会被阻塞直到获取锁。
  • 这里我们要知道synchronized时可重入锁,所以当前线程获取锁之后,可以不必竞争能再次获取该对象的锁。
public class SynchronizedTest {
    private static int i = 1;
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0;i < 10;i++){
            new Thread(()-> sync()
            ).start();
        }
        System.out.printf("sum"+i);
    }
    public static void sync(){
        synchronized (SynchronizedTest.class){
            i++;
        }
    }
}

我们通过javap后得到
在这里插入图片描述
修饰方法,当synchronized修饰方法的时候,JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区来区分一个方法是否是同步方法。当方法被调用的时候,指令会首先检查ACC_SYNCHRONIZED是否被设置了,如果被设置了,则当前线程先持有monitor,再执行方法,方法执行后释放monitor

public class SynchronizedTest {
    private static int i = 1;
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0;i < 10;i++){
            new Thread(()-> sync()
            ).start();
        }
        System.out.printf("sum"+i);
    }
    public synchronized static void sync2(){
        i++;
    }
}

无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。

三、对象头和monitor

  • 对象头
    在jvm中,对象在内存中布局分为三块:对象头,实例数据,填充对象
    实例数据:存放类的属性数据信息,包括父类的属性信息。如果是数组的实例部分,还要加上数组长度,这部分内存按四个字节
    填充对象:因为虚拟机要求对象的起始长度必须是8的倍数,填充对象不必须存在,只是为了对齐。
    而对于顶部的对象头,主要包括了两部分信息。一部分存储对象自身运行时数据,称为Mark Word。一部分是存储指向对象类型数据的指针,被称为Class MetaData Address。如果是数组对象的话,还会有一个额外的部分存储数组长度
    在这里插入图片描述
    因为对象头的信息与对象自身定义的数据是没有关系的额外定义成本,考虑JVM的空间效率,Mark Work被设置为一个非固定的数据结构,以便存储更多的有效数据
    在这里插入图片描述
    对于synchronized的对象锁,也就是锁标识位为10,指向互斥量的指针就是monitor对象的起始位置。每个对象都有一个monitor 与之关联,当一个 monitor 被某个线程持有后,它便处于锁定状态
  • Monitor
    Monitor本质是依赖操底层操作系统的Mutex Lock(互斥锁),每个对象都有一个“互斥锁”的标记,这个标记保证了在任意时刻只有一个线程能访问该对象。
    互斥锁,用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

四、锁的优化

  • 在java中,如果要阻塞或者唤醒一条线程都需要通过操作系统来完成。而这就意味着要从用户态转为核心态,在这个转换的过程中需要耗费不少的cpu资源,状态之间的转换需要相对比较长的时间。这就是synchronized为什么效率低的原因,所以说synchronized是重量级锁。但是在jdk1.6对synchronized做了不少的优化。

  • jdk1.6后 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
    锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级
    1、偏向锁
    偏向锁是jdk1.6引入的,经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低从而引入了偏向锁。当一个线程访问同步代码块并获取锁的时候,会在对象头和栈帧中的锁记录李存储偏向锁的线程ID,以后该线程在进入和退出同步代码块的时候不需要进行CAS操作来加锁和解锁,只需要简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置为1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
    (1)、偏向锁的撤销
    偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
    (2)、关闭偏向锁
    偏向锁在Java 6和Java 7里是默认启用的。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
    2、轻量级锁
    (1)、加锁
    线程在执行同步代码块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。然后线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,失败则其他线程竞争锁,当前线程尝试使用自旋来获取锁
    (2)、解锁
    解锁时,通过原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
    因为自旋会消耗cPU,为了避免无用的自旋(比如获得锁的线程被阻塞了),一旦锁升级成重量级锁,就不会恢复到轻量级锁状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值