1 MyBatis #{},${}区别?
(1) #
将传入的数据都当成一个字符串, 会对自动传入的数据加一个双引号; 而$
将传入的数据直接显示生成在 SQL 中;
(2) #
方式能够很大程度防止 SQL 注入;$
方式无法防止SQL注入;
(3) 一般能用#
就不要用$
; 但是在做排序使用 order by 动态参数的时候, 需要注意使用$
而不是#
2 MyBatis 插件用过哪些?
3 Hashmap 为什么是线程不安全的
HashMap
的线程不安全体现在,并发环境下会造成并发修改异常
具体体现为:
- 在JDK1.7中, 当并发执行扩容操作时会造成死循环和数据丢失的情况;
- 在JDK1.8中, 在并发执行put操作时会发生数据覆盖的情况;
深入解读HashMap线程安全问题
目前有以下方式可以获得线程安全的HashMap:
- HashTable
- Collections.SyncronizedMap
- ConcurrentHashMap
前两种方式由于全局锁的问题,存在很大的性能问题,所以在并发环境下基本采用
ConcurrentHashMap
3.1 我们知道并发环境下使用concurrentHashMap,为何它就是线程安全的?1.7和1.8有何不同?
JDK1.7
分段锁技术
针对HashTable会锁整个hash表的问题,ConcurrentHashMap提出了分段锁的解决方案,其思路就是:
锁的时候不锁整个hash表,而是只锁一部分;
具体实现:
主要是用到了ConcurrentHashMap中最关键的Segment
ConcurrentHashMap
中维护着一个Segment数组,每个Segment可以看做是一个HashMap
而Segment继承了ReentrantLock
,它本身就是一个锁
在每个Segment中通过HashEntry数组来维护其内部的hash表;
每个HashEntry就代表了map中的一个K-V, 用HashEntry可以组成一个链表结构,通过next字段引用到其下一个元素;
Segment自己本身就是一个锁,在put的时候,当前Segment会将自己锁住,此时其它线程无法操作这个Segment,但不会影响到其它Segment的操作,这就是锁分段带来的好处
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 因为segment本身就是一个锁
// 这里调用tryLock尝试获取锁
// 如果获取成功,那么其他线程都无法再修改这个segment
// 如果获取失败,会调用scanAndLockForPut方法根据key和hash尝试找到这个node,如果不存在,则创建一个node并返回,如果存在则返回null
// 查看scanAndLockForPut源码会发现他在查找的过程中会尝试获取锁,在多核CPU环境下,会尝试64次tryLock(),如果64次还没获取到,会直接调用lock()
// 也就是说这一步一定会获取到锁
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 扩容
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 释放锁
unlock();
}
return oldValue;
}
扩容(Rehash)的时候:ConcurrentHashMap指针对每个Segment中的HashEntry数组进行扩容,Rehash的操作是在put方法中的,而put方法是加了锁的,所以Rehash操作也是加了锁的,所在其它线程无法对当前Segment的hash表做操作,这就保证了线程安全
JDK1.8
线程安全的put操作,put操作分为以下两类:
- 当前hash表对应当前key的index上没有元素时
如果当前key的index上没有元素时, 会尝试用CAS(casTabAt())的方式在数组的指定index上创建一个新的Node
- 当前hash表对应当前key的index上已经存在元素时(hash碰撞)
不同于JDK7中的Segment概念,JDK8中直接用链表的头结点作为锁,通过该锁,使同一时间只有一个线程对某一链表执行put,解决了并发问题
线程安全的扩容操作:
put方法的最后一步是统计hash表中元素的个数,如果超过sizeCtl的值, 触发扩容
首先new一个新的hash表(nextTable)出来,大小是原来的2倍. 后面的rehash都是针对这个新的hash表操作,不涉及原hash表(table)
然后会对原hash表中的每个链表进行rehash,此时会尝试获取头结点的锁,这一步就保证了在rehash的过程中不能对这个链表执行put操作
通过sizeCtl控制,使扩容过程中不会new出多个新的hash表来.
最后,将所有键值对重新rehash到新表(nextTable)中后,用nextTable将table替换,这就避免了HashMap中get和扩容并发时,可能get到null的问题
在整个过程中,共享变量的存储和读取全部通过volatile或CAS的方式,保证了线程的安全;
参考解读Java8中的ConcurrentHashMap是如何保证线程安全的
4 Hashmap 数据结构
HashMap
底层由散列表后面跟了链表或红黑树构成;
散列表: 其实是一个list, 每个元素是一个<Key,Value>结构的Node节点.
链表: 链表存在是为了解决Hash碰撞时元素被覆盖的问题;
Hash碰撞: 不同的元素通过 hash算法可能会得到相同的 hash值, 如果都放同一个桶里, 后面放进去的就会覆盖前面放的, 所以为了解决hash碰撞时元素被覆盖的问题, 就有了在桶里放链表;
红黑树: 红黑树存在是为了解决链表查询效率低的问题;
假设现在HashMap集合中大多数的元素都放到了同一个桶里(由hash值计算而得的桶的位置相同), 那么这些元素就在这个桶后面连成了链表. 现在需要查询某个元素, 那么此时的查询效率就很慢了. 为了解决这个问题,就引入了红黑树.
问题延伸
这里有可能会被问到,红黑树的数据结构
4.1 散列表后面跟什么数据结构是怎么确定的?
- 链表节点转换成红黑树节点的阈值, 节点数 >=8 就转;
static final int TREEIFY_THRESHOLD = 8;
- 红黑树节点转换链表节点的阈值, 节点数 <= 6 就转;
static final int UNTREEIFY_THRESHOLD = 6;
- 转红黑树时, table 的最小长度为 64;
static final int MIN_TREEIFY_CAPACITY = 64;
4.2 HashMap在什么情况下会扩容?
当hashmap中的元素个数超过数组大小 * loadFactor 时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。
5 HTTPS
HTTP+SSL就是HTTPS:密文传输
一个HTTPS请求实际上包含了两次HTTP传输,可以细分为8步。
1.客户端向服务器发起HTTPS请求,连接到服务器的443端口
2.服务器端有一个密钥对,即公钥和私钥,是用来进行非对称加密使用的,服务器端保存着私钥,不能将其泄露,公钥可以发送给任何人。
3.服务器将自己的公钥发送给客户端。
4.客户端收到服务器端的公钥之后,会对公钥进行检查,验证其合法性,如果发现发现公钥有问题,那么HTTPS传输就无法继续。严格的说,这里应该是验证服务器发送的数字证书的合法性,关于客户端如何验证数字证书的合法性,下文会进行说明。如果公钥合格,那么客户端会生成一个随机值,这个随机值就是用于进行对称加密的密钥,我们将该密钥称之为client key,即客户端密钥,这样在概念上和服务器端的密钥容易进行区分。然后用服务器的公钥对客户端密钥进行非对称加密,这样客户端密钥就变成密文了,至此,HTTPS中的第一次HTTP请求结束。
5.客户端会发起HTTPS中的第二个HTTP请求,将加密之后的客户端密钥发送给服务器。
6.服务器接收到客户端发来的密文之后,会用自己的私钥对其进行非对称解密,解密之后的明文就是客户端密钥,然后用客户端密钥对数据进行对称加密,这样数据就变成了密文。
7.然后服务器将加密后的密文发送给客户端。
8.客户端收到服务器发送来的密文,用客户端密钥对其进行对称解密,得到服务器发送的数据。这样HTTPS中的第二个HTTP请求结束,整个HTTPS传输完成。
6 String 的数据结构
String
类是final的,因此其是不可修改的
String A = "abc";
String B = A + "d";
当执行A+"d"
时,底层是用StringBuilder
类的append()
方法进行连接字符串的,连接完毕再toString
返回引用地址;
String A = "abc";
String B = new String("def");
直接指定的String,如上述第一行代码,常量"abc"是存放在字符串常量池中的,变量A存放在栈中
对于通过new创建的字符串对象,如上述第二行代码,在分配内存空间的时候,会先去字符串常量池中查看有没有该字符串常量"def",如果有就直接将该常量在字符串常量池中的地址引用放置到在堆内存中开辟的空间中,变量B则放置在栈中,其拥有对堆内存空间该内存的引用
需要注意的是,在Java8之後,JVM内存空间这块,方法区被元空间取代,而本该在方法区中的常量池,一分为二,字符串常量池和运行时常量池分别隶属于堆空间和元空间
7 Runable和Thread区别
Runable是一个接口, Thread 是其实现类, 只是具有很多自己独特的属性和方法.
无论使用Runable还是Thread, 都会new Thread, 然后执行run方法.
用法上,如果有复杂的线程操作需求, 那就选择继承Thread, 如果只是简单的执行一个任务那就实现Runable.
目前,我们一般使用的是Callable,具有返回值的多线程实现
或者直接使用线程池ThreadPoolExecuter
直接自定义创建
7.1 Runable中的run()方法和Thread中的start()方法的区别
- run()方法 当做普通方法的方式调用. 程序还是会顺序执行的,需要等到run()方法体执行完毕后, 才可继续执行下面的代码;
- start()方法 通过该方法我们来启动线程, 真正实现了多线程运行, 这时无需等待run方法体代码执行完毕, 可以继续执行下面的代码;
public class MyTest1 {
public static void main(String[] args) {
Runner1 runner1 = new Runner1();
Runner2 runner2 = new Runner2();
Thread thread1 = new Thread(runner1);
Thread thread2 = new Thread(runner2);
thread1.start();
thread2.run();
}
}
class Runner1 implements Runnable { // 实现了Runnable接口,jdk就知道这个类是一个线程
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("进入Runner1运行状态——————————" + i);
}
}
}
class Runner2 implements Runnable { // 实现了Runnable接口,jdk就知道这个类是一个线程
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("进入Runner2运行状态==========" + i);
}
}
}
执行结果
打印结果很直观的反应出,start()方法在执行完毕之后,无需去立马执行其Runnable对象对run()方法的代码体的;
而是会顺序执行start()下面的代码
8 Java反射的原理, 注解的原理
反射
JAVA反射机制是在运行状态中
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
反射提供的功能:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法
注解
注解的实现依赖于反射。JAVA中的注解是一种继承自接口java.lang.annotation.Annotation的特殊接口。通过动态代理的方式为你生成了一个实现了接口Annotation的实例,然后对该代理实例的属性赋值,这样就可以在程序运行时(如果将注解设置为运行时可见的话)通过反射获取到注解的配置信息。
9 ArrayList和LinkedList的插入,取出时间复杂度
总结:
- ArrayList是线性表(动态数组),LinkedList是链表
- get,set方法,方法参数有指定位置数值的,ArrayList要优于LinkedList,因为,ArrayList有下标,LinkedList要移动指针。
- 新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList需要移动数据
10 jdk8 新特性
11 过滤器(Filter)和拦截器(Interceptor)区别,分别属于谁的技术栈
Spring 的拦截器(Interceptor)与 Servlet的过滤器(Filter) 有相似之处, 比如二者都是 AOP 编程思想的体现,都能实现 权限检查, 日志记录等
不同的是:
- 使用范围不同: Filter 是 Servlet 规范规定的, 只能用于 Web程序中, 而拦截器(Interceptor)既可以用于 Web程序, 也可以用于 Application, Swing程序中;
- 规范不同: Filter 是在 Servlet 规范中定义的, 是 Servlet 容器支持的, 而拦截器是在Spring容器内的, 是Spring框架支持的;
- 使用的资源不同: 同其他代码块一样, 拦截器是一个Spring组件, 归Spring管理, 配置在 Spring文件中, 因此能使用 Spring 里的任何资源, 对象, 例如 Service对象, 数据源, 事务管理等,通过IoC注入到拦截器即可; 而Filter 则不能;
- 深度不同: Filter只在 Servlet 前后起作用. 而拦截器能够深入到方法前后, 异常抛出前后等. 因此拦截器的使用具有更大的弹性. 所以Spring构架的程序中, 要优先使用拦截器;
12 ThreadLocal
ThreadLocal 并不是一个 Thread, 而是 Thread的局部变量(它是个局部变量)
ThreadLocal是解决线程安全问题的一个很好的思路. 它通过为每个线程提供一个独立的变量副本 解决了变量并发访问的冲突问题
在很多情况下, ThreadLocal 比 直接使用syncronized同步机制解决线程安全问题更简单, 更方便,且结果程序拥有更高的并发性;
场景
在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。
意思是,如果你的多线程是对资源的读取,而不会进行修改操作的话,这个时候我们使用ThreadLocal,但是ThreadLocal不能保证的是可见性问题, 因为是每个线程独立的副本;