索引定义:
索引作用:
通过索引可以大大地提高数据库的检索速度,提高数据库的性能。
索引的类型:
唯一索引:唯一索引不允许两行有相同的索引值。即唯一索引这一列,每个值都是唯一的
例如:在 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 表名
explain +sql语句
使用例子
可以
参考文章
explain显示了mysql如何使用索引来处理select语句以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。
使用方法,在select语句前加上explain就可以了:
explain select surname,first_name form a,b where a.id=b.id
参考文章
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);
}
}
当多个并发同时对非线程安全的集合进行增删改的时候会破坏这些集合的数据完整性;
例如:当多个线程访问同一个集合或Map时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性。
Vector、HashTable、Properties是线程安全的
ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等都是线程不安全的
为了保证集合是线程安全的,相应的效率也比较低;线程不安全的集合效率相对会高一些。
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
!!!这两种并发集合,只适合于读多写少的情况,如果写多读少,使用这个就没意义了,因为每次写操作都要进行集合内存复制,性能开销很大,如果集合较大,很容易造成内存溢出。
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);
}
获取元素并没有加锁:好处是在高并发的情况下,读取元素时无锁,写数据才加锁,大大提升了读取性能
/**
* 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的底层是数组实现,并且数组的默认值是10,如果插入10000条要不断的扩容,耗费时间,所以我们调用ArrayList的指定容量的构造器方法ArrayList(int initialCapacity) 就可以实现不扩容,就提高了性能。)
线程池源码
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个,后面的任务则根据配置的饱和策略来处理。
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实现。
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
RejectedExecutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:
1、AbortPolicy:直接抛出异常(默认)
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
参考文章1
参考文章2
参考文章3
HashSet是哈希表结构,当一个元素要存入HashSet集合时,首先通过自身的hashCode方法算出一个值,然后通过这个值查找元素在集合中的位置,如果该位置没有元素,那么就存入。如果该位置上有元素,那么继续调用该元素的equals方法进行比较,如果equals方法返回为真,证明这两个元素是相同元素,则不存。否则则在该位置上存储2个元素(一般不可能重复)所以当一个自定义的对象想正确存入HashSet集合,那么应该重写Object的hashCode和equals。HashSet是通过hashCode()和equals()方法确保元素无序不重复的 。
常见原因
解决方案
为什么 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);
Spring Data JPA可以理解为 JPA 规范的再次封装抽象,底层还是使用了 Hibernate 的 JPA 技术实现。JPA 和 Mybatis 的区别也就是 Hibernate和mybatis的区别了。
相同点:
不同点:
1.Mybatis 优势和缺点
2.Hibernate 优势和缺点
总结:
Mybatis可以进行更细致的SQL优化,查询必要的字段,但是需要维护SQL和查询结果集的映射。数据库的移植性较差,针对不同的数据库编写不同的SQL。
Hibernate对数据库提供了较为完整的封装,封装了基本的DAO层操作,有较好的数据库移植性。但是学习周期长,开发难度大于Mybatis。
在使用hibernate的session.load(),query.iterate()等方法时,Hibernate返回的是一个空对象(除主键属性外都是null),并没有去查询数据库;而是在使用返回对象的时候才会去查询数据库,并将查询结果注入到该对象中;这种查询时机推迟到对象访问的机制称之为延迟加载。
延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。
在Hibernate中提供了对实体对象的延迟加载以及对集合的延迟加载,另外在Hibernate3中还提供了对属性的延迟加载。
参考文章
需要注意:链表长度大于 8 个才转换红黑树
原因:
链表的时间复杂度是O(n),红黑树的时间复杂度O(logn)
因为红黑树需要进行左旋,右旋操作, 而单链表不需要,
以下都是单链表与红黑树结构对比。
如果元素小于8个,查询成本高,新增成本低
如果元素大于8个,查询成本低,新增成本高
【可以参考另外一篇笔记红黑树原理相关】
个人认为红黑树维护性更高,每次插入、删除的平均旋转次数远小于平衡树(最多只需要旋转三次)
具体来说,应该是往 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倍,也许还要大。
Compare and Swap,即比较再交换。
简概:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。
CAS算法原理
PageHelper详细说明
聚簇索引和非聚簇索引的区别
聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引;二级索引的叶子节点存放的是主键值或指向数据行的指针。
由于节子节点(数据页)只能按照一颗B+树排序,故一张表只能有一个聚簇索引。辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引
参考文章
Redis使用单线程,相比于多线程快在哪里?
Redis的瓶颈不在线程,不在获取CPU的资源,所以如果使用多线程就会带来多余的资源占用。比如上下文切换、资源竞争、锁的操作。
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()缺点:
epoll():
教学视频
参考文章1
参考文章2
开放地址法
以下条件缺一不可
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 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()方法后,才会到字符串常量池创建引用对象。当然有时候这个过程会自动完成,但情况比较复杂,难以确定。
有很多面试官其实自己也搞不清,所以不妨先告诉他创建了两个对象,然后再分析一番,效果更好。
参考文章
其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。
其他参考文章
B+树索引:具有动态平衡的特点。
1.支持范围查询。
2.等长的访问路径,访问时间根据数据量的变化相对稳定。
3.有明确的查找方向。
Hash索引:具有查找速度快的特点。
1.仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
2.无法被用来避免数据的排序操作。
3.不能利用部分索引键查询。
4.在任何时候都不能避免表扫描。
5.遇到大量Hash值相等的情况后性能降低。
总结:
数据量大选择B+树索引,数据量少选择Hash索引
ThreadLocal实现原理:
参考文章
参考文章1
参考文章2
参考文章
一些总结的较好的文章
答:最大区别是等待时wait会释放锁,而sleep会一直持有锁,wait通常用于线程时交,互,sleep通常被用于暂停执行。
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
3.volatile仅能实现变量的修改可见性,并不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
答:采用硬件提供原子操作指令实现的,即CAS。每次调用都会先判断预期的值是否符合,才进行写操作,保证数据安全。
答:spring主要功能有IOC,AOP,MVC等,IOC实现原理:先反射生成实例,然后调用时主动注入。AOP原理:主要使用java动态代理,
答:两者都是轻量级ORM框架,hibernate实现功能比较多,通过HQL操作数据库,比较简单方便,但hibernate自动生成的sql相长,不利测试和查找原因。复杂sql时,编写比较困难,同时性能也会降低。mybatis是半自动化,手动编写SQL语句,同时提供丰富的参数判断功能。sql语句较清晰,可以直接进行测试,性能也较好,操作起来非常简单。同时hibernate容易产生n+1问题。hibernate学习成本较mybatis高。国内一些大公司基本上使用mybatis
答:使用explain进行优化,查看sql是否充分使用索引。避免使用in,用exist替代,字段值尽可能使用更小的值,任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边。使用连接查询(join)代替子查询。
在表的多列字段上建立一个索引,但只有在查询这些字段的第一个字段时,索引才会被使用。
1:Post传输数据时,不需要在URL中显示出来,而Get方法要在URL中显示。
2:Post传输的数据量大,可以达到2M,而Get方法由于受到URL长度限制,只能 传递大约1024字节.
3:Post就是为了将数据传送到服务器段,Get就是为了从服务器段取得数据.而Get 之所以也能传送数据,只是用来设计告诉服务器,你到底需要什么样的数据.Post 的信息作为http请求的内容,而Get是在Http头部传输的。
查看线程:ps -ef|greptomcat
查看负载:top
文件内容查找:vi /aa test.txt 或者先打开文件,再查找: vi test.txt /aa
答:由栈和堆组成,栈是运行时单位,堆内存则分为年轻代、年老代、持久代等,年轻代中的对象经过几次的回收,仍然存在则被移到年老代;持久代主要是保存class,method,filed等对象。
sun回收机制:主要对年轻代和年老代中的存活对象进行回收,分为以下:
年轻代串行(Serial Copying)、年轻代并行(ParNew)、年老代串行(SerialMSC),年老代并行(Parallel Mark Sweep),年老代并发(Concurrent Mark-Sweep GC,即CMS)等等,目前CMS回收算法使用最广泛。
JVM调优主要是对堆内容和回收算法进行配置,需要对jdk产生的回收日志进行观察,同时通过工具(Jconsole,jProfile,VisualVM)对堆内存不断分析,这些优化是一个过程,需要不断地进行观察和维护。
相同点:
A. 两者都是抽象类,都不能实例化。
B. interface实现类及abstractclass的子类都必须要实现已经声明的抽象方法。
不同点:
A. interface需要实现,要用implements,而abstractclass需要继承,要用extends。
B. 一个类可以实现多个interface,但一个类只能继承一个abstractclass。
C. interface强调特定功能的实现,而abstractclass强调所属关系。
D. 尽管interface实现类及abstrctclass的子类都必须要实现相应的抽象方法,但实现的 形式不同。interface中的每一个方法都是抽象方法,都只是声明的 (declaration, 没有方 法体),实现类必须要实现。而abstractclass的子类可以有选择地实现。
使用:
abstract:在既需要统一的接口,又需要实例变量或缺省的方法的情况下,使用abstract;
interface:使用: 类与类之前需要特定的接口进行协调,而不在乎其如何实现。 作为能 够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识。需要将一组类 视为单一的类,而调用者只通过接口来与这组类发生联系。需要实现特定的多项功能, 而这些功能之间可能完全没有任何联系。