【Java相关八股文(一)】

作者: 小飞学编程..._CSDN博客-编程小白

专栏:JavaEE初阶

标题:Java相关八股文

Java相关八股文

引言

在我的前面的文章也介绍了关于锁的使用以及注意事项,这是我们学习与工作中必须掌握的知识;接下来我在这篇文章中聊一聊关于锁的其他方面的相关知识;这些知识是我们在面试中经常会遇到的相关问题,即我们所谓的"八股文";

常见的锁策略

Java标准库中给我们提供了关于锁的使用相关类,但是当我们自己想要去实现一把锁,那该怎么办呢???

所以我们首先就需要进行考虑锁的一些特性了,即在什么情况下我们会进行锁的使用呢???

一.悲观锁与乐观锁;

在生活中,我们人都有不同的心情表现,我们会高兴与快乐,悲伤与难过,作为我们设计出来的锁,自然也会有这些特性;
乐观锁:

在进行锁的使用的时候就会进行预测接下来对于锁的竞争激烈程度如何,如果锁的竞争激烈程度不大,我们不需要进行一些额外的操作,我们根据这种情况设计出来的锁就叫做"乐观锁";
悲观锁;

进行的锁的使用的时候,预测接下来锁的竞争激烈程度很大,需要我们进行一些额外的操作,这时我们根据这种情况下进行设计出来的锁就叫做"悲观锁";

复制代码
上面我们谈到的在某些不同场景下考虑到一些锁相关的特性;
接下来,我们进行遇到具体场景下我们的对于锁的实现先关的方案锁介绍;

二.重量级锁与轻量级锁

重量级锁

在悲观的场景下,锁的竞争激烈程度就很大,所以我们需要付出一定代价进行处理;这样的锁就称为重量级锁;
轻量级锁

在乐观的条件下,锁的竞争激烈程度较小,这样付出较小的代价就能解决问题,这样的锁就称为轻量级·锁;
对比

考虑到代价影响程度:重量级锁:更低效;轻量级锁;更高效;

复制代码
接下来,我们进行锁的具体实现的相关锁的介绍;

三.挂起等待锁与自旋锁

挂起等待锁

这个锁的设计是根据重量级锁的相关解决方案设计成的一把锁,

属于操系统内核级别的锁,在加锁的时候发现有其他线程进行锁的竞争情况,就会进行阻阻塞等待,后续要想唤醒竞争这个锁的线程就需要通过操作系统的内核进行唤醒了;


自旋锁

这个锁的设计是根据轻量级锁的相关方案进行设计成的一把锁;

属于应用程序级别的锁,在加锁的时候发现有其它的线程进行锁竞争的情况,也不会进行阻塞,就进行忙等,即轻量级锁的锁竞争压力很小,后续遇到锁竞争的情况概率很小,并且遇到也会叫快的获取到锁的使用权;

四.普通互斥锁与读写锁

普通互斥锁

在线程中:关于读操作方面是不涉及线程安全问题的,即使多个线程进行读取数据也不互斥;

但是:由于有一些线程进行了写操作,这样就会使得读操作不准确,为了提供读操作读到的正确性,于是就将读操作与写操作都进行了加锁操作;这样的加锁操作就是使用的普通互斥锁;即synchronized锁;


读写锁

由于先前普通互斥锁的使用,把读与读的操作也进行互斥处理,这样对读效率没有提高反倒是减少了,于是我们就引入到了读写锁,读写锁中:将多个线程进行读操作时不产生互斥,各自不受影响,将读操作与写操作进行加上读写锁操作;确保读和读操作不会产生互斥,不会产生阻塞,读与写操作会产生阻塞;

注意:写与写操作也会产生阻塞;

五.可重入锁与不可重入锁

一个线程,一把锁,把这个线程连续进行加锁多次都不会出现死锁的锁就是可重入锁;反之,一个线程,一把锁,只能将这个线程进行加一把锁,不能进行加多把锁的操作;这样的锁就叫做不可重入锁;

六.公平锁与不公平锁

我们知道在多线程的情况下,发生阻塞是很常见的,但是当一个线程用完这把锁的时候就会使得锁空闲下来,这样获取到这把锁的线程是按照先来后到的顺序获取锁呢还是什么情况呢???
公平锁

这是由于操作系统对于线程的调用是随机的,所以阻塞等待的线程获取到锁的概率是相同的,这样的锁就称为通过概率均等获取锁的策略就属于公平锁;
非公平锁

不同通过概率均等获取到锁的情况都是属于非公平锁;例如:按照先来后到顺序进行获取锁的策略来实现的锁就属于非公平锁;

注意:我们要想自己实现一把不公平的锁就需要我们进行使用的相关队列,如优先级队列:进行记录阻塞线程的顺序,进行设置优先级进行获取;

复制代码
 有了上述知识的铺垫,我们明白了关于锁得相关策略方案了,
 下面我们进行考虑一下,
 我们学过的synchronized锁是属于哪一种锁;

synchronized锁属于自适应,可重入,不公平锁;


自适应锁

synchronized锁是Java标准库提供给我们的一个锁的类,在设计这个锁的时候,大佬们就想到了会可能出现乐观锁与悲观锁的情况,于是在JVM内部中:去统计接下来使用锁遇到的情况,属于那种激烈程度的情况去让synchronized去进行自主调节去适应不同的情况来进行锁调整;

即遇到竞争激烈程度大,调整为挂起等待锁;

即遇到竞争激烈程度小,调整为自旋锁;

复制代码
通过上面知识的了解,我们已经明确了关于设计锁的相关策略问题了,接下来,我们进行学习:
随着时代的发展;锁是如何一步步进行升级改造的,发展到我们目前如更好的使用到的锁;

锁升级(锁的发展历程)

锁升级示意图


上面的示意图就是展示了随着时代的发展,锁的一步步进行升级后的锁;上述几种锁中:我们除了关于

偏向锁,其它的我们都进行学习过;呢么,下面我们就来聊一聊关于偏向锁;

偏向锁

在我们进行使用到synchronized锁的时候,我们其实不是在进行真正的进行加锁操作,当只有一个线程使用到这个锁的时候,我们代码中虽然使用到了synchronized,但是我们并没有进行真正的进行加锁操作,只是做一个标记操作,当代码执行到解锁操作的时候,我们也会消除这个标记,这样做标记的方法和真正进行加锁的操作方面就大大的提供代码执行的效率了,当然在进行到中途的时候,也有可能遇到有其他的线程要对使用这个锁对象,这时,偏向锁就会提前,由原本得标记操作转变为真正的加锁操作;这样其他线程也只能看到锁对象已经被使用,只能进行阻塞等待了;

上述的执行过程就是我们的偏向锁的执行流程;

锁消除

作为小白的我们,当学习了关于多线程安全相关的问题后,我们老是在第一时间想到使用加锁操作去解决安全问题,这样就会导致,即使这段代码没有线程安全问题我们也会进行加锁操作,或者有安全问题不是加锁可以解决的同样我们也进行了加锁操作;

基于以上情况,Java大佬们就想到了使用锁消除操作;
锁消除

锁消除就是我们在写的代码中加了锁,但是对于代码的执行逻辑没有用,这时编译器在进行编译的时候就会将这个加锁操作优化掉;这就是所谓的锁消除;

锁粗化

在进行锁粗化的了解前,我们先进行了解一个与之相关的术语;
锁粒度

在进行加锁与解锁操作之间,我们执行的相关代码越多,锁粒度越粗,反之,执行的代码越少,锁的粒度就越细;注意:执行的代码多不是指的是代码行数多,而是实际执行的指令消耗的时间少;
锁粗化

当在进行执行一段代码的时候,多次多细粒度的代码进行加锁操作,这样就很有可能被优化为粗粒度加锁;这样的策略就叫做锁粗化;
生活案例

类似于;当老板交给三个小任务,我们起初选择执行完一个小任务就去报告给老板,一个接一个的进行报告,其中汇报时间间隔就几分钟,这样就像我们在不断地进行加锁操作,多次的加锁操作会加剧锁竞争的,大大影响执行效率,现在我们进行优化操作,变成了吧这三个小任务都完成的时候:汇报给老板,这样相当于我们只进行了一次加锁操作,这样就大大提高了我们的执行效率;

对于锁的相关知识的学习,我们就进行到这里,那么我们可以不使用锁来解决一些相关的线程安全问题吗??

下面我们就进行介绍一下,我们有时候不使用锁也是可以解决线程安全问题的;;

CAS

①:含义:CAS属于一条CPU指令

②;使用由来:CAS是CPU的一条指令,操作系统把这条指令进行了封装操作,并且提供了一些api,这样就使得其可以在C++语言中进行使用,而我们的JVM底层是基于C++实现的,这样JVM就可以使用C++进行调用CAS,这样我们在java层面也就可以进行使用了;

③:作用:实现原子类;进而可以进行避免加锁操作;

在我前面的线程安全的文章中讲到:count++;这句代码在进行编译的时候会执行:load,add,save这三步操作,在多线程的情况下,由于线程被随机调度的原因,会导致这几步操作,不能一次性完成,这样就会产生线程安全问题,这样就需要进行加锁操作,而加锁操作也会影响我们的执行效率。

基于上述的情况:我们使用到了CAS进行对count++进行操作,这样既能保证线程的安全也能提高效率;

对count++操作就使用到了原子类功能,这时CAS的主要用途;

CAS在代码中的应用;

我们要想要更好的使用CAS,我们就必须了解它的底层代码,下面我们以图解的形式进行展现分析,方便我们更好的进行理解;


复制代码
我们前面谈到了CAS的主要用途就是实现了原子类,还有就是可以解决关于count++不用加锁,
就能保证线程的安全;下面我们进行具体分析以及相关伪代码实现;

利用CAS解决count++操作线程不安全问题

①:在Java标准库中提供了 java.util.concurrent.atomic 包, ⾥⾯的类都是基于这种⽅式来实现的.

典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作

②:伪代码实现;

java 复制代码
class AtomicInteger {
private int value;
public int getAndIncrement() {
int oldValue = value;
while ( CAS(value, oldValue, oldValue+1) != true) {
oldValue = value;
}
return oldValue;
}
}

代码分析

一.oldvalue:的值就相当于寄存器中保存的count值,初始值为0;

value:的值相当于真正内存中保存的count的值;

二.起初count的值为0,所以value和oldvalue的值都相等为0;这样就会触发CAS中的赋值操作,这样就会导致value的值进行加一;完成了count++的操作;

这里需要注意三点:

①:寄存器oldvaue的值没有发生变化还是为0;

②:执行完CAS的赋值操作会返回true的结果,这样就会无法进入到while循环中;;

③:结束while循环后会返回寄存器的值,还是没有变,这时为下一次的count++操作做铺垫;但是实际的内存中以及完成了count++操作,得到了结果值,就可以了,因为我们获取count的值的时候是从内存中进行取值,所以就完成了我们想要的结果了;

④:上述伪代码知只是写了一次count++的操作;所以为了更加明确返回寄存器的值没有变的作用,我们要进行分析count++进行第二次的操作;第二次操作就是我们在进行一次while循环的操作;

三。通过操作,现在:value=1;oldvalue=0;

进行判断能否进入到while循环中,接着进入CAS中:这时:value和oldvalue的值不同就会直接返回false的结果;这样就可以真正进入到while循环中,这时就执行到oldValue = value;更新寄存器的值,使得其变成1;这样就在一次进行while循环的判断:进入到CAS中,继续执行,仍然只会更新内存中的值,而不进行更新寄存器的值;这样就完成了,在进行每次的count++操作都会完成寄存器先获取内存中值的操作,完后在进行更新内存中的值;

这样就通过CAS的比较和赋值操作成功的完成了count++操作的正确执行;即使发生在多线程的情况下,通过CAS的这两部结合操作实现的原子类操作,使得其它线程无法在这两步操作间加入其它代码执行逻辑,这样就保证了count++的正确性;


复制代码
 通过上述CAS的底层原理分析,以及线程安全问题的解决,我们在进行体会CAS的用途,
 强化我们对CAS的理解;

基于CAS实现自旋锁

java 复制代码
public class SpinLock {
private Thread owner = null;
public void lock(){
while(!CAS(this.owner, null, Thread.currentThread())){
}
}
public void unlock (){
this.owner = null;
}
}

代码解释

通过 CAS 看当前锁是否被某个线程持有. 如果这个锁已经被别的线程持有, 那么就⾃旋等待.

如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.


复制代码
我们通过上述的学习,我们了解了CAS的各种优势,但是CAS也是有一定的缺陷,
所以下面我们进行了解一下CAS的相关缺点;

CAS缺陷

关于CAS的一个典型问题就是:ABA问题;
核心操作

CAS的关键操作就是"比较和赋值"操作;
核心功能

这两部操作下来,最核心的功能就是:进行比较内存中的值和寄存器中的值是否相等;

本质上就是通过判断这两个值是否相等就可以进行确定是否有其他线程插入其中做了一些修改操作;

如果内存中的值和寄存器中的是不相等的,就说明是线程安全的;
ABA问题

但是也会存在一种特殊的情况:那就是:当value的值从A修改成了B,接这又从B改成了A;这样的就会使得我们判断前后value的值没有发生改变;这样就会导致我们误判,这就是我们谈到的ABA问题;
生活案例

生活中,有时我们会进入那种取款银行进行取款操作,当我们在一个单独的房间进行取款的时候,可能在某一回正好在我们进行取款的时候网卡了一下,这时我们就会很慌,下意识就会连续多次点击取款操作,就会触发另一个线程的执行;这就会同时产生两个线程进行扣款操作;当我们操作底层是使用了CAS进行完成操作的话,并发线程就相当于执行两次CAS操作;其实我们通过前面的学习使用CAS在多线程的情况下,是不会完成多次的扣款操作;而当我进行一次扣款操作后,CAS中value值有原来的1000变成了500;但是假设这时有第三个线程执行了向你汇款了500的操作;使得value的值又变成了1000,当再次执行另一次CAS操作的时候,内部发现寄存器中的值和value的值是相同的,就会再次执行赋值操作,使得value的值再次变为了500;这样就导致了发生资金安全问题;
ABA问题的解决

ABA问题发生的原因是因为我们的资金值是可以减少也可以增加的,我们进行设置一种变量只能进行增加操作,不能进行减少操作,于是就进行引入版本号变量进行完成CAS的内部操作就可以解决这类问题,就不会出现执行完一次CAS后再次执行CAS操作中出现ABA问题导致内存值和寄存器中的值相等的情况;

相关推荐
QCzblack1 小时前
第五周作业
android
毅炼2 小时前
Java 基础常见问题总结(5)
java·后端
前路不黑暗@2 小时前
Java项目:Java脚手架项目的通用组件的封装(七)
java·开发语言·spring boot·后端·学习·spring cloud·maven
c***03232 小时前
Mysql之主从复制
android·数据库·mysql
xj198603192 小时前
Java进阶-在Ubuntu上部署SpringBoot应用
java·spring boot·ubuntu
火焰中舞蹈的小孩2 小时前
Unity和Android Studio相互调用 CH340在unity中调用
android·ide·android studio
不是AI2 小时前
【Unity开发】一、在安卓设备上运行Unity项目
android·unity·游戏引擎
Coder_Boy_2 小时前
从单体并发工具类到分布式并发:思想演进与最佳实践(二)
java·spring boot·分布式·微服务·设计模式
p***19942 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql