同步与异步
简单来说,可以将同步与异步看成发起IO请求的两种方式。
同步IO是指用户空间(进程或者线程)是主动发起IO请求的一方,系统内核是被动接收方。
异步IO则反过来,系统内核是主动发起IO请求的一方,用户空间是被动接收方。
泛型擦除
JVM并不知道泛型的存在,因为泛型在编译阶段就已经被处理成普通的类和方法。
- 若泛型类型没有指定具体类型,用Object作为原始类型;
- 若有限定类型
,使用XClass作为原始类型; - 若有多个限定
,使用第一个边界类型XClass1作为原始类型。
泛型擦除的目的:避免过多地创建类而造成运行时的过度消耗。
List删除的正确方式
错误方式(1):
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(DEL)) {
list.remove(i);
}
}
正序遍历删除会导致size变更,并且中间有元素遍历不到。
错误方式(2):
for(int x : list){
if(x.equals(DEL)) {
list.remove(x);
}
}
删除元素后会循环报错信息ConcurrentModificationException
。
正确方式(1):
for (int i = list.size() - 1; i >= 0; i--) {
if (list.get(i).equals(DEL)) {
list.remove(i);
}
}
逆序遍历,每次从数组末尾开始遍历。
正确方式(2):
Iterator<Integer> iter = list.iterator();
while (iter.hasNext()) {
int x = iter.next();
if (x.equals(DEL)) {
iter.remove();
}
}
迭代器的方式可以正常循环删除。
Object的常见方法
equals:检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。equals 方法规范:自反性、对称性、传递性、一致性、对于任何非空引用 x,x.equals(null) 返回 false。
hashCode:散列码是由对象导出的一个整型值,没有规律,每个对象都有默认散列码,值由对象存储地址得出。字符串散列码由内容导出,值可能相同。为了在集合中正确使用,一般需要同时重写 equals 和 hashCode,要求 equals 相同 hashCode 必须相同,hashCode 相同 equals 未必相同,因此 hashCode 是对象相等的必要不充分条件。
toString:打印对象时默认的方法,如果没有重写打印的是表示对象值的一个字符串。
clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。
finalize:确定一个对象死亡至少要经过两次标记,如果对象在可达性分析后发现没有与 GC Roots 连接的引用链会被第一次标记,随后进行一次筛选,条件是对象是否有必要执行 finalize 方法。假如对象没有重写该方法或方法已被虚拟机调用,都视为没有必要执行。如果有必要执行,对象会被放置在 F-Queue 队列,由一条低调度优先级的 Finalizer 线程去执行。虚拟机会触发该方法但不保证会结束,这是为了防止某个对象的 finalize 方法执行缓慢或发生死循环。只要对象在 finalize 方法中重新与引用链上的对象建立关联就会在第二次标记时被移出回收集合。由于运行代价高昂且无法保证调用顺序,在 JDK 9 被标记为过时方法,并不适合释放资源。
getClass:返回包含对象信息的类对象。
wait / notify / notifyAll:阻塞或唤醒持有该对象锁的线程。
JVM内存泄露的情况
- 大量使用static字段
- 未关闭的连接或者流
- 内部类引用外部类
- 不合理的
equals
和hashcode
- 不合理的
finalize
- 字符串的
intern
ThreadLocal
的key
对象直接进入老年代的情况
- 使用
-XX:PretenureSizeThreshold
设置可直接进入老年代的new对象大小,当创建的对象大于这个大小时直接进入老年代 - 使用
-XX:MaxTenuringThreshold
设置新生代对象的最大年龄,当对象不断晋升到最大年龄后,进入老年代 - 如果Survivor区中相同年龄的所有对象大小的综合大于Survivor空间的一般,年龄大于或等于该年龄的对象可以直接进入老年代
ClassNotFoundException,NoClassDefFoundError
ClassNotFoundException:
- java.lang.Class#forName(java.lang.String)
- java.lang.ClassLoader#findSystemClass(java.lang.String)
- java.lang.ClassLoader#loadClass(java.lang.String, boolean)
NoClassDefFoundError:
- Java虚拟机或者类加载器实例尝试加载类并且找不到类的定义时
- 搜索到的类定义在编译当前正在执行的类时存在,但已找不到该定义
equals()与hashcode()
```java // 比较对象的实际地址 public boolean equals(Object obj) { return (this == obj); }
// 返回对象地址的哈希值 public native int hashCode();
原始的equal方法直接比较对象地址,只有两个对象的地址相同(即为同一个对象)时,才会返回true。<br />原始的hashcode根据对象表示信息计算出int类型的哈希值,用于在对应的散列表中确认key是否一致。<br />当两个对象的地址相同时,计算出的哈希值是相同的,但是哈希值相同并不能代表两个对象的地址相同。(哈希冲突)
(1)equals方法和hashcode方法没有重写时
1. euqals为true,hashcode一定相同
1. hashcode相同,euqals不一定为true
(2)阿里巴巴开发手册——集合处理
1. 只要重写equals,就必须重写hashcode
1. 因为set存储的是不重复的对象,依据hashcode和equals进行判断,所以set存储的对象必须重写这两个方法
1. 如果自定义对象作为Map的键,那么必须覆写equals和hashcode(String因为重写了equals和hashcode方法,所以可以直接使用String对象作为key来使用)
<a name="DppHy"></a>
## @Autowired和@Value的区别
@Autowired默认按照类型进行装配,默认情况下要求依赖对象必须存在,可以设置属性required来允许null值,如果想使用名称装配需要结合@Qualifier注解一起使用。<br />@Value可以设置name和type两个属性实现不同的装配方式:
- 同时指定name和type,从容器中寻找唯一匹配的bean
- 指定name,进行名称装配
- 指定type,进行类型装配
- 都未指定,使用名称装配
<a name="I0EVF"></a>
## 浅拷贝与深拷贝的区别
浅拷贝:<br />(1)对于基本类型的成员对象:基础数据类型是值传递的,直接将原始对象的属性值赋值给新的对象。原始对象和克隆对象之间的基本类型属性不会共享。<br />(2)对于引用类型的成员对象,比如数组或者类对象:引用类型是引用传递,将原始对象的属性的内存地址赋值给新的成员变量,它们指向同一内存空间。原始对象和克隆对象之间的基本类型属性是共享的。<br />深拷贝:<br />(1)对于基本类型的成员对象:同浅拷贝,不会共享。<br />(2)对于引用类型的成员对象,比如数组或者类对象,会新建一个对象空间,然后拷贝原始对象的属性内容。原始对象和克隆对象之间的基本类型属性不会共享。<br />对于一个对象`Subject`,需要实现`cloneable`接口覆盖`clone`方法实现不同层次的拷贝:
```java
static class Subject implements Cloneable {
int id;
String name;
Subject sub;
// constructer
// getter and setter
@Override
public String toString() {
return "Subject(hashcode=" + hashCode() + ")[" +
"id=" + id + ", name='" + name + '\'' +
", sub=" + ((sub != null) ? sub.toString() : null) +
']';
}
}
public static void main(String[] args) throws CloneNotSupportedException {
Subject subject = new Subject(1, "K", new Subject(2, "A", null));
Subject clone = (Subject) subject.clone();
System.out.printf("修改前\n原始对象:%s\n克隆对象:%s\n", subject.toString(), clone.toString());
clone.setId(5); clone.setName("N");
Subject sub = clone.getSub();
sub.setName("3"); sub.setName("G");
System.out.printf("修改后\n原始对象:%s\n克隆对象:%s]\n", subject.toString(), clone.toString());
}
浅拷贝:
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
测试:
修改前
原始对象:Subject(hashcode=225534817)[id=1, name='K', sub=Subject(hashcode=1878246837)[id=2, name='A', sub=null]]
克隆对象:Subject(hashcode=929338653)[id=1, name='K', sub=Subject(hashcode=1878246837)[id=2, name='A', sub=null]]
修改后
原始对象:Subject(hashcode=225534817)[id=1, name='K', sub=Subject(hashcode=1878246837)[id=2, name='G', sub=null]]
克隆对象:Subject(hashcode=929338653)[id=5, name='N', sub=Subject(hashcode=1878246837)[id=2, name='G', sub=null]]]
深拷贝:
@Override
protected Object clone() throws CloneNotSupportedException {
Subject clone = (Subject) super.clone();
if (clone.sub != null)
clone.sub = (Subject) clone.sub.clone();
return clone;
}
测试:
修改前
原始对象:Subject(hashcode=225534817)[id=1, name='K', sub=Subject(hashcode=1878246837)[id=2, name='A', sub=null]]
克隆对象:Subject(hashcode=929338653)[id=1, name='K', sub=Subject(hashcode=1259475182)[id=2, name='A', sub=null]]
修改后
原始对象:Subject(hashcode=225534817)[id=1, name='K', sub=Subject(hashcode=1878246837)[id=2, name='A', sub=null]]
克隆对象:Subject(hashcode=929338653)[id=5, name='N', sub=Subject(hashcode=1259475182)[id=2, name='G', sub=null]]]
synchronized锁优化策略
JDK1.6对synchronized做了很多优化,引入了自适应自旋锁、锁消除、锁粗化、偏向锁和轻量级锁等提高锁的效率,锁一共有四个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率。
synchronized使用monitorenter和monitorexit这两个字节码指令获取和释放monitor。
自旋锁:
同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核态完成。许多应用上共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果机器有多个处理核心,我们可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待只需让线程执行一个忙循环。
自旋锁在JDK1.4中引入,默认关闭,在JDK1.6中默认开启。自旋不能替代阻塞,虽然避免了线程切换开销,但要占用处理器时间,如果锁被占用的时间很短,自旋的效果就会非常好,反之只会白白消耗处理器资源。如果自旋超过限定次数仍然没有获取到锁,那么就应该挂起线程,自旋默认次数是10。
自适应自旋:
JDK1.6对锁自旋进行了优化,自旋时间不再固定,而是由前一次的自旋时间及锁拥有者的状态决定。
如果在同一个锁上,自旋刚刚成功获得过锁且持有锁的线程正在运行,虚拟机会认为这次自旋也很有可能成功,进而允许自旋持续更久。如果自旋很少成功,以后获取锁时可能直接省略掉自旋,避免浪费处理器资源。
有了自适应自旋,随着程序运行时间的增长,虚拟机对程序锁的状况预测就会越来越精准。
锁消除:
即时编译器对检测不可能存在共享数据竞争的锁进行消除。
主要判定依据来源于逃逸分析,如果判断一段代码中堆上的所有数据都只被一个线程,就可以当作栈上的数据对待,认为它们是线程私有的而无须同步。
锁粗化:
原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待锁的线程尽快拿到锁。
但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把同步的范围扩展到整个操作序列的外部。
偏向锁:
偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步。
当锁对象第一次被线程获取时,虚拟机会将对象头MarkWord中的偏向模式设置为1,锁标志状态设置为01,同时使用CAS把获取到锁的线程ID记录在对象的MarkWord中。如果CAS成功,持有偏向锁的线程以后每次进入锁相关的同步块都不再进行任何同步操作。
一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销锁偏向,后续同步按照轻量级锁那样执行。
轻量级锁:
轻量级锁时为了没有在竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗。
在代码即将进入同步块时,如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录i空间,存储锁对象目前MarkWord的拷贝。然后虚拟机使用CAS尝试把对象的MarkWord更新为指向锁记录的指针,如果更新成功即代表该线程拥有了锁,锁标志位将转变为00,标识处于轻量级锁状态。
如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的MarkWord是否指向当前线程的栈帧,如果是则说明当前线程已经拥有了锁,直接进入同步块继续执行,否则说明锁对象已经被其他线程抢占。如果出现两条以上线程争用同一个锁,轻量级锁将进行锁膨胀,成为重量级锁,锁标志状态标为10,此时MarkWord存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
解锁同样通过CAS进行,如果对象MarkWord仍然指向线程的锁记录,就用CAS把对象的当前MarkWord和线程复制的MarkWord替换回来。加入替换成功同步过程就顺利完成了,如果失败则说明有其他线程尝试过获取该锁,就要在释放锁的同事唤醒被挂起的线程。
SpringMVC的工作流程
(1)请求发送到前端控制器DispatcherServlet。
(2)DispatcherServlet收到请求调用HandlerMapping处理器映射器。
(3)处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器一并返回给DispatcherServlet。
(4)DispatcherServlet调用HandlerAdapter处理器适配器。
(5)HandlerAdapter经过适配调用具体的处理器Controller。
(6)Controller执行完成返回ModelAndView。
(7)HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
(8)DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
(9)ViewReslover解析后返回具体View。
(10)DispatcherServlet根据View进行渲染视图。
(11)DispatcherServlet响应用户。
如何设计线程池
线程池讲白了就是一个存储线程的容器,池内保存之前建立过的线程来重新执行任务,减少创建和销毁线程的开销,提高任务的响应速度,并便于线程的管理。
我个人觉得如果要设计一个线程池的话得考虑池内工作线程的挂历、任务编排执行、线程池超负荷处理方案和监控。
初始化线程数、核心线程数和最大线程池都暴露出来可配置,包括超过核心线程数的线程空闲消亡时间。
任务的存储结构可配置,可以是无界队列也可以是有界队列,也可以根据配置分多个队列来分配不同优先级的任务,也可以采用stealing的机制来提高线程的利用率。
再提供配置来表明此线程池是IO密集型还是CPU密集型来改变任务的执行策略。
超负荷的方案可以有多种,包括丢弃任务、拒绝执行并抛出异常和丢弃最老任务或者其他等。
线程池埋好点暴露出用于监控的接口,如已处理任务数、待处理任务数、正在运行的线程数和拒绝的任务数等。