一、java的基础语法
1.1 基本数据类型和包装类
基本类型 | byte | boolean | char | short | int | float | long | double |
---|---|---|---|---|---|---|---|---|
应用类型 | Byte | Boolean | Character | Short | Integer | Float | Long | Double |
所占字节 | 1 | 1或者4 | 2 | 2 | 4 | 4 | 8 | 8 |
1.2 基本数据类型与包装类的区别
包装类 | 基本数据类型 | |
---|---|---|
包装类是对象,拥有方法和字段, 对象的调用都是通过引用对象的地址 | 基本类型不是 | |
参数传递 | 包装类型是引用的传递 | 基本类型是值的传递 |
声明不同 | 包装类型需要new在堆内存进行new来分配内存空间 | 基本数据类型不需要new关键字 |
存储位置不同 | 包装类型是把对象放在堆中,然后通过对象的引用来调用他们 | 基本数据类型直接将值保存JVM的常量池中 |
初始值不同 | 包装类型的初始值为null | int的初始值为0、boolean的初始值为false |
使用方式不同 | 包装类型是在集合如 coolection Map时会使用 | 基本数据类型直接赋值使用就好 |
1.3 ==和equals 的区别
基本数据类型 | 包装数据类型 | |
---|---|---|
== | 比较是值是否相同 | 比较的是地址指向是否相同(但是在小于高速缓存的时候出现的值得所指向的都是一个内存地址。变量的地址已经在内存变量池中,不会创建新的对象) |
equals | 如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;但是String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的值。 |
1.4 拆箱与装箱问题
package com.zhuangxiaoyan.java.base.javabase.BaseType;
public class IntegerTest {
public static void main(String[] args) {
Integer a=1000;
Integer b=1000;
System.out.println(a==b);
Integer c=100;
Integer d=100;
System.out.println(c==d);
}
}
如果两个引用指向同一个对象,用==表示它们是相等的。如果两个引用指向不同的对象,用==表示它们是不相等的,即使它们的内容相同。如果你看去看 Integer.java 类,你会发现有一个内部静态代码快中,它缓存了从-128到127之间的所有的整数对象。
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
1.5 String、StringBuffer、StringBuilder的区别
String | Stringbuffter | Stringbuilder | ||
---|---|---|---|---|
相同 | 都是继承与AbstractStringBuilder | |||
不同点 | 定长度的不变(底层就是好数组开始就赋值),效率最小 | 会自动进行扩容工作,扩展为原数组长度的2倍加2,线程安全/效率其次synchronized | 会自动进行扩容工作,扩展为原数组长度的2倍加2,线程不安全/ 效率最高 |
StringBuffer初始化及扩容机制
StringBuffer()的初始容量可以容纳16个字符,当该对象的实体存放的字符的长度大于16时,实体容量就自动增加。StringBuffer对象可以通过length()方法获取实体中存放的字符序列长度,通过capacity()方法来获取当前实体的实际容量。底层都是一个字符数组的来实现的。Stringbuffer中就是比Stringbuder多了synchronize关键字。
public StringBuffer() {
super(16);
}
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
1.6 重写和重载的区别
重写(Overriding) | 重载(Overloading) | |
---|---|---|
类的数量 | 父子类、接口与实现类 | 本类 |
方法名称 | 一致 | 一致 |
参数列表 | 一定不能修改(函数体修改) | 必须修改(参数和类型必须不一样)函数体也修改 |
返回类型 | 一定不能修改 | 可以修改 |
异常 | 可以减少或删除,但不能扩展 | 可以修改 |
package com.zhuangxiaoyan.java.base.javabase.BaseType;
public class ClassA {
public void test1(int i,int j){
System.out.println(i+j);
}
}
package com.zhuangxiaoyan.java.base.javabase.BaseType;
public class ClassB extends ClassA{
// 重写父类的方法
@Override
public void test1(int i, int j){
System.out.println(i*j);
}
public void test2(int i, int j){
System.out.println(i*j);
}
// 重载本类的方法
public void test2(Float i, Float j){
System.out.println(i*j);
}
}
1.7 父子类的加载机制:
1.8 Object类中的常用方法
一、clone() 方法
protected native Object clone() throws CloneNotSupportedException;
二、tostring()方法
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
三、getClass()方法
public final native Class<?> getClass();
四、finalize()方法
protected void finalize() throws Throwable { }
五、equals()方法
public boolean equals(Object obj) {
return (this == obj);
}
六、hashcode()方法
public native int hashCode();
七、wait()方法
wait()
wait(long timeout)
wait(long timeout,int naos)
八、notify()方法
public final native void notify();
九、notifyAll()方法
public final native void notifyAll();
1.9 String类源码分析
- 通过new关键字创建字符串对象,一定在堆中。
- 构造器中传入了以双引号引起的字符串对象,则会去查询字符串常量池中找有没有这个字符串对象,有的话直接指向,没有的话就创建。
通过字面量方式(区别于new)给一个String类型引用变量赋值,此时的字符串直接存储在方法区的字符串常量池中(编译期就可以确定)。并且凡是双引号括起来的都在字符串常量池中仅有一份,不会产生多个副本。String类是一个不可变类,一旦一个String对象创建后,包含在这个对象中的字符序列是不可修改的,直到这个对象被销毁。通俗讲就是通过双引号括起来的字符串是不可变的,从出生到死亡都不可变。
String类中Intern()方法:intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中,如果存在那就是直接使用内存池中存在的字符串变量。
在源码中涉及到sun.misc.Version类。sun.misc.Version类会在JDK类库的初始化过程中被加载并初始化,而在初始化时它需要对静态常量字段根据指定的常量值(ConstantValue)做默认初始化,此时被sun.misc.Vversion.launcher静态常量字段所引用的”java”字符串字面量就被intern到HotSpot VM的字符串常量池——StringTable里了。
String的存储位置的变化?
JDK1.6 : jdk6中的常量池是放在 Perm 区中的,Perm区和正常的 JAVA Heap 区域是完全分开的。
JDK1.7中:在 jdk7 的版本中,字符串常量池已经从Perm区移到正常的Java Heap区域了。为什么要移动?Perm 区域太小是一个主要原因,当然据消息称jdk8已经直接取消了Perm区域,而新建立了一个元区域。
以上值都是在JDK已经自带的。在加载时候sun.misc.version的类的时候就已经在存在常量池中。提示这个是openJDK的方法里面才有,其他的版本的中可能就么有相关的类。合理使用intern()确实有降低堆大小的作用,但是也会带来额外的系统开销。因此intern()需要根据场景进行使用,不可滥用。
1.10 计算机中堆栈的区别?
- 栈由操作系统自动分配释放 ,存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。
- 堆由程序员分配释放,若程序员不释放,程序结束时由OS回收,分配方式倒是类似于链表。
- 管理方式不同。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏;
- 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1M,64bits的Linux默认10M;
- 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
- 分配方式不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由操作系统进行释放,无需我们手工实现。
- 分配效率不同。栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
- 栈(stack):主要保存基本类型(或者叫内置类型)(char、byte、short、int、long、float、double、boolean)和对象的引用,数据可以共享,速度仅次于寄存器(register),快于堆。 堆(heap):用于存储对象。
二、Java的关键字
2.1 java中所有的关键字
| Const/goto | 保留关键字,没有具体含义 | | —- | —- | | instanceof | 用来在运行时判断对象是否是指定类及其父类的一个实例,不能比较基本类型。 | | final | 修饰的类叫最终类,该类不能被继承。修饰的方法不能被重写。final修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。 | | voilate | 可见性,volatile 解决了内存可见,所有被volatile关键字修饰的读写都会直接刷到主存中去,保证了变量的可见性,适用于对变量可见性有要求而对读取顺序没有要求的场景。 | | Native | 本地(操作系统函数调用) | | Synchronized | 同步锁 | | Unsafe | Unsafe 类提供了硬件级别的原子操作。 Unsafe 类在 sun.misc 包下,不属于 Java 标准。Java并发包JUC(java.util.concurrent)和很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发。 |
2.2 final与static的区别
final | static | |
---|---|---|
相同点 | 都可以修饰类、方法、成员变量。 | 都可以修饰类、方法、成员变量。 |
都不能用于修饰构造方法。 | 都不能用于修饰构造方法。 | |
final方法是不能被重写 | static 方法不能被重写 | |
不同点 | final 类不能被继承,没有子类,final 类中的方法默认是 final 的。final 标记的成员变量必须在声明的同时赋值,或在该类的构造方法中赋值,不可以重新赋值。 | static 修饰的代码块表示静态代码块,当 Java 虚拟机JVM加载类时,就会执行该代码块,只会被执行一次static 修饰的变量可以重新赋值。 |
final 不可以修饰代码块 | static 可以修饰类的代码块 | |
final可以修饰方法内的局部变量 | static 不可以修饰方法内的局部变量,static 方法必须被实现,而不能是抽象的abstract |
final finally finalize区别
final | final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。 |
---|---|
finally | finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用存放一些关闭资源的代码。 |
finalize | finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。 |
2.3 Synchronized 与volatile 关键字的区别?
volatile | Synchronized | |
---|---|---|
阻塞 | 不会造成线程的阻塞 | 会造成线程的阻塞 |
范围 | 仅能使用在变量级别 | 可以使用在变量,方法,类级别上 |
优化 | 标记的变量不会被编译器优化 | 标记的变量会被编译器优化 |
可见性 | 仅能保证变量的修改可见性,不能保证原子性 | 可以保证变量修改的可见性和原子性 |
层面 | 本质告诉JVM 当前变量在寄存器工作内存中的值是不确定的,需要从主存中去读取。 | 锁定当前变量,当前线程访问该变量,其它线程不可以。 |
三、Java面向对象的概念问题
3.1 面向对象的特性
抽象性:抽象是将类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。
继承性:指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承(C++支持多继承多实现)。
封装性:封装是将代码及其处理的数据绑定在一起的一种编程机制,该机制保证了程序和数据都不受外部干扰且不被误用。封装的目的在于保护信息。
多态性:多态性体现在父类的属性和方法被子类继承后或接口被实现类实现后,可以具有不同的属性或表现方式。
普通类和抽象类有哪些区别
抽象类 | 普通类 |
---|---|
不能被实例化 | 可以实例化 |
有抽象方法,抽象方法只需申明,无需实现,含有抽象方法的类必须申明为抽象类 | |
抽象类的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类 | |
抽象方法不能被声明为静态 | |
抽象方法不能用 private 修饰 | |
抽象方法不能用 final 修饰 |
3.2 抽象类和接口的区别
一个类只能继承一个抽象类,而一个类却可以实现多个接口。(单继承多实现方式)
接口 | 抽象类 | |
---|---|---|
相同点 | 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。实现接口或继承抽象类的普通子类都必须实现这些抽象方法。 | |
不同点 | 1只能包含抽象方法,静态方法和默认方法,不能为普通方法提供方法实现(在JDK1.8可以使用default和static关键字来修饰接口中定义的普通方法) 2接口中的成员变量只能是 public static final 类型 3接口不能包含构造器 4接口里不能包含初始化块 |
1完全可以包含普通方法,接口中的普通方法默认为抽象方法。 2抽象类中的成员变量可以是各种类型的。 3抽象类可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。 4抽象类里完全可以包含初始化块。 |
3.3 创建的对象的方式
1 | 用 new 语句创建对象 | 会调用构造函数 |
---|---|---|
2 | 运用反射,调用 java.lang.Class 或 java.lang.reflect.Constructor 类的newInstance() 方法。 | 会调用构造函数 |
3 | 调用对象的 clone() 方法,利用的是的深拷贝的原理。浅拷贝:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制,深拷贝:除了对象本身被复制外,对象所包含的所有成员变量也将复制。 | 不会调用构造函数 |
4 | 运用反序列化手段,调用 java.io.ObjectInputStream 对象的 readObject() 方法、不会调用构造函数。 | 不会调用构造函数 |
5 | Unsafe中提供allocateInstance方法 ,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。 | 不会调用构造函数 |
四、Java多线程基础概念问题
4.1 线程的状态
Java 中的线程有五种状态分别是:创建、就绪,运行、挂起、结束。
4.2 进程的几种状态
- 运行状态:进程正在处理器上上运行。在单处理器环境下,每个时刻最多只有一个进程处于运行状态。
- 就绪状态:进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器即可运行。
- 阻塞状态:又称为等待状态,进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理器)或等待输入/输出完成。即使处理器空闲,该进程也不能运行。
- 创建状态:进程正在被创建,尚未到就绪状态。
- 结束状态:进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
进程的挂起:
- 用户的请求。
- 父进程的请求。
操作系统的原因。操作系统引起的挂起可分为以下三种:
- 交换。当操作系统发现系统的内存资源已经不能满足运行进程的需要时,可以将当前不重要的进程挂起,以达到平衡系统负载的目的。
- 出现问题或故障时。当系统出现故障时,操作系统会暂时将系统中涉及该故障的进程挂起(换出),等故障恢复后,再将这些进程恢复到挂起前的状态(换入,由外存调入到内存)。
- 操作系统的需要。为监视系统的活动,操作系统可以挂起和激活(将外存中的进程调入到内存)一些记录系统资源使用状况的进程和用户进程活动的记账进程。
4.3 进程和线程的区别:
1、进程是资源分配的最小的单位;线程是程序执行的基本单位。
2、进程拥有自己的资源空间,启动一个进程,系统就会为它分配地址空间;而线程与CPU资源分配无关,多个线程共享同一进程内的资源,使用相同的地址空间。
3、一个进程可以包含若干个线程。4.4 线程的创建方式
继承Thread类创建线程
public class FirstThreadTest extends Thread { int i = 0; //重写run方法,run方法的方法体就是现场执行体 public void run() { for (; i < 100; i++) { System.out.println(getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " : " + i); if (i == 50) { new FirstThreadTest().start(); new FirstThreadTest().start(); } } } }
实现Runnable接口创建线程
public class RunnableThreadTest implements Runnable{ private int i; // 重写run方法 public void run() { for(i = 0;i <100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { for(int i = 0;i < 100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); if(i==20) { RunnableThreadTest rtt = new RunnableThreadTest(); new Thread(rtt,"新线程1").start(); new Thread(rtt,"新线程2").start(); } } } }
使用Callable和Future创建线程 ```java import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable
4. 使用线程池例如用ThreadPoolExecutor创建线程
```java
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 15, 200, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(5));
for(int i = 0;i<10;i++){
Runnable run = new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
}
}
};
executor.execute(run);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
Callable和Runnable接口的区别
两者最大的区别:实现Callable接口的任务线程能返回执行结果,而实现Runnable接口的任务线程不能返回执行结果
- Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞线程直到获取“将来”的结果,当不调用此方法时,主线程不会阻塞。
- Callable接口实现类中run()方法允许将异常向上抛出,也可以直接在内部处理(try. catch); 而Runnable接口实现类中run()方法的异常必须在内部处理掉,不能向上抛出。
4.5 Java的线程的通信方式
| 操作系统层面进程通信 | Java层面线程通信 | | —- | —- | | 1 socket通信 | wait/notify 等待 | | 2 消息队列 | Volatile 内存共享 | | 3 信号量 | CountDownLatch ,CyclicBarrier 并发工具 | | 4 共享内存 | 同步、blockingqueue | | 5 管道通信 | LockSupport锁 |
Java的层面的是通信方式有:
方式一:使用 volatile 关键字:基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
方式二:使用Object类的wait() 和 notify() 方法:众所周知,Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。注意: wait和 notify必须配合synchronized使用,wait方法释放锁,notify方法不释放锁。
方式三:使用JUC工具类 CountDownLatch:jdk1.5之后在java.util.concurrent包下提供了很多并发编程相关的工具类,简化了我们的并发编程代码的书
方式五:基本LockSupport实现线程间的阻塞和唤醒:LockSupport 是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是唤醒线程先运行,但是得知道线程的名字。
操作系统的层面线程同步方式:
互斥量(互斥锁/mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。互斥对象和临界区(代码的一个区间)对象非常相似,只是其允许在进程间使用,而临界区只限制于同一进程的各个线程之间使用,但是更节省资源,更有效率。
信号量/semaphore:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。Mutex互斥量可以说是semaphore在仅取值0/1时的特例
事件(信号):通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。比如在某些网络应用程序中,一个线程如A负责侦听通信端口,另外一个线程B负责更新用户数据,利用事件机制,则线程A可以通知线程B何时更新用户数据。
临界区(CCriticalSection)(已被舍弃):当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。具体应用方式:
- 定义临界区对象CcriticalSection g_CriticalSection;
- 在访问共享资源(代码或变量)之前,先获得临界区对象,g_CriticalSection.Lock();
访问共享资源后,则放弃临界区对象,g_CriticalSection.Unlock();
4.6 进程调度策略与线程的调度策略
4.6.1 进程调度策略
时间片轮转调度算法(RR):给每个进程固定的执行时间,根据进程到达的先后顺序让进程在单位时间片内执行,执行完成后便调度下一个进程执行,时间片轮转调度不考虑进程等待时间和执行时间,属于抢占式调度。优点是兼顾长短作业;缺点是平均等待时间较长,上下文切换较费时。适用于分时系统。
- 先来先服务调度算法(FCFS):根据进程到达的先后顺序执行进程,不考虑等待时间和执行时间,会产生饥饿现象。属于非抢占式调度,优点是公平,实现简单;缺点是不利于短作业。
- 优先级调度算法(HPF):在进程等待队列中选择优先级最高的来执行。
- 多级反馈队列调度算法:将时间片轮转与优先级调度相结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的,按时间片轮转。优点是兼顾长短作业,较好响应时间,可行性强,适用于各种作业环境。
- 高响应比优先调度算法:根据“响应比=(进程执行时间+进程等待时间)/ 进程执行时间”这个公式得到的响应比来进行调度。高响应比优先算法在等待时间相同的情况下,作业执行的时间越短,响应比越高,满足段任务优先,同时响应比会随着等待时间增加而变大,优先级会提高,能够避免饥饿现象。优点是兼顾长短作业,缺点是计算响应比开销大,适用于批处理系统。
4.6.2 线程的调度策略
有两种调度策略:
时间片策略:在非常短时间内迅速切换执行不同线程。
抢占式策略:高优先线程抢占cpu。4.7 线程池的种类与使用场景
| newCachedThreadPool(缓存线程池) | 底层:返回ThreadPoolExecutor实例,corePoolSize为0;maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为60L;unit为TimeUnit.SECONDS;workQueue为SynchronousQueue(同步队列) | 使用场景:当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。适用:执行很多短期异步的小程序或者负载较轻的服务器。 | | —- | —- | —- | | newFixedThreadPool(固定数量的线程池) | 底层:返回ThreadPoolExecutor实例,接收参数为所设定线程数量nThread,corePoolSize为nThread,maximumPoolSize为nThread;keepAliveTime为0L(不限时);unit为:TimeUnit.MILLISECONDS;WorkQueue为:new LinkedBlockingQueue() 无解阻塞队列 | 使用场景:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了,如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列),适用:执行长期的任务,性能好很多。 | | newSingleThreadExecutor(单个线程的线程池) | 底层:FinalizableDelegatedExecutorService包装的ThreadPoolExecutor实例,corePoolSize为1;maximumPoolSize为1;keepAliveTime为0L;unit为:TimeUnit.MILLISECONDS;workQueue为:new LinkedBlockingQueue () 无解阻塞队列 | 使用场景:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列),适用:一个任务一个任务执行的场景。 | | NewScheduledThreadPool(延时缓存线程池) | 底层:创建ScheduledThreadPoolExecutor实例,corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE;keepAliveTime为0;unit为:TimeUnit.NANOSECONDS;workQueue为:new DelayedWorkQueue() 一个按超时时间升序排序的队列 | 使用场景:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构。适用:周期性执行任务的场景。 |
4.8 线程池的重要参数
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static,final变量表示线程池的各个状态:
- volatile int runState:runState:表示当前线程池的状态,一个volatile变量用来保证线程之间的可见性.
- Static final int RUNNING= 0:表示线城市初始状态。
- static final int SHUTDOWN = 1:如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕。
- static final int STOP = 2;如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务。
- static final int TERMINATED = 3:当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
- private final BlockingQueue
workQueue:任务缓存队列,用来存放等待执行的任务。 - private final ReentrantLock mainLock = new ReentrantLock():线程池的主要状态锁,对线程池状态(比如线程池大小//、runState等)的改变都要使用这个锁。
- private final HashSet
workers = new HashSet ():用来存放工作集 - private volatile long keepAliveTime: 线程存活时间。
- private volatile boolean allowCoreThreadTimeOut:是否允许为核心线程设置存活时间
- private volatile int corePoolSize:核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
- private volatile int maximumPoolSize:线程池最大能容忍的线程数。
- private volatile int poolSize:线程池中当前的线程数
- private volatile RejectedExecutionHandler handler:任务拒绝策略。
- private volatile ThreadFactory threadFactory:线程工厂,用来创建线程
- private int largestPoolSize:用来记录线程池中曾经出现过的最大线程数
- private long completedTaskCount:用来记录已经执行完毕的任务个数
任务拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务重复此过程
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
线程池的关闭
- ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
线程池任务执行流程
当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行 当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭 。
4.9 wait、notify、notifyAll和await、signal、signalAll
- 如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或 notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
wait()、notify()和notifyAll()是Object类中的方法:
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
- wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
- 调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)
- 调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程;
因此调用wait()方法必须在同步块或者同步方法中进行(synchronized块或者synchronized方法) 调用某个对象的wait()方法,相当于让当前线程交出此对象的monitor(也就是释放锁),然后进入等待状态,等待后续再次获得此对象的锁(Thread类中的sleep方法使当前线程暂停执行一段时间,从而让其他线程有机会继续执行,但它并不释放对象锁)
public class Test { public static Object object = new Object(); public static void main(String[] args) { Thread1 thread1 = new Thread1(); Thread2 thread2 = new Thread2(); thread1.start(); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } static class Thread1 extends Thread{ @Override public void run() { synchronized (object) { try { // synchronized 代码快中 object.wait(); } catch (InterruptedException e) { } System.out.println("线程"+Thread.currentThread().getName()+"获取到了锁"); } } } static class Thread2 extends Thread{ @Override public void run() { synchronized (object) { // synchronized 代码快中 object.notify(); System.out.println("线程"+Thread.currentThread().getName()+"调用了object.notify()"); } System.out.println("线程"+Thread.currentThread().getName()+"释放了锁"); } } }
Condition(条件)是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。
- Condition是个接口,基本的方法就是await()和signal()方法;
- Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
- 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
- synchronized下的wait对应Condtion的await,让当前线程阻塞并释放线程锁的作用。
- synchronized下的notify对应Condtion的signal,随机唤醒阻塞线程继续执行。
synchronized下的notifyAll对应Condtion的signalAll,唤醒所有阻塞线程继续执行。
public class Test { private int queueSize = 10; private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize); private Lock lock = new ReentrantLock(); private Condition notFull = lock.newCondition(); private Condition notEmpty = lock.newCondition(); public static void main(String[] args) { Test test = new Test(); Producer producer = test.new Producer(); Consumer consumer = test.new Consumer(); producer.start(); consumer.start(); } class Consumer extends Thread{ @Override public void run() { consume(); } private void consume() { while(true){ lock.lock(); try { while(queue.size() == 0){ try { System.out.println("队列空,等待数据"); notEmpty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } queue.poll(); //每次移走队首元素 notFull.signal(); System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素"); } finally{ lock.unlock(); } } } } class Producer extends Thread{ @Override public void run() { produce(); } private void produce() { while(true){ lock.lock(); try { while(queue.size() == queueSize){ try { System.out.println("队列满,等待有空余空间"); notFull.await(); } catch (InterruptedException e) { e.printStackTrace(); } } queue.offer(1); //每次插入一个元素 notEmpty.signal(); System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size())); } finally{ lock.unlock(); } } } } }
五、java的常用集合问题
5.1 Arraylist和Linkedlist的区别
Arraylist:底层是基于动态数组,根据下表随机访问数组元素的效率高,向数组尾部添加元素的效率高;但是,删除数组中的数据以及向数组中间添加数据效率低,因为需要移动数组。
Linklist:基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。
ArrayList的默认长度是多少呢?
ArrayList它的默认初始化长度为10;同时它也是支持动态扩容的,通过这个方法进行动态扩容新增加的容量大小为原容量大小的50%。 底层调用的就是Arrays.copyOf(elementData, newCapacity),
LinkedList:可知该链表是双向链表,即可以从头遍历到尾,也可以从尾遍历到头。同样它也是线程不安全的,在这里最可能的造成的并发原因就是链表成环。5.2 HashMap原理
HashMap基于hash原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。
Hashmap是用于存储kv的键值对的数据。通过计算key的Hash值进行的存储。当数据存储大于阈值(默认是0.75)就会发生扩容。扩容成为原来的数据的2倍,为了hashmap采用2倍扩容,可以尽可能的减少元素位置的移动。可以使元素均匀的散布hashmap中,减少hash碰撞。
HashMap的底层实现分为两个不同的版本:
JDK1.7底层实现的: 数组+链表。同时链表的采用的头插法将key元素具有相同hashcode的值的对象进行的插入。由于Hashmap是的线程不安全的。所以会导致扩容时会造成环形链或数据丢失。
JDK1.8以后底层实现: 数据+链表+红黑树(链表>8个节点)。同时链表的采用的尾插法将key元素具有相同hashcode的值的对象进行的插入。由于Hashmap是的线程不安全的。在多线程环境下,发生数据覆盖的情况。5.2.1 HashMap面试问题
Hashmap中解决hash冲突的方式的是采用数组+链表的结构。在链表的插入操作的时候是在头部进行插入的时候更快的。而不是在链表的尾部,因为链表是需要遍历才能找到尾部的。在JDK1.7中的时候hashmap中的采用的结构是数组+链表,但是还是存在效率的问题。就是就是在遍历的链表的时候需要很长的时间。如果是需要采用的二叉树或者是排序树的时候可能有存在当二叉树退化为为链表的时候和严重的性能时间。所以JDK1.8中采用的是数组+链表+红黑树一种数据结构来。
Hashmap中的扩容机制
装载因子,是一个0-1之间的系数,根据它来确定需要扩容的阈值,默认值是0.75,当数据的大于真个数组的0.75倍的时候真个时候扩大为原来的2倍在将原来的数据复制到性的数据中。
负载因子为什么是0.75?
当负载因子是1.0的时候,也就意味着,只有当数组的8个值(这个图表示了8个)全部填充了,才会发生扩容。这就带来了很大的问题,因为Hash冲突时避免不了的。当负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。这种情况就是牺牲了时间来保证空间的利用率。红黑树的查询是log(n)。
HashMap中初始化大小为什么是16? 为什么链表的长度为8是变成红黑树?为什么为6时又变成链表?
链表长度大于八的条件,实际上是需要两个条件的: 当链表长度大于8并且数组长度大于64时,才会转换为红黑树。 如果链表长度大于8,但是数组长度小于64时,还是会进行扩容操作,不会转换为红黑树。因为数组的长度较小,应该尽量避开红黑树。因为红黑树需要进行左旋,右旋,变色操作来保持平衡,所以当数组长度小于64,使用数组加链表比使用红黑树查询速度要更快、效率要更高。
在红黑树的元素小于6的时候会变成链表:
这里需要注意,不是元素小于6的时候一定会变成链表,只有resize的时候才会根据UNTREEIFY_THRESHOLD 进行转换,同样也不是到8的时候就变成红黑树(不是等到扩容的时候) 链表与红黑树的转换详情。
当满足条件1以后调用treeifyBin方法转化红黑树。该方法中,数组如果长度小于MIN_TREEIFY_CAPACITY(64)就选择扩容,而不是转化为红黑树。final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 数组如果长度小于MIN_TREEIFY_CAPACITY(64)就选择扩容,而不是转化为红黑树 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
为什么不采用AVL树二采用的是的是红黑树
- 红黑树和AVL树都是最常用的平衡二叉搜索树,它们的查找、删除、修改都是Olong(n);
- AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
- 红黑树更适合于插入修改密集型任务。
- AVL树的旋转比红黑树的旋转更加难以平衡和调试。AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。
AVL树和红黑树有几点比较和区别
- AVL以及红黑树是高度平衡的树数据结构。它们非常相似,真正的区别在于在任何添加/删除操作时完成的旋转操作次数。
- 两种实现都缩放为O(lg(N)),其中N是叶子的数量,但实际上AVL树在查找密集型任务上更快:利用更好的平衡,树遍历平均更短。另一方面,插入和删除方面,AVL树速度较慢:需要更高的旋转次数才能在修改时正确地重新平衡数据结构。
- 在AVL树中,从根到任何叶子的最短路径和最长路径之间的差异最多为1。在红黑树中,差异可以是2倍。
- 两个都给O(log n)查找,但平衡AVL树可能需要O(log n)旋转,而红黑树将需要最多两次旋转使其达到平衡(尽管可能需要检查O(log n)节点以确定旋转的位置)。旋转本身是O(1)操作,因为你只是移动指针。
Hashmap多线程的安全问题
首先HashMap是线程不安全的,其主要体现:
- 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
- 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
数据丢失:在jdk8之后对hashMap进行了优化,hash碰撞不再采用头插法而采用尾插法插入新节点。若a,b线程put中key的hash值相同,当线程a获取m值后未插入,此时线程b也获取m值后进行插入操作。由于hashmap不同步,导致其中一个线程的value值会丢失,这是不允许的,这就发生了线程安全问题。
HashMap一个并发可能出现的问题是,可能产生元素丢失的现象。考虑在多线程下put操作时,执行addEntry(hash, key, value, i),如果有产生哈希碰撞,导致两个线程得到同样的bucketIndex去存储,就可能会出现覆盖丢失的情况:
put非null元素后get出来的却是null:jdk7的时候,转移元素采用头插法处理。在HashMap的transfer函数中(如下代码),当table进行newTable扩容的时候需要将原先的数据进行转移,链表的顺序将发生翻转,而在此时若hashmap不断轮询,将产生死锁,酿成悲剧。在原理的链表的数据的时候,可能会存放在新节点的因为hash值是 hash(key)%(n-1)来表示:在当一个线程遍历的是时候其他的线程获取到了这个值 这将这里null值放置新的为链表下。所以可能会造成是的null的值。
单链表闭环:多线程put后可能导致get死循环。CPU利用率过高一般是因为出现了出现了死循环,导致部分线程一直运行,占用cpu时间。产生的原因是:那么JDk1.7中使用头插法新的hash桶会倒置原hash桶中的单链表,插入在多个线程同时扩容的情况下就可能导致产生一个存在闭环的单链表,从而导致死循环在。
改进的方法:JDK1.8中的采用的是在尾部进行增加的元素。而不是在链头部。
HashMap的Hash算法的实现原理
JDK1.8中,是通过 hashCode() 的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
jdk8中对HashMap做了哪些改变?
- 在java1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。(桶的数量必须大于64,小于64的时候只会扩容)
- 发生hash碰撞时,java1.7会在链表的头部插入,而java1.8会在链表的尾部插入
在java1.8中,Entry被Node替代(换了一个马甲)。
5.3 ConcurrentHashMap原理
ConcurrentHashMap和HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。整个ConcurrentHashMap由一个一个Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了“槽”来代表一个segment。ConcurrentHashMap 是一个Segment数组Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个segment,这样只要保证每个Segment是线程安全的,也就实现了全局的线程安全。
JDK1.8在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node+ CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
重要的常量:private transient volatile int sizeCtl:当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;当为 0 时,表示 table 还没有初始化;当为其他正数时,表示初始化或者下一次进行扩容的大小。
数据结构:
- Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;
- TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;
- TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。
类中函数方法:
- 存储对象时(put() 方法):
- 如果没有初始化,就调用 initTable() 方法来进行初始化;
- 如果没有 hash 冲突就直接 CAS 无锁插入;
- 如果需要扩容,就先进行扩容;
- 如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历
- 到尾端插入,一种是红黑树就按照红黑树结构插入;
- 如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环
- 如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。
扩容方法 transfer():
- 默认容量为 16,扩容时,容量变为原来的两倍。
- helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。
获取对象时(get()方法):
- 计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;
- 如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找该结点,匹配就返回;
- 以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。
get操作:
首先计算hash值,定位到该table索引位置,如果是首节点符合就返回。如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回,以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash值表示的负数表示正在扩容,这个时候查ForwrdingNode的find方法来定位nextTable来
// eh=-1 说明节点正在迁移需要 这个时候查ForwrdingNode的find方法来定位nextTable来
// eh=-2 说明节点是Treebin此时调用TreeBin的方法来遍历红黑树 由于红黑树可能正在旋转
// eh= 0 说明该节点是一个链表 直接遍历即可。
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
ConcurrentHashMap的get()方法需要加锁吗
不需要,get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
ConcurrentHashMap的数组上有volatile吗?
transient volatile Node<K,V>[] table;
我们知道volatile可以修饰数组的,只是意思和它表面上看起来的样子不同。举个栗子,volatile int array[10]是指array的地址是volatile的而不是数组元素的值是volatile的.get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
ConcurrentHashMap在JDK 1.8中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
- 粒度降低了;
- JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
- 在大量的数据操作下,对于 JVM 的内存压力,基于API 的 ReentrantLock 会开销更多的内存。
既然volatile修饰数组对get操作没有效果那加在数组上的volatile的目的是什么呢?
其实就是为了使得Node数组在扩容的时候对其他线程具有可见性而加的volatile,在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。CAS 操作包含三个操作数 ——内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
- 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
- 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
- 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
- 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
ConcurrentHashMap和Hashtable 的区别?
底层数据结构:
- JDK1.7的 ConcurrentHashMap 底层采用分段的数组+链表实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
- Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用数组+链表的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式(重要):
- 在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。)JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后对 synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
- Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
ConcurrentHashMap为甚不允许插入null,而Hashmap下运行的插入的null值?
不允许插入的空值,在多线程线程场景下容易导致歧义,不清楚使原本就是null 还是被其他线程修改的数据为null。
5.4 HashSet/TreesSet/LinkedHashSet区别
HashSet(无序,唯一):基于 HashMap实现的,底层采用 HashMap 来保存元素。HashSet的构造方法其实就是在内部实例化了一个HashMap对象。HashMap的key是不能重复的存储之前会计算key的Hashcode值和equals()方法。
LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样。TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)
TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的, 自己定义的类必须实现 Comparable接口,并且覆写相应的 compareTo()函数,可以正常使用。
在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序。比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数。
六、java源码常用的API问题
Araays.sort(数组)用来排序的
Araays.aslist()
List 转 Array:List 的 toArray() 方法。
List中的删除某一个对象的是时候采用的:list.revome((object)name)
List中的删除某一个位置的对象 list.revome(index)
List转数组:toArray(arraylist.size())方法
数组转List:Arrays的asList(a)方法
charAt():返回指定索引处的字符。
replace():字符串替换。
trim():去除字符串两端空白。
split():分割字符串,返回一个分割后的字符串数组。
getBytes():返回字符串的 byte 类型数组。
length():返回字符串长度。
toLowerCase():将字符串转成小写字母。
toUpperCase():将字符串转成大写字符。
substring(a,b):截取字符串。截取的是范围是[a,b)的范围 所以如果是要[a,b]那么需要时subString(a,b+1)这才能表示范围是[a,b]。
如何将字符串反转:使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
Reverse()在string中 的函数new StringBuilder().reverse();
使用 StringBuilder 或 StringBuffer 的 reverse 方法,本质都调用了它们的父类 AbstractStringBuilder 的 reverse 方法实现。(JDK1.8)
Math.MAX(a,b)获取a ,b中最大的数字
Math.Min(a,b)获取a ,b中最小的数字
Math.abs(a,b)差的绝对值
HashMap.containkey()
HashMap.containvalue()
HashMap.put()
HaspMap.get();
HaspMap.keySet();
HaspMap.clear();
StringBuffer sb = new StringBuffer("abcd");
System.out.println(sb.reverse().toString());
String[] aa = {"a", "b", "c", "d", "e"}
new StringBuilder(Arrays.toString(aa)).reverse()。
Collections.Min(list) 获取list中的最小值。
Collections.Max(list) 获取list中的最大的值。
Collections.reverse(arrayList); 将数组进行反转
Stack.empty()查看stack是否为空
Stack.peek()获取栈顶元素,但是不删除。
Stack.pop() 获取栈顶元素,但是删除,弹出栈顶元素。
七、java的异常处理机制问题
7.1 Try tach的异常
程序错误分为三种:
1.编译错误;编译错误是因为程序没有遵循语法规则。throws 用在函数上,后面跟的是异常类,可以跟多个; 而 throw 用在函数内,后面跟的是异常对象。
2.运行时错误;运行时错误是因为程序在执行时,运行环境发现了不能执行的操作。(空指针异常、数组越界异常、SQL异常、非法参数异常、找不到类文件异常等)
3.逻辑错误。逻辑错误是因为程序没有按照预期的逻辑顺序执行。异常也就是指程序运行时发生错误,而异常处理就是对这些错误进行处理和控制。
try{
//try块中放可能发生异常的代码。如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。
//如果发生异常,则尝试去匹配catch块。
}catch(SQLException SQLexception){
//每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。
}catch(Exception exception){
//...
}finally{
//finally块通常是可选的。
//无论异常是否发生,异常是否匹配被处理,finally都会执行。
//一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。
}
7.2 内存溢出和内存泄露问题
内存溢出:是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
内存泄露:是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露。
Java内存溢出的几种情况
- Java堆溢出(对象数量到达最大堆的容量限制后就会产生内存溢出异常。)
- 虚拟机栈和本地方法栈溢出。
- 方法区和运行时常量池溢出
- 本机直接内存溢出,DirectMemory容量可通过-XX: MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值 (-Xmx指定)一样。
java中常见的错误的类型
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常 。递归调用方法,如果没有方法出口的方法会造成StackOverflowError,或者说如果调用的过深都会抛出,这种错误也比较容易定位。
- java.lang.OutOfMemoryError: Java heap space:溢出原因:深入理解Java虚拟机书中讲到java堆溢出,Java堆用于存储对象实例,只要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
- java.lang.OutOfMemoryError:GC overhead limit exceeded:GC回收时间长时会抛出OutOfMemoryError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假设不抛出GC overhead limit错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使GC再次执行,这样就形成恶性循环,CPU使用率一直是100%,而GC缺没有任何成果。
- java.lang.OutOfMemoryError:Direct buffer memory:Direct memory可以通过参数-XX:MaxDirectMemorySize设定本机直接内存可用大小,如果不指定,则默认与java堆内存大小相同。过使用-XX:MaxDirectMemorySize=5M,限制最大可使用的本机直接内存大小为5MB,如果超过将抛出异常。
java.lang.OutOfMemoryError:unable to create new native thread。那出现的原因呢?
- 创建太多的线程了
- 服务器的设置限制了你创建线程的数量了
- java.langOutOfMemoryError:Metaspace:我们知道Java8及以后已经使用Metaspace来替代永久代,Metaspace并不在虚拟机内存中而是使用本地内存。主要还是是加载到内存中的 class 数量太多或者体积太大。这时可以通过JVM参数-XX:MaxMetaspaceSize指定。
Java代码导致OutOfMemoryError错误的解决
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
- 在执行某个class文件时候,可以使用java -Xmx256M aa.class来设置运行aa.class时jvm所允许占用的最大内存为256M。
- 对tomcat容器,可以在启动时对jvm设置内存限度。对tomcat,可以在catalina.bat中添加
- 对resin容器,同样可以在启动时对JVM设置内存限度。在bin文件夹下创建一个startup.bat文件,
- 优化程序,释放垃圾。主要包括避免死循环,应该及时释放种资源。
异常处理的工具
- java的自带工具:使用jdk自带的jconsole、jvisualvm插件,监控远程linux服务器中tomcat的jvm情况
- 使用Alibaba的Arhtas的异常诊断工具。
八、java反射原理与序列化问题
8.1 反射的原理
java反射就是的在运行的状态中的动态的获取类的对象,方法和变量的的方式。操作的就是这个对象的.class文件,加载相应类的字节码(.class文件的字节码会加载到内存中),随后利用反射解析出字节码中的构造函数、方法以及变量。
我们知道,要使用一个类,就要先把它加载到虚拟机中,生成一个Class对象。这个class对象就保存了这个类的一切信息。反射机制的实现,就是获取这个Class对象,通过class对象去访问类、对象的元数据以及运行时的数据。有三种方法获得类的Class对象:
- Class.forName(String className)、
- className.class、
- 实例对象.getClass();
反射机制的应用场景
- 动态代理设计模式采用了反射机制,
- Spring/Hibernate 等框架也大量使用到了反射机制。
- 使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动
- Spring 通过 XML 配置模式装载 Bean 的过程:
将程序内所有 XML 或 Properties 配置文件加载入内存中; Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;使用反射机制,根据这个字符串获得某个类的Class实例;动态配置实例的属性。
8.2 计算机的序列化和返序列化问题
什么是序列化:
它是处理对象流的一种机制,即可以很方便的保存内存中java对象的状态,同时也为了方便传输。
序列化的作用:常用于不同进程之间的对象传输,方便传输,速度快,还很安全,被调用方序列化,调用方反序列化即可拿到传输前最原始的java对象
- 方便存储,不管是存储成文件还是数据库,都行,存储为文件,下回要用可以直接反序列拿到对象。
为什么要序列化:
在不同的虚拟机中的,我们常常设计到A中的程序调用B中的程序,这个时候会出现问题的是怎么样A中没有B的对象。需要通过网络传输才能得到B的对象,或者内存数据。
怎么来解决问题:将B中的序列化的数据经过序列化处理,传输到A中的,再利用但序列化的操作,生成一个B中的对象。并可以实现在A中的程序中的调用B中的方法。
java的序列化实现方式:
有java中自带的serializable的接口。利用的protocol buffer的序列化分方式。第三是采用thirft的序列化的方式。
JDK的序列化的方式:
步骤一:创建一个对象输出流,它可以包装一个其它类型的目标输出流,如文件输出流:
Object OutputStreamoos=new Object OutputStream(newFileOutputStream(“D:\object.out”));
步骤二:通过对象输出流的writeObject()方法写对象:
oos.writeObject(new User(“xuliugen”, “123456”, “male”));
JDK反序列化的方式:
步骤一:创建一个对象输入流,它可以包装一个其它类型输入流,如文件输入流:
Object InputStream ois=new Object InputStream(new FileInputStream(“object.out”));
步骤二:通过对象输出流的readObject()方法读取对象:
User user = (User) ois.readObject();
说明:为了正确读取数据,完成反序列化,必须保证向对象输出流写对象的顺序与从对象输入流中读对象的顺序一致。将内存中的数据的利用序列化的数据的用于存储和用于传输的一种方式。
九、java Web问题
9.1 Cookie和Session、SessionID工作原理
session:是浏览器和服务器会话过程中,服务器分配的一块储存空间。服务器默认为浏览器在cookie中设置sessionid,浏览器在向服务器请求过程中传输 cookie 包含 sessionid ,服务器根据sessionid 获取出会话中存储的信息。
Cookie的定义:指某些网站为了辨别用户身份、进行session跟踪而存储在用户本地终端上的数据(通常经过加密)。也就是说如果知道一个用户的Cookie,并且在Cookie有效的时间内,就可以利用Cookie以这个用户的身份登录这个网站。
session的工作原理是客户端登录完成之后,服务器会创建对应的 session,session 创建完之后,会把 session的id发送给客户端,客户端再存储到浏览器中。这样客户端每次访问服务器时,都会带sessionid,服务器拿到 sessionid 之后,在内存找到与之对应的 session 这样就可以正常工作了。session 只是依赖cookie存储 sessionid,如果cookie 被禁用了,可以使用 url 中添加 sessionid 的方式保证 session 能正常使用。
为什么有session?
http协议是无状态的,即你连续访问某个网页100次和访问1次对服务器来说是没有区别对待的,因为它记不住你。主要是解决两个问题,一个是钥匙的问题,一个是存储用户信息的问题。
每次请求什么都会自动带到服务器呢
如果你比较了解http协议,用户的的每一次请求都会携带cookie,如果你想为用户建立一次会话,可以在用户授权成功时给他一个cookie,叫做会话id,它当然是唯一的,比如PHP就会为建立会话的用户默认set一个名为phpsessid,值看起来为一个随机字符串的cookie,如果下次发现用户带了这个cookie,服务器就知道。
Cookie和Session、SessionID的关系?
当客户端第一次请求 session 对象时候,服务器会为客户端创建一个 session,并将通过特殊算法算出一个 session 的 ID,用来标识该 session 对象。 sessionid 是一个会话的 key,浏览器第一次访问服务器会在服务器端生成一个session,有一个 sessionID 和它对应,并返回给浏览器,这个 sessionID 会被保存在浏览器的会话 cookie 中。tomcat 生成的 sessionID 叫做 jsessionID。
session在下列情况下被删除:
- 程序调用HttpSession.invalidate()
- 距离上一次收到客户端发送的session id时间间隔超过了session的最大有效时间
- 服务器进程被停止
注意:
- 客户端只保存 sessionID 到 cookie 中,而不会保存 session。
- 关闭浏览器只会使存储在客户端浏览器内存中的session cookie失效,不会使服务器端的session对象失效,同样也不会使已经保存到硬盘上的持久化cookie消失。
如何实现session的共享?
为什么要实现共享,如果你的网站是存放在一个机器上,那么是不存在这个问题的,因为会话数据就在这台机器,但是如果你使用了负载均衡把请求分发到不同的机器呢?这个时候会话id在客户端是没有问题的,但是如果用户的两次请求到了两台不同的机器,而它的session数据可能存在其中一台机器,这个时候就会出现取不到session数据的情况,于是session的共享就成了一个问题。Session存放在服务器端的内存中。不过session可以通过特殊的方式做持久化管理(memcache,redis)。
- 服务器实现的session复制或session共享,这类型的共享session是和服务器紧密相关的,比如webSphere或JBOSS在搭建集群时候可以配置实现session复制或session共享,但是这种方式有一个致命的缺点,就是不好扩展和移植,比如我们更换服务器,那么就要修改服务器配置。
- 利用成熟的技术做session复制,比如12306使用的gemfire,比如常见的内存数据库如Redis或memorycache,这类方案虽然比较普适,但是严重依赖于第三方,这样当第三方服务器出现问题的时候,那么将是应用的灾难。
- 将session维护在客户端,很容易想到就是利用cookie,但是客户端存在风险,数据不安全,而且可以存放的数据量比较小,所以将session维护在客户端还要对session中的信息加密。
- 采用的是的基于的IP_hash算法:就是将每一个的请求的经过算法是的每一个的请求都是在一个服务其中。不存在的转发到在其他的地方。
- 利用的tomcat的广播机制来实现的session的共享原理是及时将sessionde复制的原理。需要修改的tomcat的配置文件:
1 修改server.xml的cluster的节点
2 应用的在的web.xml的增加的节点 distrbutable
- 采用的缓存实现共享。
Session在何时创建呢?
当然还是在服务器端程序运行的过程中创建的,不同语言实现的应用程序有不同创建Session的方法,而在Java中是通过调用HttpServletRequest的getSession方法(使用true作为参数)创建的。在创建了Session的同时,服务器会为该Session生成唯一的Session id,而这个Session id在随后的请求中会被用来重新获得已经创建的Session;在Session被创建之后,就可以调用Session相关的方法往Session中增加内容了,而这些内容只会保存在服务器中,发到客户端的只有Session id;当客户端再次发送请求的时候,会将这个Session id带上,服务器接受到请求之后就会依据Session id找到相应的Session,从而再次使用之。
创建:sessionid第一次产生是在直到某server端程序调用 HttpServletRequest.getSession(true)这样的语句时才被创建。
session 的 id 是从哪里来的,sessionID 是如何使用的?
当客户端第一次请求 session 对象时候,服务器会为客户端创建一个 session,并将通过特殊算法算出一个 session 的 ID,用来标识该 session 对象。
客户端只保存 sessionID 到 cookie 中,而不会保存 session。session 不会因为浏览器的关闭而删除,只能通过程序调用 HttpSession.invalidate() 或超时才能销毁。
Session的什么时候删除?
- 超时;程序调用HttpSession.invalidate();
- 程序关闭。
当客户端第一次请求session对象时候,服务器会为客户端创建一个session,并将通过特殊算法算出一个session的ID,用来标识该session对象。 session会因为浏览器的关闭而删除吗?不会,session只会通过上面提到的方式去关闭。
如果客户端禁止cookie 能实现 session 还能用吗?
Cookie 与 Session,一般认为是两个独立的东西,Session采用的是在服务器端保持状态的方案,而Cookie采用的是在客户端保持状态的方案。
但为什么禁用Cookie就不能得到Session呢?
因为Session是用SessionID来确定当前对话所对应的服务器Session,而Session ID是通过Cookie来传递的,禁用Cookie相当于失去了Session ID,也就得不到Session了。
假定用户关闭Cookie的情况下使用Session,其实现途径有以下几种:
- 手动通过URL传值、隐藏表单传递Session ID。
- 用文件、数据库等形式保存Session ID,在跨页过程中手动调用
session 和 cookie 有什么区别?
Session | Cookie | |
---|---|---|
会话状态保存在服务器端的技术 | Cookie是一种在浏览器的技术。 | |
存储位置不同 | session 存储在服务器端 | cookie 存储在浏览器端 |
安全性不同 | 比较安全 | cookie 安全性一般,在浏览器存储,可以被伪造和修改 |
容量和个数限制 | 没有的限制 | cookie 有容量限制,每个站点下的 cookie 也有个数限制。 |
存储的多样性 | session 可以存储在 Redis 中、数据库中、应用程序中; | cookie只能存储在浏览器中。 |
GET和POST区别?
- Get参数会直接显示到URL不安全,Post在请求主体中,比较安全。
- Get传送数据量大小有限制,根据浏览器不同限制不同,一般为2KB-6KB。Post无限制。
- Get缓存数据会直接将参数缓,保留在浏览器历史记录中,不安全。Post缓存时不缓存参数,不保 留在浏览器记录中。
- Get只能有一个种编码方式,转码的时候使用字符串转字节的方式,Post支持多种编码方式,转码直接使用setCharacterEconding()即可。
- 由于Get具有缓存功能,一般用来直接获取数据,提高查询速度; Post是发送数据到服务器作存储,适合于增删改操作。
为什么大型网站都采用get方法,而非post方法?
- get是从服务器上获取数据,post是向服务器传送数据。get和post只是一种传递数据的方式,get也可以把数据传到服务器,他们的本质都是发送请求和接收结果。
- get安全性非常低,post安全性较高。如果没有加密,他们安全级别都是一样的,随便一个监听器都可以把所有的数据监听到。
- get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。但理论上,IIS4中最大量为80KB,IIS5中为100KB。post基本没有限制,我想大家都上传过文件,都是用post方式的。只不过要修改form里面的那个type参数。
9.2 过滤器与拦截器原理
|
| Filter | 拦截器 | 监听器 | | —- | —- | —- | —- | | 实现原理 | 基于函数回调,它可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。 | 基于Java的反射机制,属于面向切面编程(AOP)的一种运用,就是在Service或者一个方法前调用一个方法,或者在方法后调用一个方法,甚至在抛出异常的时候做业务逻辑的操作。 | 监听器主要用来监听只用。通过listener可以监听web服务器中某一个执行动作,并根据其要求作出相应的响应 | | 目的 | 是用来做一些过滤操作,获取我们想要的数据,比如:JavaWeb中对传入的request、response提前过滤掉一些信息,或者提前设置一些参数,然后再传入servlet或者Controller进行业务逻辑操作。 | 由于拦截器是基于web框架的调用,因此可以使用Spring的依赖注入(DI)进行一些业务操作,同时一个拦截器实例在一个Controller生命周期之内可以多次调用。 | | | 场景 | 修改字符编码(CharacterEncodingFilter)、过滤HttpServletRequest中敏感字符(XSSFilter自定义过滤器) | 但缺点是只能对Controller请求进行拦截,也可以拦截静态资源,必须要添加上配置才可以避免静态资源被拦截,拦截器不能拦截的只有jsp。
执行顺序:过滤前——-拦截前——-Action处理——-拦截后——-过滤后 | Servlet的监听器Listener,它是实现了javax.servlet.ServletContextListener 接口的服务器端程序,它也是随web应用的启动而启动,只初始化一次,随web应用的停止而销毁 | | 配置方式 | web.xml配置 | Spring mvc的文件中配置 | 在web.xmL中配置 |
9.3 java web的9大对象
request对象 | 客户端的请求信息被封装在request对象中,通过它才能了解到客户的需求,然后做出响应。它是HttpServletRequest类的实例。 |
---|---|
response对象 |
response对象包含了响应客户请求的有关信息,但在JSP中很少直接用到它。它是HttpServletResponse类的实例。 |
session对象 | session对象指的是客户端与服务器的一次会话,从客户连到服务器的一个WebApplication开始,直到客户端与服务器断开连接为止。是HttpSession类的实例. |
out对象 | out对象是JspWriter类的实例,是向客户端输出内容常用的对象 |
page对象 | page对象就是指向当前JSP页面本身,有点象类中的this指针,它是java.lang.Object类的实例 |
application对象 | application对象实现了用户间数据的共享,可存放全局变量。它开始于服务器的启动,直到服务器的关闭,在此期间,此对象将一直存在;这样在用户的前后连接或不同用户之间的连接中,可以对此对象的同一属性进行操作;在任何地方对此对象属性的操作,都将影响到其他用户对此的访问。服务器的启动和关闭决定了application对象的生命。它是ServletContext类的实例。 |
exception对象 | exception对象是一个例外对象,当一个页面在运行过程中发生了例外,就产生这个对象。如果一个JSP页面要应用此对象,就必须把isErrorPage设为true,否则无法编译。他实际上是java.lang.Throwable的对象 |
pageContext对象 |
pageContext对象提供了对JSP页面内所有的对象及名字空间的访问,也就是说他可以访问到本页所在的SESSION,也可以取本页面所在的application的某一属性值,他相当于页面中所有功能的集大成者,它的本 类名也叫pageContext。 |
config对象 | config对象是在一个Servlet初始化时,JSP引擎向它传递信息用的,此信息包括Servlet初始化时所要用到的参数(通过属性名和属性值构成)以及服务器的有关信息(通过传递一个ServletContext对象) |
如何防止表单重复提交?
网络延迟时重复点击提交按钮,会发生重复提交表单的问题。
解决办法:
- 数据库主键唯一。
- 提交成功后页面重定向。
- 按钮提交后隐藏或不可再点击。
- 后台生成页面 token,页面表单提交携带 token,后台进行校验。
如何避免sql注入?
- 严格限制 Web 应用的数据库的操作权限,给连接数据库的用户提供满足需要的最低权限,最大限度的减少注入攻击对数据库的危害
- 校验参数的数据格式是否合法(可以使用正则或特殊字符的判断)
- 对进入数据库的特殊字符进行转义处理,或编码转换
- 预编译SQL(Java 中使用 PreparedStatement),参数化查询方式,避免 SQL 拼接
- 发布前,利用工具进行SQL注入检测
- 报错信息不要包含 SQL 信息输出到 Web 页面Spring。
转发和重定向的区别和使用
转发的特点:
- 址栏不发生变化,显示的是上一个页面的地址
- 求次数:只有1次请求
- 目录:http://localhost:8080/项目地址/,包含了项目的访问地址
求域中数据不会丢失
转发的函数:request.getRequestDispatcher("/地址").forward(request, response);
重定向的特点址栏:显示新的地址
- 求次数:2次
- 目录:http://localhost:8080/ 没有项目的名字
求域中的数据会丢失,因为是2次请求
response.sendRedirect("two");
什么时候使用转发,什么时候使用重定向?
要保留请求域中的数据,使用转发,否则使用重定向。以后访问数据库,增删改使用重定向,查询使用转发。十、java IO问题
10.1 I/O的常用函数
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。十一、JDK的新特性问题
11.1 JDk1.8的新特性
Lambda表达式
- 函数式接口
- 方法引用和构造器调用
- 接口中的默认方法和静态方法 :
- 新时间日期API:LocalDate 、 LocalTime 、 LocalDateTime
JDK1.8中什么是泛型?
在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,泛型的产生是为了提高代码的安全性和代码的代码的可读性。