Synchronized底层原理

一.synchronized的特性

1.原子性:一个操作要么都执行并且执行时不会被任何因素打断,要么都不执行。

其中synchronized和volatile最大的区别在于synchronized保证了原子性,而volatile不保证原子性。

2.可见性:当一个线程对主内存的共享变量进行了修改,其他线程也能立即看到修改后的最新值。

3.有序性:程序的执行顺序和代码的编写顺序一致。

4.可重入性:synchronized和ReentrantLock都是可重入锁。当一个线程再次请求自己持有对象锁的临界资源时,属于可重入。通俗说就是一个线程拥有了锁仍然还能重复申请锁。

二.synchronized的用法

1.修饰对象

    Object object = new Object();

    public void methos3() {
        synchronized (object) {
            System.out.println("被synchronized修饰的代码块,执行前要获得对象的锁");
        }
    }

2.修饰方法

对成员函数加锁,要获得该类的实例对象的锁才能进入同步代码块

对静态方法加锁,必须获得类的锁才能进入同步代码块

    public synchronized void method1() {
        System.out.println("对成员函数加锁");
    }

    public static synchronized void method2() {
        System.out.println("对静态方法加锁");
    }

从语法上,synchronized可以把任何非null的对象作为锁。在JVM中锁也叫对象监视器。

当synchronized作用在方法上,监视器锁就是对象实例;

当synchronized作用在静态方法上,监视器锁就是对象的Class实例;

当synchronized作用在同步代码块上,监视器锁就是括号括起来的对象实例。

三.synchronized锁的同步原理

当一个线程访问同步代码块时,要先获得对象的锁才能执行同步代码块,当执行完或者发生异常时释放锁。底层原理是,进入同步代码前先获取锁,获取到锁后锁的计数器+1,同步代码块执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。

每一个对象有一个监视器锁,当监视器锁被占用时会处于锁定状态,获取过程如下:

1.如果监视器锁的进入数为0,则该线程进入监视器锁,然后将进入数设为1,该线程为监视器锁的持有者。

2.如果该线程已经持有该监视器锁,只是重新进入,则监视器锁的进入数+1。

3.如果其他线程已经持有了监视器锁,则该线程进入阻塞状态,直到监视器锁的进入数为0,再重新尝试获取监视器锁的持有权。

四.锁的优化

1.自旋锁

线程的阻塞和唤醒会给系统的并发性带来压力,在对象锁持续很短的情况下频繁的阻塞和唤醒线程时不值得的。

自旋锁指当一个线程尝试获取某个锁时,如果该锁已经被其他线程占有,就一直循环检测锁是否被释放,而不是进入睡眠状态。

自旋锁适用于锁占用时间短的情况,自选等待不能代替阻塞,但是能避免线程切换带来的开销,不过会占用CPU的时间。自旋锁等待的时间必须要有一个限制,如果超过了限制的时间还没有获得锁就应该被挂起。

2.适应性自旋锁

适应性自旋锁指自旋的次数不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态决定。

如果当前自旋成功,那么下次自旋的次数会增加。如果对于某个锁,很少有自旋成功,那么在获取这个锁的时候自旋次数会减少甚至省略自旋过程,避免浪费处理器资源。

有了自适应自旋锁,随着程序运行和性能监控的完善,虚拟机对程序锁的状况越来越准确。

3.锁消除

为了保证数据完整性,在操作时对一部分操作进行同步控制。但是当不可能存在多线程访问共享数据时,JVM会对同步锁进行锁消除。锁消除可以节省毫无意义的请求锁的时间。

4.锁粗化

在使用同步锁时,需要让同步块的范围尽可能小,仅在共享数据的实际作用域才进行同步,这样在锁竞争时等待的线程能尽快拿到锁。但是在连续加锁解锁的操作中,可能会导致不必要的性能损耗,所以引入锁粗化。

锁粗化就是将多个连续加锁解锁的操作连在一起,合并为一个更大的锁。

五.锁升级

锁有四种状态:无锁-偏向锁-轻量锁-重量锁,并且不可逆。

1.偏向锁:减少线程获取锁的代价,如果锁不存在多线程竞争,总是由同一线程多次获得,此时就是偏向锁。

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

(1)检测Mark Word是否为可偏向状态,锁标识位为01;

(2)若是可偏向状态,则对比线程ID是否是Mark Word中的ThredID。如果是,则执行同步代码块。

(3)如果当前线程ID不等于Mark Word中的ThreadID,则通过CAS操作竞争锁,竞争成功后将Mark Wod中的ThreadID设为当前线程的ID。

(4)通过CAS获取失败的话,说明当前存在多线程竞争,偏向锁升级为轻量锁。

引入偏向锁的目的:在没有多线程竞争时减少不必要的轻量锁执行。因为轻量锁的加锁解锁依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID时依赖一次CAS原子指令。

偏向锁的释放:只有竞争才会释放锁,线程不会主动释放偏向锁,需要其他线程来竞争。

(1)暂停拥有偏向锁的线程。

(2)如果对象没有被锁定则恢复到无锁状态,否则挂起持有锁的当前i线程,并将当前线程的锁记录地址的指针放入对象头Mark Word中,升级为轻量锁,再恢复持有锁的当前线程。

2.轻量锁:由偏向锁升级而来,当存在其他线程申请当前线程持有的对象锁时,偏向锁会立即升级为轻量锁。

(1)在线程进入同步块时,如果同步对象无锁状态,虚拟机会在当前线程的栈帧中建立锁记录空间,用于存储锁对象目前的Mark Word拷贝。

(2)拷贝对象头中的Mark Word到锁记录中。

(3)拷贝成功后,虚拟机用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

(4)执行成功后,线程拥有了该对象的锁,并且对象Mark Word的标志位设为00,表示处于轻量锁状态。

(5)如果执行失败,虚拟机会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,说明当前线程拥有了这个对象的锁,否则说明有多个线程竞争,进入自旋,若自旋结束后未获得锁,则升级为重量级锁,Mark Word中存储的指向重量锁的指针。

轻量锁适用于线程交替执行同步代码块的情况。如果存在同一时间访问同一锁的情况,轻量锁会升级为重量锁。

轻量锁就是当前线程栈帧会把对象头的Mark Word复制到锁记录中,把对象头的Mark Word更新为执行锁记录,把锁记录的指针指向对象的Mark Word。

3.重锁:重锁由轻量锁升级而来,当同一时间有多个线程竞争锁时,锁会被升级为重量锁,此时申请带来的开销大。synchronized是通过对象内部的监视器锁实现的,监视器锁本质依赖于底层的操作系统的Mutex Lock实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个过程开销大。这种依赖操作系统Mutex Lock实现的锁叫重量锁。

4.偏向锁,轻量锁,重量锁之间的转换

单线程下偏向锁开销最小,不用CAS操作,只需要比较对象头。

如果出现了其他线程竞争,则偏向锁升级为轻量锁。

如果其他线程通过一定次数的CAS操作没有尝试成功,锁升级为重量锁。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

psvm_code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值