深入synchronized原理

参考博客

synchronized关键字最主要有以下3种应用方式
1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
2. 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

Java对象头

Java对象头由如下两部分组成:
1. Mark Word:存储对象的hashCode、锁信息或分代年龄或GC标志等信息
2. Class Metadata Address:类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位。由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间。

Synchronized是重量级锁

在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。

但是的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁偏向锁,同时将自旋锁默认开启。

Synchronized优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

  1. 偏向锁
    偏向锁是Java 6之后加入的,它是一种针对加锁操作的优化手段。在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

    偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即省去了获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

    所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。当有另外一个线程尝试去获取这个锁,偏向锁就失效了,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

  2. 轻量级锁
    倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(Java 6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。

    轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,一旦有两条以上的线程同时竞争同一个锁,就会导致轻量级锁膨胀为重量级锁。

    轻量级锁优化的本质是,使用CAS操作来消除同步是用的互斥量,即避免了用户态到核心态的切换。而偏向锁优化的本质是,在无竞争的情况下,把整个同步消除掉,连CAS都不做了。

  3. 自旋锁
    轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。

    因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,默认为10次循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

    为了进一步提升自旋锁的性能,在JDK 1.6之后有引入了自适应自旋锁,其自旋次数由前一次的同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机会认为这次自旋也很有可能再次成功,进而它将允许相对更多的自旋次数。相反,如果对于某个锁,自旋很少成功,那么以后要获得这个锁时将可能省略掉自旋过程,避免CPU资源浪费。

    自旋锁优化的本质也是使用CAS操作来消除同步是用的互斥量,它与轻量级锁的区别是,轻量级锁没有循环,自旋锁允许循环多次。

  4. 锁消除
    消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

    public class StringBufferRemoveSync {
    
        public void add(String str1, String str2) {
            // StringBuffer是线程安全的,其append方法是用synchronized关键字修饰的,
            // 但是sb属于不可能共享的资源,J因此VM会自动消除内部的锁
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    } 
  5. 锁粗化
    原则上,在我们编写代码时,总是推荐将同步代码块作用的范围限制的尽量小。但是如果一系列的操作都在对同一个对象反复加锁和解锁,甚至加锁操作出现在循环体里面,那及时没有线程竞争,但是频繁地进行互斥同步操作也会降低很多性能。

    int i = 0;
    public void method1(){
        for(int i = 0;i<1000;i++){
            synchronized(this){ //1 将synchronized写在循环体内
                i++;
            }
        }
    }
    
    StringBuffer sb = new StringBuffer();
    public void method2(){
        for(int i = 0;i<1000;i++){
            sb.append(i+""); // 2 调用本身就用synchronized修饰的方法
        }
    }

    如果说上述代码中,1的情况我们可以手动的去调整synchronized的位置,从而避免频繁加锁、解锁,那么2的情况,就只能靠锁粗化来调整了(当然是用synchronized加StringBuilder也可以,但如果就是要用StringBuffer呢?)。

synchronized的可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的。因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

```
int i = 0;
public void method(){
    synchronized(this){// 这里对this加了一次锁
        synchronized(this){// 这里又对this加了一次锁,对同一个monitor加了两次锁,体现了重入性
            i++;
        }   
    }
}
```

注意,当重入之后,monitor中的计数器的值将大于1。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值