面试题整理

2021/5/22 18:30:29

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

2021-05-18(面试吉利)

问1:什么是内存可见性,你是怎么理解的?
问2:你是怎么理解线程安全的?synchronized可以解决线程安全?怎么解决的?
问3:动态代理和静态代理怎么理解?有什么区别?
什么是内存可见性,你是怎么理解的?
volatile保证可见性的原理是在每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以
volatile关键字的作用之一就是保证变量修改的实时可见性。
你是怎么理解线程安全的?
《Java并发编程实践》

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或
者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
当多个线程访问某个方法时,不管你通过怎样的调用方式或者说这些线程如何交替的执行,我们在主程序中不需要去做
任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类时线程安全的。

如果一段代码可以保证多个线程访问的时候正确操作共享数据,那么它是线程安全的。
如何解决线程安全?
第一种 : 互斥同步
在多线程编程中,同步就是一个线程进入监视器(可以认为是一个只允许一个线程进入的盒子),其他线程必须等待,
直到那个线程退出监视器为止。

在实现互斥同步的方式中,最常使用的就是Synchronized 关键字。

synchronized实现同步的基础就是:Java中的每一个对象都可以作为锁。

具体表现为:1.普通同步方法,锁是当前实例对象
           2.静态同步方法,锁是当前类的Class对象
           3.同步方法块,锁是Synchronized括号里匹配的对象
如何实现?

synchronized经过编译之后,会在同步块的前后生成 monitorenter 和 monitorexit这两个字节码指令。这两个字
节码指令之后有一个reference类型(存在于java虚拟机栈的局部变量表中,可以根据reference数据,来操作堆上的
具体对象)的参数来指明要锁定和解锁的对象。根据虚拟机规范,在执行monitorenter 指令时,首先会尝试获取对象
的锁,如果该对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加一。若获取对象失败,那当前
线程就要阻塞等待,知道对象锁被另一个线程释放。

synchronized用的锁是存放在对象头里面的,在jdk1.6之后,锁一共有四种状态:无锁状态,偏向锁状态(在对象头
和栈帧中的锁记录里存储偏向锁的线程id),轻量级锁状态(将对象头的mark word复制到当前线程的栈帧的锁记录
中,使用CAS操作将对象头中的markWord指向栈帧中的锁记录,如果成功,则线程就拥有了对象的锁。如果两个以上的
线程争用一把锁的话,则膨胀为重量级锁),重量级锁状态。
因为之前我一直都很迷惑,所以我接下来讲一讲这一方面 :

大家应该都知道,java 在虚拟机中除了线程计数器,java虚拟机栈 是线程私有的,其余的java堆,方法区,和运行
时常量池都是线程共享的内存区域。java堆是存储对象和数组的,但是对象在内存中的存储布局可以分为三块区域:对
象头,实例数据(对象真正存储的有效信息,程序代码中所定义的各个类型的字段内容),对齐填充。

为什么说synchronized的锁是存放在对象头里面呢?因为对象头里面也存储了两部分信息:第一部分呢,存储对象自
身的运行时数据,包括哈希码,GC分代年龄,锁状态标识位,线程持有的锁,偏向锁Id,偏向时间戳等数据。第二部分
是类型指针,虚拟机通过这个来确定该对象是哪个类的实例。

如何判断该对象有没有被锁?对象头里面锁状态的标志位会发生变化,当其他线程查看synchronized 锁定的对象时,
会查看该对象的对象头的标志位有没有发生变化,若标志位为01,则表示未锁定,为00时,则表示轻量级锁定,为10
时,则为重量级锁定状态。为01时,则为偏向锁,为11时,则为GC标记状态。
第二种方法就是:非阻塞同步
因为使用synchronized的时候,只能有一个线程可以获取对象的锁,其他线程就会进入阻塞状态,阻塞状态就会引起
线程的挂起和唤醒,会带来很大的性能问题,所以就出现了非阻塞同步的实现方法。
先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,如果共享数据有争用,就采取补偿措施(不断地重
试)。
  
我们想想哈,互斥同步里实现了 操作的原子性(这个操作没有被中断) 和 可见性(对数据进行更改后,会立马写入到
内存中,其他线程在使用到这个数据时,会获取到最新的数据),那怎么才能不用同步来实现原子性和可见性呢? 
  
CAS是实现非阻塞同步的计算机指令,它有三个操作数:内存位置,旧的预期值,新值,在执行CAS操作时,当且仅当内
存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。  
     使用方法:使用JUC包下的整数原子类decompareAndSet()和getAndIncrement()方法
  
 缺点 :ABA 问题  版本号来解决
  
只能保证一个变量的原子操作,解决办法:使用AtomicReference类来保证对象之间的原子性。可以把多个变量放在一
个对象里。
第三种:无同步方案
线程本地存储:将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。

经常使用的就是ThreadLocal类

ThreadLocal类 最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。
  public T get() { }  
  public void set(T value) { }  
  public void remove() { }  
  protected T initialValue() { }  
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()
用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一
个延迟加载方法
其实引起线程不安全最根本的原因 就是 :线程对于共享数据的更改会引起程序结果错误。
线程安全的解决策略就是:
	保护共享数据在多线程的情况下,保持正确的取值。
动态代理和静态代理怎么理解?有什么区别?
AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系
统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。

AOP实现主要分为 静态代理 和 动态代理 。
	静态代理 主要是 AspectJ;
	动态代理 主要是 Spring AOP;
静态代理
AspectJ 的底层技术是 静态代理 ,即用一种 AspectJ 支持的特定语言编写切面,通过一个命令来编译,生成一个新
的 代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。

它会在编译阶段将Aspect织入Java字节码中, 运行的时候就是经过增强之后的AOP对象。
动态代理
Spring AOP使用的动态代理,动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对
象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式:
	JDK动态代理;
	CGLIB动态代理;
JDK 动态代理
	JDK动态代理通过反射来接收被代理的类,
	并且要求被代理的类必须实现一个接口。
核心类:
	InvocationHandler 接口
	Proxy.newProxyInstance()
CGLIB 动态代理
	如果目标类没有实现接口,那么Spring AOP会选择使用 CGLIB 来动态代理目标类。

CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类。
CGLIB是通过继承 的方式做 动态代理,因此如果某个类被标记为 final,那么它是无法使用CGLIB做动态代理的。
核心类:
	MethodInterceptor 接口;
	Enhancer 类;
AspectJ在编译时就增强了目标对象,
Spring AOP的动态代理则是在每次运行时动态的增强,生成AOP代理对象。

区别在于生成AOP代理对象的时机不同,
相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,
而Spring AOP则无需特定的编译器处理。


这篇关于面试题整理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程