Java基础之十年面试杂记(下)

简介: Java基础之十年面试杂记

什么是jvm


Jvm是一种用于计算机设备的规范,他是一个虚构出来的计算机 Java语言的一个非常重要的特点就是与平台的无关性,而使用java虚拟机是实现这一特点的关键 一般的语言 在不同平台上进行运行 需要编译成不同的目标代码 但是Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行 只需生成在java虚拟机上运行的目标代码,就可以在多重平台上不加修改的运行 这就是“一出编译 多出运行”的原因


在执行Thread.start()方法后,线程是不是马上运行:


当执行start()方法后 线程状态编程runnable(运行)状态 但是线程这个时候并不会马上运行 而是调用native方法 由他在操作系统中新建一个线程 但是操作系统上新创建的线程也是不会立即执行的 他会线程需要被cpu调度,分配了时间片之后才会真正的运行。因此jvm中的RUNNABLE状态其实对应了两个状态,ready和runnable。创建的新线程是ready状态,被cpu调度后成为runnale状态,这时候才是真正的运行状态。


Jvm堆在jdk1.7和1.8区别:


1.8版本用元数据区取代了1.7版本及以前的永久代。元数据区和永久代本质上都是方法区的实现。


不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存(也就是说jvm可以使用外边的内存)。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小


-XX:MetaspaceSize=N:设置metaspace区域的最大值 如果这个参数没有设置,那么就是通过mxbean拿到的最大值是-1,表示无穷大


-XX:MaxMetaspaceSize=N:表示metaspace首次使用不够而触发FGC的阈值,只对触发起作用,原因是:垃圾搜集器内部是根据变量_capacity_until_GC来判断metaspace区域是否达到阈值的


Gc收集相关算法:


(1)引用计数法 Reference Counting


给对象添加一个引用计数器,每过一个引用计数器值就+1,少一个引用就-1。当它的引用变为0时,该对象就不能再被使用。它的实现简单,但是不能解决互相循环引用的问题。


(2)根搜索算法 GC Roots Tracing


以一系列叫“GC Roots”的对象为起点开始向下搜索,走过的路径称为引用链(Reference Chain),当一个对象没有和任何引用链相连时,证明此对象是不可用的,用图论的说法是不可达的。那么它就会被判定为是可回收的对象。


JAVA里可作为GC Roots的对象


虚拟机栈(栈帧中的本地变量表)中引用的对象


方法区中的类静态属性引用的对象


方法区中的常量引用的对象


本地方法栈中JNI(即Native方法)的引用的对象


(3)标记-清除算法 Mark-Sweep


这是一个非常基本的GC算法,它是现代GC算法的思想基础,分为标记和清除两个阶段:先把所有所有需要回收的对象标记出来,然后把没有被标记的对象统一清除掉。但是它有两个问题,一是效率问题,两个过程的效率都不高。二是空间问题,清除之后会产生大量不连续的内存。


20210716110434682.png

(4)复制算法 Copying


复制算法是将原有的内存空间分成两块,每次只使用其中的一块。在GC时,将正在使用的内存块中的存活对象复制到未使用的那一块中,然后清除正在使用的内存块中的所有对象,并交换两块内存的角色,完成一次垃圾回收。它比标记-清除算法要高效,但不适用于存活对象较多的内存,因为复制的时候会有较多的时间消耗。它的致命缺点是会有一半的内存浪费。

20210716110449424.png

(5)标记整理算法 Mark-Compact


标记整理算法适用于存活对象较多的场合,它的标记阶段和标记-清除算法中的一样。整理阶段是将所有存活的对象压缩到内存的一端,之后清理边界外所有的空间。它的效率也不高。

20210716110503919.png

什么是死锁?


两个进程都在等待对方执行完毕才能继续往下执行的时候就发生了死锁。结果就是两个进程 都陷入了无限的等待中。如何预防死锁:


处理死锁的思路如下:


预防死锁:破坏四个必要条件中的一个或多个来预防死锁。


避免死锁:在资源动态分配的过程中,用某种方式防止系统进入不安全的状态。


检测死锁:运行时产生死锁,及时发现思索,将程序解脱出来。


解除死锁:发生死锁后,撤销进程,回收资源,分配给正在阻塞状态的进程。


预防死锁的办法:


破坏请求和保持条件:


1、一次性的申请所有资源。之后不在申请资源,如果不满足资源条件则得不到资源分配。


2、只获得初期资源运行,之后将运行完的资源释放,请求新的资源。


破坏不可抢占条件:当一个进程获得某种不可抢占资源,提出新的资源申请,若不能满足,则释放所有资源,以后需要,再次重新申请。


破坏循环等待条件:对资源进行排号,按照序号递增的顺序请求资源。若进程获得序号高的资源想要获取序号低的资源,就需要先释放序号高的资源。


扩展资料


形成死锁的四个必要条件:


(1)互斥条件:一个资源每次只能被一个进程使用。


(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。


(3)不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。


(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。


20210716110522648.png

乐观锁与悲观锁区别:


悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源只给一个线程使用 而其他线程阻塞 用完之后 才会把资源让给其他线程 所以传统关系型数据库用这种锁比较多 操作前先上锁)


实现:使用了select…for update的方式将数据锁住 通过开启排他锁的方式实现了悲观锁

如:


select status from t_goods where id=1 for update;

此时id为1的那条数据就被我们锁住了 其他事务必须等这次事务执行完毕才能执行 保证了数据不会被其他事务修改


如:


Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。


应用场景:多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适


问题:


1.加锁的机制造成数据库效率变低 并增加死锁的几率

2.在只读事务中 不会产生冲突 没必要用锁 只会增加系统负载压力

3.降低了并行性,一个事务如果锁定了某行数 据,其他事务就必须等待该事务处理完才可以处理那行数


乐观锁:


总是假设最好的情况 每次别人拿数据认为别人都不会进行修改 不上锁 但是在更新数据前会判断别人有没有进行更新(在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号进行实现)乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,都是提供的乐观锁


应用场景:适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量


问题:两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就可能会遇到不可预 期的结果


总结


乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机智的其实都是提供的乐观锁。 相反,如果经常发生冲突,上层应用会不断进行 retry,这样反而降低了性能,所以这种情况下用悲观锁比较合适


多线程:


20210716110558685.png


无论集成谁(excute(runable接口中提交方法)) 都有一个问题 就是无法立刻获取任务执行结果 可通过excutor线程池提供一些callable、future等方法进行提交任务实现:

20210716110616642.png


线程池优点:


20210716110629428.png


线程池关闭相关方法:


20210716110646656.png

Serializable接口:


一般情况下定义:

20210716110656447.png


Serializable接口就是:


一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化


什么是序列化与反序列化?


序列化是将对象状态转换为可保持或传输的流格式的过程。与序列化相对的是反序列化,

它将流转换为对象。这两个过程结合起来,可以轻松地存储和传输数据


反序列化同理就是将可传输的流格式恢复为对象的过程


这两个过程结合起来,可以轻松地存储和传输数据。


把对象转换为字节序列的过程称为对象的序列化


把字节序列恢复为对象的过程称为对象的反序列化


为什么要定义serialversionUID变量:


不定义运行时也会自行创建这个变量,但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致

InvalidClassException这个异常。用来辅助对象的序列化和反序列化的


详细过程:当序列化的时候系统将serialVersionUID写入到序列化的文件中去,当反序列化的时候系统会先去检测文件中的serialVersionUID是否跟当前的文件的

serialVersionUID是否一致,如果一直则反序列化成功,否则就说明当前类跟序列化后的类发生了变化,比如是成员变量的数量或者是类型发生了变化,那么在反序列化时就会发生crash,并且回报出错误InvalidClassException


***声明式事物(即以配置或注解的方式对事物进行声明):


spring给了一个约定(AOP开发也给了我们一个约定),如果使用的是声明式事务,那么当你的业务方法不发生异常(或者发生异常,但该异常也被配置信息允许提交事务)时,Spring就会让事务管理器提交事务,而发生异常(并且该异常不被你的配置信息所允许提交事务)时,则让事务管理器回滚事务


事务的理解


用一个简单例子说明:银行转帐业务,账户A要将自己账户上的1000元转到B账户下面,A账户余额首先要减去1000元,然后B账户要增加1000元。假如在中间网络出现了问题,A账户减去1000元已经结束,B因为网络中断而操作失败,那么整个业务失败,必须做出控制,要求A账户转帐业务撤销。这才能保证业务的正确性,完成这个操走就需要事务,将A账户资金减少和B账户资金增加放到同一个事务里,要么全部执行成功,要么全部撤销,这样就保证了数据的安全性。


事务具有四个特性,也是面试常考的四个特性ACID:


A(原子性Atomicity):原子性指的是事务是一个不可分割的,要么都执行要么都不执行。

C(一致性Consistency):事务必须使得数据库从一个一致性状态,到另外一个一致性状态。

I(隔离性Isolation):指的是一个事务的执行,不能被其他的事务所干扰。

D(持久性Durability):持久性指的是一个事务一旦提交了之后,对数据库的改变就是永久的。


创建一个事务


事务其实可以划分为两大类:隐式的事务和显示的事务


隐式的事务很简单,比如我们的insert、delete、update、select这些语句都是隐式的事务。


显示的事务指的是带有很明显的开始和结束的标记,下面就来创建一个显示的事务。


步骤一:禁用步骤提交功能


set autocommit = 0;


步骤二:开启一个事务


start transaction;


步骤三:sql语句


update table user set money=500 where name = “张三”;


update table user set money=1500 where name = “李四”;


步骤四:结束事务


commit(提交)或者是rollback(回滚)。如果确定我们的语句没有问题,那么我们就可以commit,如果认为我们的语句有问题,那就rollback。


20210716110720543.png


在这里新建了一个表,然后插入了两条数据。下面我们使用事务,来更新一下:


20210716110731210.png


在这里我们使用的是commit进行提交。当然如果突然发现我们之前的操作有错误,那就可以使用rollback。


事务的隔离级别


我们接下来就看看这四个隔离级别的具体情况


读未提交(Read Uncommitted)


读未提交,顾名思义,就是可以读到未提交的内容。


因此,在这种隔离级别下,查询是不会加锁的,也由于查询的不加锁,所以这种隔离级别的一致性是最差的,可能会产生“脏读”、“不可重复读”、“幻读”。


如无特殊情况,基本是不会使用这种隔离级别的。


读提交(Read Committed)


读提交,顾名思义,就是只能读到已经提交了的内容。


这是各种系统中最常用的一种隔离级别,也是SQL Server和Oracle的默认隔离级别。这种隔离级别能够有效的避免脏读,但除非在查询中显示的加锁,如:


select * from T where ID=2 lock in share mode;
select * from T where ID=2 for update;

不然,普通的查询是不会加锁的。


那为什么“读提交”同“读未提交”一样,都没有查询加锁,但是却能够避免脏读呢?

这就要说道另一个机制“快照(snapshot)”,而这种既能保证一致性又不加锁的读也被称为“快照读(Snapshot Read)”


假设没有“快照读”,那么当一个更新的事务没有提交时,另一个对更新数据进行查询的事务会因为无法查询而被阻塞,这种情况下,并发能力就相当的差。


而“快照读”就可以完成高并发的查询,不过,“读提交”只能避免“脏读”,并不能避免“不可重复读”和“幻读”。


可重复读(Repeated Read)

可重复读,顾名思义,就是专门针对“不可重复读”这种情况而制定的隔离级别,自然,它就可以有效的避免“不可重复读”。而它也是MySql的默认隔离级别。


在这个级别下,普通的查询同样是使用的“快照读”,但是,和“读提交”不同的是,当事务启动时,就不允许进行“修改操作(Update)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改,因此,“可重复读”能够有效的避免“不可重复读”,但却避免不了“幻读”,因为幻读是由于“插入或者删除操作(Insert or Delete)”而产生的。


这是数据库最高的隔离级别,这种级别下,事务“串行化顺序执行”,也就是一个一个排队执行。


这种级别下,“脏读”、“不可重复读”、“幻读”都可以被避免,但是执行效率奇差,性能开销也最大,所以基本没人会用。


上面的事务在单个情况下一般不会出现什么问题,但是如果同时运行多个,就会出现问题了。我们知道并发操作总是会出现各种各样的问题,对于事务来说就会出现下面三个典型的问题:


(1)脏读


有俩事务T1,T2。如果T1读了一条数据,这条数据是T2更新的但是还没提交,突然T2觉得不合适进行事务回滚了,也就是不提交了。此时T1读的数据就是无效的数据。


(2)不可重复读


有俩事务T1,T2。如果T1读了一条数据,之后T2更新了这条数据,T1再次读取就发现值变了。


(3)幻读


有俩事务T1,T2。如果T1读了一条数据,之后T2插入了一些新的数据,T1再次读取就会多出现一些数据。


如何去解决这些问题呢?既然多个事务同时运行不好,那就把他们隔离开来。这时候就用到了事务的隔离性。


20210716110752598.png


mysql默认的事务隔离级别为repeatable-read,Oracle默认的是read-committed


为什么会出现“脏读”?因为没有“select”操作没有规矩。


为什么会出现“不可重复读”?因为“update”操作没有规矩。


为什么会出现“幻读”?因为“insert”和“delete”操作没有规矩。


“读未提(Read Uncommitted)”能预防啥?啥都预防不了。


“读提交(Read Committed)”能预防啥?使用“快照读(Snapshot Read)”,避免“脏读”,但是可能出现“不可重复读”和“幻读”。


“可重复读(Repeated Red)”能预防啥?使用“快照读(Snapshot Read)”,锁住被读取记录,避免出现“脏读”、“不可重复读”,但是可能出现“幻读”。


“串行化(Serializable)”能预防啥?排排坐,吃果果,有效避免“脏读”、“不可重复读”、“幻读”,不过效果谁用谁知道。


目录
相关文章
|
2月前
|
安全 架构师 Java
Java大厂面试高频:Collection 和 Collections 到底咋回答?
Java中的`Collection`和`Collections`是两个容易混淆的概念。`Collection`是集合框架的根接口,定义了集合的基本操作方法,如添加、删除等;而`Collections`是一个工具类,提供了操作集合的静态方法,如排序、查找、同步化等。简单来说,`Collection`关注数据结构,`Collections`则提供功能增强。通过小王的面试经历,我们可以更好地理解这两者的区别及其在实际开发中的应用。希望这篇文章能帮助你掌握这个经典面试题。
58 4
|
4月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
144 2
|
2月前
|
Java 程序员
Java社招面试中的高频考点:Callable、Future与FutureTask详解
大家好,我是小米。本文主要讲解Java多线程编程中的三个重要概念:Callable、Future和FutureTask。它们在实际开发中帮助我们更灵活、高效地处理多线程任务,尤其适合社招面试场景。通过 Callable 可以定义有返回值且可能抛出异常的任务;Future 用于获取任务结果并提供取消和检查状态的功能;FutureTask 则结合了两者的优势,既可执行任务又可获取结果。掌握这些知识不仅能提升你的编程能力,还能让你在面试中脱颖而出。文中结合实例详细介绍了这三个概念的使用方法及其区别与联系。希望对大家有所帮助!
202 60
|
27天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
105 14
|
1月前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
57 13
|
2月前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
101 16
|
2月前
|
Java 程序员 调度
Java 高级面试技巧:yield() 与 sleep() 方法的使用场景和区别
本文详细解析了 Java 中 `Thread` 类的 `yield()` 和 `sleep()` 方法,解释了它们的作用、区别及为什么是静态方法。`yield()` 让当前线程释放 CPU 时间片,给其他同等优先级线程运行机会,但不保证暂停;`sleep()` 则让线程进入休眠状态,指定时间后继续执行。两者都是静态方法,因为它们影响线程调度机制而非单一线程行为。这些知识点在面试中常被提及,掌握它们有助于更好地应对多线程编程问题。
87 9
|
2月前
|
安全 Java 程序员
Java面试必问!run() 和 start() 方法到底有啥区别?
在多线程编程中,run和 start方法常常让开发者感到困惑。为什么调用 start 才能启动线程,而直接调用 run只是普通方法调用?这篇文章将通过一个简单的例子,详细解析这两者的区别,帮助你在面试中脱颖而出,理解多线程背后的机制和原理。
78 12
|
2月前
|
监控 Dubbo Java
Java Dubbo 面试题
Java Dubbo相关基础面试题
|
2月前
|
SQL Java 数据库连接
Java MyBatis 面试题
Java MyBatis相关基础面试题

热门文章

最新文章