JSP核心标签库

核心标签库

JSP 有13个核心标签库,总有四类:
表达式控制:out/set/remove/catch
流程控制:if/choose/when/otherwise
循环控制:forEach/forTokens
URL:import/redirect/url

表达式控制

1) out

向网页中输出内容,类似 System.out.print()。

2) set

存取 JSP 中变量的值,类似于赋值语句。

3) remove

从 JSP 中移除变量,类似于 C++中的 delete()。

4) catch

捕捉 JSP 中可能出现的异常,JSP是 JAVA 出身,对异常还是很重视的。

流程控制

1) if

顾名思义,就是判断标签

2) choose/when/otherwise

三者一起协作,choose标记着挑选开始,when 逐个挑,otherwise 便是处理余下所有的项(类似 switch/case:/default)

循环控制

1) forEach

根据循环条件遍历 Colletction 集合。

2) forTokens

将一段字符串分割为数个 Token。

URL控制

1) import

引用静态或动态的内容,你甚至可以在一个 JSP 引入一个实时的百度的首页。

2) redirect

将网页重定向,跳转到另一个网址。

3) url

定义一条 URL。

事务的 ACID

事务 ACID 原则是指事务的四大特征:原子性、一致性、独立性、持久性

Atomic 原子性

事务中的所有操作,要么全部完成,要么全都没完成,不允许存在一部分操作完成而另一部分未完成时事务就结束的情况。

Consistent 一致性

事务结束时系统的状态是一致的,举个例子,一个数据库中保存的资金金额为1个亿,不管多少事务在执行,这些事务结束后,数据库中的资金金额依旧为1个亿,而不会突变为2个亿。

Isolated 隔离性

各个事务之间相互独立存在,一个事务不知道另一个事务的中间状态。

Durable 持久性

事务结束后,系统数据都被固化下来,哪怕系统崩溃,也能通过日志或其他手段恢复数据,重现事务结束时的状态。

实现 ACID 可不容易,现在的数据库一般都用日志方式来做到 ACID,在做任何改变之前都必须先保证日志已经被记录下来,但写日志记录耗时很大,代价较大。
REDO,重做:是指操作已经写入到日志,但实际上在改数据库时机器崩溃,部分数据还没改完,这时需要根据日志的内容,重新改数据库。
UNDO,撤销:在提交事务前,部分操作已经在执行,但最终事务没被提交,所以需要根据日志将那些更改给撤销。

Servlet 的生命周期

Servlet的位置

处于 WEB 服务器和数据库之间的中间层,处理 WEB 服务器收到的 request,进行处理,从而传回 response。

Servlet 关键周期

Servlet 关键的生命周期有:创建、运行、终止

创建

Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,之后的调用不会创建新 Servlet,而是依旧使用该 Servlet。
创建时 Servlet,其 init()方法会被调用,简单地加载一些数据。

运行

在 Servlet 运行过程中,WEB 服务器会不断收到用户的请求,每个请求都会生成一个线程,大量的线程都访问该 Servlet,具体的访问方式是:当请求来时,服务器产生一个线程并且Servlet容器会调用Servlet 的 service()方法进行处理,最终返回 response。

终止

调用 Servlet 的 destroy()方法会终止该 Servlet,结束与数据库的连接,停止相应的后台线程等。用户再调用对应的 URL 时再重新创建 Servlet。

死锁

什么是死锁

西门追雪和他女友去约会,女友说:“你必须给我买 XXX 我才会继续和你 OOO”,而西门追雪说:“你先 OOO 我才给你买 XXX”。

相信以上情景很多人都遇到过,都很无奈,是的,上边的便是我们常说的“死锁”,一旦出现,就很难解决。

在操作系统中,死锁的定义是:

两个线程之间互相等待彼此拥有的资源,进而导致两个线程都进入了漫无期限的等待中。

死锁是怎么产生的

在人类社会中,死锁之所以产生(你希望对方先交钱,但对方希望你先交货),完全是出于人类的欲念,所有人都希望优先得到更多的东西。

机器是没有情感的,那么,机器里头,又如何能产生死锁呢?

第一,互斥。机器得到了某资源,那么其它机器就不能拥有该资源,只能等待该机器释放(我的东西只属于我一个人)

第二,请求与保持。机器得到了某资源,没释放当前资源就去申请其它资源,如果其它资源被其它机器占用,该机器只能等待(机器的贪欲也是无穷无尽的)

第三,不剥夺。机器得到了某资源,其它机器不能抢夺(我的东西谁也不能抢)

第四,环路等待。A 等 B,B 等 C,C 又等 A,谁都不让步(真倔啊)

死锁避免

死锁是件麻烦的事,我们是“能避免就避免”,避免死锁,主要有银行家算法:在分配资源之前,先判断是否会进入不安全状态,如果不会进入不安全状态,就不分配资源。什么叫不安全状态呢?

分配资源时,如果我们没有足够资源同时分配给所有线程,但如果我们可以拖延其中绝大部分,只满足需求最少的那个,满足该线程后,线程执行完毕,释放出的资源又可以满足另一个线程,我们照旧,将释放出来的资源分给它,直到最后满足所有人,这样,就算是安全状态。

也就是说,如果我们连需求最少的那位都满足不了,就绝对是不安全状态,这种情况下,不能分配资源。

用银行家算法可以避免死锁,不过成本太高,缺乏实用价值。

死锁预防

既然死锁是不可避免的,而且死锁避免的成本极高,那么,我们能不能尽量预防死锁的产生呢?

预防死锁,只需要破坏死锁四个必要条件中的一个就可以了。

对于互斥独占,让进程尽可能少的申请资源,虽然独占,但独占得少,这一定程度可以缓解死锁频度,但不是好 IDEA。
对于不可抢夺,在发生死锁时就去强抢部分进程的资源。
对于占有与保持,在一个进程申请资源时,先全部释放所有持有的资源,再一次性地申请全部的资源。
对于环路,可以将资源按顺序标上号,规定进程申请资源时,必须按升序申请,不是升序的申请不予通过。

从0到1理解JVM_线程安全

在 JAVA 中,有许多手段可以保证线程的安全,一个线程操作数据时不会被另一个线程插一脚。

有以下的手段:互斥同步(临界区、信号量、synchronized 锁、ReentranLock重入锁)、非阻塞同步(先操作再看结果,即 CAS 等原子操作)、无同步方案(可重入代码、线程本地存储)。

除了以上那些手段,JAVA 还做了一些优化,如:自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等。

互斥同步

synchronized

synchronized 的成本挺高的,会导致获不到锁的线程进入阻塞状态。线程从运行状态转入阻塞状态需要在用户和内核态间切换,代价特别高。

ReentranLock重入锁

相比 synchronized,重入锁处于 API 级别(而不是 synchronized 那样是语言级别),级别高了,效率自然就跟不上,但重入锁有它的优点,比如:等待可中断(放弃等待)、公平锁(先来先上)、绑定多个条件(可以等待多个条件)。

非阻塞同步

这种同步方式是一种乐观的锁,默认不会有冲突,所以先去修改变量值,如果修改失败才视作线程间彼此冲突。

无同步方案

###可重入代码
指那些代码执行到半路的时候,线程执行其它代码,完事后再来执行这些代码,其结果不会受到其它线程的干扰。比如有一段代码,只单纯定义一些未使用的变量而不执行任何操作,那么该段代码就是可重入的代码。

线程本地存储

每个线程都一个 ThreadLocalMap对象,存储某个变量在该线程内的值,这样一来,各个线程都拥有同一个变量的不同副本,各自修改自己的副本,而不会彼此干扰。

自旋锁

让线程忙等待(busy waiting,指进入一段空循环)一段时间而不进入被阻塞状态,自旋锁的缺点是浪费系统资源。

锁消除/粗化

锁消除,用于即时编译阶段,进行逃逸分析时判断变量是否会被争抢,如果不会就去掉变量的锁。
锁粗化,简单的说,就是扩大锁的范围,尽量将多个锁合在一起,避免频繁地进行加锁和解锁。

轻量锁

线程争抢数据时,会尝试更新对象的对象头,使得该线程拥有对象的轻量锁,如果更新失败,证明已经有其余拥有对象的锁,轻量锁就由互斥锁取代。
之所以有轻量锁,是因为互斥锁的成本太高,我们能避免用互斥锁就尽量避免。

偏向锁

偏向锁类似于轻量锁,但它比轻量锁更“轻量锁“。轻量锁会使用 CAS 原子操作去更新对象的对象头,而偏向锁连 CAS 都不用了。直接将锁默认赋给第一个获得锁的线程(偏向锁即以为偏心,将锁默认给第一个获得锁的线程)。

从0到1理解JVM_线程状态

JAVA 中线程有5种状态:new/runable/waiting/blocked/terminated。

new

线程刚被创建,还没有被运行过

ready

从 new 到 ready,一个线程拥有了自己的内存等资源,随时可以被 CPU 调度进而运行。

running

正在运行的状态,拥有 CPU 的执行权,一个处于 running 状态的线程随时可以通过 yield 方法让出执行权,回到 ready 状态, 此时线程仍保留着锁,给更高级的线程运行的机会。

blocked

当一个线程调用了 Thread 的 sleep() 方法,该线程会带着所有对象的锁进入睡眠,直到到时,才回到 ready 状态。
当一个线程调用了对象的 wait() 方法,该线程便会主动放弃对象的锁,让别人先获取锁,直到收到 notify() 才再次去抢该对象的锁。
运行中的线程也有可能因为 IO 中断而进入阻塞状态,IO 完成后再回到 ready 状态。

terminated

顾名思义,就是线程运行完毕之后的状态,即将被回收资源。

从0到1理解JVM_内存模型

JAVA 支持并发线程操作,其内存模型也不是简单的所有线程共同操作内存。在 JVM 中,每个线程都有自己的内存空间,线程的操作都在自己的内存空间中执行,必要时再同步到公共内存,让其它的线程看见。

volatile

volatile 关键字保证了各个线程都能实时性地看到某个变量的值,它是怎么做到的呢?

被 volatile 修饰的变量具有特殊性,欲更改该变量的值的线程必须先从公共内存中获取该变量的值,存入自己的空间进行操作,改完了还必须“立刻”同步回公共内存,以便其它线程知道变量值被改变,这维护了变量的一致性。

volatiel 还能保证指令不会被“重排序”,volatile 修饰了哪个变量,该变量的赋值等操作的次序就不能被编译器优化。

但 volatile 并不能保证原子性。虽然各个线程都能看到变量值的变化,但有可能有两个线程都修改该变量的值,其中任何一个线程的操作都有可能不是原子操作。

原子性

原子性有 synchronized 来保证,被 synchronized 修饰的方法,在一段时间内只能由一个线程执行。

可见性

可见性是指,一个变量的值被一个线程改变后,其它线程能够立即知晓,而不会导致彼此线程间不知道对方做了什么。
该性质明显可以由 volatile 来保证,另外,synchronized也能保证可见性,为什么呢?

被 synchronized 修饰的变量在被一个线程修改时,别的线程不能碰该变量,当轮到别的线程时,别的线程自然就看到变量被前一个线程修改了。

有序性

volatile 和 synchronized 都能保证有序性。

volatile 阻止指令的重排序优化,而 synchronized 使得线程们只能一个个排着队来执行某段代码。

从0到1理解JVM_字节码执行引擎

之前的博文里,详细地介绍了类的加载和对象的创建,我们现在的 JVM 已经有对象可以操作了,那么,对象的方法是如何执行的呢?

方法的调用

在 JVM 中,每个方法的调用都对应着一个栈中的栈帧,栈帧里包括了方法的局部变量表、操作数、动态连接、返回地址。

局部变量表,大小固定,保存着方法的参数和方法内部定义的局部变量。局部变量表的第一个元素默认是该方法所属对象的引用(这就是为什么我们在方法里用 this 可以引用到当前对象)。

操作数,一个后入先出的栈结构,当 JVM 执行指令时会将指令的操作数入栈,指令执行完就全部出栈。

动态连接,之前在类加载时我们知道,类加载时会有解析阶段,将符号引用解析为直接引用(也就是找到具体的内存地址),实际上,部分符号引用直到运行时才会被解析为直接引用,这就是动态连接。

返回地址,这很容易理解,一个方法执行完毕后,总得回到调用方法的地方继续执行。

一个方法的调用的过程,其实就是对应着生成栈帧,并将栈帧入栈的过程。

重载和重写

我们知道方法调用的过程是怎样的,那么有一个问题是,该调用哪个方法呢?

JAVA 具有多态的特性,也就是重载和重写。

重载,方法名相同而参数类型不同,在编译时就确定好改调用哪个方法,并将其存储在 class 文件中,解析阶段就可以将相应的符号引用转换为直接引用。

重写,子类覆盖掉父类的方法(异常必须更小,子类方法的访问权限必须更大,父类权限不能为 private),在运行时才将符号引用转换为直接引用。

方法执行

直到现在,我们知道调用哪个方法,知道如何调用(栈帧入栈),那么如何执行呢?

JVM 使用一种基于栈的指令集,而不是真实机器使用的基于寄存器的指令集,为了使用基于栈的指令集,先前我们准备的栈帧里的操作数栈是最关键的一点:执行指令时,不断地将操作数入栈出栈,进而做运算,直到最后,操作数栈的栈顶就是我们的执行结果

从0到1理解JVM_对象的创建

JAVA 是一门面向对象的语言,我们所能操作的元素几乎都是对象(连8种基本数据类型都有对应的包装器),在操作对象之前,一般都需要使用new语句(克隆或反序列化时无需 new 也能创建对象)来创建对象,那么,对象是怎么被创建的呢?

安身立命之所

一般的,对象被存储在堆中(栈上分配的对象存储在栈中,Class对象在方法区里)。

创建对象

在创建一个对象之前,首先需要保证对象对应的类已经被加载进方法区。

在类被加载之后,JVM 会在堆中给对象分配一定大小的内存。(至于分配多大的内存,这个在类被加载进方法区后是可以确定的)

对象得到了一块内存,首先做的是将内存中的非对象头部分初始化为0,然后再调用 inti 方法来初始化各个变量的值。

至此,对象就创建完毕并且可以使用。

对象中有什么

在堆中,对象是一块内存,那么,这块内存里都有什么呢?
第一,对象头(包含了对象的哈希码,线程持有锁,GC年龄等),这部分是申请完内存后就不会变,不会变初始化为零值,也不会被 init 方法改变。
第二,对象数据区,这部分内存里存放了该对象的所有变量,会被初始化为零值并被 init 方法改写。
第三,对齐填充,为了符合GC 给对象设下的大小要求,对象需要一定的填充空间。

可见:对象中除了一些必须的信息头外,只有变量,没有代码(代码存放在方法区中的类里,一个类的所有对象都使用同一套代码)。

寻找对象

如何从堆中茫茫的对象里找到我们想要的对象呢?

HotSpot 虚拟机使用了一种指针的方式来定位对象。方法的栈帧里保存了对象的指针(通过该指针能直接找到对象的数据区),数据区中再保留一个类的指针(通过该指针可以知道该对象是属于方法区中的哪个类)。

所以,HotSpot 只需找一次指针就能找到对象,找两次指针就能找到对象对应的类。

从0到1理解JVM_类的加载机制

JAVA 的跨平台性

JAVA是跨平台性的语言,之所以会这样,是 JAVA 源码被编译的时候,并没有直接编译成机器能运行的二进制流,而是编译成 class 文件,在实际运行时,再将 class 文件中的类加载进机器中运行。
JAVA 中的类直到运行时才会被加载和链接,这使得 JAVA 成为一门动态语言(你甚至可以在运行时才从网络上下载一个全新的类到本地运行…)

JVM 加载类

当JAVA 需要使用一个类时,JAVA 得确保类是可用的,什么叫可用呢?可用就是:类已经被加载(确切地说,是加载进 JVM 内存模型的方法区)、连接(验证、准备、解析)、初始化。

加载

类的加载是指从某个位置获得类的二进制流,通常是.class 文件,将.class 文件保存在方法区中,并在方法区中创建一个 Class 对象来表示该类。

连接

在类被加载进方法区后,类通常不完整,会进入连接阶段,这一阶段包括验证(格式、元数据、字节码数据流、符号引用验证以确保解析阶段能进行)、准备(在方法区中为静态变量开辟空间并设置静态变量为零值)、解析(将符号引用转换为直接引用,解析时优先解析父类,并确保父类已经加载)。

初始化

既然类已经被加载并且连接,是不是可以用了呢?答案是不可以,因为类的静态变量还没有被赋值,比如说定义了一个静态变量:

1
static int a = 5;

加载并连接完类后,a 的值是0而不是5,必须要经过初始化,a 的值才会变成5。

初始化是指:将所有的静态变量赋值语句和静态语句块合在一起生成 cinit()方法,并执行该方法,比如下面的一段代码:

1
2
3
4
static int a = 6;
static {
a = 8;
}

所谓初始化,就是执行类似上面的代码。值得注意的是初始化也会优先初始化父类。

类加载结果

一个类被加载之前和被加载后,机器内部都发生了那些变化呢?
简单的说就是:内存的方法区中多了一个保存类的结构,该类的符号引用被转换为直接引用,静态变量被赋值,静态语句被执行(如果该类有父类且父类不可用,那么也会加载父类)。

加载器

加载类有三个阶段:加载、连接、初始化。第一个阶段,加载需要加载器,在 JVM 中,加载器分几种:根加载器、扩展加载器、系统加载器、用户加载器。
从 JDK1.2开始,JAVA 引入了双亲加载机制,当一个类加载器要加载类时,需要将类上缴给更高级的类加载器去加载,只有当更高级的的类加载器无能为力时才轮到该类加载器去加载。
根加载器是最顶层的加载器(加载 rt.jar 类库),接着是扩展加载器(加载 java.ext.dir 指定目录下的类),然后是系统加载器(加载 classPatch 和 java.class.path),最后才是用户加载器(也就是程序员可以定义的加载器)