文章目录
- 1. Java
- 1.1. TCP三次握手/四次挥手
- 1.2 HashMap底层原理
- 1.3 Java常见IO模型
- 1.4 线程与线程池工作原理
- 1.5 讲一讲ThreadLocal、Synchronized、volatile底层原理
- 1.6 了解AQS底层原理吗
- 2. MySQL
- 2.1 MySQL索引为何不采用红黑树,而选择B+树
- 2.2 MySQL索引为何不采用B树,而选择B+树
- 2.3 数据库隔离级别
- 2.4 MySQL数据库引擎以及优缺点?
- 2.5 索引失效情况
- 2.6 MySQL数据库性能下降、慢的原因
- 2.7 SQL查询慢分析过程
- 3. JVM
1. Java
1.1. TCP三次握手/四次挥手
第一次握手:TCP客户端向服务器发出报文连接请求。该报文首部中SYN=1、初始序列号 seq=x 。请求发送后,**TCP客户端进程进入了 SYN-SENT(同步已发送状态)。**TCP规定SYN报文段(SYN=1的报文段)不能携带数据,因此需要消耗掉一个序号seq。
第二次握手:服务端收到连接请求报文段后,如果同意连接,则会发送一个应答消息确认报文,该确认报文中应该 SYN=1,ACK=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,TCP服务器进程进入了SYN-RCVD(同步收到)状态。
第三次握手:当客户端收到连接同意的应答后,还要向服务端发送一个确认报文。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,客户端发完这个报文段后便进入 ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时连接建立成功。
问题:TCP为何采用三次握手来建立连接?若采用二次/四次握手可以吗?
参考:https://blog.csdn.net/m0_67698950/article/details/127074121
1.三次握手才可以阻止历史连接重复初始化造成混乱(主要原因)
2.三次握手才可以同步双方的初始序列号
3.三次握手才可以避免资源浪费不使用「两次握手」和「四次握手」的原因:
「两次握手」:无法防止历史连接的重复初始化,无法同步双方序列号,会造成双方资源的浪费。
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
1.2 HashMap底层原理
HashMap是根据key的哈希值来存储数据,大多数情况下,我们可以根据key值直接定位到对应的value值,因而具有很快的访问速度,并且HashMap的key、value可以为空**(定义)。在同一时刻,可以有多个线程同时操作HashMap,由于其没有线程同步机制,因此会出现写值丢失、数据不一致的现象,HashMap是线程不安全的(线程安全性)**。在Java8之前,HashMap的底层是数组+链表,数组的初始容量是16,扩容增加为原来的2倍,在Java8之后,HashMap的底层是数组+链表+红黑树,数组的初始长度为16,当链表长度大于8,会将链表转为红黑树。
为什么HashMap是线程不安全的?
Java7线程不安全主要体现:扩容导致死循环
1.由于多线程对HashMap进行扩容,调用了HashMap#transfer(),导致线程死循环、数据丢失
2.具体原因:某个线程执行过程中被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成线程死循环、数据丢失。
Java8线程不安全体现在:put操作导致写值丢失、数据不一致
1. 假设线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的
2. 当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,
3. 然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
Java8之前是头插法,Java8之后为尾插法
如何解决?
HashTable
concurrentHashMap
Collections.synchronizedMap(mp);
ConcurrentHashMap底层原理
Java7: 底层采用分段数组+链表的方式,将整个桶数组分割为N段Segment,每一个Segment分配一把可重入锁ReentrantLock,允许多个线程并发操作。每次需要加锁的操作锁住的是一个segment,这样只要保证每个Segment是线程安全的,也就实现了全局的线程安全。【分段锁】
Java8: 摒弃了Segment的概念,而是直接用 Node数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。
过程:
1. 首先默认会初始化一个长度为16的数组。(由于ConcurrentHashMap它的核心仍然是hash表,所以必然会存在hash冲突问题,同样采用链式寻址法来解决hash冲突)。
2. 当数组长度大于64并且链表长度大于等于8的时候,单项链表就会转换为红黑树(同样一旦链表长度小于8,红黑树会退化成单向链表。)
ConcurrentHashMap本质上是一个HashMap,因此功能/原理和HashMap一样,但额外提供了并发安全的功能。
1. 总体来说,并发安全的主要实现是通过对指定的Node节点加分段锁来保证数据更新的安全性。
2. 详细来说,在JDK1.8中,ConcurrentHashMap锁的粒度是数组中的某一个节点,而在JDK1.7,锁定的是Segment,锁的范围要更大,因此性能上会更低。
1.3 Java常见IO模型
BIO同步阻塞I/O模型(Blocking I/O):用户空间的程序发起read调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。在客户端连接数量不高的情况下没有问题,但当面对十万甚至百万级别连接的时候,传统的BIO模型就无能为力了。
NIO同步非阻塞I/O模型(Non-blocking I/O):用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指令,即发起 IO 请求的用户进程处于非阻塞状态,与此同时,内核会立即返回给用户一个 IO 状态值。
阻塞是指用户进程一直在等待,而不能做别的事情;
非阻塞是指用户进程获得内核返回的IO状态值后,可以去做别的事情。
AIO异步IO(Asynchronous IO,AIO):异步IO是基于事件回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
总结(BIO,BIO,AIO有什么区别)
BIO(Blocking I/O:
实现模式为一个连接一个线程,客户端有连接请求时服务器就要启动一个线程进行处理,若这个连接不做任何操作还造成不必要的线程开销。BIO方式适合连接数目小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,在JDK1.4以前时唯一的IO。
NIO(NEW I/O):
实现模式为一个请求一个线程,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理,NIO方式适合用于连接数目多且连接时间短(轻操作)的架构,如聊天服务器,并发局限于应用中,编程复杂,JDK 1.4之后开始支持。
AIO(Asynchronous I/O):
实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成再通知服务器用其启动线程进处理,AIO适合用于连接数目多且连接比较长(重操作)的架构,如相册服务器,充分调用OS参与并发操作,编程复杂,JDK 1.7开始支持。
经典例子解析理解
BIO: 来到厨房,开始烧水NIO,并坐在水壶面前一直等待水烧开。
NIO: 来到厨房,开AIO烧水,但不坐在水壶面前一直等,而去做其他事,然后每隔几分钟到厨房看一下有没有烧开。
AIO: 来到厨房,开始烧水,不一直等待水烧开,而是坐在水壶上面装个开关,水烧开之后它会通知我们。
1.4 线程与线程池工作原理
- 线程状态
新建(New):
当程序使用 new 关键字、Thread 类或其子类建立一个线程对象后,该线程就处于新建状态。它保持这个状态直到程序 start() 这个线程。(new MyThread)
就绪(Runnable 或Ready to Run):
当线程对象调用了start()方法之后,该线程处于就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。(new MyThread.start() )
运行(Running):
如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。
阻塞 (Blocked):
如果一个线程执行了sleep、suspend等方法,失去CPU所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
等待阻塞: 运行状态的线程执行wait()方法,JVM 会把该线程放入等待队列中。PS:wait()释放锁
同步阻塞: 运行状态的线程在获取对象的同步锁时,线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)
其他阻塞:运行状态的线程执行 sleep()、join()、suspend()方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行状态(就绪状态)。
死亡(Dead):线程会以下面三种方式结束,结束后就是死亡状态。
1. 正常结束:run()或 call()方法执行完成,线程正常结束。
2. 异常结束:线程抛出一个未捕获的 Exception 或 Error。
3. 调用结束:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用
- 线程同步(4种方法)(volatile、synchronized、ReentrantLock、(wait、notify))
- 3种主要创建线程的方式
第一种方式:继承Thread类
实现步骤:
1.创建一个Thread类的子类 class MyThread extends Thread
2.在Thread类的子类中重写Thread类中的run()方法
3.创建Thread类的子类对象 MyThread t1=new MyThread();
4.调用Thread类中的方法start()方法,开启新的线程,执行run方法 t1.start()
第二种方式:实现Runnable接口(无返回值)、实现Callable接口(有返回值)
实现步骤:
1.创建一个Runnable接口的实现类 class myThread implements Runnable
2.在实现类中重写Runnable接口的run方法
3.创建一个Runnable接口的实现类对象 myThread t1=new myThread();
4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象Thread t=new Thread(t1);
5.调用Thread类中的start方法,开启新的线程执行run方法 t.start()
第三种方式:通过线程池方式创建
ExecutorService es= Executors.newCachedThreadPool(); //缓存型线程池
ExecutorService es= Executors .newFixedThreadPool(2); //固定型线程池
ExecutorService es=Executors.newSingleThreadExecutor(); //单线程型线程池
以上三种通过es.submit(new MyRunable03(i));提交任务
ScheduledExecutorService es = Executors.newScheduledThreadPool(3);//延迟执行或重复执行型线程池
通过es.schedule(new myRunnable(i), 2, TimeUnit.SECONDS);提交任务
- 常用线程池
Executors.newCachedThreadPool(); //缓存型线程池,通常用于执行一些生存期很短的异步型任务,
Executors.newFixedThreadPool(2); //固定型线程池,以共享的无界队列方式来运行这些线程。
Executors.newSingleThreadExecutor(); //单线程型线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
以上三种通过es.submit(new MyRunable03(i));提交任务
2、具体的2种延迟、定期重复执行任务的线程池:返回值是ScheduledExecutorService的线程池
1、Executors.newScheduledThreadPool(int n) // 创建一个定长延迟、定期线程池,支持定时及周期性任务执行
2、Executors.newSingleThreadScheduledExecutor() // 创建一个单线程、定期重复执行任务的线程池。
- 自定义线程池-七大参数
1:核心线程数(corePoolSize)
核心线程数的设计需要依据每个任务的处理时间和每秒产生的任务数量来确定。
例如:执行一个任务需要0.1秒,系统百分之 80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数剩下的百分之20可以利用最大线程数处理。
2:最大线程数(maximumPoolSize)
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间。
例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间;既最大线程数=(1000-200)*0.1=80个;
3:最大空闲时间(keepAliveTime)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;
4:最大空闲时间单位(TimeUnit)
5:阻塞队列(workQueue)
当线程个数大于核心线程数时,再提交的线程存放在任务队列中,任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可,这个任务队列是一个阻塞队列。
6:线程工厂(threadFactory): 用于创建线程,一般默认即可。
7:拒绝策略(handler):表示当等待队列满了并且工作线程大于等于线程池的最大线程数。常见拒绝策略:
AbortPolicy(默认): 直接抛出异常RejectedExecutionException,阻止系统正常运行
CallerRunsPolicy: “调用者优先策略”,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
DiscardPolicy: 直接丢弃任务,不做任何处理也不抛出异常。如果允许任务丢失,可以使用。
- 线程池工作流程
1、在创建了线程池之后,等待提交过来的任务请求
2、当调用execute()方法后添加一个请求任务
2.1 如果正在运行的线程数量小于corePoolSize,则立即执行该任务
2.2 如果正在运行的线程数量大于等于corePoolSize,则将该任务放进队列 workQueue
2.3 如果任务队列满了,并且正在运行的线程数量小于maximumPoolSize,那么线程池创建非核心线程执行任务
2.4 如果任务队列满了,并且正在运行的线程数量大于等于maximumPoolSize,那么线程池启动拒绝策略执行。
3、当一个线程完成任务时,它会从队列取下一个任务执行
4、当一个线程无事可做超过一定的时间keepAliveTime,线程池会判断
4.1 如果运行的线程数大于corePoolSize,那么会停掉该非核心线程
4.2 最后线程池的线程数目收缩到corePoolSize的大小
1.5 讲一讲ThreadLocal、Synchronized、volatile底层原理
ThreadLocal是线程本地存储(线程局部变量),作用是提供线程内的局部变量,这种变量 在该线程的生命周期内起作用,从而减少同一个线程内多个函数之间变量传递的复杂度。
ThreadLocal 变量通常被private static修饰,当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
底层实现:
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,因此ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。
ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
使用场景
1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
1)存储用户Session
2)解决线程安全的问题
比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题:
Synchronized可以将一个非空的对象当作锁,他属于可重入的、独占的悲观锁,
当作用于普通方法时,锁住的是对象的实例;初始化一个对象时,会自动生成一个与该对象对应的对象锁,被synchronized 修饰的方法就得依靠对象锁工作。
当作用于静态方法时,锁住的是Class的实例;被synchronized 修饰的静态方法要靠类锁工作。当多线程同时访问某个被synchronized修饰的静态方法时,一旦某个进程抢得该类的类锁之后,其他的进程只有排队对待。
当作用于一个对象实例时,锁住的是所有以该对象为锁的代码块;
synchronized(偏向锁、轻量级锁、重量级锁:锁优化过程)
Volatile(java高级编程.docx)轻量级的线程同步机制,volatile 于一个变量被多个线程共享的场景,并保证变量的原子操作。
总的来说,必须同时满足下面两个条件才能保证在并发环境的线程安全:
(1)对变量的写操作不依赖于当前值(比如 i++),或者说是单纯的变量赋值(boolean flag = true)。
(2)该变量没有包含在具有其他变量的不变式中,也就是说不同的 volatile 变量之间不 能互相依赖。只有在状态真正独立于程序内其他内容时才能使用 volatile。
问题一:synchronized和volatile区别
1、synchronized用来修饰成员方法,静态方法,代码块,而volatile只能用于修饰成员变量。
2、sychronized可以保证线程的原子性、可见性、有序性,而volatile能保证变量在多个线程之间的可见性、有序性,但不能保证原子性。
3、volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好
问题二:synchronized和ReentrantLock区别
1、Synchronized 是关键字,属于JVM层面,底层是通过 monitorenter 和 monitorexit 实现。ReentrantLock是一个接口,是 java.util.concurrent.locks.lock 包下的,是 API层面的锁。(底层实现)
2、synchronized在发生异常时,会自动释放线程占有的锁,因此不会发生死锁;
ReentrantLock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此需要在finally块中释放锁;(使用方法)
3、synchronized 是非公平锁,ReentrantLock 默认非公平锁,可以在构造方法传入 boolean 值,true 代表公平锁,false 代表非公平锁。(加锁是否公平)
1.6 了解AQS底层原理吗
AQS是什么?
AQS(AbstractQueueSynchronizer)叫抽象队列同步器,是一个抽象类。其内部定义了一套多线程访问共享资源的同步器框架, 诸如ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等工具底层都使用AQS去维护锁的获取与释放,也就是说AQS就是各种锁、线程同步组件的公共抽象部分。
AQS能干什么?
AQS提供了三个功能,实现独占锁、实现共享锁、实现Condition模型。
1、同步队列的管理和维护
2、同步状态的管理(volatile修饰的state,表示是否持有锁)
3、线程阻塞、唤醒的管理。
基本设计思路
1、首先把来竞争的线程及其等待状态,封装为Node对象
2、然后把这些Node对象放到一个同步队列中
(同步队列是一个FIFO的双端队列,是基于CLH队列实现的。在CLH同步队列中,一个节点表示一个线程,保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next))
3、AQS使用一个int volatile state来表示线程同步状态,比如是否有线程获得锁、锁的重入次数等等。具体的含义由具体的子类锁定义。
4、至于线程的唤醒和阻塞,伴随着同步队列的维护,使用LockSupport来实现对线程的唤醒和阻塞
2. MySQL
2.1 MySQL索引为何不采用红黑树,而选择B+树
1. 红黑树本质上是一个二叉树,当MySQL表数据都是比较大时,如果使用红黑树的高度会特别高,如果查询的数据正好在树的叶子节点,那查询会非常慢。
2. 而B+树不一定非得是二叉树,一般情况下,一个三层的B+树结构可存储上百万条数据,可以满足我们几乎所有的需求了。
2.2 MySQL索引为何不采用B树,而选择B+树
1. B+树非叶子节点不存储数据,只存储索引值,相比较B树来说,B+树可存储更多的索引值,使得整颗B+树变得更矮,减少磁盘I/O次数
2. B+树的叶结点构成一个有序链表,便于区间查找和搜索。而B树则需要进行每一层的递归遍历,相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
2.3 数据库隔离级别
脏读:对于两个事务T1、T2,T1读取了已经被T2更新但没有提交的字段,若T2回滚,T1读取的数据就会失效。(针对更新update)T1读—>T2更新未提交->T2回滚->T1失效(事务A读取到了事务B已修改但尚未提交的数据)
不可重复读:对于两个事务T1、T2,T1读取了一个字段,随后T2更新了该字段,之后T1再次读取该字段,出现前后数据不一致的现象。T1读—> T2更新提交-> T1读不一致(事务A读取到了事务B已经提交的修改数据,不符合隔离性)
幻读:对于两个事务T1、T2,T1从一个表中读取了一个字段,随后T2在该表中插入了新的数据,如果T1再次读取该表,就会多出几行数据。T1读—> T2插入-> T1读不一致(事务A读取到了事务B新增的数据,不符合隔离性)
2.4 MySQL数据库引擎以及优缺点?
2.5 索引失效情况
2.6 MySQL数据库性能下降、慢的原因
a、SQL查询语句写的烂
b、没有创建索引、索引失效(单值、复合)
c、关联太多的表Join(设计缺陷或不得已的需求,原则上越少越好)
d、服务器调优参数设置不合理(缓冲区大小、线程个数设置等)
2.7 SQL查询慢分析过程
1、观察,至少跑1天,看看生产的慢SQL情况
2、开启慢查询日志: 设置阙值,比如超过5秒钟的就是慢SQL,并将它抓取出来。
3、explain+慢SQL: 分析慢SQL查询语句的性能状况(以上完成80%优化)
1、单表优化:(建立索引并合理使用,避免索引失效)
2、双表优化(左连接给右表B主键加索引、右连接给左表A主键加索引)
3、多表优化(连接查询时,永远用小结果集驱动大结果集、尽可能的避免复杂的jion和子查询, 尽量不要超过3张表join)
4、show profile:分析当前SQL语句执行的资源消耗情况(以上完成99%优化)
show profile cpu, block io [查询参数] for query Query_ID
CPU:显示CPU相关开销信息 MEMORY:显示内存相关开销信息
BLOCK IO:显示块IO相关开销 ALL:显示所有的开销信息
5、运维经理 or DBA, 进行SQL数据库服务器的参数调优。
3. JVM
详见深入理解JVM.docx