Java问题整理
- 数据库索引的定义与使用
- 索引是怎么加快查询速率的
- 在条件中加大于,小于或者等于会影响索引的命中和查询效率吗?
- 字符串可以加索引吗?
- like模糊查询,前缀,中间,后缀查询会走索引吗?那种最快?(待)
- sql 关键字 explain
- 数据库中悲观锁和乐观锁的代码实现
- Arraylist 的实现
- ArrayList 是线程不安全,有哪些 list 是安全的
- 一百万个对象存入arraylist要怎么处理
- 线程池怎么实现,工作流程
- 数据库连接池应该设置多大
- HashSet是怎么确保元素不重复
- 上下文切换是什么
- Java内存中哪些会产生溢出
- Java缓存应用
- 接口与抽象类的区别
- MyBatis与Hibernate区别
- Hibernate 延迟加载机制
- HashMap底层为什么使用红黑树
- ArrayList为什么扩容为原来的1.5倍
- 什么是 CAS (了解)
- PageHelper 原理
- 聚簇索引和非聚簇索引区别
- Redis 为什么那么“快”
- Hash表处理冲突的方式
- 死锁的四个必要条件
- JVM 内存模型
- JAVA 内存模型
- JVM 内存模型中的 栈帧
- Session 怎样实现
- 阻塞队列底层怎么实现的(待)
- B+树索引和 Hash 索引的区别
- ThreadLocal
- Spring 事务隔离级别
- 一些简答题
数据库索引的定义与使用
索引定义:
- 索引是为了加速对表中数据行的检索而创建的一种分散的存储结构。索引是针对表而建立的,它是由数据页面以外的索引页面组成的,每个索引页面中的行都会含有逻辑指针,以便加速检索物理数据。
- SQL Server 允许用户在表中创建索引,指定按某列预先排序,从而大大提高查询速度。类似于汉语词典中按照拼音或字画查找。
索引作用:
通过索引可以大大地提高数据库的检索速度,提高数据库的性能。
索引的类型:
-
唯一索引:唯一索引不允许两行有相同的索引值。即唯一索引这一列,每个值都是唯一的
例如:在 user 表中,user 的 用户名(uname)列上创建了唯一索引,则所有用户的用户名不能重复。 -
主键索引:定义表主键的时候,会自动创建主键索引(主键索引是唯一索引的特例),主键索引要求每一个值都是唯一且非空
主键索引是唯一索引的特殊类型。主键索引要求主键中的每个值都是唯一的,当在查询中使用主键索引时,允许快速访问数据。 -
聚集索引:表中各行的物理顺序与键值的逻辑顺序相同,每个表只有一个
在聚集索引中,表中各行的物理顺序与键值的逻辑(索引)顺序相同。表中只能包含一个聚集索引。
例如:汉语字典默认按拼音排序编排字典中的每页页码。拼音字母 a,b,c,…,x,y,z 就是索引的逻辑顺序,而页码1,2,3…就是物理顺序。默认按拼音排序的字典其索引顺序和逻辑顺序是一致的。拼音顺序越后的字对应的页码也越大(ha对应的页码比 ba 页码靠后)。 -
非聚集索引:非聚集索引指定表的逻辑顺序,数据存储在一个位置,索引存储在另一个位置,索引中包含指向数据存储位置的指针
如果不是聚集索引,表中各行的物理顺序与键值逻辑顺序不匹配。聚集索引比非聚集索引有更快的数据访问速度。
例如:按笔画顺序的索引就是非聚集索引,“1”画的字(词)对应的页码可能比“3”画的字(词)对应的页码大(靠后)。
注意:SQL Server中,一个表只能创建1个聚集索引,多个非聚集索引。设置某列为主键,该列就默认为聚集索引。
索引的优缺点:
- 优点:加快访问速度,将强行的唯一性
- 缺点:带索引的表在数据库中的存储需要更多的空间
适合场景:
- 该列频繁用于搜索
- 该列用于对数据进行排序
索引操作
- 建立索引
create index 索引名 on 表名(索引字段名)
- 建立唯一索引
CREATE UNIQUE INDEX <索引的名字> ON tablename (列的列表);
//修改表
ALTER TABLE tablename ADD UNIQUE [索引的名字] (列的列表);
//创建表的时候指定索引
CREATE TABLE tablename ( [...], UNIQUE [索引的名字] (列的列表) );
- 建立复合索引
create index 索引名 on 表名(索引字段名1,索引字段名2...)
- 查看表中索引
show index from 表名
- 删除索引
drop index 索引名 on 表名
- 查看索引是否命中
Mysql:结果 type=index 表示命中
explain +sql语句
索引是怎么加快查询速率的
- 索引就是通过事先排好序,从而在查找时可以应用二分查找等高效率的算法。一般的顺序查找,复杂度为O(n),而二分查找复杂度为O(log2n)。当n很大时,二者的效率相差及其悬殊。
- 索引和表都是以数据块的形式存储,但是二者存储结构不同。索引是b-tree结构的一组有序的数据,里面存放的是 key值和行地址(oracle 是rowid)。比如一张表中id字段,有1-100 行,查询id=5这行,那么只需要遍历B-tree树,找到5这个节点,就可以确定一行。但是如果没有索引,就需要遍历整个表的数据块,来确定id=5的这一行。遍历有序的b-tree数据块,确定此行所在位置之后,再一次读取数据,比扫描表所有数据块要快很多
在条件中加大于,小于或者等于会影响索引的命中和查询效率吗?
- 等于肯定会使用索引,但是小于,大于就不一定
字符串可以加索引吗?
可以
- 使用前缀索引
- 倒序存储
- 使用 hash 字段
第二,第三占用空间差不多,第二 CPU 消耗较小,第三查询效率好
like模糊查询,前缀,中间,后缀查询会走索引吗?那种最快?(待)
- like 模糊查询
1、模糊查询,后通配 走索引 ,前通配 走全表—— *% 或 % *
2、where条件用in或or 不会走索引
3、order by 排序时,如果碰到相同的值,则会随机进行排序,排序出来的结果集不是固定的。建议使用id序列进行排序(唯一的值)。
sql 关键字 explain
explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。
使用方法,在select语句前加上explain就可以了:
explain select surname,first_name form a,b where a.id=b.id
数据库中悲观锁和乐观锁的代码实现
- 悲观锁:就是对数据的冲突采取一种悲观的态度,也就是说假设数据肯定会冲突,所以在数据开始读取的时候就把数据锁定住。【数据锁定:数据将暂时不会得到修改】
- 排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。
- 大多数情况下依靠数据库的锁机制实现
- 例如select * from account where name=”Max” for update;
这条sql 语句锁定了account 表中所有符合检索条件(name=”Max”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
- 乐观锁:认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让用户返回错误的信息。让用户决定如何去做。
Arraylist 的实现
ArrayList是List接口的可变数组的实现,底层使用数组保存所有元素。其操作基本上是对数组的操作。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。
- 底层使用数组
private transient Object[] elementData;
- 构造方法
//构造一个默认初始容量为10的空列表
public ArrayList() {
this(10);
}
//构造一个指定初始容量的空列表
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
}
//构造一个包含指定 collection 的元素的列表
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
- 存储
// 用指定的元素替代此列表中指定位置上的元素,并返回以前位于该位置上的元素。
public E set(int index, E element) {
RangeCheck(index);
E oldValue = (E) elementData[index];
elementData[index] = element;
return oldValue;
}
// 将指定的元素添加到此列表的尾部。
public boolean add(E e) {
ensureCapacity(size + 1);
elementData[size++] = e;
return true;
}
// 将指定的元素插入此列表中的指定位置。
// 如果当前位置有元素,则向右移动当前位于该位置的元素以及所有后续元素(将其索引加1)。
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+", Size: "+size);
// 如果数组长度不足,将进行扩容。
ensureCapacity(size+1); // Increments modCount!!
// 将 elementData中从Index位置开始、长度为size-index的元素,
// 拷贝到从下标为index+1位置开始的新的elementData数组中。
// 即将当前位于该位置的元素以及所有后续元素右移一个位置。
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
// 按照指定collection的迭代器所返回的元素顺序,将该collection中的所有元素添加到此列表的尾部。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
// 从指定的位置开始,将指定collection中的所有元素插入到此列表中。
public boolean addAll(int index, Collection<? extends E> c) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: " + index + ", Size: " + size);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacity(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
- 读取
// 返回此列表中指定位置上的元素。
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
- 删除
// 移除此列表中指定位置上的元素。
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
// 移除此列表中首次出现的指定元素(如果存在)。这是应为ArrayList中允许存放重复的元素。
public boolean remove(Object o) {
// 由于ArrayList中允许存放null,因此下面通过两种情况来分别处理。
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 类似remove(int index),移除列表中指定位置上的元素。
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
注意:从数组中移除元素的操作,也会导致被移除的元素以后的所有元素向左移动一个位置
- 调整数组容量
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1;
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
-
每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。
-
数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
-
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
-
将底层数组的容量调整为当前列表保存的实际元素的大小—trimToSize
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
ArrayList 是线程不安全,有哪些 list 是安全的
常见集合
线程不安全
当多个并发同时对非线程安全的集合进行增删改的时候会破坏这些集合的数据完整性;
例如:当多个线程访问同一个集合或Map时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性。
-
Vector、HashTable、Properties是线程安全的
-
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的
-
为了保证集合是线程安全的,相应的效率也比较低;线程不安全的集合效率相对会高一些。
Vector -> SynchronizedList -> CopyOnWriteArrayList
- Vector
每个方法都添加了synchronized关键字来保证同步,所以它是线程安全的,但是正是这些方法的同步,让其效率大大的降低了。比ArrayList的效率要慢。
源码解析 - SynchronizedList
除了使用 Vector 来代替 ArrayList 线程安全问题,还可以使用:
java.util.Collections.SynchronizedList
它能把所有 List 接口的实现类转换成线程安全的 List,比 Vector 有更好的扩展性和兼容性,其构造方法如下
final List<E> list;
SynchronizedList(List<E> list){
super(list);
this.list = list;
}
Synchronized 部分方法
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
有代码可以看出,方法都带同步对象锁,性能不是最优。下面介绍更合适的解决方案
java.util.concurrent.CopyOnWriteArrayList
java.util.concurrent.CopyOnWriteArraySet
!!!这两种并发集合,只适合于读多写少的情况,如果写多读少,使用这个就没意义了,因为每次写操作都要进行集合内存复制,性能开销很大,如果集合较大,很容易造成内存溢出。
- CopyOnWriteArrayList
CopyOnWrite(简称:COW):即复制再写入,就是在添加元素的时候,先把原 List 列表复制一份,再添加新的元素。
add方法源码
添加元素时,先加锁,再进行复制替换操作,最后释放锁
public boolean add(E e) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取原始集合
Object[] elements = getArray();
int len = elements.length;
// 复制一个新集合
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 替换原始集合为新集合
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
get 方法源码
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
获取元素并没有加锁:好处是在高并发的情况下,读取元素时无锁,写数据才加锁,大大提升了读取性能
- CopyOnWriteArraySet
就是使用 CopyOnWriteArrayList 的 addIfAbsent 方法来去重的,添加元素的时候判断对象是否已经存在,不存在才添加进集合。
/**
* Appends the element, if not present.
*
* @param e element to be added to this list, if absent
* @return {@code true} if the element was added
*/
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
一百万个对象存入arraylist要怎么处理
- 使用多线程,将 ArrayList 划分为多个片,每个线程搜索一个片,最后将结果集合起来(使用CPU核心数2倍数量线程)
- 使用 fork-join框架——任务偷取算法
参考代码
fork-join框架
(类似插入多条数量:因为ArrayList的底层是数组实现,并且数组的默认值是10,如果插入10000条要不断的扩容,耗费时间,所以我们调用ArrayList的指定容量的构造器方法ArrayList(int initialCapacity) 就可以实现不扩容,就提高了性能。)
线程池怎么实现,工作流程
-
线程池的主要处理流程
- 判断核心线程池是否已满(都有任务),若不是则创建一个新的工作线程来执行新任务;否则判断盘对工作队列是否已满
- 判断工作队列是否已满,若不是则将提交的新任务加入工作队列中;若工作队列已满,则判断线程池是否已满
- 判断线程池是否已满,若不是则创建一个新的线程来执行新任务;否则交给饱和策略来处理这个任务
-
线程池源码
- ThreadPoolExecutor的execute()方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//如果线程数大于等于基本线程数或者线程创建失败,将任务加入队列
if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
//线程池处于运行状态并且加入队列成功
if (runState == RUNNING && workQueue.offer(command)) {
if (runState != RUNNING || poolSize == 0)
ensureQueuedTaskHandled(command);
}
//线程池不处于运行状态或者加入队列失败,则创建线程(创建的是非核心线程)
else if (!addIfUnderMaximumPoolSize(command))
//创建线程失败,则采取阻塞处理的方式
reject(command); // is shutdown or saturated
}
}
- 线程池代码实现的注意点
-
不推荐使用jdk自带的executors的方式来创建线程池——阿里巴巴java开发手册中明确规定不允许使用Executors创建线程池。
即:
FixedThreadPool,
SingleThreadPoo,
CacheThreadPool,
ScheduledThreadPool.
-
创建线程池的正确方式
避免使用 Executors 创建线程池:主要避免了其中的默认实现,可以改用 ThreadPoolExecutor构造方法指定参数
需要指定核心线程池的大小、最大线程池的数量、保持存活的时间、等待队列容量的大小。在这种情况下一旦提交的线程数超过当前可用的线程数时就会抛出拒绝执行的异常 java.util.concurrent.RejectedExecutionException 有界队列已经满了便无法处理新的任务。 -
使用工具类来创建线程池:
apache guava(不仅可以避免OOM的问题,还可以自定义线程名称,更加方便出错时溯源)等
刚开始都是在创建新的线程,达到核心线程数量5个后,新的任务进来后不再创建新的线程,而是将任务加入工作队列,任务队列到达上线5个后,新的任务又会创建新的普通线程,直到达到线程池最大的线程数量10个,后面的任务则根据配置的饱和策略来处理。
-
线程池的参数——7个
corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler
-
corePoolSize 线程池核心线程大小
-
maximumPoolSize 线程池最大线程数量
-
keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
-
unit 空间线程存活时间单位:keepAliveTime的计量单位
-
workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
①ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
②LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而不会去创建新线程直到maxPoolSize,因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
③SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
④PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
- threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
- handler 拒绝策略/饱和策略
饱和策略
RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:
1、AbortPolicy:直接抛出异常(默认)
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
数据库连接池应该设置多大
- 数据库连接数 maxActive 与下列有关
- cpu
- 磁盘 IO
- 网络 IO
- 数据库连接池的大小最合适设置为:((核心数 * 2)+ 有效磁盘数)
HashSet是怎么确保元素不重复
HashSet是哈希表结构,当一个元素要存入HashSet集合时,首先通过自身的hashCode方法算出一个值,然后通过这个值查找元素在集合中的位置,如果该位置没有元素,那么就存入。如果该位置上有元素,那么继续调用该元素的equals方法进行比较,如果equals方法返回为真,证明这两个元素是相同元素,则不存。否则则在该位置上存储2个元素(一般不可能重复)所以当一个自定义的对象想正确存入HashSet集合,那么应该重写Object的hashCode和equals。HashSet是通过hashCode()和equals()方法确保元素无序不重复的 。
上下文切换是什么
- 多线程编程中一遍线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
- 概括来说:当前任务在执行完 CPU 分配给自己的时间片要切换到另一个任务之前会先保存自己的状态,以便下次再切换回当前这个任务时,可以重新加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
Java内存中哪些会产生溢出
常见原因
- 内存中加载的数据量过于庞大,如一次从数据库取出大量过多数据
- 集合类中有对 对象 的引用,使用完未清空,使得 JVM 不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 使用的第三方软件中的bug
- 启动参数内存值设置过小
解决方案
- 修改 JVM 启动参数,直接增加内存(-Xms参数必须加上)
-
- 检查错误日志,查看“OutOfMemory”错误前是否有其他异常或错误
- 对代码进行走查分析,找出可能发生内存溢出的位置,重点如下
- 检查对数据库的查询中,是否有一次获得全部数据的查询。(对于数据库查询尽量采用分页的方式查询)
- 检查代码中是否出现死循环或递归调用
- 检查是否大循环重复产生新对象实体
- 检查List,Map等集合对象是否有使用完毕后,未清除的问题。List,Map 等集合对象会始终有对 对象 的引用,不能被 GC 回收
Java底层GC
- 使用内存查看工具动态查看内存使用情况
Java缓存应用
为什么 Java 中 “1000 == 1000” 为false,而 “100 == 100” 为true
Integer a = 1000, b = 1000;
System.out.println(a == b);//1
Integer c = 100, d = 100;
System.out.println(c == d);//2
结果
false
true
如果两个引用指向同一个对象,用 == 他们是相等的。如果两个引用指向不同的对象,用 == 他们是不相等的,即使他们内容相同。
原因:Integer.java 类中有一个内部私有类 IntegerCache.java。其中缓存了从 -128 到 127 之间的所有整数对象。
所以所有的小整数在内部缓存,当我们声明类似
Integer c = 100;
实际上内部处理
Integer i = Integer.valueOf(100);
观察 valueOf()方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
所以当值的范围在 -128到127 之间,他就从高速缓存返回实例。即下面代码指向同一个对象
Integer c = 100, d = 100;
System.out.println(c == d);
接口与抽象类的区别
- 抽象类和接口都不能被直接实例化,子类必须实现了这个其定义的所有的方法
- 抽象类要被子类继承,接口要被子类实现
- 接口里面只能对方法进行声明,抽象类既可以对方法进行声明也可以对方法进行实现,抽象类还可以有构造方法
- 抽象类里面可以没有抽象方法,如果一个类里面有抽象方法,那么这个类一定是抽象类
- 抽象类中的方法都要被实现,所以抽象方法不能是静态的static,也不能是私有的private
- 接口中只能够有静态的不能被修改的数据成员(也就是必须是 static final的,不过在 interface中一般不定义数据成员);抽象类中的变量默认是 friendly 型,其值可以在子类中重新定义,也可以重新赋值
- 接口(类)可以继承接口,甚至可以继承多个接口。但是类只能继承一个类
- 抽象类主要是用来抽象类别,接口主要是用来抽象方法功能
* 当你关注事物的本质的时候,请用抽象类;当你关注一种操作的时候,用接口
MyBatis与Hibernate区别
Spring Data JPA可以理解为 JPA 规范的再次封装抽象,底层还是使用了 Hibernate 的 JPA 技术实现。JPA 和 Mybatis 的区别也就是 Hibernate和mybatis的区别了。
相同点:
- 两者生成 Session 的过程及 Session 的生命周期相似
- 两者都支持 JDBC 和 JTA 事务
- JTA:解决多数据源和分布式事务问题
JTA原理
- JTA:解决多数据源和分布式事务问题
不同点:
1.Mybatis 优势和缺点
- 可以更加精确地定位 SQL 语句,进行 SQL 优化
- 映射条件灵活,可以根据不同条件组装 SQL
- 使用 Mapper 的接口编程,只要一个接口和一个 XML 文件就可以创建映射,简化开发过程
- 面向 SQL 语句,数据库移植性差
2.Hibernate 优势和缺点
- 完全面向对象,数据库无关性好,O/R映射能力强
- 对对象的维护和缓存要比 Mybatis 好,对增删改查的对象的维护更方便
- 数据库移植性好
- 有更好的二级缓存机制,可以使用第三方缓存。Mybatis 本身提供的缓存机制不佳
总结:
-
Mybatis可以进行更细致的SQL优化,查询必要的字段,但是需要维护SQL和查询结果集的映射。数据库的移植性较差,针对不同的数据库编写不同的SQL。
-
Hibernate对数据库提供了较为完整的封装,封装了基本的DAO层操作,有较好的数据库移植性。但是学习周期长,开发难度大于Mybatis。
Hibernate 延迟加载机制
-
在使用hibernate的session.load(),query.iterate()等方法时,Hibernate返回的是一个空对象(除主键属性外都是null),并没有去查询数据库;而是在使用返回对象的时候才会去查询数据库,并将查询结果注入到该对象中;这种查询时机推迟到对象访问的机制称之为延迟加载。
-
延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。
-
在Hibernate中提供了对实体对象的延迟加载以及对集合的延迟加载,另外在Hibernate3中还提供了对属性的延迟加载。
HashMap底层为什么使用红黑树
需要注意:链表长度大于 8 个才转换红黑树
原因:
链表的时间复杂度是O(n),红黑树的时间复杂度O(logn)
因为红黑树需要进行左旋,右旋操作, 而单链表不需要,
以下都是单链表与红黑树结构对比。
如果元素小于8个,查询成本高,新增成本低
如果元素大于8个,查询成本低,新增成本高
【可以参考另外一篇笔记红黑树原理相关】
个人认为红黑树维护性更高,每次插入、删除的平均旋转次数远小于平衡树(最多只需要旋转三次)
ArrayList为什么扩容为原来的1.5倍
具体来说,应该是往 ArrayList 添加第 11 个元素时会扩容 1.5 倍
添加一个元素,首先计算当前的list所需最小的容量大小,是否需要扩容等。当需要扩容时:
1.得到当前的ArrayList的容量(oldCapacity)。
2.计算除扩容后的新容量(newCapacity),其值(oldCapacity + (oldCapacity >> 1))约是oldCapacity 的1.5倍。
这里采用的是移位运算(关于移位运算,后续会讲到)。为什么采用这种方法呢?应该是出于效率的考虑。
3.当newCapacity小于所需最小容量,那么将所需最小容量赋值给newCapacity。
4.newCapacity大于ArrayList的所允许的最大容量,处理。
5.进行数据的复制,完成向ArrayList实例添加元素操作。
由上述可知,所说的1.5倍扩容,并不确切。最后真正的容量,可能不止1.5倍,也许还要大。
什么是 CAS (了解)
Compare and Swap,即比较再交换。
简概:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。
PageHelper 原理
聚簇索引和非聚簇索引区别
- 聚簇索引就是按照每张表的主键构造一颗B+树,同时叶子节点中存放的就是整张表的行记录数据,也将聚集索引的叶子节点称为数据页。这个特性决定了索引组织表中数据也是索引的一部分,每张表只能拥有一个聚簇索引。
- 在聚簇索引之上创建的索引称之为辅助索引(非聚簇索引),辅助索引访问数据总是需要二次查找。辅助索引叶子节点存储的不再是行的物理位置,而是主键值。通过辅助索引首先找到的是主键值,再通过主键值找到数据行的数据页,再通过数据页中的Page Directory找到数据行。
聚簇索引和非聚簇索引的区别
聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引;二级索引的叶子节点存放的是主键值或指向数据行的指针。
由于节子节点(数据页)只能按照一颗B+树排序,故一张表只能有一个聚簇索引。辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引
Redis 为什么那么“快”
- 纯内存KV操作
- 内部是单线程实现的(不需要创建/销毁线程,避免上下文切换,无并发资源竞争的问题)
- 异步非阻塞的I/O(多路复用)
Redis使用单线程,相比于多线程快在哪里?
Redis的瓶颈不在线程,不在获取CPU的资源,所以如果使用多线程就会带来多余的资源占用。比如上下文切换、资源竞争、锁的操作。
- 上下文的切换
上下文其实不难理解,它就是CPU寄存器和程序计数器。主要的作用就是存放没有被分配到资源的线程,多线程操作的时候,不是每一个线程都能够直接获取到CPU资源的,我们之所以能够看到我们电脑上能够运行很多的程序,是应为多线程的执行和CPU不断对多线程的切换。但是总有线程获取到资源,也总有线程需要等待获取资源,这个时候,等待获取资源的线程就需要被挂起,也就是我们的寄存。这个时候我们的上下文就产生了,当我们的上下文再次被唤起,得到资源的时候,就是我们上下文的切换。 - 竞争资源
竞争资源相对来说比较好理解,CPU对上下文的切换其实就是一种资源分批,但是在切换之前,到底切换到哪一个上下文,就是资源竞争的开始。在我redis中由于是单线程的,所以所有的操作都不会涉及到资源的竞争。 - 锁的消耗
对于多线程的情况来讲,不能回避的就是锁的问题。如果说多线程操作出现并发,有可能导致数据不一致,或者操作达不到预期的效果。这个时候我们就需要锁来解决这些问题。当我们的线程很多的时候,就需要不断的加锁,释放锁,该操作就会消耗掉我们很多的时间
I/O复用,非阻塞模型
对于I/O阻塞可能有很多人不知道,I/O操作的阻塞到底是怎么引起的,Redis又是怎么解决的呢?
-
I/O操作的阻塞:当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
-
Redis采用多路复用:I/O 多路复用其实是在单个线程中通过记录跟踪每一个sock(I/O流) 的状态来管理多个I/O流。select, poll, epoll 都是I/O多路复用的具体的实现。epoll性能比其他几者要好。redis中的I/O多路复用的所有功能通过包装常见的select、epoll、evport和kqueue这些I/O多路复用函数库来实现的。
-
select(),poll()缺点:
- 涉及到用户态跟内核态的切换,耗性能,需要上下文切换,消耗资源大;
- 返回值是 int,只能知道就绪的socket个数,需要重新轮询找出是具体哪个socket就绪。
- select()跟poll()主要区别:传参不一样——select():bitmap;poll():数组
解决了bitmap长度 1024 难以修改的问题
-
epoll():
Hash表处理冲突的方式
开放地址法
- 线性探测法:若当前想放的位置有值了,就往后寻找最近一个空位置,将值放入。
可能会造成某一块区域数据很密集(如 上面代码中,18岁的学生有很多个,从“18”该放的位置起数据很密集),不推荐。 - 二次探测法:若当前想放的位置有值了,就往后查找 i^2 个位置是否为空,否则就继续指导找到空的位置(如:当前想放在位置 1,位置不为空,往后移动 1 ^ 2 位置查看是否为空;结果位置 2 有值,再往后移动 2 ^ 2 个位置,结果2 + 2 ^ 2 = 6 即位置6 为空,放下数值。否则继续前面操作)。
可能会造成很多位置的浪费,即数组长度过长中间却很多空位置。 - 再哈希法:需要两个(或三个及以上)散列函数。假设用函数一发现当前位置不为空,不继续调用函数一寻找空位置,而是转调用函数二寻找空位置
链地址法
死锁的四个必要条件
以下条件缺一不可
- 互斥条件:一个锁同一时刻只能被一个线程所获取
- 请求与保持条件:既想请求获取新的锁,又不愿意释放原有的锁
- 不剥夺条件:当想获取某把锁,而在等待某线程释放该锁时,不能去剥夺此时拥有该锁的线程的持有权
- 循环等待条件:一直等待直到想获取的锁被其他线程释放
JVM 内存模型
JVM内存主要分为:程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区。
-
程序计数器: 为了线程切换能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。计数器记录的是正在执行的虚拟机字节码指令的地址。
-
Java虚拟机栈: 每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接(例如多态就要动态链接以确定引用的状态)、方法出口等信息。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 returnAddress 类型(指向了一条字节码指令的地址)。
其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 -
本地方法栈: Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
-
Java 堆: Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC 堆”。
-
方法区: 方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量(final )、静态变量(static)、即时编译器编译后的代码等数据。
一个类中主要有:常量、成员变量、静态变量、局部变量。其中常量与静态变量位于方法区,成员变量位于 Java 堆,局部变量位于 Java 虚拟机栈。
运行时常量池: 是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
Java内存模型:每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个线程共享。
JAVA 内存模型
Java Memory Model (JAVA 内存模型,JMM)描述线程之间如何通过内存(memory)来进行交互。具体说来,JVM中存在一个主存区(Main Memory或Java Heap Memory),对于所有线程进行共享,而每个线程又有自己的工作内存(Working Memory,实际上是一个虚拟的概念),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中,而线程之间是不能直接相互访问的,变量在程序中的传递,是依赖主存来完成的。具体的如下图所示:
JMM描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存中读取出变量这样的底层细节。
所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中变量的一份拷贝)。
JMM的两条规定
1、线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写;
2、不同的线程之间无法直接访问其他线程工作内存中的变量,线程变量值的传递需要通过主内存来完成。
追加问题
1.描述一下jvm内存模型
jvm内存模型分为5个区域,其中线程独占的区域是栈、本地方法栈、程序计数器,线程共享的区域是堆、方法区。
2.描述一下java内存模型
回答这个问题一定要问清楚是不是要问java内存模型,确认是的话,可以回答:java内存模型规定了变量的访问规则,保证操作的原子性、可见行、有序性。
3.谈一下你对常量池的理解
常量池是类在编译后储存常量的一块区域,当程序运行到该类,常量池的大部分数据会进入运行时常量池,字符串会进入字符串常量池。
4.什么情况下会发生栈内存溢出?和内存溢出有什么不同?
栈内存溢出指程序调用的栈深度多大或栈空间不足时抛出的StackOverflowError。
一般所谓内存溢出指的是堆内存溢出,抛出的是OutOfMemoryError:java heap space。
在jdk1.7中,还可能出现永久代内存溢出,抛出的是OutOfMemoryError: PermGen space
在jdk1.8中,则会出现元空间溢出,抛出的是OutOfMemoryError:MetaSpace
5.String str = new String(“abc”)创建了多少个实例?
虽然很多博客都告诉我们创建了两个对象:一个字符串abc对象和一个字符串常量池里指向abc的引用对象。
但实际情况要更复杂一点。
实际上在执行完String str = new String(“abc”)之后,其实只创建了一个对象:堆里的字符串对象。而str直接指向该对象。在执行intern()方法后,才会到字符串常量池创建引用对象。当然有时候这个过程会自动完成,但情况比较复杂,难以确定。
有很多面试官其实自己也搞不清,所以不妨先告诉他创建了两个对象,然后再分析一番,效果更好。
JVM 内存模型中的 栈帧
- 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。
- 一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)
Session 怎样实现
其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。
阻塞队列底层怎么实现的(待)
B+树索引和 Hash 索引的区别
B+树索引:具有动态平衡的特点。
1.支持范围查询。
2.等长的访问路径,访问时间根据数据量的变化相对稳定。
3.有明确的查找方向。
Hash索引:具有查找速度快的特点。
1.仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
2.无法被用来避免数据的排序操作。
3.不能利用部分索引键查询。
4.在任何时候都不能避免表扫描。
5.遇到大量Hash值相等的情况后性能降低。
总结:
数据量大选择B+树索引,数据量少选择Hash索引
ThreadLocal
- ThreadLocal从字面意思来理解,是一个线程本地变量,也可以叫线程本地变量存储。有时候一个对象的变量会被多个线程所访问,这个时候就会有线程安全问题,当然可以使用synchronized关键字来为该变量加锁,进行同步处理来限制只能有一个线程来使用该变量,但是这样会影响程序执行的效率,这时ThreadLocal就派上了用场;
- 使用ThreadLocal维护变量的时候,会为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个当前变量。这样同时有多个线程访问该变量并不会相互影响,因为他们都是使用各自线程存储的变量,所以不会存在线程安全的问题。
- 同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式,前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问且互不影响。
ThreadLocal实现原理:
- 每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。也就是说ThreadLocal本身不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。值得注意的是图中(图片摘自网络)的虚线,表示ThreadLocalMap是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被回收。
ThreadLocal 的内存泄露解决
Spring 事务隔离级别
一些简答题
- java中wait和sleep有什么区别?多线程条件下如何保证数据安全?
答:最大区别是等待时wait会释放锁,而sleep会一直持有锁,wait通常用于线程时交,互,sleep通常被用于暂停执行。
- java中volatile和synchronized有什么区别?
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
3.volatile仅能实现变量的修改可见性,并不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
- 有了解java的原子类?实现原理是什么?
答:采用硬件提供原子操作指令实现的,即CAS。每次调用都会先判断预期的值是否符合,才进行写操作,保证数据安全。
- spring主要使用了哪些?IOC实现原理是什么?AOP实现原理是什么?
答:spring主要功能有IOC,AOP,MVC等,IOC实现原理:先反射生成实例,然后调用时主动注入。AOP原理:主要使用java动态代理,
- mybatis有了解吗?它与hibernate有什么区别?项目中,你会选哪个?
答:两者都是轻量级ORM框架,hibernate实现功能比较多,通过HQL操作数据库,比较简单方便,但hibernate自动生成的sql相长,不利测试和查找原因。复杂sql时,编写比较困难,同时性能也会降低。mybatis是半自动化,手动编写SQL语句,同时提供丰富的参数判断功能。sql语句较清晰,可以直接进行测试,性能也较好,操作起来非常简单。同时hibernate容易产生n+1问题。hibernate学习成本较mybatis高。国内一些大公司基本上使用mybatis
- 说说数据库性能优化有哪些方法?
答:使用explain进行优化,查看sql是否充分使用索引。避免使用in,用exist替代,字段值尽可能使用更小的值,任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边。使用连接查询(join)代替子查询。
在表的多列字段上建立一个索引,但只有在查询这些字段的第一个字段时,索引才会被使用。
- HTTP请求方法get和post有什么区别?
1:Post传输数据时,不需要在URL中显示出来,而Get方法要在URL中显示。
2:Post传输的数据量大,可以达到2M,而Get方法由于受到URL长度限制,只能 传递大约1024字节.
3:Post就是为了将数据传送到服务器段,Get就是为了从服务器段取得数据.而Get 之所以也能传送数据,只是用来设计告诉服务器,你到底需要什么样的数据.Post 的信息作为http请求的内容,而Get是在Http头部传输的。
- linux命令熟悉?查看某个线程命令是什么?查看整个机器负载命令?文件内容快速查找命令是什么?
查看线程:ps -ef|greptomcat
查看负载:top
文件内容查找:vi /aa test.txt 或者先打开文件,再查找: vi test.txt /aa
- JVM内存模型是如何?垃圾回收机制有哪些?如何对JVM进行调优?
答:由栈和堆组成,栈是运行时单位,堆内存则分为年轻代、年老代、持久代等,年轻代中的对象经过几次的回收,仍然存在则被移到年老代;持久代主要是保存class,method,filed等对象。
sun回收机制:主要对年轻代和年老代中的存活对象进行回收,分为以下:
年轻代串行(Serial Copying)、年轻代并行(ParNew)、年老代串行(SerialMSC),年老代并行(Parallel Mark Sweep),年老代并发(Concurrent Mark-Sweep GC,即CMS)等等,目前CMS回收算法使用最广泛。
JVM调优主要是对堆内容和回收算法进行配置,需要对jdk产生的回收日志进行观察,同时通过工具(Jconsole,jProfile,VisualVM)对堆内存不断分析,这些优化是一个过程,需要不断地进行观察和维护。
- java抽象类和接口有什么区别?项目中怎么去使用它们?
相同点:
A. 两者都是抽象类,都不能实例化。
B. interface实现类及abstractclass的子类都必须要实现已经声明的抽象方法。
不同点:
A. interface需要实现,要用implements,而abstractclass需要继承,要用extends。
B. 一个类可以实现多个interface,但一个类只能继承一个abstractclass。
C. interface强调特定功能的实现,而abstractclass强调所属关系。
D. 尽管interface实现类及abstrctclass的子类都必须要实现相应的抽象方法,但实现的 形式不同。interface中的每一个方法都是抽象方法,都只是声明的 (declaration, 没有方 法体),实现类必须要实现。而abstractclass的子类可以有选择地实现。
使用:
abstract:在既需要统一的接口,又需要实例变量或缺省的方法的情况下,使用abstract;
interface:使用: 类与类之前需要特定的接口进行协调,而不在乎其如何实现。 作为能 够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识。需要将一组类 视为单一的类,而调用者只通过接口来与这组类发生联系。需要实现特定的多项功能, 而这些功能之间可能完全没有任何联系。