《面试补习》- Java锁知识大梳理

2020/3/9 17:02:24

本文主要是介绍《面试补习》- Java锁知识大梳理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

面试补习系列:

一、锁的分类

1、乐观锁和悲观锁

乐观锁就是乐观的认为不会发生冲突,用cas和版本号实现 悲观锁就是认为一定会发生冲突,对操作上锁

1.悲观锁

悲观锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。
复制代码

适用场景:

比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

实现方式: synchronizedLock

2.乐观锁

每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制

ABA问题(JDK1.5之后已有解决方案):CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

循环时间长开销大:CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。

只能保证一个共享变量的原子操作(JDK1.5之后已有解决方案):对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
复制代码

适用场景:

比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

实现方式:

1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标识,不一致时可以采取丢弃和再次尝试的策略。

2、Java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

3、在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

2、公平锁/非公平锁

公平锁:

指多个线程按照申请锁的顺序来获取锁。
复制代码

非公平锁:

指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
有可能,会造成优先级反转或者饥饿现象。

复制代码

拓展线程饥饿:

一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态
导致无法获取的原因:
线程优先级较低,没办法获取cpu时间
其他线程总是能在它之前持续地对该同步块进行访问。
线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其他线程总是被持续地获得唤醒。

复制代码

实现方式: ReenTrantLock(公平/非公平)

对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS(AbstractQueuedSynchronizer)的来实现线程调度,所以并没有任何办法使其变成公平锁。

3、可重入锁

如果一个线程获得过该锁,可以再次获得,主要是用途就是在递归方面,还有就是防止死锁,比如在一个同步方法块中调用了另一个相同锁对象的同步方法块

实现方式: synchronizedReentrantLock

4、独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
复制代码

实现方式: 独享锁: ReentrantLocksynchronized 贡献锁: ReadWriteLock

拓展:

互斥锁/读写锁 就是对上面的一种具体实现:

互斥锁:在Java中的具体实现就是ReentrantLock,synchronized
读写锁:在Java中的具体实现就是ReadWriteLock
复制代码

对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁

5、偏向锁/轻量级锁/重量级锁

基于 jdk 1.6 以上

偏向锁指的是当前只有这个线程获得,没有发生争抢,此时将方法头的markword设置成0,然后每次过来都cas一下就好,不用重复的获取锁.指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价

轻量级锁:在偏向锁的基础上,有线程来争抢,此时膨胀为轻量级锁,多个线程获取锁时用cas自旋获取,而不是阻塞状态

重量级锁:轻量级锁自旋一定次数后,膨胀为重量级锁,其他线程阻塞,当获取锁线程释放锁后唤醒其他线程。(线程阻塞和唤醒比上下文切换的时间影响大的多,涉及到用户态和内核态的切换)

实现方式: synchronized

6、分段锁

在1.7的concurrenthashmap中有分段锁的实现,具体为默认16个的segement数组,其中segement继承自reentranklock,每个线程过来获取一个锁,然后操作这个锁下连着的map。

实现方式:

我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,
它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一
                   

这篇关于《面试补习》- Java锁知识大梳理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程