知识屋:更实用的电脑技术知识网站
所在位置:首页 > 科技  > 软件

java 面试 大全

发表时间:2022-03-25来源:网络

1.并行和并发的区别?

并发是指同时执行通常不相关的各种任务,并发程序并不需要多核处理器。比如:windows系统边听歌边玩游戏,在单核cu系统中是靠cpu来回切换执行。
并行是依靠多核的优势,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
简单理解: 一个人同时做多件事情就是并发,多个人同时做多件事情就是并行。

2.sleep和wait的区别? sleep是Thread里面的方法,wait是Object里面的方法。sleep不会导致锁行为改变,调用sleep前持有对象锁,sleep后同样持有,与锁相关的都是Object里面的方法。sleep和wait都会让出cpu暂停当前线程,wait需要另外线程调用notify/notifyAll来唤醒。sleep后,用jstack查看线程状态:TIMED_WAITING (sleeping)

wait后,用jstack查看线程状态:WAITING (on object monitor)3.线程池execute和submit的区别?

execute() 没有返回值;而 submit() 有返回值。当业务异常时,execute会抛出异常,submit会把异常封装到返回值FutureTask中。

源码简单分析submit:

当执行execute时,如果线程池有线程执行就会执行RunnableFuture的run方法,也就是FutureTask的run方法。

设置异常就是把异常设置到outcome变量中

设置结果也一样。

4.如何避免Java线程中的死锁?

死锁检测: 用kill -3 , jstack ,VisualVM都可以 获取到线程堆栈,然后用easygc工具去分析。
内置锁(synchronized)死锁线程堆栈案例:

上图得知:

线程等待拿锁的状态为:BLOCKED线程another-thread,获取到了0x22bc3f98对象的锁,但是在等待对象0x22bc3f90的锁。main-thread,获取到了0x22bc3f90对象的锁,但是在等待对象0x22bc3f98的锁。another-thread线程与main-thread线程已经发生死锁。

线程池死锁实例:

final ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.submit(new Callable() { public Long call() throws Exception { System.out.println("start f1"); FutrueTask f2 = (FutrueTask)executorService.submit(new Callable() { public Long call() throws Exception { System.out.println("start f2"); return -1L; } }); System.out.println("result" + f2.get()); System.out.println("end f1"); return -1L; } });

我们在看线程池死锁堆栈:

上图得知:

线程处于WATING状态,当调用 LockSupport.park 方法,日志中显示为 java.lang.Thread.State: WAITING (parking)。线程在等待一个FutrueTask的锁对象。一直等不到被唤醒,所以就卡在这里了。
我们跟踪FutrueTask的get方法源码:

所以我们定位到是代码的 System.out.println(“result” + f2.get()); 位置卡死了。
防止死锁:
(1)以确定的顺序获得锁,也就是不要写出锁A->锁B, 然后锁B->锁A , 然后线程交替执行。
(2)超时放弃,使用Lock的tryLock指定超时时间。
(3)使用ThreadMXBean来检测死锁,发现有死锁线程就进行interrupt,如下代码:(注:ThreadMXBean耗性能,谨慎使用)public static void main(String[] args) { ThreadMXBean mbean = ManagementFactory.getThreadMXBean(); // 只能检测 synachrozied 同步代码块 的死锁 // long[] deadlockedThreadIds = mbean.findMonitorDeadlockedThreads(); // 可以检测 juc下的 Lock造成的死锁和 synachrozied代码块的死锁 long[] deadlockedThreadIds = mbean.findDeadlockedThreads(); if (deadlockedThreadIds != null) { ThreadInfo[] threadInfos = mbean.getThreadInfo(deadlockedThreadIds); for (Thread t : Thread.getAllStackTraces().keySet()) { for (ThreadInfo ti : threadInfos) { if (ti.getThreadId() == t.getId()) { t.interrupt(); break; } } } } } } 5.偏向锁、轻量级锁和重量级锁 ?

传送门:偏向锁、轻量级锁和重量级锁 最易懂解释

6.在Java的反射中,Class.forName和ClassLoader的区别?

Class.forName加载类是将类进了初始化,会执行类的静态方法,并赋值。
而ClassLoader的loadClass并没有对类进行初始化,只是把类加载到了虚拟机中。

7.Jvm为什么不直接解释执行java源代码而是编译好的class? 字节码更便于虚拟机读取,不用在解析字符串,所以运行速度比直接解析源代码快。语法是会变的,而源代码中没有版本信息,而字节码中不但有版本信息,还可以经由编译过程抹平一些语言层面的变化(即语言语法虽然有变化,但字节码依然遵照原来的规则即可)。字节码也可以由其他语言生成,如Groovy,Clojure,Scala。需要注意的事,既然这些语言可以编译成字节码,也就可以被Java或其他JVM语言调用。8.Java代码的执行流程?

(1) 编译java源文件生成class文件(Java 源码编译机制)

最后生成的 class 文件由以下部分组成:

结构信息:包括 class 文件格式版本号及各部分的数量与大小的信息。元数据: 对应于 Java 源码中声明与常量的信息。包含类/继承的超类/实现的接口 的声明信息、域与方法声明信息和常量池。方法信息:对应 Java 源码中语句和表达式对应的信息。包含字节码、异常处理器表、求值栈与局部变量区大小、求值栈的类型记录、调试符号信息。

(2) jvm类加载器加载class到内存(类加载机制)

类的加载: 将class文件中二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。在这个阶段,会执行类中声明的静态代码块。

类加载的过程:

装载:将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象。

验证:分为 JVM规范校验 和 代码逻辑校验。
JVM规范校验: 对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。
代码逻辑校验:从加载阶段的class对象里面找到类的一些方法,变量元信息,来验证是否调用正确,比如:一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。
总结:验证阶段就是 虚拟机对加载的代码数据进行校验,看看这份代码是不是真的按照JVM规范去写的。

准备:为 类变量(static修饰) 分配内存并初始化。
注意: 这个附初始值 只是jvm默认的值,并不是被赋予用户希望的值。如:

public static int sector = 3;

这个准备阶段只是 将sector赋值为0, 并不是3, 但如果是被final修饰: public static final int number = 3; 那么准备阶段就会直接赋值为3。
被 final 修饰的类变量在准备阶段就会被赋予想要的值。而没有被 final 修饰的类变量,其可能在初始化阶段或者运行阶段发生变化,所以就没有必要在准备阶段对它赋予用户想要的值。

解析:将其在常量池中的符号引用替换成直接其在内存中的直接引用。(下面会详解符号引用和直接引用)

初始化:用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化

符号引用

有一段java源代码:

public class X { public void foo() { bar(); } public void bar() { } }

比如javap查看A.class有一段常量池的信息。

还有一个方法的信息

来解析下 foo()方法里的一条字节码指令:

invokevirtual #2 // Method bar:()V

这在Class文件中的实际编码为:

[B6] [00 02]

其中0xB6是invokevirtual指令的操作码(opcode),后面的0x0002是该指令的操作数(operand),用于指定要调用的目标方法。这个参数 #2 是Class文件里的常量池的下标。那么去找下标为2的常量池项,是(第一张图里面):

#2 = Methodref #3.#17 // X.bar:()V

这在Class文件中的实际编码为(以十六进制表示,Class文件里使用高位在前字节序(big-endian)):

[0A] [00 03] [00 11]

00 11 是引用的另外的常量池下标。
其中0x0A是 常量表的类型,下面截取了部分常量表的类型:


从图中看到 10就是0xA ,就是 方法的符号引用。

顺着上面的找法,最终会得到:


就变成字符串: X.bar:()V 也就是 X类 的 bar 方法签名。所以

invokevirtual #2 // Method bar:()V

实际就是 执行 X类的bar方法。
符号引用就是#2等字符串。

直接引用
将上面的符号引用转换成jvm的内存地址,不需要每次都通过#2来到常量池去一个一个寻根。

言归正传,回到我们java代码执行的第三步
(3) 类执行机制(解释执行,编译执行)
JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。由于解释器在解释执行的过程中,每次只能看到一行代码,所以很难生成高效指令序列;而编译器可以事先看到所有代码,因此,一般来说,解释性代码比编译性代码要慢。为了提高Java程序的执行速度,引入了JIT技术。
在主流JVM中,Java程序一开始是通过解释器进行解释执行的。当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为热点代码(Hot Spot Code),然后JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT)。

9.foreach原理

我们利用JAD反编译下面这段代码的class(jd-gui不行,不够原生)

List list = new ArrayList(); list.add("aaa"); for (String s : list) { System.err.println(s); }

反编译后

ArrayList arraylist = new ArrayList(); arraylist.add("aaa"); String s; for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.err.println(s)) s = (String)iterator.next();

其实,foreach是Java给我们提供的一个语法糖,其实就是依赖了for循环和Iterator实现的。

10.hashcode与equals

equals已经能实现对比的功能了,为什么还要hashcode?
因为重新equals里面的逻辑一般比较复杂,效率就低,而利用hashcode只是比较一个值就行了,效率很高。

那问题来了,hashcode效率这么高还要equals干嘛?
因为hascode不完全可靠,有时不同对象会生成相同的hashcode(生成hashcode的算法可能存在冲突,总结两点:

equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。11.wait/notify原理

每个对象在出生的时候就有一把锁(监视器Monitor),下面是Monitor的大致结构:

The Owner:当前拿到Monitor的线程。Entry Set(锁池):The Owner不为空,有线程已经拿到了Monitor了,就进入Entry Set集合等待获取Monitor。Wait Set(等待池):表示线程通过对象的wait方法,释放对象的锁,并在等待区等待被唤醒。

由于Wait Set在对象的Monitor中,所以调用wait方法必须要先得到Monitor
Synchronized代码块通过javap生成的字节码中包含monitorenter和monitorexit两个指令。当在进加锁的时候会执行monitorenter指令,执行该指令可以获取对象的Monitor,所以wait方法必须在Synchronized里面。

wait方法步骤:

当线程A调用obj.wait()方法时,会先构造一个ObjectWaiter对象(里面有线程A的引用),将其放入到obj对象头的Monitor的Wait Set集合中。释放掉obj对象头的Monitor,这样其它线程就能获取到Monitor进行执行。调用底层的park方法,将线程A挂起。

notify方法步骤:

判断当前Wait Set是否为空,为空直接返回。notify会唤醒第一个Wait Set的ObjectWaiter节点,将节点放入Entry Set(锁池)中进行拿Monitor竞争。(notifyAll会唤醒所有)notify并不会释放对象的Monitor,只有monitorexit才会释放。12.字符串常量池、class常量池和运行时常量池

(1) 全局字符串池(string pool也有叫做string literal pool)

在类加载完成,经过验证,准备阶段之后在堆中生成字符串对象实例,然后将引用堆字符串实例的指针放到string pool中,string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串的引用,在HotSpot VM的实例只有一份,被所有的类共享。

(2) class文件常量池(class constant pool)

class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池(constant pool table),用于存放编译器生成的各种字面量符号引用

字面量相当于Java语言层面常量的概念,如文本字符串、被声明为final的常量值等。符号引用则属于编译原理方面的概念,一般描述:类和接口的全限定名,字段的名称和描述符和方法的名称和描述符。如下图:

(3) 运行时常量池(runtime constant pool)

虚拟机会将各个class文件中的常量池载入到运行时常量池中,即编译期间生成的字面量、符号引用,总之就是装载class文件。为什么它叫运行时 常量池呢?因为这个常量池在运行时,里面的常量是可以增加的。

方法区结构

方法区包括:class文件信息和运行时常量池,class文件信息包括class文件常量池。

class文件信息结构

class文件信息包括:魔数,版本号,常量池,类,父类和接口数组,字段,方法等信息,其实类里面又包括字段和方法的信息。

class文件常量池和运行时常量池的关系以及区别
class文件常量池是存在于编译好的字节码文件的,是静态的。 运行时常量池是在运行时加载类后将字节码的静态内容加载到内存,并在需要的时候解析。

运行时常量池是当class文件被加载完成后,java虚拟机会将class文件常量池里的内容转移到运行时常量池里,在class文件常量池的符号引用有一部分是会被转变为直接引用的(这个过程有的需要查询StringTable),比如说类的静态方法或私有方法,实例构造方法,父类方法,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的。

13.垃圾回收过程


当对象new出来,先放到Eden区(伊甸园区)。第一次垃圾回收时,将存活的对象复制到from区,Eden区清空。当from区也满了,将存活的对象复制到to区,并清空from区,对象年龄加1,并且from和to交换角色,也就是每次垃圾回收时,to区必须是空的,当对象的年龄达到一个阈值(默认值为15)还存活,就移动到老年代。

14.CAS与ABA问题

CAS(乐观锁)
利用native方法compareAndSwapInt(),在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

ABA问题
CAS是检查内存值没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。

解决方法: 在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A。

15.tomcat使用类加载器的原因 隔离性:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以实现相互隔离。设想一下,两个Web应用,一个使用了Spring2.5,另一个使用了教新的4.0,应用服务器使用一个类加载器,Web应用将会因为jar包覆盖而无法启动。灵活性:Web应用之间的类加载器相互独立,那么就能针对一个Web应用进行重新部署,此时Web应用的类加载器会被重建,而且不会影响其他的Web应用。性能:部署在同一个Web容器上的两个Web应用程序所使用的Java类库可以互相共享。这个需求也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费。16.Redis故障排查

设置好redis日志
在redis.conf中设置logfile /usr/local/logs/redis.log, 级别设置为notice。

慢查询日志
redis的慢查询日志用于记录执行时间过长的命令。有以下两个配置:

slowlog-log-slower-than :指定超过多少微秒的命令会被记录到慢日志。slowlog-max-len : 服务器最多保存多少条慢日志。

查询慢日志

redis> SLOWLOG GET 1) 1) (integer) 4 # 日志的唯一标识符(uid) 2) (integer) 1378781447 # 命令执行时的 UNIX 时间戳 3) (integer) 13 # 命令执行的时长,以微秒计算 4) 1) "SET" # 命令以及命令参数 2) "database" 3) "Redis" 2) 1) (integer) 3 2) (integer) 1378781439 3) (integer) 10 4) 1) "SET" 2) "number" 3) "10086"

ping命令检测存活
通过命令 PING 的响应是否为 PONG 来判断.

连接数检测
redis> redis-cli info Clients|grep connected_clients
查看连接的客户端数量。

rejected_connections
查看拒绝连接数,这个值一般是0,如果大于0说明创建的连接数大于 maxclients,需要排查原因,是 Redis 连接池配合不合理还是连接这个Redis 的服务过多。

阻塞客户端数量
blocked_clients
通常是执行了 list 数据类型的 BLPOP 或者 BRPOP 命令引起的,可以通过[redis-cli info Clients|grep blocked_clients]得到,这个值最应该=0。

持久化
rdb_last_bgsave_status/aof_last_bgrewrite_status,即最近一次或者最后一次 RDB/AOF 持久化是否有问题,这两个值都应该时 “OK”。

由于 Redis 持久化时会 fork 子进程,且 fork 是一个完全阻塞的过程,所以可以监控 fork 耗时即:latest_fork_usec,单位是微妙,如果这个值比较大会影响业务,甚至出现 timeout 。

client list命令

127.0.0.1:6379> client list id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get id:客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。addr:客户端连接的ip和端口。fd:socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1代表当前客户端不是外部客户端,而是Redis内部的伪装客户端。name:客户端的名字。age和idle: 分别代表当前客户端已经连接的时间和最近一次的空闲时间(单位s)。

redis未设置idle超时时间导致连接过多
有如下日志

redis.clients.jedis.exceptions.JedisConnectionException:Cloud not get a resource from the pool 错误是无法为客户端分配资源,查看redis连接数: # redis-cli info | grep connected connected_clients:5036 connected_slaves:0 客户端的连接竟然有达到5000多 查看客户端连接情况: redis 127.0.0.1:6379> client list addr=10.247.64.115:52834 fd=5 idle=2144 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=rpush addr=127.0.0.1:38484 fd=7 idle=0 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=blpop addr=10.105.1.2:53125 fd=8 idle=2144 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=rpush addr=10.105.1.3:32639 fd=9 idle=2144 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=rpush addr=10.105.1.4:57134 fd=10 idle=2144 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=rpush addr=10.105.1.5:53126 fd=11 idle=2144 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=rpush addr=10.105.1.7:8887 fd=12 idle=2144 flags=N db=0 sub=0 psub=0 qbuf=0 obl=0 oll=0 events=r cmd=rpush 发现客户端的idle空闲时长太长,连接池维持了太多的连接,需要将不用的连接及时释放掉: redis 127.0.0.1:6379> CONFIG SET timeout 30 OK 17.秒杀系统怎么设计

服务单一职责
首先确保秒杀挂了不会影响到别的服务,所以应该把秒杀业务单独一个服务部署,数据库也单独建一个秒杀库。

秒杀链接加盐
如果秒杀地址是静态的话,会导致有人刷接口,且在秒杀开始后0点几毫秒请求几百次,这样商品全被他秒杀到了。所以我们要把URL动态化:
每次点击秒杀按钮,先从服务器获取动态拼接而成的秒杀地址。用户请求秒杀商品的时候,要带上秒杀地址进行校验。

Redis配置lua脚本扣库存
在数据库中扣库存需要用到锁机制,而且并发量极差。因为redis可以执行lua脚本且每步操作都是原子的,所以非常完美。

nginx拦截恶意请求
传送门:nginx各种配置功能

页面静态化,按钮置灰
秒杀一般都是特定的商品还有页面模板,现在一般都是前后端分离的,所以页面一般都是不会经过后端的,但是前端也要自己的服务器啊,那就把能提前放入cdn服务器的东西都放进去,反正把所有能提升效率的步骤都做一下,减少真正秒杀时候服务器的压力。

秒杀网关接口做好限流熔断

最后附一张秒杀架构图

17.独占锁与共享锁

独占锁就是只有一个线程可以获取锁,共享锁就是同时可以有多个线程获取锁。

CountDownLatch与Semaphore为共享锁 , 因为它们都可以让多个线程拿到锁。ReentrantLock与CyclicBarrier为独占锁。

它们的用法请参加: CountDownLatch、CyclicBarrier和Semaphore用法

源码解析请见: CountDownLatch 源码详解

独占锁与共享锁的区别:

独占功能:当锁被头节点获取后,只有头节点获取锁,其余节点的线程继续沉睡,等待锁被释放后,才会唤醒下一个节点的线程。共享功能:只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。18.给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url? 遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,…,a999,每个小文件约300M);遍历文件b,采取和a相同的方式将url分别存储到1000个小文件(记为b0,b1,…,b999);这样处理后,所有可能相同的url都被保存在对应的小文件(a0vsb0,a1vsb1,…,a999vsb999)中,不对应的小文件不可能有相同的url。然后我们只要求出这个1000对小文件中相同的url即可。比较小块文件,放入HashMap去比较。19.RocketMQ事务消息?

假如有两个分布式事务:支付事务 ==> 订单付款事务
要必须保证支付成功后,订单才能付款。我们可以使用RocketMQ的分布式事务实现:

构造一个事务生产者TransactionM producer = new TransactionM("please_rename_unique_group_name"); 设置事务监听器producer.setTransactionListener(transactionListener);

TransactionListener 有两个回调函数:

executeLocalTransaction : 执行本地事务,也就是我们的支付逻辑,本地事务执行成功返回commit,是否返回rollback(此时消息就不会被消费者消费到)。如果由于网络原因等导致m没有收到此步骤的确认,就会执行下面的逻辑进行回查。checkLocalTransaction: 检查本地事务到底是不是执行成功,当执行本地事务后由于网络等其它未知原因没有对半消息确认,mq就会每隔1s调用这个checkLocalTransaction来看本地事务到底是否执行成功,成功就投递半消息。发送事务消息:producer.sendMessageInTransaction(msg, null);

原理:

生产者先向m 投递一个半消息,此时消费者不会被消费到。投递完成后,执行事务监听器的executeLocalTransaction 逻辑来执行本地事务,并返回commit还是rollback。如果由于网络等其它原因导致,m收不到本地事务的确认,就会每隔1分钟对生产者进行本地事务消息回查(也就是执行checkLocalTransaction),回查可以返回commit还是rollback。20.Spring 循环依赖的三种方式? 构造器参数循环依赖: spring会将正在创建的bean放在一个"当前创建Bean池"中,如果在创建Bean过程中发现自己已经在“当前创建Bean池”里时将抛出BeanCurrentlyInCreationException异常表示循环依赖。setter方式单例,默认方式: spring 会先实例化bean并放入缓存中,后才执行set 属性,当设置的属性间接引用到自己,就直接从缓存中取,所以不会有循环依赖问题。setter方式原型,prototype : 这种每次调用getBean都会重新new一个实例,所以无法解决循环依赖问题。21.ThreadLocal原理?

Thread类中有一个ThreadLocalMap(定制化的HashMap)成员变量threadLocals

ThreadLocal的set方法:

public void set(T value) { //(1)获取当前线程 Thread t = Thread.currentThread(); //(2)当前线程作为key,去查找对应的线程变量,找到则设置 ThreadLocalMap map = t.threadLocals; if (map != null) map.set(this, value); else //(3)第一次调用则创建当前线程对应的HashMap createMap(t, value); }

初始化的时候Thread中的threadLocals是为null的,所以就创建ThreadLocalMap然后设置到threadLocals中。

ThreadLocalMap的key 就是ThreadLocal的this引用,value就是要设置的值。

子线程中如何获取父线程的threadLocal中的值?

public class BaseTest { public static final InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal(); public static final ThreadLocal threadLocal = new ThreadLocal(); public static void main(String[] args) { inheritableThreadLocal.set("Inheritable hello"); threadLocal.set("hello"); new Thread(()->{ // get 为Inheritable hello System.out.println(String.format("子线程可继承值:%s",inheritableThreadLocal.get())); // get为null System.out.println(String.format("子线程值:%s",threadLocal.get())); new Thread(()->{ // get 为Inheritable hello System.out.println(String.format("孙子线程可继承值:%s",inheritableThreadLocal.get())); // get为null System.out.println(String.format("孙子线程值:%s",threadLocal.get())); }).start(); }).start(); }

在线程中除了ThreadLocal外还有InheritableThreadLocal,可以让子线程获取到父线程中ThreadLocal的值。
原理:

可以看到,创建线程时,调用了init方法,然后将父线程的inheritableThreadLocals赋值给当前线程。

22.ConcurrentHashMap 原理?

Hashtable全表锁,ConcurrentHashMap 分段锁。

ConcurrentHashMap 原理图:

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry(结构跟HashMap类似)组成,每个Segment都是继承与ReentrantLock,持有锁的特性。

put操作

当执行put操作时,会进行第一次key的hash来定位Segment的位置。然后进行第二次hash操作,找到相应的HashEntry的位置,然后通过继承ReentrantLock的tryLock方法尝试去获取锁。如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock方法,去获取锁,超过指定次数就挂起,等待唤醒。

get操作
第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

size操作
第一种方案他会使用不加锁的模式去尝试多次计算ConcurrentHashMap的size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的;
第二种方案是如果第一种方案不符合,他就会给每个Segment加上锁,然后计算ConcurrentHashMap的size返回。

23.线程池线程大小合理设置?

任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。

CPU密集型任务 : 此类型的任务占用较高cpu,若开大于cpu核心数的线程的话会导致过多上下文切换,带来额外开销,一般设置为cpu核心数+1IO密集型任务一般设置为2*CPU核心数混合型任务 :可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。24.动态代理的几种方式?

1. JDK动态代理

public interface IHello { void sayHello(); } public class HelloImpl implements IHello { @Override public void sayHello() { System.out.println("Hello world!"); } } public class MyInvocationHandler implements InvocationHandler { /** 目标对象 */ private Object target; public MyInvocationHandler(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------插入前置通知代码-------------"); // 执行相应的目标方法 Object rs = method.invoke(target,args); System.out.println("------插入后置处理代码-------------"); return rs; } } /测试 IHello iHello2 = (IHello) Proxy.newProxyInstance(IHello.class.getClassLoader(), // 加载接口的类加载器 new Class[]{IHello.class}, // 一组接口 new MyInvocationHandler(new HelloImpl())); // 自定义的InvocationHandler iHello2.sayHello();

Proxy.newProxyInstance 主要做了三部:

生成代理类: Class cl = getProxyClass0(loader, intfs);获取构造器: Constructor c = cl.getConstructor();生成代理对象: c.newInstance(new Object[]{h});

关键是代理class的怎么生成的,也就是getProxyClass0实现步骤?
首先从缓存里面找;

private static Class getProxyClass0(ClassLoader loader, Class... interfaces) { // 接口数不得超过65535个,这么大,足够使用的了 if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); } // 如果缓存中有代理类了直接返回,否则将由代理类工厂ProxyClassFactory创建代理类 return proxyClassCache.get(loader, interfaces); }

缓存没有,则生成主要有下面两块:

byte[] proxyClassFile = ProxyGenerator.generateProxyClass( proxyName, interfaces, accessFlags); try { return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length); } catch (ClassFormatError e) { throw new IllegalArgumentException(e.toString()); }

先利用ProxyGenerator 工具(ProxyGenerator这个类存放在sun.misc包下,就不详解了)根据接口里面的方法生成具体代理class文件加载到byte[] ,然后利用classloader 加载, ProxyGenerator生成的class格式大概是下面:

public final class $Proxy0 extends Proxy implements IHello // 继承了Proxy类和实现IHello接口 { // 代理类的构造函数,其参数正是是InvocationHandler实例,Proxy.newInstance方法就是通过通过这个构造函数来创建代理实例的 public $Proxy0(InvocationHandler paramInvocationHandler) throws { super(paramInvocationHandler); } // 接口代理方法 public final void sayHello() throws { //当前方法class Method m3 = getClass().getMethod("sayHello", new Class[0]); // 代理到 InvocationHandler 的invoke方法 paramInvocationHandler.invoke(this, m3, null); return; }

这样当程序执行 $Proxy0 类的sayHello方法时 就会调用 InvocationHandler 里面的invoke 了,实现了 代理的功能。

2. cglib动态代理实现

它的底层使用ASM在内存中动态的生成被代理类的子类,使用CGLIB即使代理类没有实现任何接口也可以实现动态代理功能。

25.cloneable接口实现原理,浅拷贝or深拷贝

浅克隆:当对象内部有对象时,克隆时只是把内部对象的引用拷贝,并没有重新构造新的对象。

使用浅克隆:
Cloneable是一个空接口, clone方法是在Object里面 (一个native方法), 必须要重写Object的clone并且要实现Cloneable接口,使用 super.clone() 就能使用浅克隆。如果不实现Cloneable, clone方法就会抛出CloneNotSupportedException异常。

深克隆 :对象内部有对象,克隆出来的内部对象也是重新构造的实例。

使用深克隆:

重写的clone方法,对每个内部对象都调用对象.clone方法。通过ByteArrayOutputStream的 readObject方法实现。json框架也能实现。26.签名与加密区别?

签名是为了确保信息没有被篡改过;加密就是为了数据不被泄漏。
就拿A给B发送经过签名加密信息来说:
1、A对信息签名的作用是确认这个信息是A发出的,不是别人发出的;
2、加密是对内容进行机密性保护,主要是保证信息内容不会被其他人获取,只有B可以获取。

27.方法区,永久代,元空间?

方法区
方法区是一种规范,不同的虚拟机厂商可以基于规范做出不同的实现,永久代和元空间就是出于不同jdk版本的实现。

永生代
java8之前,hotspot使用在内存中划分出一块区域来存储类的元信息、类变量以及内部字符串(interned string)等内容,称之为永生代。

元空间
Java8, HotSpots取消了永生代,取而代之的是元空间。

永生代和元空间不同

存储位置不同,永久代物理是是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存。存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。

java8为什么废弃永久代?

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen。

为什么要在直接内存里拿出来一块内存作为元空间取代永久代呢?

类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。28.什么情况下会发生栈内存溢出? 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常,方法递归调用产生这种结果。新建立线程的时候没有足够的内存去创建对应的虚拟机栈,那么Java虚拟机将抛出一个OutOfMemory 异常。(线程启动过多),参数 -Xss 去调整JVM栈的大小。29.为什么要分为Eden和Survivor?为什么要设置两个Survivor区?

为什么要分为Eden和Survivor?

如果没有Survivor, Eden区每进行一次gc,都会把存活对象放入老年代,这样老年代很快被填满,FullGC就会很频繁。

为什么要设置两个Survivor区?

设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空。

30.为什么指令重排可以提高执行效率?

大多数现代微处理器都会采用将指令乱序执行,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。

除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。

31.jvm中如何打破双亲委托机制?

在重写loadclass的过程中,只要不遵从JVM的规范就行了,不盲目的优先向Parent 的ClassLoader进行查找就行了。
Tomcat中的web 容器类加载器是破坏了双亲委托模式的,自定义的WebApplicationClassLoader除了核心类库外,都是优先加载自己路径下的Class;

32.强引用、软引用、弱引用、虚引用的区别?

1)强引用
我们平时new了一个对象就是强引用,例如 Object obj = new Object();即使在内存不足的情况下,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。

2)软引用
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。

3)弱引用
具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

ThreadLocal中,获取到线程私有对象是通过线程持有的一个threadLocalMap,然后传入ThreadLocal当做key获取到对象的,这时候就有个问题,如果你在使用完ThreadLocal之后,将其置为null,这时候这个对象并不能被回收,因为他还有 ThreadLocalMap->entry->key的引用,直到该线程被销毁,但是这个线程很可能会被放到线程池中不会被销毁,这就产生了内存泄露,jdk是通过弱引用来解决的这个问题的,entry中对key的引用是弱引用,当你取消了ThreadLocal的强引用之后,他就只剩下一个弱引用了,所以也会被回收。

4)虚引用
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
系统gc监听 因为虚引用每次GC都会被回收,那么我们就可以通过虚引用来判断gc的频率,如果频率过大,内存使用可能存在问题,才导致了系统gc频繁调用。

33.并发访问时,在数据库如何安全地修改同一行数据?

悲观锁
select…for update : 除了有查询的作用外,还会加锁,没用索引/主键的话就是表锁,否则就是是行锁。

使用乐观锁
版本号,重试机制实现

34.索引不适合哪些场景? 数据量少的不适合加索引更新比较频繁的也不适合加索引区分度低的字段不适合加索引(如性别)35.索引下推?

假如有(name,age)联合索引,有这样一个查询:

SELECT * from user where name like '陈%' and age=20

5.6之前的版本是没有索引下推这个优化的,因此执行的过程:
根据(username,age)联合索引查询所有满足名称以“陈”开头的索引,然后回表查询出相应的全行数据,然后再筛选出满足年龄等于20的用户数据。

索引下推
根据(username,age)联合索引查询所有满足名称以“陈”开头的索引,然后直接再筛选出年龄等于20的索引,之后再回表查询全行数据。

36.事务的隔离级别有哪些?MySQL的默认隔离级别是什么? 读未提交(Read Uncommitted):可能读取到其他会话中未提交事务修改的数据。读已提交(Read Committed):只能读取到已经提交的数据。可重复读(Repeatable Read):事务B只能在事务A修改过数据并提交后,自己也提交事务后,才能读取到事务B修改的数据。串行化(Serializable):完全串行化的读。

Mysql默认的事务隔离级别是可重复读(Repeatable Read)

Mysql四大特性:原子性,一致性,隔离性,持久性。

37.zookeeper集群的强一致性?

zookeeper集群在对外提供服务和写入数据时,必须半数以上主机可用并且数据写入成功,才返回成功。虽然有短暂的数据不一致窗口,但是因为有超过半数成功的限制条件,即使leader挂掉,选举出来的新leader也肯定会有所有完整的数据(有最新数据日志的节点才有可能被选为leader),所以最终数据会在集群中的所有机器上保持一致(单调弱一致性:两次读取操作,后发起的不会读取到比前一次读取到的数据更老的数据)。因此zookeeper集群是CP系统。

38.Lock.lockInterruptibly?

lockInterruptibly: 线程A获取锁,如果获取不到则等待,在等待中,如果其它线程调用了线程A的interrupt方法,则lockInterruptibly会抛出InterruptedException异常,避免死锁。

39.synchronized与Lock的区别?

锁释放
synchronized必须在执行完同步代码块才释放锁, 同步代码抛出异常jvm也会自动释放锁。Lock必须调用unlock才解锁。

锁状态获取
synchronized无法判断是否能获取锁。Lock可以用tryLock。

锁类型
synchronized是可重入,非公平,不可中断。
Lock可重入,公平与非公平,可中断与不可中断锁。

性能
synchronized用在代码量少的地方性能高,Lock适合大量同步的地方。

40.可重入锁使用场景

避免死锁,一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,如果锁不可重入的话就会导致死锁。

41.LockSupport?

LockSupport.park() : 阻塞当前线程, 当线程被interrupt时,会唤醒,当调用LockSupport.unpark(线程) 时,也会被唤醒。

LockSupport.park() 与LockSupport.park(obj)的区别:
调用park()方法dump线程堆栈

"main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) at learn.LockSupportDemo.main(LockSupportDemo.java:7)

调用park(obj)方法dump线程堆栈

"main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for (a java.lang.String) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at learn.LockSupportDemo.main(LockSupportDemo.java:7)

带Object的park方法相较于无参的park方法会增加 parking to wait for (a java.lang.String)的信息。有助于迅速发现等待锁的问题。

41.负载均衡算法? 轮询法 : 将请求按顺序轮流地分配到后端服务器上。随机法 : 通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。源地址哈希法: 根据客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。加权轮询法 : 不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。加权随机法 : 与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。最小连接数法 : 根据后端服务器当前的连接情况,动态地选取其中当前 积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
收藏
  • 人气文章
  • 最新文章
  • 下载排行榜
  • 热门排行榜