多线程五 synchronized实现原理及优化

1. synchronized实现原理

sychronized的使用场景:
在这里插入图片描述

对象锁monitor机制

1. synchronized修饰同步代码块

先来看一段简单的代码
在这里插入图片描述

为了解到synchronized的底层实现原理,我们来对这段代码进行反编译

先通过cd命令进入到src下的package目录

先编译javac Test.java,生成class文件

反编译命令:

javap -c -v Test.class

然后我们来看生成的字节码:
在这里插入图片描述

执行同步代码块后首先要先执行monitorenter指令,退出的时候执行monitorexit指令。通过分析之后可以看出,使用synchronized进行同步,其关键就是要获取对象的监视器monitor,当线程获取monitor后才能继续往下执行,否则就只能等待。

而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor

上述字节码中包含一个monitorenter指令以及两个monitorexit指令。这是因为JVM需要确保获得的锁在正常执行路径、异常执行路径上都能够被解锁。

2. synchronized修饰同步方法
在这里插入图片描述
当用synchronized标记方法时,字节码中方法的访问标记包括 ACC_SYNCHRONIZED

  • 进入该方法时,JVM需要进行 monitorenter 操作
  • 退出该方法时,不管是正常返回,还是向调用者抛异常,JVM均需要进行 monitorexit 操作

monitorenter—monitorexit

  • 关于monitorenter 和 monitorexit 的作用,我们可以抽象地理解为:每个锁对象拥有一个锁计数器和一个 “指向持有该锁的线程” 的指针

  • 当执行monitorenter时,如果目标锁对象的计数器为0,那么说明它没有被其他线程所持有。在这个情况下JVM会将该锁对象的持有线程设置为当前线程,并且将其计数器加1

  • 在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么JVM可以将其计数器再加1;否则需要等待,直至持有线程释放该锁

  • 当执行monitorexit时,JVM则需将锁对象的计数器减1。当计数器减为0时,便代表该锁已经被释放掉了


可重入锁

当执行monitorenter时,对象的monitor计数器值不为0,但是持有锁的线程恰好是当前线程。此时将monitor计数器值再次+1,当前线程再次进入同步方法或代码块

之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁

【证明锁的可重入与互斥】

synchronized修饰的test1方法中调用test2方法

class Sync implements Runnable {

    @Override
    public void run() {
        test1();
        test2();
    }

    public synchronized void test1() {
        if (Thread.currentThread().getName().equals("A")) {
            test2();
        }
    }

    public synchronized void test2() {
        if (Thread.currentThread().getName().equals("B")) {
            System.out.println("B线程进入该同步方法test2()...");
        }else {
            //此时B线程还没有启动
            System.out.println(Thread.currentThread().getName() + "线程--->进入test2()方法");
        }
    }
}

public class Reentrant {
    public static void main(String[] args) throws InterruptedException {
        Sync run = new Sync();
        new Thread(run,"A").start();
        //有时间差,保证A先启动
        Thread.sleep(2000);
        new Thread(run,"B").start();
    }
}

在这里插入图片描述

如果一个类中拥有多个synchronized方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作


2. JDK1.6后对synchronized的优化

synchronized的操作都是互斥的,效率低。当一个线程拿到了锁资源之后,因为要保证同步,所以其他线程只能等待该线程释放锁,效率自然降低。

优化的思想:让每个线程通过同步代码快时的速度提高


CAS自旋

CAS自旋,无锁实现的同步----乐观锁Compare And Swap

在这里插入图片描述
Compare And Swap(O, V, N)

  • O:当前线程存储的变量值

  • V:内存中该变量的具体值

  • N:希望修改后的变量值

线程1先进入同步代码块:

  1. O ----> i = 0

  2. V ----> i = 0

  3. O == V

  4. N ----> i = 10

线程2再进入同步代码块:

  1. O ----> i = 0

  2. V ----> i = 10

  3. O != V

  4. N ----> 修改失败

  • 当 O == V 时,此时表示还没有线程修改共享变量的值,此时可以成功的将内存中的值修改为N

  • 当 O != V 时,表示此时内存中的共享变量值已经被其他线程修改,此时返回内存中的最新值V,再此尝试修改

自旋问题可以理解为开车过红绿灯:

  • 线程挂起阻塞:车熄火

  • 自旋: 脚踩刹车,车不熄火

自旋带来的问题: ABA问题

  • 解决方法: 添加版本号 A.1----> B.2

自旋在CPU上跑无用指令,虽然减少了操作系统的开销,但是浪费了CPU资源

JVM自适应自旋 : 根据以往自旋等待时能否获取锁,来动态调整自旋的时间(循环数)

  • JVM尝试自旋一段时间,若在此时间内线程成功获取到锁,在下次获取锁时,适当延长自旋时间;
  • 若在此时间内线程没有获取到锁,在下次获取锁时,适当缩短自旋时间

公平性问题

  • 处于阻塞状态的线程可能一直无法获取到锁,自旋状态的线程更容易获取到锁

  • Lock锁可以实现公平性,synchronized无法实现公平锁


偏向锁

JDK 1.6 之后默认synchronized

最乐观的锁:进入同步块或同步方法始终是一个线程

在不同时刻时,当出现另一个线程也尝试获取锁,偏向锁会升级为轻量级锁


轻量级锁

  • 不同时刻有不同的线程获取锁,“亮黄灯策略”,基本不存在锁竞争
  • 同一时刻,如果不同线程尝试获取锁,会将偏向锁自动升级为重量级锁

重量级锁

  • JDK 1.6 之前的锁都是重量级锁,将线程阻塞挂起

  • 锁只有升级过程,没有降级


锁粗化

将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁

比如使用StringBuffer中的apperd方法来添加字符串

sb.append("a");
sb.append("b");
sb.append("c");

这里每次调用append方法都需要加锁和解锁操作

如果虚拟机检测到有一系列连串对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁


锁消除

当对象不属于共享资源时,对象内部的同步方法或同步代码块的锁会被自动解除

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值