理解Java中锁的状态与优化

2018-07-23 10:53:30 浏览数 (1)

前言

关于锁的知识,按大类来说,通常我们只分乐观锁和悲观锁。但在Java语言里对同步锁的状态又进行了细化通常有无锁状态,偏向锁,自旋锁,轻量级锁,重量级锁,这么做的目的主要还是为了提高并发性能。

乐观锁

乐观锁是一种乐观思想表现,适合读多写少的场景,它假设每次去取数据的时候别人不会修改数据,所以不会加锁,但是在更新的时候会判断一下在此期间有没有人去更新这个数据,具体就是通过版本号来判断的,如果版本号发生变化,则更新失败,补救措施就是重复执行这个操作,直到成功或者尝试一定次数后彻底失败。Java中的乐观锁一般都是通过CAS来实现的,如Atom系列的并发工具包类。在nosql的数据里面elasticsearch就是典型的使用乐观锁来更新每条数据。

悲观锁

悲观锁与乐观锁相反,它一般认为应用程序是写多读少的应用,所以可能存在大量并发,所以每次去获取数据的时候,都会加锁,这样以来就能保证在此之间只有自己可以操作这块数据,只有当自己释放了锁,别人才可以获取,如Java里面的synchronized关键字。悲观锁一般在关系型数据库中比较常见,这也是为什么关系型数据的并发吞吐能力比NoSQL弱的原因与其事务机制有很大关系。

关于线程阻塞

如果多个线程同时访问同步块,那么没有得到锁的线程会被阻塞,线程状态的改变与唤醒这里需要线程的上下文切换,而线程的上下文切换是一个耗时的操作,但真实情况下并发可能分很多种情况,不一定就全部得阻塞线程从而引起上下文切换,在jdk5之前synchronized关键字的锁可以认为是一个重量级耗时的锁,所以在jdk5之后,又引入了分多种情况的锁状态,这其实是对synchronized锁性能的优化。

关于Java对象的存储结构

提到锁的状态,这里必须要提一下Java对象的存储结构,总体上来说每个实例对象由三部分组成:对象头,实例数据,对其填充。其中对象头的部分信息就是用来存储锁的状态,该状态标记位(mark word),有5种情况如下图:

在32位的jvm虚拟机中:

在64位jvm虚拟机如下:

正是因为对象头有存锁状态变化的信息,所以为锁状态的改变提供了依据。

锁状态根据竞争情况从弱到强分别是:无锁->偏向锁->轻量级锁 自旋失败->重量级锁 锁不能降级只能升级:这是为了提高获得锁和释放锁的效率

偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁

偏向锁适合场景是始终只有一个线程在执行同步块,在它没有执行完释放锁之前,没有其它线程去执行同步块。

轻量级锁

轻量级锁,顾名思义,相比重量级锁,其加锁和解锁的开销会小很多。重量级锁之所以开销大,关键是其存在线程上下文切换的开销。而轻量级锁通过JAVA中CAS的实现方式,避免了这种上下文切换的开销。当compare失败的时候(理解成没有拿到"锁"),当compare成功的时候,可以直接对互斥资源进行修改(就好像拿到了“锁一样”),此外,轻量级锁失败的时候线程不会被挂起,会通过自旋的方式再次尝试获取(也称自旋锁),如果多次尝试均失败,则说明存在激烈的竞争,这个时候就会升级成重量级锁。

轻量级锁和重量级锁的重要区别是: 拿不到“锁”时,是否有线程调度和上下文切换的开销。

轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁

重量级锁

重量级锁,是JDK1.6之前,内置锁的实现方式。简单来说,重量级锁就是采用互斥量来控制对互斥资源的访问。在前面的几种锁状态均失效的情况下,最终锁会升级成重量级锁,这意味着并发非常强,同一时刻有大量线程请求临界区,当然最终只能有一个线程获取锁进入临界区,其他的线程则阻塞等待,重量级锁会频繁的进行上下文切换,从而非常影响性能,所以在需要支持高并发的场景下应该避免出现同步。

锁优化

(1)锁的可重入性,在synchronized方法中,可以继续调用该实例的其他的synchronized方法,这就是可重入性。

(2)减少锁的时间,不需要同步的代码就不需要放在同步块中

(3)减少锁的粒度,提高并发性如ConcurrentHashMap的分段锁

(4)锁粗化

大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度; 在以下场景下需要粗化锁的粒度: 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;

(5)锁分离,如读写锁。

ReentrantReadWriteLock 是一个读写锁,读操作加读锁,可以并发读,写操作使用写锁,只能单线程写;或者如CopyOnWriteArrayList 、CopyOnWriteArraySet CopyOnWrite即写时复制的容器,这种适合读多写少,且总数量不大的情况下,否则复制也是一笔很大的开销。

(6)使用CAS

如果需要同步的操作执行速度非常快,并且线程竞争并不激烈,这时候使用cas效率会更高,因为加锁会导致线程的上下文切换,如果上下文切换的耗时比同步操作本身更耗时,且线程对资源的竞争不激烈,使用volatiled cas操作会是非常高效的选择;

总结

本文主要介绍了Java里面对锁优化的相关内容,主要目的是为了避免频繁的线程的上下文切换导致的对应用程序性能的影响

0 人点赞