计算机系统应用教程网站

网站首页 > 技术文章 正文

互联网面试-Java中各种锁机制介绍?

btikc 2024-10-08 01:17:43 技术文章 6 ℃ 0 评论

在Java中存在各种各样的锁机制,可以支持在各种不同场景下的业务逻辑,并且在对应场景下有着高效的执行效率。下面我们就来看一下在Java中常见的锁机制以及使用场景。

乐观锁与悲观锁

根据概念,两个线程对同一个共享资源进行并发操作,悲观锁认为,在自己操作数据的时候一定会有其他线程来对数据进行修改,这个时候,就会先对共享资源进行加锁操作,这样可以确保在自己使用的过程中数据不会被其他线程修改,在Java中Synchroinzed关键字和Lock锁都可以实现类似悲观锁的功能。

而乐观锁则是认为自己在修改数据的时候,不会有其他线程来修改数据,所以不会对共享资源进行加锁操作,并且最终只需要在更新的时候对数据进行判断即可,如果没有其他线程更新数据那么自己就更新数据,如果其他线程已经更新过数据了,那么当前线程就不会对数据进行更新,并且根据具体的业务进行相应。乐观锁在Java中通常采用无锁编程来实现,其中最常用的方式就是CAS算法。在Java中对于原子类的操作就是基于CAS自旋方式来实现。

根据上面的介绍,对于悲观锁来讲,主要是用于适用于写操作较多的场景,首先因为写操作较多最重要的事情就是要保证写操作的数据一致性。而对于乐观锁来讲,其主要适用的场景就是读操作较多的场景,因为只是涉及数据的读取,不会对数据进行修改,所以在读取的时候并不需要数据的强一致性,使用乐观锁可以极大的提升系统的性能。

自旋锁和自适应自旋锁

首先我们在了解自旋锁之前需要知道什么是自旋?在我们CPU执行一个操作的时候,由于每个线程都需要自己执行的上下文环境,所以当CPU所执行的当前线程发生变化的时候,就会对线程上下文进行切换,而切换线程上下文是一个非常耗时的操作。

在很多的使用场景中,同步资源的锁定时间都非常的短,了这一个很短的锁定时间去切换线程上下文资源,其实有点得不偿失了。尤其是对现在的多处理系统来讲,多线程并发其实是一个很常见的现象,并不需要为了某个线程的一点点的锁定释放时间来消耗线程上下文切换的时间,这个时候,我们就可以让线程在这个操作上等待一下,这个线程等待的时间要比线程上下文切换的时间要短,从而避免了在线程切换上的资源开销,这就是整个的自旋的概念。如图所示。

自旋锁本身的也是存在缺点的,就是它不能代替阻塞。根据上面的描述虽然自旋可以避免线程上下文切换的开销,但是它是占用CPU的处理时间,如果自旋锁占用时间短的情况,自旋的效果就是非常好的,但是如果自旋锁被线程占用时间较长的时候,这个自旋锁就相当于在浪费资源。所以在我们使用自旋锁的时候自旋的时间一定是有所控制的。例如我们可以通过-XX:PreBlockSpin参数来更改自旋锁的自旋次数。

无锁、偏向锁、轻量级锁、重量级锁

这四种锁其实是指Synchroinzed锁的四种状态。原则上来讲,在我们使用同步资源加锁的时候,都会尽量将锁定同步块的范围控制在一个尽量小的范围内。这样可以节省很多的资源,避免了除共享资源之外的其他资源的浪费。但是这这只是一个理想的状态,而最理想的状态就是无锁。

偏向锁

在多线程访问共享资源的时候,锁的获取不但存在线程与线程之间的竞争,还存在同一个线程多次获取共同一个锁的情况。这种情况下其实并不涉及到锁竞争,并且如果这种情况出现的概率大的话,如果这个锁能很容易被这个线程持有的话,就会避免线程上下文切换带来的资源损耗。而这个时候就需要我们的偏向锁的参与,也就是说如果当某个线程获取到该锁次数较多的情况下,这个锁就会更加偏向于这个该线程获取。

轻量级锁

轻量级锁其实是指在同步块不会有太多的线程来进行竞争的时候而提供的一种锁优化的方案。它可以在一定程度上减少重量级锁对线程带来的阻塞所带来的线程开销。轻量级锁在底层原理实现上其实可以理解为前面提到的乐观锁,在默认情况下会认为没有多个线程来抢占该锁,当发现有其他线程抢占该锁的时候,轻量级锁就会失效变成重量级锁。

重量级锁

与轻量级锁相比,重量级锁就是真正的悲观锁,在线程获取到锁的情况下,其他线程只能等待锁释放。也就是说除了得到锁的线程,其他线程全部都是阻塞的。

公平锁与非公平锁

在多个线程获取锁的过程中,如何能够保证每个线程获取锁的概率都是一样的呢?其实这是无法保证的。当线程获取锁的时候会进入到等待队列中,那么这个根据这个队列的特性,第一个排队的线程就应该比最后一个进入队列的线程更有优先权来获取到锁。而这个时候如果是公平锁的情况,那么就会按照顺序,从第一个到最后一个依次获取资源来进行处理。但是所带来的问题就是整体的效率较低,因为在有些情况下,会将一些不那么重要的线程排在前面,而将重要的线程排在后面。这样整个的执行效率就会非常低。

那么这个时候非公平锁就出现了。多个线程等待获取锁,获取不到的时候就会到队列尾部等待,如果这个时候锁的数量正好够用,那么每个线程都会获取到锁,但是就会有锁不够的情况,那么这个时候就会有限考虑一些高吞吐的线程,而对于一些边缘的处理线程则会因为获取不到锁一直在队列中等待,就会被遗忘,而无法得到执行。如图所示,图片来源网络


锁的可重入性

锁的可重入又可以被称为是锁的递归,什么是递归,就是再方法内部继续调用同样的方法,而在这个方法中还要存在递归结束的条件。那么对于一个锁来讲,它要实现递归,就是指在获取到该锁的线程中如果因为逻辑需要再次调用该锁的时候也应该能正常获取到该锁。

在Java中ReentrantLock和Synchroinzed都是可重入锁。可重入锁的有点就是再一定程度上可以避免死锁现象的发生。

假设线程A获取到和锁A这个时候,在线程A中又调用到了共享资源A,这个时候锁A正好被线程A锁占用,如果不是可重入的话,这个情况就相当于蛇头咬蛇尾一样形成了一个死循环也就是所谓的死锁。

而如果这个锁A是一个可重入锁的时候,那么就可以继续获取到锁A并且将锁的计数器加一,以便在释放的时候可以将所有的加锁操作都进行释放。

共享锁与排他锁

排他锁就是指一次只能被一个线程持有,如果线程对数据加上的排他锁之后,其他线程都不能获取到该锁了,获取到排他锁的线程既能对数据进行修改,也能对数据进行读取。

共享锁则是指该锁可以被多个线程持有,如果线程对数据加上了共享锁,那么其他线程也只能对数据加上共享锁,不能再进行修改锁类型,获取共享锁的线程只能对数据进行读取而无法对数据进行修改。

也就是说,读锁其实就是一个共享锁,而写锁就是一个排他锁

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表