admin管理员组文章数量:1600085
目录
一、HashMap和CocurrentHashMap
说一下你对HashMap的理解;他的底层结构是什么样的;
一个数据是怎么存到,map里面的;
怎么可以让HashMap的碰撞变小;
为什么不能用可变类型做键值呢;
链表什么时候转成红黑树;为什么要转成红黑树;
红黑树是什么样的,他的结构是什么样的;他还要其他什么特征吗;
CocurrentHashMap历史变化、数据结构及原理
为什么1.7到1.8要换一种方式
分段锁是的底层怎么实现的;
他的继承结构;
自旋锁是怎么实现的
如果俩个key都在同一个分段上面,是怎么保证安全的;
CAS的实现原理
二、锁:
java里面的锁常用的都有哪些;
synchronized修饰一个静态类和静态方法和修饰一个非静态方法有什么区别吗?
说说synchronized和ReentrantLock区别;
ReentrantLock的实现原理
讲讲乐观锁和悲观锁;
三、线程:
线程池的原理;线程池它里面的主要参数有哪些;
keepAliveTime是什么;
回收的话回收到多少个线程呢;
线程池是怎么实现一个线程复用的;
初始化线程池时线程数的选择
说说线程池的拒绝策略
五种线程池的使用场景
线程池的关闭
线程池都有哪几种工作队列
execute和submit的区别?
volatile原理怎么实现;
四、jvm及GC:
jvm垃圾回收的流程;哪些对象会被认为是垃圾;有一个对象A它有一个属性是B,B这个对象他又有一个属性是A,这个对象最终会不会被认为是垃圾;
GC root哪些对象会被认为是root;
jvm里面有一个存储虚拟s1和s2
什么样的数据会往老年代里面迁移呢;
如果老年代内存也不够用了怎么办呢;
fullGC的时候会有什么现象吗;有没有遇到到fullGC的时候影响业务的场景;
CMS收集器
G1收集器:
jvm报错,OOM;
五、数据库:
接触过哪些数据库;
oracle和mysql分页的区别;oracle分页的原理;
数据库查询执行流程
数据库怎么优化;索引的原因;为什么要使用B+树存储索引;
什么情况要加索引,哪些字段上适合加索引;
使用索引的优缺点?
MySQL如何定位慢sql
Mycat原理
mycat会用吗;平时会自己弄分库分表吗;什么时候需要分库什么时候需要分表;
你们现在的数据量大吗;是怎么进行拆分存储的;数据库是分库的对吧;他的分库的规则是什么样的;
读锁和写锁的实现方式,加了读锁,其他资源能不能读,加了写锁其他资源能不能读;
六、redis:
redis是什么语言开发的;
redis底层的实现原理有去研究过吗;为什么redis的性能能达到这么快呢;
redis里面有个string,一个字符串类型的值能存储最大容量是多少;你知道他的底层是怎么实现的;
redis的keys为什么影响性能,redis时间复杂度是O(n)的命令;
一般用redis都做什么;
什么是redis的缓存穿透;什么是缓存雪崩;怎么解决这些问题;缓存穿透不通过ip过滤,最简单的方式怎么解决;
redis 和 memcached 的区别:
Redis 哈希槽的概念
redis 设置过期时间;
redis有做集群吗;怎么做集群的;你在项目中还遇到什么问题吗;
redis持久化问题;会有数据损失吗,开启aof的持久化吗;
Redis Sentinel的工作流程
如何解决 Redis 的并发竞争 Key 问题
cps是多少;一秒钟处理多少;
分布式锁的实现方式;有哪些常见的数据分布式算法,比如说现在部署了3个redis,里面的内存是不一样的,怎么保证有一个内存回落到固定的一个redis实例上面的分布式算法;一致性hash算法相较于普通hash算法有什么优势;
你对sentinel hystrix有用过吗;你们微服务有用sentinel 吗
说一下你们的熔断是怎么做的
你认为什么时候是不可用的
sentinel hystrix的区别
七、MQ:
消息会有消费失败的情况吗;消费失败系统怎么处理;
kafka的底层是怎么实现的;怎么可以达到这么高的吞吐量
你们用kafka有遇到什么问题吗;
如果保证一个消息消费他是有序的怎么做
kafka和activemq和ribbitmq的区别
如果消费端异常,后续的操作流程是什么;
死信队列如果消息失败可以放;mq的更新频路是怎么设置的;
八、jdk1.8有什么新特性;
函数式编程;Foreach 是怎么退出循环的;
九、forName与loadClass的区别
十、spring:
ApplicationContext和beanfactory的区别
十一、SpringCloud 和 Dubbo
SpringCloud 和 Dubbo 有哪些区别
SpringBoot 和 SpringCloud 请你谈谈对他们的理解;
什么是服务熔断?什么是服务降级?
微服务的优缺点是什么?说下你在项目开发中碰到的问题
你所知道的微服务技术栈有哪些?请举例一二
Eureka 和 Zookeeper 都可以提供服务注册与发现的功能,请说说两个的区别?
微服务与 SOA 服务的区别
一、HashMap和CocurrentHashMap
说一下你对HashMap的理解;他的底层结构是什么样的;
jdk7之前底层使用的是数组+链表的形式,1.8之后改成了数据加链表加红黑二叉树,提高查询性能,
一个数据是怎么存到,map里面的;
1.首先创建hashmap,默认大小是16,负载因子是0.75,我们可以去指定他的长度size,如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方。那么为什么2的整数次方,例如如果传10,大小为16。
2.然后我们进行一个插入,首先他会对key取一个hash值,是32位的int值,让他的高16位和低16位进行一个异或运算,原因的是因为后16位有可能是相同的 为了使我们hash的值分布的更加均匀,并且使用位运算也非常高效 。
源码里面就把散列值和((数组的长度-1 )因为-1之后他的地位全都是1高位都为0这种形式)做了与运算 算出数组的下标。之所以要做是因为int值的范围是 -2的31次方-1到2的32次方 前后加起来大概与40亿的映射空间 但问题是一个40亿长度的数组 内存是放不下的。
3.之后我们把hashCode的节点组装成一个entry节点 把这个entry节点存储到数组下表里面 存储的时候首先会判断一下这个数组的下标是不是有值 如果说有值得话 他首先去判断这个key的值是不是是相等的 如果是相等的 说明我们是相同的一个值 那我们就要用现在的值替换掉原来的值如果是不相等的 那就使用链表的形式将他串联起来。存储之后链表也会做一个判断如果说链表他是大于8的 我们会把它转化成一个红黑二叉树进行排列。
这里有一个jdk7和jdk8的区别
- 数组+链表改成了数组+链表或红黑树;
- 链表的插入方式从头插法改成了尾插法,原因是因为我们在非线程安全下使用hashmap的时候会出现一个死循环的问题 这个问题在1.8之后进行了一个修复.
- 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
- 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
怎么可以让HashMap的碰撞变小;
算法让他分布的更均匀 位运算 扩容
为什么不能用可变类型做键值呢;
因为可变类型作为key,如果修改会造成数据无法找到。
链表什么时候转成红黑树;为什么要转成红黑树;
我们在jdk1.8使用了红黑树,原因是因为我们在发生hash碰撞的时候,我们会把数据按照链表的形式把他插入进去,那么这个时候因为我们链表只能从头结点进行一个遍历,那么他的一个时间复杂度就On,非常影响性能,所以我们当他大于2三次方8的时候就会转成红黑树;原因是使用红黑二叉树之后,当我们在查询数据的时候复杂度就会降为O1,可以大大的提高我们的性能
红黑树是什么样的,他的结构是什么样的;他还要其他什么特征吗;
他的结构的话我们平常会像这种平衡二叉树来维护他,但是平衡二叉树可能说是我每插入一个节点,他就要去维护这种情况,比较频繁,就有了红黑二叉树这种红黑节点相互交替,他有四个规则,
CocurrentHashMap历史变化、数据结构及原理
在并发编程里面使用的CocurrentHashMap,在1.7我们使用sengment分段锁,每一次key进来之后,他都会判断你这个key是属于哪一个分段里面的,从而拿到sengment。锁主要是加在链表上的,因为链表会导致我们线程安全的问题,1.8之后我们将锁改成了CAS+synchronized,解决分段锁的弊端。
为什么1.7到1.8要换一种方式
之所以改锁机制是因为sengment分段锁他可以设置,但是他默认是16段,这个时候我们的并发量就会有一个限制,包括我们需要去估sengment的个数,如果估少了,就会导致锁的空闲,估大了会导致锁的一些竞争,而且一旦初始化以后,它是不可以扩容的,所以之后改成了自旋锁,大大提高了性能。
分段锁是的底层怎么实现的;
分段锁底层维护了一个数组,我们默认的话是16段,每一个段就相当于一把锁,然后最大的并发量可能到达16;
他的继承结构;
它继承了ReentrantLock,然后ReentrantLock继承了AQS(AbstractQueuedSynchronizer)
自旋锁是怎么实现的
思想是给 table的每一个下标都加锁,也就是当对下标进行操作时都会加锁(CAS+Synchronize)。ConcurrentHashMap 成员变量使用 volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用 CAS操作和 synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。
如果俩个key都在同一个分段上面,是怎么保证安全的;
CAS的实现原理
CAS其实就是CompareAndSwap首先他会传入一个值还有他的地址,然后进行一个CAS的判断,当前的值与我们现在的值进行比较,如果说一样的话,那我再会进行一个数据的插入,当前的第三个值插入进去,如果不相同从新获取值,然后进行修改,进行插入,这个值可能会有一个ABA的问题,所以每次拿到值以后,会给他加一个版本号,然后进行比较插入的。
二、锁:
java里面的锁常用的都有哪些;
常用的有synchronized(Object)、ReentrantLock(可重用锁)这些;
synchronized修饰一个静态类和静态方法和修饰一个非静态方法有什么区别吗?
Synchronized修饰非静态方法,俗称“对象锁”,要调用这个方法,必须创建这个对象,再来调用这个方法,因为一个类可以创建多个对象,而不同的对象就有不同的对象头,不同的对象头就有不同的锁,不同的锁用不同的线程创建的,他去调用这个方法的时候,他是不阻塞的,因为他们拿到的是不一样的锁。
Synchronized修饰静态方法,俗称“类锁”,因为类只有一个class文件,他在调用的时候是拿class类加锁,多个线程来调用这个方法的时候只有一把锁,所以执行完run方法之后他才会释放,下一个线程才可以拿到锁接着执行。
说说synchronized和ReentrantLock区别;
synchronized是可重用锁,他有俩种使用方式,方法或者代码块。加锁的话是传入一个人对象或者一个静态类,其实是对对象头进行一个操作,会把线程的id存储到对象头里面,线程在第二次调用这把锁的时候,他去判断这个id是不是锁里面的id,如果是的话他就可以直接操作。因为是jvm自己管理的,所以锁释放的话,当run方法执行完,也会直接释放。
ReentrantLock需要手动加锁和释放,首先我们要加锁,并在finally里面进行释放。和synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能
ReentrantLock的实现原理
ReentrantLock的使用
Lock lock = new ReentranLock();
lock.lock();
try{
//do something
}finally{
lock.unlock();
}
ReentrantLock实现了Lock接口,加锁和解锁都需要显式写出,注意一定要在适当时候unlock。
和synchronized相比,ReentrantLock用起来会复杂一些。在基本的加锁和解锁上,两者是一样的,所以无特殊情况下,推荐使用synchronized。ReentrantLock的优势在于它更灵活、更强大,增加了轮训、超时、中断等高级功能。
ReentrantLock的原理
公平锁和非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock的内部类Sync继承了AQS,分为公平锁FairSync和非公平锁NonfairSync。
- 公平锁:线程获取锁的顺序和调用lock的顺序一样,FIFO;
- 非公平锁:线程获取锁的顺序和调用lock的顺序无关,全凭运气。
ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。
讲讲乐观锁和悲观锁;
乐观锁 :总是认为不会产生并发问题,每次去取数据的时候总认为不会有其他线程对数据进行修改,因此不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现
version方式:
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
CAS操作方式:
即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。
悲观锁:总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁,在Java中,synchronized的思想也是悲观锁
适用场景
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。
三、线程:
java内存模型(Java Memory Model,JMM)
CPU的运行计算速度是非常快的,而其他硬件比如IO,网络、内存读取等等,跟cpu的速度比起来是差几个数量级的。而不管任何操作,几乎是不可能都在cpu中完成而不借助于任何其他硬件操作。所以协调cpu和各个硬件之间的速度差异是非常重要的,目前基于高速缓存的存储交互很好的解决了cpu和内存等其他硬件之间的速度矛盾,多核情况下各个处理器(核)都要遵循一定的诸如MSI、MESI等协议来保证内存的各个处理器高速缓存和主内存的数据的一致性。
Java内存模型中涉及到的概念有:
-
主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
-
工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。可以与前面说的高速缓存相比。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。
这里需要说明一下:主内存、工作内存与java内存区域中的java堆、虚拟机栈、方法区并不是一个层次的内存划分。这两者是基本上是没有关系的,上文只是为了便于理解,做的类比
volatile原理怎么实现;
volatile可见性,防止指令重排;
如果有一个static的变量,值会存储在主内存。如果多个线程访问这个变量,每个线程都会将变量的值拷贝到自己的工作内存,之后的操作就是针对自己工作内存里副本的操作,最后再写回主内存。
如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
线程池的原理;线程池它里面的主要参数有哪些;
public ThreadPoolExecutor(int corePoolSize, //核心线程的数量
int maximumPoolSize, //最大线程数量
long keepAliveTime, //超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间的单位
BlockingQueue<Runnable> workQueue, //保存待执行任务的队列
ThreadFactory threadFactory, //创建新线程使用的工厂
RejectedExecutionHandler handler // 当任务无法执行时的处理器
) {...}
线程池的参数有7个,首先根据系统的核数,设置核心线程数(corePoolSize),如果说核心线程数满了的话,会定义一个阻塞队列(workQueue),当阻塞队列满了的话,会继续去创建线程,所以这个时候有一个最大的线程数(maximumPoolSize),如果到达最大线程数之后的话,会有一个淘汰策略(handIer),进来之后的话,这个淘汰策略,我们会设置成lfu,还有就是我们线程池去连接的时候,他是有一个连接的超时时间(keepAliveTime)以及超时时间的单位(unit:keepAIiveTime),还有就是我们创建这个线程所用到的一个类加载器(threadFactory)是什么
keepAliveTime是什么;
调用线程池如果多长时间没有继续再去访问了,就会把线程回收进来
getTask 怎么使用 keepAliveTime
(1)首先也是一个自旋,当allowCoreThreadTimeout(运行空闲核心线程超时) 或 wc>corePoolSize(当前线程数量大于核心线程数量) 时,timed会标识为true,表示需要进行超时判断。
(2)当wc(当前工作者数量)大于 最大线程数 或 空闲线程的空闲时间大于keepAliveTime(timed && timeout),以及wc>1或(workQueue)任务队列为空时,会进入compareAndDecrementWorkerCount方法,对wc的值减1。
(3)当compareAndDecrementWorkerCount方法返回true时,则getTask方法会返回null,终止getTask方法的自旋。这时候回到runWorker方法,就会进入到processWorkerExit方法,进行销毁worker。
回收的话回收到多少个线程呢;
回收到核心的线程数;
线程池是怎么实现一个线程复用的;
线程创建的时候是怎么创建核心线程数的,当用完就会释放,其他线程进来就会在用,达到一个线程复用;
初始化线程池时线程数的选择
如果任务是IO密集型,一般线程数需要设置2倍CPU数以上,以此来尽量利用CPU资源。
如果任务是CPU密集型,一般线程数量只需要设置CPU数加1即可,更多的线程数也只能增加上下文切换,不能增加CPU利用率。
说说线程池的拒绝策略
当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。RejectedExecutionHandler接口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
除了JDK默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。
五种线程池的使用场景
newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
线程池的关闭
关闭线程池可以调用shutdownNow和shutdown两个方法来实现
shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表。
shutdown:当我们调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
线程池都有哪几种工作队列
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
execute和submit的区别?
execute适用于不需要关注返回值的场景,只需要将线程丢到线程池中去执行就可以了。
submit方法适用于需要关注返回值的场景
谈谈Threadlocal(本地线程)
ThreadLocal是的作用是提供线程的局部变量。ThreadLocal的核心机制:
- 每个Thread线程内部都有一个Map。
- Map里面存储线程本地对象(key)和线程的变量副本(value)
- 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
线程的五大状态
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
1.新建状态
当用new操作符创建一个线程时。此时程序还没有开始运行线程中的代码。
2.就绪状态
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序来调度的。
3.运行状态(running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
4.阻塞状态(blocked)
线程运行过程中,可能由于各种原因进入阻塞状态:
①线程通过调用sleep方法进入睡眠状态;
②线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
③线程试图得到一个锁,而该锁正被其他线程持有;
④线程在等待某个触发条件;
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
5.死亡状态(dead)
有两个原因会导致线程死亡:
①run方法正常退出而自然死亡;
②一个未捕获的异常终止了run方法而使线程猝死;
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法,如果是可运行或被阻塞,这个方法返回true;如果线程仍旧是new状态且不是可运行的,或者线程死亡了,则返回false。
四、jvm及GC:
内存模型
其中,
线程私有的:程序计数器,虚拟机栈,本地方法栈
线程共享的:堆,方法区,直接内存
1 程序计数器
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。
java虚拟机的多线程是通过线程轮流切换并分配CPU的时间片的方式实现的,因此在任何时刻一个处理器(如果是多核处理器,则只是一个核)都只会处理一个线程,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,因此这类内存区域为“线程私有”的内存。
从上面的介绍中我们知道程序计数器主要有两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
注意:程序计数器是唯不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
2 Java 虚拟机栈
Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是 Java 方法执行的内存模型。Java虚拟机栈是由一个个栈帧组成,线程在执行一个方法时,便会向栈中放入一个栈帧,每个栈帧中都拥有局部变量表、操作数栈、动态链接、方法出口信息。局部变量表主要存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)和对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。
OutOfMemoryError:若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
Java 虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。
3 本地方法栈
和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。方法执行完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和 OutOfMemoryError 两种异常。
4 堆
堆是Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存(目前由于编译器的优化,对象在堆上分配已经没有那么绝对了,参见:https://wwwblogs/aiqiqi/p/10650394.html)。
Java 堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代:其中新生代又分为:Eden空间、From Survivor、To Survivor空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。“分代回收”是基于这样一个事实:对象的生命周期不同,所以针对不同生命周期的对象可以采取不同的回收方式,以便提高回收效率。从内存分配的角度来看,线程共享的java堆中可能会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
如图所示,JVM内存主要由新生代、老年代、永久代构成。
① 新生代(Young Generation):大多数对象在新生代中被创建,其中很多对象的生命周期很短。每次新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本就可以完成回收。
新生代内又分三个区:一个Eden区,两个Survivor区(一般而言),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足“晋升”条件的对象将被复制到另外一个Survivor区。对象每经历一次Minor GC,年龄加1,达到“晋升年龄阈值”后,被放到老年代,这个过程也称为“晋升”。显然,“晋升年龄阈值”的大小直接影响着对象在新生代中的停留时间,在Serial和ParNew GC两种回收器中,“晋升年龄阈值”通过参数MaxTenuringThreshold设定,默认值为15。
② 老年代(Old Generation):在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代,该区域中对象存活率高。老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。整堆包括新生代和老年代的垃圾回收称为Full GC(HotSpot VM里,除了CMS之外,其它能收集老年代的GC都会同时收集整个GC堆,包括新生代)。
③ 永久代(Perm Generation):主要存放元数据,例如Class、Method的元信息,与垃圾回收要回收的Java对象关系不大。相对于新生代和年老代来说,该区域的划分对垃圾回收影响比较小。
在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
5 方法区
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
HotSpot 虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价。仅仅是因为 HotSpot 虚拟机设计团队用永久代来实现方法区而已,这样 HotSpot 虚拟机的垃圾收集器就可以像管理 Java 堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
6 运行时常量池
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
既然运行时常量池时方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError 异常。
JDK1.7及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。
7 直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致OutOfMemoryError异常出现。
JDK1.4中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。
本机直接内存的分配不会收到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。
jvm垃圾回收的流程;哪些对象会被认为是垃圾;有一个对象A它有一个属性是B,B这个对象他又有一个属性是A,这个对象最终会不会被认为是垃圾;
允许GC之后,开始查找那些允许被回收的(两个算法)-> 开始回收(四个算法)
第一步:那些对象是垃圾:
1,引用计数法:通过对引用的遍历,找到对应的实例,让对应的实例计数加 1 ,如果引用取消,或者指向null,实例的引用减 1 。把找到的引用都遍历一遍之后,如果发现有对象实例的计数是0。那么这个对象 就是垃圾对象了。在通过垃圾回收算法对其进行 回收即可。
缺点:想想一下,有两个类,互相引用,也就是A对象的实例(也就是对象的全局变量)是一个指向B对象的引用,B对象实例是一个指向A对象的引用。那么这两个对象的引用计数,永远不可能是0 。也就不可能对其进行回收了。
2,可达性分析法:这个算法类似于树的遍历,学过数据结构的小伙伴应该会好理解。简单来说,按照一定的规则说明那些可以作为一个根节点(GC root),然后以这些根节点去访问其引用的对象,被访问的对象又会有其他对象的引用。想象一下,是不是像极了树的遍历。这个路径称作引用链,但凡是在引用链上的对象,都是可用的。注意,引用连的起始点都是GC root 哦。虽然有其他对象存在类似于引用链的结构,但是,起始点不是GC root的那一些,都是垃圾,可以被回收的。
一般情况下,都是使用的 可达性分析法去查找垃圾类实例。
GC root哪些对象会被认为是root;
GC root的查找规则:java栈中的引用,方法区中的静态属性(静态变量 + 静态常量),方法区中常量引用的对象(方法区中有个结构 叫做 常量池 ,存储的一部分是常量),本地方法(线程独占区中有个结构叫做 本地方法栈)。
jvm里面有一个存储虚拟s1和s2
年轻代里面有一个复制算法,这个就要说到
第二步:垃圾回收器算法(标记-清除、复制算法、标记-整理、分代算法)
1,标记-清除:找到垃圾类之后,标记一下。然后直接 清除即可。(算法很快)
缺点:产生空间碎片,不利于大对象的安排进去。
2,复制算法:将内存分为四块:新生代(Eden),生存代(Survivor * 2),老年代。有五种内存分配策略,讲完之后再说。类的升级流程是Eden->Survivor->老年代;
算法流程:1),先找到垃圾类,将可以使用的类移动到Survivor2,将Eden + 另一块Survivor1中的内存全部清除。
2),将新生成的类实例优先分配到Eden,分配不下时,放到Survivor2。进行GC时,将Survivor2中对象的满足一定条件(例如对象年龄达到某一个标准)的对象分配到老年代中。将本次GC存活下来的分配到Survivor1中,在清除Eden + Survivor2 。依次循环即可。
缺点:很容易发现吧,Survivor中每次都会浪费一个Survivor的内存没有使用,所以为了减少浪费,一般将Eden的内存扩大,Survivor的内存设置小一点。例如:HotSpot(HotSpot是8中的jvm默认虚拟机) 中设置的是 8 : 1 : 1;
3,标记-整理:看名字是不是感觉很熟悉,没错。跟标记-清除很像,也是直接标记。改算法使用到了前面两个算法的精华,改善了缺点。
算法流程:1),直接标记
2),集中,无缝隙的移动到一端,此时会发现,剩下的垃圾类,都会在其他地方。移动完成之后就会发现有一个边界,就是可用类跟其他空间的一个边界,下一步直接把边界以外的空间直接清除掉就可以了。
缺点:看起来很完美,但是越完美的,往往在时间上过不去。
4,分代算法:根据在哪里清除,选用算法不一样。
算法流程:1),新生代采用复制算法
2),老年代采用标记-清除算法(老年代GC很少访问,类也很少去直接分配到里面,内存碎片的可怕性就显得不那么重要了)
什么样的数据会往老年代里面迁移呢;
每次进行垃圾回收的时候,比如说当前这个对象存活下来了,计数器就会给他+1,默认的话,是当计数器达到16的时候就会放到老年代里面;
GC的作用域:
JVM内存分配原则:
1,对象优先分配到Eden区域;
2,大对象直接分配到老年区:大对象的就是,对象里面有很大数组或者很大的字符串;
3,长时间存活的对象存入老年区:就是上面复制算法里面说的那个对象升级流程;
4,动态对象年龄判定:jvm并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才可以进入老年代,如果Survivor空间中年龄相同的所有对象的总空间>=本servivor中的一半,那么年龄>=本年龄的对象可以直接进入老年区;
5,空间分配原则:简单来说,就是在发生Minor GC(在新生代进行GC)情况下,为了防止发生在Minor GC后,Eden有大量存活的对象,导致survivor不能全部存入,这时需要老年代去担保,把这些对象放入老年代,但是要确保老年要存的下。
1),再发生Minor GC之前,检查老年区的可用的连续空间是否是大于新生代(Eden)的所有对象的总空间,如果是,直接全部晋升老年代,保证Minor GC的安全;
2),如果不行,就检查HandlePromotionFailure(可以手工设定)参数时候允许担保失败,允许的话,直接分配。不能的话,发生一次full GC(或者是Major GC 在老年代进行GC)。
3),不允许担保失败,发生一次 full GC。
为什么不直接进行full GC ,因为速度慢呀。而且经常GC 也 效果不大,因为老年代都是一些长期存活的对象。
如果老年代内存也不够用了怎么办呢;
他会进行fullGC
fullGC的时候会有什么现象吗;有没有遇到到fullGC的时候影响业务的场景;
如果频繁的fullGC会出现cpu内存飙升的问题
收集器有:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(G1是目前最好的收集器)
上图是HotSpot的垃圾收集器的使用范围,HotSpot是现在主流的 jvm。
CMS收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。
工作原理:基于标记-清除算法实现的,流程分为4步:
1),初始标记(仅仅标记一下GC Root,)
2),并发标记(是一个单独的线程,一起跟用户的线程一起 运行)
3),重新标记(去标记一下在运行期间发生变化的对象)
4),并发清除(是一个单独的线程,一起跟用户的线程一起 运行)
其中,初始标记跟重新标记任然需要Stop The World。
图中的安全点,跟发动GC有关,任何一个GC收集器动作都会设计安全点。另附博客说明。
G1收集器:
目前最强收集器,强到什么地步。不需要其他收集器配合,自己就可以管理新生代跟老年代。G1是面向服务端应用的垃圾收集器,HotSpot开发团队称在未来可以替换掉JDK1.5中发布的CMS收集器。
工作流程:算法:整体采用了标记-整理算法,局部使用了复制算法。工作流程也是分为4步:
1),初始标记
2),并发标记
3),最终标记
4),筛选回收
跟CMS的工作流程差不多。因为G1还正在开发优化,在大数据上应用停顿时间以及吞吐量还有缺陷。还没有大方面的普及
jvm报错,OOM;
JVM 发生OOM的四种情况
1、Java堆溢出:heap
Java堆内存主要用来存放运行过程中所以的对象,该区域OOM异常一般会有如下错误信息;
java.lang.OutofMemoryError:Java heap space
此类错误一般通过Eclipse Memory Analyzer分析OOM时dump的内存快照就能分析出来,到底是由于程序原因导致的内存泄露,还是由于没有估计好JVM内存的大小而导致的内存溢出。
2、栈溢出:stack
栈用来存储线程的局部变量表、操作数栈、动态链接、方法出口等信息。如果请求栈的深度不足时抛出的错误会包含类似下面的信息:
java.lang.StackOverflowError
另外,由于每个线程占的内存大概为1M,因此线程的创建也需要内存空间。操作系统可用内存-Xmx-MaxPermSize即是栈可用的内存,如果申请创建的线程比较多超过剩余内存的时候,也会抛出如下类似错误:
java.lang.OutofMemoryError: unable to create new native thread
3、运行时常量溢出 constant
运行时常量保存在方法区,存放的主要是编译器生成的各种字面量和符号引用,但是运行期间也可能将新的常量放入池中,比如String类的intern方法。
如果该区域OOM,错误结果会包含类似下面的信息:
java.lang.OutofMemoryError: PermGen space
4、方法区溢出 directMemory
方法区主要存储被虚拟机加载的类信息,如类名、访问修饰符、常量池、字段描述、方法描述等。理论上在JVM启动后该区域大小应该比较稳定,但是目前很多框架,比如Spring和Hibernate等在运行过程中都会动态生成类,因此也存在OOM的风险。
如果该区域OOM,错误结果会包含类似下面的信息:
java.lang.OutofMemoryError: PermGen space
五、数据库:
接触过哪些数据库;
oracle mysql
oracle和mysql分页的区别;oracle分页的原理;
Mysql使用limit分页
select * from stu limit m, n; //m = (startPage-1)*pageSize,n = pageSize
Oracle使用rownum分页
select * from (
select rownum rn,a.* from table_name a where rownum <= x
//结束行,x = startPage*pageSize
)
where rn >= y; //起始行,y = (startPage-1)*pageSize+1
(1)>= y,<= x表示从第y行(起始行)~x行(结束行) 。
(2)rownum只能比较小于,不能比较大于,因为rownum是先查询后排序的,例如你的条件为rownum>1,当查询到第一条数据,rownum为1,则不符合条件。第2、3...类似,一直不符合条件,所以一直没有返回结果。所以查询的时候需要设置别名,然后查询完成之后再通过调用别名进行大于的判断。
数据库查询执行流程
数据库怎么优化;索引的原因;为什么要使用B+树存储索引;
为什么要使用B+树存储索引;
B+树是在B树的基础上改造,它的数据都在叶子节点,同时,叶子节点之间还加了指针形成链表
这是一个4路B+树,它的数据都在叶子节点,并且有链路相连。
为什么要这样设计呢?
B+树在数据库的索引中用到最多,数据库的select操作,有时会查找多条,B树查询需要做局部的中序遍历,可能要跨层访问,而B+树所有的数据都在叶子节点,不用跨层,同时有链表结构,只需要找到首尾,通过链表就能把所有数据查询出来
比如,找7~9,只需要在叶子节点中就能找到。
数据库优化的几个方面
1. SQL以及索引的优化
什么情况要加索引,哪些字段上适合加索引;
表的主键、外键必须有索引;
经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
经常与其他表进行连接的表,在连接字段上应该建立索引;
索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
使用索引的优缺点?
索引就像书的目录一样可以非常快速的定位到书的页面
优点:提高查询效率,没有索引的话查询数据库表会进行全表扫描
缺点:插入慢,占用硬盘空间
MySQL如何定位慢sql
步骤1:查询是否开启了慢查询
mysql> show variables like '%slow%';
mysql> show variables like '%slow%';
+---------------------------+--------------------------------+
| Variable_name | Value |
+---------------------------+--------------------------------+
| log_slow_admin_statements | OFF |
| log_slow_slave_statements | OFF |
| slow_launch_time | 2 |
| slow_query_log | ON |
| slow_query_log_file | /data/mysql/localhost-slow.log |
+---------------------------+--------------------------------+
5 rows in set (0.01 sec)
mysql>
我这里是开启了,没有开启的,直接set global slow_query_log=on;就ok了。
步骤2:设置慢查询的时间限制
mysql默认的慢查询时间是10秒,可以设置成其它的时间。
mysql> show variables like 'long_query_time';
+-----------------+-----------+
| Variable_name | Value |
+-----------------+-----------+
| long_query_time | 10.000000 |
+-----------------+-----------+
1 row in set (0.03 sec)
mysql> set long_query_time=1;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'long_query_time';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| long_query_time | 1.000000 |
+-----------------+----------+
1 row in set (0.00 sec)
mysql>
set global 只是全局session生效,重启后失效,如果需要以上配置永久生效,需要在mysql.ini(linux myf)中配置
步骤3:查看慢查询
show status like ‘slow_queries’;
它会显示慢查询sql的数目,具体的sql就在上面的Log file日志中可以看到。
mysql> show status like 'slow_queries';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| Slow_queries | 0 |
+---------------+-------+
1 row in set (0.01 sec)
mysql>
其它命令
show processlist: 查看哪些线程在运行;
show open tables:查看哪些表在使用。
慢查询分析日志
改一下慢查询配置
mysql> set long_query_time=0.1;
Query OK, 0 rows affected (0.05 sec)
mysql>
执行几条慢的SQL
mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
| 100005 |
+----------+
1 row in set (0.28 sec)
mysql> select * from users;
...
...
100005 rows in set (1.41 sec)
mysql>
mysql> select count(*) from user_address_copy;
+----------+
| count(*) |
+----------+
| 30006 |
+----------+
1 row in set (0.08 sec)
mysql> select * from user_address_copy;
...
...
30006 rows in set (0.39 sec)
mysql>
vim 打开慢查询记录的文件slow_query_log_file | /data/mysql/localhost-slow.log
vim /data/mysql/localhost-slow.log
localhost-slow.log 内容如下:
/software/mysql/bin/mysqld, Version: 5.7.24 (MySQL Community Server (GPL)). started with:
Tcp port: 3306 Unix socket: /software/mysql/mysql.sock
Time Id Command Argument
# Time: 2018-12-08T03:08:23.877322Z
# User@Host: root[root] @ localhost [] Id: 24
# Query_time: 0.551358 Lock_time: 0.000514 Rows_sent: 1 Rows_examined: 100005
use test;
SET timestamp=1544238503;
select count(*) from users;
# Time: 2018-12-08T03:09:06.038256Z
# User@Host: root[root] @ localhost [] Id: 24
# Query_time: 1.401716 Lock_time: 0.000220 Rows_sent: 100005 Rows_examined: 100005
SET timestamp=1544238546;
select * from users;
# Time: 2018-12-08T03:12:03.207302Z
# User@Host: root[root] @ localhost [] Id: 24
# Query_time: 0.395499 Lock_time: 0.000378 Rows_sent: 30006 Rows_examined: 30006
SET timestamp=1544238723;
select * from user_address_copy;
Time :日志记录的时间
User@Host:执行的用户及主机
Query_time:查询耗费时间 Lock_time 锁表时间 Rows_sent 发送给请求方的记录条数 Rows_examined 语句扫描的记录条数
SET timestamp 语句执行的时间点
select .... 执行的具体语句
慢查询日志分析工具
这里以MySQL为例,最常见的方式是,由自带的慢查询日志或者开源的慢查询系统定位到具体的出问题的SQL,然后使用explain、profile等工具来逐步调优,最后经过测试达到效果后上线。
2. 合理的数据库是设计
根据数据库三范式来进行表结构的设计。
数据库三范式:
第一范式:数据表中每个字段都必须是不可拆分的最小单元,也就是确保每一列的原子性;
第二范式:满足一范式后,表中每一列必须有唯一性,都必须依赖于主键;
第三范式:满足二范式后,表中的每一列只与主键直接相关而不是间接相关(外键也是直接相关),字段没有冗余。
分表
分表方式
水平分割(按行)、垂直分割(按列)
分表场景
A: 根据经验,mysql表数据一般达到百万级别,查询效率就会很低。
B: 一张表的某些字段值比较大并且很少使用。可以将这些字段隔离成单独一张表,通过外键关联,例如考试成绩,我们通常关注分数,不关注考试详情。
水平分表策略
按时间分表:当数据有很强的实效性,例如微博的数据,可以按月分割。
按区间分表:例如用户表 1到一百万用一张表,一百万到两百万用一张表。
hash分表:通过一个原始目标id或者是名称按照一定的hash算法计算出数据存储的表名。
3. 系统配置的优化
4. 硬件优化
缓存
搜索引擎
例如:solr,elasticsearch
Mycat原理
MyCAT是一款由阿里Cobar演变而来的用于支持数据库读写分离、分片的分布式中间件。MyCAT可不但支持Oracle、MSSQL、MYSQL、PG、DB2关系型数据库,同时也支持MongoDB等非关系型数据库。
MyCAT主要是通过对SQL的拦截,然后经过一定规则的分片解析、路由分析、读写分离分析、缓存分析等,然后将SQL发给后端真实的数据块,并将返回的结果做适当处理返回给客户端。
mycat会用吗;平时会自己弄分库分表吗;什么时候需要分库什么时候需要分表;
你们现在的数据量大吗;是怎么进行拆分存储的;数据库是分库的对吧;他的分库的规则是什么样的;
读锁和写锁的实现方式,加了读锁,其他资源能不能读,加了写锁其他资源能不能读;
数据库的并发操作会带来许多问题,比如丢失更新、不可重复读、读脏数据(幽灵数据)等等,为避免该类问题的产生,我们采用了封锁机制,一般DBMS进行并发控制的方法是封锁机制和事务机制。
最基本的封锁类型有两种:排它锁(Exclusive Locks,X锁)和共享锁(Share Locks,S锁):
- 排它锁也称独占锁、写锁或X锁,若sessionA获得某数据表的排他锁权限,那么sessionA只能对该表进行读取或修改,其他session既不能读取也不能修改该表,更不能对该表加任何类型的锁,直到sessionA释放排它锁权限。加锁方式:lock tables tablename write;
- 共享锁也称读锁或S锁,若sessionA获得某数据表的共享锁权限,那么任何session(包括sessionA)只能对该表进行读取,不能修改该表,其他session可以对该数据表继续加S锁但不能加X锁,直到sessionA释放共享锁权限。加锁方式:set tables tablename read;
六、redis:
redis是什么语言开发的;
Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写。
redis底层的实现原理有去研究过吗;为什么redis的性能能达到这么快呢;
官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路I/O复用模型,非阻塞IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
多路 I/O 复用模型:
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
原因是因为他底层的话是用的是这种io多路复用的这种机制像我们linux底下的这种epoll 就是根据事件来触发的 当你有这种事件的时候 就会通知我的 那我就会进行一个事件的触发 而且还防止了我们多线程之间的一个上下文切换或者锁的一些竞争的一些情况 从而他的性能会比较高一点 但是我们对其禁止使用一些严重影响他性能的一些命令 例如说是keys啊 这种 我们会给他重命名掉 不让他使用
Redis五种数据结构及操作如下:
对redis来说,所有的key(键)都是字符串。
1.String 字符串类型
是redis中最基本的数据类型,一个key对应一个value。
String类型是二进制安全的,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。
常用命令:
基础命令
- set :设置存储在给定键中的值
- get:获取存储在给定键中的值
- del:删除存储在给定键中的值
字符串
- INCR:返回增加后键的值
- DECR:返回删除后键的值
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> del hello
(integer) 1
127.0.0.1:6379> get hello
(nil)
127.0.0.1:6379> get counter
"2"
127.0.0.1:6379> incr counter
(integer) 3
127.0.0.1:6379> get counter
"3"
127.0.0.1:6379> incrby counter 100
(integer) 103
127.0.0.1:6379> get counter
"103"
127.0.0.1:6379> decr counter
(integer) 102
127.0.0.1:6379> get counter
"102"
实战场景:
1.缓存: 经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis作为缓存层,mysql做持久化层,降低mysql的读写压力。
2.计数器:redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。
3.session:常见方案spring session + redis实现session共享,
2.Hash (哈希)
是一个Mapmap,指值本身又是一种键值对结构,如 value={{field1,value1},......fieldN,valueN}}
使用:所有hash的命令都是 h 开头的
- hget:获取存储在哈希表中指定字段的值
- hset:获取存储在哈希表中指定字段的值
- hdel:删除一个或多个哈希表字段
- hgetall:获取在哈希表中指定 key 的所有字段和值
127.0.0.1:6379> hset user name1 hao
(integer) 1
127.0.0.1:6379> hset user email1 hao@163
(integer) 1
127.0.0.1:6379> hgetall user
1) "name1"
2) "hao"
3) "email1"
4) "hao@163"
127.0.0.1:6379> hget user user
(nil)
127.0.0.1:6379> hget user name1
"hao"
127.0.0.1:6379> hset user name2 xiaohao
(integer) 1
127.0.0.1:6379> hset user email2 xiaohao@163
(integer) 1
127.0.0.1:6379> hgetall user
1) "name1"
2) "hao"
3) "email1"
4) "hao@163"
5) "name2"
6) "xiaohao"
7) "email2"
8) "xiaohao@163"
实战场景:
1.缓存: 能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。
3.链表
List 说白了就是链表(redis 使用双端链表实现的 List),是有序的,value可以重复,可以通过下标取出对应的value值,左右两边都能进行插入和删除数据。
使用列表的技巧
- lpush+lpop=Stack(栈)
- lpush+rpop=Queue(队列)
- lpush+ltrim=Capped Collection(有限集合)
- lpush+brpop=Message Queue(消息队列)
操作命令
Lpush——先进后出,在列表头部插入元素
Rpush——先进先出,在列表的尾部插入元素
Lrange——出栈,根据索引,获取列表元素
Lpop——左边出栈,获取列表的第一个元素
Rpop——右边出栈,获取列表的最后一个元素
Lindex——根据索引,取出元素
Llen——链表长度,元素个数
Lrem——根据key,删除n个value
Ltrim——根据索引,删除指定元素
Rpoplpush——出栈,入栈
Lset——根据index,设置value
Linsert before——根据value,在之前插入值
Linsert after——根据value,在之后插入值
注意
出栈,该元素在链表中,就不存在了
左边,默认为列表的头部,索引小的一方
右边,默认为列表的尾部,索引大的一方
使用:
127.0.0.1:6379> lpush mylist 1 2 ll ls mem
(integer) 5
127.0.0.1:6379> lrange mylist 0 -1
1) "mem"
2) "ls"
3) "ll"
4) "2"
5) "1"
127.0.0.1:6379>
实战场景:
1.timeline:例如微博的时间轴,有人发布微博,用lpush加入时间轴,展示新的列表信息。
4.Set 集合
集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中 1. 不允许有重复的元素,2.集合中的元素是无序的,不能通过索引下标获取元素,3.支持集合间的操作,可以取多个集合取交集、并集、差集。
使用:命令都是以s开头的 sset 、srem、scard、smembers、sismember
127.0.0.1:6379> sadd myset hao hao1 xiaohao hao
(integer) 3
127.0.0.1:6379> SMEMBERS myset
1) "xiaohao"
2) "hao1"
3) "hao"
127.0.0.1:6379> SISMEMBER myset hao
(integer) 1
实战场景;
1.标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。
2.点赞,或点踩,收藏等,可以放到set中实现
5.zset 有序集合
有序集合和集合有着必然的联系,保留了集合不能有重复成员的特性,区别是,有序集合中的元素是可以排序的,它给每个元素设置一个分数,作为排序的依据。
(有序集合中的元素不可以重复,但是score 分数 可以重复,就和一个班里的同学学号不能重复,但考试成绩可以相同)。
使用: 有序集合的命令都是 以 z 开头 zadd 、 zrange、 zscore
127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao
(integer) 2
127.0.0.1:6379> ZRANGE myscoreset 0 -1
1) "xiaohao"
2) "hao"
127.0.0.1:6379> ZSCORE myscoreset hao
"100"
实战场景:
1.排行榜:有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。
redis里面有个string,一个字符串类型的值能存储最大容量是多少;你知道他的底层是怎么实现的;
最大容量是512M。
对于字符串类型,其做出了改进,是一种基于动态字符串sds实现,redis作为数据库,查询必然多,修改也会有一定多,sds解决了C语言字符串动态扩展的不方便,以及查询长度操作从O(n)变为了O(1)。
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS 遵循 C 字符串以空字符结尾的惯例, 保存空字符的 1 字节空间不计算在 SDS 的 len 属性里面, 并且为空字符分配额外的 1 字节空间, 以及添加空字符到字符串末尾等操作都是由 SDS 函数自动完成的, 所以这个空字符对于 SDS 的使用者来说是完全透明的。
遵循空字符结尾这一惯例的好处是, SDS 可以直接重用一部分 C 字符串函数库里面的函数。
其free属性是代表buf数组没有被利用的空间数,便于sds的空间分配策略。
这样的设计也打破了C语言字符串会自动认为’\0’为分隔符号,但是sds不会,所以可以保存的字符串中间存在空字符
通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略
redis的keys为什么影响性能,redis时间复杂度是O(n)的命令;
keys时间复杂度是O(n),flushdb、flushall这类命令我们可以配置redis.conf
禁用这些命令
使用scan替代keys命令
语法:
scan cursor [MATCH pattern] [COUNT count]
案例:
scan 0 match report:* count 10
1) "3932160"
2) 1) "report:12360412"
2) "report:12749274"
scan 第一个参数是游标,表示从游标开始
返回的第一行是游标,第二行是匹配到的数据,
如果第一行返回0,表示没有更多数据,否则下次使用scan时,就要用第一行返回的值作为scan的游标
一般用redis都做什么;
- 性能:
我们在碰到需要执行耗时特别久,且结果不频繁变动的SQL,就特别适合将运行结果放入缓存,这样,后面的请求就去缓存中读取,请求使得能够迅速响应。
- 并发:
在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用的的Redis的做一个缓冲操作,让请求先访问到的Redis的的,而不是直接访问数据库。
- 分布式锁等其他功能
Redis的的的还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件代替,
什么是redis的缓存穿透;什么是缓存雪崩;怎么解决这些问题;缓存穿透不通过ip过滤,最简单的方式怎么解决;
- 缓存处理流程
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。
- 缓存穿透
描述:
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
解决方案:
接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
- 缓存击穿
描述:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
解决方案:
设置热点数据永远不过期。
加互斥锁,互斥锁参考代码如下:
说明:
1)缓存中有数据,直接走上述代码13行后就返回结果了
2)缓存中没有数据,第1个进入的线程,获取锁并从数据库去取数据,没释放锁之前,其他并行进入的线程会等待100ms,再重新去缓存取数据。这样就防止都去数据库重复取数据,重复往缓存中更新数据情况出现。
3)当然这是简化处理,理论上如果能根据key值加锁就更好了,就是线程A从数据库取key1的数据并不妨碍线程B取key2的数据,上面代码明显做不到这点。
- 缓存雪崩
描述:
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
设置热点数据永远不过期。
redis 和 memcached 的区别:
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!
redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
Redis 哈希槽的概念
Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384个哈希槽,每个 key通过 CRC16校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash槽。
redis 设置过期时间;
定期删除+惰性删除。
定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!
如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? redis 内存淘汰机制。
redis 提供 6种数据淘汰策略:
redis有做集群吗;怎么做集群的;你在项目中还遇到什么问题吗;
参考:https://wwwblogs/L-Test/p/11626124.html
集群的三种模式:
一、主从同步/复制
redis有俩种复制模式:Rdb和aof
redis持久化问题;会有数据损失吗,开启aof的持久化吗;
redis提供2种持久化方案。rdb和aof
redis有自己默认的持久化方案 (Rdb 方案)
Rdb(默认):在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF:append only file。以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
优缺点:
相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。
由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。不过生产环境其实更多都是二者结合使用的。
常用配置
RDB持久化配置
Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
AOF持久化配置
在Redis的配置文件中存在三种同步方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件。
appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync no #从不同步。高效但是数据不会被持久化。
(1)首先从节点根据当前状态,决定如何调用psync命令:
- 如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量复制;
- 如果从节点之前执行了slaveof,则发送命令为psync <runid> <offset>,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。
(2)主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制:
- 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制;
- 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
- 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC <runid> <offset>,表示要进行全量复制,其中runid表示主节点当前的runid,offset表示主节点当前的offset,从节点保存这两个值,以备使用。
二、哨兵模式
Redis Sentinel是Redis高可用的实现方案。Sentinel是一个管理多个Redis实例的工具,它可以实现对Redis的监控、通知、自动故障转移。
Redis Sentinel的工作流程
由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求 。如下图:
Sentinel负责监控集群中的所有主、从Redis,当发现主故障时,Sentinel会在所有的从中选一个成为新的主。并且会把其余的从变为新主的从。同时那台有问题的旧主也会变为新主的从,也就是说当旧的主即使恢复时,并不会恢复原来的主身份,而是作为新主的一个从。
在Redis高可用架构中,Sentinel往往不是只有一个,而是有3个或者以上。目的是为了让其更加可靠,毕竟主和从切换角色这个过程还是蛮复杂的。
-
主观失效
SDOWN(subjectively down),直接翻译的为”主观”失效,即当前sentinel实例认为某个redis服务为”不可用”状态.
-
客观失效
ODOWN(objectively down),直接翻译为”客观”失效,即多个sentinel实例都认为master处于”SDOWN”状态,那么此时master将处于ODOWN,ODOWN可以简单理解为master已经被集群确定为”不可用”,将会开启failover
三、Cluster 集群
使用虚拟节点,比如说有3主、3从6台服务器。将16384个槽均匀的分配到三台主服务器(master)上,从服务器(slave)复制主服务器上的所有数据,存储key时,通过CRC16(key)算法得到32位的哈希值在和16384取模,计算出槽的编号。因为redis是非幂等性(无状态的,每次请求服务和请求别的是不一样的)的服务,必须要搭从节点。
部署集群:借助 redis-tri.rb 工具可以快速的部署集群,如果本机没有该命令行需要自行安装,只需要执行/redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 就可以成功创建集群。
后面如果扩容或者删除从节点,对我们的数据的影响都不会很大,因为主要是扩容节点的,因为redis是无中心化的,我们只需要去连接一台服务器的redis,如果key不在目前这台服务器上,他就可以帮你重定向到另一台服务器,对数据进行操作,我们在操作数据的时候,必须使用“-c”来表示我们操作的是集群,而不是在单服务器上进行操作的。
如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
参考:
https://www.jianshu/p/8bddd381de06
cps是多少;一秒钟处理多少;
分布式锁的实现方式;有哪些常见的数据分布式算法,比如说现在部署了3个redis,里面的内存是不一样的,怎么保证有一个内存回落到固定的一个redis实例上面的分布式算法;一致性hash算法相较于普通hash算法有什么优势;
你对sentinel hystrix有用过吗;你们微服务有用sentinel 吗
因为他需要监控redis里面 因为redis属于非幂等性的集群 那我搭建完集群之后我还需要搭建主从复制 如果说我们不用sentinel 的话肯定会对我们数据造成一定的影响的
sentinel 是你们自己搭的吗直接从ali看了 就搭了吗?hystrix你们也用过吗
说一下你们的熔断是怎么做的
当我们另一个服务不能用的时候他会调用failback给他返回一个页面
你认为什么时候是不可用的
当高并发或者是服务降级的时候 你这个系统可能不是很重要这个时候就会出发这种failback
sentinel hystrix的区别
你们用redis主要是用的缓存这一块吗说一下你怎么用redis实现分布式锁的
它提供了这种命令像nx ex 也就是说是给他指定的一个变量然后当他为1的时候肯定是一个加锁的状态那么如果说是我操作完之后的话 我是要对他进行一个释放的 然后别的会通过这个命令来判断如果当前存在而且为1的话他肯定是拿不到锁的
springboot整合redis,注解方式使用 Redis 缓存
使用缓存有两个前置步骤
-
在
pom.xml
引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在启动类上加注解
@EnableCaching
@SpringBootApplication @EnableCaching public class SellApplication { public static void main(String[] args) { SpringApplication.run(SellApplication.class, args); } }
常用的注解有以下几个
@Cacheable
查询和添加缓存 eg:@Cacheable(cacheNames = "product", key = "123")
,属性如下图
用于查询和添加缓存,第一次查询的时候返回该方法返回值,并向 Redis 服务器保存数据。
以后调用该方法先从 Redis 中查是否有数据,如果有直接返回 Redis 缓存的数据,而不执行方法里的代码。如果没有则正常执行方法体中的代码。
value 或 cacheNames 属性做键,key 属性则可以看作为 value 的子键, 一个 value 可以有多个 key 组成不同值存在 Redis 服务器。
验证了下,value 和 cacheNames 的作用是一样的,都是标识主键。两个属性不能同时定义,只能定义一个,否则会报错。
condition 和 unless 是条件,后面会讲用法。其他的几个属性不常用,其实我也不知道怎么用…
@CachePut
更新 Redis 中对应键的值。属性和@Cacheable
相同 eg:@CachePut(cacheNames = "prodcut", key = "123")
@CacheEvict
删除 Redis 中对应键的值。eg:@CacheEvict(cacheNames = "prodcut", key = "123")
3.1 添加缓存
在需要加缓存的方法上添加注解 @Cacheable(cacheNames = "product", key = "123")
,
cacheNames
和 key
都必须填,如果不填 key
,默认的 key
是当前的方法名,更新缓存时会因为方法名不同而更新失败。
如在订单列表上加缓存
@RequestMapping(value = "/list", method = RequestMethod.GET)
@Cacheable(cacheNames = "product", key = "123")
public ResultVO list() {
// 1.查询所有上架商品
List<ProductInfo> productInfoList = productInfoService.findUpAll();
// 2.查询类目(一次性查询)
//用 java8 的特性获取到上架商品的所有类型
List<Integer> categoryTypes = productInfoList.stream().map(e -> e.getCategoryType()).collect(Collectors.toList());
List<ProductCategory> productCategoryList = categoryService.findByCategoryTypeIn(categoryTypes);
List<ProductVO> productVOList = new ArrayList<>();
//数据拼装
for (ProductCategory category : productCategoryList) {
ProductVO productVO = new ProductVO();
//属性拷贝
BeanUtils.copyProperties(category, productVO);
//把类型匹配的商品添加进去
List<ProductInfoVO> productInfoVOList = new ArrayList<>();
for (ProductInfo productInfo : productInfoList) {
if (productInfo.getCategoryType().equals(category.getCategoryType())) {
ProductInfoVO productInfoVO = new ProductInfoVO();
BeanUtils.copyProperties(productInfo, productInfoVO);
productInfoVOList.add(productInfoVO);
}
}
productVO.setProductInfoVOList(productInfoVOList);
productVOList.add(productVO);
}
return ResultVOUtils.success(productVOList);
}
可能会报如下错误
对象未序列化。让对象实现 Serializable
方法即可
@Data
public class ProductVO implements Serializable {
private static final long serialVersionUID = 961235512220891746L;
@JsonProperty("name")
private String categoryName;
@JsonProperty("type")
private Integer categoryType;
@JsonProperty("foods")
private List<ProductInfoVO> productInfoVOList ;
}
生成唯一的 id 在 IDEA 里有一个插件:GenerateSerialVersionUID
比较方便。
重启项目访问订单列表,在 rdm 里查看 Redis 缓存,有 product::123
说明缓存成功。
3.2 更新缓存
在需要更新缓存的方法上加注解: @CachePut(cacheNames = "prodcut", key = "123")
注意
cacheNames
和key
要跟@Cacheable()
里的一致,才会正确更新。@CachePut()
和@Cacheable()
注解的方法返回值要一致
3.3 删除缓存
在需要删除缓存的方法上加注解:@CacheEvict(cacheNames = "prodcut", key = "123")
,执行完这个方法之后会将 Redis 中对应的记录删除。
3.4 其他常用功能
-
cacheNames
也可以统一写在类上面,@CacheConfig(cacheNames = "product")
,具体的方法上就不用写啦。@CacheConfig(cacheNames = "product") public class BuyerOrderController { @PostMapping("/cancel") @CachePut(key = "456") public ResultVO cancel(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ buyerService.cancelOrder(openid, orderId); return ResultVOUtils.success(); } }
-
Key 也可以动态设置为方法的参数
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
如果参数是个对象,也可以设置对象的某个属性为 key。比如其中一个参数是 user 对象,key 可以写成
key="#user.id"
-
缓存还可以设置条件。
设置当 openid 的长度大于3时才缓存
@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
还可以指定
unless
即条件不成立时缓存。#result
代表返回值,意思是当返回码不等于 0 时不缓存,也就是等于 0 时才缓存。@GetMapping("/detail") @Cacheable(cacheNames = "prodcut", key = "#openid", condition = "#openid.length > 3", unless = "#result.code != 0") public ResultVO<OrderDTO> detail(@RequestParam("openid") String openid, @RequestParam("orderId") String orderId){ OrderDTO orderDTO = buyerService.findOrderOne(openid, orderId); return ResultVOUtils.success(orderDTO); }
七、MQ:
首先我们说说为什么要使用队列,什么情况下才会使用队列?
实时性要求不高,且比较耗时的任务,是队列的最佳应用场景。
比如说我在某网站注册一个账号,当我的信息入库注册成功后,网站需要发送一封激活邮件,让我激活账号,而这个发邮件的操作并不是需要实时响应的,没有必要卡在那个注册界面,等待邮件发送成功,再说发送邮件本来就是一个耗时的操作(需要调用第三方smtp服务器),此时,选择消息队列去处理。注册完成,我只要向队列投递一个消息,消息的内容中包含我要发送邮件的一些设置,以及发送时间,重试次数等消息属性。这里的投递操作(可以是入库,写入缓存等)是要消息进入一个实体的队列。其中应该有一进程(消费者)一直在后台运行,他不断的去轮训队列中的消息(按照时间正序,队列是先进先出),看有没有达到执行条件的,如果有就取出一条,根据消息配置,执行任务,如果成功,则销毁这条消息,继续轮训,如果失败,则重试,知道达到重试次数。这时用户已经收到注册成功的提示,但是已经去做其他事了,邮件也来了,用户点击邮件,注册成功。这就是消息队列的一个典型应用。
再说一个场景,点赞,这个在高并发的情况下,很容易造成数据库连接数占满,到时整个网站响应缓慢,才是就是想到要解决数据库的压力问题,一般就是两种方案,一是提高数据库本身的能力(如增加连接数,读写分离等),但是数据库总是有极限的,到达了极限是没有办法在提升了的,此时就要考虑第二种方案,释放数据库的压力,将压力转移到缓存里面。就拿实际的点赞来说吧,用户的点赞请求到来,我只是将点赞请求投递到消息队列里面,后续的点赞请求可以将消息合并,即只更新点赞数,不产生新的任务,此时有个进行再不断的轮训消息队列,将点赞消息消耗,并将值更新到数据库里面,这样就有效的降低了数据库的压力,因为在缓存层将数个数据库更新请求合并成一个,大大提高了效率,降低了负载。
Java消息服务(Java Message Service,JMS)应用程序接口是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
点对点与发布订阅最初是由JMS定义的。这两种模式主要区别或解决的问题就是发送到队列的消息能否重复消费(多订阅)
消息队列的两种模式及kafka、activemq、ribbitmq的俩种模式区别
JMS规范目前支持两种消息模型:点对点(point to point, queue)和发布/订阅(publish/subscribe,topic)。
1.1、点对点:Queue,不可重复消费
消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。
消息被消费以后,queue中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
1.2、发布/订阅:Topic,可以重复消费
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
支持订阅组的发布订阅模式:
发布订阅模式下,当发布者消息量很大时,显然单个订阅者的处理能力是不足的。实际上现实场景中是多个订阅者节点组成一个订阅组负载均衡消费topic消息即分组订阅,这样订阅者很容易实现消费能力线性扩展。可以看成是一个topic下有多个Queue,每个Queue是点对点的方式,Queue之间是发布订阅方式。
2、区别
2.1、点对点模式
生产者发送一条消息到queue,一个queue可以有很多消费者,但是一个消息只能被一个消费者接受,当没有消费者可用时,这个消息会被保存直到有 一个可用的消费者,所以Queue实现了一个可靠的负载均衡。
2.2、发布订阅模式
发布者发送到topic的消息,只有订阅了topic的订阅者才会收到消息。topic实现了发布和订阅,当你发布一个消息,所有订阅这个topic的服务都能得到这个消息,所以从1到N个订阅者都能得到这个消息的拷贝。
3、流行模型比较
传统企业型消息队列ActiveMQ遵循了JMS规范,实现了点对点和发布订阅模型,但其他流行的消息队列RabbitMQ、Kafka并没有遵循JMS规范。
3.1、RabbitMQ
RabbitMQ实现了AQMP协议,AQMP协议定义了消息路由规则和方式。生产端通过路由规则发送消息到不同queue,消费端根据queue名称消费消息。
RabbitMQ既支持内存队列也支持持久化队列,消费端为推模型,消费状态和订阅关系由服务端负责维护,消息消费完后立即删除,不保留历史消息。
(1)点对点
生产端发送一条消息通过路由投递到Queue,只有一个消费者能消费到。
(2)多订阅
当RabbitMQ需要支持多订阅时,发布者发送的消息通过路由同时写到多个Queue,不同订阅组消费不同的Queue。所以支持多订阅时,消息会多个拷贝。
3.2、Kafka
Kafka只支持消息持久化,消费端为拉模型,消费状态和订阅关系由客户端端负责维护,消息消费完后不会立即删除,会保留历史消息。因此支持多订阅时,消息只会存储一份就可以了。但是可能产生重复消费的情况。
(1)点对点&多订阅
发布者生产一条消息到topic中,不同订阅组消费此消息。
kafka、activemq、ribbitmq、rocketmq的区别
1.单机吞吐量
ActiveMQ:万级,吞吐量比RocketMQ和Kafka要低一个数量级。
RabbitMQ:万级,吞吐量比RocketMQ和Kafka要低一个数量级。
RocketMQ:10万级,RocketMQ也是可以支撑高吞吐的一种MQ。
Kafka:10万级,这是Kafka最大的优点,就是吞吐量高;一般配合大数据类的系统来进行实时数据基数按、日志采集等场景。
2.Topic数量对吞吐量的影响
ActiveMQ:
RabbitMQ:
RocketMQ:topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降;这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic。
Kafka:topic从几十个到几百个的时候,吞吐量会大幅度下降所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源。
3.时效性
ActiveMQ:ms级;
RabbitMQ:微妙级,这是Rabbitmq的一大特点,延迟是最低的;
RocketMQ:ms级;
Kafka:在ms级内。
4.可用性
ActiveMQ:高,基于主从架构实现高可用性;
RabbitMQ:高,基于主从架构实现高可用性;
RocketMQ:非常高,分布式架构;
Kafka:非常高,Kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用。
5.消息可靠性
ActiveMQ:有较低的概率丢失数据;
RabbitMQ:
RocketMQ:参数经过优化配置,可以做到0丢失;
Kafka:参数经过优化配置,可以做到0丢失;
6.功能支持
ActiveMQ:MQ领域的功能及其完备;
RabbitMQ:基于erlang语言开发,并发性能极其好,延时很低;
RocketMQ:MQ功能较为完善,还是分布式的,扩展性能好;
Kafka:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准;
ActiveMQ社区也不是很活跃。 Kafka 是业内标准的,绝对没问题,社区活跃度很高
activemq因为他是用java开发的,不支持跨语言的这种形式。ribbitmq的话他主要是用在事务级别的,对大数据的处理性能比较弱一点。
ActiveMQ:
1.非常成熟,功能强大,在业内大量的公司以及项目都有应用;
2.偶尔会有较低概率丢失消息;
3.现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ5.x维护越来越少,几个月才发布一个版本;
4.确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用。
RabbitMQ:
1.erlang语言开发的,性能极其好,延时很低;
2.吞吐量到万级,MQ功能比较完备;
3.开源提供管理界面非常棒,用起来很好用;
4.社区很活跃,几乎每个月发布几个版本;
5.国内一些互联网公司近几年用RabbitMQ比较多一些;
6.问题是RabbitMQ的吞吐量也会低一些,这是应为它做的实现机制比较重;
7.而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,很难去看懂源码,公司对这个东西的掌控很弱,基本只能依赖于开源社区的快速维护和修复bug;
8.而且Rabbitmq集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和掌控。
RocketMQ:
1.接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障;
2.日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景;
3.而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控;
4.社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码;
5.还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ挺好的;
Kafka:
1.kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展;
2.同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量;
3.而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略;
4.这个特性天然适合大数据实时计算以及日志收集。
消息会有消费失败的情况吗;消费失败系统怎么处理;
kafka的底层是怎么实现的;怎么可以达到这么高的吞吐量
他的话 底层也是使用的多路复用的这种形式 就是我作为一个数据的提供者我去发消息 那么他的话把topic分成一个个postion然后提高并发量所以他的一个性能的话 其实也跟这个有很大的关系 其实postion底层的话 数据的存储也是根据.log文件 跟日志也比较容易混淆 里面的话使用sengment对底层存储分段就是它里面有一个偏移量offset 然后发送他kafka服务器brock上 我们要进行一个存储 我们会根据我们的业务场景进行一个设计来保证我们数据丢失的一个情况 他的话我们平常的话也会设置成为1的一个情况 1的话就是我当前的一个lander接收到就行了 -1的话可能是这个lrp里面所有的从节点都接受到这个数据之后我们才给反馈 客户这边的话发数据也会分这种buffer 我们会给buffer定义一个长度 还有就是分批的一个处理 我给他每次手机达到一个batch 也会去设计一个批处理的量 达到这个数据量之后每一个时间段我给他发送 想这些都可以提高性能
你们用kafka有遇到什么问题吗;
kafka几个基本的概念:
broker: 消息处理结点,多个broker组成kafka集群。
topic: 一类消息,如page view,click行为等。
partition: topic的物理分组,每个partition都是一个有序队列。
replica:partition 的副本,保障 partition 的高可用。
producer: 产生信息的主体,可以是服务器日志信息等。
consumer: 消费producer产生话题消息的主体。
Consumer group:high-level consumer API 中,每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 的一个 Consumer 消费,但可以被多个 consumer group 消费。
segment: 多个大小相等的段组成了一个partition。
offset: 一个连续的用于定位被追加到分区的每一个消息的序列号,最大值为64位的long大小,19位数字字符长度。
massage: kafka中最基本的传递对象,有固定格式。
zookeeper:kafka 通过 zookeeper 来存储集群的 meta 信息。
controller:kafka 集群中的其中一个服务器,用来进行 leader election 以及 各种 failover。
partition、segment、offset都是为topic服务的,每个topic可以分为多个partition,一个partition相当于一个大目录,每个partition下面有多个大小相等的segment文件,这个segment是由message组成的,而每一个的segment不一定由大小相等的message组成。segment大小及生命周期在server.properties文件中配置。offset用于定位位于段里的唯一消息,这个offset用于定位partition中的唯一性。
kafka如何保证发的消息是有序的
客户端获取到的每一个postion的数据 其实都是有序的 但是我们客户端这边可能说是并发的这种情况那我们会给他加一个内存优化之类的
方案一,kafka topic 只设置一个partition分区
方案二,producer将消息发送到指定partition分区
解析:
方案一:kafka默认保证同一个partition分区内的消息是有序的,则可以设置topic只使用一个分区,这样消息就是全局有序,缺点是只能被consumer group里的一个消费者消费,降低了性能,不适用高并发的情况
方案二:既然kafka默认保证同一个partition分区内的消息是有序的,则producer可以在发送消息时可以指定需要保证顺序的几条消息发送到同一个分区,这样消费者消费时,消息就是有序。
kafka和activemq和ribbitmq的区别
activemq因为他是用java开发的,不支持跨语言的这种形式。ribbitmq的话他主要是用在事务级别的,对大数据的处理性能比较弱一点。
如果消费端异常,后续的操作流程是什么;
死信队列如果消息失败可以放;mq的更新频路是怎么设置的;
八、jdk1.8有什么新特性;
【1】Lambda 表达式:将函数式编程引入了Java。Lambda 允许把函数作为一个方法的参数,或者把代码看成数据。
String[] atp = {"Nadal", "Djokovic", "Wawrinka"};
List<String> players = Arrays.asList(atp);
// 以前的循环方式
for (String player : players) {
System.out.print(player + "; ");
}
// 使用 lambda 表达式以及函数操作(functional operation)
players.forEach((player) -> System.out.print(player + "; "));
【2】接口的默认方法与静态方法:我们可以在接口中定义默认方法,使用 default关键字,并提供默认的实现。所有实现这个接口的类都会接受默认方法的实现,除非子类提供的自己的实现。
public interface DefaultFunctionInterface {
default String defaultFunction() {
return "default function";
}
}
//我们还可以在接口中定义静态方法,使用static关键字,也可以提供实现。
public interface StaticFunctionInterface {
static String staticFunction() {
return "static function";
}
}
【3】方法引用:通常与 Lambda表达式联合使用,可以直接引用已有 Java类或对象的方法。
【4】重复注解:在 Java 5中使用注解有一个限制,即相同的注解在同一位置只能声明一次。Java 8 引入重复注解,这样相同的注解在同一地方也可以声明多次。重复注解机制本身需要用@Repeatable注解。Java 8在编译器层做了优化,相同注解会以集合的方式保存,因此底层的原理并没有变化。
【5】扩展注解的支持:Java 8 扩展了注解的上下文,几乎可以为任何东西添加注解,包括局部变量、泛型类、父类与接口的实现,连方法的异常也能添加注解。
【6】Optional:Java 8 引入 Optional 类来防止空指针异常,Optional 类最先是由 Google 的 Guava项目引入的。Optional类实际上是个容器:它可以保存类型T的值,或者保存null。使用Optional类我们就不用显式进行空指针检查了。
Optional<String> str = Optional.of("test");
//1、判断str是否为null,如果不为null,则为true,否则为false
if (str.isPresent()) {
//get用于获取变量的值,当变量不存在的时候会抛出NoSuchElementException,如果不能确定变量一定存在值,则不推荐使用
str.get();
}
【7】Stream:Stream API 是把真正的函数式编程风格引入到 Java中。其实简单来说可以把 Stream理解为 MapReduce,当然Google 的 MapReduce的灵感也是来自函数式编程。她其实是一连串支持连续、并行聚集操作的元素。从语法上看,也很像 Linux的管道、或者链式编程,代码写起来简洁明了,非常酷帅!
【8】Date/Time API (JSR 310):Java 8 新的 Date-Time API (JSR 310)受Joda-Time的影响,提供了新的 java.time包,可以用来替代 java.util.Date和 java.util.Calendar。一般会用到 Clock、LocaleDate、LocalTime、LocaleDateTime、ZonedDateTime、Duration这些类,对于时间日期的改进还是非常不错的。
【9】JavaScript 引擎 Nashorn:Nashorn允许在 JVM上开发运行 JavaScript应用,允许 Java与 JavaScript相互调用。
【10】Base64:在 Java 8中,Base64 编码成为了Java类库的标准。Base64 类同时还提供了对URL、MIME友好的编码器与解码器。
函数式编程;Foreach 是怎么退出循环的;
java8中使用foreach,但是不是lamada表达式写法,可以正常使用break或者return,可以直接跳出循环.
lamada表达式:不能使用break,会提示错误;使用return,会跳出当前循环,继续下一次循环,作用类似continue;直接跳出循环有三种方式:
1.可以通过抛异常的方法:throw new RuntimeException();
2.anyMatch()里接收一个返回值为boolean类型的表达式,只要返回true就会终止循环
list.stream().anyMatch(s -> { System.out.println("do something"); System.out.println("s=" + s); return s.equals("b"); });
3.采用类似的思路可以使用filter()方法,思路是一样的,其中findAny表示只要找到满足的条件时停止。
list.stream().filter(s -> { System.out.println("s=" + s); return s.equals("b"); }).findAny();
sleep()和wait()的区别;
sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,要让b线程睡觉要在b的代码中调用sleep。
sleep不出让系统资源;wait是进入线程等待池等待,出让系统资源,其他线程可以占用CPU。一般wait不会加时间限制,因为如果wait线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll唤醒等待池中的所有线程,才会进入就绪队列等待OS分配系统资源。sleep(milliseconds)可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()强行打断。
Thread.Sleep(0)的作用是“触发操作系统立刻重新进行一次CPU竞争”。
使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
synchronized(x){
x.notify()
//或者wait()
}
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
九、forName与loadClass的区别
java类装载过程分为3步:
1:加载
Jvm把class文件字节码加载到内存中,并将这些静态数据装换成运行时数据区中方法区的类型数据,在运行时数据区堆中生成一个代表这个类
的java.lang.Class对象,作为方法区类数据的访问入口。
*释:方法区不仅仅是存放方法,它存放的是类的类型信息。
2:链接:执行下面的校验、准备和解析步骤,其中解析步骤是可选的
a:校验:检查加载的class文件的正确性和安全性
b:准备:为类变量分配存储空间并设置类变量初始值,类变量随类型信息存放在方法区中,生命周期很长,使用不当和容易造成内存泄漏。
*释:类变量就是static变量;初始值指的是类变量类型的默认值而不是实际要赋的值
c:解析:jvm将常量池内的符号引用转换为直接引用
3:初始化:执行类变量赋值和静态代码块
因为他们都能在运行时对任意一个类,都能够知道该类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。
- Classloder.loaderClass(String name)
其实该方法内部调用的是:Classloder. loadClass(name, false)
方法:Classloder. loadClass(String name, boolean resolve)
1:参数name代表类的全限定类名
2:参数resolve代表是否解析,resolve为true是解析该类
- Class.forName(String name)
其实该方法内部调用的是:Class.forName(className, true, ClassLoader.getClassLoader(caller))
方法:Class.forName0(String name, boolean initialize, ClassLoader loader)
参数name代表全限定类名
参数initialize表示是否初始化该类,为true是初始化该类
参数loader 对应的类加载器
- 两者最大的区别
Class.forName得到的class是已经初始化完成的
Classloder.loaderClass得到的class是还没有链接的
- 怎么使用
有些情况是只需要知道这个类的存在而不需要初始化的情况使用Classloder.loaderClass,而有些时候又必须执行初始化就选择Class.forName
十、spring:
spring 事务的传播机制
spring 对事务的控制,是使用 aop 切面实现的,我们不用关心事务的开始,提交 ,回滚,只需要在方法上加 @Transactional
注解,这时候就有问题了。
- 场景一: serviceA 方法调用了 serviceB 方法,但两个方法都有事务,这个时候如果 serviceB 方法异常,是让 serviceB 方法提交,还是两个一起回滚。
- 场景二:serviceA 方法调用了 serviceB 方法,但是只有 serviceA 方法加了事务,是否把 serviceB 也加入 serviceA 的事务,如果 serviceB 异常,是否回滚 serviceA 。
- 场景三:serviceA 方法调用了 serviceB 方法,两者都有事务,serviceB 已经正常执行完,但 serviceA 异常,是否需要回滚 serviceB 的数据。
因为 spring 是使用 aop 来代理事务控制 ,是针对于接口或类的,所以在同一个 service 类中两个方法的调用,传播机制是不生效的
传播机制类型(七种)
一般用得比较多的是 PROPAGATION_REQUIRED
, REQUIRES_NEW
;
下面的类型都是针对于被调用方法来说的,理解起来要想象成两个 service 方法的调用才可以。
PROPAGATION_REQUIRED (默认)propagation required
-
支持当前事务,如果当前没有事务,则新建事务
-
如果当前存在事务,则加入当前事务,合并成一个事务
REQUIRES_NEW requires new
-
新建事务,如果当前存在事务,则把当前事务挂起
-
这个方法会独立提交事务,不受调用者的事务影响,父级异常,它也是正常提交
NESTED
-
如果当前存在事务,它将会成为父级事务的一个子事务,方法结束后并没有提交,只有等父事务结束才提交
-
如果当前没有事务,则新建事务
-
如果它异常,父级可以捕获它的异常而不进行回滚,正常提交
-
但如果父级异常,它必然回滚,这就是和
REQUIRES_NEW
的区别
SUPPORTS
-
如果当前存在事务,则加入事务
-
如果当前不存在事务,则以非事务方式运行,这个和不写没区别
NOT_SUPPORTED
-
以非事务方式运行
-
如果当前存在事务,则把当前事务挂起
MANDATORY
-
如果当前存在事务,则运行在当前事务中
-
如果当前无事务,则抛出异常,也即父级方法必须有事务
NEVER
-
以非事务方式运行,如果当前存在事务,则抛出异常,即父级方法必须无事务
spring中bean的作用域有哪些;
scope配置项有5个属性,用于描述不同的作用域。
① singleton
使用该属性定义Bean时,IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。
② prototype
使用该属性定义Bean时,IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。
③ request
该属性仅对HTTP请求产生作用,使用该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。
④ session
该属性仅用于HTTP Session,同一个Session共享一个Bean实例。不同Session使用不同的实例。
⑤ global-session
该属性仅用于HTTP Session,同session作用域不同的是,所有的Session共享一个Bean实例。
其中比较常用的是singleton和prototype两种作用域。对于singleton作用域的Bean,每次请求该Bean都将获得相同的实例。容器负责跟踪Bean实例的状态,负责维护Bean实例的生命周期行为;如果一个Bean被设置成prototype作用域,程序每次请求该id的Bean,Spring都会新建一个Bean实例,然后返回给程序。在这种情况下,Spring容器仅仅使用new 关键字创建Bean实例,一旦创建成功,容器不在跟踪实例,也不会维护Bean实例的状态。
如果不指定Bean的作用域,Spring默认使用singleton作用域。Java在创建Java实例时,需要进行内存申请;销毁实例时,需要完成垃圾回收,这些工作都会导致系统开销的增加。因此,prototype作用域Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,可以重复使用。因此,除非必要,否则尽量避免将Bean被设置成prototype作用域。
spring框架的优点;
1、非侵入式设计
Spring是一种非侵入式(non-invasive)框架,它可以使应用程序代码对框架的依赖最小化。
2、方便解耦、简化开发
Spring就是一个大工厂,可以将所有对象的创建和依赖关系的维护工作都交给Spring容器的管理,大大的降低了组件之间的耦合性。
3、支持AOP
Spring提供了对AOP的支持,它允许将一些通用任务,如安全、事物、日志等进行集中式处理,从而提高了程序的复用性。
4、支持声明式事务处理
只需要通过配置就可以完成对事物的管理,而无须手动编程。
5、方便程序的测试
Spring提供了对Junit4的支持,可以通过注解方便的测试Spring程序。
6、方便集成各种优秀框架
Spring不排斥各种优秀的开源框架,其内部提供了对各种优秀框架(如Struts、Hibernate、MyBatis、Quartz等)的直接支持。
7、降低Jave EE API的使用难度。
Spring对Java EE开发中非常难用的一些API(如JDBC、JavaMail等),都提供了封装,使这些API应用难度大大降低。
ApplicationContext和beanfactory的区别
BeanFacotry是spring中比较原始的Factory。
ApplicationContext接口,它由BeanFactory接口派生而来,因而提供BeanFactory所有的功能。并且他还实现了Resource这些是一个更高级的容器。
BeanFactroy只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化,这样,我们就不能发现一些存在的Spring的配置问题。
ApplicationContext则相反,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误。
十一、SpringCloud 和 Dubbo
SpringCloud 和 Dubbo 有哪些区别
Dubbo | SpringCloud | |
服务注册中心 | Zookeeper | Eureka |
服务调用方式 | RPC | REST API |
服务监控 | Dubbo-monitor | Spring BootAdmin |
断路器 | 不完善 | Spring Cloud Netflix Hystrix |
服务网关 | 无 | Spring Cloud Netflix Zuul |
分布式配置 | 无 | Spring Cloud Config |
服务跟踪 | 无 | Spring Cloud Sleuth |
消息总线 | 无 | Spring Cloud Bus |
数据流 | 无 | Spring Cloud Stream |
批量任务 | 无 | Spring Cloud Task |
最大区别:SpringCloud 抛弃了 Dubbo 的 RPC 通信,采用的是基于 HTTP 的 REST 方式。总体来说,两者各有优势。虽说后者牺牲了服务调用的功能,但也避免了上面提到的原生 RPC 带来的问题。而且 REST 相比 RPC 更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的依赖,这在强调快速演化的微服务环境下,显得更加合适。
品牌机与组装机的区别:很明显 SpringCloud 比 Dubbo 的功能更强大,覆盖面更广,而且能够与 SpringFramework、SpringBoot、SpringData、SpringBatch 等其他 Spring 项目完美融合,这些对于微服务至关重要。使用 Dubbo 构建的微服务架构就像组装电脑、各环节我们选择自由度高,但是最终可能会因为内存质量而影响整体,但对于高手这也就不是问题。而SpringCloud 就像品牌机,在 Spring Source 的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性。在面临微服务基础框架选型时Dubbo与SpringCloud只能二选一。
SpringBoot 和 SpringCloud 请你谈谈对他们的理解;
【1】SpringBoot 专注于快速方便的开发单个个体微服务。
【2】SpringCloud 是关注全局的微服务协调、整理、治理的框架,它将 SpringBoot 开发的单体整合并管理起来。
【3】SpringBoot 可以离开 SpringCloud 独立使用开发项目,但是 SpringCloud 离不开 SpringBoot,属于依赖关系。
SpringCloud的组件有什么了解,比如网关;什么情况下会熔断;还有用过什么组件吗,负载均衡是怎么做的;配置config用的是什么;
Spring的Threadlocal(本地线程)一般在哪些场景会用到;
你对Springcloud和dubbo的有理解吗;dubbo的服务提供方和注册方能说一下吗;
什么是服务熔断?什么是服务降级?
熔断机制:应对雪崩效应的一种微服务链路保护机制。当查出链路中的某个微服务不可用或者响应时间太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回“错误”的响应信息。当检测到该节点微服务调用响应正常时则恢复调用链路。在SpringCloud 框架里熔断机制通过 Hystrix 实现,Hystrix 会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内调用20次,如果失败,就会启动熔断机制。熔断机制的注解是 @HystrixCommand
服务降级:一般是从整体负荷考虑。就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback 回调,返回一个缺省值。这样做,虽然水平下降,但好歹可用,比直接挂掉强。
微服务的优缺点是什么?说下你在项目开发中碰到的问题
优点:【1】每个服务足够内聚,足够小,代码容易理解这样能聚焦一个指定的业务功能或业务需求。
【2】开发简单,开发效率提高,一个服务可能就是专一的只干一件事。
【3】微服务能够被小团队开发,这个团队可以是2到5个开发人员组成。
【4】微服务是松耦合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的。
【5】微服务能使用不同的语言开发。
【6】易于第三方集成,微服务允许使用容易且灵活的方式集成自动部署,通过持续集成集成工具,如Jenkins、Hudson等。
【7】微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果。无需通过合作体现价值。
【8】微服务允许你融合最新技术。
【9】微服务知识业务逻辑代码,不会和 HTML 和 CSS 其他界面组件混合。
【10】每个微服务都有自己的存储能力,可以有自己的数据库,也可以由统一的数据库。
缺点:【1】开发人员要处理分布式系统的复杂性。
【2】多服务运维难度,随着服务的增加,运维的压力也在增加。
【3】系统部署依赖。
【4】服务间通讯成本。
【5】数据一致性。
【6】系统集成测试。
【7】性能监控.....
你所知道的微服务技术栈有哪些?请举例一二
微服务的技术栈(各项功能的实现所使用的技术)具体如下:
微服务条目 | 落地的技术 |
服务开发 | SpringBoot、Spring、SpringMVC |
服务配置管理 | Netfilx公司的Archaius、阿里的Diamond等 |
服务注册与发现 | Eureka、Consul、Zookeeper |
服务调用 | RPC、Rest、gRPC |
服务熔断器 | Hystrix、Envoy等 |
负载均衡 | Nginx、Ribbon |
服务接口调用(客户端调用服务的简化工具) | Feign等 |
消息队列 | Kafka、RabbitMQ、ActiveMQ等 |
服务配置中心管理 | SpringCloudConfig、Chef等 |
服务路由(API网关) | Zuul等 |
服务监控 | Zabbix、Naggios、Metrics、Spectator等 |
全链路追踪 | Zipkin、Brave、Dapper等 |
服务部署 | Docker、OpenStack、Kubernetes等 |
数据流操作开发包 | SpringCloud Stream |
事件消息总线 | Spring Cloud Bus |
Eureka 和 Zookeeper 都可以提供服务注册与发现的功能,请说说两个的区别?
Zookeeper 保证了CP(C:一致性,P:分区容错性),Eureka保证了AP(A:高可用)
【1】当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的信息,但不能容忍直接 down 掉不可用。也就是说,服务注册功能对高可用性要求比较高,但 zk 会出现这样一种情况,当 master 节点因为网络故障与其他节点失去联系时,剩余节点会重新选 leader。问题在于,选取 leader 时间过长,30 ~ 120s,且选取期间 zk 集群都不可用,这样就会导致选取期间注册服务瘫痪。在云部署的环境下,因网络问题使得 zk 集群失去 master 节点是较大概率会发生的事,虽然服务能够恢复,但是漫长的选取时间导致的注册长期不可用是不能容忍的。
【2】Eureka 保证了可用性,Eureka 各个节点是平等的,挂掉几个节点不会影响正常节点的工作,剩余的节点仍然可以提供注册和查询服务。而 Eureka 的客户端向某个 Eureka 注册或发生连接失败,则会自动切换到其他节点,只要有一台 Eureka 还在,就能保证注册服务可用,只是查到的信息可能不是最新的。除此之外,Eureka 还有自我保护机制,如果在15分钟内超过85%的节点没有正常的心跳,那么 Eureka 就认为客户端与注册中心发生了网络故障,此时会出现以下几种情况:
①、Eureka 不在从注册列表中移除因为长时间没有收到心跳而应该过期的服务。
②、Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上(即保证当前节点仍然可用)
③、当网络稳定时,当前实例中新的注册信息会被同步到其他节点。
因此,Eureka 可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像 Zookeeper 那样使整个微服务瘫痪。
微服务与 SOA 服务的区别
【1】服务拆分粒度:SOA 首先要解决的是异构应用的服务化;微服务强调的是服务拆分尽可能小,最好是独立的原子服务。
【2】服务依赖:传统的 SOA 服务,由于需要重用已有的资产,存在大量服务间依赖;微服务的设计理念是服务自治、功能单一独立,避免依赖其他服务产生耦合,耦合会带来更高的复杂度。
【3】服务规模:传统 SOA 服务粒度比较大,多数会采用将多个服务合并打成 war 包的方案,因此服务实例数比较有限;微服务强调尽可能拆分,同时很多服务独立部署,这将导致服务规模急剧膨胀,对服务治理和运维带来新的挑战。
【4】架构差异:微服务化之后,服务数量的激增会引起架构质量属性的变化,例如企业继承总线 ESB(实总线)逐渐被 P2P 的虚拟总线替换;为了保证高性能、低延迟,需要高性能的分布式服务框架保证微服务架构的实施。
【5】服务治理:传统基于 SOA Governance 的静态治理转型为服务运行态微服务治理、实时生效。
【6】敏捷交付:服务由小研发团队服务微服务设计、开发、测试、部署、线上治理、灰度发布和下线,运维整个生命周期支持,实现真正的DevOps。
十二、java基础:
==和eaquls的区别
==:对于基本类型来说 ,==比较两个基本类型的值是否相等,对于引用类型来说,==比较的是内个引用类型的内存地址。
equals 说明:equals 用来比较的是两个对象的内容是否相等,由于所有的类都是继承自 java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖,调用的仍然是 Object类中的方法,而 Object中的 equals方法返回的却是 ==的判断。 【重写 equals一般是要重写 hashcode方法的,首先 equals与 hashcode间的关系如下】:
1)、如果两个对象相同(即用 equals比较返回 true),那么它们的 hashCode值一定要相同;
2)、如果两个对象的 hashCode相同,它们并不一定相同(即用 equals比较返回 false) ;
比如说两个字符串的 hashcode相同,但是这两个字符串可以是不同的字符串,对象也是同理。
比如说我现在有俩个Interger比较,结果为什么;
public void test() {
Integer a1 = 1000;
Integer b1 = 1000;
Integer a2 = 100;
Integer b2 = 100;
Integer a3 = new Integer(100);
Integer b3 = new Integer(100);
System.out.println("第一组:"+ (a1 == b1));
System.out.println("第二组:"+ (a2 == b2));
System.out.println("第三组:"+ (a3 == b3));
}
结果:
第一组:false
第二组:true
第三组:false
在值域为 [-128,127]之间,用==符号来比较Integer的值,是相等的。是因为方法区的常量池中有这些数据,我们知道在Java的对象是引用的,所以当用Integer 声明初始化变量时,会先判断所赋值的大小是否在-128到127之间,若在,则利用静态缓存中的空间并且返回对应cache数组中对应引用,存放到运行栈中,而不再重新开辟内存。
所以第一个为true第二个为false
第三个重新创建对象,就会重在堆内存开辟空间,所以地址不同为false。
String,StringBuffer,StringBuilder的区别
String是不可变类,String对象一旦被创建,其值就不能改变,而 StringBuffer是可变类,当对象被创建后仍然可以对其值进行修改。由于 String是不可变类,因此适合在被共享的场合中使用,而当一个字符串经常被修改时,最好使用 StringBuffer来实现。如果用 String保存一个经常修改的字符串时,字符串被修改时会比 StringBuffer多很多附加的操作,同时生成很多无用的对象,由于这些无用的对象会被垃圾回收器来回收,因此会影响程序的性能。在规模小的项目里面这个影响很小,但是在一个规模大的项目里面,这会对程序的运行效率带来很大的影响。
String 与 StringBuffer实例化时存在区别:String 可以通过构造函数的方式(String s = new String("hello"))和直接赋值(String s="world")两种方式。而 StringBuffer只能使用构造函数进行赋值(StringBuffer sb = new StringBuffer("hello"))。
为什么修改的时候不建议使用string
String 字符串修改实现的原理:当 String修改字符串时,先创建一个 StringBuffer,其次调用 append()方法,最后调用toString()方法把结果返回。实例如下(下述过程比使用 StringBuffer多了一些附加操作,同时也生成了一些临时的对象,从而导致程序执行效率下降):
String s = "HELLO";
s+="WORLD";
//以上代码 实现底层 如下
StringBuffer sb = new StringBuffer(s);
sb.append("WORLD");
s=sb.toString();
StringBuilder:可以被修改的字符串,他与 StringBuffer类似,都是字符缓冲区,但 StringBuilder不是线程安全的,如果只是单线程访问时可以使用 StringBuilder,当有多个线程访问时,最好使用线程安全的 StringBuffer。因为 StringBuffer必要时会对这些方法进行同步,所以任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
【5】在执行效率方面:StringBuilder 最高,StringBuffer 次之,String 最低,鉴于以上情况,一般使用数据量较小的情况下,优先使用 String;如果单线程下使用大量数据,应优先使用 StringBuilder类;如果是在多线程下操作大量数据,应优先考虑StringBuffer类。
数组和list的相互转换;
List转Array:
ArrayList<String> list=new ArrayList<String>();
String[] strings = new String[list.size()];
list.toArray(strings);
数组转List
String[] s = {"a","b","c"};
List list = java.util.Arrays.asList(s);
Collections提供的第二种排序方法sort(List<T> list, Comparator<? super T> c)
package core.java.collection.collections;
public class Students {
private int age;
private int score;
......
}
public static void main(String[] args) {
List<Students> students = new ArrayList<Students>();
students.add(new Students(23, 100));
...
Collections.sort(students, new Comparator<Students>() {
@Override
public int compare(Students o1, Students o2) {
int i = o1.getScore() - o2.getScore();
if(i == 0){
return o1.getAge() - o2.getAge();
}
return i;
}
});
for(Students stu : students){
System.out.println("score:" + stu.getScore() + ":age" + stu.getAge());
}
}
深拷贝和浅拷贝怎么做。
深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设B复制了A,修改A的时候,看B是否发生变化:
浅拷贝(shallowCopy):A变B变,只是增加了一个指针指向已存在的内存地址;
深拷贝(deepCopy):A变B不变,增加了一个指针并且申请了一个新内存,使这个增加的指针指向这个新的内存;
浅拷贝(shallowCopy):实现对象拷贝的类,需要实现 Cloneable
接口,并覆写 clone()
方法。
return super.clone();
深拷贝(deepCopy):在 Student
的 clone()
方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。
Student student = (Student) super.clone();
student.subject = (Subject) subject.clone();
return student;
浅拷贝实例:
实现对象拷贝的类,需要实现 Cloneable
接口,并覆写 clone()
方法。
public class Subject {
private String name;
set get constructor...
@Override
public String toString() {
return "[Subject: " + this.hashCode() + ",name:" + name + "]";
}
}
public class Student implements Cloneable {
//引用类型
private Subject subject;
//基础数据类型
private String name;
private int age;
set get ...
@Override
public String toString() {
return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name +
",age:" + age + "]";
}
/**
* 重写clone()方法
* @return
*/
@Override
public Object clone() {
//浅拷贝
try {
// 直接调用父类的clone()方法
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}
public class ShallowCopy {
public static void main(String[] args) {
Subject subject = new Subject("yuwen");
Student studentA = new Student();
studentA.setSubject(subject);
studentA.setName("Lynn");
studentA.setAge(20);
Student studentB = (Student) studentA.clone();
studentB.setName("Lily");
studentB.setAge(18);
Subject subjectB = studentB.getSubject();
subjectB.setName("lishi");
System.out.println("studentA:" + studentA.toString());
System.out.println("studentB:" + studentB.toString());
}
}
输出的结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:lishi],name:Lynn,age:20]
studentB:[Student: 1956725890,subject:[Subject: 1163157884,name:lishi],name:Lily,age:18]
由输出的结果可见,通过 studentA.clone()
拷贝对象后得到的 studentB
,和 studentA
是两个不同的对象。studentA
和 studentB
的基础数据类型的修改互不影响,而引用类型 subject
修改后是会有影响的。
浅拷贝和对象拷贝的区别
public static void main(String[] args) {
Subject subject = new Subject("yuwen");
Student studentA = new Student();
studentA.setSubject(subject);
studentA.setName("Lynn");
studentA.setAge(20);
Student studentB = studentA;
studentB.setName("Lily");
studentB.setAge(18);
Subject subjectB = studentB.getSubject();
subjectB.setName("lishi");
System.out.println("studentA:" + studentA.toString());
System.out.println("studentB:" + studentB.toString());
}
这里把 Student studentB = (Student) studentA.clone()
换成了 Student studentB = studentA
。
输出的结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:lishi],name:Lily,age:18]
studentB:[Student: 460141958,subject:[Subject: 1163157884,name:lishi],name:Lily,age:18]
可见,对象拷贝后没有生成新的对象,二者的对象地址是一样的;而浅拷贝的对象地址是不一样的。
深拷贝
1. 深拷贝介绍
通过上面的例子可以看到,浅拷贝会带来数据安全方面的隐患,例如我们只是想修改了 studentB
的 subject
,但是 studentA
的 subject
也被修改了,因为它们都是指向的同一个地址。所以,此种情况下,我们需要用到深拷贝。
深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。
2. 深拷贝特点
(1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,所以是直接将属性值赋值给新的对象。基础类型的拷贝,其中一个对象修改该值,不会影响另外一个(和浅拷贝一样)。
(2) 对于引用类型,比如数组或者类对象,深拷贝会新建一个对象空间,然后拷贝里面的内容,所以它们指向了不同的内存空间。改变其中一个,不会对另外一个也产生影响。
(3) 对于有多层对象的,每个对象都需要实现 Cloneable
并重写 clone()
方法,进而实现了对象的串行层层拷贝。
(4) 深拷贝相比于浅拷贝速度较慢并且花销较大。
结构图如下:
深拷贝图
3. 深拷贝的实现
对于 Student
的引用类型的成员变量 Subject
,需要实现 Cloneable
并重写 clone()
方法。
public class Subject implements Cloneable {
private String name;
public Subject(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
//Subject 如果也有引用类型的成员属性,也应该和 Student 类一样实现
return super.clone();
}
@Override
public String toString() {
return "[Subject: " + this.hashCode() + ",name:" + name + "]";
}
}
在 Student
的 clone()
方法中,需要拿到拷贝自己后产生的新的对象,然后对新的对象的引用类型再调用拷贝操作,实现对引用类型成员变量的深拷贝。
public class Student implements Cloneable {
//引用类型
private Subject subject;
//基础数据类型
private String name;
private int age;
public Subject getSubject() {
return subject;
}
public void setSubject(Subject subject) {
this.subject = subject;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* 重写clone()方法
* @return
*/
@Override
public Object clone() {
//深拷贝
try {
// 直接调用父类的clone()方法
Student student = (Student) super.clone();
student.subject = (Subject) subject.clone();
return student;
} catch (CloneNotSupportedException e) {
return null;
}
}
@Override
public String toString() {
return "[Student: " + this.hashCode() + ",subject:" + subject + ",name:" + name + ",age:" + age + "]";
}
}
一样的使用方式
public class ShallowCopy {
public static void main(String[] args) {
Subject subject = new Subject("yuwen");
Student studentA = new Student();
studentA.setSubject(subject);
studentA.setName("Lynn");
studentA.setAge(20);
Student studentB = (Student) studentA.clone();
studentB.setName("Lily");
studentB.setAge(18);
Subject subjectB = studentB.getSubject();
subjectB.setName("lishi");
System.out.println("studentA:" + studentA.toString());
System.out.println("studentB:" + studentB.toString());
}
}
输出结果:
studentA:[Student: 460141958,subject:[Subject: 1163157884,name:yuwen],name:Lynn,age:20]
studentB:[Student: 1956725890,subject:[Subject: 356573597,name:lishi],name:Lily,age:18]
由输出结果可见,深拷贝后,不管是基础数据类型还是引用类型的成员变量,修改其值都不会相互造成影响。
Java异常Error和Exception的区别;常见的Error和Exception;Error一般怎么报错;CheckedException,RuntimeException的区别
Error 和 Exception的区别,CheckedException,RuntimeException的区别
【1】Error:当程序发生不可控的错误时,通常做法是通知用户并中止程序的执行。与异常不同的是 Error及其子类的对象不应被抛出。Error 是 Throwable的子类,代表编译时间和系统错误,用于指示合理的应用程序不应该试图捕获的严重问题。Error由Java 虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。
【2】Exception:一般分为 Checked异常和 Runtime异常,所有 RuntimeException类及其子类的实例被称为 Runtime异常,不属于该范畴的异常则被称为 CheckedException。
1)、Checked 异常:只有 java语言提供了 Checked异常,Java 认为 Checked异常都是可以被处理的异常,所以 Java程序必须显示处理 Checked异常。如果程序没有处理 Checked异常,该程序在编译时就会发生错误无法编译。这体现了 Java的设计哲学:没有完善错误处理的代码根本没有机会被执行。对 Checked异常处理方法有两种:①、当前方法知道如何处理该异常,则用try-catch块来处理该异常。②、当前方法不知道如何处理,则在定义该方法是声明抛出该异常。我们比较熟悉的Checked异常有:
● Java.lang.ClassNotFoundException
● Java.lang.NoSuchMetodException
● Java.io.IOException
2)、RuntimeException:Runtime 如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。我们比较熟悉的 RumtimeException类的子类有:
● Java.lang.ArithmeticException
● Java.lang.ArrayStoreExcetpion
● Java.lang.ClassCastException
● Java.lang.IndexOutOfBoundsException
● Java.lang.NullPointerException
列出5个运行时异常
● ClassCastException (类转换异常)
● IllegalArgumentException (非法参数异常)
● IndexOutOfBoundsException (下标越界异常)
● NullPointerException (空指针异常)
● ArithmeticException (算术运算异常)
● OutOfMemoryError (内存不足)
● StackOverflowError (堆栈溢出)
● ClassNotFoundException (找不到类异常)
● InterruptedException (终止异常)
本文标签: 面试题
版权声明:本文标题:面试题整理 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.elefans.com/xitong/1728336363a1154809.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论