为什么重写equals要重写hashcode 如果你重载了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能某两个对象明明是“相等”,而hashCode却不一样。 这样,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另一个作为键值去查找他们的时候,则根本找不到。 使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()。
索引 查询速度快,修改更新慢,因为修改更新时要不断的维护树状结构,消耗性能,占空间
聚集索引和非聚集索引区别 聚集索引: 定义:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。 1.属于Innodb。 2.按照主键B+树的排列方式存放,子节点存放的就是数据。(如果没有主键,以第一列为聚集索引) 3.只有一个聚集索引。 4.普通索引指向聚集索引。 非聚集索引: 定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。(有二次查询) 1.属于MyIsam。 2.普通索引和非聚集索引没什么区别。 3.存放的是地址。1.ALTER TABLE
创建索引 1.ALTER TABLE用来创建普通索引、UNIQUE索引或PRIMARY KEY索引。 ALTER TABLE table_name ADD INDEX index_name (column_list) ALTER TABLE table_name ADD UNIQUE (column_list) ALTER TABLE table_name ADD PRIMARY KEY (column_list) 其中table_name是要增加索引的表名,column_list指出对哪些列进行索引,多列时各列之间用逗号分隔。 索引名index_name可选,缺省时,MySQL将根据第一个索引列赋一个名称。另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。 2.CREATE INDEX CREATE INDEX可对表增加普通索引或UNIQUE索引。 CREATE INDEX index_name ON table_name (column_list) CREATE UNIQUE INDEX index_name ON table_name (column_list) table_name、index_name和column_list具有与ALTER TABLE语句中相同的含义,索引名不可选。另外,不能用CREATE INDEX语句创建PRIMARY KEY索引。
哈希索引和B+树索引 哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。 如果是等值查询,那么哈希索引明显有绝对优势。如果是范围查询检索,这时候哈希索引就毫无用武之地了, 因为原先是有序的键值,经过哈希算法后,有可能变成不连续的了,就没办法再利用索引完成范围查询检索; 在有大量重复键值情况下,哈希索引的效率也是极低的,因为存在所谓的哈希碰撞问题。
B+树索引结构适用于绝大多数场景,像下面这种场景用哈希索引才更有优势:如果存储的数据重复度很低(也就是说基数很大), 对该列数据以等值查询为主,没有范围查询、没有排序的时候,特别适合采用哈希索引。
sql优化 可以用explain查看
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引 2.应尽量避免在 where 子句中对字段进行 null 值判断。 3.应尽量避免在 where 子句中使用!=或<>操作符。 4.应尽量避免在 where 子句中使用 or 来连接条件,in 和 not in 也要慎用。 5.应尽量避免在 where 子句中对字段进行表达式操作,函数操作。 6.尽可能的使用 varchar 代替 char ,因为首先变长字段存储空间小,可以节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。 7.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。 8.避免频繁创建和删除临时表,以减少系统表资源的消耗。
联合索引 1):查询条件中出现联合索引第一列,或者全部,则能利用联合索引. 2):查询条件中没有出现联合索引的第一列,而出现联合索引的第二列,或者第三列,都不会利用联合索引查询. 最左前缀原则:顾名思义是最左优先,以最左边的为起点任何连续的索引都能匹配上.
行锁和表锁 行锁粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗自然也就更大了。此外,行级锁定也最容易发生死锁。 表锁该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快
数据库优化
分库分表 垂直分库/分表 ?垂直划分数据库是根据业务进行划分,例如将shop库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库(表)的大小来提高性能,但这种方式并没有解决高数据量带来的性能损耗。 同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如用户表可根据业务分成基本信息表和详细信息表等。 水平分库/分表 ?水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。 又比如根据用户id的值,根据规则分成若干个表。每个表结构一致,(这点与垂直分库分表相反)。
事务问题 解决事务问题目前有两种可行的方案:分布式事务和通过应用程序与数据库共同控制实现事务 下面对两套方案进行一个简单的对比。
方案一:使用分布式事务 优点:交由数据库管理,简单有效 缺点:性能代价高,特别是shard越来越多时 方案二:由应用程序和数据库共同控制 原理:将一个跨多个数据库的分布式事务分拆成多个仅处于单个数据库上面的小事务,并通过应用程序来总控各个小事务。 优点:性能上有优势 缺点:需要应用程序在事务控制上做灵活设计。如果使用 了spring的事务管理,改动起来会面临一定的困难。 2.跨节点join问题 A.全局表 所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。 为了避免跨库 join 查询,我们可以将这类表在其他每个数据库中均保存一份。 同时,这类数据通常也很少发生修改(甚至几乎不会),所以也不用太担心“一致性”问题。 B.字段冗余 字段冗余能带来便利,是一种“空间换时间”的体现。但其适用场景也比较有限,比较适合依赖字段较少的情况。数据库读写分离,主从同步实现方法 通过设置主从数据库实现读写分离,主数据库负责“写操作”,从数据库负责“读操作”,根据压力情况,从数据库可以部署多个提高“读”的速度,借此来提高系统总体的性能。 要实现读写分离,就要解决主从数据库数据同步的问题,在主数据库写入数据后要保证从数据库的数据也要更新 过程:主服务器master记录数据库操作日志到Binary log,从服务器开启i/o线程将二进制日志记录的操作同步到relay log(存在从服务器的缓存中), 另外sql线程将relay log日志记录的操作在从服务器执行。
数据库设计三大范式 第一范式:每一列属性都是不可再分的属性值,确保每一列的原子性,两列的属性相近或相似或一样,尽量合并属性一样的列,确保不产生冗余数据。比如联系方式可以再细分就不行 第二范式:每一行的数据只能与其中一列相关,即一行数据只做一件事。 只要数据列中出现数据重复,就要把表拆分开来。一个人同时订几个房间,就会出来一个订单号多条数据, 这样子联系人都是重复的,就会造成数据冗余。我们应该把他拆开来 第三范式:数据不能存在传递关系,即每个属性都跟主键有直接关系而不是间接关系。比如Student表(学号,姓名,年龄,性别,所在院校,院校地址,院校电话)不符合
存储过程:一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后调用不需要再次编译,用户通过指定存储过程的名字并给出参数来执行它 1、存储过程只在创造时进行编译,以后每次执行存储过程都不需再重新编译,而一般SQL语句每执行一次就编译一次,所以使用存储过程可提高数据库执行速度。 2、存储过程可以重复使用,可减少数据库开发人员的工作量 3、安全性高,可设定只有某些用户才具有对指定存储过程的使用权 触发器是特殊的存储过程,是由事件触发的
左外连接、右外连接、内连接、外连接 左外连接(left join)即为两张表进行连接时,是以处于left join语句左侧的表为基准去匹配left join语句右边的表,如果左表中的一条数据在右表中能找到与之对应的一条数据, 那么就会出现在以虚表形式存在的结果表中,如果没有找到,那么会以null来代替右表中的数据去匹配左表 右外连接(right join)与上面类似。 内连接(inner join)就是在用两张表进行匹配的时候,如果表中任意一条数据在另一张表中都是找不到对应数据的话,那么在结果表中是不会有这一条数据的。 也就是说必须是两张表中任意两条能够互相对应着的数据才能被存入到结果表中
mysql缓存 1.服务器接收SQL,以SQL和一些其他条件为key查找缓存表 2.如果找到了缓存,则直接返回缓存 3.如果没有找到缓存,则执行SQL查询,包括原来的SQL解析,优化等。 4.执行完SQL查询结果以后,将SQL查询结果缓存入缓存表
mysql和nosql mysql特性是acid使用sql语句操作,持久化的保存数据 nosql采用键值对存储,采用命令操作,CAP定理,分布式计算 在计算机科学中, CAP定理(CAP theorem), 又被称作 布鲁尔定理(Brewer’s theorem), 它指出对于一个分布式计算系统来说,不可能同时满足以下三点: 一致性(Consistency) (所有节点在同一时间具有相同的数据) 可用性(Availability) (保证每个请求不管成功或者失败都有响应) 分隔容忍(Partition tolerance) (系统中任意信息的丢失或失败不会影响系统的继续运作)
MyISAM和InnoDB MyISAM不支持事务,但是每次查询都是原子的;支持表级锁,即每次操作是对整个表加锁; 一个MYISAM表有三个文件:索引文件、表结构文件、数据文件;存储表的总行数;允许没有任何索引和主键的表存在 InnoDB支持事物,支持行级锁及外键约束:因此可以支持写并发;不存储总行数; 所有的表都保存在同一个数据文件中;如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键
sql注入 select * from user where userName=‘dsadas’ and password=’*’ or ‘1’=‘1’ 用户名随便 密码 *’ or ‘1’='1
for update 加锁,加在select查询语句后面,行级锁 “使用FOR UPDATE WAIT”子句的优点如下: 1防止无限期地等待被锁定的行; 2允许应用程序中对锁的等待时间进行更多的控制。 3对于交互式应用程序非常有用,因为这些用户不能等待不确定 4 若使用了skip locked,则可以越过锁定的行,不会报告由wait n 引发的‘资源忙’异常报告
HashMap和ConcurrentHashMap 底层是哈希表也就是数组加链表的形式,entry数组,存储的是key-value 链表是为了解决哈希冲突存在的, 添加数据的时候经过hashcode方法计算出存储位置,当位置为空时直接放进去,不为空时当在这个位置上是一个链表,会用equals方法比较相等即覆盖,否则新增, get方法也是这种操作。首先也是根据 key 计算出 hashcode,然后定位到具体的桶中。判断该位置是否为链表。 不是链表就根据 key、key 的 hashcode 是否相等来返回值。为链表则需要遍历直到 key 及 hashcode 相等时候就返回值。啥都没取到就直接返回 null 。 默认大小为16,负载因子为0.75,当达到12是会新建一个32的数组,并把里面的东西复制进去。Entry 是 HashMap 中的一个内部类 1.7时当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。 1.8链表过长时会形成红黑树。无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。 在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。 HashMap在put的时候,插入的元素超过了容量的范围就会触发扩容操作这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作, 如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。HashMap 使用第 0 个桶存放键为 null 的键值对。
ConcurrentHashMap 是由 Segment 数组、HashEntry 组成。采用了分段锁技术,其中 Segment 继承于 ReentrantLock。 不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。 put方法:首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。 如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功 ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。1.7和1.8:其中抛弃了原有的 Segment 分段锁,1.8采用了 CAS + synchronized 来保证并发安全性。
jdk1.7和jdk1.8中的区别: 1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。 2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。 3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。 4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。 5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
缓存问题 缓存并发问题:在高并发场景下,有可能多个请求并发的去从数据库获取数据,对后端数据库造成极大的冲击,甚至导致 “雪崩”现象。 此外,当某个缓存key在被更新时,同时也可能被大量请求在获取,这也会导致一致性的问题。那如何避免类似问题呢?我们会想到类似“锁”的机制, 在缓存更新或者过期的情况下,先尝试获取到锁,当更新或者从数据库获取完成后再释放锁,其他的请求只需要牺牲一定的等待时间,即可直接从缓存中继续获取数据。
缓存穿透问题:在高并发场景下,如果某一个key被高并发访问,没有被命中, 出于对容错性考虑,会尝试去从后端数据库中获取,从而导致了大量请求达到数据库,而当该key对应的数据本身就是空的情况下, 这就导致数据库中并发的去执行了很多不必要的查询操作,从而导致巨大冲击和压力。 可以通过下面的几种常用方式来避免缓存穿透问题: 1、缓存空对象,对查询结果为空的对象也进行缓存。 2、单独过滤处理,对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库
缓存的雪崩现象: 如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。从而导致数据库崩溃,整个系统崩溃,发生灾难。 为了避免这种周期性失效,可以通过设置不同的过期时间,来错开缓存过期时间,从而避免缓存集中失效。也可以通过多级缓存来避免这种灾难
乐观锁和悲观锁 悲观锁:是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。 传统的关系型数据库里边就用到了很多这种锁机制, 比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如Java里面的同步原语synchronized关键字的实现也是悲观锁。 乐观锁:是一种思想,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据, 可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。 Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。乐观锁的一种实现方式-CAS(Compare and Swap 比较并交换):
公平锁和非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会造成饥饿现象。Synchronized
volatile和synchronized的区别 volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的 volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性 volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。 volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
Lock和synchronized synchronized是Java语言的关键字 Lock是一个接口 synchronized不需要用户去手动释放锁,而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。 Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断; Lock可以提高多个线程进行读操作的效率
ReenreantLock和synchronized的区别 1.ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候 2.synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定, 但是使用Lock则不行,lock是通过代码实现的,要保证锁一定会被释放,就必须将unLock()放到finally{}中 3.在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock, 但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态; 4.Synchronized和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量, Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,而ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程. 5.线程并发量不大的情况下,那么Synchronized因为自旋锁,偏向锁,轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比ReentrantLock高效。
重入锁和不可重入锁主要的差别在对相同线程是否能够重复获取 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
产生死锁的必要条件是:1、互斥条件;2、不可剥夺条件(不可抢占);3、请求与保持条件;4、循环等待。 例如两个用户同时投资,A用户金额随机分为2份,分给借款人1,2。B用户金额随机分为2份,分给借款人2,1,由于加锁的顺序不一样,死锁当然很快就出现了 根据产生死锁的四个必要条件,只要使其中之一不能成立,死锁就不会出现。为此,可以采取下列三种预防措施: 1、采用资源静态分配策略,破坏"请求与保持条件"条件; 2、允许进程剥夺使用其他进程占有的资源,从而破坏"不可剥夺"条件; 3、采用资源有序分配法,破坏"环路"条件。
解除死锁常常采用下面两种方法:1、资源剥夺法;2、撤消进程法 可以使用 jstack或者pstack 和 gdb 工具对死锁程序进行分析。pstack: 功能是打印输出此进程的堆栈信息。可以输出所有线程的调用关系栈 jstack:jstack是java虚拟机自带的一种堆栈跟踪工具,所以仅适用于java程序,功能跟pstack一样,但是更强大,可以提示哪个地方可能死锁了。 jstack工具:用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。jstack命令主要用来查看Java线程的调用堆栈的,可以用来分析线程问题(如死锁)。
sychronized底层 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。
volatile 当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据, 当运算结束之后,再将高速缓存中的数据刷新到主存当中 如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。 volatile可以保证可见性也就是当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。 可以禁止指令重排序 不能保证原子性 保证原子性:并发包中的原子操作类(AtomicInteger、AtomicLong、AtomicBoolean,AtomicReference等),使用同步技术(sychronized)来让它变成一个原子操作
CAS和AQS CAS(Compare And Swap),即比较并交换。是解决多线程并发情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值
AQS(抽象的队列式的同步器)AQS定义了一套多线程访问共享资源的同步器框架,是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。 维护着共享资源和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。如常用的ReentrantLock/Semaphore/…。 AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。 1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回; 2.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式; tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。 比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了 任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。 等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
CyclicBarrier是一个同步工具类,它允许一组线程互相等待,直到到达某个公共屏障点。与CountDownLatch不同的是该barrier在释放等待线程后可以重用,所以称它为循环(Cyclic)的屏障(Barrier)。
Semaphore Semaphore是一种计数信号量,用于管理一组资源,内部是基于AQS的共享模式。它相当于给线程规定一个量从而控制允许活动的线程数。 Semaphore主要用于控制当前活动线程数目,就如同停车场系统一般,而Semaphore则相当于看守的人,用于控制总共允许停车的停车位的个数,而对于每辆车来说就如同一个线程, 线程需要通过acquire()方法获取许可,而release()释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。
下面对上面说的三个辅助类进行一个总结: 1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同: CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行; 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行; 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。 2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限
类加载器双亲委派模型 类加载器就是将class文件加载到JVM内存,成字节码文件 如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。 每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。 启动类加载器(Bootstrap ClassLoader):由C++语言实现,是最底层的类加载器 黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。 每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。 为什么要使用这种双亲委托模式呢?因为这样可以避免重复加载、安全
类加载过程 类加载的过程主要分为三个部分: 加载 加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。 验证:主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。 准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予默认值。 解析:将常量池内的符号引用替换为直接引用的过程 初始化 类变量初始化 如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
jvm内存 程序计数器:严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。 当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。 为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储, 我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。
栈中主要存放一些局部变量和引用,他是线程私有的
堆是JVM所管理的内存中最大的一块,是被所有Java线程所共享的,不是线程安全的,主要存储new出来的对象,是GC管理的主要区域。 堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。
方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量。是被Java线程共享的。 当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory(OOM)的错误信息。方法区也是堆中的一部分, 就是我们通常所说的Java堆中的永久区 Permanet Generation,大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。 方法区如何判断是否需要回收:该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例; 加载该类的ClassLoader已经被回收;该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾回收 判断对象是否存活的算法!引用计数算法引用计数是垃圾收集器中的早期策略。当一个对象被创建时,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1, 但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集 但是无法检测出循环引用 可达性分析算法是从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后, 剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
强引用、软引用、弱引用、虚引用 强引用:这类引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 软引用用来描述一些还有用但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收后还没有足够的内存,才会抛出内存溢出异常 弱引用它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 虚引用是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。 SoftReference sr = new SoftReference(new String(“hello”)); WeakReference sr = new WeakReference(new String(“hello”)); GCROOT对象 1、虚拟机栈(栈帧中的本地变量表)中引用的对象。 2、 本地方法栈中JNI(即一般说的native方法)引用的对象。 3、 方法区中的静态变量和常量引用的对象。
垃圾回收算法 标记-清除算法(Mark-Sweep) 复制算法(Copying) 标记-整理算法(Mark-compact) 分代收集算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收, 标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象, 并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。 标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。 老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
年轻代((回收主要以Copying为主)):所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区 大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。 老年代(回收主要以Mark-Compact为主):在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。 持久代用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代也称方法区
Scavenge GC 一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。 这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行 。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC 对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中, 很大一部分工作就是对于Full GC的调节。有如下原因可能导致Full GC: a) 年老代(Tenured)被写满; b) 持久代(Perm)被写满; c) System.gc()被显示调用; 频繁发生fullgc 新生代的内存空间不够用先用jstat看看内存使用情况
CMS和G1垃圾回收器 CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法实现 过程:初始标记、并发标记、重新标记、并发清除。 主要优点:并发收集、低停顿。缺点:对CPU非常敏感、无法处理浮动垃圾、产生大量的空间碎片
G1是一款面向服务端应用的垃圾收集器 步骤:1、初始标记;2、并发标记;3、最终标记;4、筛选回收 G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间、分代收集、可预测的停顿
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。 Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
内存泄漏和内存溢出 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory; 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重, 无论多少内存,迟早会被占光。静态集合类,数据库连接,网络连接和io连接 OOM的原因及解决办法 内存泄露的堆积、static等关键字过多,线程池不管理式滥用 解决:关闭连接和流、没必要时少用static、使用软引用. 工具:jconsole、jvisualVM,对OOM监控。dump+EMA Dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。 在Windbg中可以通过.dump命令保存进程的dump文件。dump还是oracle及SQL数据库中导出的数据文件。可以备份数据,并可以实现后期的导入
Java应用CPU占用100%原因分析 1 、 使用linux系统top命令查看cpu占用资源较高的PID 2、 通过jps 命令确定想要分析的应用的进程编号 3、查看Java应用中线程CPU占比 4、从中选择占比较高的线程的编号(PID),并将该PID转换为16进制。通过jstack工具分析出线程的具体信息,再通过相应的解决方法来解决cpu占用过高的问题。
两个栈模拟一个队列 public class Solution { Stack stack1 = new Stack(); Stack stack2 = new Stack();
public void push(int node) { stack1.push(node); } public int pop() { while(!stack1.isEmpty()){ stack2.push(stack1.pop()); } int first = stack2.pop(); while(!stack2.isEmpty()){ stack1.push(stack2.pop()); } return first; }} 输入n个整数,找出其中最小的K个数 public class Solution { public ArrayList GetLeastNumbers_Solution(int [] input, int k) { ArrayList al = new ArrayList(); if (k > input.length) { return al; } for (int i = 0; i < k; i++) { for (int j = 0; j < input.length - i - 1; j++) { if (input[j] < input[j + 1]) { int temp = input[j]; input[j] = input[j + 1]; input[j + 1] = temp; } } al.add(input[input.length - i - 1]); } return al; } }
输出1到100内的素数 public static void getsu() { for (int i = 2; i <= 100; i++) // 1不是素数,所以直接从2开始循环 { int j = 2; while (i % j != 0){ j++; //测试2至i的数字是否能被i整除,如不能就自加 } if (j == i) {// 当有被整除的数字时,判断它是不是自身 System.out.println(i); // 如果是就打印出数字 } } }
Spring框架 什么是Spring框架?Spring框架有哪些主要模块? spring 是一个开源的轻量级 控制反转 (inversion of control -> IOC ) 和面向切面( AOP ) 的容器框架,它主要的目的是简化企业开发。Spring可以理解为是Bean容器或管理家, 我们可以面向接口进行编程,当没有Spring时,我们在业务逻辑层访问数据持久层时,是要new出对象的,这样对项目层次间的解耦不是很好, 但是用spring之后,在应用程序中需要对象时,spring就会自动的给我们创建对象注入到我们所需的程序中来也降低了各个层次间的耦合度
优点: 1、轻量级的容器框架没有侵入性; 2、使用IoC容器更加容易组合对象之间的直接关系,面向接口编程,降低耦合; 3、使用AOP可以更加容易的进行功能扩展; 4、创建对象默认是单例的,不需要再使用单例模式进行处理。 控制反转和依赖注入有什么区别? IOC:就是将原本在程序中手动创建UserService对象的控制权,交由Spring框架管理,简单说,就是创建UserService对象控制权被反转到了Spring框架 DI:依赖注入,在Spring框架负责创建Bean对象时,动态的将依赖对象注入到Bean组件 IOC和DI有什么关系呢?其实它们是同一个概念的不同角度描述,以前创建对象都是通过new关键字去创建,但是用了Spring之后就可以不用这么创建对象了,把控制权交给Spring容器, 哪里用对象由容器给,这就是控制反转。依赖注入就是站在应用程序的角度,当需要用某个对象的时候,由容器注入进来,这就是依赖注入。@Resouce和@Autowired @Resouce用这个注解,进行依赖注入(此注解是java提供的,并不是Spring提供的)默认是按名称进行装配的,再按类型进行装配(在不指定名称的情况下) @Autowired 注解和 @Resource 功能类似,它是在 org.springframework.beans.factory.annotation.Autowired(它是spring中的) 默认是按类型装配,默认情况下,它要求依赖对象必须存在,要不会报错, 如果不想报错可以设置required=false表示忽略当前要注入的bean,如果有直接注入,没有跳过,不会报错。
四种实例化bean的方式: 构造器注入 Setter方法注入 静态工厂 实例工厂SpringAOP AOP,一般称为面向方面(切面)编程,作为面向对象的一种补充,用于解剖封装好的对象内部,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块, 这个模块被命名为“切面”(Aspect),切面将那些与业务无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。 可用于权限认证、日志、事务处理。 IOC 应用本身不负责依赖对象的创建和维护,而是交给其他程序去创建, 这样,控制权就由应用转到了外部容器,控制权的转移, 就叫控制反转
7大核心模块 1)Spring Core //核心容器 核心容器: 提供Spring 框架的基本功能,核心容器的主要组件是BeanFactory,它是工厂模式的实现 BeanFactory 例用控制反转(IOC) 模式将应用程序的配置和依赖规范与实际的应用程序代码分开
2)Spring Context //上下文的一个配置文件 Spring 上下文是一个配置文件,向Spring框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI 、EJB、电子邮件、国际化、校验和调度功能。 3)Spring AOP 通过配置管理特性,Spring AOP 模块直接将面向切面的编程功能集成到了Spring 框架中。 所以,可以很容易地使Spring框架管理的任何对象支持AOP 。Spring AOP 模块为基于Spring 的应用程序中的对象提供了事务管理服务。通过使用Spring AOP ,不用依赖EJB 组件,就可以 将声明性事务管理集成到应用程序中。 4)Spring DAO //对JDBC、Hibernate等DAO层支持。 DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库 供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写 的异常代码数量(例如打开和关闭连接)。 5)Spring ORM Spring 框架插入了若干个ORM 框架,从而提供了ORM的对象关系工具,其中包括JDO 、 Hibernate 和MyBatis。所有这些都遵从Spring 的通用事务和DAO 异常层次结构。 6) Spring WEB Web上下文模块建立于应用上下文模块之上,提供了一个适合于Web应用的上下文。另外, 这个模块还提供了一些面向服务支持。例如:实现文件上传的multipart请求,它也提供了 Spring和其它Web框架的集成,比如Struts、WebWork。 7) Spring MVC Spring为构建Web应用提供了一个功能全面的MVC框架。虽然Spring可以很容易地与其它MVC框架集成, 例如Struts,但Spring的MVC框架使用IoC对控制逻辑和业务对象提供了完全的分离。 它也允许你声明性地将请求参数绑定到你的业务对象中,此外,Spring的MVC框架还可以利 用Spring的任何其它服务,例如国际化信息与验证。Bean的作用于范围 1) singleton 单例 2) prototype 多实例 3) request 4) session 5) global-session 类似于标准的 HTTP Session作用域,不过它仅仅在基于 portlet 的web应用中才有意义。 spring自动装配bean no – 缺省情况下,自动配置是通过“ref”属性手动设定 byName – 根据属性名称自动装配。如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。 byType – 按数据类型自动装配。如果一个bean的数据类型是用其它bean属性的数据类型,兼容并自动装配它。 constructor – 在构造函数参数的byType方式。 autodetect – 如果找到默认的构造函数,使用“自动装配用构造”; 否则,使用“按类型自动装配”。
spring事务几种实现方式 (1)基于 TransactionProxyFactoryBean的声明式事务管理(基于xml) ①引入一系列的约束头文件以及标签 ②配置C3P0数据源以及DAO、Service ③配置事务管理器以及事务代理工厂Bean。测试类getBean获取的是代理工厂id
(2)基于 @Transactional 的声明式事务管理(基于注解)测试类getBean获取的id是原始对象的service (3)基于Aspectj AOP配置事务 测试类getBean获取的id获取的是原始对象静态代理和动态代理 静态:由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了 动态:所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法, 并且在特定的切点做了增强处理,并回调原对象的方法。 代理模式:(起到中介作用:客户端和目标对象之间) ① 静态 (不灵活,每个业务都要有一个代理角色,大量使用会导致类的急剧膨胀) ② 动态 对于Spring AOP 非常重要 AOP通过代理模式完成的
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理: 1、JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。 生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法,当我们调用代理类对象的方法时,这个“调用”会转送到invoke方法中, 代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数。 2、如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象, 并覆盖其中特定方法,覆盖方法时可以添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
BeanFactory 接口和 ApplicationContext 接口有什么区别 ? 1、ApplicationContext 接口继承BeanFactory接口,Spring核心工厂是BeanFactory ,BeanFactory采取延迟加载,第一次getBean时才会初始化Bean, ApplicationContext是会在加载配置文件时初始化Bean。 2、ApplicationContext是对BeanFactory扩展,它可以进行国际化处理、事件传递和bean自动装配以及各种不同应用层的Context实现 开发中基本都在使用ApplicationContext, web项目使用WebApplicationContext ,很少用到BeanFactory
SpringMVC框架 SpringMVC是一个基于MVC架构的用来简化web应用程序开发的应用开发框架,它是Spring的一个模块,无需中间整合层来整合?,它和Struts2一样都属于表现层的框架。 在web模型中,MVC是一种很流行的框架,通过把Model,View,Controller分离,把较为复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。
SpringMVC的处理流程 1、DispatcherServlet前端控制器接收发过来的请求,交给HandlerMapping处理器映射器 2、HandlerMapping处理器映射器,根据请求路径找到相应的HandlerAdapter处理器适配器(处理器适配器就是那些实现Controller接口的控制层的类) 3、HandlerAdapter处理器适配器,处理一些功能请求,返回一个ModelAndView对象(包括模型数据、逻辑视图名) 4、ViewResolver视图解析器,先根据ModelAndView中设置的View解析具体视图 5、然后再将Model模型中的数据渲染到View上
组件 DispatcherServlet:前端控制器,用于请求到达前端控制器,由它调用其他组件处理用户的请求。 HandlerMapping:处理器映射器,负责根据用户请求找到Handler(处理器),springmvc提供了不同的映射器实现方式。 Handler:处理器,对具体的用户请求进行处理。 HandlerAdapter:处理器适配器,通过HandlerAdapter对处理器进行执行。 View Resolver:视图解析器,负责将处理结果生成view视图。View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象, 最后对View进行渲染将处理结果通过页面展示给用户。springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。
@RequestMapping注释用于映射url到控制器类或一个特定的处理程序方法。可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 value:指定请求的实际地址,指定的地址可以是URI Template 模式 method:指定请求的method类型, GET、POST、PUT、DELETE等 consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html; produces: 指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回 params: 指定request中必须包含某些参数值时 headers: 指定request中必须包含某些指定的header值
springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析, 并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。