Fork me on GitHub

基础面试题

基础面试题

引言:

以下各方面知识点的面试题,是为了将要出来工作的小师妹和小师弟而精心整理的。希望对你们都帮助。这些面试题都是很基础的,希望你们能够好好利用起来。有问题,或者不对的地方欢迎给我留言哈!

forward 和redirect的区别

forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容是从哪儿来的,所以它的地址栏中还是原来的地址。

redirect就是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,一般来说浏览器会用刚才请求的所有参数重新请求,所以session,request参数都可以获取。

int 和 Integer 有什么区别

Java 提供两种不同的类型:引用类型和原始类型(或内置类型)。Int 是 java 的原始数据类型,Integer 是 java为int提供的封装类。Java为每个原始类型提供了封装类。
原始类型封装类,booleanBoolean,charCharacter,byteByte,shortShort,intInteger,longLong,floatFloat,doubleDouble
引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关

error和exception有什么区别

error 表示恢复不是不可能但很困难的情况下的一种严重问题。比如说内存溢出。不可能指望程序能处理这样的情况。exception 表示一种设计或实现问题。也就是说,它表示如果程序运行正常,从不会发生的情况。

最常见到的runtime exception

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ArithmeticException, ArrayStoreException,
BufferOverflowException, BufferUnderflowException,
CannotRedoException, CannotUndoException,
ClassCastException, CMMException,
ConcurrentModificationException, DOMException,
EmptyStackException, IllegalArgumentException,
IllegalMonitorStateException, IllegalPathStateException,
IllegalStateException, ImagingOpException,
IndexOutOfBoundsException, MissingResourceException,
NegativeArraySizeException, NoSuchElementException,
NullPointerException, ProfileDataException,
ProviderException, RasterFormatException, SecurityException,
SystemException, UndeclaredThrowableException,
UnmodifiableSetException, UnsupportedOperationException

Overload和Override区别,Overloaded方法可以改变返回值的类型吗

方法的重写Overriding和重载Overloading是Java多态性的不同表现。重写Overriding是父类与子类之间多态性的一种表现,重载Overloading是一个类中多态性的一种表现。如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被”屏蔽”了。如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overloading)。Overloaded的方法是可以改变返回值的类型。

OOP是什么

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

java中有哪些集合,主要方法有哪些

主要有LinkedList,ArrayList,Vector等。下面是详细:
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap
最常用的集合类是 List 和 Map。 List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建、存储和操作任何类型对象的元素列表。 List 适用于按数值索引访问元素的情形。 Map 提供了一个更通用的元素存储方法。 Map 集合类用于存储元素对(称作“键”和“值”)其中每个键映射到一个值。

List、Map、Set接口,存取元素时各自特点

List 以特定次序来持有元素,可有重复元素。Set 无法拥有重复元素,内部排序。Map 保存key-value值,value可多值。
List的遍历: List接口有size()和get()方法,用这两个方法可以实现对List的遍历。size()方法得到List中的元素个数。get()方法取得某个位置上的元素

HashMap与HashTable的区别

1、HashMap 是非线程安全的,HashTable 是线程安全的。

2、HashMap 的键和值都允许有 null 值存在,而 HashTable 则不行。

3、因为线程安全的问题,HashMap 效率比 HashTable 的要高。
HashMap 的实现机制:
维护一个每个元素是一个链表的数组,而且链表中的每个节点是一个 Entry[] 键值对的数据结构。
实现了 数组+链表 的特性,查找快,插入删除也快。
对于每个 key , 他对应的数组索引下标是 int i = hash(key.hashcode)&(len-1);
每个新加入的节点放在链表首,然后该新加入的节点指向原链表首

Hashcode的作用

Java中的集合有两类,一类是List,再有一类是Set。前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复

equals方法可用于保证元素不重复,但如果每增加一个元素就检查一次,若集合中现在已经有1000个元素,那么第1001个元素加入集合时,就要调用1000次equals方法。这显然会大大降低效率。?于是,Java采用了哈希表的原理

哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。

这样一来,当集合要添加新的元素时,先调用这个元素的HashCode方法,就一下子能定位到它应该放置的物理位置上

(1)如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了。

(2)如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了。

(3)不相同的话,也就是发生了Hash key相同导致冲突的情况,那么就在这个Hash key的地方产生一个链表,将所有产生相同HashCode的对象放到这个单链表上去,串在一起(很少出现)。

这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

如何理解HashCode的作用:

从Object角度看,JVM每new一个Object,它都会将这个Object丢到一个Hash表中去,这样的话,下次做Object的比较或者取这个对象的时候(读取过程),它会根据对象的HashCode再从Hash表中取这个对象。这样做的目的是提高取对象的效率。若HashCode相同再去调用equal。

HashMap,ConcurrentHashMap与LinkedHashMap的区别

ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的,锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问
ConcurrentHashMap 是在每个段(segment)中线程安全的
LinkedHashMap维护一个双链表,可以将里面的数据按写入的顺序读出

ConcurrentHashMap应用场景
1:ConcurrentHashMap 的应用场景是高并发,但是并不能保证线程安全,而同步的 HashMap 和 HashMap 的是锁住整个容器,而加锁之后 ConcurrentHashMap 不需要锁住整个容器,只需要锁住对应的 Segment 就好了,所以可以保证高并发同步访问,提升了效率。
2:可以多线程写。
ConcurrentHashMap把HashMap分成若干个Segmenet
1.get时,不加锁,先定位到segment然后在找到头结点进行读取操作。而value是volatile变量,所以可以保证在竞争条件时保证读取最新的值,如果读到的value是null,则可能正在修改,那么久调用ReadValueUnderLock函数,加锁保证读到的数据是正确的。

2.Put时会加锁,一律添加到hash链的头部。

3.Remove时也会加锁,由于next是final类型不可改变,所以必须把删除的节点之前的节点都复制一遍。

4.ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对Hash表的不同Segment进行的修改。
ConcurrentHashMap的应用场景是高并发,但是并不能保证线程安全,而同步的HashMap和HashTable的是锁住整个容器,而加锁之后ConcurrentHashMap不需要锁住整个容器,只需要锁住对应的segment就好了,所以可以保证高并发同步访问,提升了效率。

HashMap的hashcode的作用

hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的。

如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同。

如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点。

两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。

什么时候需要重写?

一般的地方不需要重载hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会重载hashCode,那么为什么要重载hashCode呢?

要比较两个类的内容属性值,是否相同时候,根据hashCode 重写规则,重写类的 指定字段的hashCode(),equals()方法。

Vector和ArrayList的区别

首先看这两类都实现List接口,而List接口一共有三个实现类,分别是 ArrayList、Vector 和 LinkedList 。List 用于存放多个元素,能够维护元素的次序,并且允许元素的重复。3个具体实现类的相关区别如下:

1.ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
2.Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
3.LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

ArrayList 与 LinkedList 的区别

最明显的区别是 ArrrayList 底层的数据结构是数组,支持随机访问,而 LinkedList 的底层数据结构是链表,不支持随机访问。使用下标访问一个元素,ArrayList 的时间复杂度是 O(1),而 LinkedList 是 O(n)。
1.LinkedList内部存储的是Node,不仅要维护数据域,还要维护prev和next,如果LinkedList中的结点特别多,则LinkedList比ArrayList更占内存。
插入删除操作效率:
2.LinkedList在做插入和删除操作时,插入或删除头部或尾部时是高效的,操作越靠近中间位置的元素时,需要遍历查找,速度相对慢一些,如果在数据量较大时,每次插入或删除时遍历查找比较费时。所以LinkedList插入与删除,慢在遍历查找,快在只需要更改相关结点的引用地址。
ArrayList在做插入和删除操作时,插入或删除尾部时也一样是高效的,操作其他位置,则需要批量移动元素,所以ArrayList插入与删除,快在遍历查找,慢在需要批量移动元素。
3.循环遍历效率:
由于ArrayList实现了RandomAccess随机访问接口,所以使用for(int i = 0; i < size; i++)遍历会比使用Iterator迭代器来遍历快
而由于LinkedList未实现RandomAccess接口,所以推荐使用Iterator迭代器来遍历数据。
因此,如果我们需要频繁在列表的中部改变插入或删除元素时,建议使用LinkedList,否则,建议使用ArrayList,因为ArrayList遍历查找元素较快,并且只需存储元素的数据域,不需要额外记录其他数据的位置信息,可以节省内存空间。

Java 中的 LinkedList 是单向链表还是双向链表

是双向链表。

String、StringBuffer、StringBuilder之间区别

1.三者在执行速度方面的比较:StringBuilder > StringBuffer > String
2.在线程方面:StringBuilder是线程非安全的;StringBuffer是线程安全的

3.对于三者的使用:如果要操作少量的数据用 = String;单线程操作字符串缓冲区 下操作大量数据 = StringBuilder;多线程操作字符串缓冲区 下操作大量数据 = StringBuffer;

Object 的常用方有哪些

clone()、equals()、hashCode()、notify()、notifyAll()、toString()、wait()、finalize()

Java序列化的方式

(1).Java原生以流的方法进行的序列化

(2).Json序列化

(3).FastJson序列化

(4).Protobuff序列化

传值和传引用的区别,Java是怎么样的,有没有传值引用

定义:

传值:传递的是值的副本。方法中对副本的修改,不会影响到调用方

传引用:传递的是引用的副本,共用一个内存,会影响到调用方。此时,形参和实参指向同一个内存地址。
对引用副本本身(对象地址)的修改,如设置为null,重新指向其他对象,不会影响到调用方。

总结:

1.基本类型(byte,short,int,long,double,float,char,boolean)为传值

2.对象类型(Object,数组,容器)为传引用

3.String、Integer、Double等immutable类型因为类的变量设为final属性,无法被修改,只能重新赋值或生成对象。
当Integer作为方法参数传递时,对其赋值会导致原有的引用被指向了方法内的栈地址,失去原有的的地址指向,所以对赋值后的Integer做任何操作都不会影响原有值。

补充:

值传递和引用传递,属于函数调用时参数的求值策略(Evaluation Strategy),这是对调用函数时,求值和传值的方式的描述,而非传递的内容的类型(内容指:是值类型还是引用类型,是值还是指针)。值类型/引用类型,是用于区分两种内存分配方式,值类型在调用栈上分配,引用类型在堆上分配。(不要问我引用类型里定义个值类型成员或反之会发生什么,这不在这个本文的讨论范畴内,而且你看完之后,你应该可以自己想明白)。一个描述内存分配方式,一个描述参数求值策略,两者之间无任何依赖或约束关系。

一个ArrayList在循环过程中删除,会不会出问题

会,发报出并发修改异常Java.util.ConcurrentModificationException。

错误原因都是ArrayList集合中remove方法底层的源码中有一个fastRemove(index)方法,然后会有一个modCount++的操作,然后在ArratList内部的迭代器中有一个checkForComodification操作,也就是检查modCount是否改变,如果改变了,就抛出并发修改错误。
同样的在For each增强for循环中,也是利用了ArrayList自身的Iterator迭代器,也是会出现这样的错误。

对于一般的for遍历,可能并没有删除要修改的数,可以采用倒序删除的写法改正这个错误。
对于增强for循环中的遍历,会抛出并发修改异常,使用Iterator自己的remove方法。

要避免这种情况的出现,则在使用迭代器迭代时(显式或for each的隐式)不要使用ArrayList的remove,改用Iterator的remove即可。

@transactional注解在什么情况下会失效

1.@Transactional 注解只能应用到 public 可见度的方法上。 如果应用在protected、private或者 package可见度的方法上,也不会报错,不过事务设置不会起作用。

2.默认情况下,Spring会对unchecked异常进行事务回滚;如果是checked异常则不回滚。
辣么什么是checked异常,什么是unchecked异常。

3.只读事务:
@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)
只读标志只在事务启动时应用,否则即使配置也会被忽略。
启动事务会增加线程开销,数据库因共享读取而锁定(具体跟数据库类型和事务隔离级别有关)。通常情况下,仅是读取数据时,不必设置只读事务而增加额外的系统开销。

JVM的内存结构

主要分为6个区域:

程序计数器:可看做是当前线程执行的字节码的行号指示器,字节码解释器就是通过改变这个计数器的值来获取下一条需要执行的字节码指令,完成分支、循环、跳转和异常处理等功能。

虚拟机栈:每创建一个线程时,JVM就会为这个线程创建一个对应的栈,所以栈是线程私有的。方法执行的时候还会创建一个栈帧在虚拟机栈上,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表所需的空间在编译期间就完成了分配。

本地方法栈:基本同虚拟机栈,只不过本地方法栈是为本地方法服务。

java堆:JVM管理的内存最大的部分,线程共享,用于存放对象实例。java堆可以处于物理上不连续的内存空间上。用-Xms表示堆起始内存大小,-Xmx表示堆最大内存大小。当堆内存大小大于-Xmx时抛出OutOfMemoryError异常。

方法区:存储已被JVM加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。在Hotspot实现中,它位于java堆上。可称为永久代(PermGen),使用-XX:MaxPermGen参数配置永久代最大内存。

运行时常量池:方法区的一部分。在class文件被JVM加载后,其常量池中的字面量和符号引用将被保存到这里。

补充了解:逃逸分析

常用的对象堆外存储技术需要基于逃逸分析(Escape Analysis)技术实现。其目标就是分析出对象的作用域。比如当一个对象定义在方法体内部时,它的受访范围就在方法体内,jvm会在栈帧中为其分配内存空间。但一旦被外部成员引用后,这个对象就发生了逃逸。

在JDK 6u23后,HotSpot就默认开启了逃逸分析,早期版本可用-XX:+DoEscapeAnalysis参数开启,-XX:+PrintEscapeAnalysis查看。

Mysql 的分页 SQL 语句

select * from tablename limit m,n(n是指从第m+1条开始,取n条)

Hibernate与MyBatis的异同

相同点:
Hibernate与MyBatis都可以是通过SessionFactoryBuider由XML配置文件生成SessionFactory,然后由SessionFactory 生成Session,最后由Session来开启执行事务和SQL语句。其中SessionFactoryBuider,SessionFactory,Session的生命周期都是差不多的。Hibernate和MyBatis都支持JDBC和JTA事务处理。
Mybatis优势:
MyBatis可以进行更为细致的SQL优化,可以减少查询字段。
MyBatis容易掌握,而Hibernate门槛较高。
Hibernate优势:
Hibernate的DAO层开发比MyBatis简单,Mybatis需要维护SQL和结果映射。
Hibernate对对象的维护和缓存要比MyBatis好,对增删改查的对象的维护要方便。
Hibernate数据库移植性很好,MyBatis的数据库移植性不好,不同的数据库需要写不同SQL。
Hibernate有更好的二级缓存机制,可以使用第三方缓存。MyBatis本身提供的缓存机制不佳。

Hibernate与MyBatis在sql优化方面异同

Hibernate的查询会将表中的所有字段查询出来,这一点会有性能消耗。Hibernate也可以自己写SQL来指定需要查询的字段,但这样就破坏了Hibernate开发的简洁性。
而Mybatis的SQL是手动编写的,所以可以按需求指定查询的字段。
Hibernate HQL语句的调优需要将SQL打印出来,而Hibernate的SQL被很多人嫌弃因为太丑了。
MyBatis的SQL是自己手动写的所以调整方便。但Hibernate具有自己的日志统计。Mybatis本身不带日志统计,使用Log4j进行日志记录。

Hibernate与MyBatis对象管理对比

Hibernate 是完整的对象/关系映射解决方案,它提供了对象状态管理(state management)的功能,使开发者不再需要理会底层数据库系统的细节。也就是说,相对于常见的 JDBC/SQL 持久层方案中需要管理 SQL 语句,Hibernate采用了更自然的面向对象的视角来持久化 Java 应用中的数据。
换句话说,使用 Hibernate 的开发者应该总是关注对象的状态(state),不必考虑 SQL 语句的执行。这部分细节已经由 Hibernate 掌管妥当,只有开发者在进行系统性能调优的时候才需要进行了解。而MyBatis在这一块没有文档说明,用户需要对对象自己进行详细的管理。

Jsp九大内置对象

1.Request: request对象主要用于客户端请求处理
2.Response: response对象提供了多个方法用来处理HTTP响应,可以调用response中的方法修改ContentType中的MIME类型以及实现页面的跳转等等,
3.Page: page对象有点类似于Java编程中的this指针,就是指当前JSP页面本身。page是java.lang.Object类的对象。
4.Session: session是与请求有关的会话期,它是java.servlet.http.HttpSession类的对象,用来表示和存储当前页面的请求信息。
5.Application: application是javax.servlet.ServletContext类对象的一个实例,用于实现用户之间的数据共享
6.Out:
7.Exception: exception内置对象是用来处理页面出现的异常错误
8.Config: config内置对象是ServletConfig类的一个实例。在Servlet初始化的时候,JSP引擎通过config向它传递信息。这种信息可以是属性名/值匹配的参数,也可以是通过ServletContext对象传递的服务器的有关信息。
9.pageContext: pageContext对象是一个比较特殊的对象。它相当于页面中所有其他对象功能的最大集成者,即使用它可以访问到本页面中所有其他对象

Comparator 与 Comparable 有什么不同

Comparable 接口用于定义对象的自然顺序,而 comparator 通常用于定义用户定制的顺序。Comparable 总是只有一个,但是可以有多个 comparator 来定义对象的顺序。

Collection 和 Collections的区别

Collection是集合类的上级接口,继承与他的接口主要有Set 和List.
Collections是针对集合类的一个帮助类,他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作。

String s = new String(“xyz”);创建了几个String Object

两个对象,一个是“xyx”,一个是指向“xyx”的引用对象s。

线程同步的方法

wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

Volatile和Synchronized四个不同点

  1. 粒度不同,前者锁对象和类,后者针对变量
  2. syn阻塞,volatile线程不阻塞
  3. syn保证三大特性,volatile不保证原子性
  4. syn编译器优化,volatile不优化
    volatile具备两种特性:
    保证此变量对所有线程的可见性,指一条线程修改了这个变量的值,新值对于其他线程来说是可见的,但并不是多线程安全的。
    禁止指令重排序优化。
    Volatile如何保证内存可见性:
    1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
    2.当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
    同步:就是一个任务的完成需要依赖另外一个任务,只有等待被依赖的任务完成后,依赖任务才能完成。
    异步:不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,只要自己任务完成了就算完成了,被依赖的任务是否完成会通知回来。(异步的特点就是通知)。
    打电话和发短信来比喻同步和异步操作。
    阻塞:CPU停下来等一个慢的操作完成以后,才会接着完成其他的工作。
    非阻塞:非阻塞就是在这个慢的执行时,CPU去做其他工作,等这个慢的完成后,CPU才会接着完成后续的操作。
    非阻塞会造成线程切换增加,增加CPU的使用时间能不能补偿系统的切换成本需要考虑。

SpringMVC运行原理

  1. 客户端请求提交到DispatcherServlet
  2. 由DispatcherServlet控制器查询HandlerMapping,找到并分发到指定的Controller中。
  3. Controller调用业务逻辑处理后,返回ModelAndView
  4. DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图
  5. 视图负责将结果显示到客户端

SpringMVC与Struts2区别与比较总结

1、Struts2是类级别的拦截, 一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文,而方法同时又跟一个url对应,所以说从架构本身上SpringMVC就容易实现restful url,而struts2的架构实现起来要费劲,因为Struts2中Action的一个方法可以对应一个url,而其类属性却被所有方法共享,这也就无法用注解或其他方式标识其所属方法了。

2、由上边原因,SpringMVC的方法之间基本上独立的,独享request response数据,请求数据通过参数获取,处理结果通过ModelMap交回给框架,方法之间不共享变量,而Struts2搞的就比较乱,虽然方法之间也是独立的,但其所有Action变量是共享的,这不会影响程序运行,却给我们编码 读程序时带来麻烦,每次来了请求就创建一个Action,一个Action对象对应一个request上下文。
3、由于Struts2需要针对每个request进行封装,把request,session等servlet生命周期的变量封装成一个一个Map,供给每个Action使用,并保证线程安全,所以在原则上,是比较耗费内存的。

4、 拦截器实现机制上,Struts2有以自己的interceptor机制,SpringMVC用的是独立的AOP方式,这样导致Struts2的配置文件量还是比SpringMVC大。

5、SpringMVC的入口是servlet,而Struts2是filter(这里要指出,filter和servlet是不同的。以前认为filter是servlet的一种特殊),这就导致了二者的机制不同,这里就牵涉到servlet和filter的区别了。

6、SpringMVC集成了Ajax,使用非常方便,只需一个注解@ResponseBody就可以实现,然后直接返回响应文本即可,而Struts2拦截器集成了Ajax,在Action中处理时一般必须安装插件或者自己写代码集成进去,使用起来也相对不方便。

7、SpringMVC验证支持JSR303,处理起来相对更加灵活方便,而Struts2验证比较繁琐,感觉太烦乱。

8、Spring MVC和Spring是无缝的。从这个项目的管理和安全上也比Struts2高(当然Struts2也可以通过不同的目录结构和相关配置做到SpringMVC一样的效果,但是需要xml配置的地方不少)。

9、 设计思想上,Struts2更加符合OOP的编程思想, SpringMVC就比较谨慎,在servlet上扩展。

10、SpringMVC开发效率和性能高于Struts2。
11、SpringMVC可以认为已经100%零配置。

简单总结springMVC和struts2的区别

  1. springmvc的入口是一个servlet即前端控制器,而struts2入口是一个filter过虑器。
  2. springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。
  3. Struts采用值栈存储请求和响应的数据,通过OGNL存取数据, springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。

SpringMvc怎么和AJAX相互调用的

通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象
具体步骤如下
1.加入Jackson.jar
2.在配置文件中配置json的映射
3.在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解

Spring有哪些优点

1.轻量级:Spring在大小和透明性方面绝对属于轻量级的,基础版本的Spring框架大约只有2MB。
2.控制反转(IOC):Spring使用控制反转技术实现了松耦合。依赖被注入到对象,而不是创建或寻找依赖对象。
3.面向切面编程(AOP): Spring支持面向切面编程,同时把应用的业务逻辑与系统的服务分离开来。
4.容器:Spring包含并管理应用程序对象的配置及生命周期。
5.MVC框架:Spring的web框架是一个设计优良的web MVC框架,很好的取代了一些web框架。
6.事务管理:Spring对下至本地业务上至全局业务(JAT)提供了统一的事务管理接口。
7.异常处理:Spring提供一个方便的API将特定技术的异常(由JDBC, Hibernate, 或JDO抛出)转化为一致的、Unchecked异常。

spring 主要使用了哪些 ,IOC和AOP实现原理是什么

spring主要功能有IOC,AOP,MVC等,IOC实现原理:先反射生成实例,然后调用时主动注入。AOP原理:主要使用java动态代理。

解释AOP模块

AOP(Aspect Oriented Programming) 面向切面编程,是目前软件开发中的一个热点,是Spring框架内容,利用AOP可以对业务逻辑的各个部分隔离,从而使的业务逻辑各部分的耦合性降低,提高程序的可重用性,踢开开发效率,主要功能:日志记录,性能统计,安全控制,事务处理,异常处理等。

AOP实现原理是java动态代理,但是jdk的动态代理必须实现接口,所以spring的aop是用cglib这个库实现的,cglis使用里asm这个直接操纵字节码的框架,所以可以做到不使用接口的情况下实现动态代理。

AOP与OOP的区别

OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。

举例:

对于“雇员”这样一个业务实体进行封装,自然是OOP的任务,我们可以建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP 设计思想对“雇员”进行封装则无从谈起。

同样,对于“权限检查”这一动作片段进行划分,则是AOP的目标领域。

OOP面向名次领域,AOP面向动词领域。

总之AOP可以通过预编译方式和运行期动态代理实现在不修改源码的情况下,给程序动态同意添加功能的一项技术。

IoC容器是什么其优点

Spring IOC负责创建对象、管理对象(通过依赖注入)、整合对象、配置对象以及管理这些对象的生命周期。
优点:
IOC或依赖注入减少了应用程序的代码量。它使得应用程序的测试很简单,因为在单元测试中不再需要单例或JNDI查找机制。简单的实现以及较少的干扰机制使得松耦合得以实现。IOC容器支持勤性单例及延迟加载服务。

Spring 的依赖注入方式有哪一些

Spring 的依赖注入可以有两种方式来完成:setter 方法注入和构造方法注入。
构造器依赖注入:构造器依赖注入在容器触发构造器的时候完成,该构造器有一系列的参数,每个参数代表注入的对象。
Setter方法依赖注入:首先容器会触发一个无参构造函数或无参静态工厂方法实例化对象,之后容器调用bean中的setter方法完成Setter方法依赖注入。

Spring支持的事务管理类型

Spring支持如下两种方式的事务管理:
编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者XML配置管理事务。

ThreadLocal(线程变量副本)

Synchronized实现内存共享,ThreadLocal为每个线程维护一个本地变量。

采用空间换时间,它用于线程间的数据隔离,为每一个使用该变量的线程提供一个副本,每个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。

ThreadLocal类中维护一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值为对应线程的变量副本。

ThreadLocal在Spring中发挥着巨大的作用,在管理Request作用域中的Bean、事务管理、任务调度、AOP等模块都出现了它的身影。

Spring中绝大部分Bean都可以声明成Singleton作用域,采用ThreadLocal进行封装,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

throw 和 throws 的区别

throw 用于抛出 java.lang.Throwable 类的一个实例化对象,意思是说你可以通过关键字 throw 抛出一个 Error 或者 一个Exception,如:

throw new IllegalArgumentException(“size must be multiple of 2″)

而throws 的作用是作为方法声明和签名的一部分,方法被抛出相应的异常以便调用者能处理。Java 中,任何未处理的受检查异常强制在 throws 子句中声明。

final关键字的作用

final class 表示此类不允许有子类。
final virable 表示一个常量。
final method 表示一个方法不能被重写

static关键字有哪些作用

static 修饰变量、修饰方法;静态块;静态内部类;静态导包;

String是最基本的数据类型吗

基本数据类型包括byte、int、char、long、float、double、boolean和short。java.lang.String类是final类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用StringBuffer类。

synchronized和java.util.concurrent.locks.Lock的异同

主要相同点:
Lock 能完成 synchronized 所实现的所有功能.
主要不同点:
Lock 有比 synchronized 更精确的线程语义和更好的性能(在相同点中回答此点也行)
synchronized 会自动释放锁. 而 Lock 一定要求程序员手工释放.并且必须在 finally 从句
中释放,如果没有答出在 finally 中释放不得分.就如 Connection 没有在 finally 中关闭一
样.连最基本的资源释放都做不好,还谈什么多线程编程.

spring的事务有几种它的隔离级别和传播行为

声明式事务和编程式事务
隔离级别:

  • DEFAULT使用数据库默认的隔离级别
  • READ_UNCOMMITTED会出现脏读,不可重复读和幻影读问题
  • READ_COMMITTED会出现重复读和幻影读
  • REPEATABLE_READ会出现幻影读
  • SERIALIZABLE最安全,但是代价最大,性能影响极其严重
    和传播行:
  • REQUIRED存在事务就融入该事务,不存在就创建事务
  • SUPPORTS存在事务就融入事务,不存在则不创建事务
  • MANDATORY存在事务则融入该事务,不存在,抛异常
  • REQUIRES_NEW总是创建新事务
  • NOT_SUPPORTED存在事务则挂起,一直执行非事务操作
  • NEVER总是执行非事务,如果当前存在事务则抛异常
  • NESTED嵌入式事务

sleep() 和 wait() 有什么区别

最大区别是等待时wait会释放锁,而sleep会一直持有锁,wait通常用于线程时交互,sleep通常被用于暂停执行。

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

多线程和同步有几种实现方法

多线程有两种实现方法,分别是继承Thread类与实现Runnable接口
同步的实现方面有两种,分别是synchronized,wait与notify

启动一个线程是用run()还是start()

启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。run()方法可以产生必须退出的标志来停止一个线程。

final,finally,finalize的区别

final—修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能重载。
  finally—再异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
  finalize—方法名。Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize() 方法是在垃圾收集器删除对象之前对这个对象调用的。

abstract class和interface有什么区别

抽象类与接口的区别:
1.接口可以多重继承 ,抽象类不可以
2.接口定义方法,不给实现;而抽象类可以实现部分方法
3.接口中基本数据类型的数据成员,都默认为static和final,抽象类则不是
如果事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。
只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类。

Set里的元素不能重复,用==还是equals ()判断

Set里的元素是不能重复的,那么用iterator()方法来区分重复与否。equals()是判读两个Set是否相等。
equals()和==方法决定引用值是否指向同一对象equals()在类中被覆盖,为的是当两个分离的对象的内容和类型相配的话,返回真值。

struts 框架是如何体现MVC模式

struts 框架为开发者提供了MVC 的3个逻辑组成部分,主要由ActionServlet、Action和strust-config.xml配置文件组成控制层,由ActionForm 来承担模型层的功能,而struts 下的视图由JSP来完成。
处理请求:由ActionServlet接收请求,然后根据 struts-config.xml 中的配置,类判断由于哪个Action来处理请求和由哪个ActionForm来保存数据,在通过Action的返回值来判断应该由哪个JSP来负责页面的展示,最后由 JSP 来完成结果响应。

Hibernate 的实体存在哪几种状态

Hibernate 中的实体在它的生命周期里面,存在 3 中状态。
瞬时:new语句创建的实体类对象是就是瞬时状态,它一般没有id。
持久:存放在 Session 中的实体对象就属于持久状态,一般通过 save() 或 saveOrUpdate()等等,方法转换而来。
托管:实体中Session中脱离出来的时候,它的状态就属于托管状态了,尽管它具有 id 值,但已经不存在Session 中了,即使 实体中的数据发生变化也不能同步到数据库中。通过 close()、evict()等方法转化而来。

Hibernate 的get()和load()的区别

Hibernate 对于 load() 方法该方法认为数据一定存在于数据,可以放心的代理来延迟加载,如果在使用过程中发现了问题,只能抛出异常,而get()方法可以不存在。

为什么wait和notify方法要在同步块中调用

主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常。还有一个原因是为了避免wait和notify之间产生竞态条件。

线程有几种状态

在Java当中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

第一是创建状态。在生成线程对象,并没有调用该对象的start方法,这是线程处于创建状态;

第二是就绪状态。当调用了线程对象的start方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态

第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行run函数当中的代码。

第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend等方法都可以导致线程阻塞。

第五是死亡状态。如果一个线程的run方法执行结束,该线程就会死亡。对于已经死亡的线程,无法再使用start方法令其进入就绪状态。

有A、B、C线程,A线程输出A, B线程输出B, C线程输出C要求, 同时启动线程, 按顺序输出ABC

主要通过join方法来实现顺序输出ABC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package thread;
public class TestThread1 {
public static void main(String[] args) {
// 线程A
final Thread a = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("A");
}
});
// 线程B
final Thread b = new Thread(new Runnable() {
@Override
public void run() {
try {
// 执行b线程之前,加入a线程,让a线程执行
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
});
// 线程C
final Thread c = new Thread(new Runnable() {
@Override
public void run() {
try {
// 执行c线程之前,加入b线程,让b线程执行
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
});
// 线程D
Thread d = new Thread(new Runnable() {
@Override
public void run() {
try {
// 执行d线程之前,加入c线程,让c线程执行
c.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("D");
}
});
// 启动四个线程
a.start();
b.start();
c.start();
d.start();
}
}

什么是ThreadLocal变量

ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocal就是每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法,比如你可以用ThreadLocal让SimpleDateFormat变成线程安全的,因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它,如果为每个线程提供一个自己独有的变量拷贝,将大大提高效率。首先,通过复用减少了代价高昂的对象的创建个数。其次,你在没有使用高代价的同步或者不变性的情况下获得了线程安全。线程局部变量的另一个不错的例子是ThreadLocalRandom类,它在多线程环境中减少了创建代价高昂的Random对象的个数。

从使用场景的角度出发来介绍对ReentrantLock的使用,相对来说容易理解一些。

可重入锁的用处及实现原理

场景1:如果已加锁,则不再重复加锁

a、忽略重复加锁。
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

1
2
3
4
5
6
7
if (lock.tryLock()) { //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果
try {
//操作
} finally {
lock.unlock();
}
}

场景2:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

1
2
3
4
5
6
7
8
9
10
11
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException
}

场景3:如果发现该操作已经加锁,则等待一个一个加锁(同步执行,类似synchronized)

这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。

这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

ReentrantLock默认情况下为不公平锁

1
2
private ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
private ReentrantLock lock = new ReentrantLock(true); //公平锁
1
2
3
4
5
6
try {
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
//操作
} finally {
lock.unlock();
}

不公平锁与公平锁的区别:

公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

场景4:可中断锁

synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景3的另一种改进,没有超时,只能等待中断或执行完毕)

这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

1
2
3
4
5
6
7
8
try {
lock.lockInterruptibly();
//操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}

可重入概念:若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。

如何避免死锁

Java多线程中的死锁
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。

Java中活锁和死锁有什么区别

这是上题的扩展,活锁和死锁类似,不同之处在于处于活锁的线程或进程的状态是不断改变的,活锁可以认为是一种特殊的饥饿。一个现实的活锁例子是两个人在狭小的走廊碰到,两个人都试着避让对方好让彼此通过,但是因为避让的方向都一样导致最后谁都不能通过走廊。简单的说就是,活锁和死锁的主要区别是前者进程的状态可以改变但是却不能继续执行。

怎么检测一个线程是否拥有锁

我一直不知道我们竟然可以检测一个线程是否拥有锁,直到我参加了一次电话面试。在java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

Maven有哪些优点

优点如下:
简化了项目依赖管理:
易于上手,对于新手可能一个”mvn clean package”命令就可能满足他的工作
便于与持续集成工具(jenkins)整合
便于项目升级,无论是项目本身升级还是项目使用的依赖升级。
有助于多模块项目的开发,一个模块开发好后,发布到仓库,依赖该模块时可以直接从仓库更新,而不用自己去编译。
maven有很多插件,便于功能扩展,比如生产站点,自动发布版本等

Maven常见的依赖范围有哪些

1.compile:编译依赖,默认的依赖方式,在编译(编译项目和编译测试用例),运行测试用例,运行(项目实际运行)三个阶段都有效,典型地有spring-core等jar。
2.test:测试依赖,只在编译测试用例和运行测试用例有效,典型地有JUnit。
provided:对于编译和测试有效,不会打包进发布包中,典型的例子为servlet-api,一般的web工程运行时都使用容器的servlet-api。
3.runtime:只在运行测试用例和实际运行时有效,典型地是jdbc驱动jar包。
4.system: 不从maven仓库获取该jar,而是通过systemPath指定该jar的路径。
5.import: 用于一个dependencyManagement对另一个dependencyManagement的继承。

使用“Mvn Clean Package”进行项目打包,其过程执行了哪些动作

在这个命令中我们调用了maven的clean周期的clean阶段绑定的插件任务,以及default周期的package阶段绑定的插件任务
默认执行的任务有(maven的术语叫goal, 也有人翻译成目标,我这里用任务啦):

maven-clean-plugin:clean->
maven-resources-plugin:resources->
maven-compile-plugin:compile->
mavne-resources-plugin:testResources->
maven-compile-plugin:testCompile->
maven-jar-plugin:jar

Maven 多模块如何聚合

配置一个打包类型为pom的聚合模块,然后在该pom中使用元素声明要聚合的模块

缓存框架memcache和redis的区别?项目中,怎么去选择?

ehcache,memcache和redis等。

区别:

  1. Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等。
  2. Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
  3. 虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value 交换到磁盘。
  4. 过期策略–memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通 过例如expire 设定,例如expire name 10。
  5. 分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都 可以一主一从。
  6. 存储数据安全–memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化)。
  7. 灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复。
  8. Redis支持数据的备份,即master-slave模式的数据备份。

java的原子类,实现原理是什么

采用硬件提供原子操作指令实现的,即CAS。每次调用都会先判断预期的值是否符合,才进行写操作,保证数据安全。

数据库性能优化有哪些方法

使用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头部传输的。
  4. 其他 HTTP 请求方法
  • HEAD 与 GET 相同,但只返回 HTTP 报头,不返回文档主体。
  • PUT上传指定的 URI 表示。
    DELETE 删除指定资源。
  • OPTIONS 返回服务器支持的 HTTP 方法
  • CONNECT 把请求连接转换到透明的 TCP/IP 通道。

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)对堆内存不断分析,这些优化是一个过程,需要不断地进行观察和维护。

高并发时,又如何保证性能和数据正确

如果是单机内完成这些操作,那使用数据库的事务,即可轻松实现。

分布式事务如何实现

分布式事务可以采用分布式锁进行实现,目前zookeeper就提供此锁;分布式锁需要牺牲一定性能去实现,若业务支付最终一致性,那此方法是最佳方案。如在京东下订单,过一会才会告诉你订单审核通过,而不是马上响应订单结果。

抽象类和接口的区别,项目中如何使用它们

相同点:

  1. 两者都是抽象类,都不能实例化。
  2. interface实现类及abstractclass的子类都必须要实现已经声明的抽象方法。

不同点:

  1. interface需要实现,要用implements,而abstractclass需要继承,要用extends。
  2. 一个类可以实现多个interface,但一个类只能继承一个abstractclass。
  3. interface强调特定功能的实现,而abstractclass强调所属关系。
  4. 尽管interface实现类及abstrctclass的子类都必须要实现相应的抽象方法,但实现的形式不同。interface中的每一个方法都是抽象方法,都只是声明的 (declaration, 没有方法体),实现类必须要实现。而abstractclass的子类可以有选择地实现。

使用:

  1. abstract:在既需要统一的接口,又需要实例变量或缺省的方法的情况下,使用abstract;
  2. interface:类与类之前需要特定的接口进行协调,而不在乎其如何实现。 作为能够实现特定功能的标识存在,也可以是什么接口方法都没有的纯粹标识。需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系。需要实现特定的多项功能,而这些功能之间可能完全没有任何联系。

TCP通讯有几次握手,有使用过哪些socket框架

​ 3次握手,客户端–>服务端,服务端–>客户端,客户端–>服务端,当这些过程完成之后,才真正建立起通信。java中比较有名的socket框架有:mina,netty,都是韩国小棒子写的。

反射的作用与原理

反射概念:Java 反射是可以让我们在运行时,通过一个类的Class对象来获取它获取类的方法、属性、父类、接口等类的内部信息的机制。这种动态获取信息以及动态调用对象的方法的功能称为JAVA的反射。

作用:在任意一个方法里,

1.如果我知道一个类的名称/或者它的一个实例对象, 我就能把这个类的所有方法和变量的信息找出来(方法名,变量名,方法,修饰符,类型,方法参数等等所有信息)

2.如果我还明确知道这个类里某个变量的名称,我还能得到这个变量当前的值。

3.当然,如果我明确知道这个类里的某个方法名+参数个数类型,我还能通过传递参数来运行那个类里的那个方法。

反射机制主要提供了以下功能:

  • 在运行时判断任意一个对象所属的类。
  • 在运行时构造任意一个类的对象。
  • 在运行时判断任意一个类所具有的成员变量和方法。
  • 在运行时调用任意一个对象的方法。
  • 生成动态代理。

反射的原理:JAVA语言编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。

反射的实现API:反射的实现主要借助以下四个类:

1
2
3
4
Class:类的对象
Constructor:类的构造方法
Field:类中的属性对象
Method:类中的方法对象

java反射机制,反射生成类,可否访问私有变量

即动态生成java的实例,可以。

Java反射机制是一个非常强大的功能,在很多的项目比如Spring,Mybatis都都可以看到反射的身影。通过反射机制,我们可以在运行期间获取对象的类型信息。利用这一点我们可以实现工厂模式和代理模式等设计模式,同时也可以解决java泛型擦除等令人苦恼的问题。

获取一个对象对应的反射类,在Java中有三种方法可以获取一个对象的反射类,

  • 通过getClass()方法
  • 通过Class.forName()方法
  • 使用类.class
  • 通过类加载器实现,getClassLoader()

RPC是什么,有使用过哪些RPC框架

​ 远程进程调用,本地机器调用远程的服务,在项目规模大到一定程度,需要使用RPC相关框架进行服务化部署。如:hessian 、webservice等

jquery如何绑定页面某元素的点击事件

​ $(“#btn”).click(function(){ …. })

volatile实现原理

volatile如何保证可见性和禁止指令重排序的:

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个 内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

  • 它会 强制将对缓存的修改操作立即写入主存;

  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

  1. cookie数据存放在客户的浏览器上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用COOKIE。
  4. 单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
  5. 所以个人建议: 将登陆信息等重要信息存放为SESSION 其他信息如果需要保留,可以放在COOKIE中

session 分布式处理

第一种:粘性session

粘性Session是指将用户锁定到某一个服务器上,比如上面说的例子,用户第一次请求时,负载均衡器将用户的请求转发到了A服务器上,如果负载均衡器设置了粘性Session的话,那么用户以后的每次请求都会转发到A服务器上,相当于把用户和A服务器粘到了一块,这就是粘性Session机制。

第二种:服务器session复制

原理:任何一个服务器上的session发生改变(增删改),该节点会把这个 session的所有内容序列化,然后广播给所有其它节点,不管其他服务器需不需要session,以此来保证Session同步。

第三种:session共享机制

使用分布式缓存方案比如memcached、Redis,但是要求Memcached或Redis必须是集群。

原理:不同的 tomcat指定访问不同的主memcached。多个Memcached之间信息是同步的,能主从备份和高可用。用户访问时首先在tomcat中创建session,然后将session复制一份放到它对应的memcahed上

第四种:session持久化到数据库

原理:就不用多说了吧,拿出一个数据库,专门用来存储session信息。保证session的持久化。 优点:服务器出现问题,session不会丢失 缺点:如果网站的访问量很大,把session存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

第五种terracotta实现session复制

原理:就不用多说了吧,拿出一个数据库,专门用来存储session信息。保证session的持久化。 优点:服务器出现问题,session不会丢失 缺点:如果网站的访问量很大,把session存储到数据库中,会对数据库造成很大压力,还需要增加额外的开销维护数据库。

说说自定义注解的场景及实现

跟踪代码的依赖性,实现代替配置文件的功能。比较常见的是Spring等框架中的基于注解配置。

还可以生成文档常见的@See@param@return等。如@override放在方法签名,如果这个方法 并不是覆盖了超类方法,则编译时就能检查出。

使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节,在定义注解时,不能继承其他注解或接口。

HashSet 和 HashMap 区别

HashSet:

HashSet实现了Set接口,它不允许集合中出现重复元素。当我们提到HashSet时,第一件事就是在将对象存储在

HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有储存相同的对象。如果不重写上述两个方法,那么将使用下面方法默认实现:

public boolean add(Object obj)方法用在Set添加元素时,如果元素值重复时返回 “false”,如果添加成功则返回”true”

HashMap:

HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。Map接口有两个基本的实现TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null))HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。

public Object put(Object Key,Object value)方法用来将元素添加到map中。

总结:

HashMap 实现了 Map 接口;存储键值对;调用put()向map中添加元素;HashMap使用键(Key)计算Hashcode;HashMap相对于HashSet较快,因为它是使用唯一的键获取对象。

HashSet 实现了 Set 接口;仅存储对象;调用add()方法向Set中添加元素;HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false;HashSet较HashMap来说比较慢。

HashMap 的工作原理及代码实现

  1. HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序。
  2. 底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。
  3. HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。
  4. HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能
  5. 采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常

ConcurrentHashMap 的工作原理及代码实现

  1. ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
  2. 它使用了多个锁来控制对hash表的不同段进行的修改,每个段其实就是一个小的hashtable,它们有自己的锁。只要多个并发发生在不同的段上,它们就可以并发进行。
  3. ConcurrentHashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Entry对象。Hashtable底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。
  4. 与HashMap不同的是,ConcurrentHashMap使用多个子Hash表,也就是段(Segment)
  5. ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。

ConcurrentHashMap能完全替代HashTable吗

HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的
ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea 也将这个判断留给用户自己决定是否使用ConcurrentHashMap。

什么是强一致性和弱一致性

get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。

下面将结合代码和java内存模型相关内容来分析下put/get方法。put方法我们只需关注Segment#put,get方法只需关注Segment#get,在继续之前,先要说明一下Segment里有两个volatile变量:count和table;HashEntry里有一个volatile变量:value。

总结:ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。

ThreadLocal 原理分析

ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

每个线程中都保有一个 ThreadLocalMap 的成员变量,ThreadLocalMap 内部采用 WeakReference 数组保存,数组的key即为 ThreadLocal 内部的Hash值。

创建线程的方式及实现

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用三种方式来创建线程,如下所示:

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程

    继承Thread类创建线程

通过继承Thread类来创建并启动多线程的一般步骤如下

1】d定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。

2】创建Thread子类的实例,也就是创建了线程对象

3】启动线程,即调用线程的start()方法

代码实例

1
2
3
4
5
6
7
8
9
10
11
public class MyThread extends Thread{//继承Thread类
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

实现Runnable接口创建线程

通过实现Runnable接口创建并启动线程一般步骤如下:

1】定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体

2】创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象

3】第三部依然是通过调用线程对象的start()方法来启动线程

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}
public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread2 myThread=new MyThread2();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者 new Thread(new MyThread2()).start();
  }
}

使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法**功能要强大。

》call()方法可以有返回值

》call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

>boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务

>V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值

>V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException

>boolean isDone():若Callable任务完成,返回True

>boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

介绍了相关的概念之后,创建并启动有返回值的线程的步骤如下:

1】创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。

2】使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

3】使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

4】调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

代码实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Main {
  public static void main(String[] args){
   MyThread3 th=new MyThread3();
   //使用Lambda表达式创建Callable对象
   //使用FutureTask类来包装Callable对象
   FutureTask<Integer> future=new FutureTask<Integer>(
    (Callable<Integer>)()->{
      return 5;
    }
   );
   new Thread(task,"有返回值的线程").start();//实质上还是以Callable对象来创建并启动线程
   try{
    System.out.println("子线程的返回值:"+future.get());
//get()方法会阻塞,直到子线程执行结束才返回
   }catch(Exception e){
    ex.printStackTrace();
   }
  }
}

————————————–三种创建线程方法对比————————————–

实现Runnable和实现Callable接口的方式基本相同,不过是后者执行call()方法有返回值,后者线程执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:

1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。

2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。

4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

注:一般推荐采用实现接口的方式来创建多线程

sleep() 、join()、yield()有什么区别

sleep():方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态,但不会释放“锁标志”,不推荐使用。

wait():在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。

yield():暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。yield()只能使同优先级或更高优先级的线程有执行的机会。

join():等待调用join方法的线程结束,再继续执行。

sleep是针对于thread对象,wait是针对于Object对象。

ConcurrentHashMap如何保证线程安全

JDK 1.7及以前:

ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。

JDK 1.8:

Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。

插入时使用CAS算法:unsafe.compareAndSwapInt(this, valueOffset, expect, update)。 CAS(Compare And Swap)意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。插入时不允许key或value为null

与Java8的HashMap有相通之处,底层依然由“数组”+链表+红黑树;

底层结构存放的是TreeBin对象,而不是TreeNode对象;

CAS作为知名无锁算法,那ConcurrentHashMap就没用锁了么?当然不是,当hash值与链表的头结点相同还是会synchronized上锁,锁链表。

new与newInstance()的区别

  • new是一个关键字,它是调用new指令创建一个对象,然后调用构造方法来初始化这个对象,可以使用带参数的构造器
  • newInstance()是Class的一个方法,在这个过程中,是先取了这个类的不带参数的构造器Constructor,然后调用构造器的newInstance方法来创建对象。
  • Class.newInstance不能带参数,如果要带参数需要取得对应的构造器,然后调用该构造器的Constructor.newInstance(Object … initargs)方法

JDK中用到的设计模式

  • 装饰模式:java.io
  • 单例模式:Runtime类
  • 简单工厂模式:Integer.valueOf方法
  • 享元模式:String常量池、Integer.valueOf(int i)、Character.valueOf(char c)
  • 迭代器模式:Iterator
  • 职责链模式:ClassLoader的双亲委派模型
  • 解释器模式:正则表达式java.util.regex.Pattern

hashCode() && equals()

hashcode() 返回该对象的哈希码值,支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。

在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改(equals默认返回对象地址是否相等)。如果根据 equals(Object)方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。

以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能

实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧I。)

  • hashCode的存在主要是用于查找的快捷性,如 Hashtable,HashMap等,hashCode 是用来在散列存储结构中确定对象的存储地址的;
  • 如果两个对象相同,就是适用于 equals(java.lang.Object) 方法,那么这两个对象的 hashCode 一定要相同;
  • 如果对象的 equals 方法被重写,那么对象的 hashCode 也尽量重写,并且产生 hashCode使用的对象,一定要和 equals 方法中使用的一致,否则就会违反上面提到的第2点;
  • 两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”

Object类的finalize方法的实现原理

Object 类提供的实现不Finalize方法和垃圾回收器将派生的类型不标记Object终止除非它们将覆盖Finalize方法。

如果类型未重写Finalize方法,则垃圾回收器会将类型的每个实例的条目添加到调用终止队列中的内部结构。 终止队列中包含垃圾回收器才能回收其内存之前,必须运行其终止代码托管堆中的所有对象的条目。 然后,垃圾回收器调用Finalize在以下情况下自动的方法︰

  • 垃圾回收器发现,一个对象不可访问,除非您通过调用从终止豁免已对象后 GC.SuppressFinalize 方法。
  • 在关闭应用程序域中,除非该对象是免于终止的对象。 在关闭期间,终止甚至仍是可访问的对象。

Finalize将自动调用一次在给定实例中,除非的对象重新注册通过使用一种机制,如GC.ReRegisterForFinalize和GC.SuppressFinalize尚未随后调用方法。

Finalize操作具有以下限制︰

  • 终结器执行时的确切时间不确定。 若要确保确定性释放资源,对你的类的实例实现Close方法,或者提供IDisposable.Dispose实现。
  • 两个对象的终结器不保证任何特定顺序运行即使另一个对象引用。 也就是说,如果对象 A 具有对对象 B 的引用,并且二者的终结器,对象 B 可能已经被终结的对象 A 终结器启动时。
  • 终结器运行的线程未指定。

Finalize方法可能无法运行完成,或可能根本不运行下列异常情况下︰

  • 如果另一个终结器会无限期阻止 (进入无限循环,尝试获取的锁,它可以永远不会获取,等等)。 运行时尝试运行终结器来完成,因为其他终结器可能不会调用终结器块如果无限期。
  • 如果不提供机会清理的运行时,进程将终止。 在这种情况下,运行时的第一个通知的进程是终止的一个 DLL_PROCESS_DETACH 通知。

运行时将继续完成在关闭过程的对象,仅当可终结对象数目继续减少。

如果Finalize或的重写Finalize引发异常,并且运行时不承载的应用程序将替代默认策略,运行时终止进程,且无活动try/finally执行块或终结器。如果终结器无法释放或销毁资源,则此行为确保处理完整性。

实施者注意事项

应重写Finalize类使用非托管的资源,如文件句柄或数据库必须在垃圾回收期间放弃使用它们的托管的对象时释放的连接。

Lock是Java 5以后引入的新的API,和关键字synchronized相比主要相同点:Lock 能完成synchronized所实现的所有功能;主要不同点:Lock有比synchronized更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且最好在finally 块中释放(这是释放外部资源的最好的地方)。

CAS 乐观锁

CAS是通过unsafe类的compareAndSwap方法实现的;方法参数作用,第一个参数是要修改的对象,第二个参数是对象中要修改变量的偏移量,第三个参数是修改之前的值,第四个参数是预想修改后的值;CAS指令有缺点,存在ABA问题。

ABA 问题

就是一个变量V,如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。解决:针对这种情况,java并发包中提供了一个带有标记的原子引类”AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。

CAS有什么缺陷,该如何解决

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
  2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

乐观锁的业务场景及实现方式

1.我们经常会在访问数据库的时候用到锁,怎么实现乐观锁和悲观锁呢?以Hibernate为例,可以通过为记录添加版本或时间戳字段来实现乐观锁。可以用session.Lock()锁定对象来实现悲观锁(本质上就是执行了SELECT * FROM t FOR UPDATE语句)。

2.如果把乐观锁看作是关于冲突检测的,那么悲观锁就是关于冲突避免的。在实际应用的源代码控制系统中,

这两种策略都可以被使用,但是现在大多数源代码开发者更倾向于使用乐观锁策略。(有一种很有道理的说法:乐观锁并不是真正的锁定,但是这种叫法很方便并且广泛流传,以至于不容忽略。)

在乐观锁和悲观锁之间进行选择的标准是:冲突的频率与严重性。如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略。

访问修饰符public,private,protected,以及不写时的区别

修饰符 当前类 同 包 子 类 其他包
public
protected ×
default × ×
private × × ×

类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java中,外部类的修饰符只能是public或默认,类的成员(包括内部类)的修饰符可以是以上四种。

String 是不是最基本的数据类型

不是。Java中的基本数据类型只有8个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type),Java 5以后引入的枚举类型也算是一种比较特殊的引用类型。

short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗

对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋值给short型可修改为s1 =(short)(s1 + 1)。而short s1 = 1; s1 += 1;可以正确编译,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。

是否可以继承String类

String 类是final类,不可以被继承。

补充:继承String本身就是一个错误的行为,对String类型最好的重用方式是关联关系(Has-A)和依赖关系(Use-A)而不是继承关系(Is-A)

JVM加载class文件的原理机制

JVM中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java中的类加载器是一个重要的Java运行时系统组件,它负责在运行时查找和装入类文件中的类。
由于Java的跨平台性,经过编译的Java源程序并不是一个可执行程序,而是一个或多个类文件。当Java程序需要使用某个类时,JVM会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件,然后产生与所加载类对应的Class对象。加载完成后,Class对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后JVM对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;2)如果类中存在初始化语句,就依次执行这些初始化语句。
类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader的子类)。从Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM更好的保证了Java平台的安全性,在该机制中,JVM自带的Bootstrap是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM不会向Java程序提供对Bootstrap的引用。下面是关于几个类加载器的说明:

  • Bootstrap:一般用本地代码实现,负责加载JVM基础核心类库(rt.jar);
  • Extension:从java.ext.dirs系统属性所指定的目录中加载类库,它的父加载器是Bootstrap;
  • System:又叫应用类加载器,其父类是Extension。它是应用最广泛的类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中记载类,是用户自定义加载器的默认父加载器。

静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同

Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化,其语法看起来挺诡异的,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 扑克类(一副扑克)
* @author 骆昊
*
*/
public class Poker {
private static String[] suites = {"黑桃", "红桃", "草花", "方块"};
private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};
private Card[] cards;
/**
* 构造器
*
*/
public Poker() {
cards = new Card[52];
for(int i = 0; i < suites.length; i++) {
for(int j = 0; j < faces.length; j++) {
cards[i * 13 + j] = new Card(suites[i], faces[j]);
}
}
}
/**
* 洗牌 (随机乱序)
*
*/
public void shuffle() {
for(int i = 0, len = cards.length; i < len; i++) {
int index = (int) (Math.random() * len);
Card temp = cards[index];
cards[index] = cards[i];
cards[i] = temp;
}
}
/**
* 发牌
* @param index 发牌的位置
*
*/
public Card deal(int index) {
return cards[index];
}
/**
* 卡片类(一张扑克)
* [内部类]
* @author 骆昊
*
*/
public class Card {
private String suite; // 花色
private int face; // 点数
public Card(String suite, int face) {
this.suite = suite;
this.face = face;
}
@Override
public String toString() {
String faceStr = "";
switch(face) {
case 1: faceStr = "A"; break;
case 11: faceStr = "J"; break;
case 12: faceStr = "Q"; break;
case 13: faceStr = "K"; break;
default: faceStr = String.valueOf(face);
}
return suite + faceStr;
}
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PokerTest {
public static void main(String[] args) {
Poker poker = new Poker();
poker.shuffle(); // 洗牌
Poker.Card c1 = poker.deal(0); // 发第一张牌
// 对于非静态内部类Card
// 只有通过其外部类Poker对象才能创建Card对象
Poker.Card c2 = poker.new Card("红心", 1); // 自己创建一张牌
System.out.println(c1); // 洗牌后的第一张
System.out.println(c2); // 打印: 红心A
}
}

GC是什么?为什么要有GC?

GC是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。Java程序员不用担心内存管理,因为垃圾收集器会自动进行管理。要请求垃圾收集,可以调用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉显示的垃圾回收调用。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在Java诞生初期,垃圾回收是Java最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今Java的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得iOS的系统比Android系统有更好的用户体验,其中一个深层次的原因就在于Android系统中垃圾回收的不可预知性。

补充:垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:

伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。

幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。

终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。

String s = new String(“xyz”);创建了几个字符串对象

两个对象,一个是静态区的”xyz”,一个是用new创建在堆上的对象。

如何实现字符串的反转及替换

方法很多,可以自己写实现也可以使用String或StringBuffer/StringBuilder中的方法。有一道很常见的面试题是用递归实现字符串反转,代码如下所示:

public static String reverse(String originStr) {
​ if(originStr == null || originStr.length() <= 1)
​ return originStr;
​ return reverse(originStr.substring(1)) + originStr.charAt(0);
​ }

List、Set、Map是否继承自Collection接口

List、Set 是,Map 不是。Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素且不允许有重复元素(数学中的集合也是如此),List是线性结构的容器,适用于按数值索引访问元素的情形。

List、Map、Set三个接口存取元素时,各有什么特点

List以特定索引来存取元素,可以有重复元素。Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B

不能。其它线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。

线程同步以及线程调度相关的方法

  • wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
  • sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
  • notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
  • notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;

启动一个线程是调用run()还是start()方法

启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM 调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。

Java中有几种类型的流

字节流和字符流。字节流继承于InputStream、OutputStream,字符流继承于Reader、Writer。在java.io 包中还有许多其他的流,主要是为了提高性能和使用方便。关于Java的I/O需要注意的有两点:一是两种对称性(输入和输出的对称性,字节和字符的对称性);二是两种设计模式(适配器模式和装潢模式)。另外Java中的流不同于C#的是它只有一个维度一个方向。

在项目中哪些地方用到了XML

XML的主要作用有两个方面:数据交换和信息配置。在做数据交换时,XML将数据用标签组装成起来,然后压缩打包加密后通过网络传送给接收者,接收解密与解压缩后再从XML文件中还原相关信息进行处理,XML曾经是异构系统间交换数据的事实标准,但此项功能几乎已经被JSON(JavaScript Object Notation)取而代之。当然,目前很多软件仍然使用XML来存储配置信息,我们在很多项目中通常也会将作为配置信息的硬代码写在XML文件中,Java的很多框架也是这么做的,而且这些框架都选择了dom4j作为处理XML的工具,因为Sun公司的官方API实在不怎么好用。

vector、ArrayList、LinkedList 的区别是什么

vector是同步的,arraylist和linkedlist不是同步的。底层方面,vector与arraylist都是基于object[]array实现的,但考虑vector线程安全,所以arraylist效率上回比vector较快。元素随机访问上,vector与arraylist是基本相同的,时间复杂度是O(1),linkedlist的随机访问元素的复杂度为O(n)。但在插入删除数据上,linkedlist则比arraylist要快很多。linkedlist比arraylist更占内存,因为linkedlist每个节点上还要存储对前后两个节点的引用。

NIO和传统的IO有什么区别

IO是面向流的,NIO是面向块(缓冲区)的。

IO面向流的操作一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。,导致了数据的读取和写入效率不佳。

NIO面向块的操作在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多,同时数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。通俗来说,NIO采取了“预读”的方式,当你读取某一部分数据时,他就会猜测你下一步可能会读取的数据而预先缓冲下来。

IO是阻塞的,NIO是非阻塞的

对于传统的IO,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

而对于NIO,使用一个线程发送读取数据请求,没有得到响应之前,线程是空闲的,此时线程可以去执行别的任务,而不是像IO中那样只能等待响应完成。

NIO和IO适用场景

NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长,NIO也有缺点,因为NIO是面向缓冲区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。

那么NIO和IO各适用的场景是什么呢?

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用NIO处理数据可能是个很好的选择。

而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。

通俗解释,最后,对于NIO和传统IO

有一个网友讲的生动的例子:

以前的流总是堵塞的,一个线程只要对它进行操作,其它操作就会被堵塞,也就相当于水管没有阀门,你伸手接水的时候,不管水到了没有,你就都只能耗在接水(流)上。

nio的Channel的加入,相当于增加了水龙头(有阀门),虽然一个时刻也只能接一个水管的水,但依赖轮换策略,在水量不大的时候,各个水管里流出来的水,都可以得到妥

善接纳,这个关键之处就是增加了一个接水工,也就是Selector,他负责协调,也就是看哪根水管有水了的话,在当前水管的水接到一定程度的时候,就切换一下:临时关上当

前水龙头,试着打开另一个水龙头(看看有没有水)。

当其他人需要用水的时候,不是直接去接水,而是事前提了一个水桶给接水工,这个水桶就是Buffer。也就是,其他人虽然也可能要等,但不会在现场等,而是回家等,可以做

其它事去,水接满了,接水工会通知他们。

这其实也是非常接近当前社会分工细化的现实,也是统分利用现有资源达到并发效果的一种很经济的手段,而不是动不动就来个并行处理,虽然那样是最简单的,但也是最浪费资源的方式

如何通过反射创建对象

  • 方法1:通过类对象调用newInstance()方法,例如:String.class.newInstance()
  • 方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance(“Hello”);

Statement与PreparedStatement的区别,什么是SQL注入,如何防止SQL注入

使用PreparedStatement可以提升代码的可读性和可维护性,可以尽最大可能提高性能。因为Statement每次执行一个SQL命令都会对其编译,但PreparedStatement则只编译一次。PreparedStatement就类似于流水线生产。另一方面PreparedStatement可以极大提高安全性:它对传递过来的参数进行了强制参数类型转换,确保插入或查询数据时,与底层数据库格式匹配。
SQL注入:就是通过将sql命令插入到web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意SQL命令。如sql命令:select id from test where name=’1’ or 1=1; drop table test,但用PreparedStatement就可以避免这种问题。

用Java写一个单例类

饿汉式单例:

public class Singleton {
​ private Singleton(){}
​ private static Singleton instance = new Singleton();
​ public static Singleton getInstance(){
​ return instance;
​ }
}

懒汉式单例:

public class Singleton {
​ private static Singleton instance = null;
​ private Singleton() {}
​ public static synchronized Singleton getInstance(){
​ if (instance == null) instance = new Singleton();
​ return instance;
​ }
}

用Java写一个冒泡排序

冒泡排序几乎是个程序员都写得出来,但是面试的时候如何写一个逼格高的冒泡排序却不是每个人都能做到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Comparator;
/**
* 排序器接口(策略模式: 将算法封装到具有共同接口的独立的类中使得它们可以相互替换)
* @author骆昊
*
*/
public interface Sorter {
/**
* 排序
* @param list 待排序的数组
*/
public <T extends Comparable<T>> void sort(T[] list);
/**
* 排序
* @param list 待排序的数组
* @param comp 比较两个对象的比较器
*/
public <T> void sort(T[] list, Comparator<T> comp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.util.Comparator;
/**
* 冒泡排序
*
* @author骆昊
*
*/
public class BubbleSorter implements Sorter {
@Override
public <T extends Comparable<T>> void sort(T[] list) {
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (list[j].compareTo(list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
@Override
public <T> void sort(T[] list, Comparator<T> comp) {
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (comp.compare(list[j], list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
}

char型变量中能不能存贮一个中文汉字

char型变量是用来存储Unicode编码的字符的,unicode编码字符集中包含了汉字,所以,char型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么,这个char型变量中就不能存储这个特殊汉字。补充说明:unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。

用最有效率的方法算出2乘以8等于几

2<< 3,(左移三位)因为将一个数左移n位,就相当于乘以了2的n次方,那么,一个数乘以8只要将其左移3位即可,而位运算cpu直接支持的,效率最高,所以,2乘以8等於几的最效率的方法是2<< 3。

静态变量和实例变量的区别

在语法定义上的区别:静态变量前要加 static 关键字,而实例变量前则不加。

在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。

例如,对于下面的程序,无论创建多少个实例对象,永远都只分配了一个staticVar变量,并且每创建一个实例对象,这个staticVar就会加1;但是,每创建一个实例对象,就会分配一个instanceVar,即可能分配多个instanceVar,并且每个instanceVar的值都只自加了1次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class VariantTest{
publicstatic int staticVar = 0;
publicint instanceVar = 0;
publicVariantTest(){
staticVar++;
instanceVar++;
System.out.println(staticVar +instanceVar);
}
}

switch语句能否作用在 byte 、 long 和String 上

在switch(e)中,e只能是一个整数表达式或者枚举常量(更大字体),整数表达式可以是int基本类型或Integer包装类型,由于byte,short,char都可以隐含转换为int,所以,这些类型以及这些类型的包装类型也是可以的。显然,long和String类型都不符合switch的语法规定,并且不能被隐式转换成int类型,所以,它们不能作用于swtich语句中。

switch语句能否作用在String上说错了,Java1.7之后已经支持这种写法了!

如何跳出当前的多重嵌套循环

在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break语句,即可跳出外层循环。

例如:

1
2
3
4
5
6
for(int i=0;i<10;i++){
for(intj=0;j<10;j++){
System.out.println(“i=” + i + “,j=” + j);
if(j == 5) break ok;
}
}

另外,我个人通常并不使用标号这种方式,而是让外层的循环条件表达式的结果可以受到里层循环体代码的控制,例如,要在二维数组中查找到某个数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int arr[][] ={{1,2,3},{4,5,6,7},{9}};
boolean found = false;
for(int i=0;i<arr.length&&!found;i++) {
for(intj=0;j<arr[i].length;j++){
System.out.println(“i=” + i + “,j=” + j);
if(arr[i][j] ==5) {
found =true;
break;
}
}
}

说说&和&&的区别

&和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。

​ &&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str!= null&& !str.equals(s))表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。If(x==33 &++y>0) y会增长,If(x==33 && ++y>0)不会增长

​ &还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。

一个”.java”源文件中是否可以包括多个类(不是内部类)有什么限制

可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。

可以从一个static方法内部发出对非static方法的调用吗

不可以。因为非static方法是要与对象关联在一起的,必须创建一个对象后,才可以在该对象上进行方法调用,而static方法调用时不需要创建对象,可以直接调用。也就是说,当一个static方法被调用时,可能还没有创建任何实例对象,如果从一个static方法中发出对非static方法的调用,那个非static方法是关联到哪个对象上的呢?这个逻辑无法成立,所以,一个static方法内部发出对非static方法的调用。

Hibernate中怎样实现类之间的关系

类与类之间的关系主要体现在表与表之间的关系进行操作,它们都是对对象进行操作,我们在程序中把所有的表与类都映射在一起,它们通过配置文件中的many-to-one、one-to-many、many-to-many进行操作。

Hibernate中的update()和saveOrUpdate()的区别

saveOrUpdate():

​ 1、如果对象已经在本session中持久化了,不做任何事

​ 2、如果另一个与本session关联的对象拥有相同的持久化标识(identifier),抛出一个异常

​ 3、如果对象没有持久化标识(identifier)属性,对其调用save()

​ 4、如果对象的持久标识(identifier)表明其是一个新实例化的对象,对其调用save()

​ 5、如果对象是附带版本信息的(通过)并且版本属性的值表明其是一个新实例化的 对象,调用save()。否则update() 这个对象。

update() :是将一个游离状态的实体对象直接更新。

Hibernate的缓存机制

  1. 一级缓存:内部缓存存在Hibernate中,属于应用事物级缓存。
  2. 二级缓存:应用级缓存、 分布式缓存。使用场景:数据不会被第三方修改、数据大小在可接受范围、数据更新频率低、同一数据被系统频繁使用、非关键数据
  3. 引入第三方缓存(如ehcache等)。

如何优化Hibernate

1.使用双向一对多关联,不使用单向一对多

2.灵活使用单向一对多关联

3.不用一对一,用多对一取代

4.配置对象缓存,不使用集合缓存

5.一对多集合使用Bag,多对多集合使用Set

6.继承类使用显式多态

7.表字段要少,表关联不要怕多,有二级缓存撑腰

hibernate的延迟加载和openSessionInView

延迟加载要在session范围内,用到的时候再加载;

opensessioninview是在web层写了一个filter来打开和关闭session,这样就表示在一次request过程中session一直开着,保证了延迟加载在session中的这个前提。

Mysql 优化

1.如果明确知道只有一条结果返回,limit1能够提高效率

2.把计算放在业务层而不是数据库层,除了节省数据的 CPU ,还有意想不到的查询缓存优化效果。

3.强制类型转换会全表扫描

4.在属性上进行计算不能命中索引

5.使用 ENUM 而不是字符串

6.数据分区度不大的字段不宜使用索引

7.负向查询和前导模糊查询不能使用索引

8.用TRUNCATE替代DELETE

9.删除重复记录

10.用Where子句替换HAVING子句

11.用EXISTS替代IN、用NOT EXISTS替代NOT IN

12.用索引提高效率

13.用EXISTS替换DISTINCT

14.用>=替代>

15.用IN来替换OR

Mysql 的交集、差集、并集

只有并集没有交集差集的关键字。

1.并集

1
2
3
4
-- UNION 不包含重复数据
-- UNION ALL 包含重复数据
SELECT NAME FROM a UNION
SELECT NAME FROM b;

2.差集

找出在a表中存在的id 但是在b表中不存在的id

1
2
3
4
5
6
7
-- 利用 union
SELECT ID FROM (
-- 并集
SELECT DISTINCT a.id AS ID FROM a
UNION ALL
SELECT DISTINCT B.ID AS ID FROM b
)TEMP GROUP BY ID HAVING COUNT(ID) = 1;
1
2
-- 子查询 not in
SELECT id FROM a WHERE id NOT IN (SELECT id FROM b);
1
2
-- 子查询 not exists
SELECT id FROM a WHERE NOT EXISTS (SELECT id FROM b WHERE a.id = b.id);
1
2
-- 左连接判断右表IS NULL
SELECT a.id FROM a LEFT JOIN b ON a.id = b.id WHERE b.id IS NULL ORDER BY a.id

3.交集 INTERSECT

1
2
3
4
5
6
SELECT ID FROM (
-- 并集
SELECT DISTINCT a.id AS ID FROM a
UNION ALL
SELECT DISTINCT B.ID AS ID FROM b
)TEMP GROUP BY ID HAVING COUNT(ID) != 1;

Java内存模型是什么

Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了:

​ 线程内的代码能够按先后顺序执行,这被称为程序次序规则。

​ 对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。

​ 前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。

​ 一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。

​ 一个线程的所有操作都会在线程终止之前,线程终止规则。

​ 一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。

可传递性

Thread接口和Runnable接口的区别

  1. 可以避免由于Java的单继承特性而带来的局限.
  2. 使用Runnable实现多线程可以达到资源共享目的。

Runnable接口和Callable接口的区别

Runnable应该是比较熟悉的接口,它只有一个run()函数,用于将耗时操作写在其中,该函数没有返回值,不能将结果返回给客户程序。然后使用某个线程去执行runnable即可实现多线程,Thread类在调用start()函数后就是执行的是Runnable的run()函数。Runnable的声明如下 :

1
2
3
public interface Runnable {
public abstract void run();
}

Callable与Runnable的功能大致相似,Callable中有一个call()函数,但是call()函数有返回值。

Callable的声明如下 :

1
2
3
public interface Callable<V> {
V call() throws Exception;
}

可以看到,这是一个泛型接口,call()函数返回的类型就是客户程序传递进来的V类型。
不同之处:
1.Callable可以返回一个类型V,而Runnable不可以;
2.Callable能够抛出checked exception,而Runnable不可以;
3.Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的;
4.Callable和Runnable都可以应用于executors。而Thread类只支持Runnable;
Callable与executors联合在一起,在任务完成时可立刻获得一个更新了的Future;而Runable却要自己处理。

5.加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法。

线程池的实现原理

先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又处于睡眠状态。

节约大量的的系统资源,使得更多的CPU时间和内存用来处理实际的商业应用,而不是频繁的线程创建与销毁。

线程池的几种方式

Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

(1).newCacheThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

(2). newFixedThreadPool:

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}

因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。

定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。可参考PreloadDataCache。

(3).newScheduledThreadPool:

创建一个定长线程池,支持定时及周期性任务执行。延迟执行示例代码如下:

1
2
3
4
5
6
7
8
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);

表示延迟3秒执行。

定期执行示例代码如下:

1
2
3
4
5
6
7
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);

表示延迟1秒后每3秒执行一次。

ScheduledExecutorService比Timer更安全,功能更强大

(4).newSingleThreadExecutor:

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}

结果依次输出,相当于顺序执行各个任务。

现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。

线程池的作用

线程池作用就是限制系统中执行线程的数量。
根 据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了造成系统拥挤效率不高。用线程池控制线程数量,其他线程排 队等候。一个任务执行完毕,再从队列的中取最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如果线程池 中有等待的工作线程,就可以开始运行了;否则进入等待队列。

为什么要使用线程池

1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

比较重要的几个类:

ExecutorService: 真正的线程池接口。

ScheduledExecutorService: 能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

ThreadPoolExecutor: ExecutorService的默认实现。

ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

newSingleThreadExecutor 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
newFixedThreadPool 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
newCachedThreadPool 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

synchronized关键字的用法,优缺点

用法:

指定对象加锁:对给定的对象进行加锁,进入同步代码块要获得给定对象的锁

直接作用于实例方法:相当于对当前实例加锁,进入同步代码块要获得当前实例的锁(这要求创建Thread的时候,要用同一个Runnable的实例才可以)

直接作用于静态方法:相当于给当前类加锁,进入同步代码块前要获得当前类的锁

优缺点:

使用synchronized,当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗。而ReentranLock的lockInterruptibly()可以优先相应中断。举例:两个线程A,B,A获得了锁(A.lockInterruptibly()),B在请求锁的时候发生阻塞,如果调用 B.interrupt(),会中断B的阻塞。

多线程的作用

(1)发挥多核CPU的优势,提高CPU的利用率(2)防止阻塞,提高效率

什么是线程安全

当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

线程安全级别

(1)不可变(2)绝对线程安全(3)相对线程安全(4)线程非安全

如何在两个线程之间共享数据

线程之间数据共享,其实可以理解为线程之间的通信,可以用wait/notify/notifyAll 进行等待和唤醒。

Java中提供了对象的那些级别引用

在Java中提供了对象的4个级别引用, 分别是强引用、软引用、弱引用以及虚引用。这四个类型的引用中, 只有强类型的引用是包内可见的, 其他级别的引用都是Public, 可以直接被应用程序开发者使用的。

强引用 我们在Java中创建的对象引用, 一般都是强类型引用. 这同样意味着当该对象如果有引用存在的情况下, 同时在JVM整个环境中该对象是路径可达的, 那么该对象永远不会被垃圾回收机制回收的。 例如我们创建一个对象Object object = new Object(); 该object就是一个强引用. 当强引用的对象占用内存过多, 而又没有释放的时候, 就会出现OOM问题;
软引用 软引用使用的时候需要通过一个软引用对象来进行声明, 软引用对象比强引用稍微弱一点, 通过软引用的对象当出现内存不足的情况的时候, 垃圾回收机制会将该类型的对象进行回收
弱引用 弱引用使用的时候需要通过一个弱引用对象进行声明。弱引用可以维持对对象的引用, 但是一旦垃圾回收线程工作的时候, 发现一个对象只有弱引用保持的时候, 那么就会对该对象进行垃圾回收.引用类型是一种比软引用弱的易用类型, 在系统GC时, 只要发现一个对象只有弱引用时不管系统堆空间是否足够, 都会将对象回收。但是由于垃圾回收器的线程通常优先级很低, 因此并不一定能够很快发现只有弱引用持有的对象, 在这种情况下, 弱引用对象可以存在很长的时间。一旦弱引用对象被垃圾回收器回收, 那么有一种机制能够保证该弱引用添加到一个注册引用的队列中。
虚引用 虚引用与其他的引用都不相同, 虚引用并不会决定引用对象的生命周期, 所以虚引用又成为”幽灵引用”。如果一个对象只持有虚引用, 那么该对象就跟没有任何引用一样, 在任何时候都有可能被垃圾回收器回收. 所以没办法在程序中调用它的任何相关函数, 因为存在太多的不确定性。而虚引用主要是用来提供当对象被GC的时候的通知机制. 通过该通知机制我们可以做一些资源回收等方面的工作. 因为对象只存在虚引用完全没有意义, 即在程序中声明一个对象的虚引用完全没有意义, 所以虚引用一定要与Reference队列一起使用, 才能起到对象回收通知机制.

Finalizer对象什么时候会在引用队列中

对于Java而言: 调用时机:当垃圾回收器要宣告一个对象死亡时,至少要经过两次标记过程:如果对象在进行可达性分析后发现没有和GC Roots相连接的引用链,就会被第一次标记,并且判断是否执行finalizer( )方法,如果对象覆盖finalizer( )方法且未被虚拟机调用过,那么这个对象会被放置在F-Queue队列中,并在稍后由一个虚拟机自动建立的低优先级的Finalizer线程区执行触发finalizer( )方法,但不承诺等待其运行结束。

finalization的目的:对象逃脱死亡的最后一次机会。(只要重新与引用链上的任何一个对象建立关联即可。)但是不建议使用,运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。可用try-finally或其他替代。

CountDownLatch 原理

实现原理:计数器的值由构造函数传入,并用它初始化AQS的state值。当线程调用await方法时会检查state的值是否为0,如果是就直接返回(即不会阻塞);如果不是,将表示该节点的线程入列,然后将自身阻塞。当其它线程调用countDown方法会将计数器减1,然后判断计数器的值是否为0,当它为0时,会唤醒队列中的第一个节点,由于CountDownLatch使用了AQS的共享模式,所以第一个节点被唤醒后又会唤醒第二个节点,以此类推,使得所有因await方法阻塞的线程都能被唤醒而继续执行。

从源代码和实现原理中可以看出一个CountDownLatch对象,只能使用一次,不能重复使用。

CyclicBarrier 原理

实现原理:在CyclicBarrier的内部定义了一个Lock对象,每当一个线程调用CyclicBarrier的await方法时,将剩余拦截的线程数减1,然后判断剩余拦截数是否为0,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁,接着先从await方法返回,再从CyclicBarrier的await方法中返回。

Semaphore 原理

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

Semaphore的流程的一些特性:

• 管理一系列许可证,即state共享资源值;
• 每acquire一次则state就减1一次,直到许可证数量小于0则阻塞等待;
• 释放许可的时候要保证唤醒后继结点,以此来保证线程释放他们所持有的信号量;
• 是Synchronized的升级版,因为Synchronized是只有一个许可,而Semaphore就像开了挂一样,可以有多个许可;

Exchanger 原理

作用:Exchanger类用于两个线程之间交换数据。

换句话说Exchanger提供的是一个交换服务,允许原子性的交换两个(多个)对象,但同时只有一对才会成功。先看一个简单的实例模型。

mark

在上面的模型中,我们假定一个空的栈(Stack),栈顶(Top)当然是没有元素的。同时我们假定一个数据结构Node,包含一个要交换的元素E和一个要填充的“洞”Node。这时线程T1携带节点node1进入栈(cas_push),当然这是CAS操作,这样栈顶就不为空了。线程T2携带节点node2进入栈,发现栈里面已经有元素了node1,同时发现node1的hold(Node)为空,于是将自己(node2)填充到node1的hold中(cas_fill)。然后将元素node1从栈中弹出(cas_take)。这样线程T1就得到了node1.hold.item也就是node2的元素e2,线程T2就得到了node1.item也就是e1,从而达到了交换的目的。

算法描述就是下图展示的内容。

mark

JDK 5就是采用类似的思想实现的Exchanger。JDK 6以后为了支持多线程多对象同时Exchanger了就进行了改造(为了支持更好的并发),采用ConcurrentHashMap的思想,将Stack分割成很多的片段(或者说插槽Slot),线程Id(Thread.getId())hash相同的落在同一个Slot上,这样在默认32个Slot上就有很好的吞吐量。当然会根据机器CPU内核的数量有一定的优化,有兴趣的可以去了解下Exchanger的源码。

CountDownLatch 与 CyclicBarrier 区别

  • CountDownLatch:一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;
  • CyclicBarrier:多个线程互相等待,直到到达同一个同步点,再继续一起执行。

对于CountDownLatch来说,重点是“一个线程(多个线程)等待”,而其他的N个线程在完成“某件事情”之后,可以终止,也可以等待。而对于CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。

CountDownLatch是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而CyclicBarrier更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。

线程池中的coreNum和maxNum有什么不同

在不同的业务场景中,线程池参数如何设置

线程的生命周期

mark

上图是一个线程的生命周期状态流转图,很清楚的描绘了一个线程从创建到终止的过程。

这些状态的枚举值都定义在java.lang.Thread.State下

1
2
3
4
5
6
7
8
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

NEW:毫无疑问表示的是刚创建的线程,还没有开始启动。

RUNNABLE: 表示线程已经触发start()方式调用,线程正式启动,线程处于运行中状态。

BLOCKED:表示线程阻塞,等待获取锁,如碰到synchronized、lock等关键字等占用临界区的情况,一旦获取到锁就进行RUNNABLE状态继续运行。

WAITING:表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,如通过wait()方法进行等待的线程等待一个notify()或者notifyAll()方法,通过join()方法进行等待的线程等待目标线程运行结束而唤醒,一旦通过相关事件唤醒线程,线程就进入了RUNNABLE状态继续运行。

TIMED_WAITING:表示线程进入了一个有时限的等待,如sleep(3000),等待3秒后线程重新进行RUNNABLE状态继续运行。

TERMINATED:表示线程执行完毕后,进行终止状态。

需要注意的是,一旦线程通过start方法启动后就再也不能回到初始NEW状态,线程终止后也不能再回到RUNNABLE状态。

synchronize 实现原理

synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性

Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

普通同步方法,锁是当前实例对象

静态同步方法,锁是当前类的class对象

同步方法块,锁是括号里面的对象

Lock接口有哪些实现类,使用场景是什么

Lock接口有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLockWriteLock
与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,
任何数量的线程可以同时读取共享数据(reader 线程)。从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。
在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行
修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。

ReentrantLock 的原理

原理:可重入锁的原理是在锁内部维护了一个线程标示,标示该锁目前被那个线程占用,然后关联一个计数器,一开始计数器值为0,说明该锁没有被任何线程占用,
当一个线程获取了该锁,计数器会变成1,其他线程在获取该锁时候发现锁的所有者不是自己所以被阻塞,
但是当获取该锁的线程再次获取锁时候发现锁拥有者是自己会把计数器值+1, 当释放锁后计数器会-1,当计数器为0时候,锁里面的线程标示重置为null,这时候阻塞的线程会获取被唤醒来获取该锁.

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。

此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)

ReentrantLock扩展的功能

1.实现可轮询的锁请求:

–在内部锁中,死锁是致命的——唯一的恢复方法是重新启动程序,唯一的预防方法是在构建程序时不要出错。而可轮询的锁获取模式具有更完善的错误

恢复机制,可以规避死锁的发生。

–如果你不能获得所有需要的锁,那么使用可轮询的获取方式使你能够重新拿到控制权,它会释放你已经获得的这些锁,然后再重新尝试。

可轮询的锁获取模式,由tryLock()方法实现。此方法仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值true。

如果锁不可用,则此方法将立即返回值false。此方法的典型使用语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}

2.实现可定时的锁请求

–当使用内部锁时,一旦开始请求,锁就不能停止了,所以内部锁给实现具有时限的活动带来了风险。为了解决这一问题,可以使用定时锁。

当具有时限的活动调用了阻塞方法,定时锁能够在时间预算内设定相应的超时。如果活动在期待的时间内没能获得结果,定时锁能使程序提前返回。

可定时的锁获取模式,由tryLock(long, TimeUnit)方法实现。

3.实现可中断的锁获取请求

–可中断的锁获取操作允许在可取消的活动中使用。lockInterruptibly()方法能够使你获得锁的时候响应中断。

ReentrantLock不好与需要注意的地方

–lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,但是实际上,

它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。

而使用同步,JVM 将确保锁会获得自动释放.

–当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息。这些对调试非常有价值,因为它们能标识死锁或者

其他异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪个线程拥有 Lock 对象。

synchronized 和java.util.concurrent.locks.Lock的异同

主要相同点:Lock能完成synchronized所实现的所有功能

主要不同点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。

介绍下栈和队列

栈和队列都是动态集合,且在其上进行DELETE操作所移除的元素是预先设定的。在(stack)中,被删除的是最近插入的元素:栈实现的是一种后进先出(last-in, first-out, LIFO)策略。类似地,在队列(queue)中,被删去的总是在集合中存在时间最长的那个元素:队列实现的是一种先进先出(first-in, first-out, FIFO)策略。

synchronized、Lock、ReentrantLock、ReadWriteLock

Lock和synchronized有以下几点不同:

  1. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
  2. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
  3. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
  4. 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  5. Lock可以提高多个线程进行读操作的效率。
    在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

ReentrantReadWriteLock相比ReentrantLock的最大区别是:

ReentrantReadWriteLock的读锁是共享锁,任何线程都可以获取,而写锁是独占锁。ReentrantLock不论读写,是独占锁。

介绍下CAS(无锁技术)

CAS: java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。

CAS应用:CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS原理:CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。

CAS缺点:

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题循环时间长开销大只能保证一个共享变量的原子操作

ThreadPoolExecutor的内部工作原理

  1. 如果线程池大小poolSize小于corePoolSize,则创建新线程执行任务。
  2. 如果线程池大小poolSize大于corePoolSize,且等待队列未满,则进入等待队列。
  3. 如果线程池大小poolSize大于corePoolSize且小于maximumPoolSize,且等待队列已满,则创建新线程执行任务。
  4. 如果线程池大小poolSize大于corePoolSize且大于maximumPoolSize,且等待队列已满,则调用拒绝策略来处理该任务。
  5. 线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在keepAliveTime里等不到新的任务了,那么线程就会退出。

ThreadPoolExecutor线程池中拒绝策略:

  1. AbortPolicy:为java线程池默认的阻塞策略,不执行此任务,而且会直接抛出一个执行时异常,切记TreadPoolExecutor.execute需要try catch,否则程序会直接退出。
  2. DiscardPolicy:直接抛弃,任务不执行,空方法
  3. DiscardOldestPolicy:从队列里面抛弃head的一个任务,并再次execute 此任务(task)
  4. CallerRunsPolicy:在调用execute的线程里面执行此command,会阻塞入口。
  5. 用户自定义拒绝策略:实现RejectdExecutionHandler,并自己定义策略模式。

分布式环境下,怎么保证线程安全

有哪些类加载器

能不能自己写一个类叫java.lang.String

可以,但在应用的时候,需要用自己的类加载器去加载,否则,系统的类加载器永远只是去加载jre.jar包中的那个java.lang.String。由于在tomcat的web应用程序中,都是由webapp自己的类加载器先自己加载WEB-INF/classess目录中的类,然后才委托上级的类加载器加载,如果我们在tomcat的web应用程序中写一个java.lang.String,这时候Servlet程序加载的就是我们自己写的java.lang.String,但是这么干就会出很多潜在的问题,原来所有用了java.lang.String类的都将出现问题。

虽然java提供了endorsed技术,可以覆盖jdk中的某些类,具体做法是….。但是,能够被覆盖的类是有限制范围,反正不包括java.lang这样的包中的类。

例如,运行下面的程序:

1
2
3
4
5
6
7
8
9
10
11
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println("string");
}
}

报告的错误如下:

java.lang.NoSuchMethodError: main

Exception in thread “main”

这是因为加载了jre自带的java.lang.String,而该类中没有main方法。

介绍下B树、二叉树

B树:

B树(英语:B-tree)是一种自平衡的),能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库文件系统的实现上。

概括关键词:自平衡,可以拥有多于2个子节点,适用于数据库和文件系统。

二叉树:

二叉树是一种特殊的有序树:每个节点至多有两个分支(子节点),分支具有左右次序,不能颠倒。
两种特殊的二叉树:
完全二叉树:除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点(注意是右边,而不能是左边缺少)。
满二叉树:每一层都是满的(除了最后一层,这里的最后一层是指叶节点)。

红黑树:

红黑树(英语:Red–black tree)是一种自平衡二叉查找树
它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。

  • 红黑树的性质
  • 节点是红色或黑色。
  • 根是黑色。
  • 所有叶子都是黑色(叶子是NIL节点)。
  • 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  • 从任一节点到其每个叶子的所有简单路径)都包含相同数目的黑色节点。

分布式锁的实现

针对分布式锁的实现,目前比较常用的有以下几种方案:

(1).基于数据库实现分布式锁

(2).基于缓存(redis,memcached,tair)实现分布式锁

(3).基于Zookeeper实现分布式锁

实现方式 优点 缺点
数据库实现 直接借助数据库,容易理解。 1. 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。 2.操作数据库需要一定的开销,性能问题需要考虑。 使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候。
缓存实现 性能好,实现起来较为方便。 1.通过超时时间来控制锁的失效时间并不是十分的靠谱。
Zookeeper实现 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。 1.性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。

三种方法比较:

上面几种方式,哪种方式都无法做到完美。就像CAP一样,在复杂性、可靠性、性能等方面无法同时满足,所以,根据不同的应用场景选择最适合自己的才是王道。

从理解的难易程度角度(从低到高)

数据库 > 缓存 > Zookeeper

从实现的复杂程度角度(从低到高)

Zookeeper >= 缓存 > 数据库

从性能角度(从高到低)

缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低)

Zookeeper > 缓存 > 数据库

分布式session存储解决方案

1. Session Stick

Session Stick 方案即将客户端的每次请求都转发至同一台服务器,这就需要负载均衡器能够根据每次请求的会话标识(SessionId)来进行请求转发,如下图所示。

mark

这种方案实现比较简单,对于Web服务器来说和单机的情况一样。但是可能会带来如下问题:

  • 如果有一台服务器宕机或者重启,那么这台机器上的会话数据会全部丢失。
  • 会话标识是应用层信息,那么负载均衡要将同一个会话的请求都保存到同一个Web服务器上的话,就需要进行应用层(第7层)的解析,这个开销比第4层大。
  • 负载均衡器将变成一个有状态的节点,要将会话保存到具体Web服务器的映射。和无状态节点相比,内存消耗更大,容灾方面也会更麻烦。

2. Session Replication

Session Replication 的方案则不对负载均衡器做更改,而是在Web服务器之间增加了会话数据同步的功能,各个服务器之间通过同步保证不同Web服务器之间的Session数据的一致性,如下图所示。

mark

Session Replication 方案对负载均衡器不再有要求,但是同样会带来以下问题:

  • 同步Session数据会造成额外的网络带宽的开销,只要Session数据有变化,就需要将新产生的Session数据同步到其他服务器上,服务器数量越多,同步带来的网络带宽开销也就越大。
  • 每台Web服务器都需要保存全部的Session数据,如果整个集群的Session数量太多的话,则对于每台机器用于保存Session数据的占用会很严重。

3. Session 数据集中存储

Session 数据集中存储方案则是将集群中的所有Session集中存储起来,Web服务器本身则并不存储Session数据,不同的Web服务器从同样的地方来获取Session,如下图所示。

mark

相对于Session Replication方案,此方案的Session数据将不保存在本机,并且Web服务器之间也没有了Session数据的复制,但是该方案存在的问题在于:

  • 读写Session数据引入了网络操作,这相对于本机的数据读取来说,问题就在于存在时延和不稳定性,但是通信发生在内网,则问题不大。
  • 如果集中存储Session的机器或集群出现问题,则会影响应用。

4. Cookie Based

Cookie Based 方案是将Session数据放在Cookie里,访问Web服务器的时候,再由Web服务器生成对应的Session数据,如下图所示。

mark

但是Cookie Based 方案依然存在不足:

  • Cookie长度的限制。这会导致Session长度的限制。
  • 安全性。Seesion数据本来是服务端数据,却被保存在了客户端,即使可以加密,但是依然存在不安全性。
  • 带宽消耗。这里不是指内部Web服务器之间的宽带消耗,而是数据中心的整体外部带宽的消耗。
  • 性能影响。每次HTTP请求和响应都带有Seesion数据,对Web服务器来说,在同样的处理情况下,响应的结果输出越少,支持的并发就会越高。

总结
前面四个方案都是可行的,但是对于大型网站来说,Session Sticky和Session数据集中存储是比较好的方案。

常用的linux命令

mkdir: 用于新建一个新目录

pwd: 显示当前工作目录

rmdir: 删除给定的目录。

rm: 会删除给定的文件

mv: 命令对文件或文件夹进行移动,如果文件或文件夹存在于当前工作目录,还可以对文件或文件夹进行重命名。

cat: 用于在标准输出(监控器或屏幕)上查看文件内容

tail: 默认在标准输出上显示给定文件的最后10行内容,可以使用tail -n N 指定在标准输出上显示文件的最后N行内容。

less: 按页或按窗口打印文件内容。在查看包含大量文本数据的大文件时是非常有用和高效的。你可以使用Ctrl+F向前翻页,Ctrl+B向后翻页。

grep: 在给定的文件中搜寻指定的字符串。grep -i “” 在搜寻时会忽略字符串的大小写,而grep -r “” 则会在当前工作目录的文件中递归搜寻指定的字符串。

find: 这个命令会在给定位置搜寻与条件匹配的文件。你可以使用find -name 的-name选项来进行区分大小写的搜寻,find -iname 来进行不区分大小写的搜寻。

tar: 命令能创建、查看和提取tar压缩文件。tar -cvf 是创建对应压缩文件,tar -tvf 来查看对应压缩文件,tar -xvf 来提取对应压缩文件。

gzip: 命令创建和提取gzip压缩文件,还可以用gzip -d 来提取压缩文件。

unzip: 对gzip文档进行解压。在解压之前,可以使用unzip -l 命令查看文件内容。

whatis: 会用单行来描述给定的命令,就是解释当前命令。

exit: 用于结束当前的终端会话。

who: 能列出当前登录的用户名。

su: 用于切换不同的用户。即使没有使用密码,超级用户也能切换到其它用户。

uname: 会显示出关于系统的重要信息,如内核名称、主机名、内核版本、处理机类型等等,使用uname -a可以查看所有信息。

df: 查看文件系统中磁盘的使用情况–硬盘已用和可用的存储空间以及其它存储设备。你可以使用df -h将结果以人类可读的方式显示。

ps: 显示系统的运行进程。

top: 命令会默认按照CPU的占用情况,显示占用量较大的进程,可以使用top -u 查看某个用户的CPU使用排名情况。

文章地址:https://www.jianshu.com/p/0056d671ea6d

https://juejin.im/post/5a9f5ce86fb9a028de443ed9

坚持原创技术分享,您的支持将鼓励我继续创作!