Java互联网面试突击三总结

Java互联网面试突击

为什么在Java面试中一定会深入考察HashMap?

因为作为一名Java开发工程师,在些代码的时候必然会用到一些数据结构,其中尤为经典的就是HashMap,关于HashMap考察会很多

JDK1.8中对Hash算法和寻址算法是如何优化的?

map.put(“张三”,“测试数据”)
首先会对"张三"这个key计算他的hash值,是有一定的优化的,请看源码(jdk1.8)

static final int hash(Object key){
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h>>>16);
}

经过一系列二进制的与或,取模,进行数组寻址(取模运算,性能是比较差一些的,要优化这个数组的寻址过程)
hash & (n-1) -> 效果是跟hash对n取模是一样的,但是与运算的性能要比hash对n取模要高很多,注:这是一个数学问题:数组的长度一直是2的n次方,只要他保持数组长度是2的n次方,hahs对n取模的效果 ->hash & (n-1),效果是一样的,但是后者性能更高

比如说有一个hash值,将高16位和低16位进行与运算,这样运算出来的结果在低16位会融合了之前高16位和低16位的值,这样可以避免在hash值一样->导致他们都会放在数组的一个位置,进行复杂的hash冲突的处理

总结:
hash算法的优化:对每个hash值,在它的低16位中,让高低16位进行异或,让它的低16位同时保持了高低16位的特征,尽量避免了一些hash值后续出现冲突,导致会进入数组的同一个位置
寻址算法的优化:用与运算,替代取模,提升性能

HahMap是如何解决hash碰撞问题的?

hash冲突问题,链表+红黑树,O(n)和O(log n)
可能出现的问题:
多个key,它们算出来的hash的值,与n-1,与运算之后,发现定位出来的数组的位置还是一样的
解决方案:
会在这个位置挂一个链表,这个链表里面放入多个元素,让多个key-value对,同时放在数据的一个位置里
执行get方法的时候,如果定位到这个数组里这个位置挂了一个链表,此时就会遍历链表,从中找到那个key-value就行了
注:可能出现的问题,加入链表很长,可能导致遍历链表的性能会比较差,O(n)
针对这个问题做了优化:如果链表的长度达到了一定的长度之后,其实会把这个链表转换为红黑树,遍历一颗红黑树,时间复杂度
是O(log n),性能比链表高一些

HashMap是如何进行扩容的?

底层是一个数组,当这个数组满了之后,他就会自动进行扩容,变成一个更大的数组,让你在里面可以去放更多的元素
–>2倍扩容
数组长度=16,扩容之后要进行rehash策略
rehash策略:
判断二进制结果中是否多一个bit的1,如果没多,那么就是原来的index,如果多出来,那么就是index+oldCap(原来数组长度),通过这个方式,就避免了rehash的时候,用每个hash对新数组.length取模,取模性能不高,位运算的性能比较高

Synchronized关键字底层的原理是什么?

了解:加锁一般来说是对一个对象进行加锁的,进行加锁的时候,底层会执行moniterentry,释放锁的时候会执行monitorexit

执行monitorentry指令的时候会干什么呢?
每个对象都关联一个monitor,比如说一个对象实例就有一个monitor,一个类的Class对象也有一个monitor,如果要对这个对象加锁,那么必须获取这个对象关联的monitor的lock锁
原理:monitor里面有一个计数器,从0开始,如果一个线程要获取monitor锁,就看看他的计数器是不是0,如果是0的话,那么说明没人获取锁,他就可以获取锁了,然后对计数器加1,如果说线程走出了synchronized片段,就会有一个monitorexit指令,此时获取锁的线程就会对那个对象的monitor计数器减1,如果有多次重入加锁就会对应多次减1,直到最后,计数器是0
synchronized加锁的原理

对CAS的理解以及底层的实现原理?

前提:多个线程可能要访问同一个数据

HashMap map = new HashMap();

此时有多个线程要同时读写类似上面的这种内存的数据,必然会出现多线程的并发安全问题

synchronized(map){
	//对map里的数据进行复杂的读写处理
}

CAS(compare and set)
单纯的使用synchronized可能会导致效率低下,很多线程都需要去排队执行

public class MyObject{
	AtomicInteger i = new AtomicInteger(0);//底层基于CAS来实现的
	//多个线程此时来执行这个代码
	//不需要synchronized加锁,也是线程安全的
	public void increment(){
		i.incrementAndGet();
	}
}

CAS在底层的硬件级别给你保证一定是原子的,同一时间只有一个线程可以执行CAS,先比较再设置,其他线程的CAS同时间去执行,此时会失败
CAS底层执行原理

ConcurrentHashMap实现线程安全的底层原理是什么?

前提:多个线程要访问同一个数据:synchronized加锁,CAS去进行安全的累加。来实现多线程场景下的安全的更新一个数据的效果,比较多的场景下,可能就是多个线程同时读写一个HashMap

在JDK1.7以及以前的版本,是进行分段加锁
[数组1],[数组2],[数组3] ->每个数组都对应一个锁

JDK1.8之后,做了一些优化和改进,进行了锁粒度的细化
数组里的每个元素进行put操作,都是有一个不同的锁,刚开始进行put的时候,如果两个线程都是在数组[5]这个位置进行put,这个时候,对数组[5]这个位置进行put的时候,是进行的CAS的策略
通过对数组每个元素执行CAS的策略,如果是很多线程对数组里不同的元素执行put,大家是没有关系的,如果说之前数组[5]这个位置已经有人放进去值了,就需要在这个位置基于链表+红黑数来进行处理,synchronized(数组[5])加锁,基于链表或者是红黑树在这个位置插进去自己的数据

对JDK中AQS的理解,以及AQS的实现原理是什么?

前提:多个线程访问同一个共享数据,除了用synchronized,CAS,ConcurrentHashMap,Lock,还可以用AQS(abstract queue synchronizer 抽象队列同步器)

ReentrantLock lock = new ReentrantLock(true);  //默认是非公平锁,传值为true就是公平锁
lock.lock();//进行加锁
//处理复杂的业务逻辑
lock.unlock();//释放锁

AQS实现原理

线程池底层的工作原理?

系统是不可能的无限制的创建很多线程的,会构建一个线程池,有一定数量的线程,让他们执行各种各样的任务,线程执行完任务之后,不要销毁掉自己,继续去等待执行下一个任务

ExecutorService threadPool = Executors.newFixedThreadPool(3);	//3:corePoolSize:核心线程池的数量
threadPool.submit(new Callable(){
	public void run(){}
});

线程池是空的,当前没有线程,并且会带一个新的队列,一开始有提交任务时候,会比较当前线程池数量是否小于corePoolSize,小于的话会创建一个线程,反复执行上述流程,只要线程池的数量小于corePoolSize,都会直接创建新线程来执行这个任务,执行晚了就尝试从无界队列里获取任务,知道线程池有corePoolSize个线程

设置线程池

接着再提交任务,会发现线程数量已经跟corePoolSize一样大了,此时就直接把任务放入队列中就可以了,线程会争抢获取任务执行的,如果所有的线程此时都在执行任务,那么无界队列的任务就会越来越多

newFixedThreadPool队列而言,是LinkedBlockingQueue(无界阻塞队列),就是说队列的长度是无限大的

线程池的核心配置参数是干什么的?

new ThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
//corePoolSize:3
//maximumPoolSize:Integer.MAX_VALUE
//keepAliveTime:60s
//new ArrayBlockingQueue<Runnable>(200)//队列最多放的任务数

如果说任务的增长速度大于线程池的处理速度,会额外的生成线程出来,最多能创建maximumPoolsize个额外线程
假设任务被处理的差不多了,这些额外的线程会空闲一段时间,当空闲时间等于keepAliveTime时,额外的线程就会被销毁掉
如果说额外的线程还不够,并且任务还在不断的增长,会有拒绝策略,可以传入RejectedExecutionHandler
拒绝策略有以下几种:
1.AbortPolicy 2.DiscardPolicy 3.DiscardOldestPolicy 4.CallerRunsPolicy 5.自定义
常用的线程池就是newFixedThreadPool

如果在线程池中使用无界阻塞队列会发生什么问题?

面试题:在远程服务异常的情况下,使用无界阻塞队列,是否会导致内存异常飙升?
答:调用超时,队列(栈内存)会变得越来越大,此时会导致内存飙升起来,而且还可能会导致OOM,内存溢出

线上突然宕机,线程池中阻塞队列中的请求怎么办?

产生问题:必然会导致线程池里积压的任务实际上来说会是丢失的
解决方案:如果说要提交一个任务大线程池里去,在提交之前,麻烦现在数据库里插入这任务的信息,更新他的状态:未提交,已提交,已完成。提交成功之后,更新他的状态是已提交状态,系统重启之后,后台线程去扫描数据库里的未提交和已提交状态的任务。可以把任务的信息读取出来,重新提交到线程池里去,继续进行执行

谈一下对Java内存模型的理解?

public class HelloWord(){
	private int data = 0;
	public void increment(){
		data++;
	}
}
HelloWord helloword = new HelloWord();//对象其实是在堆内存里,包含实例变量
//线程1
new Thread(){
	public void run(){
		helloword.increment();
	}
}.start();
//线程2
new Thread(){
	public void run(){
		helloword.increment();
	}
}.start();

Java内存模型

volatile关键字的原理?

1.可见性:
根据《谈一下对Java内存模型的理解》的流程图,可以看出,线程1在进行对data=0进行一系列read,load,use,assign,store操作,把data=1,因为加上了volatile关键字,在同时会失效掉线程2工作内存里面的缓存,这样在线程2进行use的时候,会发现工作内存里面的缓存已经失效了,会强制从主内存里面加载
2.禁止指令重排
java中有一个happens-before原则:
编译器、指令起可能对代码重排序,乱排,要守一定的规则,happens-before原则,只要符合happens-before的原则,那么就不能胡乱重排,如果不符合这些规则的话,那就可以自己排序

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unlock操作先行发生于后面对同一个锁lock操作,比如说代码中有一个对lock.lock(),lock.unlock(),lock.lock(),一定要保证规则
  • valatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作(重点)
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过thread.join()方法结束,thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
    3.不保证原子性

volatile底层是如何基于内存屏障保证可见性和有序性?

连环炮:内存模型 -> 原子性、可见性、有序性 -> volatile+可见性 ->volatile+有序性(指令重排+happens-before) ->volatile底层的原理(内存屏障级别的原理)
volatile+原子性:不能够保证原子性,虽然说有些极端特殊情况下有保证原子性的效果
使用synchronized、lock保证原子性
禁止指令重排:底层是前后进行读写的时候,加入了内存屏障
保证可见性:对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改,如果发现了别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了

对Spring的IOC机制的理解?

Spring IOC机制
spring ioc,spring容器,根据xml配置,或者是注解,去实例化一些bean对象,然后根据xml配置或者注解,去对bean对象之间的引用关系,进行依赖注入,某个bean依赖了另外一个bean
此层的核心技术:反射,通过反射技术,根据类去构建对应的对象出来,用的就是反射技术,这样可以使得类与类之间彻底的解藕

对Spring AOP机制的理解?

Spring 核心框架里面,最关键的两个机制,就是ioc和aop。根据xml配置或者注解,去实例化我们所有的bean,管理bean之间的依赖注入,让类与类之间的耦合性降低,维护代码的时候更加方便轻松。

AOP是面向切面编程,简单的说就是把我们重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们已有的方法进行增强。

AOP的作用和优势
作用:在程序运行期间,不修改源码对已有方法进行增强。
优势:减少重复代码,提高开发效率,维护方便

了解过cglib动态代理吗?跟jdk动态代理有什么区别?

jdk动态代理:如果类实现类某个借口,Spring aop就会使用jdk动态代理,生成一个实现同样接口的代理类,构造一个实例对象出来
cglib动态代理:如果很多时候某个类没有实现接口,Spring aop会该用cglib来生成动态代理,生成类的子类,覆盖方法,在方法里加入增强代码

能说说Spring中的Bean说线程安全的吗?

Spring容器中的bean可以分为5个范围:

  1. singleton:默认,每个容器中只有一个bean的实例
  2. prototype:为每个bean请求提供一个实例
  3. request:为每个网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收
  4. session:与request范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效
  5. global-session

Spring中的bean是不是线程安全的,答案是否定的,spring bean默认来说是单例的,是线程不安全的。但是java web系统中,一般来说很少在spring bean中放一些实例变量,通常都是多个组件互相调用,最终去访问数据库的,所以一般结果就是多个线程并发的访问数据库。

Spring的事务实现原理是什么?

事务的实现原理:事务传播机制,如果加了一个@Transactional注解,此时就spring会使用aop思想,对这个方法在执行之前,先去开启事务,执行完毕之后,根据你方法是否报错,来决定回滚还是提交事务
比如说,我们现在有一段业务逻辑,方法A调用方法B,我希望的是如果说方法A出错了,此时仅仅回滚方法A,不能回滚方法B,必须得用REQUIRES-NEW,传播机制,让他们两的事务是不同的。

方法A调用方法B,如果出错,方法B只能回滚他自己,方法A可以带着方法B一起回滚,NESTED嵌套事务。

说说spring的核心架构?

spring的生命周期:

  1. 实例化Bean:
    对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于AplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有bean

  2. 设置对象属性(依赖注入)
    实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息以及通过BeanWrapper提供的设置属性的接口完成依赖注入,比如说通过构造函数注入,setter

public class Service{
	private MyDao Mydao;
	public MyService(){
		this.myDao=myDao;
	}
}
public void setMyDao(MyDao myDao){
	this.myDao=myDao;
}
  • 处理Aware接口
    如果这个Bean已经实现了applicationcontextAware接口,Spring容器就会调用我们的bean的setApplicationContext(ApplicationContext)方法,传入Spring上下文,把Spring容器传递给这个bean
  • BeanPostProcessor
    如果相对Bean进行一袭诶自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitalization(Objcect obj,String s)方法
  • InitializingBean与init-method:
    如果Bean在Spring配置文件中配置了init-method属性,则会自动低哦啊用其配置的初始化
  • 如果这个bean实现了beanpostprocessor接口,将会调用postprocessafterinitialization方法
  • DisposableBean:
    当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean接口,会调用其实现的destroy()方法
  • destory-method:
    最后,如果这个Bean的spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
    spring生命周期

Spring中的设计模式?

  • 工厂模式:spring ioc核心的设计模式的思想体现,他自己就是一个大的工厂,把所有的bean实例都放在了spring容器里(大工厂),如果要使用bean,就找spring容器就可以了,自己就不需要创建对象了
public class MyControll{
	private MyService myService = MyServiceFactory.getMyService();
}
public class MyServiceFactory{
	public static MyService getMyService(){
		return new MyServiceImpl();
	}
}
  • 单例模式:spring默认来说,对每个bean走的都是一个单例模式,确保说一个类在系统运行期间,只有一个实例对象,只走一个bean
public class MyService(){
	private static volatile MyService myService;
	public static MyService getInstance(){
		if(myService == null){
			synchronized(MyService.class){
				if(myService == null){
					myService = new MyService();
				}
			}
		}
	}
}
  • 代理模式:就是spring的aop,如果说要对一些类的防范切入一些增强的代码,会创建一些动态代理的对象,这样对目标对象的访问,先经过动态代理对象,动态代理对象先做一些增强的代码,调用目标对象

JVM有哪几块内存区域?Java8之后对内存分代做了什么改进?

我们在执行代码的时候,肯定会有很多线程,tomcat里就有很多自己的工作线程,去执行我们写的代码,每个工作线程都会有自己的一块数据结构:栈内存,这个里面会存放一些东西
java 8以后的内存分代的改进,永久代里放了一些常量池+类信息,常量池 ->堆内存,类信息 -> metaspace(元空间)

JVM是如何运行起来的吗?我们的对象是如何分配的?

JVM分配流程图:
jvm运行原理

JVM在哪些情况下会触发垃圾回收?

假如给年轻代一共是2GB内存,给老年代是2GB内存,默认情况下,eden和2个S(S1和S2区域)的比例是8:1:1
如果说eden区域满了,此时必然会出现垃圾回收,ygc(young gc)
回收的对象:没有人引用的对象 注:有方法引用的对象、static静态变量不能被认为是垃圾对象
哪些情况下会触发垃圾回收

JVM年轻代垃圾回收算法?对象什么时候转移到老年代

如果说让代码一边运行,一边有变动,一边判断哪些对象是可以回收的,这是不现实的,垃圾回收的时候有一个概念,叫做stop the world 停止jvm里的工作线程的运行,然后扫描所有对象,判断哪些可以回收,哪些不可以回收

年轻代:大部分情况下,对象生存周期是很短的,此时对象方法执行结束了,就可以认为是垃圾,可以回收
对于年轻代的垃圾回收算法,采取的是复制算法,一次young gc 就完成了一次垃圾回收

什么情况下,年轻代的对象会转移到老年代中?

  1. 有的对象在年轻代里熬过了很多次垃圾回收,15次垃圾回收,此时会认为这个对象是要长期存活的对象
  2. S区域放不下的对象,直接会放到老年代
  3. 大对象

说说老年代的垃圾回收算法,常用的垃圾回收器是什么?

老年代对象越来越多,空间也会满的,但是不合适使用年轻代的复制算法,因为老年代里的对象,很多都是被长期引用的,里面有spring管理的各种bean

对于老年代而言,他里面的垃圾可能是没有那么多的,采用标记-清理,找出来那些对象,然后直接把垃圾对象在老年代里清理掉,但是这样会产生一个内存碎片的问题。故采用标记-压缩把老年代里面存活的对象标记出来,移动到一起,存活的对象压缩到一片内存空间里去,这样剩余的空间都是垃圾对抗给整个清理掉,剩余的都是连续可用的内存空间,解决了内存碎片的问题

常用的垃圾回收器:
parnew+cms的组合
parnew是使用多个线程,并发的把垃圾对象清理掉
cms是老年代回收的方法,使用在jdk8及以前,分成好几个阶段:初始标记->并发标记->并发预清理->重新标记->并发清理->并发重置,老年代的垃圾回收是比较慢的,一般来说比年轻代慢10倍以上
g1直接分代回收,直接可以同时回收年轻代和老年代,使用在jdk9及以后

TCP三次握手的流程?为啥不是两次或者是四次呢?

举例子:
A客户端:给服务端B说,我想要和你建立连接
B服务端:给A客户端说,我已经收到你的信息了,可以建立连接
A客户端:好的,我已经知道连接已经建立了

如果是两次握手会出现的问题:客户端发送的第一次握手卡在半路上了,所以又重新发送了一次连接,这时,卡在半路上的连接竟然通了,服务端会返回第二次握手,但是这次连接已经不需要了,如果不进行第三次握手,服务端会开辟资源准备通信,所以这时候需要客户端发送第三次握手,要求释放资源
TCP建立三次握手
TCP四次挥手断开连接

HTTPS的工作原理?为啥用HTTPS就可以加密通信?

https的工作原理:

  1. 浏览器把自己支持的加密规则发送给网站
  2. 网站从这套加密规则里选出来一套加密算法和hash算法,然后把自己的身份信息用证书的方式发回给浏览器,证书里有网站地址、加密公钥、证书颁发机构
  3. 浏览器验证证书的合法性,然后浏览器地址栏上会出现一把小锁,浏览器接着生成一串随机数密码,然后用证书里的公钥进行加密,这块走非对称加密,用约定好的hash算法生成握手消息的hash值,然后用密码对消息进行加密,然后把所有东西发给网站,这块走对称加密
  4. 网站用本地的私钥对消息解密取出来密码,然后用密码解密浏览器发来的握手消息,计算消息的hash值,并验证与浏览器发送过来的hash值是否一致,最后用密码加密一段握手消息,发给浏览器
  5. 浏览器解密握手消息,然后计算消息的hash值,如果跟网站发来的hash一样,握手就结束,之后所有的数据都会由之前浏览器生成的随机密码,然后用对称加密进行加密

Mysql索引的原理?

B+树和B-树不太一样的地方在于:

  1. 每个节点的指针上线为2d而不是2d+1
  2. 内节点不存储data,只存储key;叶子节点不存储指针
    Innodb存储引擎索引
    不建议使用uuid作为主键,会导致每一个索引文件都很大,浪费磁盘空间,建议使用aotu_increment自增主键,这样就可以保证在添加数据的时候,这个树是不用分裂的
    索引设计:
    create index (shop_id,product_id,gmt_create);
  3. 全列匹配:就是说在一个sql里,正好where条件用到这个三个条件,那么就一定可以用到这个联合索引的
  4. 最左前缀匹配:确保在sql里面起码一个或者是两个字段,可以匹配上索引,从最左边开始,按照顺序匹配,如果能匹配上的话,先让部分字段根据索引去查,查出来的结果在根据后面的复杂的自查询,函数之类的去进行处理
  5. 前缀匹配:如果你是不等值的,比如=,>=,<=之类的操作,是like操作,那么必须要是like ‘XXX%’,这样才可以用上索引
  6. 范围列匹配:如果你是范围查询,比如>=,<=,between操作,只能是符合最左前缀的规则才可以范围,范围之后的列就不用索引了
    比如说 where shop_id>=1,product_id =1这样就在联合索引中根据shop_id来查询了

事务的几个特性是啥?有哪几种隔离级别?

先说一下事务的ACID

  1. Automic:原子性,要不全部成功,要么全部失败
  2. Consistency:一致性,针对数据一致性来说,就是一组SQL执行之前,数据必须是准确的,执行之后,数据也必须是准确的
  3. Isolation:隔离性,事务在跑的时候不能互相干扰
  4. Durability:持久性,事务成功之后,就必须永久对数据的修改是有效的
    事务的隔离级别:
    1、读未提交 2、读已提交 3、可重复读(默认,通过MVCC机制实现的,就是多版本并发控制) 4、幻读 5、串行化

Mysql数据库锁的实现原理?

innodb的行锁有共享锁(s)和排他锁(x)两种,共享锁就是多个事务都可以加共享锁读同一行数据,但是别的事务不能写这行数据,排他锁,就是一个事务可以写这行数据,别的事务只能读,不能写
innodb的表锁,分为意向共享锁,就是说加共享行锁的时候,必须先加这个共享表锁,还有一个意向排他锁,就是说,给某行加排他锁的时候,必须先给表加排他锁,是innodb引擎自动加的,不用手动加

insert、update、delete,innodb会自动给那一行加行级排他锁
select,innodb啥锁都不加,因为innodb默认实现可重复读,也就是mvcc机制,所以多个事务随便读一个数据,一般不会有冲突,大家就读自己的那个快照就可以了,不涉及到什么锁的问题

Mysql的sql调优一般有哪些手段?

explain查看索引的执行计划:
type:这个最重要了,all(全表扫描)、const(读常量,最多一条记录匹配)、eq、ref(走主键,一般就最多一条记录匹配)、
index(扫描全部索引)、range(扫描部分索引)
possible_keys:显示可能使用的索引
key:实际使用的索引
key_len:使用索引的长度
ref:联合索引的哪一列被哦那个了
rows:一共扫描和返回了多少行
extra:using(需要额外进行排序)、using temporary(mysql构建了临时表,常见于自查询,比如排序的时候)、
using where(就是对索引扫出来的数据再次根据where来过滤出了结果)

Synchronized锁同时对原子性、可见性、有序性的保证

原子性:基本的赋值写操作都是可以保证原子性的,复杂的操作是无法保证原子性的
可见性:MESI协议、flush、refresh,配合起来,才可以解决可见性
有序性:三个层次,最后一个层次有4种重排(LoadLoad、StoreStore、LoadStore、StoreLoad)

synchronized是同时可以保证原子性、可见性以及有序性
原子性的层面而言,他加了之后,有一个加锁和释放锁的机制,加锁了之后,同一段代码就只有他可以执行了
可见性,在同步代码块对变量做的写操作,都会在释放锁的时候,全部强制执行flush操作,在进入同步代码块的时候,对变量的读操作,全部会强制执行refresh操作,更新的数据,别的线程只要进入代码块,就一定可以读到
有序性,synchronized关键字,会通过加各种内存屏障,来解决LoadLoad、StoreStore等重排序

synchronized是如何通过加锁保证原子性的?

在出锁代码块的时候,加一个monitorexit的指令,然后递减锁计数器,如果锁计数为0,就会标志当前线程不持有锁,释放锁

MyObject lock = new MyObject();
synchronized(){

}

深入分析底层原理:
Java对象都是分为对象头和实例变量两块的,其中实例变量就是大家平时看到的对象里的那些变量数据,然后对象头包含了两块东西,一个是Mark World(包含hashCode、锁数据、GC数据等),另一个是Class Metadata Address(包含了指向类的愿数据的指针)
在Mark World里就有一个指针,是指向了这个对象实例关联的monitor的地址,这个monitor是C++实现的,不是java实现的,这个monitor实际上是c++实现的一个ObjectMonitor对抗,里面包含了一个_owner指针,指向了持有锁的线程
ObjectMonitor里还有一个entryList,想要加锁的线程先进入这个entryList等待获取机会尝试加锁,实际有机会加锁的线程,就会设置_owner指针指向自己,然后对_owner计数器累加1次
然后是释放锁的时候,先是对_count计数器递减1,如果为了0了就会设置_owner为null,不再指向自己,代表自己彻底释放锁
如果获取锁的线程执行wait,就会将计数器递减,同时_owner设置为null,然后自己进入waitset中等待唤醒,别人获取了锁执行notify的时候就会唤醒waitset中线程竞争尝试获取锁
synchronized加锁细化

Synchronized是如何使用内存屏障保证可见性和有序性的?

int b = 0;
int c = 0;
synchronized(this){//->monitorenter
//load内存屏障
//Acquire内存屏障
	int a = b;
		c =1;//synchronized代码块里面还是可能发生指令重排
//Release内存屏障
}//->monitorexit
//store内存屏障

按照可见性来划分的话,内存屏障可以分为Load屏障和Store屏障
Load屏障的作用是执行refresh处理器缓存的操作,说白了就是对别的树立起更新过的变量,从其他处理器的高速缓存(或则是主内存)加载数据到自己的高速缓存来,确保自己看到的是最新的数据
Store屏障的作用是执行flush处理器缓存的操作,说白了就是把自己当前处理器更新的变量的值,都刷新到高速缓存(或者主内存)里去

在monitorexit指令之后,会有一个store屏障,让线程把自己在同步代码块里修改的变量的值都执行flush处理器缓存的操作,刷到高速缓存(或者主内存)里去,然后在monitorenter指令之后会加一个Load屏障,执行refresh处理器缓存的操作,把别的处理器修改过的最新值加载到自己高速缓存里来

所以说通过Load屏障和Store屏障,就可以让synchronized保证可见性
synchronized处理流程
synchronized总结:

  1. 原子性:加锁和释放锁,ObjectMonitor
  2. 可见性:加了Load屏障和Store屏障,释放锁flush数据,加锁会refresh数据
  3. 有序性:Acquire屏障和Release屏障,保证了同步代码块可以重排,但是同步代码块里面和外面的不能重排

volatile关键字对原子性、可见性以及有序性的保证

注意一点:volatile是不保证原子性的,特殊情况下,在32为的jvm中的long/double类型变量的赋值操作是不具备原子性的,加上volatile就可以保证原子性了

volatile boolean isRunning = true//线程1
//Release屏障
isRunning=false//Store屏障

//线程2
//Load屏障
while(isRunning){
//Acquire屏障
}

在volatile变量写操作的前面会加入一个Release屏障,然后在之后会加入一个Store屏障,这样就可以保证volatile写跟Release屏障之前的任何读写操作都不会指令重排,然后Store屏障保证了写完数据之后,立马会执行flush处理器缓存的操作

在volatile变量读操作的前面会加入一个Load屏障,这样就可以保证对这个变量读取时,如果被别的处理器修改过了,必须得从其他处理器的高速缓存(或者主内存)中加载到自己本地高速缓存中,保证读到的是最新数据,在之后会加入一个Acquire屏障,禁止volatile读操作之后的任何读写数据操作会跟volatile读指令重排序

高速缓存的数据结构:拉链散列表、缓存条目以及地址解码

volatile和synchronized
从原子性、可见性和有序性三个方面分别来聊,这两个关键字对那几个“性”的保障是通过什么来实现的,聊到他们会加哪些内存屏障,怎么加,这么内存屏障的效果,结合底层硬件层面的一个初步的原理,来给面试官聊一下
硬件级别的高速缓存

结合硬件级别的缓存数据结构深入分析缓存一致性协议

MESI的工作原理:
处理器0读取某个变量的数据时,首先会根据index、tag和offset从高速缓存的拉链散列表数据,如果发现状态为I,也就是无效的,此时就会发送read消息到总线
接着主内存会返回读经的数据给处理器0,处理器0就会把数据放到高速缓存里,同时cache entry的flag状态是s
在处理器0对一个数据进行更新的时候,如果数据转改是s,此时就需要发送一个invalidate消息到总线,尝试让其他的处理器的高速缓存的cache entry 全部变为I,以获得数据的独占锁
其他的处理器1会从总线嗅探到invalidate消息,此时就会把自己的cache entry设置为1,也就是过期掉自己本地的缓存,然后就是返回invalidate ack消息总线,传递回处理器0,处理器0必须收到所有处理器返回的ack消息
接着处理器0就会将cache entry先设置为E,独占这条数据,在独占期间,别的处理器就不能修改数据,因为别的处理器此时发出invalidate消息,这个处理器0是不会返回invalidate ack消息的,除非他先修改完再说
接着处理器0就是修改这条数据,接着将数据设置为M,也有可能是把数据此时强制写回到主内存中,具体看底层硬件实现
然后其他处理器此时这条数据的状态都是I了,那如果要读的话,全部都需要重新发送read消息,从主内存(或者是其他处理器)来加载,这个具体怎么实现要看底层的硬件了,都有可能

cache entry的flag代表了缓存数据的状态,MESI协议中划分为:

  1. invalid:无效的,标记为I,这个意思就是当前cache entry无效,里面的数据不能使用
  2. shared:共享的,标记为S,这个意思是当前的cache entry有效,而且里面的数据在各个树立起中都有各自的副本,但是这些副本的值跟主内存的值是一样的,各个处理器就是并发的在读而已
  3. exclusive:独占的,标记问E,这个意思就是当前处理器对这个数据独占了,只有他可以有这个副本,其他的处理器都不能包含这个副本

硬件层面的MESI协议为何会引发有序性和可见性问题?

可见性和有序性问题:
可见性:是因为写缓冲期和无效队列导致的,写数据不一定立马写入自己的高速缓存(或者主内存),读数据不一定立马从别人的高速缓存(或者主内存)刷新最新的值过来,invalidate消息在无效队列里面
有序性:略~

Java虚拟机对锁的优化:锁消除、锁粗化、偏向锁、自旋锁

  1. 锁消除
    锁消除是JIT编译器对synchronized锁的优化,在编译的时候,JIT会通过逃逸分析技术,来分析synchronized锁对象,是不是可能被一个线程来加锁,没有其他的线程来竞争加锁,这个时候编译就不用加入monitorenter和monitorexit的指令
    这就是仅仅一个线程争用锁的时候,就可以消除这个锁了,提升这段代码的执行效率,因为可能就只有一个线程来加锁,不涉及到多个线程竞争锁
  2. 锁粗化
    这个意思就是,JIT编译器如果发现有代码里连续多次加锁释放锁的代码,会给合并为一个锁,就是锁粗化,把一个锁给搞粗来,避免频繁多次进行加锁和释放锁
  3. 偏向锁
    这个意思就是说,monitorentry和monitorexit是要使用CAS操作加锁和释放锁的,开销比较大,因此如果发现大概率只有一个线程会主要竞争一个锁,那么会给这个锁维护一个偏好(Bias),后面他加锁和释放锁,基于Bias来执行,不需要通过CAS,性能会提升很多,但是如果有偏好之外的线程来竞争锁,就要收回之前分配的偏好
  4. 轻量级锁
    如果偏向锁没能成功实现,就是因为不同线程竞争锁太频繁了,此时就会尝试采用轻量级锁的方式来加锁,就是将对象头的Mark World里有一个轻量级锁指针,尝试指向持有锁的线程,然后判断下是不是自己加的锁,如果是自己加的锁,那就执行代码,如果不是自己加的锁,那就是加锁失败,说明有其他人加了锁,这个时候就是升级为重量级锁
  5. 自旋锁
    这是JIT编译器对锁的另外一个优化,如果各个线程持有锁的时间很短,那么一个线程竞争锁不到,就会暂停,然后发生上下文切换,这个操作是比较耗时的,但是其他线程很快就释放锁了,然后暂停的线程会再次被唤醒,也就是说,这种情况下,线程会频繁的上下文切换,导致开销过大。所以锁这种线程持有锁很短时间的情况下,是可以采取忙等策略的,也就是一个线程没竞争到锁,进入一个while循环,不停等待,不会暂停不会发生上下文切换,等到机会获取锁就继续执行了,这样可以大幅度减少线程上下文切换,而这种自旋等待获取锁的方式,就是所谓的自旋锁,就是不断的自旋尝试获取锁
    如果一个线程持有锁的时间很长,那么其他线程获取不到锁,就会暂停,发生上下文切换,让其他线程来执行,这种自己暂停获取锁的方式,就是所谓的重量级锁

更新到这为止!!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值