Synchronized关键字及锁升级

使用场景:

多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。 要解决线程安全问题,java提供了同步机制,同步就是同一时刻,只有一个线程能访问共享资源。java提供了sync关键字来实现线程同步,我们可以在代码中使用synchronized关键字对类或者对象加锁。

sync关键字的三种使用形式:

  • Synchronized修饰普通同步方法:锁对象当前实例对象(this);
  • Synchronized修饰静态同步方法:锁对象是当前的类Class对象(类名.class);
  • Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象

作用:能保证原子性,可见性,有序性。

原子性:线程在获得锁的时候,由于时间片机制,当cpu时间片用完了,它并不会释放锁,而是等待cpu的调度,而sync是可重入的,下一次能进入临界区代码的还是这条线程,然后接着继续执行完剩下的代码。

可见性:线程的工作内存会将主内存的共享数据拷贝一份作为副本,线程对变量的所有操作都必须在工作内存中进行,当解锁的时候,线程会将工作线程的变量刷到主内存中,后续线程就能访问这个修改过的变量了。

有序性:有序性是指程序执行的顺序按照代码的先后顺序执行。由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,但是不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

Synchronized可以把任何一个非null对象作为"锁",每个java对象都有一个内部锁,在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。Synchronized是基于monitor(锁)来实现的,锁存在于对象的对象头中。

sync同步实现原理:

jvm基于进入和退出Monitor对象来实现方法同步和代码块同步

线程要获取锁,也即是获取monitor,

通过将Synchronized编译后可以看出:

对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。

对于同步代码块。JVM采用monitorentermonitorexit两个指令来实现同步,它们分别位于同步代码块的开始和结束位置。当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有,就把锁的计数器(及monitor的计数器)+1;当执行monitorexit指令时,锁计数器-1;当锁计数器为0时,该锁就被释放了。

synchronized取得的锁都是对象锁,而不是把一段代码或方法当做锁。 如果多个线程访问的是同一个对象,哪个线程先执行带synchronized关键字的方法,则哪个线程就持有该方法,那么其他线程只能呈等待状态。如果多个线程访问的是多个对象则不一定,因为多个对象会产生多个锁。

对象在内存的布局:

对象是存放在堆内存中的,可以分为三个部分,分别是对象头、实例变量和填充字节

mark word的数据结构

Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

java6之前的synchronized属于重量级锁,效率低下,jdk1.6 为了减少获取锁和释放锁带来的性能消耗而引入了偏向锁、轻量级锁以及锁的存储结构和升级过程。锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态。

偏向锁、轻量级锁、重量级锁三者各自的应用场景:

  • 偏向锁:只有一个线程进入临界区;
  • 轻量级锁:多个线程交替进入临界区;意思就是一个接着一个进入临界区,没有多个线程争夺锁
  • 重量级锁:多个线程同时进入临界区。

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。多个线程同时对某个资源进行CAS操作。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。synchroized是非公平的,无法实现公平锁。

偏向锁

使用背景:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁

一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。(偏向锁只进行一次cas操作)


如果偏向锁的标识位为0,说明此时是处于无锁状态,则当前线程通过CAS操作尝试获取偏向锁,如果获取锁成功,则将Mark Word中的偏向线程ID设置为当前线程ID;并且将偏向标识位设为1。以后该线程在进入和退出同步块时就不需要进行CAS操作来加锁和解锁,只需简单地判断一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,第一次进入获取锁的时候,是需要进行cas操作的。

轻量级锁

指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

引入轻量级锁的原因:轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,开销大,时间长,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。自旋是为了避免切换线程的开销。

重量级锁

但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

总结:

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作(第一次获取偏向锁是需要cas一次操作)。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。

参考链接:

锁对象、偏向锁、轻量级锁、重量级锁_为什么重量级锁开销大-CSDN博客

再有人问你synchronized是什么,就把这篇文章发给他。

面试不愁,这篇详细介绍Java中的各种锁 - 知乎

Java-技术专题-synchronized关键字_洛神灬殇_InfoQ写作社区

java同步锁以及级别升级的理解-CSDN博客

彻底搞懂synchronized(从偏向锁到重量级锁)_android 偏向锁-CSDN博客   (易懂)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值