JAVA基础
1、8种基本数据类型及其字节数
2、i++与++i的异同之处
同:
(1)i++与++i都是变量自增1,等价于i=i+1;
(2)i++与++i的使用仅仅针对变量,如 final int i=0;++i; 编译报错; ++2; 编译报错;
异:
i++:先运算后增1 ;
@Test public void test() { int x = 1; int y = x++; System.out.println("x=" + x + ",y=" + y); // x=2,y=1 }
++i:先增1后运算;
@Test public void test() { int x = 1; int y = ++x; System.out.println("x=" + x + ",y=" + y); // x=2,y=2 }
3、&和&&的区别和联系,|和||的区别和联系
与运算分为普通与(&)和短路与(&&)两种。
(1) 普通与(&):所有的判断条件都要判断
(2)短路与(&&): 如果前面的判断返回了false,后面不再判断,最终结果就是false
或运算分为普通或(|)和短路或(||)两种。
(1)普通或(|):所有的判断条件都要判断
(2) 短路或(||): 如果前面的判断返回了true,后面不再判断,最终结果就是true
4、private/默认/protected/public权限修饰符的区别
5、关键字
this和super关键字:
this:当前对象;super:直接父类对象。
this():当前类的无参构造方法,也可以指定有参的如:this(a);
super():直接父类的无参构造方法,也可以指定有参的如:super(a)。
默认顺序:如果有个变量b,调用时为向上遍历,b—>this.b—>super.b,如果未找到,编译器将显式提示错误信息;
static关键字:
主要用途是方便在没有创建对象的情况下来进行调用(方法/变量)。
static可以用来修饰类的成员方法、类的成员变量,修饰代码块。
来源:https://www.cnblogs.com/dolphin0520/p/3799052.html
final和abstract关键字:
final可以声明成员变量、方法、类以及本地变量。

package xcj.homepage.demo; public class FinalDemo { static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(); public static void main(String[] args) { threadLocal.set("1"); // 被final修饰的引用变量指向的对象内容可变 System.out.println(threadLocal.get()); String a = "hello2"; final String b = "hello"; // final修饰的编译期知道其值 String d = "hello"; String c = b + 2; String e = d + 2; System.out.println((a == c)); System.out.println((a == e)); final String f = getHello(); // final修饰的运行期才能知道其值 String g = f + 2; System.out.println((a == g)); String h = getHello1(); // 运行期才能知道其值 String i = h + 2; System.out.println((a == i)); new FinalDemo().changeValue(1); // final修饰的参数是不可改变的 StringBuffer buffer1 = new StringBuffer("hello"); new FinalDemo().changeValue(buffer1); System.out.println(buffer1); // final修饰引用类型的参数,不能再让其指向其他对象,但是对其所指向的内容是可以更改的。 StringBuffer buffer2 = new StringBuffer("hello"); new FinalDemo().changeValue1(buffer2); System.out.println(buffer2); } public static String getHello() { return "hello"; } public final static String getHello1() { return "hello"; } // final修饰成员变量、成员方法、类,还可以修饰参数、若某个参数被final修饰了,则代表了该参数是不可改变的 public void changeValue(final int i) { // i++; // The final local variable i cannot be assigned. It must be blank and not using a compound System.out.println(i); } public void changeValue(final StringBuffer buffer) { // final修饰引用类型的参数,不能再让其指向其他对象,但是对其所指向的内容是可以更改的。 // buffer = new StringBuffer("hi"); buffer.append("world"); System.out.println(buffer); } public void changeValue1(StringBuffer buffer) { // buffer重新指向另一个对象 buffer = new StringBuffer("hi"); buffer.append("world"); System.out.println(buffer); } }
abstract修饰方法和类。抽象方法只有方法的声明,没有方法的实现。抽象类不能被实例化,不能通过new关键字创建类的对象。如果子类(非抽象类)继承了抽象类,抽象类可以指向子类的引用。
final、finally、finalize的区别:
final关键字可以用来修饰属性,方法和类。还有就是如果内部类要访问局部变量的话,那么对应的局部变量也必须为final关键字修饰的。
finally关键字一般也用在处理try{}catch{}finally{}的异常中,finally里面的内容一定会执行。
finalize在垃圾回收中会使用。finalize是Object类中的方法。在垃圾回收器执行的时候会调用被回收对象的此方法。
6、==和equals的区别和联系
(1)==:比较的是值是否相等;
对于基本数据类型,则直接比较其存储的 “值”是否相等;
对于引用类型,则比较的是所指向的对象的地址。
(2)equals:equals方法不能作用于基本数据类型的变量,会编译报错。equals继承Object类,比较的是是否是同一个对象。
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
像String,Double,Date,Integer等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
如String重写了equals方法,代码如下:

public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
------------------
Integer重写了equals方法,为值比较;Integer重写的equals方法,代码如下:

public boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
[-128,127]范围的两个相同值使用==或equals都为true;非[-128,127]范围的两个Integer使用==为false,因为比较的是地址;使用equals为true。
原因:在-128~127的Integer值并且以Integer x = value;的方式赋值的Integer值在进行==和equals比较时,都会返回true,因为Java里面对处在-128~127之间的Integer值,用的是原生数据类型int,此时调用的是Integer.valueOf()方法,会在内存里供重用,也就是说这之间的Integer值进行==比较时只是进行int原生数据类型的数值比较,而超出-128~127的范围,进行==比较时是进行地址及数值比较。
int、Integer使用==和equals代码示例:

@Test public void test() { int i1 = 1,i2 = 1; System.out.println("两个int == " + (i1 == i2)); // true // System.out.println("两个int equals " + (i1.equals(i2))); // 基本类型无equals方法,这样写会编译报错 Integer i3 = 1,i4 = 1; System.out.println("[-128,127]范围的两个Integer == " + (i3 == i4)); // true System.out.println("[-128,127]范围的两个Integer equals " + i3.equals(i4)); // true System.out.println("一个Integer,一个int == " + (i3 == i1)); // true System.out.println("Integer equals int" + (i3.equals(i1))); // true,i3为引用类型,有equals方法,不会报错 // System.out.println("int equals Integer" + (i1.equals(i3))); // 基本类型无equals方法,这样写会编译报错 Integer i5 = 128,i6 = 128; System.out.println("非[-128,127]范围的两个Integer == " + (i5 == i6)); // false System.out.println("非[-128,127]范围的两个Integer equals " + (i5.equals(i6))); // true int i7 = 128,i8 = 128; System.out.println("非[-128,127]范围的两个int == " + (i7 == i8)); // true }
执行结果:

两个int == true [-128,127]范围的两个Integer == true [-128,127]范围的两个Integer equals true 一个Integer,一个int == true Integer equals inttrue 非[-128,127]范围的两个Integer == false 非[-128,127]范围的两个Integer equals true 非[-128,127]范围的两个int == true
--------------------
对于String示例: String s1 = "ABC",s2 = "ABC"; String s7 = "A" + "BC",s8 = "ABC"; 使用==均为true,因为像这种写法产生的这种"常量"就会被放到常量池;
String s3 = new String("ABC"),s4 = new String("ABC"); String s5 = "ABC",s6 = new String("ABC"); String s9 = "A" + new String("BC"),s10 = "ABC"; 使用==均为false,因为new的对象是向堆申请新的空间存储。

@Test public void test() { String s1 = "ABC",s2 = "ABC"; System.out.println("两个String == " + (s1 == s2)); // true System.out.println("两个String equals " + (s1.equals(s2))); // true String s3 = new String("ABC"),s4 = new String("ABC"); System.out.println("两个String == " + (s3 == s4)); // false System.out.println("两个String equals " + (s3.equals(s4))); // true String s5 = "ABC",s6 = new String("ABC"); System.out.println("两个String == " + (s5 == s6)); // false System.out.println("两个String equals " + (s5.equals(s6))); // true String s7 = "A" + "BC",s8 = "ABC"; System.out.println("两个String == " + (s7 == s8)); // true System.out.println("两个String equals " + (s7.equals(s8))); // true String s9 = "A" + new String("BC"),s10 = "ABC"; System.out.println("两个String == " + (s9 == s10)); // false System.out.println("两个String equals " + (s9.equals(s10))); // true }
继承中,类初始化顺序:父类静态成员和静态方法块 -> 子类静态成员和静态方法块 -> 父类普通成员和普通方法块 -> 父类构造函数 -> 子类普通成员和普通方法块 -> 子类构造函数
public class TestB extends MyTestA { public TestB() { System.out.println("子类构造函数"); } {System.out.println("子类普通代码块");} static { System.out.println("子类静态代码块"); } public static void main(String[] args) { new TestB(); } } class MyTestA { public MyTestA() { System.out.println("父类构造函数"); } {System.out.println("父类普通代码块");} static { System.out.println("父类静态代码块"); } }
父类静态代码块
子类静态代码块
父类普通代码块
父类构造函数
子类普通代码块
子类构造函数
7、 类和对象的关系
对象是对客观事物的抽象,类是对对象的抽象,类是一种抽象的数据类型。
它们的关系是,对象是类的实例,类是对象的模板。
类是抽象的,不占用内存,而对象是具体的,占用存储空间。
8、面向过程和面向对象的区别
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
参考https://blog.csdn.net/jerry11112/article/details/79027834
面向对象的思想就是把一切都看成对象,而对象一般都由属性+方法组成!
属性属于对象静态的一面,用来描述具体某个对象的特征,方法属于对象动态的一面。
类:具有同种属性的对象称为类,是个抽象的概念。
面向对象有三大特性,分别是封装性、继承性和多态性。
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏。
public,protected,default,private修饰的这四个成员属性的访问权限依次降低。
继承,可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态,是允许你将父对象设置为和一个或多个他的子对象相等的技术,父对象可以根据当前赋值给它的子对象特性以不同的方式运作。
简而言之,允许将自类类型的指针赋值给父类类型的指针。
实现多态方式,覆盖和重载。
9、谈谈Java的多态,Java中如何实现多态
多态是同种类的多个对象,在接收到同一消息时产生了不同反应和效果。
多态可以表现在子父类上或者通过接口实现。
多态有编译时多态(通过方法重载实现)和运行时多态(通过方法覆盖实现(子类覆盖父类方法)即动态绑定,使用父类引用指向子类对象,再调用某一父类中的方法时,不同子类会表现出不同结果)。
对象类型的转换分为以下两种:
(1)向上转型(类型的自动转换):子类的对象可以直接赋给父类的对象变量。
(2)向下转型: 将父类的引用强制转换成子类的引用。它必须强制转换。
10、简述Java的垃圾回收机制
(1)JVM虚拟机通过可达性分析算法,来确实对象是否会被回收;
可达性分析算法:设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找不到根时,就认为该对象不可达,可以被回收。
(2)使用标记-清除算法、复制算法、标记-整理算法、分代收集算法进行垃圾回收。
①标记-清除算法:遍历所有的GC Root,分别标记可达的对象和不可达的对象,标记完成后就将不可达的对象回收。但是效率低、回收得到的空间不连续(空间问题,标记清除之后会产生大量不连续的内存碎片,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而造成内存空间浪费)。
②复制算法:将内存分为大小相等的两块,每次只使用一块。当这一块内存满了,就将还存活的对象复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存统一回收。这样能够得到连续的内存空间,每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,但是,内存缩小为原来的一半,浪费了一半内存。
③标记-整理算法:与标记-清除算法类似;但标记-整理算法除了会不可达的对象回收外,还会对剩余的存活对象进行重新整理,不会产生内存碎片。
④分代收集算法:jvm使用最多的一种算法,它会在具体的场景(JVM三个区域:新生代、老年代、永久代)自动选择以上三种算法进行垃圾对象回收。
新生代:有大批对象死亡,少量存活,JVM选用复制算法。(新生代是生命周期较短的对象,所有新生成的对象都先放在新生代中,存放满时会移到老年代)
老年代:对象存活率高,没有额外空间对其分配担保,使用标记-清除或者标记-整理。(注:老年代都是生命周期较长的对象,在新生代中经历N次垃圾回收后仍存在的会放到老年代中)
永久代:主要存放静态文件,如JAVA类、方法等,不会回收。
注:新生代和老年代存储在java虚拟机堆上 ;永久代存储在方法区上。
11、java程序编译后会生成字节码文件(byte code),就是.class文件。
12、 继承条件下构造方法的执行过程
先执行父类无参构造方法,再执行子类构造方法。
13、存在i+1<i吗?
存在,i=2147483647时,i+1<1。2147483647 + 1为负数。
@Native public static final int MAX_VALUE = 0x7fffffff; Integer的最大值为2147483647,超出后,就为负数了。
计算机中用补码来运算加减法,用补码计算-2147483647-1和2147483647+1都得到1000 0000 0000 0000 0000 0000 0000 0000,而加法溢出的结果在范围[-214748368,2147483647]中,故得到-214748368。
参考https://www.cnblogs.com/zakers/p/6739708.html
测试代码如下:

@Test public void test() { int i = 0; while(true) { if (i+1 < i) { System.out.println(i); // 2147483647 break; } i++; } System.out.println(2147483647 + 1); // -2147483648 }
14、二进制、八进制、十进制、十六进制的相互转换
(1)十进制转二进制:十进制数除2取余法,即十进制数除2,余数为权位上的数,得到的商值继续除2,依此步骤继续向下运算直到商为0为止。
(2)二进制转十进制:把二进制数按权展开、相加即得十进制数。
(3)二进制转八进制:3位二进制数按权展开相加得到1位八进制数。(注意事项,3位二进制转成八进制是从右到左开始转换,不足时补0)。
(4)八进制转成二进制:八进制数通过除2取余法,得到二进制数,对每个八进制为3个二进制,不足时在最左边补零。
(5)二进制转十六进制:与二进制转八进制方法近似,八进制是取三合一,十六进制是取四合一。(注意事项,4位二进制转成十六进制是从右到左开始转换,不足时补0)。
(6)十六进制转二进制:十六进制数通过除2取余法,得到二进制数,对每个十六进制为4个二进制,不足时在最左边补零。
(7)十进制转八进制或者十六进制有两种方法:
①间接法:把十进制转成二进制,然后再由二进制转成八进制或者十六进制。
②直接法:把十进制转八进制或者十六进制按照除8或者16取余,直到商为0为止。
(8)八进制或者十六进制转成十进制:把八进制、十六进制数按权展开、相加即得十进制数。
(9)八进制与十六进制之间的转换有两种方法:
①他们之间的转换可以先转成二进制然后再相互转换。
②他们之间的转换可以先转成十进制然后再相互转换。
参考 https://zhidao.baidu.com/question/2144662682878005588.html
JAVA集合
15、Java集合体系结构(List、Set、Collection、Map的区别和联系)
Java 集合可分为 Collection 和 Map 两种体系。简化后结构如下:
(1)Collection接口:
Set:不可重复的集合。
①HashSet:无序,可以放一个null,线程不安全;
②TreeSet:有序,线程不安全;默认情况下,元素不允许为null值,元素必须是相同类型。
③LinkedHashSet:有序,可以放一个null,线程不安全;
List:可重复的集合。
①ArrayList:动态数组,线程不安全;
②LinkedList:双向链表,线程不安全;
③Vector:动态数组,线程安全;
④Stack:动态数组,线程安全。
Queue:元素有序,先进先出。
(2)Map接口:具有映射关系“key-value对”的集合。Map中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。
①HashMap:键唯一,无序,key可为null,线程不安全;
②HashTable:键唯一,无序,key和value不能为null,线程安全;
③TreeMap:键唯一,有序,key不能为null,线程不安全;
④LinkedHashMap:键唯一,有序,key可为null,线程不安全。
16、HashMap的工作原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。
来源 https://www.cnblogs.com/ITtangtang/p/3948798.html
17、Vector、ArrayList、LinkedList的使用、原理、异同、存储性能和特性
18、HashMap、LinkedHashMap、Hashtable、TreeMap的使用、原理、异同、存储性能和特性
19、HashSet、TreeSet的使用、原理、异同、存储性能和特性
20、List里面如何剔除相同的对象?
21、Collections工具类中的sort()方法如何比较元素?
JAVA IO流
22、Java 中有几种类型的流?
根据处理数据类型的不同分为:字符流(Reader和Writer为基类)和字节流(InputStream和OutputStream为基类);
根据数据流向不同分为:输入流(InputStream和Reader为基类)和输出流(OutputStream和Writer为基类);
字节流是 8 位通用字节流,字符流是16位Unicode字符流。
23、字符流和字节流联系区别;什么时候使用字节流和字符流?
字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。
字节流和字符流的区别:
(1)读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。
(2)处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。
(3)字节流在操作的时候本身是不会用到缓冲区的,是文件本身的直接操作的;而字符流在操作的时候下后是会用到缓冲区的,是通过缓冲区来操作文件。
结论:优先选用字节流。首先因为硬盘上的所有文件都是以字节的形式进行传输或者保存的,包括图片等内容。但是字符只是在内存中才会形成的,所以在开发中,字节流使用广泛。
24、列举常用字节输入流和输出流并说明其特点。
字节输入流:InputStream -> {FileInputStream}、{FilterInputStream -> BufferedInputStream}、{ByteArrayInputStream}、{StringBufferInputStream}、{PipedInputStream}、{ObjectInputStream}
字节输出流:OutputStream -> {FileOutputStream}、{FilterOutputStream -> BufferedOutputStream}、{ByteArrayOutputStream}、{PipedOutputStrea}、{ObjectOutputStream}
字符输入流:Reader -> {BufferedReader}、{InputStreamReader -> FileReader} 、{CharArrayReader -> CharReader}、{StringReader}、{PipedReader}、{FilterReader -> PushbackReader}
字符输出流:Writer -> {BufferedWriter}、{OutputStreamWriter -> FileWriter}、{CharArrayWriter -> CharWriter}、{StringWriter}、{PipedWriter}、{PrintWriter}、{FileWriter}
使用FileInputStream的read方法,读取文件内容,代码示例如下:

@Test public void test() { InputStream is = null; try { is = new FileInputStream("F://test.txt"); byte[] buf = new byte[1024]; int length = 0; //循环读取文件内容,输入流中将最多buf.length个字节的数据读入一个buf数组中,返回类型是读取到的字节数。 //当文件读取到结尾时返回 -1,循环结束。 while((length = is.read(buf)) != -1){ System.out.print(new String(buf,0,length)); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭流 if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用BufferedReader的readLine方法,读取文件内容,代码示例如下:

@Test public void test() { BufferedReader br = null; try { br = new BufferedReader(new FileReader("F://test.txt")); String line; while((line = br.readLine()) != null){ System.out.print(line + "\n"); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 关闭流 if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用FileOutputStream将字符串(字符串需先使用getByte方法转为字节)写出到文件,代码示例如下:

@Test public void test() { OutputStream outputStream = null; try { outputStream = new FileOutputStream("F:\\log.txt", true); try { outputStream.write("test 您好".getBytes("utf-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
使用BufferedWriter将字符串写出到文件,代码示例如下:

@Test public void test() { BufferedWriter bw = null; try { bw = new BufferedWriter(new FileWriter("F:\\log.txt")); try { bw.write("test 您好"); bw.newLine(); bw.write("再见"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (bw != null) { try { bw.close(); } catch (IOException e) { e.printStackTrace(); } } } }
25、说说BIO、NIO和AIO的区别。
BIO:同步阻塞IO,位于java.io包,基于流模型实现。在读写动作完成前,一直阻塞。调用时有可靠的线性顺序。代码简单直观,但IO的效率和扩展性低。如:InputStream、OutputStream、Reader、Writer。
NIO:同步非阻塞IO,位于java.nio包,基于Channel和Buffer实现。
AIO:NIO升级版,提供了异步非阻塞IO,异步IO是基于事件和回调机制实现的。
来源 http://www.imooc.com/article/265871
示例 https://blog.csdn.net/anxpp/article/details/51512200
26、JAVA中如何解析xml,不同方式有何优缺点?
HTTP
27、TCP为何采用三次握手来建立连接,若釆用二次握手可以吗,请说明理由?(TCP三次握手,四次挥手)
三次握手(假设有A和B两端要进行通信):
第一次:首先A发送一个(SYN)到B,意思是A要和B建立连接进行通信;
第二次:B收到A要建立连接的请求之后,发送一个确认(SYN+ACK)给A,意思是收到A的消息了,B这里也是通的,表示可以建立连接;
第三次:A如果收到了B的确认消息之后,再发出一个确认(ACK)消息,意思是告诉B,这边是通的,然后A和B就可以建立连接相互通信了;
如果只有两次通信的话,没有第三次确认消息,这时候B不确定A是否收到了确认消息,有可能这个确认消息由于某些原因丢了。
如果采用二次握手,那么只要B端发出确认报文就会认为新的连接已经建立了,但是A端并没有发出建立连接的请求,因此不会去向B端发送数据,B端没有收到数据就会一直等待,这样B端就会白白浪费掉很多资源。
而三次握手可以保证信道数据传输的可靠性。
28、HTTP协议工作原理及其特点
HTTP协议(超文本传输协议)是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则,是一种通信协议,它允许将超文本标记语言(HTML)文档从Web服务器传送到客户端的浏览器。
通常,由HTTP客户端发起一个请求,建立一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端发送过来的请求。一旦收到请求,服务器(向客户端)发回一个状态行,比如"HTTP/1.1 200 OK",和(响应的)消息,消息的消息体可能是请求的文件、错误消息、或者其它一些信息。
HTTP使用TCP而不是UDP的原因在于(打开)一个网页必须传送很多数据,而TCP协议提供传输控制,按顺序组织数据,和错误纠正。
工作原理:输入URL后,浏览器给Web服务器发送了一个Request, Web服务器接到Request后进行处理,生成相应的Response,然后发送给浏览器, 浏览器解析Response中的HTML,这样我们就看到了网页。
主要特点:
(1)支持客户/服务器模式。
(2)简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方 法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
(3)灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
(4)无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
(5)无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就 、较快。客户端与服务器进行动态交互的 Web 应用程序出现之后,HTTP 无状态的特性严重阻碍了这些应用程序的实现,毕竟交互是需要承前启后的,两种用于保持 HTTP 连接状态的技术就应运而生了,一个是 Cookie,而另一个则是Session。
注:浏览器分析Response中的 HTML,发现其中引用了很多其他文件,比如图片,CSS文件,JS文件。浏览器会自动再次发送Request去获取图片,CSS文件,或者JS文件。
来源:https://www.cnblogs.com/youzaijiang/p/10710200.html
JS
29、JavaScript常用数据类型有哪些
按数据类型的复杂方式划分为基本类型、特殊类型和复杂类型。
基本数据类型:number(数字类型)、string(字符串类型)、boolean(布尔类型);
复杂数据类型:array(数组类型)、object:对象数据类型;
特殊数据类型:null(空对象数据);underfined(未定义的任何数据类型)。
写法:
array数组类型:简单的由中括号包括起来,元素间用逗号分隔。如
一维数组 var Province =new Array('江苏','浙江','安徽'); var Province = ['江苏','浙江','安徽'];
二维数组 var City =[['南京','苏州'],['杭州','宁波']]; 或者

var Province=new Array(); Province[0]=new Array(); Province[0][0]="南京"; Province[0][1]="苏州"; Province[1]=new Array(); Province[1][0]="杭州"; Province[1][1]="宁波";
对象数据类型: var Province = {name:'江苏',id:'A'}; 对象Province 有两个属性:name和id。
一个对象包括属性和方法的示例如下:

var person = { firstName: "Bill", lastName : "Gates", id : 678, fullName : function() { return this.firstName + " " + this.lastName; } };
来源:https://www.gyzgl.com/jsnews/309.html
扩展:
JSON:是JavaScript Object Notation的缩写,它是一种数据交换格式。
使用 JSON.stringify(Object); 可将对象序列化成JSON格式的字符串。如下:

var xiaoming = { name: '小明', age: 14, gender: true, height: 1.65, grade: null, 'middle-school': '\"W3C\" Middle School', skills: ['JavaScript', 'Java', 'Python', 'Lisp'] }; // 对象 var s = JSON.stringify(xiaoming); // 对象序列化成JSON格式的字符串
使用 JSON.parse() 可将JSON格式的字符串反序列化为JS对象。如下:

JSON.parse('[1,2,3,true]'); // [1, 2, 3, true] JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14} JSON.parse('true'); // true JSON.parse('123.45'); // 123.45
30、Javascript是面向对象的,怎么体现Javascript的继承关系?
1、原型链继承:将父类的实例作为子类的原型。(使用prototype实现继承)

function Person (name) { this.name = name; }; Person.prototype.getName = function () { //对原型进行扩展 return this.name; }; function Parent (age) { this.age = age; }; Parent.prototype = new Person('老明'); //这一句是关键 //通过构造器函数创建出一个新对象,把老对象的东西都拿过来。 Parent.prototype.getAge = function () { return this.age; }; // Parent.prototype.getName = function () { //可以重写从父类继承来的方法,会优先调用自己的。 // console.log(222); // }; var result = new Parent(22); console.log(result.getName()); //老明 //调用了从Person原型中继承来的方法(继承到了当前对象的原型中) console.log(result.getAge()); //22 //调用了从Parent原型中扩展来的方法
2、构造继承:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型),就是把父类中通过this指定的属性和方法复制(借用)到子类创建的实例中(利用call或者apply)。

function Person (name) { this.name = name; this.friends = ['小李','小红']; this.getName = function () { return this.name; } }; // Person.prototype.geSex = function () { //对原型进行扩展的方法就无法复用了 // console.log("男"); // }; function Parent = (age) { Person.call(this,'老明'); //这一句是核心关键 //这样就会在新parent对象上执行Person构造函数中定义的所有对象初始化代码, // 结果parent的每个实例都会具有自己的friends属性的副本 this.age = age; }; var result = new Parent(23); console.log(result.name); //老明 console.log(result.friends); //["小李", "小红"] console.log(result.getName()); //老明 console.log(result.age); //23 console.log(result.getSex()); //这个会报错,调用不到父原型上面扩展的方法
3、组合继承: 通过调用父类构造,继承父类的属性并保留传参的优点,然后再通过将父类实例作为子类原型,实现函数复用。(使用prototype和call)

function Person (name) { this.name = name; this.friends = ['小李','小红']; }; Person.prototype.getName = function () { return this.name; }; function Parent (age) { Person.call(this,'老明'); //这一步很关键 this.age = age; }; Parent.prototype = new Person('老明'); //这一步也很关键 var result = new Parent(24); console.log(result.name); //老明 result.friends.push("小智"); // console.log(result.friends); //['小李','小红','小智'] console.log(result.getName()); //老明 console.log(result.age); //24 var result1 = new Parent(25); //通过借用构造函数都有自己的属性,通过原型享用公共的方法 console.log(result1.name); //老明 console.log(result1.friends); //['小李','小红']
4、寄生组合继承:通过寄生方式,砍掉父类的实例属性。

function Person(name) { this.name = name; this.friends = ['小李','小红']; } Person.prototype.getName = function () { return this.name; }; function Parent(age) { Person.call(this,"老明"); this.age = age; } (function () { var Super = function () {}; // 创建一个没有实例方法的类 Super.prototype = Person.prototype; Parent.prototype = new Super(); //将实例作为子类的原型 })(); var result = new Parent(23); console.log(result.name); console.log(result.friends); console.log(result.getName()); console.log(result.age);
来源 https://www.cnblogs.com/chaixiaozhi/p/8515087.html
31、谈谈JS的闭包
闭包是函数和声明该函数的词法环境的组合。
参考 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
JSP
32、JSP九大内置对象
(1)输入输出对象:out对象、response对象、request对象
(2)通信控制对象:pageContext对象、session对象、application对象
(3)Servlet对象: page对象、config对象
(4)错误处理对象:exception对象
来源 https://blog.csdn.net/qq_42806915/article/details/82717694
33、列举JSP的四大作用域
(1)page:只在当前页面有效;
(2)request:在当前请求中有效;
(3)session:他在当前回话中有效;
(4)application:在所有的应用程序中都有效。
34、JSP的执行过程
来源:https://cloud.tencent.com/developer/article/1400348
35、JSP和Servlet的区别和联系
1、JSP编译后就变成了Servlet。JSP是Servlet的一种简化,使用JSP只需要完成程序员需要输出到客户端的内容,JSP中的Java脚本如何镶嵌到一个类中,由JSP容器完成。
而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应。
2、JSP更擅长表现于页面显示,Servlet更擅长于逻辑控制。
3、Servlet中没有内置对象,JSP中的内置对象都是必须通过HttpServletRequest对象,HttpServletResponse对象以及HttpServlet对象得到。
框架
36、谈谈过滤器原理及其作用?谈谈拦截器原理和作用?
过滤器:在客户端到服务器的过程中,当发送请求时,如果有不符合的信息将会被filter进行拦截,如果符合则会进行放行,在服务器给客户端响应时也会进行判断,如果有不符合的信息将会被filter进行拦截,如果符合则会进行放行。简而言之,filter负责拦截请求和放行。
过滤器原理:Filter 接口中有一个doFilter 方法,我们可以自定义类实现Filter接口;并在web.xml中配置过滤器设置对哪些资源进行拦截;web服务器每次调用 web 资源的 service 方法之前,都会先调用一下 Filter 的 doFilter 方法 doFilter(ServletRequest request, ServletResponse response,FilterChain filterChain){} ,做一些预处理后,执行 filterChain.doFilter(request, response); 代码进行放行,或者对不符合的请求进行拦截;如果放行(执行了doFilter方法),则调用链完成后, web 服务器就会调用 web 资源的 service 方法。
过滤器作用:过滤器主要的作用是过滤请求,可以通过Filter技术,对web服务器管理的所有web资源(如:JSP、Servlet、静态图片文件、或静态HTML文件)进行拦截,或者实现URL级别的权限控制、过虑敏感词汇、压缩响应信息等一些高级功能。
代码示例:

package xcj.homepage.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("这里可以做一些处理"); // if (false) { // return ; // 也可以加一些校验,如URL权限拦截等,不符合则return,则不会放行该次请求 // } chain.doFilter(request, response); // doFilter放行 } @Override public void destroy() { System.out.println("destroy"); } }

<filter> <filter-name>MyFilter</filter-name> <filter-class>xcj.homepage.filter.MyFilter</filter-class> </filter> <filter-mapping> <filter-name>MyFilter</filter-name> <url-pattern>/user/*</url-pattern> </filter-mapping>
参考:
图解 https://blog.csdn.net/pinnuli/article/details/81296969
原理与使用 https://blog.csdn.net/qq_35246620/article/details/69230454
拦截器:主要作用也是在服务端真正处理请求前后进行一些相关的操作。 例如初始化资源,权限监控,会话设置,菜单获取,资源清理等。
Struts2拦截器是在访问某个Action或Action的某个方法,字段之前或之后实施拦截,并且Struts2拦截器是可插拔的,拦截器是AOP的一种实现。Struts2规定用户自定义拦截器必须实现com.opensymphony.xwork2.interceptor.Interceptor接口。该接口声明了3个方法init,destroy,intercept方法。

public class LoginInterceptor extends AbstractInterceptor{ @Override public String intercept(ActionInvocation invocation) throws Exception { //得到拦截到的action的名称,看是否是login,当是login的时候,不用进行下面的检测了,直接执行下一个拦截器 String actionName=invocation.getProxy().getActionName(); if("login".equals(actionName)){ return invocation.invoke(); } //如果不是login.则判断是否已登录,及检测session中key为user的值是否存在,如果不存在,跳回到登录页面 String user=(String)invocation.getInvocationContext().getSession().get("user"); if(user==null){ System.out.println("未登录"); return "login"; } //进行到这里.说明用户已登录,则跳转到下一个拦截器 return invocation.invoke(); } }

<package name="default" namespace="/" extends="struts-default"> <interceptors> <!-- 配置自定义的拦截器--> <interceptor name="checkLogin" class="com.wang.interceptor.LoginInterceptor"/> <!--配置一个拦截器栈,里面包含自己定义的拦截器和defaultStack默认拦截器--> <interceptor-stack name="myStack"> <interceptor-ref name="defaultStack"></interceptor-ref> <interceptor-ref name="checkLogin"></interceptor-ref> </interceptor-stack> </interceptors> <!--引用默认的拦截器(栈)--> <default-interceptor-ref name="myStack"></default-interceptor-ref> <!--配置一个全局结果集--> <global-results> <result name="login">/login.jsp</result> </global-results> <action name="login" class="com.wang.action.LoginAction" > <result>/succ.jsp</result> <result name="error">/login.jsp</result> </action> </package>
Struts拦截器 https://www.jb51.net/article/128542.htm
Struts拦截器和过滤器 https://blog.csdn.net/a707819156/article/details/79659369
Spring拦截器(AOP):拦截器(Interceptor), 是 AOP (Aspect-Oriented Programming) 的另一种叫法。
SpringMVC的拦截器不同于Spring的拦截器,SpringMVC具有统一的入口DispatcherServlet,所有的请求都通过DispatcherServlet,所有的操作都是通过该servlet进行的顺序操作,SpringMVC的拦截器一般继承自HandlerInterceptorAdapter 或者实现 HandlerInterceptor 接口,对应提供了三个preHandle,postHandle,afterCompletion方法。

public class MyInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { /** * preHandle方法是进行处理器拦截用的,顾名思义,该方法将在Controller处理之前进行调用, * SpringMVC中的Interceptor拦截器是链式的,可以同时存在多个Interceptor, * 然后SpringMVC会根据声明的前后顺序一个接一个的执行, * 而且所有的Interceptor中的preHandle方法都会在Controller方法调用之前调用。 * SpringMVC的这种Interceptor链式结构也是可以进行中断的, * 这种中断方式是令preHandle的返回值为false,当preHandle的返回值为false的时候整个请求就 结束了。 这种方式,如果返回false,一般情况需要重定向或者转发到其他页面, 采用request的转发或者response的重定向即可 */ } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { /** * 这个方法只会在当前这个Interceptor的preHandle方法返回值为true的时候才会执行。 postHandle是进行处理器拦截用的,它的执行时间是在处理器进行处理之 后, 也就是在Controller的方法调用之后执行,但是它会在DispatcherServlet进行视图的渲染之前执行,也就是说在这个方法中你可以对ModelAndView进行操作。这个方法的链式结构跟正常访问的方向是相反的,也就是说先声明的Interceptor拦截器该方法反而会后调用,这跟Struts2里面的拦截器的执行过程有点像,只是Struts2里面的intercept方法中要手动的调用ActionInvocation的invoke方法,Struts2中调用ActionInvocation的invoke方法就是调用下一个Interceptor或者是调用action,然后要在Interceptor之前调用的内容都写在调用invoke之前,要在Interceptor之后调用的内容都写在调用invoke方法之后。 */ } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { /** * 该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行。 * 该方法将在整个请求完成之后,也就是DispatcherServlet渲染了视图执行, 这个方法的主要作用是用于清理资源的, */ } }

<mvc:interceptors> <!-- 写在外面,表示拦截所有链接 --> <bean id="" class=""/> <mvc:interceptor> <mvc:mapping path="/**" /> <!-- 排除拦截的链接 --> <mvc:exclude-mapping path="/static/**" /> <bean class="拦截器java代码路径" /> </mvc:interceptor> </mvc:interceptors>
SpringMVC拦截器 https://www.cnblogs.com/kevinShaw/p/9179446.html
监听器:TODO...
JavaWeb三大组件(Servlet、Filter、Listener):https://www.cnblogs.com/kefir/p/9426754.html
Java三大器(Filter、Listener、Interceptor)。
37、Ajax含义及其主要技术、工作原理
38、web项目从浏览器发起交易响应缓慢,请简述从哪些方面如数分析
39、设计模式
链接:23种设计模式 http://c.biancheng.net/design_pattern/
设计原则:
(1)封装变化:找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混合在一起。或者理解为“把会变化的部分取出并封装起来,以便以后可以轻易地扩充此部分,而不影响不需要变化的其他部分”。
(2)面向接口编程:针对接口编程,不针对实现编程:指的是针对超类型编程,即变量的声明类型是超类型,通常是一个抽象类或一个接口,关键在于多态。
(3)多用组合,少用继承:可以在允许时动态。
(4)迪米特原则:为交互对象之间的松耦合设计而努力。
(5)开放-关闭原则:类应该对扩展开放,对修改关闭。
(6)依赖倒置原则:要依赖抽象,不要依赖具体类。
含义:不能让高层组件依赖底层组件,高层和底层组件都应该依赖于抽象。
指导方针:变量不可以持有具体类的引用;不要让类派生自具体类;不要覆盖基类中已实现的方法。
(7)最少知识原则(迪米特原则):减少对象间的交互,只留几个密友。也就是不让太多的类耦合在一起,免得修改系统中的一部分,会影响到其他部分。
(8)好莱坞原则:高层组件对待底层组件的方式是“别调用我们,我们会调用你”。
(9)单一原则:一个类应该只有一个引起变化的原因。
参考书籍《HeadFirst设计模式》
示例:
(1)单例模式

public class SingletonDemo implements Serializable { // final域修饰的私有静态成员 private transient static final SingletonDemo INSTANCE = new SingletonDemo(); // 私有的构造器 private SingletonDemo() {} // 公有的静态工厂方法 public static SingletonDemo getInstance() { return INSTANCE; } private Object readResolve() { return INSTANCE; } }
40、Maven和ANT的区别
41、什么是RPC远程过程调用?
42、什么是Dubbo?Dubbo中有哪些角色?Dubbo执行流程是什么?Dubbo支持的协议有哪些?Dubbo支持的注册中心有哪些?
调用关系说明:
(1)服务容器 Container
负责启动,加载,运行服务提供者。
(2)服务提供者 Provider
在启动时,向注册中心注册自己提供的服务。
(3)服务消费者 Consumer
在启动时,向注册中心订阅自己所需的服务。
(4)注册中心 Registry
返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
(5)服务消费者 Consumer
,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
(6)服务消费者 Consumer
和提供者 Provider
,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心 Monitor
。
dubbo支持的通信协议:dubbo、rmi、hessian、http、webservice、thrift、memcached、redis等。
Dubbo支持的注册中心:ZooKeeper注册中心、Redis注册中心、Multicast注册中心。
代码示例:https://www.jianshu.com/p/cb9373c1ddba
服务提供方:

<!-- 提供方应用信息,用于计算依赖关系 --> <dubbo:application name="gw-service-user" /> <!-- 使用zookeeper注册中心暴露服务地址 --> <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" /> <!-- 用dubbo协议在20880端口暴露服务 --> <dubbo:protocol name="dubbo" port="20880" /> <!-- 用户服务接口 --> <dubbo:service interface="wusc.edu.facade.user.service.PmsUserFacade" ref="pmsUserFacade" />
服务消费方:

<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 --> <dubbo:application name="edu-web-boss" /> <!-- 使用zookeeper注册中心暴露服务地址 --> <!-- 注册中心地址 --> <dubbo:registry protocol="zookeeper" address="127.0.0.1:2181" /> <!-- 用户服务接口 --> <dubbo:reference interface="wusc.edu.facade.user.service.PmsUserFacade" id="pmsUserFacade" check="false" />
43、ZooKeeper的作用是什么?
(1)master节点选举,主节点挂了以后,从节点就会接受工作,并保证这个节点是唯一的,这也是所谓的首脑模式,从而保证我们的集群是高可用的。
(2)统一配置文件管理,即只需要不熟一台服务器,则可以把相同配置文件同步更新到其他所哟与的服务器,次操作在云计算中用的特别的多(假设修改了redis统一配置)。
(3)发布与订阅,类似消息队列MQ(amq,rmq。。),dubbo发布者把数据存在znode上,订阅者会读取这个数据。
(4)提供分布式锁,分布式环境不同进程之间争夺资源,类似于多线程中的锁。
(5)集群管理,集群保证数据的强一致性。 主节点数据会同步到附属节点 客户端不管链接任何客户端的时候都会保证读取数据的一致性。
dubbo中zookeeper做注册中心,如果注册中心集群都挂掉,那发布者和订阅者还能通信吗?(面试高频题)
zookeeper的信息会缓存到服务器本地作为一个cache缓存文件,并且转换成properties对象方便使用,每次调用时,按照本地存储的地址进行调用,但是无法从注册中心去同步最新的服务列表,短期的注册中心挂掉是不要紧的,但一定要尽快修复。所以挂掉是不要紧的,但前提是你没有增加新的服务,如果你要调用新的服务,则是不能办到的。44、什么是跨域?怎么解决
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源(域名,协议,端口均相同)策略造成的,是浏览器对JS施加的安全限制。
解决办法:(1)JSONPP只支持GET请求;(2)代理:写一个接口www.123.com/server,由这个接口在后端去调用www.456.com/server并拿到返回值,然后再返回给index.htm;这样就绕过了浏览器端,不存在跨域问题。
来源:https://www.cnblogs.com/wennice/p/7150461.html
45、什么是JMS?JMS有哪些模型?
JMS 全称:Java Message Service,JMS是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的消息传输。
来源:https://www.cnblogs.com/xinhuaxuan/p/6104274.html
46、为什么要使用连接池?数据库连接池的原理?项目中使用的数据库连接池?
(1)数据库连接是一件费时的操作,连接池可以使多个操作共享一个连接,提高对数据库连接资源的管理。
(2)基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量、使用情况,为系统开发,测试及性能调整提供依据。
常用数据库连接池:DBCP、C3P0、druid、Proxool。
参考:https://www.cnblogs.com/nuccch/p/8120349.html
47、Struts2的执行流程
(1)客户端初始化一个指向Servlet容器(例如Tomcat)的请求。
(2)这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做ActionContextCleanUp的可选过滤器,这个过滤器对于Struts2和其他框架的集成很有帮助,例如:SiteMesh Plugin)。
(3)接着FilterDispatcher被调用,FilterDispatcher询问ActionMapper来决定这个请求是否需要调用某个Action。
(4)如果ActionMapper决定需要调用某个Action,FilterDispatcher把请求的处理交给ActionProxy。
(5)ActionProxy通过ConfigurationManager询问框架的配置文件,找到需要调用的Action类 ,这里,我们一般是从struts.xml配置中读取。
(6)ActionProxy创建一个ActionInvocation的实例。
(7)ActionInvocation实例使用命名模式来调用,在调用Action的过程前后,涉及到相关拦截器(Intercepter)的调用。
(8)一旦Action执行完毕,ActionInvocation负责根据struts.xml中的配置找到对应的返回结果。返回结果通常是(但不总是,也可能是另外的一个Action链)一个需要被表示的JSP或者FreeMarker的模版。在表示的过程中可以使用Struts2 框架中继承的标签。在这个过程中需要涉及到ActionMapper。
48、SpringMVC的执行流程
(1)用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
(2)DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
(3)DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(…)方法)
(4)提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作: HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息; 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等; 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等; 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中;
(5)Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
(6)根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
(7)ViewResolver 结合Model和View,来渲染视图;
(8)将渲染结果返回给客户端。
49、说一下Spring中的两大核心
两大核心:(1)IOC(Inversion of Control, 控制反转)把创建对象的操作交给框架,亦被称为 DI(Dependency Injection, 依赖注入);(2)AOP(面向切面编程)。
IOC:主要是对bean的注册以及对bean中参数的初始化。
(1)控制反转:
最基础的调用的对象是通过new一个对象出来,例如: People p=new People();
我们Spring框架中的IOC即改变这种方式的调用,将后面“new People”转换为xml文件去调用,即使用第三者调用
(2)依赖注入
实现依赖注入的三种方式:setter方式注入,构造注入,使用P命名实现注入
AOP:
面向切面编程,简单地说就是在不改变原程序的基础上为代码段增加新功能,对代码段进行增强处理
前置增强、后置增强、最终增强、异常增强、环绕增强(定义一个用于增强的FirstAop类)
(1)使用XML配置:

<!-- 配置切面 --> <aop:config> <!-- 声明目标点 --> <aop:pointcut expression="execution(* xcj.homepage.service..*Service*.*(..))" id="target"/> <!-- 连接切点跟被切点 --> <aop:aspect ref="first"> <!-- 前置增强 --> <aop:before method="pre_study" pointcut-ref="target"/> <!-- 后置增强 --> <aop:after-returning method="do_work" returning="result" pointcut-ref="target"/> <!-- 最终增强 --> <aop:after method="do_final" pointcut-ref="target"/> <!-- 异常增强 --> <aop:after-throwing method="do_exception" throwing="e" pointcut-ref="target"/> <!-- 环绕增强 --> <aop:around method="around" pointcut-ref="target"/> </aop:aspect> </aop:config>
(2)使用注解:
TODO..(要单独写一个SpringAOP相关知识点的博客)
Spring AOP详解:https://www.cnblogs.com/hongwz/p/5764917.html
Spring AOP实现原理:https://baijiahao.baidu.com/s?id=1615034709376787673&wfr=spider&for=pc
https://blog.csdn.net/u010452388/article/details/80868392
50、Spring的事务的传播特性
Spring事务传播特性: 该特性是保证事务是否开启,业务逻辑是否使用同一个事务的保证。当事务在传播过程中会受其影响。其传播特性包括
(1)PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
(2)PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
(3)PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
(4)PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
(5)PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
(6)PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
(7)PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
Spring提供了两种事务管理方式, 编程式事务和声明式事务。
编程式事务指的是通过编码方式实现事务控制;
声明式事务基于 AOP,将具体业务逻辑与事务处理逻辑解耦。
Spring中声明式事务处理有两种方式,一种是在配置文件(xml)中做相关的事务规则声明,另一种是基于@Transactional 注解的方式。
事务的四大特性ACID:原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
(1)原子性:一个事务必须被视为一个不可分割的最小工作单元,要么全部提交成功,要么全部失败回滚(一个事务中的多个操作要么都成功要么都失败)。
(2)一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态(例如存钱操作,存之前和存之后的钱数应该是一致的)。
(3)隔离性:一个事务所做的修改在最终提交以前,对其他事务是不可见的(事务与事务应该是相互隔离的)。
(4)持久性:一旦事务提交,则其所做的修改就会永久保存到数据库中(事务一旦提交,数据要持久保存)。
数据库
51、数据库MySQL,Oracle分页时用的语句
MySQL:
SELECT * FROM admin_company LIMIT 0,10; SELECT * FROM admin_company ORDER BY id LIMIT 0,10;
Oracle:
SELECT * FROM (SELECT ROWNUM rn ,* FROM admin_company WHERE rn <= 10) WHERE rn > 0;
没有ORDER BY,两层查询
rownum伪列产生的序号是按照数据被查询出来的顺序添加上去的,第一条是1,第二条是2,依次加1。
在ORACLE中使用rownum伪列分页时,需要多加一层查询,以保证rownum序号的连续性。
SELECT * FROM (SELECT ROWNUM rn , c.* FROM (SELECT * FROM admin_company ORDER BY companyno) c WHERE rn <= 10 ) WHERE rn > 0;
有ORDER BY,三层查询
Orcale排序后分页查询,需要多加一层查询原因:
参考 https://www.cnblogs.com/lgzslf/archive/2010/05/30/1747469.html
52、SQL怎么优化执行效率更高、SQL优化经验
(1)SELECT子句中避免使用‘*’:Oracle在解析的过程中, 会将‘*’依次转换成所有的列名, 这个工作是通过查询数据字典完成的, 这意味着将耗费更多的时间。
(2)使用表的别名(Alias): 当在SQL语句中连接多个表时, 请使用表的别名并把别名前缀于每个Column上。这样一来,就可以减少解析的时间并减少那些由Column歧义引起的语法错误。
(3)用IN来替换OR、用UNION替换OR (适用于索引列)、用EXISTS替代IN、用NOT EXISTS替代NOT IN。
(4)如果不需要去重,用UNION-ALL 替换UNION:UNION 将对结果集合进行合并和排序,这个操作会使用到SORT_AREA_SIZE这块内存,UNION ALL 将重复输出两个结果集合中相同记录,排序也不是必要的,效率就会因此得到提高。
(5)优化GROUP BY:提高GROUP BY 语句的效率,可以通过将不需要的记录在GROUP BY 之前过滤掉。
(6)使用DECODE函数来减少处理时间。【TODO】
(7)用Where子句替换HAVING子句:HAVING 只会在检索出所有记录之后才对结果集进行过滤。这个处理需要排序,总计等操作。
(8)合理使用索引:
①避免在索引列上使用NOT,当Oracle“遇到”NOT,他就会停止使用索引转而执行全表扫描。
②避免在索引列使用 !=、||、+,WHERE子句中,优化器将不使用索引而使用全表扫描。
③避免在索引列上使用计算。WHERE子句中,如果索引列是函数的一部分。优化器将不使用索引而使用全表扫描。
④避免在索引中使用任何可以为空的列,Oracle将无法使用该索引。对于单列索引,如果列包含空值,索引中将不存在此记录。对于复合索引,如果每个列都为 空,索引中同样不存在此记录。如果至少有一个列不为空,则记录存在于索引中。
⑤总是使用索引的第一个列:如果索引是建立在多个列上,只有在它的第一个列(leading column)被where子句引用时,优化器才会选择使用该索引。这也是一条简单而重要的规则,当仅引用索引的第二个列时,优化器使用了全表扫描而忽略 了索引。
⑥避免对WHERE子句的列名使用函数(避免改变索引列的类型):当比较不同数据类型的数据时,如:
SELECT * FROM EMP WHERE EMPNO = TO_NUMBER(‘123') ; 类型转换没有发生在索引列上,索引的用途没有被改变。
SELECT * FROM EMP WHERETO_NUMBER(EMP_TYPE)=123; 因为内部发生的类型转换, 这个索引将不会被用到。
SQL优化大全 https://blog.csdn.net/hguisu/article/details/5731629
53、数据库索引的了解
作用:提高查询速度、确保数据的唯一性、可以加速表和表之间的连接,实现表和表之间的参照完整性、使用分组和排序子句进行数据检索时,可以减少分组和排序的时间、全文检索字段进行搜素优化。
分类:主键索引(PRIMAY KEY)、唯一索引(UNIQUE)、常规索引(INDEX)、全文索引(FULLTEXT)。
主键索引的几种创建方式:确保数据记录的唯一性,主键索引只能有一个。(以下为MYSQL示例)
CREATE TABLE mytable ( ID INT(11) AUTO_INCREMENT PRIMARY KEY, username VARCHAR (16) NOT NULL #或 PRIMARY KEY(`ID`) ) ;
唯一索引的几种创建方式:避免同一个表中某数据列中的值重复,唯一索引可有多个。(以下为MYSQL示例)
(1)创建索引: CREATE UNIQUE INDEX indexName ON mytable(username(length));
(2)修改表结构: ALTER table mytable ADD UNIQUE [indexName] (username(length));
(3)创建表时指定:
CREATE TABLE mytable ( ID INT NOT NULL, username VARCHAR (16) NOT NULL, UNIQUE [ indexName ] (username (LENGTH)) # 或者username VARCHAR(16) NOT NULL UNIQUE ) ;
常规索引的几种创建方式:快速定位特定数据,应加在查询条件的字段,不易添加太多常规索引,影响数据的插入,删除和修改操作,使用KEY或INDEX关键字设置。(以下为MYSQL示例)
(1)创建表时添加:
CREATE TABLE mytable ( ID INT NOT NULL, userno VARCHAR (16) NOT NULL, username VARCHAR (16) NOT NULL, loginname VARCHAR (16) NOT NULL, INDEX `index1` (userno, username), KEY `index2` (userno, loginname) ) ;
(2)创建后追加: ALTER TABLE `mytable` ADD INDEX `ind` (`userno`,`username`);
全文索引的几种创建方式:快速定位特定数据,只能用于MyISAM类型的数据表,只能用于CHAR ,VARCHAR,TEXT数据列类型。(以下为MYSQL示例)
(1)创建表时添加:
CREATE TABLE mytable( username VARCHAR (16) NOT NULL, FULLTEXT(`username`) )ENGINE=MYISAM;
(2)创建后追加: ALTER TABLE mytable ADD FULLTEXT(`username`);
JAVA虚拟机
54、简述Java内存管理机制,以及垃圾回收的原理和使用过Java调优工具,垃圾回收器可以马上回收内存吗?如何通知虚拟机进行垃圾回收?
内存管理主要包括内存分配和内存回收两个部分。
JAVA程序执行过程
JAVA内存模型划分:
类成员初始化顺序总结:先静态后普通再构造, 先父类后子类,同级看书写顺序。
垃圾回收机制:同上面[10、简述Java的垃圾回收机制]知识点。
JAVA监控工具:
jconsole
: 提供JVM活动的图形化视图,包括线程的使用,类的使用和GC活动。
jvisualvm
: 监视JVM的GUI工具,可以用来剖析运行的应用,分析JVM堆转储。
Java语言规范并不保证GC一定会执行。强制执行垃圾回收(不符合规范):System.gc()。Runtime.getRuntime().gc(),手动执行System.gc(),通知GC运行。
调用了System.gc(),也只是通知虚拟机要回收垃圾,至于虚拟机什么时候运行回收器就不知道了。
来源:https://www.cnblogs.com/KingIceMou/p/6967129.html
《深入理解Java虚拟机》https://www.cnblogs.com/scorpio-cat/p/jvm.html
55、描述JVM加载class文件的原理机制
类加载机制的流程:包括了加载、连接(验证、准备、解析)、初始化五个阶段。
(1)加载:查找装载二进制文件,通过一个类的全限定名获取类的二进制字节流,并将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。
(2)验证:为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
(3)准备:准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
(4)解析:解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程,可选。
(5)初始化:初始化阶段是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,也就是执行类构造器()方法的过程。
56、说说JVM原理?内存泄漏与溢出的区别?何时产生内存泄漏?
JVM原理:
JVM是Java Virtual Machine(Java虚拟机)的缩写,它是整个java实现跨平台的最核心的部分,所有的Java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行,也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。JVM是Java平台的基础,和实际的机器一样,它也有自己的指令集,并且在运行时操作不同的内存区域。JVM通过抽象操作系统和CPU结构,提供了一种与平台无关的代码执行方法,即与特殊的实现方法、主机硬件、主机操作系统无关。JVM的主要工作是解释自己的指令集(即字节码)到CPU的指令集或对应的系统调用,保护用户免被恶意程序骚扰。JVM对上层的Java源文件是不关心的,它关注的只是由源文件生成的类文件(.class文件)。
内存泄漏与溢出的区别:
1) 内存泄漏是指分配出去的内存无法回收了。
2) 内存溢出是指程序要求的内存,超出了系统所能分配的范围,从而发生溢出。比如用byte类型的变量存储10000这个数据,就属于内存溢出。
3) 内存溢出是提供的内存不够;内存泄漏是无法再提供内存资源。
何时产生内存泄漏:
(1)静态集合类:在使用Set、Vector、HashMap等集合类的时候需要特别注意,有可能会发生内存泄漏。当这些集合被定义成静态的时候,由于它们的生命周期跟应用程序一样长,这时候,就有可能会发生内存泄漏。
(2)监听器:在Java中,我们经常会使用到监听器,如对某个控件添加单击监听器addOnClickListener(),但往往释放对象的时候会忘记删除监听器,这就有可能造成内存泄漏。好的方法就是,在释放对象的时候,应该记住释放所有监听器,这就能避免了因为监听器而导致的内存泄漏。
(3)各种连接:Java中的连接包括数据库连接、网络连接和io连接,如果没有显式调用其close()方法,是不会自动关闭的,这些连接就不能被GC回收而导致内存泄漏。一般情况下,在try代码块里创建连接,在finally里释放连接,就能够避免此类内存泄漏。
(4)外部模块的引用:调用外部模块的时候,也应该注意防止内存泄漏。如模块A调用了外部模块B的一个方法,如:public void register(Object o)。这个方法有可能就使得A模块持有传入对象的引用,这时候需要查看B模块是否提供了去除引用的方法,如unregister()。这种情况容易忽略,而且发生了内存泄漏的话,比较难察觉,应该在编写代码过程中就应该注意此类问题。
(5)单例模式:使用单例模式的时候也有可能导致内存泄漏。因为单例对象初始化后将在JVM的整个生命周期内存在,如果它持有一个外部对象(生命周期比较短)的引用,那么这个外部对象就不能被回收,而导致内存泄漏。如果这个外部对象还持有其它对象的引用,那么内存泄漏会更严重,因此需要特别注意此类情况。这种情况就需要考虑下单例模式的设计会不会有问题,应该怎样保证不会产生内存泄漏问题。
57、Java的类加载器都有哪些,每个类加载器都有加载哪些类,什么是双亲委派模型,是做什么的?
Java的类加载器有三个,对应Java的三种类:
Bootstrap Loader // 负责加载系统类 (指的是内置类,像是String,对应于C#中的System类和C/C++标准库中的类)
|
- - ExtClassLoader // 负责加载扩展类(就是继承类和实现类)
|
- - AppClassLoader // 负责加载应用类(程序员自定义的类)。
双亲委派模型过程:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
使用双亲委派模型的好处在于Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
参考:https://blog.csdn.net/xu768840497/article/details/79175335
58、Java 中会存在内存泄漏吗,请简单描述。
存在,JAVA中内存泄露的发生场景,通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是JAVA中的内存泄露。
来源:https://www.cnblogs.com/Berryxiong/p/6220890.html
多线程
59、启动一个线程是调用 run() 还是 start() 方法?start() 和 run() 方法有什么区别?
start():它的作用是启动一个新线程,新线程处于就绪状态,拿到cpu执行权就会执行相应的run()方法。start()不能被重复调用。
run():run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程。
60、Thread类的方法sleep()、yield()与Object的方法wait()、notify()比较。
61、用Thread和Runnable哪种方式更好?区别是什么?
显然是用Runnable更好,实现Runnable接口比继承Thread类所具有的优势:
(1)适合多个相同的程序代码的线程去处理同一个资源。
(2)可以避免java中的单继承的限制。
(3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
(4)线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类。
实现线程几种方式:(1)继承java.lang.Thread类;(2)实现java.lang.Runnable接口;(3)实现java.util.concurrent.Callable接口。

public class ThreadDemo { //进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位) //线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位) //多线程的好处:发挥多核CPU的优势、防止阻塞、便于建模 //单核CPU上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作 //如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行 //假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了 class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; System.out.println(name); } @Override public void run() { for (int i=0;i<3;i++) { System.out.println("--线程"+ name); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Test public void testMyThread() { Thread thread1 = new MyThread("1"); Thread thread2 = new MyThread("2"); Thread thread3 = new MyThread("3"); Thread thread4 = new MyThread("4"); Thread thread5 = new MyThread("5"); Thread thread6 = new MyThread("6"); System.out.println("====thread start===="); System.out.println("====thread1.run()===="); thread1.run(); // 直接调用run则为普通方法,按顺序执行 System.out.println("====thread2.run()===="); thread2.run(); System.out.println("====thread3.start()===="); thread3.start(); System.out.println("====thread4.start()===="); thread4.start(); System.out.println("====thread5.run()===="); thread5.run(); System.out.println("====thread6.run()===="); thread6.run(); System.out.println("====thread end===="); } class MyRunnable implements Runnable { private String name; public MyRunnable(String name) { this.name = name; System.out.println(name); } @Override public void run() { for (int i=0;i<3;i++) { System.out.println("--线程"+ name); try { Thread.sleep((int) Math.random() * 10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } @Test public void testMyRunnable() { Thread thread1 = new Thread(new MyRunnable("1")); Thread thread2 = new Thread(new MyRunnable("2")); Thread thread3 = new Thread(new MyRunnable("3")); Thread thread4 = new Thread(new MyRunnable("4")); Thread thread5 = new Thread(new MyRunnable("5")); Thread thread6 = new Thread(new MyRunnable("6")); System.out.println("====thread start===="); System.out.println("====thread1.run()===="); thread1.run(); // 直接调用run则为普通方法,按顺序执行 System.out.println("====thread2.run()===="); thread2.run(); System.out.println("====thread3.start()===="); thread3.start(); System.out.println("====thread4.start()===="); thread4.start(); System.out.println("====thread5.run()===="); thread5.run(); System.out.println("====thread6.run()===="); thread6.run(); System.out.println("====thread end===="); } class Mycallable implements Callable<String> { @Override public String call() throws Exception { String str = ""; str += "A"; System.out.println(str); return str; } } @Test public void testCallable() { Mycallable mycallable = new Mycallable(); // 1.执行 Callable 方式,需要 FutureTask 实现类的支持 FutureTask<String> futureTask = new FutureTask<String>(mycallable); Thread thread = new Thread(futureTask); System.out.println("--start--"); thread.start(); System.out.println("--end--"); try { System.out.println("11" + futureTask.get()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ExecutionException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
62、线程安全理解
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
还有一种通俗的解释:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
这个问题有值得一提的地方,就是线程安全也是有几个级别的:
(1)不可变
像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用
(2)绝对线程安全
不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
(3)相对线程安全
相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。
Vector 和 ArrayList 实现了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 关键修饰。但对于复合操作,Vector仍然需要进行同步处理。
63、ThreadLocal
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储。变量值的共享可以使用public static变量的形式,所有的线程都使用同一个public static变量,但是如果每一个线程都有自己的变量该如何共享呢,就是通过ThreadLocal,ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。
ThreadLocal方法:
get()方法是用来获取ThreadLocal在当前线程中保存的变量副本;
set()用来设置当前线程中变量的副本;
remove()用来移除当前线程中变量的副本;
initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。
代码示例:

public class ThreadLocalDemo { private static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(); public static void setValue(ThreadLocalValue value) { threadLocal.set(value); } public static ThreadLocalValue getValue() { return (ThreadLocalValue) threadLocal.get(); } public static void remove() { threadLocal.remove(); } @Test public void test() { ThreadLocalDemo.setValue(ThreadLocalValue.VALUE_1); System.out.println(ThreadLocalDemo.getValue()); ThreadLocalDemo.remove(); System.out.println(ThreadLocalDemo.getValue()); } } enum ThreadLocalValue { VALUE_1, VALUE_2; }
64、synchronized
synchronized:当一个线程获取了对应的锁,其他线程便只能一直等待,等待获取锁的线程释放锁,而获取锁的线程释放锁会有三种情况:
(1)获取锁的线程执行完该代码块,然后线程释放对锁的占有;
(2)线程执行发生异常,此时JVM会让线程自动释放锁;
(3)调用wait方法,在等待的时候立即释放锁,方便其他的线程使用锁。
synchronized作用:
(1)可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作);
(2)可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)。
synchronized3种应用方式:
(1)修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。
(2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。
(3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
修饰实例方法代码错误示例:synchronized修饰实例方法,获得当前实例的锁;启用两个线程,两个线程分别拥有两个不同的对象实例锁,故不会产生互斥,得不到期望的结果。

public class SynchronizedDemo implements Runnable { // 共享资源 static int no = 0; public synchronized void increase() { no++; } @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含义:当前线程等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修饰实例方法,获得当前实例的锁;两个线程两个实例,得到的数量可能少于2000 } }
上述代码,increase()改为静态方法加synchronized,则可以得到期望的结果,因为静态方法同步锁,锁定的是当前类对象。

/** * synchronized同步不仅可以阻止一个线程看到对象处于不一致的状态之中, * 它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。 * */ public class SynchronizedDemo implements Runnable { // 共享资源 static int no = 0; public static synchronized void increase() { no++; } @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含义:当前线程等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修饰静态方法,获得当前类对象的锁;得到结果都为2000 } }
如果使用实例方法,想产生互斥,则需要两个线程拥有的是同一个对象实例,如下:

public class SynchronizedDemo implements Runnable { static SynchronizedDemo instance = new SynchronizedDemo(); // 共享资源 static Integer no = 0; public synchronized void increase() { no++; } @Override public void run() { for (int i = 0; i < 1000; i++) { increase(); } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo = new SynchronizedDemo(); Thread t1 = new Thread(demo); Thread t2 = new Thread(demo); t1.start(); t2.start(); // join含义:当前线程等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修饰普通方法,两个线程拥有同一对象实例;得到结果都为2000 } }
修饰代码块代码错误示范:synchronized修饰代码块,锁为this-当前实例对象锁;锁定的是两个不同的实例,故得到结果可能少于2000

public class SynchronizedDemo implements Runnable { // 共享资源 static int no = 0; public void increase() { no++; } @Override public void run() { // this,当前实例对象锁 synchronized(this){ for (int i = 0; i < 1000; i++) { increase(); } } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含义:当前线程等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修饰代码块,锁为this-当前实例对象锁;得到结果可能少于2000 } }
如果改成锁定SynchronizedDemo.class,则可以得到期望的结果

public class SynchronizedDemo implements Runnable { // 共享资源 static int no = 0; public void increase() { no++; } @Override public void run() { synchronized(SynchronizedDemo.class){ for (int i = 0; i < 1000; i++) { increase(); } } } @Test public void testMethod() throws InterruptedException { SynchronizedDemo demo1 = new SynchronizedDemo(); SynchronizedDemo demo2 = new SynchronizedDemo(); Thread t1 = new Thread(demo1); Thread t2 = new Thread(demo2); t1.start(); t2.start(); // join含义:当前线程等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修饰代码块,获得class对象锁;得到结果都为2000 } }
或者改成锁定同一对象实例,也可以得到正常结果

public class SynchronizedDemo implements Runnable { static SynchronizedDemo instance = new SynchronizedDemo(); // 共享资源 static Integer no = 0; public void increase() { no++; } @Override public void run() { synchronized(instance){ for (int i = 0; i < 1000; i++) { increase(); } } } @Test public void testMethod() throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); // join含义:当前线程等待thread线程终止之后才能从thread.join()返回 t1.join(); t2.join(); System.out.println(no); // synchronized修饰代码块,获得是同一个对象实例;得到结果都为2000 } }
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
synchronized的可重入性:synchronized是基于原子性的内部锁机制,是可重入的。
锁重入:一个线程得到一个对象锁后再次请求该对象锁。也就是,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。

public class Student { public static void main(String[] args) { Student student = new Student(); student.doA(); } public synchronized void doA() { System.out.println("do a"); doB(); } public synchronized void doB() { System.out.println("do b"); } }
65、Lock
Lock是位于java.util.concurrent.locks.Lock包下的一个接口。主要提供方法有:

public interface Lock { /** * 获取锁 */ void lock(); /** * 获取锁 * @throws InterruptedException */ void lockInterruptibly() throws InterruptedException; /** * 获取锁 * @return */ boolean tryLock(); /** * 获取锁 * 在时间期限之内如果还拿不到锁,就返回false;如果一开始拿到锁或者在等待期间内拿到了锁,则返回true * @param time * @param unit * @return * @throws InterruptedException */ boolean tryLock(long time, TimeUnit unit) throws InterruptedException; /** * 释放锁 */ void unlock(); /** * * @return */ Condition newCondition(); }
获取锁:lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()
释放锁:unLock()
使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

package xcj.homepage.concurrent; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockDemo { public void lockTest() { Lock lock = new ReentrantLock(); lock.lock(); // 获取锁 try { System.out.println("处理任务"); } finally { lock.unlock(); // 释放锁 } } public void tryLockTest() { Lock lock = new ReentrantLock(); if (lock.tryLock()) { // 获取到锁 try { System.out.println("处理任务"); } finally { lock.unlock(); // 释放锁 } } else { System.out.println("未获取到锁,做其他事情"); } } public void lockInterruptiblyTest() throws InterruptedException { Lock lock = new ReentrantLock(); lock.lockInterruptibly(); // 获取锁 try { System.out.println("处理任务"); } finally { lock.unlock(); // 释放锁 } } }
当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的,调用thread.interrupt()即可中断阻塞过程中的线程。
66、ReentrantLock
ReentrantLock实现了Lock接口,也是可重入锁。
使用lock()示例:

package xcj.homepage.concurrent; import java.util.ArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private ArrayList<Integer> arrayList = new ArrayList<Integer>(); private Lock lock = new ReentrantLock(); // 注意这个地方 public static void main(String[] args) { final ReentrantLockDemo test = new ReentrantLockDemo(); new Thread() { public void run() { test.insert(Thread.currentThread()); }; }.start(); new Thread() { public void run() { test.insert(Thread.currentThread()); }; }.start(); } public void insert(Thread thread) { lock.lock(); try { System.out.println(thread.getName() + "得到了锁"); for (int i = 0; i < 5; i++) { arrayList.add(i); } } catch (Exception e) { // TODO: handle exception } finally { System.out.println(thread.getName() + "释放了锁"); lock.unlock(); } } }
执行结果:

Thread-0得到了锁 Thread-0释放了锁 Thread-1得到了锁 Thread-1释放了锁
使用lockInterruptibly()响应中断,通过调用thread.interrupt()示例:

package xcj.homepage.concurrent; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo { private Lock lock = new ReentrantLock(); public static void main(String[] args) { ReentrantLockDemo demo = new ReentrantLockDemo(); MyThread thread1 = new MyThread(demo); MyThread thread2 = new MyThread(demo); thread1.start(); thread2.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } thread2.interrupt(); // 中断 } public void insert(Thread thread) throws InterruptedException { lock.lockInterruptibly(); // 注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出 try { System.out.println(thread.getName() + "得到了锁"); long startTime = System.currentTimeMillis(); for (;;) { if (System.currentTimeMillis() - startTime >= Integer.MAX_VALUE) break; // 插入数据 } } finally { System.out.println(Thread.currentThread().getName() + "执行finally"); lock.unlock(); System.out.println(thread.getName() + "释放了锁"); } } } class MyThread extends Thread { private ReentrantLockDemo demo = null; public MyThread(ReentrantLockDemo demo) { this.demo = demo; } @Override public void run() { try { demo.insert(Thread.currentThread()); } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "被中断"); } } }
执行结果:

Thread-0得到了锁
Thread-1被中断
67、Lock和synchronized的不同点
(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
(2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
(4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
(5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
参考链接:
synchronized:https://blog.csdn.net/zjy15203167987/article/details/82531772
synchronized-锁重入:https://blog.csdn.net/zjy15203167987/article/details/80558515
面试synchronized: https://www.cnblogs.com/noKing/p/9190673.html
Lock:https://www.cnblogs.com/lucky_dai/p/5498295.html
volatile:https://www.cnblogs.com/kubidemanong/p/9505944.html
ThreadLocal:https://www.jianshu.com/p/98b68c97df9b
https://blog.csdn.net/zjy15203167987/article/details/80480947
queue:https://blog.csdn.net/zjy15203167987/article/details/80400658
线程池之ThreadPoolExecutor使用:https://www.jianshu.com/p/f030aa5d7a28