java面试
java基础知识
spring boot 启动过程
binglog日志
java面向对象有哪些特征
- 封装:增加代码的复用性
- 继承:增加代码的复用性
- 多态:增加代码的可移植性,健壮性,灵活性
- 面向对象编程是利用类和对象编程的一种思想。万物可归类,类是对于世界事物的高度抽象,不同的事物之间有不同的关系,一个类自身与外界的封装关系,一个父类和子类的继承关系,一个类和多个类的多态关系。万物皆对象,对象是具体的世界事物,面向对象的三大特征封装,继承,多态。
- 封装,封装说明一个类行为和属性与其他类的关系,低耦合,高内聚;继承是父类和子类的关系,多态说的是类与类的关系。封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据。对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法。属性的封装:使用者只能通过事先定制好的方法来访问数据,可以方便地加入逻辑控制,限制对属性的不合理操作;方法的封装:使用者按照既定的方式调用方法,不必关心方法的内部实现,便于使用;便于修改,增强代码的可维护性;
- 继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力。继承避免了对一般类和特殊类之间共同特征进行的重复描述,通过继承可以清晰地表达每一项共同特征所适应的概念范围,在一般类中定义的属性和操作适应于这个类本身以及它以下的每一层特殊类的全部对象。运用继承原则使得系统模型比较简练也比较清晰。
- 封装和继承最后归结于多态,多态指的是类和类的关系,两个类由继承关系,存在有方法的重写,故而可以调用时有父类引用指向子类对象。多态必备三个要素:继承,重写,父类引用指向子类对象。
java中的引用类型
-
强引用,有指向对象,该对象不会被回收
-
软引用,空间够随便分配,如果有强引用且空间不够,会回收软引用,创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象
-
弱引用,只要遇到gc,都会被回收 WeakReference,Java源码中的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
-
虚引用,虚引用通过PhantomReference类实现,虚应用完全类似于没有引用,无法get,给垃圾回收用的,处理堆外内存。但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue。对象销毁前的一些操作,比如说资源释放等。Object.finalize()虽然也可以做这类动作,但是这个方式即不安全又低效
-
堆外内存,jvm之外的内存,不归jvm管
ThreadLocal
TreadLocal 每个线程的值私有,其他线程访问不到,在Java的多线程并发执行过程中,为了保证多个线程对变量的安全访问,可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。
ThreadLocal使用场景为用来解决数据库连接、Session管理,多线程等。 比如spring的@Transaction,访问数据库,为了保持原子性,必须用同一个线程,使用TreadLocal。
原理:每个线程会获取自己的map,在自己的map中存数据,map的Entry的key用弱引用指向对象,这样的话当原来的强引用置空之后,该弱引用会被直接回收,不会引起key的内存泄漏。但不能解决value的内存泄漏。
HashMap和HashTable的区别是什么
- HashTable线程同步,HashTable会加synchronized关键字,效率更低, HashMap非线程同步。
- HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值。
- HashTable使用Enumeration, HashMap使用Iterator
- HashTable中hash数组的默认大小是11,增加方式的old*2+1, HashMap中hash数组的默认大小是16,增长方式是2的指数倍。
- hashtable继承与Dictionary类, hashmap继承自AbstractMap类
HashMap有哪些线程安全的方式
HashMap不是线程安全的,往往在写程序时需要通过一些方法来回避.其实JDK原生的提供了2种方法让HashMap支持线程安全.
-
方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的.这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现.
-
方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.
-
方法一特点:
通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装,封装的关键点有2处,1)使用了经典的synchronized来进行互斥,2)使用了代理模式new了一个新的类,这个类同样实现了Map接口.在Hashmap上面,synchronized锁住的是对象,所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒.优点:代码实现十分简单,一看就懂缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差. -
方法二特点:
重新写了HashMap,比较大的改变有如下几点.使用了新的锁机制,把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能,使用的是NonfairSync.这个特性调用CAS指令来确保原子性与互斥性.当如果多个线程恰好操作到同一个segment上面,那么只会有一个线程得到运行。
优点:需要互斥的代码段比较少,性能会比较好.ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好.缺点:代码繁琐
HashMap在扩容上有哪些优化
- 7之前每次扩容要rehash,重新hash,效率较慢。
- 8以后每次扩容2倍,满足2的n次方,只需要看原来的hash值新增的bit是0还是1就可以,是0的话索引不变,是1的话变成“原索引+原来的空间大小”
HashMap的底层数据结构,如何进行扩容的?
底层
1.7 数组+链表
1.8 数组+链表,链表长度>8变成红黑树
扩容机制:
初始值为16,负载因子为0.75,阈值为负载因子*容量
resize()方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize()方法进行扩容。
每次扩容,容量都是之前的两倍
扩容时有个判断e.hash & oldCap是否为零,也就是相当于hash值对数组长度的取余操作,若等于0,则位置不变,若等于1,位置变为原位置加旧容量。
ConcurrentHashMap如何实现线程安全?size()方法是加锁的吗?如何实现的?
如何实现线程安全?
JDK1.7和JDK1.8在实现线程安全上略有不同
JDK1.7采用了分段锁的机制,当一个线程占用锁时,会锁住一个Segment对象,不会影响其他Segment对象。
JDK1.8则是采用了CAS和synchronize的方式来保证线程安全。
size()方法是加锁的吗?如何实现的?
这个问题本质是ConcurrentHashMap是并发操作的,所在在计算size时,可能还会进行并发地插入数据,ConcurrentHashMap是如何解决这个问题的?
在JDK1.7会先统计两次,如果两次结果一致表示值就是当前ConcurrentHashMap的大小,如果两次不一样,则会对所有的segment都进行加锁,统计一个准确的值。
1.8,首先会CAS地更新baseCount的值,如果存在并发,CAS失败的线程则会进行方法中,后面会执行到fullAddCount()方法,该方法就是在初始化counterCells, 这也解释了为什么在 sumCount()中通过baseCount和遍历counterCells统计sum,所以在JDK1,8中size()是不加锁的
高并发中的集合有哪些问题
第一代线程安全集合类Vector. Hashtable
是怎么保证线程安排的:使用synchronized修饰方法
缺点:效率低下
第二代线程非安全集合类ArrayList, HashMap线程不安全,但是性能好,用来替代Vector, Hashtable
使用ArrayList, HashMap,需要线程安全怎么办呢?
使用 Collections.synchronizedList(list); Collections.synchronizedMap(m);
底层使用synchronized代码块锁虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解为稍有提高吧。毕竟进方法本身就要分配资源的
第三代线程安全集合类
在大量并发情况下如何提高集合的效率和安全呢?
java.util.concurrent.*
ConcurrentHashMap:
CopyOnWriteArrayList :
CopyOnWriteArrayset:
注意 不是CopyOnWriteHashSet*
底层大都采用Lock锁(1.8的ConcurrentHashMap不使用Lock锁),保证安全的同时,性能也很高。
jdk1.8的新特性
接口的默认方法
lambda表达式
函数式编程
java抽象类和接口有什么区别
相同:
1.不能够实例化
2.可以将抽象类和接口类型作为引用类型
3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
不同:抽象类:1
.抽象类中可以定义构造器
2.可以有抽象方法和具体方法
3.接口中的成员全都是public的
4.抽象类中可以定义成员变量
5.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
6.抽象类中可以包含静态方法
7.一个类只能继承一个抽象类
接口:
1.接口中不能定义构造器
2.方法全部都是抽象方法
3.抽象类中的成员可以是 private,默认、protected, public
4.接口中定义的成员变量实际上都是常量
5.接口中不能有静态方法
6一个类可以实现多个接口
抽象类描述抽象概念,一般是实际存在的概念,这种建议用抽象类
接口描述的是一个共同事务的相同特征,特征用接口
hashcode 和 equals 如何使用
equals()源自于java.lang.Object,该方法用来简单验证两个对象的相等性。Object类中定义的默认实现只检查两个对象的对象引用,以验证它们的相等性。通过重写该方法,可以自定义验证对象相等新的规则,如果你使用ORM处理一些对象的话,你要确保在hashCode()和equals()对象中使用getter和setter而不是直接引用成员变量
hashCode()源自于java.lang.Object,该方法用于获取给定对象的唯一的整数(散列码)。当这个对象需要存储在哈希表这样的数据结构时,这个整数用于确定桶的位置。默认情况下,对象的hashCode()方法返回对象所在内存地址的整数表示。hashCode()是HashTable,HashMap和HashSet使用的。默认的, Object类的hashCode()方法返回这个对象存储的内存地址的编号。
hash散列算法,使得在hash表中查找一个记录速度变O(1).每个记录都有自己的hashcode,散列算法按照hashcode把记录放置在合适的位置,在查找一个记录,首先先通过hashcode快速定位记录的位置.然后再通过equals来比较是否相等。如果hashcode没找到,则不equal,元素不存在于哈希表中;即使找到了,也只需执行hashcode相同的几个元素的equal,如果不equal,还是不存在哈希表中。
Java代理的几种方式
第一种:静态代理,只能静态的代理某些类或者某些方法,不推荐使用,功能比较弱,但是编码简单
真实对象和代理对象都要实现同一个接口,代理对象要代理真实角色
好处,代理对象可以做很多真实对象做不了的事情,真实对象专注做自己的事情
第二种:动态代理,包含Proxy代理和CGLIB动态代理
静态代理会为每一个业务增强都提供一个代理类, 由代理类来创建代理对象, 而动态代理并不存在代理类, 代理对象直接由代理生成工具动态生成.
JDK动态代理基于拦截器和反射来实现,JDK代理不需要第三方库支持,只需要JDK环境就可以进行代理。JDK动态代理是基于接口的方式,换句话来说就是代理类和目标类都实现同一个接口,那么代理类和目标类的方法名就一样了
CGLib动态代理是代理类去继承目标类,然后重写其中目标类的方法啊,这样也可以保证代理类拥有目标类的同名方法;
JDK动态代理必须要有接口, 但如果要代理一个没有接口的类该怎么办呢? 这时我们可以使用CGLIB动态代理. CGLIB动态代理的原理是生成目标类的子类, 这个子类对象就是代理对象, 代理对象是被增强过的.Cglib是无法代理final修饰的方法的,因为final类型的方法无法继承。
什么是代理模式呢?
代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
Java中==和equals的区别
equals 和==最大的区别是一个是方法一个是运算符。
==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
equals():用来比较方法两个对象的内容是否相等。注意: equals方法不能用于基本数据类型的变量,如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。
java中异常处理的机制
try,catch,finally异常捕获
java重载和重写有哪些区别
重载overload:同一个类中方法名相同参数不同
重写override:子类父类中方法相同参数相同,抛出异常要小于等于原来的,父类的权限修饰符低于子类,父类的返回值类型大于子类
String StringBuffer StringBuilder
String不可变
StringBuffer线程安全,效率慢
StringBuilder线程不安全,效率快
Java平台提供了两种类型的字符串: String 和StringBuffer/StringBuilder,它们都可以储存和操作字符串,区别如下。
- String 是只读字符串,也就意味着String引用的字符串内容是不能被改变的。初学者可能会有这样的误解
String str = "abc";
str = "bcd";
如上,字符串str明明是可以改变的呀!其实不然, str仅仅是一个引用对象,它指向一个字符串对象"abc"。第二行代码的含义是让str重新指向了一个新的字符串"bcd"对象,而"abc"对象并没有任何改变,只不过该对象已经成为一个不可及对象罢了。
2) StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改。
3) StringBuilder 是Java5 中引入的,它和StringBuffer的方法完全相同,区别在于它是在单线程环境下使用的,因为它的所有方法都没有被synchronized修饰,因此它的效率理论上也比 StringBuffer要高。
怎么样声明一下类不会被继承
如果一个类被final修饰,此类不可以有子类,不能被其它类继承,如果一个中的所有方法都没有重写的需要,当前类没有子类也罢,就可以使用final修饰类。
自定义异常
Java虽然提供了丰富的异常处理类,但是在项目中还会经常使用自定义异常,其主要原因是Java提供的异常类在某些情况下还是不能满足实际需球。
例如以下情况:
1、系统中有些错误是符合Java语法,但不符合业务逻辑。
2、在分层的软件结构中,通常是在表现层统一对系统其他层次的异常进行捕获处理。
线程池参数
线程池的常用创建方式主要有两种,通过Executors工厂方法创建和通过new ThreadPoolExecutor方法创建。
ThreadPoolExecutor构造函数的重要参数分析:
三个比较重要的参数:
corePoolSize :核心线程数,定义了最小可以同时运行的线程数量。
maximumPoolSize :线程中允许存在的最大工作线程数量
workQueue:存放任务的阻塞队列。新来的任务会先判断当前运行的线程数是否到达核心线程数,如果到达的话,任务就会先放到阻塞队列。
其他参数:
keepAliveTime:当线程池中的数量大于核心线程数时,如果没有新的任务提交,核心线程外的线程不会立即销毁,而是会等到时间超过keepAliveTime时才会被销毁。
unit :keepAliveTime 参数的时间单位。
threadFactory:为线程池提供创建新线程的线程工厂。
handler :线程池任务队列超过maxinumPoolSize 之后的拒绝策略
线程池大小如何设置
CPU 密集型应用,线程池大小设置为 N + 1(N表示CPU数量)
IO 密集型应用,线程池大小设置为 2N
CAS是什么?
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
循环时间长开销很大。
只能保证一个共享变量的原子操作。
ABA问题。
循环时间长开销很大:我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
数据库
事务的基本特征是什么
事务四大特征:原子性,一致性,隔离性和持久性。
-
原子性(Atomicity)
一个原子事务要么完整执行,要么干脆不执行。这意味着,工作单元中的每项任务都必须正确执行。如果有任一任务执行失败,则整个工作单元或事务就会被终止。即此前对数据所作的任何修改都将被撤销。如果所有任务都被成功执行,事务就会被提交,即对数据所作的修改将会是永久性的。 -
一致性(Consistency)
一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求;应用开发人员则需要保证数据库有适当的约束(主键,引用完整性等),并且工作单元中所实现的业务逻辑不会导致数据的不一致(即,数据预期所表达的现实业务情况不相一致)。例如,在一次转账过程中,从某一账户中扣除的金额必须与另一账户中存入的金额相等。支付宝账号100你读到余额要取,有人向你转100但是事物没提交(这时候你读到的余额应该是100,而不是200)这种就是一致性 -
隔离性(Isolation)
隔离性意味着事务必须在不干扰其他进程或事务的前提下独立执行。换言之,在事务或工作单元执行完毕之前,其所访问的数据不能受系统其他部分的影响。 -
持久性(Durability)
持久性表示在某个事务的执行过程中,对数据所作的所有改动都必须在事务成功结束前保存至某种物理存储设备。这样可以保证,所作的修改在任何系统瘫疾时不至于丢失。
ACID靠什么来保证的
- 原子性 由undolog日志来保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql
- 一致性 是由其他三大特性保证,程序代码要保证业务上的一致性
- 隔离性 是由MVCC来保证
- 持久性 由redolog来保证, mysql修改数据的时候会在redolog中记录一份日志数据,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失
MVCC解决的问题是什么
数据库并发场景有三种,分别为:
- 读读:不存在任何问题,也不需要并发控制
- 读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
- 写写:有线程安全问题,可能存在更新丢失问题
MVCC是一种用来解决读写冲突的无锁并发控制,也就是为事务分配单项增长的时间截,为每个修改保存一个版本,版本与事务时间截关联,读操作只读该事务开始数据库的快照,所以MVCC可以为数据库解决以下问题
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的功能
- 解决脏读。 幻读。 不可重复读等事务隔离问题,但是不能解决更新丢失问题。
MVCC实现原理是什么
MVCC的实现原理主要依赖于记录中的三个隐藏字段,undolog,read view来实现
隐藏字段:
- DB_TRX_ID 6字节,最近修改的事务id,记录创建这条记录或者最后一次修改该记录的事务id
- DB_ROLL_PTR 7字节,回滚指针,指向这条记录的上一个版本,配合undolog,指向上一个旧版本
- DB_ROW_ID 6字节,隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id
undolog
undolog被称之为回滚日志,表示在进行insert, delete, update操作的时候产生的方便回滚的日志
当进行insert操作的时候,产生的undolog只在事务回滚的时候需要,并且在事务提交之后可以被立刻丢弃
当进行update和delete操作的时候,产生的undolog不仅在事务回滚的时候需要,在快照读的时候也需要,所以不能随便删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除(当数据发生更新和删除操作的时候都只是设置一下。并不是真正的将过时的记录删除,因为为了节省磁盘空间,innodb有专门的purge线程来清除deleted_bit为true的记录,如果某个记录的deleted_id为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被清除的)
read_view
Read View是事务进行快照读操作的时候生产的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。
其实Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据
Read View遵循的可见性算法主要是将要被修改的数据的最新记录中的DB_TRX_ID (当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录就是当前事务能看到的最新老版本数据。
Read View的可见性规则如下所示:
首先要知道Read View中的三个全局属性:
- trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID (1,2,3)
- up_limit_id:记录trx_list列表中事务ID最小的ID (1)
- low_limit_id:Read View生成时刻系统尚未分配的下一个事务ID, (4)
比较规则
-
首先比较DB_TRX_ID < up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,如果大于等于进入下一个判断
-
接下来判断DB_TRX_ID >= low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断
-
判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit,修改的数据,当前事务也是看不到,如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。
RC, RR级别下的InnoDB快照读有什么不同
因为Read View生成时机的不同,从而造成RC, RR级别下快照读的结果的不同
-
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照即Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见
-
在RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早于Read View创建的事务所做的修改均是可见
-
在RC级别下,事务中,每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
总结:在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。
mysql的隔离级别有哪些
MySQL定义了四种隔离级别,包括一些具体规则,用于限定事务内外哪些改变是可见的,哪些改变是不可见的。低级别的隔离一般支持更高的并发处理,并且拥有更低的系统开销。
-
READ UNCOMMITTED 读取未提交内容
在这个隔离级别,所有事务都可以"看到"未提交事务的执行结果。在这种级别上,可能会产生很多问题,除非用户真的知道自己在做什么,并有很好的理由选择这样做。本隔离级别很少用于实际应用,因为它的性能也不必其他性能好多少,而别的级别还有其他更多的优点。读取未提交数据,也被称为"脏读” -
READ COMMITTED 读取提交内容
大多数数据库系统的默认隔离级别(但是不是MySQL的默认隔离级别) ,满足了隔离的早先简单定义:一个事务开始时,只能“看见“已经提交事务所做的改变,一个事务从开始到提交前,所做的任何数据改变都是不可见的,除非已经提交。这种隔离级别也支持所谓的“不可重复读”。这意味着用户运行同一个语句两次,看到的结果是不同的。 -
REPEATABLE READ 可重复读
MySQL数据库默认的隔离级别。该级别解决了READ UNCOMMITTED隔离级别导致的问题。它保证同一事务的多个实例在并发读取事务时,会“看到同样的"数据行。不过,这会导致另外一个棘手问题"幻读"。InnoDB和Falcon存储引擎通过多版本并发控制机制解决了幻读问题。 -
SERIALIZABLE 可串行化
该级别是最高级别的隔离级。它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简而言之,SERIALIZABLE是在每个读的数据行上加锁。在这个级别,可能导致大量的超时Timeout和锁竞争Lock Contention现象,实际应用中很少使用到这个级别,但如果用户的应用为了数据的稳定性,需要强制减少并发的话,也可以选择这种隔离级。
- 脏读
脏读是指一个事务读取了未提交事务执行过程中的数据。 - 不可重复读
不可重复读是指对于数据库中的某个数据,一个事务执行过程中多次查询返回不同查询结果,这就是在事务执行过程中,数据被其他事务提交修改了。不可重复读读取了另一事务已提交数据 - 虚读(幻读)
幻读指读取到其他事务的新增数据
mysql聚簇索引和非聚簇索引的区别是什么
mysql的索引类型跟存储引擎是相关的,innodb存储引擎数据文件跟索引文件全部放在ibd文件中,而myisam的数据文件放在myd文件中,索引放在myi文件中,其实区分聚簇索引和非聚簇索引非常简单,只要判断数据跟索引是否存储在一起就可以了。
innodb存储引擎在进行数据插入的时候,数据必须要跟索引放在一起,如果有主键就使用主键,没有主键就使用唯一键,没有唯一键就使用6字节的rowid,因此跟数据绑定在一起的就是聚簇索引,而为了避免数据冗余存储,其他的索引的叶子节点中存储的都是聚簇索引的key值,因此innodb中既有聚簇索引也有非聚簇索引,而myisam中只有非聚簇索引。
索引的基本原理
为什么要有索引?
一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,因此对查询语句的优化显然是重中之重。说起加速查询,就不得不提到索引了。
什么是索引?
索引在MySQL中也叫是一种"键”,是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键,尤其是当表中的数据量越来越大时,索引对于性能的影响愈发重要。索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高好几个数量级。索引相当于字典的音序表,如果要查某个字,如果不使用音序表,则需要从几百页中逐页去查。
索引的原理
索引的目的在于提高查询效率,与我们查阅图书所用的目录是一个道理:先定位到章,然后定位到该章下的一个小,节,然后找到页数。相似的例子还有:查字典,查火车车次,飞机航班等
本质都是:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件,也就是说,有了这种索引机制,我们可以总是用同一种查找方式来锁定数据。
数据库也是一样,但显然要复杂的多,因为不仅面临着等值查询,还有范围查询(>、<, between, in)、模糊查询(like)、并集查询(or)等等。数据库应该选择怎么样的方式来应对所有的问题呢?我们回想字典的例子,能不能把数,据分成段,然后分段查询呢?最简单的如果1000条数据,1到100分成第一段,101到200分成第二段,201到300分成第三段 这样查第250条数据,只要找第三段就可以了,一下子去除了90%的无效数据。但如果是1千万的记录呢,分成几段比较好?按照搜索树的模型,其平均复杂度是lgN,具有不错的查询性能。但这里我们忽略了一个关键的问题,复杂度模型是基于每次相同的操作成本来考虑的。而数据库实现比较复杂,一方面数据是保存在磁盘上的,另外一方面为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道访问磁盘的成本大概是访问内存的十万倍左右,所以简单的搜索树难以满足复杂的应用场景。
索引的数据结构
MySQL主要用到两种结构: B+Tree索引和Hash索引
Innodb存储引擎默认是B+Tree索引
Memory存储引擎默认Hash索引;
MySQL中,只有Memory(Memory表只存在内存中,断电会消失,适用于临时表)存储引擎显示支持Hash索引,是Memory表的默认索引类型,尽管Memory表也可以使用B+Tree索引。Hash索引把数据以hash形式组织起来,因此当查找某一条记录的时候,速度非常快。但是因为hash结构,每个键只对应一个值,而且是散列的方式分布。所以它并不支持范围查找和排序等功能。
B+Tree是mysql使用最频繁的一个索引数据结构,是InnoDB和MyISAM存储引擎模式的索引类型。相对Hash索引,B+Tree在查找单条记录的速度比不上Hash索引,但是因为更适合排序等操作,所以它更受欢迎。毕竟不可能只对数据库进行单条记录的操作。
对比:
- hash类型的索引:查询单条快,范围查询慢
- b+树,层数越多,数据量指数级增长(我们就用它,因为innodb默认支持它)
B+树在实现索引上的优势以及过程
B+ 树非叶子节点上是不存储数据的,仅存储键值,而 B 树节点中不仅存储键值,也会存储数据。
之所以这么做是因为在数据库中页的大小是固定的,InnoDB 中页的默认大小是 16KB****。
如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。
另外,B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。
一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO****。
② 因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。
那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。而 B 树因为数据分散在各个节点,要实现这一点是很不容易的。
B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。
其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。
也就是说上图中的 B+ 树索引就是 InnoDB 中 B+ 树索引真正的实现方式,准确的说应该是聚集索引(聚集索引和非聚集索引下面会讲到)。
通过上图可以看到,在 InnoDB 中,我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。
MyISAM 中的 B+ 树索引实现与 InnoDB 中的略有不同。在 MyISAM 中,B+树索引的叶子节点并不存储数据,而是存储数据的文件地址。
为什么B+树比B树更适合数据库索引?
B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,B+树只需要去遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。
B+树与B树的不同:
-
B+树非叶子节点不存在数据只存索引,B树非叶子节点存储数据
-
B+树查询效率更高。B+树使用双向链表串连所有叶子节点,区间查询效率更高(因为所有数据都在B+树的叶子节点,扫描数据库 只需扫一遍叶子结点就行了),但是B树则需要通过中序遍历才能完成查询范围的查找。
-
B+树查询效率更稳定。B+树每次都必须查询到叶子节点才能找到数据,而B树查询的数据可能不在叶子节点,也可能在,这样就会造成查询的效率的不稳定
-
B+树的磁盘读写代价更小。B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,通常B+树矮更胖,高度小查询产生的I/O更少。
1、 B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。
2、B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
3、由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。
索引的设计原则有哪些
在进行索引设计的时候,应该保证索引字段占用的空间越小越好,这只是一个大的方南,还有一些细节点需要注意下:
- 适合索引的列是出现在where字句中的列,或者连接子句中指定的列
- 基数较小的表,索引效果差,没必要创建索引
- 在选择索引列的时候,越短越好,可以指定某些列的一部分,没必要用全部字段的值
- 不要给表中的每一个字段都创建索引,并不是索引越多越好
- 定义有外键的数据列一定要创建索引
- 更新频繁的字段不要有索引
- 创建索引的列不要过多,可以创建组合索引,但是组合索引的列的个数不建议太多
- 大文本、大对象不要创建索引
mysql索引结构有哪些,各自优劣势是什么
索引的数据结构和具体存储引擎的实现有关, mysql中使用较多的索引有hash索引,B+树索引,innodb的索引实现为B+树,memory存储为hash索引。
B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的二节点间有指针相关连接,在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因此,B+树索引被广泛应用于数据库、文件系统等场景
哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。
如果是等值查询,那么哈希索引明显有绝对优势,因为只需要经过一次算法即可找到相应的键值,前提是键值都是唯一的。如果键值不是唯一的,就需要先找到该键所在位置,然后再根据链表往后扫描,直到找到对应的数据。如果是范围查询检索,这时候哈希索引就毫无用武之地了,因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索
哈希索引也没办法利用索引完成排序,以及like这样的部分模糊查询。哈希索引也不支持多列联合索引的最左匹配规则
B+树索引的关键字检索效率比较平均,不像B树那样波动大,在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在哈希碰撞问题。
mysql锁的类型有哪些
-
基于锁的属性分类:共享锁、排他锁。
-
基于锁的粒度分类:行级锁(innodb)、表级锁(innodb, myisam),页级锁(innodb引擎)、记录锁、间隙锁、临键锁。
-
基于锁的状态分类:意向共享锁、意向排它锁。
-
共享锁(share lock) :共享锁又称读锁,简称S锁;当一个事务为数据加上读锁之后,其他事务只能对该数据加读锁,而不能对数据加写锁,直到所有的读锁释放之后其他事务才能对其进行加持写锁。共享锁的特性主要是为了支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题。
-
排他锁(exclusive lock) :排他锁又称写锁,简称X锁;当一个事务为数据加上写锁时,其他请求将不能再为数据加任何锁,直到该锁释放之后,其他事务才能对数据进行加锁。排他锁的目的是在数据修改时候,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题。
-
表锁(table lock):表锁是指上锁的时候锁住的是整个表,当下一个事务访问该表的时候,必须等前一个事务释放了锁才能进行对表进行访问;特点:粒度大,加锁简单,容易冲突;
-
行锁:行锁是指上锁的时候锁住的是表的某一行或多行记录,其他事务访问同一张表时,只有被锁住的记录不能访问,其他的记录可正常访问,特点:粒度小,加锁比表锁麻烦,不容易冲突,相比表锁支持的并发要高
-
记录锁(Record lock) :记录锁也属于行锁中的一种,只不过记录锁的范围只是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录,加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题
-
页锁:页级锁是MysQL中锁定粒度介于行级锁和表级锁中间的一种锁,表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。特点:开销和加锁时间界于表锁和行锁之间,会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
-
间隙锁:是属于行锁的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。没有索引或非唯一索引会产生间隙锁,范围查询并且查询未命中记录,查询条件必须命中索引、间除锁只会出现在REPEATABLE_READ (重复读)的事务级别中。
-
临键锁(Next-Key lock):也属于行锁的一种,并且它是INNODB的行锁默认算法,总结来说它就是记录锁和间隙锁的组合,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住。
什么是mysql的主从复制
MySQL主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。
mysql复制原理是什么
(1) master服务器将数据的改变记录二进制binlog日志,当master上的数据发生改变时,则将其改变写入二进制日志中;I
(2) slave服务器会在一定时间间隔内对master二进制日志进行探测其是否发生改变,如果发生改变,则开始一个I/OThread请求master二进制事件
(3)同时主节点为每个1/O线程启动一个dump线程,用于向其发送二进制文件,并保存至从节点本地的中继日志中,从节点将启动SQL线程从中继日志中读取二进制日志,在本地重放,使得其数据和主节点的保持一致,最后I/OThread和SQLThread将进入睡眠状态,等待下一次被唤醒。
也就是说:
- 从库会生成两个线程,一个I/O线程,一个SQL线程
- I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中;
- 主库会生成一个log dump线程,用来给从库I/O线程传binlog;
- SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行;
具体步骤:
- 从库通过手工执行change master to 语句连接主库,提供了连接的用户一切条件(user,password,port, ip),并且让从库知道,二进制日志的起点位置(file名 position号) ; start slave
- 从库的IO线程和主库的dump线程建立连接。
- 从库根据change master to 语句提供的file名和position号,IO线程向主库发起binlog的请求。
- 主库dump线程根据从库的请求,将本地binlog以events的方式发给从库IO线程。
- 从库IO线程接收binlog events,并存放到本地relay-log中,传送过来的信息,会记录到master。info中
- 从库SQL线程应用relay-log,并且把应用过的记录到relay-log。info中,默认情况下,已经应用过的relay会自动被清理purge
mysql为什么需要主从同步
1.在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。
2.做数据的热备
3、架构的扩展。业务量越来越大,I/O访问频率变高,单机无法满足, 此时做多库的存储,降低磁盘/O访问的频率, 提高单个机器的/O性能。
mysql执行计划怎么看
- id
- table
- type
- key
- rows
- extra
myisam和innodb的区别
InnoDB存储引:主要面向OLTP(Online Transaction Processing,在线事务处理)方面的应用,是第一个完整支持ACID事务的存储引擎(BDB第一个支持事务的存储引擎,已经停止开发)。idb文件保存数据和索引
特点:
- 支持行锁
- 支持外键
- 支持自动增加列AUTO_INCREMENT属性
- 支持事务
- 支持MVCC模式的读写
- 读的效率低于MYISAM
- 写的效率高优于MYISAM
- 适合频繁修改以及设计到安全性较高的应用
- 清空整个表的时候,Innodb是一行一行的删除,
MyISAM存储引擎:是MySQL官方提供的存储引擎,主要面向OLAP(Online Analytical Processing.在线分析处理)方面的应用。
- 独立于操作系统,当建立一个MyISAM存储引擎的表时,就会在本地磁盘建立三个文件,例如我建立tb_demo表,那么会生成以下三个文件tb_demo.frm(表结构) ,tb_demo.MYD(数据) ,tb_demo.MYI(索引)
- 不支持事务,
- 支持表锁和全文索引
- MyISAM存储引擎表由MYD和MYI组成, MYD用来存放数据文件, MYI用来存放索引文件。MySQL数据库只缓存其索引文件,数据文件的缓存交给操作系统本身来完成;
- MySQL5.0版本开始, MyISAM默认支持256T的单表数据;
- 选择密集型的表: MYISAM存储引擎在筛选大量数据时非常迅速,这是他最突出的优点,
- 读的效率优于InnoDB
- 写的效率低于InnoDB
- 适合查询以及插入为主的应用
- 清空整个表的时候,MYISAM则会新建表
mysql索引类型有哪些
- 普通索引:允许被索引的数据列包含重复的值
- 唯一索引:可以保证数据记录的唯一性,
- 主键索引:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字primary key来创建
- 联合索引:索引可以覆盖多个数据列
- 全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术
索引可以极大地提高数据的查询速度
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能
但是会降低插入、删除、更新表的速度,因为在执行这些写操作的时候,还要操作索引文件
索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大,如果非聚簇索引很多,一旦聚簇索引改变,那么所有非聚簇索引都会跟着变
怎么处理mysql的慢查询?
- 开启慢查询日志,准确定位到哪个sql语句出现了问题
- 分析sql语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写
- 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引
- 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表
redis
数据类型
string(字符串),Hash(哈希),List(列表),Set(集合),zset(有序集合)
redis是什么
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即master-slave模式的数据备份。
redis持久化
Redis 的持久化机制有两种,第一种是快照RDB,第二种是 AOF 日志。快照是一次全量备份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,基本一次五分钟左右,而 AOF 日志记录的是内存数据修改的指令记录文本。AOF 日志在长期的运行过程中会变得无比庞大,数据库重启时需要加载 AOF 日志进行指令重放,这个时间就会无比漫长,所以需要定期进行 AOF 重写,给 AOF 日志进行瘦身。
Redis 本身有持久化,为什么还要写进 MySQL?
- 权限控制
MySQL 有权限控制,用户可以精确到每个 IP 的每个账户,目标可以精确到每个表的每个操作。
Redis 则是天生设计成完全开放权限,包括完全删除数据库的操作,任何人都可以执行。要么就只能把指令重命名成空的,完全禁止任何人执行。
- 数据完整
MySQL 的数据库保存在磁盘中,万一崩溃断电,也有数据库日志可以用以完成数据库事务。
MySQL 支持主从备份,所有的写入操作都可以实时发送到异地,哪怕突然机房被核弹轰炸,也不会丢失数据(可能除了最后几条语句)。
Redis 的崩溃……嗯小心数据全丢。
Redis 的 Replication 备份……嗯小心数据全丢。
- 负载均衡
MySQL 可以单主多从,也可以胆子够大在内网做双主,也可以用 innodb 配合 galera 做集群,每台机器都有一个独立的拷贝,因此服务器之间只要传输写指令即可。
Redis 可以单主多从(然而小心数据全丢),但是不能做多主互联。最多最多只能做 sharding ,也就是每台机器只保存一部分数据,读写一律被分散到其他机器上。直接后果就是内网流量大增。
- 数据隔离
MySQL 里我可以选择删掉某个应用的所有数据而保留另一个应用的所有数据。
Redis 里要么依赖 11 个 DB 的选择,要么依赖命名空间。
- 性价比
MySQL 是内存+硬盘,上个 SSD 配合 Query Cache 那速度已经是很快了。
Redis 是纯内存。乖乖掏钱加内存换至强啦。而且你还是得配备高性能磁盘,因为定时刷到磁盘和开机加载数据的操作还是要磁盘性能的。
数据库可以使用事务
- 需求不同
开发这样想是对的,但是其他的人可能会面临新的压力,新的技术挑战,所以需求定位是最终的出发点,除非哪天 Redis 的统计也能做的很溜.
Redis常见问题解析:击穿
概念:在Redis获取某一key时, 由于key不存在, 而必须向DB发起一次请求的行为, 称为“Redis击穿”。
引发击穿的原因:
- 第一次访问
- 恶意访问不存在的key
- Key过期
合理的规避方案:
- 服务器启动时, 提前写入
- 规范key的命名, 通过中间件拦截
- 对某些高频访问的Key,设置合理的TTL或永不过期
Redis常见问题解析:雪崩
概念:Redis缓存层由于某种原因宕机后,所有的请求会涌向存储层,短时间内的高并发请求可能会导致存储层挂机,称之为“Redis雪崩”。
合理的规避方案:
- 使用Redis集群
- 限流
分布式Session的实现方式
粘性Session (将用户请求固定在一台服务器)
服务器Session复制 (广播实现Session同步)
缓存存储 (Sticky模式和Non-Sticky模式)
CDN
CDN就是采用更多的缓存服务器(CDN边缘节点),布放在用户访问相对集中的地区或网络中。当用户访问网站时,利用全局负载技术,将用户的访问指向距离最近的缓存服务器上,由缓存服务器响应用户请求。
CDN=更智能的镜像+缓存+流量导流。
三大范式
- 列不可再分
- 属性完全依赖于主键
- 属性不依赖于其它非主属性 属性直接依赖于主键
- 在 3NF 的基础上消除主属性对于码的部分与传递函数依赖。
spring
spring、springmvc、springboot的区别是什么
spring和springMvc:
- spring是一个一站式的轻量级的java开发框架,核心是控制反转(IOC)和面向切面(AOP),针对于开发的WEB层(springMvc)、业务层(loc)、持久层(jdbcTemplate)等都提供了多种配置解决方案;
- springMvc是spring基础之上的一个MVC框架,主要处理web开发的路径映射和视图渲染,属于spring框架中WEB层开发的一部分;
springMvc和springboot:
- springMvc属于一个企业WEB开发的MVC框架,涵盖面包括前端视图开发、文件配置、后台接口逻辑开发等,XML, config等配置相对比较繁琐复杂;
- springBoot框架相对于springMvc框架来说,更专注于开发微服务后台接口,不开发前端视图,同时遵循默认优于配置,简化了插件配置流程,不需要配置xml,相对springmvc,大大简化了配置流程;
总结:
- Spring框架就像一个家族,有众多衍生产品例如 boot, security, jpa等等,但他们的基础都是Spring的ioc, aop等.ioc提供了依赖注入的容器, aop解决了面向横切面编程,然后在此两者的基础上实现了其他延伸产品的高级功能;(基石,底层框架)
- springMvc主要解决WEB开发的问题,是基于Servlet的一个MVC框架,通过XML配置,统一开发前端视图和后端逻辑;(web)
- 由于Spring的配置非常复杂,各种XML, JavaConfig, servlet处理起来比较繁项,为了简化开发者的使用,从而创造性地推出了springBoot框架,默认优于配置,简化了springMvc的配置流程;但区别于springMvc的是,springBoot专注于单体微服务接口开发,和前端解耦,虽然springBoot也可以做成springMvc前后台一起开发。但是这就有点不符合springBoot框架的初衷了;
springboot自动配置原理是什么?
springmvc工作流程是什么
spring的核心
spring是一个开源框架。
spring是为了简化企业开发而生的,使得开发变得更加优雅和简洁。
spring是一个IOC和AOP的容器框架。
IOC:控制反转
AOP:面向切面编程
容器:包含并管理应用对象的生命周期,就好比用桶装水一样, spring就是桶,而对象就是水
Spring AOP底层原理
应用场景: SpringAOP主要用于处理各个模块的横切关注点,比如日志、权限控制等。
SpringAOP的思想: SpringAOP的底层实现原理主要就是代理模式,对原来目标对象创建代理对象,并且在不改变原来对象代码的情况下,通过代理对象,调用增强功能的方法,对原有的业务进行增强。
AOP的代理分为动态代理和静态代理,SpringAOP中是使用动态代理实现的AOP,AspectJ则是使用静态代理实现的AOP。
SpringAOP中的动态代理分为JDK动态代理和CGLIB动态代理。
JDK动态代理
JDK动态代理原理: 基于Java的反射机制实现,必须有接口才能使用该方法生成代理对象。
JDK动态代理主要涉及到了两个类java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler。这两个类的主要方法如下:
java.lang.reflect.Proxy:
static InvocationHandler getInvocationHandler(Object proxy),该方法用于获取指定代理对象所关联的调用处理器
static Class getProxyClass(ClassLoader loader, Class... interfaces),该方法主要用于返回指定接口的代理类
static boolean isProxyClass(Class cl),该方法主要用于返回 cl 是否为一个代理类
static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)该方法主要用于构造实现指定接口的代理类的实例,所有的方法都会调用给定处理器对象的invoke()方法
java.lang.reflect.InvocationHandler:
Object invoke(Object proxy, Method method, Object[] args)该方法主要定义了代理对象调用方法时所执行的代码。
篇幅有限,就不展开介绍了,大致流程如下:
实现InvocationHandler接口创建方法调用器
通过为 Proxy 类指定 ClassLoader 对象和一组interface 创建动态代理
通过反射获取动态代理类的构造函数,参数类型就是调用处理器接口类型
通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数传入
CGLib 动态代理原理:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
SpringAOP何时使用JDK动态代理,何时使用CGLiB动态代理?
当Bean实现接口时,使用JDK动态代理。
当Bean没有实现接口时,使用CGlib动态代理
对AOP的理解?
AOP全称叫做 Aspect Oriented Programming面向切面编程,它是为解耦而生的,解期是程序员编码开发过程中一直追求的境界,AOP在业务类的隔离上,绝对是做到了解耦,在这里面有几个核心的概念:
- 切面(Aspect) :指关注点模块化,这个关注点可能会横切多个对象。事务管理是企业级Java应用中有关横切关注点的例子。在Spring AOP中,切面可以使用通用类基于模式的方式(schema-based approach)或者在普通类中以@Aspect注解(@Aspect)注解方式)来实现。
- 连接点(Join point):在程序执行过程中某个特定的点,例如某个方法调用的时间点或者处理异常的时间点。在Spring AOP中,一个连接点总是代表一个方法的执行。
- 通知(Advice) :在切面的某个特定的连接点上执行的动作。通知有多种类型,包括"around", "before" and"after"等等。通知的类型将在后面的章节进行讨论。许多AOP框架,包括Spring在内,都是以拦截器做通知模型的,并维护着一个以连接点为中心的拦截器链。
- 切点(Pointcut):匹配连接点的断言。通知和切点表达式相关联,并在满足这个切点的连接点上运行(例如,当执行某个特定名称的方法时)。切点表达式如何和连接点匹配是AOP的核心: Spring默认使用Aspect切点语义。
- 引入(Introduction) :声明额外的方法或者某个类型的字段。 Spring允许引入新的接口(以及一个对应的实现)到任何被通知的对象上。例如,可以使用引入来使bean实现ISModified接口,以便简化缓存机制(在Aspectj社区,引入也被称为内部类型声明(inter)).
- 目标对象(Target object) :被一个或者多个切面所通知的对象。也被称作被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,那么这个对象永远是一个被代理(proxied)的对象。
- AOP代理(AOP proxy) :AOP框架创建的对象,用来实现切面契约(aspect contract) (包括通知方法执行,等功能)。在Spring中, AOP代理可以是JDK动态代理或CGLIB代理。
- 织入(Weaving) :把切面连接到其它的应用程序类型或者对象上,并创建一个被被通知的对象的过程。这个过程可以在编译时(例如使用Aspect)编译器)、类加载时或运行时中完成。Spring和其他纯Java AOP框架样,是在运行时完成织入的。
这些概念都太学术了,如果更简单的解释呢,其实非常简单:
任何一个系统都是由不同的组件组成的,每个组件负责一块特定的功能,当然会存在很多组件是跟业务无关的,例如日志、事务、权限等核心服务组件,这些核心服务组件经常融入到具体的业务逻辑中,如果我们为每一个具体业务逻辑操作都添加这样的代码,很明显代码冗余太多,因此我们需要将这些公共的代码逻辑抽象出来变成一个切面,然后注入到目标对象(具体业务)中去,AOP正是基于这样的一个思路实现的,通过动态代理的方式,将需要注入切面的对象进行代理,在进行调用的时候,将公共的逻辑直接添加进去,而不需要修改,原有业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。
对IOC的理解?
IOC与大家熟知的依赖注入同理,这是一个通过依赖注入对象的过程也就是说,它们所使用的对象,是通过构造函数参数,工厂方法的参数或者是从工厂方法的构造函数或返回值的对象实例设置的属性,然后容器在创建bean时注入这些需要的依赖。这个过程相对普通创建对象的过程是反向的(因此称之为IOC),bean本身通过直接构造类来控制依赖关系的实例化或位置,或提供诸如服务定位器模式之类的机制。
- 谁控制谁:在之前的编码过程中,都是需要什么对象自己去创建什么对象,有程序员自己来控制对象,而有了IOC容器之后,就会变成由IOC容器来控制对象,
- 控制什么:在实现过程中所需要的对象及需要依赖的对象
- 什么是反转:在没有IOC容器之前我们都是在对象中主动去创建依赖的对象,这是正转的,而有了IOC之后,依赖的对象直接由IOC容器创建后注入到对象中,由主动创建变成了被动接受,这是反转
- 哪些方面被反转:依赖的对象
如何实现一个IOC容器
IOC(Inversion of Control),意思是控制反转,不是什么技术,而是一种设计思想,IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。
在传统的程序设计中,我们直接在对象内部通过new进行对象创建,是程序主动去创建依赖对象,而IOC是有专门的容器来进行对象的创建,即IOC容器来控制对象的创建。
在传统的应用程序中,我们是在对象中主动控制去直接获取依赖对象,这个是正转,反转是由容器来帮忙创建及注入依赖对象,在这个过程过程中,由容器帮我们查找级注入依赖对象,对象只是被动的接受依赖对象。
-
先准备一个基本的容器对象,包含一些map结构的集合,用来方便后续过程中存储具体的对象,
-
进行配置文件的读取工作或者注解的解析工作,将需的bean对象都封装成BeanDefinition对象存储在容器中
-
容器将封装好的BeanDefinition对象通过反转完成对象的实例化工作
-
进行对象的初始化操作,也就是给类中的对应属性值进行设置,也就是进行依赖注入,完成整个对象的创建,变成一个完整的bean对象,存储在容器的某个map结构中
-
通过容器对象来获取对象,进行对象的获取和逻辑处理工作
-
提供销毁操作,当对象不用或者容器关闭的时候,将无用的对象进行销毁
BeanFactory和ApplicationContext有什么区别
相同:
- Spring提供了两种不同的IOC容器,一个是BeanFactory,另外一个是ApplicajionContext,它们都是Javainterface, ApplicationContext 继承于BeanFactory (ApplicationContext 继承ListableBeanFactory.
- 它们都可以用来配置XML属性,也支持属性的自动注入。
- 而ListableBeanFactory继承BeanFactory), BeanFactory 和ApplicationContext都提供了一种方式,使用getBean("bean name")获取bean.
不同:
- 当你调用getBean()方法时, BeanFactory仅实例化bean,而ApplicationContext在启动容器的时候实例化单例bean,不会等待调用getBean()方法时再实例化。
- BeanFactory不支持国际化,Di18n,但ApplicationContext提供了对它的支持。
- BeanFactory与ApplicationContext之间的另一个区别是能够将事件发布到注册为监听器的bean
- BeanFactory 的一个核心实现是XMLBeanFactory mApplicationContext的一个核心实现是ClassPathXmlApplicationContext , Web容器的环境我们使用WebApplicationContext并且增加了getServletContext 方法。
- 如果使用自动注入并使用BeanFactory,则需要使用API注册AutoWiredBeanPostProcessor,如果使用ApplicationContext,则可以使用XML进行配置。
- 简而言之, BeanFactory提供基本的IOC和DI功能,而ApplicationContext提供高级功能,BeanFactory可用于测试和非生产使用,但ApplicationContext是功能更丰富的容器实现,应该优于BeanFactory
mybatis插件原理和开发流程
mybatis的优缺点
- Mybait的优点:
(1) 简单易学,容易上手(相比于Hibernate)基于SQL编程:
(2) JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
(3) 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持,而JDBC提供了可扩展性,所以只要这个数据库有针对Java的jar包就可以就可以与MyBatis兼容),开发人员不需要考虑数据库的差异性。
(4) 提供了很多第三方插件(分页插件page helper/逆向工程);
(5) 能够与Spring很好的集成;
(6) MyBatis相当灵活,不会对应用程序或者数据库的现有设计强加任何影响, SQL写在XML里,从程序代码中彻底分离,解除sql与程序代码的精合,便于统一管理和优化,并可重用。
(7) 提供XML标签,支持编写动态SQL语句。
(8) 提供映射标签,支持对象与数据库的ORM字段关系映射。
(9) 提供对象关系映射标签,支持对象关系组建维护。 - MyBatis框架的缺点:
(1) SQL语句的编写工作量较大,尤其是字段多、关联表多时,更是如此,对开发人员编写SQL语句的功底有一定要求。
(2) SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
mybatis和hibernate的区别
mybatis中#{}和${}的区别是什么
a. #{}是预编译处理,${}是字符串替换
b. Mybatis 在处理#{}时,会将sql 中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
c. Mybatis 在处理${}时,就是把${}替换成变量的值。
d. 使用#{}可以有效的防止SQL注入,提高系统安全性
JVM (java virtual machine)
JVM是什么?
JVM(Java Virtual Machine)是 Java 虚拟机,用于运行 Java 编译后的二进制字节码,最后生成机器指令。
JDK,JRE,JVM三者关系
- JDK :(Java Development Kit),Java 开发工具包。JDK 是整个 Java 开发的核心,集成了 JRE 和javac.exe,java.exe,jar.exe 等工具。
- JRE :(Java Runtime Environment),Java 运行时环境。主要包含两个部分,JVM 的标准实现和 Java 的一些基本类库。它相对于 JVM 来说,多出来的是一部分的 Java 类库。
- 三者的关系是:一层层的嵌套关系。JDK>JRE>JVM
JVM位置
硬件体系->操作系统->JVM->java字节码->java程序
.java -> class file -> 类装载器 class loader <-> 运行时数据区(Runtime Data Area){
方法区(Method Area),java栈(stack),本地方法栈(Native Method Stack),堆(heap),程序计数器
} <-> 本地方法接口 <- 本地方法库
<-> 执行引擎
JVM调优在调堆
类加载器
作用:加载class文件
- 虚拟机自带的加载器
- 启动类(根)加载器 rt.jar
- 扩展类加载器
- 应用程序加载器
4->3->2->1一层层往上找
双亲委派机制
保证安全
- APP<--EXT<--BOOT
java类先从根加载器找,然后是扩展加载器,最后是应用加载器,应用加载器有重名重地址的类可能就不会执行
1.类加载器收到类加载的请求!
2.将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类加载器
3.启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
4,重复步骤3CLass Not Found
如何打破双亲委派
核心: 重写ClassLoader#loadClass方法
沙箱安全机制
Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么? CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱,可以定制安全策略。
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于受信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增加了 安全策略,允许用户指定代码对本地,资源的访问权限。
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。
当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限
组成沙箱的基本组件:
字节码校验器(bytecode verifier) :确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
类装载器(class loader) :其中类装载器在3个方面对Java沙箱起作用
它防止恶意代码去干涉善意的代码;
它守护了被信任的类库边界;
它将代码归入保护域,确定了代码可以进行哪些操作。
虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成 , 每一个被装载的类将有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的,它们互相之间甚至不可见。类装载器采用的机制是双亲委派模式。
1.从最内层VM自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
2.由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
存取控制器(access controller) :存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
安全管理器(security manager) :是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
安全软件包(security package): java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:安全提供者,消息摘要,数字签名,加密,鉴别
native (JNI)
凡是带了native的方法,说明java的作用范围达不到了,会去调用底层C语言的库,会进入本地方法栈,会调用本地方法接口 JNI,JNI的作用:扩展Java的使用,融合不同的编程语言为java所用。它在内存区专门开辟了一块标记区域:Native Method Stack,登记native方法。最终执行的时候,通过JNI加载本地方法库中的方法
比如java程序驱动打印机,管理系统
PC寄存器
程序计数器 program counter register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方法字节码(用来存储指向一条指令的地址,也即将要执行的指令代码),在执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不记
方法区
Method Area 方法区
方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间;
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关
static final Class 常量池
栈内存
栈内存,主管程序的生命周期和线程同步;线程结束,栈内存被释放,对于栈来说,不存在垃圾回收问题;
栈:八大基本类型 + 对象引用 + 实例的方法
运行原理:栈帧
对象实例化过程
(先父后子)静态变量->静态代码块->(先父后子)变量->代码块->构造器
三种jvm
- Sun公司 HotSpot
- BEA JRockit
- IBM J9 VM
堆,heap
一个JVM只有一个堆内存,堆内存大小是可以调节的
类加载器读取了类文件后,会把 类,方法,常量,变量 保存在堆中,保存所有引用的真实对象
三个区域:
- 新生区:伊甸园区(Eden Space),幸存0区,幸存1区
类:诞生成长死亡的地方
伊甸园区 - 老年区:新生区杀不死的
- 永久区:这个区域常驻内存的。用来存放JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个不存在垃圾回收,关闭虚拟机释放
一个启动类,加载了大量的第三方jar包。Tomcat部署了太多的应用,大量动态生成的反射类。不断的被加载。直到内存满,就会出现OOM
--- jdk1.6之前:永久代,常量池是在方法区;
jdk1.7 :永久代,但是慢慢的退化了,去永久代,常量池在堆中
jdk1.8之后:无永久代,常量池在元空间
GC垃圾回收,主要在新生区和老年区,内存满了报错OOM
在jdk8以后,永久区叫元空间,
元空间逻辑上存在,物理上不存在
OOM:
- 尝试扩大内存看结果
- 分析内存,看一下哪个地方除了问题
元空间:逻辑上存在:物理上不存在
在一个项目中,突然出现了OOM故障,那么该如何排除~
研究为什么出错~.
能够看到代码第几行出错:内存快照分析工具, MAT, Jprofiler.
Dubug, 一行行分析代码!MAT, Jprofiler 作用.
分析Dump内存文件,快速定位内存泄露;.获得堆中的数据.获得大的对象~
GC
GC的作用区域:方法区+堆
JVM在进行GC时,并不是对这三个区域统一回收。大部分时候,回收都是新生代
新生代
幸存区(form, to)
老年区
GC两种类:轻GC(普通的GC) ,重GC(全局GC)
标记清除法,标记整理,复制算法,引用计数器
GC题目:
• JVM的内存模型和分区-详细到每个区放什么?
.堆里面的分区有哪些? Eden, form, to,老年区,说说他们的特点!
GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器,怎么用的?
·轻GC和重GC分别在什么时候发生?
Java中的垃圾收集器(GC)有两种主要类型:轻GC和重GC。轻GC是一种快速但低效的垃圾收集方式,通常用于清除新生代中的小对象。重GC是一种慢速但高效的垃圾收集方式,通常用于清除老年代中的大对象。
在Java中,轻GC和重GC的发生时机取决于应用程序的运行情况。一般来说,当应用程序创建了大量的新对象时,轻GC会发生;而当应用程序运行了一段时间,并且大量的对象进入老年代时,重GC会发生。
另外,Java中还有一种称为Full GC的垃圾收集方式,它是由轻GC和重GC触发的,它会清除整个堆内存中所有无用的对象。Full GC一般由于内存不足而触发,它会对应用程序的性能产生一定的影响。
复制算法:
每次gc都会将新生区活的对象移到幸存区,新生区被GC后,就会是空的
每次gc的时候,from区和新生区活下来的会进入to区,from区清空,当某个对象经历15次(-XX: MaxTenuringThreshod=15)gc仍然活着,就会进入老年区
好处:没有内存碎片,每次复制会进行内存的重排
坏处:浪费了一部分的空间
最佳使用场景,对象存活度较低的时候
标记清除算法:
扫描活着的对象,并进行标记,清除没有被标记的对象
缺点:两次扫描浪费时间,会产生内存碎片
优点:不需要额外的空间
标记整理算法:
对标记清除算法的结果再次扫描,移动存活的对象,多了一个移动成本
可以多次清除再整理
总结:
内存效率:复制算法>标记清除算法>标记压缩算法
内存整齐度:复制算法=标记整理>标记清楚
内存利用率: 标记整理=标记清楚>复制算法
没有最好的算法,只有最合适的算法
gc:分代收集算法
年轻代: 存活率低,复制算法
老年代:区域大,存活率高,标记清除+标记整理混合实现
JMM
JMM: java memory model
什么是JMM: java内存模型
干嘛的:缓存一致性协议,用于定义数据读写的规则
每个线程有自己的工作区域,从主内存中拷贝
解决共享对象可见性问题:volilate
JMM:抽象的概念,理论
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load, store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在内存中诞生,不允许工作内存直接使用一个未被初始化的变量,就是对变量实施use, store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
Volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
当一个变量定义为 volatile 之后,将具备两种特性:
1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。
2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
synchronized 特性
原子性:确保线程互斥的访问同步代码。synchronized保证只有一个线程拿到锁,进入同步代码块操作共享资源,因此具有原子性。
可见性:保证共享变量的修改能够及时可见。执行 synchronized时,会对应执行 lock 、unlock原子操作。lock操作,就会清空工作空间该变量的值;执行unlock操作之前,必须先把变量同步回主内存中。
有序性:synchronized内的代码和外部的代码禁止排序,至于内部的代码,则不会禁止排序,但是由于只有一个线程进入同步代码块,因此在同步代码块中相当于是单线程的,根据 as-if-serial 语义,即使代码块内发生了重排序,也不会影响程序执行的结果。
悲观锁:synchronized是悲观锁。每次使用共享资源时都认为会和其他线程产生竞争,所以每次使用共享资源都会上锁。
独占锁(排他锁):synchronized是独占锁(排他锁)。该锁一次只能被一个线程所持有,其他线程被阻塞。
非公平锁:synchronized是非公平锁。线程获取锁的顺序可以不按照线程的阻塞顺序。允许线程发出请求后立即尝试获取锁。
可重入锁:synchronized是可重入锁。持锁线程可以再次获取自己的内部的锁。
synchronized的三种应用方式
synchronized关键字最主要有以下3种应用方式,下面分别介绍
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
synchronized底层语义原理
Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。
synchronized代码块底层原理
从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令
synchronized方法底层原理
从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。
CMS垃圾收集器
采用的是“标记-清除算法”,整个过程分为4步
-
- 初始标记 CMS initial mark 标记GC roots直接关联对象,不用tracing,速度很快
-
- 并发标记 CMS concurrent mark 进行GC Roots Tracing
-
- 重新标记 CMS remark 修改并发标记因用户程序变动的内容
-
- 并发清除 CMS concurrent sweep 清除不可达对象回收空间,同时有新垃圾产生,留着下次清理,称为浮动垃圾
由于整个过程中,并发标记和并发清除,收集器线程可以与用户线程一起工作,所以总体上来说,CMS收集器的内存你回收过程是与用户线程一起并发地执行的
- 优点:并发收集,低停顿
- 缺点:产生大量空间碎片,并发阶段会降低吞吐率
G1
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂。
如果对象太大,一个Region放不下(超过Region大小的50%),那么就会直接放到H中
设置Region大小:-XX:G1HeapRegionSize= <N>M
所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于"标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上,的时间不得超过N毫秒)
工作过程:
初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
关于 Object o = new Object();
markword: 8 字节
类型指针 class pointer: 4 字节 指向Class的对象
实例数据:变量大小
对齐:要对8整除
比如没有实例数据,那么大小应该是8 + 4 + 4(补齐)=16b
如果实例数据中有对象,则只占 4 bytes,只是指针,真正存储在堆中
对象头包括什么:markword + class pointer
markword包括什么信息: 锁信息,hashcode,gc信息(记录颜色,新生区幸存区老年区)
对象怎么定位:
直接定位:对象大小,对象开始位置
间接方式:实例数据指针,类型数据指针,当对象移动的时候,句柄不用变,有利于垃圾回收
对象怎么分配:
先看可以不可以往栈上放,如果可以,放栈上,然后看对象大不大,如果大,直接放老年代
否则,尝试在线程本地(ThreadLocal)分配,这样不用线程同步,效率更快,否则再分配在伊甸园区的公共区
gc,幸存区,多次gc年龄够进入老年区
对象创建过程
申请空间,设置默认值;调构造函数,设初始值;与引用建立关联(加载,链接,初始化)
DCL double check lock
上锁前检查一次,上锁后检查一次
if(INSTANCE==null){
synchronized (A.class){
if(INSTANCE==null){
try{
Thread.sleep(1);
} catch (InterruptedException e){
e.printStackTrace();
}
INSTANCE=new A();
}
}
}
dcl要不要加volatile?
要!new 过程分三步,二三步可能发生指令重排,这样一来别的引用可能获得该对象的默认值而不是初始值
private static volatile A INSTANCE;
禁止重排序
JVM调优,调整JVM参数符合项目需求
为什么不使用C++对象直接表示Java对象
把C++的虚函数表移到了class文件里,更加紧凑
OOP_CLASS模型表示java对象
class实例究竟在Method Area还是在Heap
Method Area
class实例信息在堆,原文件信息在方法区,反射信息在堆
注解
元注解
@Target 用于描述注解的适用范围
@Retention 表示需要在什么级别保存该注释信息,用于描述注解的生命周期
(SOURCE<CLASS<RUNTIME)
@Document 说明该注解被包含在javadoc中
@Inherited 说明子类可以继承父类中的注解
自定义注解用 @interface
什么时候会发生类初始化?
类的主动引用(一定会发生类的初始化)
当虚拟机启动,先初始化main方法所在的类new一个类的对象
调用类的静态成员(除了final常量)和静态方法使用java.lang.reflect包的方法对类进行反射调用
当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类
类的被动引用(不会发生类的初始化)
当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量,不会导致子类初始化
通过数组定义类引用,不会触发此类的初始化
引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
什么是反射?
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
- 哪里用到反射机制?
JDBC中,利用反射动态加载了数据库驱动程序。
Web服务器中利用反射调用了Sevlet的服务方法。
Eclispe等开发工具利用反射动态刨析对象的类型与结构,动态提示对象的属性和方法。
很多框架都用到反射机制,注入属性,调用方法,如Spring。 - 什么叫对象序列化,什么是反序列化,实现对象序列化需要做哪些工作?
对象序列化,将对象中的数据编码为字节序列的过程。
反序列化;将对象的编码字节重新反向解码为对象的过程。
JAVA提供了API实现了对象的序列化和反序列化的功能,使用这些API时需要遵守如下约定:
被序列化的对象类型需要实现序列化接口,此接口是标志接口,没有声明任何的抽象方法,JAVA编译器识别这个接口,自动的为这个类添加序列化和反序列化方法。
为了保持序列化过程的稳定,建议在类中添加序列化版本号。
不想让字段放在硬盘上就加transient
以下情况需要使用 Java 序列化:
想把的内存中的对象状态保存到一个文件中或者数据库中时候;
想用套接字在网络上传送对象的时候;
想通过RMI(远程方法调用)传输对象的时候。 - 反射机制的优缺点?
优点:可以动态执行,在运行期间根据业务功能动态执行方法、访问属性,最大限度发挥了java的灵活性。
缺点:对性能有影响,这类操作总是慢于直接执行java代码。 - 动态代理是什么?有哪些应用?
动态代理是运行时动态生成代理类。
动态代理的应用有 Spring AOP数据查询、测试框架的后端 mock、rpc,Java注解对象获取等。