当前位置: 首页 > 科技观察

深入理解Synchronized锁优化_0

时间:2023-03-20 15:15:57 科技观察

我们都知道synchronized关键字可以实现线程安全,但是你知道它背后的原理吗?今天我们就来聊聊synchronized线程同步背后的原因以及相关的锁优化策略。其背后的原理synchronized关键字编译后,会在同步块前后形成两条字节码指令monitorenter和monitorexit。这两个字节码中只有一个需要指定要锁定或解锁的对象。如果在Java程序中指定了一个对象参数,那么这个对象就被用作锁。如果不指定,则根据synchronized修饰的是实例方法还是类方法,将对应的对象实例或Class对象作为锁对象。因此,我们可以知道,实现线程同步的synchronized关键字背后其实是Java虚拟机规范中对monitorenter和monitorexit的定义。在Java虚拟机规范中对monitorenter和monitorexit行为的描述中,有两点需要特别注意。synchronized的同步块是flushable到同一个线程,即不会有自己加锁的问题。同步类会在已经进入的线程执行完之前阻塞其他线程的进入。synchronized关键字是通过JDK1.6版本之前的操作系统的MutexLock进行同步的。操作系统的互斥锁是操作系统级的方法,需要切换到内核态才能执行。这需要从用户态转换到内核态,所以我们说synchronized同步是一个重量级的操作。锁优化在JDK1.6版本中,HotSpot虚拟机开发团队花费了大量精力实现了各种锁优化技术,例如:自适应自旋、锁消除、锁语言、偏向锁、轻量级锁等。最重要的其中包括:自旋锁、轻量级锁和偏向锁。我们将着重对这三把锁进行优化。对于重量级的同步操作,自旋锁和自适应自旋在内核态和用户态的切换中消耗最多。在很多情况下,共享数据的运行时间可能很短,比从内核态切换到用户态所花费的时间还要短。所以有人想:如果有多个线程并发获取锁,如果可以让请求锁的线程“稍等”而不放弃CPU的执行时间,看持有锁的线程会不会快。释放锁。为了让线程等待,我们只需要让线程执行一个忙循环(自旋),这种技术称为自旋锁。从理论上讲,如果所有线程快速获取和释放锁,那么自旋锁可以带来更大的性能提升。JDK1.4.2引入了自旋锁,默认自旋10次。但是自旋锁默认是关闭的,在JDK1.6中是默认开启的。自旋等待虽然避免了线程切换的开销,但仍然占用处理器时间。如果锁被占用一段时间,自旋等待的效果会很好。但是如果锁被长期占用,自旋线程就会白白消耗处理器资源,造成性能的浪费。为了解决自旋锁在特殊情况下的性能消耗问题,在JDK1.6中引入了自适应自旋锁。自适应意味着自旋时间不再是固定的,而是由同一把锁上的前一次自旋时间和锁拥有者的状态决定的。如果自旋等待刚刚成功获取了同一个锁对象上的锁,那么虚拟机认为本次自旋很可能再次成功,然后让线程自旋更长的时间,比如自旋100个周期。但是如果对于某个锁,很少能成功获得自旋。为了避免浪费CPU资源,虚拟机可以省略自旋过程。有了自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对锁状态的预测会更加准确,虚拟机也会变得越来越聪明。轻量锁轻量锁是JDK1.6新增的一种锁机制。名字中的“轻量级”是相对于操作系统互斥量的重量级锁来说的。轻量级锁之所以诞生,是因为对于大多数锁来说,在整个同步周期中是不存在竞争的。如果没有竞争,就没有必要使用重量级锁,于是诞生了轻量级锁来提高效率。对于轻量级锁,同步过程如下:当代码进入同步块时,如果同步对象没有被锁定(锁定标志为01),那么虚拟机会创建一个名为LockRecord的空间,用于存放锁定对象的当前MarkWord副本。虚拟机将使用CAS操作尝试将对象的MarkWord更新为指向LockRecord的指针。如果更新动作成功,则线程获得了对象的锁,对象MarkWord的锁标志位变为00,表示对象处于轻量锁状态。简单的说,轻量级锁的同步过程可以概括为:使用CAS操作在线程栈帧和锁对象之间建立一个双向指针。在没有线程竞争的情况下,轻量级锁使用CAS自旋操作来避免使用互斥锁的开销,提高效率。但是如果存在锁竞争,除了互斥体的开销之外,还会发生额外的CAS操作。因此,在竞争的情况下,轻量级锁会比传统的重量级锁慢。偏向锁偏向锁是JDK1.6引入的一种优化,意思是锁会偏向第一个获得它的线程。如果在后续的执行过程中,锁没有被其他线程获取到,那么持有偏向锁的线程就再也不需要同步了。对于偏向锁,同步过程如下:假设当前虚拟机启动了偏向锁,那么当锁对象第一次被线程获取时,虚拟机会将该对象的锁标志设置为01,并将偏向锁的锁位设置为1。同时使用CAS操作将线程ID记录在对象的MarkWord中。如果CAS操作成功,当持有偏向锁的线程进入该锁对应的同步块时,虚拟机将不再进行任何同步操作。当另一个线程试图获取锁时,根据锁对象当前是否被锁定,将其恢复到未锁定(01)或轻量级锁定(00)状态。后续的同步操作和上面介绍的轻量级锁一样进行。可以看出,偏向锁还是需要做一些CAS操作,但是相对于轻量级锁来说,要设置的内容大大减少了,所以也提高了一些效率。偏向锁可以提高有同步但无争用的程序的性能。也是一种有利益权衡(TradeOff)的优化,即并不总是对程序的运行有利。如果程序中的大多数锁总是被多个不同的线程访问,那么就偏向于Patternsareredundant。优化后的锁获取流程经过JDK1.6的优化,synchronized同步机制的流程变成:首先synchronized会尝试使用偏向锁来竞争锁资源。如果能竞争到偏向锁,则说明加锁成功,直接返回。如果竞争锁失败,说明当前锁已经偏向其他线程。该锁需要升级为轻型锁。在轻量级锁状态下,竞争锁的线程根据自适应自旋次数尝试抢占锁资源。如果在轻量级锁状态下仍然没有锁的竞争,则只能升级为重量级锁。在重量级锁状态下,不竞争锁的线程会被阻塞。处于锁等待状态的线程需要等待获得锁的线程触发唤醒。上面的锁获取过程可以用下面的示意图来表示:Java对象锁竞争过程总结本文首先简单说明一下synchronized关键字实现同步的原理。其实就是通过Java虚拟机规范支持monitorenter和monitorexit,这样synchronized就可以同步了。synchronized同步本质上是通过操作系统的互斥锁来实现的。由于操作系统互斥锁占用资源过多,HotSpot虚拟机在JDK1.6之后做了一系列的锁优化,其中最重要的有:自旋锁、轻量级锁、偏向锁。这三种锁的诞生原因和改进点如下表所示。现状锁名受益于使用场景大多数情况下锁的等待时间比操作系统互斥锁要短很多自旋锁减少内核态和用户态切换的开销大多数情况下线程的锁同步周期更短无线程竞争轻量级锁相比自旋锁减少自旋时间无线程竞争锁大多数时候,锁同步时无线程竞争偏向锁相比轻量级锁减少冗余对象复制在运行中没有线程竞争锁。从上表可以看出,自旋锁、轻量级锁、偏向锁的优化逐渐深入。对于重量级锁,自旋锁减少了互斥体的内核态和用户态切换开销。对于自旋锁,轻量级锁减少了等待自旋所花费的时间。对于轻量级锁,首选项减少了冗余的对象复制操作。