1、自增变量
1.赋值操作,最后计算,也就是把操作数栈中的结果重新赋值给变量。
2.自增自减的操作都是直接修改变量值的,是不需要经过操作数栈的。
3.计算过程中的临时结果先会保存在操作数栈中。
2、单例模式
创建步骤:
1.构造器的私有化
2.自行创建实例,并且使用静态变量保存
3.对外提供此实例
类型:
饿汉式:天然的线程安全,但是不管你需不需要这个类的实例都会去进行创建实例,在一些情况下比较浪费资源。(实现方式直接实例化,枚举,静态代码块)
饱汉式:线程不安全,但是可以根据自身需要获取实例,节约了资源开支,为了结果线程不安全的情况,需要引进锁的机制。(静态内部类的方式)
3、类的初始化步骤
1.类初始化:
main方法所在类需要初始化(如果此类存在父类则需要初始化父类),此方法调用的是clinit方法主要是初始化静态变量和静态代码块(从上到下执行)
2.实例处理化:
非静态变量和非静态代码块(从上到下执行),构造器代码最后执行。主要是调用init方法来初始化,若存在父类则需要显初始化父类的非静态变量和非静态代码块以及构造器代码。
4.方法中的参数传递
1.基本数据类型:数据值
2.引用数据类型:地址值
ps:特殊情况:String,包装类型等对象不可变性.
5.bean的作用域
1.singleton:默认是singleton(单例),IOC容器中所有的bean实例都指向同一个bean
2.prototype: 原型。IOC容器不会再创建bean,每次调用getbean的时候才会创建,并且每次都会创建一个新的实例。
3.request:每次请求实例化一个bean
4.session:在一次会话中共享一个bean
6.事务的传播和隔离
事务的特征:ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)
事务的传播是属性:
Propagation.REQUIRED(默认)
如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务。
Propagation.SUPPORTS
如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行。
Propagation.MANDATORY
如果当前存在事务,则加入该事务;如果当前不存在事务,则抛出异常。
Propagation.REQUIRES_NEW
重新创建一个新的事务,如果当前存在事务,延缓当前的事务。
Propagation.NOT_SUPPORTED
以非事务的方式运行,如果当前存在事务,暂停当前的事务。
Propagation.NEVER
以非事务的方式运行,如果当前存在事务,则抛出异常。
Propagation.NESTED
如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。
事务隔离级别
1、DEFAULT
默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别
2、READ_UNCOMMITTED
读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用
3、READ_COMMITED
读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读
4、REPEATABLE_READ
重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决
5、SERLALIZABLE
串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了
事务的隔离级别带来的问题:
1.脏读:一个事务读到的某一个事务修改但未成功提交的数据
2.不可重复读: 一个事务在读取数据之后,紧跟着被另一个事务修改,导致第二次读取的值不一致
3.幻读:一个事务查询某表的数据之后,另一个事务插入一行数据,导致第二次读取的数据多了一些。
网上专门有图用表格的形式列出了事务隔离级别解决的并发问题:
解决办法:设置事物的隔离级别,mysql的隔离级别默认为可重复读,oracle的默认隔离级别是读已提交。
7.redis的持久化方式
1.rdb方式
fork一个子进程将全量的redis数据持久化到磁盘,此过程在内存里面完成,每隔一段时间进行保存
优点:节省磁盘空间,速度较快
缺点:如果数据量一旦过大则会比较消耗性能,每隔一段时间进行保存的则会导致最新一部分数据可能丢失
2.AOF方式
以日志的方式记录每次对redis 的操作,并不会去改写文件
优点:
备份机制稳健,丢失概率低
日志的可读性强
缺点:
会占用更多的磁盘空间
恢复速度慢
8.redis的数据类型和使用场景
String,hash,list,set,zset
使用场景
(一)String
这个其实没啥好说的,最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。
(二)hash
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。
(三)list
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。
(四)set
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。
另外,就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
(五)sorted set
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。另外,参照另一篇《分布式之延时任务方案解析》,该文指出了sorted set可以用来做延时任务。最后一个应用就是可以做范围查找。
8.1、redis的过期策略以及内存淘汰机制
redis的过期策略采用的定期清除+惰性清除。是定期而不是定时,如果redis起一个定时器定时扫描并且清除对象的话,太耗费cpu资源了,一旦对象过多,很可能会拉跨redis。所以采用定期,定期清除也不是全部清除,而已随机抽取清除,这样的话,就不会占用太多的资源,但是定期随机清除会导致一些数据已经过期但没有清除,所以就需要惰性清除,惰性清除就是当访问redis的key值时,此时的key要是已经过期了,则会清除这个key。返回null。
但是redis 的过期策略会导致一个问题的出现:如果一些key没有被定期清除掉,并且一直没有去进行请求访问,那么这个key则会一直存在于这个redis中长此以往,内存则会占满,那么应该怎么办?
redis提供一种机制,叫做内存淘汰机制。需要在redis.conf中进行配置
maxmemory-policy allkeys-lru
一共有五种模式:
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。(应该没人用吧。)
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(推荐使用。)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。(应该也没人用吧,你不删最少使用Key,去随机删。)
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。(不推荐)
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。(不推荐)
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。(不推荐)
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
8.2、redis和数据库的双写一致性的问题
一致性分为两种:强一致性和最终一致性
如果数据要求为强一致性,那么就不要放在缓存中了。
如果数据要求为最终一致性,那么在数据更新时,应该先更新数据库,后更新缓存,如果数据库更新失败,则不会再更新缓存,若是数据库更新成功,但是缓存更新失败了,则可以借助消息队列,直到缓存运行正常的时再重新写入。
8.3、redis中并发竞争key问题
这个并发竞争和并发线程的执行顺序有关。
1)若是对执行顺序无要求的,那么只需要加锁(使用分布式锁)即可,但是不建议对整个redis加锁操作,这样可能导致其他线程操作受限,最好是细化加锁的颗粒,只对key加锁。
2)若是对执行顺序有要求。比如:A->B->C。那么可以借助类似与CAS中引发ABA问题的解决方案一样,在修改值时候带上时间戳,若是A更新的时候发现,key值被B修改了并且时间和A所读取的时间不同,则放弃修改。
当然也可以将这些线程放入队列顺序执行。
8.4、缓存穿透,缓存击穿,缓存雪崩
上述三个问题一般出现的场景是在高并发的情况下,流量足够大的时候才可能出现。
缓存穿透:一般缓存穿透为恶意访问,就是大量的请求去访问缓存不存在并且数据库也不存在的数据,导致数据库连接瘫痪。
解决办法:
1:设置对参数的校验,比如序号不允许小于零。
2:设置null值,若是 发现此请求在数据库不存在值,那么就将这个key所对应的value设置为null存入缓存。这样后面按这个key值得请求都会被立刻返回。
缓存击穿:由于某个缓存的时间过期,导致大量的请求访问了缓存不存在但是数据库却存在的数据,由于请求过大,从而导致数据库连接瘫痪。
解决办法:
1)将热点数据设置为永不过期
2)加互斥锁,若是发现缓存数据不存在,则获得锁,从数据库获取数据,然更新缓存之后释放锁并返回数据,未获取所锁的线程先等待,再查询缓存,若是存在正常返回,若是不存在继续尝试获取锁。最好是细化加锁的颗粒,只对key加锁,这样就可以操作的key值不同就可以避免等待了。
缓存雪崩:大面积的缓存在同一时间段内过期,导致大面积的请求只能直接访问数据库,从而引起数据库连接瘫痪
解决办法:
在redis方:缓存的失效时间设置为随机值,避免同时失效
在系统内: 要做降级,熔断,限流等处理。
8.5redis实现分布式锁
使用Redisson实现分布式锁。
原理:
加锁:
如果面对的是一个redis集群的话,就是通过hash节点去选择一个redis机器进行加锁步骤,主要是发送一段lua脚本。
脚本步骤:(假设加锁的对象是mylock)
1)线程A使用“exists mylock”判断一下在redis中这个mylock的key存不存在,如果不存在则使用hset命令创建此key,并设置value为一个随机数(这个数字每个线程是唯一的),所的数量为1;并且给这个key设置一个超时间,默认为30s
这个hset命令执行之后的redis数据结构如下
mylock:
{
"8af585x2c8f9x2a5sd8s52g4d83a:1":1
}
2)如果加锁完成完成之后。线程B又跑过来加锁,先使用“exists mylock“ 判断mylock存不存在,由于上一个线程已经完成加锁,所以这个锁已经存在,那么就会判断已经存在mylock的key所对应的hash的数据结构中的随机数是不是线程B的随机数,显然不是,那么线程B则加锁失败,就会返回线程A加锁的过期时间,还剩多少。
线程B拿到剩余过期时间,就会进入一个while的循环,不停的尝试获取锁。
可重入锁:
线程A对同一个锁加了多次,先判断了锁已经存在,然后判断这个锁对应的hash值就是线程A的,那么对这个hash值进行+1操作。
此时数据结构就会变成:
mylock:
{
"8af585x2c8f9x2a5sd8s52g4d83a:1":2
}
watch dog自动延期机制
线程A在拿到锁之后,reids会自动一起一个看门狗的线程,每隔10s中就会检查一下锁有没有到期,如果线程A的锁即将到期,但是线程A还需要继续持有锁,那么看门狗就会把锁的过期时间延长
释放锁
redis释放锁。就是找到对应的锁不停的对hash值进行减一操作,直到hash值为0,那么就可以删除此锁,执行“del myLock”命令,从redis里删除这个key。同时线程B就会尝试获取锁成功,进行接下来的业务操作。
问题
如果在redis加锁和释放锁的过程中,由于redis 的主从复制集群,redis的主节点一旦宕机,那么就会出现,redis从节点变成了主节点,此时恰好第二个线程进来获取锁,那么就会造成redis 的各个节点都可能获取同样的锁,就会造成业务上的脏数据产生。
9.volatile是什么?
volatile是java虚拟机提供的轻量级的同步机制
特点:保证可见性,不保证原子性,禁止指令重排
jvm中的线程在执行修改变量时将共享变量拷贝至线程自身的工作区完成,但是由于多个线程可能一起操作同一个变量所以导致可能某一个线程修改的共享变量值但是其他线程并不能及时获取。
不保证原子性的解决方案:
第一 :Synchronize关键字
第二: atom的包装类
指令重排可能导致编写的代码执行顺序发生变化从而导致某些执行步骤提前执行,使得一些值发生变化。所以需要volatile需要来修饰变量避免指令重排。
volatile 使用场景:
单例模式:DCL(双端检索机制)+volatile 指令重排,防止单例对象已经分配完毕了空间但是并没有实例化,从而导致获取的单例为null
10、CAS是什么?
CAS就是比较并交换,compareAndSwap,是一个CPU的原语。例如线程需要修改某一个值的时候会获取值的副本到工作内存中进行修改,在回写到主内存的时候先会把之前的值的作比较,如果不正确则回写失败,如果正确则修改值。
cas原理:自旋锁和unsafe
unsafe:是jdk中rt.jar中的sun.misc类,通过unsafe获取对象的在内存的地址,直接操作内存中的对象。直接调用的操作系统的底层资源。unsafe类的操作是可以保证原子性的,无法被其他线程中断。
CAS例子:
AtomicIntger 中的 getAndIncrement方法,就是调用unsafe类中的getAndAddInt用来保证原子性,getAndAddInt方法中使用自旋锁,用来保证修改的值在回写之前没有被操作,否则一直循环修改,直到成功为止。
cas的缺点:
1.循环时间可能过长,开销过大。
2.只能保证一个共享变量的原子性。
3.ABA问题
11、CAS所引发的ABA问题
ABA问题:当执行CAS算法时可能出现A线程获取共享变量1,B线程也获取了共享变量1,但是有用A线程执行速度快,而B线程慢,A线程先将其改为2,之后又改回了1,当B线程执行的时候发现没有变化,进而修改成功,但是这个过程中的值是被修改过的,这就叫做ABA问题,是由于CAS获取的是某一时刻的值,比较的是当前时刻的值,但是这个过程中的时间差无法控制。
原子引用,可以使用AtomicReference对指定类进行包装应用。例如:AtomicReference<类>
ABA问题解决方案:
使用原子引用时间戳AtomicStampedReference(带有时间戳的原子引用)
简单来说就是,在原子引用的基础之上添加时间戳标识,每修改一次变量值,同时修改时间戳,当其他线程进行修改的时候,不止要判断变量值是否相同,也需要判断时间戳。
12、集合类不安全问题
1 ArrayList的安全问题
问题: java.util.ConcurrentModificationException(List并发修改异常)
原因: ArrayList在提升效率的同时,没有对Arrlist的操作加锁,当多线程进行操作是就会报错。
解决方案:
1、使用vector
2、使用Collections工具类
3、使用CopyOnwriteArrayList(思想:写时复制,在往list中添加数据的时候,不是直接给list添加,而是复制出来一份新的list,将元素写在新list的最后面,完成之后,用新list将之前的list 替换。这个过程中用lock来控制,也是一种读写分离的思想,读和写是不同的容器)
2 set的安全问题
问题: java.util.ConcurrentModificationException(set并发修改异常)
解决方案:
1、使用Collections工具类
2、使用CopyOnwriteset(底层其实是创建一个CopyOnwriteArrayList)
ps:hashset的底层其实是由hashmap,而hashset的add方法就是调用的hashmap的put的方法,key值为set的实际添加值,value为统一的present(object类型)的定值,对key进行hash就保证值不会重复。
3map的安全问题
问题: java.util.ConcurrentModificationException(map并发修改异常)
解决方案:
1、使用Collections工具类
2、ConcurrentHashMap
ps:hashmap底层原理:如图所示
13、公平锁和非公平锁
实例:
Lock lock = new ReentrantLock();默认是一个非公平锁,Lock lock = new ReentrantLock(true)是一个公平锁。
公平锁:按照线程到达的顺序,按次序获取锁。
非公平锁:在高并发的情况下,可能出现线程优先级的反转。不一定按照线程到达顺序执行,先会尝试占用锁,如果成功占用锁,就先执行,否则按照公平锁的方式执行。
Synchronized是一种非公平锁。
14、可重入锁(递归锁)
可重入锁:同一线程外层获取锁之后,内层递归函数仍可获取该锁的代码
实例:ReentrantLock ,Synchronized都为可重入锁
可重入锁最大作用就是避免死锁
15、自旋锁
自旋锁:不会产生阻塞队列,利用循环的方式进行访问,可以避免线程的上下文切换的消耗,缺点循环比较消耗CPU。
实例:
public class TbMenuController {
AtomicReference<Thread> ar= new AtomicReference<Thread>();
private void mylock (){
Thread t= Thread.currentThread();
while (!ar.compareAndSet(null,t)){
}
System.out.println("獲取");
}
private void unlock(){
Thread t= Thread.currentThread();
ar.compareAndSet(t,null);
System.out.println("釋放");
}
public static void main(String[] args) {
TbMenuController tc = new TbMenuController();
new Thread(()->{
try {
tc.mylock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"第一綫程");
}finally {
tc.unlock();
}
tc.mylock();
},"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
try {
tc.mylock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"第二綫程");
}finally {
tc.unlock();
}
},"t2").start();
}
}
16、独占锁(写锁)和共享锁(读锁)
共享锁(读锁):多个线程一起读取某一数据,读取的操作是可以共享的
独占锁(写锁):若是有一个线程要修改某一个数据,对于操作来说,其他线程的操作是不允许进行的。
解决方案:
使用ReentrantReadWriteLock
public class TbMenuController {
private volatile Map<Integer,String> map = new HashMap<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void put(Integer key,String value){
lock.writeLock().lock();
System.out.println(key+"写入开始");
map.put(key,value);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(key+"写入完成");
lock.writeLock().unlock();
}
public void get(Integer key){
lock.readLock().lock();
System.out.println(key+"读取开始");
String value = map.get(key);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(key+"读取完成:"+value);
lock.readLock().unlock();
}
public static void main(String[] args) {
TbMenuController tc = new TbMenuController();
for(int i=1 ;i<=5;i++) {
int s = i;
new Thread(() -> {
tc.put(s, s+"");
}, i + "").start();
}
for(int i=1 ;i<=5;i++) {
int s = i;
new Thread(() -> {
tc.get(s);
}, i + "").start();
}
}
}
17、CountDownLatch
CountDownLatch:使得调用await的线程阻塞,直到其他线程全部执行完毕之后(每一个线程执行完毕的时候调用countdown方法直到减到0,表示所有线程执行完毕),这时主线程才会被唤醒执行。
public class TbMenuController {
public void closeDoor(String i){
System.out.println(i+"离开教室");
}
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(5);
TbMenuController tc = new TbMenuController();
for(int i=1 ;i<=5;i++) {
int s = i;
new Thread(() -> {
tc.closeDoor(s+"");
countDownLatch.countDown();
}, i + "").start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("班长关门");
}
}
18、CyclicBarrier
与CountDownLatch相反,当数值从0 达到某一预设值的时候才会执行后面操作。
实例:
public static void main(String[] args) {
CyclicBarrier cb= new CyclicBarrier(5,()->{System.out.println("人到齐了,正式开会");});
for(int i=1 ;i<=5;i++) {
int s = i;
new Thread(() -> {
try {
System.out.println("第"+s+"个人到了");
cb.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, i + "").start();
}
}
19、Semaphore
Semaphore :是一个信号灯,多个线程同时抢占多个资源,资源一旦被占满,则多余的线程阻塞,直到资源被释放后,剩余的线程才会一个一个使用资源。
实例:
public static void main(String[] args) {
Semaphore sp= new Semaphore(3);
for(int i=1 ;i<=7;i++) {
int s = i;
new Thread(() -> {
try {
sp.acquire();
System.out.println("第"+s+"个人到了");
TimeUnit.SECONDS.sleep(2);
System.out.println("第"+s+"个人走了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
sp.release();
}
}, i + "").start();
}
}
20、Spring和SpringMVC,MyBatis以及SpringBoot的注解分别有哪些?SpringMVC的工作原理,SpringBoot框架的优点,MyBatis框架的优点
22、设计模式
22.1、 观察者模式(发布-订阅的关系),行为模式
观察模式:是一个一对多的依赖关系,当被观察的对象发生变化时,所对应的观察者对此变化会做出相应的改变。
优点:降低观察者和被观察者的耦合度,是一种抽象的耦合,建立了一套触发机制。
缺点:如果观察者过多的话,会花费更多的时间。观察者和被观察者之间不能存在循环依赖,否则ui系统崩溃。观察者只是知道了目标发生了变化,不知道为什么发生了变了。
22.2、责任链模式(职责链),行为模式
责任链模式:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
责任链模式是一种对象行为型模式,其主要优点如下。
降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
其主要缺点如下。
不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
23、Synchronized和lock的区别,lock有什么好处?
1、Synchronized是关键字,Synchronized底层是由minitorenter和minitorexit组成,而lock是一个具体类
2、Synchronized可以自己释放锁,而lock需要手动释放。
3、Synchronized不可中断,而lock可以中断,在trylock代码块和lockintteruptibly中使用interrupt()中断。
4、Synchronized是非公平锁,而lock默认为非公平锁,但可以设置成公平锁
5、Synchronized只能唤醒全部线程或者随机一个,而lock可以利用condition分组唤醒。
举例:A->B->C->A
class Person{
private int age =10;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
public void print5(){
lock.lock();
try {
while(age!=10){
condition1.await();
}
age=11;
for (int i= 1;i<5;i++){
System.out.println(Thread.currentThread().getName()+" "+age);
}
condition2.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10(){
lock.lock();
try {
while(age !=11){
condition2.await();
}
age = 12;
for (int i= 1;i<10;i++){
System.out.println(Thread.currentThread().getName()+" "+age);
}
condition3.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15(){
lock.lock();
try {
while(age !=12){
condition3.await();
}
age = 10;
for (int i= 1;i<15;i++){
System.out.println(Thread.currentThread().getName()+" "+age);
}
condition1.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class PCDeom {
public static void main(String[] args) {
Person p = new Person();
for (int i=1;i<=3;i++){
new Thread(()->{
p.print5();
},"AA").start();
}
for (int i=1;i<=3;i++){
new Thread(()->{
p.print10();
},"BB").start();
}
for (int i=1;i<=3;i++){
new Thread(()->{
p.print15();
},"CC").start();
}
}
}
24、阻塞队列(BlockingQueue)
当队列为空的时候,获取队列元素的线程将会被阻塞。
当队列为满的时候,向队列插入元素的线程将会被阻塞
阻塞队列的实现类(3种常用):
1. ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。
ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue;按照实现原理来分析,ArrayBlockingQueue完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea之所以没这样去做,也许是因为ArrayBlockingQueue的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue和LinkedBlockingQueue间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别。而在创建ArrayBlockingQueue时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
2.LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了
3. SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式的区别:
如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
阻塞队列核心方法:
阻塞队列的场景就是生产者消费者模式
传统的生产者消费者是借助Sync和lock实现的
传统生产者消费者模式实例(lock)
class Person{
private int age =10;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increasAge(){
lock.lock();
try {
while(age!=10){
condition.await();
}
age++;
System.out.println(Thread.currentThread().getName()+" "+age);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void decreasAge(){
lock.lock();
try {
while(age ==10){
condition.await();
}
age--;
System.out.println(Thread.currentThread().getName()+" "+age);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class PCDeom {
public static void main(String[] args) {
Person p = new Person();
for (int i=1;i<=5;i++){
new Thread(()->{
p.increasAge();
},"AA").start();
}
for (int i=1;i<=5;i++){
new Thread(()->{
p.decreasAge();
},"BB").start();
}
}
}
使用阻塞队列实现消费者生产者模式:
实例:
class Person{
private volatile boolean flag = true;
private AtomicInteger atomicInteger = new AtomicInteger();
private BlockingQueue<String> queue ;
public Person(BlockingQueue<String> queue) {
this.queue = queue;
}
public void product(){
String data = null;
boolean f = false;
while (flag){
data = atomicInteger.incrementAndGet()+"";
try {
f = queue.offer(data,2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(f){
System.out.println(Thread.currentThread().getName()+"插入成功"+data);
}else{
System.out.println(Thread.currentThread().getName()+"插入失败"+data);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"生产者停止");
}
public void consumer(){
String data = null;
while (flag){
try {
data= queue.poll(2,TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(data==null||data.equalsIgnoreCase("")){
flag = false;
System.out.println(Thread.currentThread().getName()+"获取失败");
return;
}
System.out.println(Thread.currentThread().getName()+"获取成功"+data);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"消费者停止");
}
public void stop(){
flag = false;
}
}
public class PCDeom {
public static void main(String[] args) {
Person p = new Person(new ArrayBlockingQueue<>(10));
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"生产者开始");
p.product();
},"pro").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"消费者开始");
p.consumer();
},"con").start();
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.stop();
}
}
25、线程
线程的五种状态:创建、就绪、运行、阻塞和死亡
Java中创建线程主要有三种方式:
一、继承Thread类创建线程类
二、通过Runnable接口创建线程类
三、通过Callable和Future创建线程(创建Callable的实现并且重写call()方法,并具有返回值,Callable实现类要借助FutureTask类进行包装,才能进行初始化线程。此处使用了适配器模式)
26、线程池
JDK自带线程池类型:
1.newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。
2、newFixedThreadPool
创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
3、newSingleThreadExecutor
创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
4、newScheduleThreadPool
创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
5、newSingleThreadScheduledExecutor
创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行并且可定时或者延迟执行线程活动。
线程池参数说明:
一、corePoolSize 线程池核心线程大小
线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut。这里的最小线程数量即是corePoolSize。
二、maximumPoolSize 线程池最大线程数量
一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行,如果没有则会缓存到工作队列(后面会介绍)中,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。
三、keepAliveTime 空闲线程存活时间
一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁,这里的指定时间由keepAliveTime来设定
四、unit 空闲线程存活时间单位
keepAliveTime的计量单位
五、workQueue 工作队列
新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列:
六、threadFactory 线程工厂
创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等等
七、handler 拒绝策略
当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,该如何处理呢。这里的拒绝策略,就是解决这个问题的,jdk中提供了4中拒绝策略:
①CallerRunsPolicy
该策略下,在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务。
②AbortPolicy
该策略下,直接丢弃任务,并抛出RejectedExecutionException异常。
③DiscardPolicy
该策略下,直接丢弃任务,什么都不做。
④DiscardOldestPolicy
该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
重点重点!!!!!
一般项目中都不会使用以上线程池,因为都有缺陷。一般会手写改造线程池。
手写改造线程池:
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(2,5,2,TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
try {
for (int i=1;i<=20;i++){
executor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t"+"业务办理");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executor.shutdown();
}
}
27、死锁
为什么会产生死锁?
两个或者两个以上的线程在执行过程中,因为争夺资源而相互等待的过程,并且非外力介入否则无法进行下去。
死锁的实例:
class myThread implements Runnable{
private String lockA;
private String lockB;
public myThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"\t"+"占有锁"+lockA);
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"\t"+"等待获取"+lockB);
}
}
}
}
public class Test {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new myThread(lockA,lockB),"线程A").start();
new Thread(new myThread(lockB,lockA),"线程B").start();
}
}
死锁的排查方法:
使用JDK命令 jps查询java线程,使用jstack命令查询线程运行的堆栈详情。
死锁日志:
28、JVM
(1)jvm如何确定垃圾?什么是GcRoots
从gcroots的对象进行对象可达性分析,如果对象可达,则判定为对象存活,如果对象不可达,则对象需要回收;gcRoots是一个对象集合(可作为gcRoots的对象的有:1.栈中的局部变量的对象引用;2.方法区中的静态变量对象引用;3.方法区中的常量对象应用;4.本地方法栈中native的对象引用)
(2)jvm的参数类型有哪些?
标配参数,-X参数,-XX参数
-XX参数:
PS:可以使用jps配合jinfo 命令可以用来查看 Java 进程运行的 JVM 参数(jsp -l , jinfo -flag PrintfGCDetail 进程号 )
1.boolean类型,例如:-XX:-PrintfGCDetail(表示关闭GC详情);-XX:+PrintfGCDetail(表示开启GC详情)
2.KV类型,
(3)如何查询jvm的默认参数?
java -XX: +printFlagsInitial(查询jvm的默认参数配置)
java -XX: +printFlagsFinal(查询最终jvm的参数,包含修改过的参数值)
java -XX: +printCommandLineFlags (主要用于查询此次执行的是哪种垃圾回收器)
(4)jvm的相关参数
xms:初始化堆内存,内存的64分之一
xmx:最大堆内存,内存的4分之一
xss:线程运行时的栈大小,一般默认为512k~1024k之间,具体默认值与平台有关。
xmn:年轻的的大小,默认是对内存的三分之一
XX:MetaspaceSize:默认元空间的大小是20M.
XX:PrintfGCDetail :打印GC详情
XX:SurvivorRatio:设置新生代中Eden与form和to的所占比例(默认为8:1:1),修改Edne的所占比例,form和to都是1:1(例如:- XX:SurvivorRatio:4就是4:1:1)。
XX:NewRatio:设置新生代和老年代的占比(默认是:1:2),设置XX:NewRatio:4(代表1:4)。
XX:MaxTenuringThreshloud:设置垃圾的最大年龄(最大为15,默认是15)
29、强引用,软引用,弱引用,虚引用、引用队列
强引用:
1、当内存不足时 jvm启用垃圾回收机制,但是对于强引用对象,只要引用还在就不会被回收,哪怕是出现OOM也不会回收。
2、造成内存泄漏的主要原因
3、只有当对象超过的自身作用域或者为null 了才可回收。
软引用:
1、当内存足够时,只要引用还在就不会被回收。
2、当内存不足够时,就会被回收。
弱引用(weakHashMap):
1、主要出现了GC,不论内存是否够用,都会将此对象回收。
虚引用:
1、它不决定对象的生命周期,会在任何收被回收,而且他的引用 形同虚设,没有任何实际意义,但是在回收之前可以做一些通知工作,一般搭配引用队列(ReferenceQueue)使用,Gc回收之后就会把对象放在引用队列当中。
30、Nginx的高可用
nginx借助keepalived完成高可用
1:主从关系
2:生成一样的虚拟ip
3:设置对应的优先级,初始设置主机(master)的优先级高于从机(backup),每次请求访问时,选取组内的优先级最高的机器。
4:主从机器上都配置检查脚本,keepalied定时检查主从机的运行状态,如果某一台机器挂了,那么就会减少对应的优先级。
31、MQ
31.1、MQ的作用。
1)异步
假如开始的业务场景是A系统->B系统->C系统,这个业务流程是同步的,如果各个系统之间的响应速度过慢,那么就用导致这个请求响应过慢甚至异常,而MQ出现就是生产一个消息,然后让A,B,C三个系统同时来做这件事,将同步机制转化为异步就会减少系统的响应时间,提供用户的体验。
2)解耦
假如,系统B,C,D,都需要系统的A所提供的数据,那么系统A就必须通过API的当时给B,C,D提供数据,如果后续又来了新的系统,那么系统的A的耦合度一下子就上来了,此时系统A只需要向MQ生产一条消息,通知其他的系统来消费即可,降低了系统之间耦合度,实现了解耦。
3)削峰
在高并发下可以让并发的访问在队列内部变成串行,实现了限流访问,降低了系统压力。
31.2、 MQ的优缺点
优点
1)异步
2)解耦
3)削峰
缺点
1)系统的可用性降低了(万一MQ挂了怎么办)
2)系统的复杂度很高(消息的消费顺序,怎么避免重复消费)
3)一致性问题(A处理完毕之后正常返回,万一后面的挂了呢)
31.3、如何避免消息的重复消费
1、在消息生产方,生产消息时,给消息体添加一个Id保证消息不会重复生产,也避免队列中出现重复的消息。
2、在消息消费方,要求消息中存在一个唯一ID,避免消息被重复消费。
31.4、如何确定消息的可靠性传输?或者说,如何确保生产者生产了消息安全到达队列,如何确保消费者正常消费消息,如何确保消息在队列中不丢失?
1)生产者:启用生产者confirm模式,在发送消息时向MQ会携带一个Id,如果MQ收到消息,则回复确认ack,没有收到则恢复neck。
2)队列中:对MQ做持久化操作,将队列中的信息存入磁盘,保证队列中消息不会丢失
3)消费者:关闭队列的自动确认,启动手动确认,只有当消费者成功消费之后,才会向MQ发送确认ack,MQ才会删除此消息。
31.5、如何保证消息的顺序执行?
可以将多个消息使用不同的队列进行存放,使用不同的消费者来进行消费,然后都这些消费者使用队列排序执行即可。
31.6、RabbitMq消息怎么路由?
RabbitMq不会将消息直接存放至队列,在消息生产时,会产生一个路由键,而队列也会有一个路由键并且绑定在交换机上,所以当消息到达交换机时消息的路由键就和歌队列的路由键进行匹配,然后完成入队操作。
31.7、MQ集群
镜像集群:
所有的mq的节点都会保存一份一摸一样的队列信息,这就保证了某一个节点挂了之后,也不会影响系统的使用,因为其他节点都有所有的队列信息。但是这就要求每次生产消息的时候都要同步到各个节点上去,资源消耗非常大,而且每次创建一个新的队列则系统同步每一个节点,扩展新很差。
**综上所述MQ有MQ的优势,但同样会带来一些不可避免的问题,需要代码的介入和一些方案来对应实现。
32、mybatis
32.1、#{}和${}的区别是什么?
1.#{}代表的是预编译;KaTeX parse error: Expected 'EOF', got '#' at position 14: {}代表的是字符替换 2.#̲{}会使用perparemen…{}只是做了字符串替换。
32.2、mybatis一级缓存和二级缓存
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存.使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。
32.3mybatis 的工作原理
Mapper 接口的工作原理是 JDK 动态代理.
Mybatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法.转而 执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回.
33、Mysql
33.1Mysql的锁
(1)表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最 高,并发度最低。
(2)行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最 低,并发度也最高。
(3)页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表 锁和行锁之间,并发度一般。
33.2Mysql引擎
MyISAM:
(1)不支持事务,但是每次查询都是原子的;
(2)支持表级锁,即每次操作是对整个表加锁;
(3)存储表的总行数;
(4)一个 MYISAM 表有三个文件:索引文件、表结构文件、数据文件;
(5)采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯一性。
InnoDb:
(1)支持 ACID 的事务,支持事务的四种隔离级别;
(2)支持行级锁及外键约束:因此可以支持写并发;
(3)不存储总行数:
(4)一个 InnoDb 引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为 2G),受操作系统文件大小的限制;
(5)主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值;因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引;最好使用自增主键,防止插入数据时,为维持 B+树结构,文件的大调整。
33.3 数据库优化策略
最好是按照以下顺序优化:
(1)SQL 语句及索引的优化
(2)数据库表结构的优化
(3)系统配置的优化
(4)硬件的优化
方法:
(1)选取最适用的字段属性,尽可能减少定义字段宽度,尽量把字段设置 NOTNULL,例如’省份’、’性别’最好适用 ENUM
(2)使用连接(JOIN)来代替子查询
(3)适用联合(UNION)来代替手动创建的临时表
(4)事务处理
(5)锁定表、优化事务处理
(6)适用外键,优化锁定表
(7)建立索引
(8)优化查询语句
举例:
MySQL 数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?
(1)设计良好的数据库结构,允许部分数据冗余,尽量避免 join 查询,提高效率。
(2)选择合适的表字段数据类型和存储引擎,适当的添加索引。
(3)MySQL 库主从读写分离。
(4)找规律分表,减少单表中的数据量提高查询速度。
(5)添加缓存机制,比如 memcached,apc 等。
(6)不经常改动的页面,生成静态页面。
(7)书写高效率的 SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE.
33.4、锁的优化策略
(1)读写分离
(2)分段加锁
(3)减少锁持有的时间
(4)多个线程尽量以相同的顺序去获取资源
不能将锁的粒度过于细化,不然可能会出现线程的加锁和释放次数过多,反而效率不如一次加一把大锁。
33.5Mysql的InnoDB的mvcc的工作原理
mvcc只存在与事务隔离性为 读已提交和可重复读中使用,因为版本控制保证的是数据提交之后的版本,读未提交不能保证,而串行化是表级锁,不存在这种情况。
mvcc使用的是两个字段,一个是create_version版本id和delete_version还有undo表。
举例:
当某一事务创建一条记录时,它的create_version为1
更新此数据是原纪录就会变成create_version=1,delete_version=2,新纪录create_version=2,新记录与原纪录有关联的。
当删除此数据的时候就会删除create_version=2的记录,把delete_version=3,但是事务2要是来查询的时候是可以查询到这条记录的。
33.6mysql的索引
1、mysql索引具体采用的哪种数据结构?两者的区别?
1)B+树
2)hash索引
区别:
hash索引的底层就是哈希表,k-v键值的存储方式,所以它的存储是无序的,只有当等值查询的时候hash索引的响应速度快,但是一旦是范围查询,那么就需要扫描整个索引,之后才能返回值。
b+树,是一种多路平衡查询树,它的左节点小于父节点,父节点小于右节点,所以对范围查找不需要扫描整棵树。
哈希索引适合等值查询,但是无法进行范围查询
哈希索引没办法利用索引完成排序
哈希索引不支持多列联合索引的最左匹配规则
如果有大量重复键值的情况下,哈希索引的效率会很低,因为存在哈希碰撞问题
B+树更适合做存储索引:
1 B+树磁盘读写代价低
2 B+树查询效率稳定
3 B+树有利于对数据库扫描
2、b+树的叶子节点可以存储什么?区别?为什么?
1)存储主键值(称为非聚簇索引,非主键索引)
2)可以存储整行数据(称为聚簇索引,主键索引)
区别:
聚簇索引会查询的比较快,因为在一次查询中聚簇索引会直接返回行数据;
而非聚簇索引则需要拿到主键值进行二次查询。但是不是所有的情况都是如此,可以通过索引覆盖完成一次查询
覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。
比如一个查询只查询非聚簇索引中的值此时就不需要回表。
3、联合索引,最左前缀匹配
当创建联合索引的时候需要注意,需要将频繁使用的字段放在最前面,因为在检索的时候是从往右一次匹配的。
33.7索引失效
1、like 以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效。
2、or语句前后没有同时使用索引。当or左右查询字段只有一个是索引,该索引失效,只有当or左右查询字段均为索引时,才会生效
3、组合索引,不是使用第一列索引,索引失效。
4、数据类型出现隐式转化。如varchar不加单引号的话可能会自动转换为int型,使索引无效,产生全表扫描。
5、在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。
6、对索引字段进行计算操作、字段上使用函数。
查看索引失效命令
可以使用explain命令加在要分析的sql语句前面,在执行结果中查看key这一列的值,如果为NULL,说明没有使用索引
33.8 explain 命令
id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.
select_type: SELECT 查询的类型.
table: 查询的是哪个表
partitions: 匹配的分区
type: join 类型
possible_keys: 此次查询中可能选用的索引
key: 此次查询中确切使用到的索引.
ref: 哪个字段或常数与 key 一起被使用
rows: 显示此查询一共扫描了多少行. 这个是一个估计值.
filtered: 表示此查询条件所过滤的数据的百分比
extra: 额外的信息
type: all(全表) ,index(索引),range(索引范围),ref(join),eq_ref(join的结果时一一对应的),const(主键或者唯一索引查询返回一条数据),system(表中只有一条数据)
33.9sql优化
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2.应尽量避免索引失效
3.尽量避免大事务操作,提高系统并发能力。
34、AQS
AQS是抽象队列同步器,在Reentrantlock,semophere,countDownLatch等等类中都有作用。
主要是用来控制管理线程阻塞,线程唤醒等操作。
主要使用队列和int类型的状态值来控制。等待线程将以node形式进入队列,排队等待。
举例:Reentanctlock中的lock和unlock方法。A-》B-》C三个线程
lock方法开始,A线程进入lock方法,调用尝试抢占资源方法,发现资源没有被占用 state状态值为0,所以A线程抢占资源,将state置为1,接着B线程进入lock方法,发现资源已经被占用,则调用排队方法,进入排队方法之后,再次尝试去抢占资源,发现资源被占用,调用,写入队列的操作,将自身封装成为一个node节点(包含前驱指向,后驱指向,当前线程,以及状态值),将自己状态值设置为0,设置当前node节点中的线程为自己,返回此node节点,然后调用入队方法,先判断队列中是否有线程排队,若没有,则先生成一个哨兵节点,设置哨兵节点的当前线程为null状态值为0,将头指针指向哨兵节点,尾指针也指向哨兵节点,由于此处是自旋锁,所以第二次进入之后判断已经有了哨兵节点,那么再次调用抢占资源的方法,发现资源依旧被占用则进行入队操作,将B线程的node节点的前驱节点设置为哨兵节点,将哨兵节点的后驱节点设置为B线程的node,将尾指针指向B线程的node,然后调用locksupport的park方法阻塞线程,让线程等待被唤醒。
此时C线程也进入lock,与B线程不同的是,C线程入队时不需要生成哨兵节点,因为已经有了哨兵节点,所以直接阻塞入队即可。
unlock方法:
A线程调用unlock方法释放资源,将状态值设置为0,调用方法之后将哨兵节点的状态值更新为-1,哨兵节点将自己节点的线程更新为后置node线程,将后置节点的线程置为null,把头指针指向哨兵节点的后置节点,哨兵节点出队,原第二个节点变成了哨兵节点,A线程调用locksupport的unpark方法,唤醒出队线程。
35、基础
1)抽象类和普通类的区别
抽象类不能被实例化,普通类可以
抽象类继承之后必须重写抽象方法,普通类继承之后可以不用重写方法。
2)抽象类和接口的区别
- 两者表达的概念不一样。抽象类是一类事物的高度聚合,那么对于继承抽象类的子类来说,对于抽象类来说,属于“是”的关系;而接口是定义行为规范,因此对于实现接口的子类来说,相对于接口来说,是“行为需要按照接口来完成”。
- 抽象类在定义类型方法的时候,可以给出方法的实现部分,也可以不给出;而对于接口来说,其中所定义的方法都不能给出实现部分。
- 抽象是继承,接口是实现
3)HashMap和Hashtable的区别
相同点:
实现原理相同,功能相同,底层都是哈希表结构,查询速度快,在很多情况下可以互用
不同点:
- Hashtable是早期提供的接口,HashMap是新版JDK提供的接口。
- Hashtable继承Dictionary类,HashMap实现Map接口。
- Hashtable线程安全,HashMap线程非安全。
- Hashtable不允许null值,HashMap允许null值。
4)Array和ArrayList之间的区别
- Array类型的变量在声明的同时必须进行实例化(至少得初始化数组的大小),而ArrayList可以只是先声明。
- Array只能存储同构的对象,而ArrayList可以存储异构的对象。
- Array是始终是连续存放的,而ArrayList的存放不一定连续。
- Array对象的初始化必须只定指定大小,且创建后的数组大小是固定的,而ArrayList的大小可以动态指定,其大小可以在初始化时指定,也可以不指定,也就是说该对象的空间可以任意增加。
- Array不能够随意添加和删除其中的项,而ArrayList可以在任意位置插入和删除项。
36、网络
36.1tcp三次握手四次挥手
tcp三次握手:
第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
四次挥手:
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
36.2、为什么连接的时候是三次握手,关闭的时候却是四次握手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
37、Spring的AOP
38、spring的循环依赖
依赖注入的方式:构造器注入 和 set方法注入
构造器注入的循环依赖问题无法解决。
spring在解决单例模式下的循环依赖时,使用了DefaultSingletonBeanRegistry,具体是使用了三级缓存也就是三个map来解决的。
第一级缓存 singletonObjects 存放初始化好的bean
第二级缓存earlySingletonObjects存放已经实例化,但并没有初始化的bean
第三级缓存singletonFactories存放的是FactoryBean。假如A类实现了FactoryBean,那么依赖注入的不是A类,而是A类所产生的bean。