(一)基础
在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个应用程序的执行过程中,执行hashCode方法所返回的值可以不一致。
如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果
如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
如果重写了equals方法而没有重写hashCode方法的话,就违反了第二条规定。相等的对象必须拥有相等的hashcode。
final关键字可以用于三个地方。用于修饰类、类属性和类方法
被final关键字修饰的类不能被继承,被final关键字修饰的类属性和类方法不能被重写
对于被final关键字修饰的类属性而言,子类不能给他重新赋值
扩展:修饰符
final:
当final修饰类时,表明该类不能被其他类所继承。当我们需要一个类永远不能被继承时,此时就可以使用final修饰,但要注意:
final类中所有的成员方法都会隐式的定义为final方法
final修饰方法时,表明此方法不能被重写
注意:若父类中final方法的访问权限为private,将导致子类中不能直接继承该方法,因此,此时可以在子类中定义相同方法名的函数,此时不会与重写final的矛盾,而是在子类中重新地定义了新方法。
final修饰成员变量,表示常量,只能被赋值一次,赋值后其值不再改变
**final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
finally:
finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。
finalize:
finalize是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
基础数据类型包括:byte、short、int、long、float、double、boolean、char
String属于类,因此String不属于基础的数据类型
String str = “i”; java虚拟机会将其分配到常量池中,而常量池中没有重复的元素
String str = new String(“i”); java虚拟机会将其分配到堆内存中
因此不一样(参考JVM)
自己实现:获取字符串长度,定义char数组,反转赋值,创建新的String
使用StringBuilder的reverse()方法:return new StringBuilder(str).reverse().toString();
不能,抽象类必须被继承才可使用,而使用final修饰的类不可修改、不可继承
实现Cloneable接口,重写clone()方法,不实现Cloneable接口,会报CloneNotSupportedException异常
Object的clone()方法是 浅拷贝 , 即如果类中属性有自定义引用类型,只拷贝引用,不拷贝引用指向的对象 。
深拷贝实现方法:
-RuntimeException类型的异常,在程序中是不会捕获的,也不会在方法体中声明抛出的异常。它是表示程序中出现了编程错误,需要找出错误修改程序。
-其他异常都会在正确的运行中,会去捕获。
异常是能被程序本身处理的,而错误是无法处理的。
-抛出异常:当一个方法出现错误引发异常时,方法会创建异常对象并交给运行时的系统,异常对象包含了异常类型和异常出现时程序状态等信息。
-捕获异常:当一个方法抛出异常之后,运行时系统会尝试去寻找合适的处理异常处理器。try{}catch{}通常使用catch去捕获异常。
1.数组索引越界异常 2.算术异常 3.非法参数异常 4. 空指针异常 5.数组长度为负异常
1.throws出现在方法头上,而throw出现在代码块中,try{}catch{}
2.Throws表示出现异常的一种可能性,可能发生,可能不发生;而一旦执行throw的话,表示异常一定会发生。
Collection是集合的接口
Collections是集合的工具类,定义了许多操作集合的静态方法(实现对各种集合的搜索、排序、线程安全等),不能被实例化
查询使用HashMap、增加、快速创建的时候使用TreeMap
原因:HashMap的Key值实现散列hashCode(),分布是散列的均匀的,不支持排序;TreeMap迭代时默认按照Key值升序排列
HashMap是基于哈希表的Map接口实现。HashMap使用数组加链表实现,每个数组中存储着链表。通过put()和get()方法存储和获取对象。
使用数组的好处,寻址容易,缺点在于插入和删除困难。使用链表的好处插入和删除容易,缺点在于寻址困难。因此将数组和链表组合可以提高查询效率又便于频繁修改。
哈希表的思想是通过哈希算法,将不定长度的Key映射为数组下标,访问Key的数据时再次通过哈希算法计算出数组下标,访问下标对应的数据项。
由于在实现哈希函数时存在着哈希冲突的问题,就得处理哈希冲突。常见的算法有:开放寻址法、公共溢出区、拉链法(链地址法)
HashMap使用的是拉链法,如果分桶方式合理,数据能够被均匀分布到所有桶中,在数据量不是特别巨大的前提下,查询效率接近于O(1),插入或删除效率接近于O(1)
HashMap在链表中存储的是键值对,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
当HashMap需要进行扩容时,会重新进行哈希和分桶,因此不能保证插入顺序不变。由于需要重建数组,开销巨大,当知道数据量时可以设定HashMap的容量。
另外一个重要参数是装载因子(默认0.75),当存储数据量/数组容量>装载因子时需要扩容。
装载因子越大,则空间利用率提升,但发生冲突的概率也就变大,查找的成本变高;装载因子越小,则发生冲突的概率变小了,但空间浪费了。
HashMap的三个构造函数
HashMap不是线程安全的,在并发过程中容易出现死循环,主要是因为链表的插入操作不是原子性的,每一个HashEntry都是一个链表,假设我们要插入一个数据,可能已经设置了后节点,但是实际上并没有插入,反而插入到了后节点后面的位置,这样就出现了环,破坏了链表结构,所以他不是线程安全的。
–首先可以考虑HashTable,因为它是使用Synchronized关键字来保证线程安全的,但是在线程竞争比较激烈的情况下HashTable的处理速度就变的很缓慢,这主要是因为Synchronized关键字使代码块或者同步方法进入临界区,如果一个线程想进入临界区,而其他线程也想,但同一时刻进入临界区的线程只有一个,其他线程必须在外面等待进入阻塞状态,锁只有一个,但是想获取锁的线程却在增加,这样就导致HashTable在处理多线程的时候效率低下。
–所以我们会选用CocurrentHashMap,它是利用锁分段技术,将数据分成很多段,每一段都独立拥有一把锁,从而想争取锁的线程数目得到了控制,并且每一段之间竞争锁是不影响的。与HashTable主要不同的是CocurrentHashMap的get方法是不需要用到锁的,共享变量是由volatile关键字修饰了,这就保证了共享变量在内存的可见性,使得每一次获取的都是最新值,同时还避免了指令重排序。
在JDK1.7的时候,CocurrentHashMap的底层是由Segment和数组实现的。其中Segment是继承于Reentranlock的,和HashTable非常相似,但是不会像HahTable不管是put方法还是get方法都使用锁机制保证线程安全,不同的地方就是核心数据value和链表是由volatile修饰的,保证了内存可见性。而且每一个线程占用锁访问Segment,是相互独立的。
–put()方法 首先通过要根据插入的key的hash值定位到Segment,再通过Hash定位到具体的Hashenrty上,如果HashEntry不为空,就比较要插入的key与已经存在的key是否相等,如果相等,就替换,如果不等,就以链表的结构插入。如果HashEntry为空,就新建一个HashEntry到Segment中,但前提要需要判断扩容。
–get() 先通过key的hash值定位到具体的Segment上,再通过hash值定位到具体的HashEntry上,如果没有就返回空,如果有,就直接返回。由于Hashentry中的value是由volatile修饰的,保证了内存的可见性,每一次获取都是最新值。
在jdk1.8的时候,CocurrentHashMap将HashEntry数组替换成Node[]数组,其中val,next都是由volatile修饰的,保证了可见性。
–put()方法 首先根据key计算出hash值,定位到具体的node上,判断Node是否为空,如果为空,表示当前可以写入数据,利用CAS尝试写入,如果失败,能够自旋成功。如果不为空,就先当前位置hashcodemoved-1,就需要扩容。如果都不满足,就使用Synchronized写入数据,如果达到一定阈值时,就改用红黑树进行存储,这样的话,查找的效率会高很多。
–get() 首先计算key的hash值,根据hash值来寻址,如果正好在table上,就直接返回。如果是红黑树就按数的查找方式寻找,如果是链表结构,就按链表的方式查找。
jdk1.8在1.7的版本上作出了很大的改动,将链表在一定的条件下改用红黑树进行查找,保证了查询效率,而且还将Reentranlock替换成了Synchronized。
基于HashMap实现,只使用Key,value使用一个static final的Object对象标识。
Arraylist:底层是基于动态数组,根据下标随机访问数组元素的效率高,向数组尾部添加元素的效率高;但是,删除数组中的数据以及向数组中间添加数据效率低,因为需要移动数组。
Linkedlist:基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。
同步性:Vector是线程安全的,方法之间是线程同步的;ArrayList是线程不安全的,方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好使用ArrayList。
数据增长:Vector增长为原来的两倍,ArrayList增长为原来的1.5倍。Vector可设置初始的空间大小。
Array可以包含基本数据类型和对象类型,ArrayList只能包含对象类型。
Array大小固定,ArrayList大小动态变化。
ArrayList提供了更多的方法和特性。
offer()和add()都是向队列添加一个元素,当队列满时,offer()返回false,add()会抛出一个 unchecked 异常
poll()和remove()都将移除并返回队头,但在队空时,poll()返回null,remove()会抛出NoSuchElementException异常
peek()和element()都将在不移除的情况下返回队头,但在队空时,peek()返回null,element()抛出NoSuchElementException异常
Vector、HashTable、ConcurrentHashMap、Stack
迭代器是一种设计模式,它是一个对象,可以遍历并选择序列中的对象。Java中的Iterator功能比较简单,只能单向移动。
并行是指两个或者多个事件在同一时刻发生,并发是指两个或者多个事件在同一时间间隔发生。
进程是资源分配的最小单位,线程是CPU调度的最小单位。由于线程的上下文切换,需要操作系统内核进行,十分消耗系统资源,因此在线程中可以拥有多个协程,多个协程的切换完全是在用户态下执行的。
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
java提供了三种创建线程的方法:
通过实现Runnable接口;
public void run()
Thread(Runnable threadOb, String threadName)
通过继承Thread类本身;
通过Callable和Future创建线程。
4.线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池找那个等待下一个任务。
start()
执行后,处于 就绪 状态,等待 获得CPU资源 ,而后进行 运行 状态yield()
方法 后进入 就绪 状态stop()
方法 ,进入 死亡 状态。sleep()方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等待休眠时间结束后,线程进入就绪状态。
当一个synchronized块中调用了sleep()方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程已然无法访问这个对象。
wait()方法是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notiyAll方法来唤醒等待的线程。wait()方法必须在同步代码块或同步方法中调用。
锁池:假设线程A已经拥有了某个对象的锁,而其他的线程想要调用这个对象的某个synchronized方法或synchronized块,由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中
如果有对象调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或者notify()方法(只随机唤醒一个wait线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假设某线程没有竞争到该对象锁,它还会留在锁池中,只有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
使用工厂类Executors创建线程池
使用new ThreadPoolExecutor自定义创建
在ThreadPoolExecutor类中提供了四个构造方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
}
各个参数的含义如下:
corePoolSize:核心池的大小。默认情况下,在创建了线程池之后,线程池中线程数量为0,当有任务到来之后,就会创建一个线程去执行任务,当线程池中的线程数量达到corePoolSize之后,就会把到达的任务放到缓存队列中。
maximumPoolSize:线程池最大线程数。表示线程池最多能创建多少个线程。
keepAliveTime:表示线程没有执行任务时保持多久时间会终止。
unit:参数keepAliveTime的时间单位。
workQueue:阻塞队列,用来存储等待执行的任务。
threadFactory:线程工厂
handler:表示当拒绝处理任务时的策略,有以下四种取值:
ThreadPoolExecutor继承了AbstractExecutorService实现了ExecutorService继承了Executor
ThreadPoolExecutor类中几个比较重要的方法
execute()
submit()
shutdown()
shutdownNow()
RUNNING -> SHUTDOWN: 当调用shutdown()时
(RUNNING or SHUTDOWN) -> STOP: 当调用shutdownNow()时
SHUTDOWN -> TIDYING: 当队列和线程池都为空时
STOP -> TIDYING: 当线程池为空时
TIDYING -> TERMINATED: 当terminated()方法完成时
任务的执行
线程池中线程的初始化
默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
任务缓存队列及排队策略
workQueue:一个任务缓存队列,用来存储等待执行的任务
常用的缓存队列:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
任务拒绝策略
当线程池的任务缓存队列已经满并别线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
handler:表示拒绝处理任务时的策略,有四种取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 这是线程池默认的拒绝策略,在任务不能再提交的时候,跑出异常,及时反馈程序运行状态。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。使用此策略,可能会使我们无法发现系统的异常状态。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),此拒绝策略,是一种喜新厌旧的拒绝策略。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 ,如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务
线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
线程池容量的动态调整
ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),
当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。
线程数=cpu可用核心数/(1-阻塞系数),其中阻塞系数的取值在[0,1]之间。计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。
1、工作线程是不是越多越好?
不是。
a、服务器cpu核数有限,所以同时并发或者并行的线程数是有限的,所以1核cpu设置1000个线程是没有意义的。
b、线程切换也是有开销的。频繁切换线程会使性能降低。
如果是计算密集型,换句话说,线程绝大部分消耗都在CPU计算处理上,那么启动再多的线程也无济于事,所以线程数量=cpu合数比较合适。
如果是IO密集型,线程很到部分消耗在等待上,所以可以启动更多的线程,但是线程数量不能超过线程池的上限,
newFixedThreadPool:
public class FixPoolDemo {
private static Runnable getThread(final int i){
return new Runnable() {
@Override
public void run() {
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(i);
}
};
}
public static void main(String[] args) {
ExecutorService fixPool = Executors.newFixedThreadPool(5);
for(int i=0;i<10;i++){
fixPool.execute(getThread(i));
}
fixPool.shutdown();
}
}
newCachedThreadPool:
public class CachePoolDemo {
private static Runnable getThread(final int i){
return new Runnable(){
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
};
}
public static void main(String[] args) {
ExecutorService cachePool = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
cachePool.execute(getThread(i));
}
}
}
newSingleThreadExecutor:
public class SinglePoolDemo {
private static Runnable getThread(final int i){
return new Runnable(){
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
};
}
public static void main(String[] args) {
ExecutorService singlePool = Executors.newSingleThreadExecutor();
for(int i=0;i<10;i++){
singlePool.execute(getThread(i));
}
singlePool.shutdown();
}
}
newScheduledThreadPool:
public class ScheduledPoolDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(10);
scheduledPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId() + "执行了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 0, 2, TimeUnit.SECONDS);
}
}
使用new ThreadPoolExecutor自定义创建
public class ThreadPoolDemo {
private static Runnable getThread(final int i){
return new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
};
}
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 100, 200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(5));
for(int i=0;i<10;i++){
threadPool.execute(getThread(i));
}
threadPool.shutdown();
}
}
线程安全性问题体现在:
导致原因:
解决方法:
synchronized、volatile、LOCK,可以解决可见性问题
synchronized
作用:
用法:
同步代码块是通过monitorenter和monitorexit指令获取线程的执行权
同步方法通过加ACC_SYNCHRONIZED标识实现线程的执行权的控制
lock他是Reentranlock类的,
Reentranlock则需要用户手动释放锁,就有可能出现死锁现象;需要lock()和unlock()方法配合try/finally语句块来完成
volatile可以保证内存可见性,被volatile关键字修饰的变量在进行写操作转换成汇编语言时,会加上一个lock前缀指令,lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
如何使其他数据无效,主要是通过MESI协议,当cpu写数据时,如果发现操作的变量是共享变量,那么他会发出信号通知其他CPU将该变量的缓存设置为无效状态。当其他cpu使用这个变量时,首先会先去判断是否有对该变量更改的信号,当发现这个变量的缓存已经无效时,会重新从内存中读取变量。
Happens-Before规则如下:
锁的级别从低到高:无锁->偏向锁->轻量级锁->重量级锁
锁分级别原因:
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。
自旋锁
是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。
自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。
所以当线程竞争不激烈,并且保持锁的时间段,适合使用自旋锁。
自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。
可能引起的问题:
1.过多占据CPU时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据cpu时间片,导致CPU资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片阻塞;
2.死锁问题:试想一下,有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。
让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify()
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁
显示锁用Lock来定义、内置锁用syschronized。
内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
内置锁是互斥锁。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。
java死锁产生的四个必要条件:
public class Main{
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
Lock1 lock1 = new Lock1();
Lock2 lock2 = new Lock2();
Thread t1 = new Thread(lock1);
Thread t2 = new Thread(lock2);
t1.start();
t2.start();
}
}
class Lock1 implements Runnable{
@Override
public void run() {
synchronized (Main.obj1){
System.out.println("Lock1 lock obj1");
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (Main.obj2){
System.out.println("Lock1 lock obj2");
}
}
}
}
class Lock2 implements Runnable{
@Override
public void run() {
synchronized (Main.obj2){
System.out.println("Lock2 lock obj2");
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (Main.obj1){
System.out.println("Lock2 lock obj1");
}
}
}
}
A:死锁预防
B:解决方法
死锁预防
1.破坏“互斥”条件:就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。
2.破坏“占有并等待”条件:破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。
方法一:创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。这是所谓的 “ 一次性分配”方案。
方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。
3.破坏“不可抢占”条件:破坏“不可抢占”条件就是允许对资源实行抢夺。
方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。
4.破坏“循环等待”条件:破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。
死锁避免
1、判断“系统安全状态”法
在进行系统资源分配之前,先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待。
2、银行家算法
1、申请的贷款额度不能超过银行现有的资金总额
2、分批次向银行提款,但是贷款额度不能超过一开始最大需求量的总额
3、暂时不能满足客户申请的资金额度时,在有限时间内给予贷款
4、客户要在规定的时间内还款
死锁检测和解除:先检测是否发生死锁,再采取适当措施将死锁清除,如系统重启、撤销进程剥夺资源、进程回退策略
ThreadLocal是线程本地存储,在每个线程中都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMap对象内的value。
经典的使用场景是为每个线程分配一个JDBC连接Connection。这样就可以保证每个线程都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection;还有Session管理等问题。
ThreadLocal仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在Thread里面。ThreadLoacalMap属于Thread。
在线程池中线程的存活时间太长,往往都是和程序同生共死的,这样Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用,所以只要ThreadLocal结束了自己的生命周期是可以被回收掉的。Entry中的Value是被Entry强引用的,即便value的生命周期结束了,value也是无法被回收的,导致内存泄漏。
线程池中正确使用ThreadLocal的方法为:在finally代码块中手动清理ThreadLocal中的value,调用ThreadLocal的remove()方法
作用:
用法:
原理:
volatile可以保证内存可见性,被volatile关键字修饰的变量在进行写操作转换成汇编语言时,会加上一个lock前缀指令,lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
如何使其他数据无效,主要是通过MESI协议,当cpu写数据时,如果发现操作的变量是共享变量,那么他会发出信号通知其他CPU将该变量的缓存设置为无效状态。当其他cpu使用这个变量时,首先会先去判断是否有对该变量更改的信号,当发现这个变量的缓存已经无效时,会重新从内存中读取变量。
1.状态量标记,这种对变量的读写操作,标记为volatile可以保证修改对线程立刻可见,比synchronized,lock有一定的效率提升。
2.单例模式的实现,典型的双重检查锁定dcl,懒汉式的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排,非instance加上了volatile
首先是不能保证原子性的,只是对单个volatile变量的读写具有原子性,但对类似于i++就不能保证原子性
假设线程A读取了i的值为10,这时候进入了阻塞状态,因为还没有对变量进行修改,触发不了volatile规则,线程B此时也读取I,主内存i的值依旧为10,做自增,然后立刻写回主存了,为11,
此时又轮到线程A执行,由于工作内存保存的是10,所以继续做自增,再写回内存,11又被写了一遍,所以虽然这两个线程执行了两次add。结果却只加了一次
这是因为线程A的读取操作已经做过了,只有在读取操作的时候,发现自己的缓存无效,才会去读主存的值,所以这里线程A只能继续做自增
原始构成:
使用方法:
等待是否可中断:
加锁是否公平
锁绑定多个条件Condition
Atomic包中的类的基本特性就是在多线程环境下,当有多个线程同时对单个变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁
但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它自己拿到锁。
传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
主要是指程序可以访问、检测和修改它本身状态或行为的一种能力
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
当我们new student时,jvm会加载我们的student.class,jvm会去本地磁盘找student.class文件,并加载到jvm内存中,将.class文件读入内存,同时产生相对应的class对象,一个类只产生一个class对象。class对象中包含着student类的一些信息。反射的本质理解:就是得到class对象后,反向获取studengt对象的各种信息。
class类的实例表示正在运行的java应用程序中的类和接口,没有公共构造方法,class对象是在加载类时由java虚拟机以及调用类加载器中的defineClass方法自动构造的。
获取class对象的三种方式
1.getClass()
2.任何数据类型都有一个静态的class属性
3.通过class类的静态方法:forName
调用方法
1.获取构造方法:
1).批量的方法:
public Constructor[] getConstructors():所有"公有的"构造方法
public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)
2).获取单个的方法,并调用:
public Constructor getConstructor(Class… parameterTypes):获取单个的"公有的"构造方法:
public Constructor getDeclaredConstructor(Class… parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;
调用构造方法:
Constructor–>newInstance(Object… initargs)
2、 newInstance是 Constructor类的方法(管理构造函数的类)
api的解释为:
``newInstance(Object… initargs)
使用此 Constructor
对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。
它的返回值是T类型,所以newInstance是创建了一个构造方法的声明类的新实例对象。并为之调用
获取成员变量及调用
1.批量的*
1.Field[] getFields():获取所有的"公有字段"*
2).Field[] getDeclaredFields():获取所有字段,包括:私有、受保护、默认、公有;*
2.获取单个的:*
** 1).public Field getField(String fieldName):获取某个"公有的"字段;*
** 2).public Field getDeclaredField(String fieldName):获取某个字段(可以是私有的)*
设置字段的值:*
Field --> public void set(Object obj,Object value)
参数说明:*
1.obj:要设置的字段所在的对象;*
2.value:要为字段设置的值;
获取成员方法并调用:
1.批量的:*
public Method[] getMethods():获取所有"公有方法";(包含了父类的方法也包含Object类)*
public Method[] getDeclaredMethods():获取所有的成员方法,包括私有的(不包括继承的)*
2.获取单个的:*
public Method getMethod(String name,Class… parameterTypes)
参数:*
name : 方法名;*
Class … : 形参的Class类型对象
public Method getDeclaredMethod(String name,Class… parameterTypes)*
调用方法:*
Method --> public Object invoke(Object obj,Object… args)
参数说明:*
obj : 要调用方法的对象;*
args:调用方式时所传递的实参;*
通过反射越过泛型检查,泛型用在编译期,编译过后泛型擦除(消失掉)。所以是可以通过反射越过泛型检查的。
Java反射机制主要提供了以下功能:
-补充代码
首先需要定义一个被代理的接口,还需要一个实现类去实现InvocationHandler调用处理程序接口,通过Proxy提供创建动态代理类和实例静态方法,将实现类的类加载器、被代理的接口、以及实现类本身作为参数传入到Proxy中的newProxyInstance,用来创建生成代理类,需要在invoke()方法中处理代理实例,并返回结果,动态代理的本质,就是使用反射机制实现。
//等我们会用这个类,自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {
//被代理的接口
private Rent rent;
public void setRent(Rent rent) {
this.rent = rent;
}
//生成得到代理类
/**
* this.getClass().getClassLoader() 这个类所在的位置
* rent.getClass().getInterfaces() 被代理对象的接口
* this ProxyInvocationHandler implements InvocationHandler 处理方法
* @return
*/
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
rent.getClass().getInterfaces(),this);
}
//处理代理实例,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//动态代理的本质,就是使用反射机制实现
Object result = method.invoke(rent, args);
return result;
}
}
Servelt,Fliter,Listen
Servlet是用来处理客户端请求的动态资源,也就是当我们在浏览器中输入一个地址回车跳转后,请求就会被发送到对应的Servlet上进行处理。
Servlet的任务有:
1.接收请求数据:我们都知道客户端请求会被封装成HttpServletRequesr对象,里面包含了请求头、参数等各种信息。
2.处理请求:通常我们会在service,dopost,doget方法进行接收参数,并且调用业务层的方法来处理请求
3.完成响应“处理完请求后,我们一般会转发(forword)或者重定向(redirect)到某个页面,转发是HttpServletRequest中的方法,重定向是HttpServletResponse中的方法,两者是有很大区别的
Servlet的创建:Servlet可以在以第1次接收时被创建,也可以在在服务器启动时就被创建,这需要在web.xml的中添加一条配置信息,< load-on-startup>5< /load-on-startup>,当值为0或者大于0时,表示容器在应用启动时就加载这个servlet,当是一个负数时或者没有指定时,则指示容器在该servlet被请求时才加载。
servlet的生命周期方法
servlet的初始化,旨在servlet实例时候调用一次,Servlet是单例,整个服务器就只创建一个同类型Servlet
servlet的处理请求方法,在servlet被请求时,会被马上调用,每处理一次请求,就会被调用一次,
servlet销毁之前执行的方法,只执行一次,用于释放servlet占有的资源,通常servlet是没有什么可要释放的,所以该方法一般都是空
Fliter
filter与servlet在很多的方面极其相似,但是也有不同,例如filter和servlet一样都又三个生命周期方法,同时他们在web.xml中的配置文件也是差不多的、 但是servlet主要负责处理请求,而filter主要负责拦截请求,和放行。
filter四种拦截方式
1.REQUEST:直接访问目标资源时执行过滤器。包括:在地址栏中直接访问、表单提交、超链接、重定向,只要在地址栏中可以看到目标资源的路径,就是REQUEST
2.FORWOARD:转发访问执行过滤器。包括RequestDispatcher#forward()方法、< jsp:forward>标签都是转发访问;
3.INCLUDE:包含访问执行过滤器。包括RequestDispatcher#include()方法、< jsp:include>标签都是包含访问;
4.ERROR:当目标资源在web.xml中配置包括RequestDispatcher#include()方法、< jsp:include>标签都是包含访问;
url-mapping的写法
匹配规则有三种:
执行filter的顺序
如果有多个过滤器都匹配该请求,顺序决定于web.xml filter-mapping的顺序,在前面的先执行,后面的后执行
Listener
listener就是监听器,我们在JAVASE开发时,经常会给按钮加监听器,当点击这个按钮就会出发监听事件,调用onClick方法,本质是方法回掉。
在JavaWeb的Listener也是这么个原理,但是它监听的内容不同,它可以监听Application、Session、Request对象,当这些对象发生变化就会调用对应的监听方法。
应用域监听:
Ø ServletContext(监听Application)
¨ 生命周期监听:ServletContextListener,它有两个方法,一个在出生时调用,一个在死亡时调用;
¨ 属性监听:ServletContextAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。
HttpSession(监听Session)
¨ 生命周期监听:HttpSessionListener,它有两个方法,一个在出生时调用,一个在死亡时调用;
¨ 属性监听:HttpSessioniAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。
ServletRequest(监听Request)
¨ 生命周期监听:ServletRequestListener,它有两个方法,一个在出生时调用,一个在死亡时调用;
属性监听:ServletRequestAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。
感知Session监听:
1:HttpSessionBindingListener监听
⑴在需要监听的实体类实现HttpSessionBindingListener接口
⑵重写valueBound()方法,这方法是在当该实体类被放到Session中时,触发该方法
⑶重写valueUnbound()方法,这方法是在当该实体类从Session中被移除时,触发该方法
2:HttpSessionActivationListener监听
⑴在需要监听的实体类实现HttpSessionActivationListener接口
⑵重写sessionWillPassivate()方法,这方法是在当该实体类被序列化时,触发该方法
⑶重写sessionDidActivate()方法,这方法是在当该实体类被反序列化时,触发该方法
IO是面向流的、阻塞的
NIO是面向块的、非阻塞的,NIO有三个重要组成部分:通道(Channel)、缓冲区(Buffer)、选择器(Selector)
选择器可以注册到多个通道上,结合多线程NIO,可以处理大量IO操作。
BIO同步阻塞IO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成
NIO同步非阻塞IO:线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成。
AIO异步非阻塞IO:线程发起IO请求,立即返回;内存在做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败。
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
序列化步骤:
步骤一:创建一个ObjectOutputStream输出流;
步骤二:调用ObjectOutputStream对象的writeObject()输出可序列化对象。
public class writeObject {
public static void main(String[] args) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
Person person = new Person("张三", 24);
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
}
}
反序列化步骤:
步骤一:创建一个ObjectInputStream输入流;
步骤二:调用ObjectInputStream对象的readObject()得到序列化对象。
public class readObject {
public static void main(String[] args) {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Person person = (Person) ois.readObject();
System.out.println(person.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
反序列化并不会调用构造方法,反序列对象是由JVM自己生成的对象,不通过构造方法生成。
1.2 成员是引用的序列化
如果一个可序列化的类成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。
1.3 同一对象序列化多次的机制
Java序列化同一对象,并不会将此对象序列化多次得到多个对象。
1.4 Java序列化算法潜在的问题
由于Java序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内容可变)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。
1.5 可选的自定义序列化
使用transient关键字选择不需要序列化的字段。
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对引用类型,值是null;对基本类型,值是0;对boolean类型,值是false。
可以通过重写writeObject与readObject方法来实现自定义序列化。
可以通过重写writeReplace与readResolve方法来实现自定义序列化。
Java序列化算法
通过实现externalizable接口,必须实现writeExternal、readExternal方法。
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。
实现Serializable接口 | 实现Externalizable接口 |
---|---|
系统自动存储必要的信息 | 程序员决定存储哪些信息 |
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持 | 必须实现接口内的两个方法 |
性能略差 | 性能略好 |
java序列化提供了一个private static final long serialVersionUID
的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
如果只是修改了方法,反序列化不容影响,则无需修改版本号;
如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;
如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。
所有需要网络传输的对象都需要实现序列化接口,通常建议所有的javaBean都实现Serializable接口。
对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
如果想让某个变量不被序列化,使用transient修饰。
序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
反序列化时必须有序列化对象的class文件。
当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。
单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。
同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。
Java中一般认为有23种设计模式,总体来说设计默认分为三大类:
创建型模式,共五种:
结构型模式,共七种:
行为型模式,共十一种:
工厂模式:创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式,根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式。
简单工厂模式:该模式对对象的创建管理方式最为简单,因为其仅仅简单的对不同类的创建进行了一层薄薄的封装。该模式通过向工厂传递类型来指定要创建的对象。
工厂方法模式:和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式将生成具体产品的任务分发给具体的产品工厂。也就是定义一个抽象工厂,其定义了产品的生产接口,但不负责具体的产品,将生产任务交给不同的派生类工厂,这样就不用通过指定类型来创建对象了。
抽象工厂模式:在工厂方法模式中通过增加新产品接口来实现产品的增加。
单例模式:顾名思义只有一个实例,并且自己负责创建自己的对象,这个类提供了一种唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
public class Singleton{
private volatile static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
适配器模式:将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以相互合作。
命令模式:将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
— 200:请求被正常处理
— 204:请求被受理但没有资源可以返回
— 206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行get方法,相应报文中通过conteng-range指定范围的资源。
— 301:永久性重定向
— 302:临时性重定向
— 303:与302状态码有相似功能,只是希望客户端在请求一个Url的时候,能通过get方法重定向搭配另一个uri上
— 304:发送附带条件的请求时,条件不满足时返回,与重定向无关
— 307:临时重定向,只是强制要求使用post方法
— 400:请求报文语法有误,服务器无法识别
— 401:请求需要认证
— 403:请求的对应资源禁止被访问
— 404:服务器无法找到对应资源
— 500:服务器内部错误
— 503:服务器正忙
HTTP1.1规定了默认长度连接,数据传输完成了保持TCP连接不断开(不四次握手),等待在同域名下继续用这个通过传输数据;相反的就是短连接
2XX:表示成功处理了请求的状态码
3XX:(重定向)表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向
4XX:(请求错误)这些状态码表示请求可能出错,妨碍了服务器的处理(客户端出现的错误)
5XX:(服务器错误)这些状态码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求错误
1.转发只能将请求转发给同一个WEB应用中的组件;而重定向不仅仅可以重定向到当前应用程序中的其他资源,还可以重定向到同一个站点上的其他应用程序中的资源,甚至是使用URL重定向到其他站点的资源
2.重定向访问过程后,浏览器的地址栏中显示的URL会发生改变,由初始的URL地址变成重定向的目标URL;请求转发结束后,浏览器地址栏的URL保持不变
3.重定向方法对浏览器的请求直接作出相应,相应的结果就是告诉浏览器去重新发出对另外一个URL的访问请求。
转发在服务器端内部将请求转发给另外一个资源,浏览器只知道发出了请求并得到了响应结果,并不知道在服务器程序内部发生了转发行为。
4.转发方法的调用者与被调用者之间共享相同的request对象和reponse对象,属于同一个访问请求和响应过程
重定向方法的调用者与被调用者使用各自的request对象和reponse对象,他们属于两个独立的访问请求和响应过程
5.无论是request.getRequestDispatcher().forward()方法,还是response.sendRedirect()方法,在调用它们之前,都不能有内容已经被实际输出到了客户端。如果缓冲区中已经有了一些内容,这些内容将被从缓冲区中。
1、转发使用的是getRequestDispatcher()方法;重定向使用的是sendRedirect();
2、转发:浏览器URL的地址栏不变。重定向:浏览器URL的地址栏改变;
3、转发是服务器行为,重定向是客户端行为;
4、转发是浏览器只做了一次访问请求。重定向是浏览器做了至少两次的访问请求
5、转发2次跳转之间传输的信息不会丢失,重定向2次跳转之间传输的信息会丢失(request范围)。
转发和重定向的选择
1、重定向的速度比转发慢,因为浏览器还得发出一个新的请求,如果在使用转发和重定向都无所谓的时候建议使用转发。
**2、因为转发只能访问当前WEB的应用程序,所以不同WEB应用程序之间的访问,特别是要访问到另外一个WEB站点上的资源的情况,这个时候就只能使用重定向了。**