深入理解 Java 并发之 synchronized 实现原理

如果某一个资源被多个线程共享,为了避免因为资源抢占导致资源数据错乱,我么需要对锁进行同步,那么synchronized就是是西安线程同步的关键字。

一、synchronized的特性

1.1原子性
原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要不就都不执行。
被synchronized修饰的类或者对象的所有操作都是原子性的,因为在执行操作之前必须先获得类或者对象的锁,直到执行完才能释放,并且在执行过程中不能被打断,即保证了原子性。
1.2可见性
可见性就是指多个线程访问一个资源时,该资源的状态,值信息等对于其他线程都是可见的。
1.3有序性
有序性就是指程序的执行顺序按照代码的先后顺序执行。
1.4可重入性
synchronized和ReentrantLock都是可重入锁。也就是一个线程拥有了锁仍然可以重复申请锁。

二、synchronized的用法

  • 修饰实例方法,对当前实例对象this加锁
  • 修饰静态方法,对当前类的Class对象加锁
  • 修饰代码块,指定加锁对象,对给定对象加锁
  private int i= 0;
    private static int j = 0;
    private final MySynchronized mySynchronized = new MySynchronized();
    /**
     * 对成员函数加锁,必须获得该类的实例对象的锁才能进入同步代码块
     */
    public synchronized void add(){
        i++;
    }
    /**
     * 对静态方法加锁,必须获得类的锁才能进入同步代码块
     */
    public static synchronized void add1(){
        j++;
    }
    public void method(){
        synchronized (MySynchronized.class){
            //同步块,执行前必须获得MySynchronized类的锁
        }
        synchronized (mySynchronized){
            //同步块,执行前必须获得实例对象的锁
        }
    }
 

被static修饰的静态方法,静态属性都是归类所有,同时该类的所有实例对象都可以访问。但是普通成员的属性,方法是归实例化的对象所有,必须实例化之后才可以访问,这也就是为什么静态方法不能访问非静态属性的原因了。

三、synchronized锁的底层实现

在理解锁实现原理之前先了解一下Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据、对其填充。
在这里插入图片描述
实例数据和对其填充与synchronized无关。
实例数据存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐;对其填充不是必须部分,由于虚拟机要求对象起始地址必须是8字节的整数倍。
对齐填充仅仅是为了使字节对齐。
对象头是是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word 和 Class Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁,其中无锁就是一种状态了。锁的类型和状态在对象头Mark Word中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。

四、JVM对synchronized的优化

Java的开发团队一直对synchronized进行优化,其中最大的一次优化就是在JDK6的时候,新增了俩个锁的状态,通过锁消除,锁粗化,自旋锁等方法使用个各种场景,给synchronized性能带来了很大的提升。
5.1锁膨胀
锁有四种状态,并且会因实际情况进行膨胀升级,其膨胀方向是:
无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
5.1.1偏向锁
在大多数情况下,锁不存在多线程竞争,总是由同一个线程多次获得,那么此时就是偏向锁。

核心思想:
如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。

5.1.2 轻量级锁
轻量级锁是由偏向锁升级而来的,当存在第二个线程申请同一个锁对象时偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在竞争的问题,可以是一前一后地交替执行同步代码块。

5.1.3 重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
重量级锁一般使用场景会在追求吞吐量,同步块或者同步方法执行时间较长的场景。

5.2 锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。比如,对于代码块私有变量不存在竞争关系,则可以把这个锁进行消除。

5.3 锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

5.4 自旋锁与自适应自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。

自旋锁:许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得,通过让线程执行循环等待锁的释放,不让出CPU。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是它也存在缺点:如果锁被其他线程长时间占用,一直不释放CPU,会带来许多的性能开销。

自适应自旋锁:这种相当于是对上面自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的缺点。

小结:

  • synchronized上锁的资源只有俩类:一个是对象,一个是类。
  • synchronized的区别在于JDK6,在6之前synchronized是重量级锁,然而在JDK6之后进行锁优化,分为无锁–>偏向锁–>轻量级锁–>重量级锁,并且膨胀方向是不可逆的。
  • 对象头是是synchronized实现锁的基础,锁的类型和状态在对象头MarkWord中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word数据。
  • Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值