导图社区 java知识大纲
下图梳理了java基础和面试题,包括JVM类加载机制、java集合、java多线程、java基础、面试问题等,有需要的朋友收藏下图学习吧!
编辑于2021-10-21 19:21:12java
GC
分代收集算法
在新生代-复制算法
因为在新生代中 每次gc只有少量的对象还存活,只要通过复制存活的对象来完成收集即可。
老年代-标记算法
在老年代存放的都是长期存活的对象,只有少量需要回收,只需要一一标记回收,直接释放内存即可。
分区收集算法
将整个堆空间分成连续的小分区,每个小区间独立使用,回收。好处:不需要停掉整个堆空间,只需要根据小分区来回收,很短暂的停顿。
GC垃圾收集器
新生代
复制收集
标记-整理收集
老年代
标记-整理收集
serial(连续)垃圾收集器
通过单线程去收集垃圾,同时会暂停所有线程,所以能获得最高的单线程收集效率,简单高效。 在jdk1.3.1前使用
ParNew(新式)垃圾收集器 (serial+多线程)
是serial垃圾收集器多线程版本,同样会暂停其他工作线程,默认开启和cup数目线程,是所有新生代的默认垃圾收集器。
Parallel Scavenge(并行清除)收集器
根据吞吐量控制收集时间,配置最大GC时间 时间越少,就会牺牲新生代的内存空间。内存越小 收集越快。
Serial Old 收集器
用于年老代收集算法, 单线程标记整理算法。配合Parallel Scavenge收集器 一起使用,新生代多线程,老年代单线程。 只能保证新生代吞吐量,不能保证整体吞吐量。
Parallel Old (并行年老代)收集器
在jdk1.6后,使用多线程整理标记算法,给年老代也提供吞吐量优先的垃圾收集器,对吞吐量有要求可以搭配Parallel Old (年老代) 和 Parallel Scavenge 收集器
cms收集器
多线程标记-清除算法 是一种年老代垃圾收集器。优点 获取最短垃圾回收停顿时间。
步骤
初始标记:标记 GCRoots(根)能直接关联的对象 ,如静态变量在调用的对象,如果无效就收集。
并发标记:根据GC roots 找到的对象 顺藤摸瓜 继续找下去。 这个不会暂停工作线程。
重新标记: 当用户程序运行导致 标记的对象继续被使用,需要修正 仍要暂停所有工作线程。
并发清除:清除GC Roots(根) 不可达对象,能和工作线程一起运行,不用暂停线程
G1 收集器
目前垃圾收集器理论发展最前沿的成果
1.基于标记-整理算法 不会产生内存碎片
2.可以非常精准控制停顿时间,在不牺牲吞吐量的前提下,实现低停顿垃圾回收
将堆内存划分几个大小固定的独立区域,并将这几个区域进行一个优先级排序,每次根据允许的收集时间,根据垃圾最多的区域 优先回收。 区域划分和优先级区域回收机制,确保在有限时间内获得最高的垃圾收集效率
java IO/NIO
阻塞IO模型
当用户发起io请求,内核会检查数据是否准备就绪,如果没有就绪就会等待就绪,用户线程处于阻塞状态,用户线程就会交出cpu。当数据准备好了 内核将拷贝到用户线程,并返回结果给用户线程。
非阻塞IO模型
当一个请求io,发起read方法,数据没有准备好,用户线程会一直发送read方法确认,直到数据准备好。非阻塞IO模型中,用户线程不断询问内核数据是否准备好,不会交出CPU ,会导致cpu占用率很高。
多路复用io模型
应用多路复用IO有NIO, 会开启一个子线程去监测 轮询多个socket的状态,当socket真正有需要处理的指令,才需要使用到IO资源。 可以大大减少资源的占用。 非阻塞IO是通过用户线程轮询, 但多路复用io的是用内核来执行循环,效率大大提高。 一旦响应体很大,会导致后续事件得不到及时处理,会影响新的轮询。
信号驱动IO
当用户线程发起了一个IO请求操作,会给对应的socket注册一个信号函数,线程可以先去做其他事情,当内核将数据复制到用户线程内存中,通知线程进行处理。 优点:线程不用阻塞。 缺点: 当数据量变大,信号产生频繁,性能会降低。
异步io模型
用户线程只需要给内核发送一个异步 IO 请求,就可以去其他的事情。 当内核将数据处理完成,回调操作成功通知 ,用户线程直接可以使用数据。IO操作两个阶段都不需要用户线程阻塞,全部由内核处理,收到信号IO就操作完成。
IO包
字节流
InputStream输入流
ByteArrayInputStream 字节数组输入流
fileInputSteam 文件输入流
FilterInputStream 过滤器输入流
BufferedInputStream 缓冲输入流
DataInputStream 数据输入流
LineNumberInputStream 行号输入流
Pushback(普修白客)InputStream 回推输入流
ObjectInputStream 对象输入流
PipedInputStream 管道输入流
SequenceInputStream 顺序输入流
StringBufferInputStream 字符串输入流
OutputStream 输出流
BayteArrayOutputStream 字节输出流
fileOutSteam 文件输出流
FilterOutStream 过滤器输出流
BufferedOutStream 缓冲输出流
DataOutStream 数据输出流
PrintStream 字符打印输出流
ObjectOutStream 对象输出流
PipedOutStream 管道输出流
字符流
Reader 读取流
BufferedReader 缓冲读取流
lineNumberReader 行号读取流
CharArrayReader字符数组读取流
FilterReader 过滤器读取流
PushbackReader 回推读取流
InputStreamReader 输入流读取流
FileReader 文件读取流
PipedReader 管道读取流
StringReader 字符串读取流
Writer 写入流
BufferedWriter 缓冲写入流
CharArrayWriter 字符数组写入流
FilterWriter 过滤器写入流
OutputStreamWriter 输出流写入流
FileWriter 文件写入流
PipedWriter 管道写入流
PrintWriter 字符打印写入流
StringWriter 字符串写入流
NIO
NIO三大核心
Channel(通道)
Channel和io 流是一个级别。流是单向的,但Channel是双向。
实现类有: 1. FileChannel 文件通道 2.DatagramChannel 数据报通道 3.SocketChannel 网络通道 4.ServerSocketChannel 服务网络通道
Buffer(缓冲区)
缓冲区实际上是个容器,连续数组。channel读写数据都要经过Buffer,
实现类: 1.ByteBuffer 字节缓冲区 2.IntBuffer 整数缓冲区 3.CharBuffer 字符缓冲区 4.LongBuffer 长缓冲区 5.DoubleBuffer 双重缓冲区 6.FloatBuffer 浮动缓冲区 7.ShortBuffer 短暂缓冲区
selector(选择器)
Selector类 轮询监测通道是否有事件发生,如果有事件要处理,就对每个事件相应响应,有读写事件,调用函数来进行读写,只需要单线程去处理,可以减少性能的开销。
基于Channel(通道)和Buffer(缓冲区)进行操作,数据读取 是从通道到缓冲区,写入是从缓冲区到通道。selector(选择器)用于监听多个通道的事件,比如连接打开,数据到达,通过单线程进行监听多个数据通道。IO是面向字流,NIO面向缓存区。
NIO缓存区: 只需要将数据读取到缓冲区,在缓冲区中可以前后移动,处理过程中灵活。但是要检查数据是否包含,不能覆盖未处理数据。 同比IO 需要从流读取所有数据,需要前后移动数据,也需要放入缓冲区操作。
NIO的非阻塞:只需要一个线程向通道发送一个请求读取数据,能拿到当前的可以用的数据,如果当前没有数据就什么也拿不到,当前线程可以去做其他事情。
JVM类加载机制
加载:加载class文件到内存里,作为方法区类的各种数据调用入口。 也可以读取jar包 ,war包。 或者是代理动态生成,jsp文件转换成class。
验证
检查class文件是否符合虚拟机的规范,不危害虚拟机的安全。
准备
这个阶段是为类分配内存,对变量进行赋值。 非static变量,在class编译后生成构造器 进行赋值。 static 的变量, 在编译时就给变量ConstantValue属性赋值。
解析
将符号引用解析成直接引用 解析前: String s=”adc”; System.out.println(“s=”+s); 解析后:System.out.println(“s=”+”abc”);
符号引用
虚拟机能接受的引用符号必须是一致的,引用符号定义要符合虚拟机的规范。 引用的目标不一定会加载到内存中。
直接引用
直接指向目标的指针,或者是一个能间接定位的句柄。但必须在内存是存在的。
初始化
所有class 加载完成后,进行执行java代码实现初始化
类构造器
编译器会手机类中的变量,生成构造器进行赋值。构造器执行顺序 父构造器早与子构造器。 不会执行类初始化的情况: 1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。 2. 定义对象数组,不会触发该类的初始化。 3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触 发定义常量所在的类。 4. 通过类名获取Class对象,不会触发类的初始化。 5. 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初 始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 6. 通过ClassLoader默认的loadClass方法,也不会触发初始化动作
类加载器
类加载器在JVM外实现,让应用程序来决定
启动类加载器((Bootstrap ClassLoader): 负责加载JAVA_HOME\lib目录下的类和jar包,也可以通过-Xbootclasspath参数指定路径去加载。
扩展类加载器(Extension ClassLoader): 负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类 库
应用程序类加载器(Application ClassLoader): 负责加载用户路径(classpath)上的类库。可以继承java.lang.ClassLoader 实现自定义加载器。
双亲委派
当类请加载,会先让父类先去加载,当父类加载器无法加载,在委派子类加载器去加载。无论怎么加载,都由顶层加载器加载到都是同一个Object类。
OSGI(Open Service Gateway Initiative)动态模型系统
面向java的动态模型系统,是java动态化模块化系统的规范
动态改变构造
无需重启去动态改变构造的功能,最小化耦合度和促使这些耦合可以管理。osgi 是面向服务的架构,让组件动态发现对方。
模块化编程与热插拔
osgi 可以实现 部分代码部署 停止 重新安装。但是不遵守双亲委托模型,不是用顶层加载器去加载,而是由自定义动态加载器去加载。
java集合
接口继承与实现关系
集合类:set(集),list(列表包含queue)和Map(映射)
Collection: collection是集合list,set,queue的最基本接口
Iterator:迭代器,可以通过迭代器遍历集合所有的数据
Map
是映射表的基础接口
集合框架
Collection
List
ArrayList 数组列表
当删除,插入需要大量的复制 移动,比较消耗性能。比较适合的场景查找和遍历。
排列有序,可重复
底层使用数组
速度快,增删慢,getter()和seth
线程不安全
当容量不够时,ArrayList是当前*1。5+1
Vector 矢量
排列有序,可重复
底层使用数组
速度快,增删慢
线程安全,效率低
当容量不够时,Vector默认扩一倍容量
LinkedList 链表集合
排列有序,可重复
底层使用双向循环链表数据结构
查询速度慢,增删快,ADD()和remove()方法快
添加数据在尾部添加
线程不安全
可以成堆栈,队列,双向队列使用。
Set
set 存储无序,值不能重复
HashSet 哈希集
HashSet 数据结构 数组+链表 || 数组+红叉数树 将对象的hashcode算出来放到数组中,然后将对象放入数组下标中的链表。 当有相同哈希code,会比对象是否一致,一致就帮不能存储。 只能放不同的对象。
排列无序,不可重复
底层是HashMap
存取速度快
底层使用HashMap
TreeSet 树集
排列无序,不可重复
add可以进行升序 降序的排序,适用于Integer和String对象,但是自定义对象不可以,如果要支持需要重写Comparable接口的compareTo()方法
底层使用二叉树实现
排序存储
内部都是TreeMap的SortedSet
LinkeHashSet链表哈希集
采用hash表存储,并用双向链表记录插入顺序
内部是LinkeHashMap
继承HashSet类,操作与父类相同。
Queue
在两端出入的list,通过数组和链表实现。
Map
HashMap
键不可重复,值可重复
底层哈希表
线程不安全
允许Kay值为NULL, value也可以为NULL
可以使用synchronizedMap方法让hashMap有线程安全的能力
java7中
数据结构是数组+单向链表 数组放的是Entry的实例,包含四个属性 Kay,value,hash值,和用于单向链表的next
capacity(分配的存储容量) 数组每次扩容大小是当前两倍 loadFactor(负载因子),默认是0.75 threshoid(扩容的阈值): 等于capacity*loadFator
java8中
hashMap数据结构 数组+链表+红叉树
当链表元素超过8个,将链表转换为红黑树
HashTable
键不可以重复,值可重复
底层哈希表
线程安全
kay,value都不允许null
TreeMap
键不可以重复,值可重复
底层二叉树
java 多线程
Thread类
实现Runnable的一个实例,真正启动先,需要调.start方法,start方法是一个native(本地)方法。启动一个新线程,并调用run方法
Runnable接口
自定义类 当已经继承了其他类,不能继承Thread类。可以通过实现Runnable接口,实现run方法。调用时候new Thread(myThread)传入,调用start方法启动。
ExecutorService(线程池服务类),Callable<class>(可调用)接口, Futrue接口
ExecutorService 实现线程池
callable 让线程执行完成后调用这个接口实现返回方法
Futrue 获取返回的内容
线程池
基于线程池的方式
因为每次生成 销毁一个线程 非常消耗资源。 所以通过缓存起来,提前创建好,使用的时候调用。
newCachedThreadPool(缓存线程池)
创建一个根据需要的线程池,当需要的时候会创建一个线程,并把线程缓存起来。 同时也会检测如果线程超过60秒未被调用就清除掉。这样的好处不会占用很多资源。适用于异步任务。
newFixedThreadPool 固定线程池
创建固定数量可重用的线程池,当所有线程都在活动的状态时提交附加任务。没有可用线程时候,会先放到队列等待可用线程处理。如果在执行期间失败导致任何线程终止,会有新的线程代替处理。
newScheduledThreadPool 计划线程池
自定义计划的线程池,线程可以设置一定时间执行
newSingleThreadExecutor单线程池
这个线程池创建后只有一个线程,当这个线程死亡就会在创建一个线程继续执行任务
线程生命周期(状态)
新建(new)
由JVM分配内存,初始化其成员变量
就绪(runnable)
当线程对象调用了start()方法后,JVM会创建方法的调用栈和程序计数器,等等运行。
运行(running)
当就绪的线程获得CPU后就执行run方法,当前状态就处于运行状态
阻塞(Blocked)
阻塞状态是因为某种原因,让线程放弃CPU使用权。暂停运行直到Runnable就绪状态,才有机会再次运行。
等待阻塞 运行的线程执行wait()方法,JVM把线程放到等待队列
同步阻塞 运行时的线程碰到同步锁,当前有其他线程占用了这个对象,JVM会将线程放入锁池中
其他阻塞 (sleep/join) 调用sleep()和join()方法 或者发送i/O请求。JVM让当前线程阻塞。当sleep超时,join等待线程终止或超时,io请求结束。线程就会回到可运行状态。
死亡(Dead)
run()或者call()方法执行完结束
线程抛出未捕获的异常或者error
调用stop()方法,但是这种方法会容易死锁。
并不是创建线程池后,cpu一直被占着,是在多个线程中切换,多次运行,阻塞切换。
终止线程4种方式
程序运行结束
使用退出标志退出线程,如满足某种条件进行终止,使用while循环当条件是true或false 的变量来控制退出。 定义变量在外部控制 使用volatile 关键字
Interrupt()方法
阻塞过程中调用Interrupt() 会抛出InterruptException异常 ,一定要捕获异常break来跳出循环才能正常结束run方法
使用interrupt()方法做中断标志,和自定义中断标志是一样的。
stop()方法终止线程
Thread.stop()方法调用后会抛出ThreadDeatherror的错误,并且会是否子线持有的所有锁,导致其他数据拿到破坏的数据会出现程序错误。
sleep 与 wait 区别
sleep()是Thread类的 wait()方法是对象的
sleep 根据设置时间会程序暂停,让出cpu给其他线程,但是他的监控依旧保持着。到了指定时间就会回到运行状态
sleep 不需要获取对象锁
wait()方法 线程会放弃对象锁,将这个对象放入对象的等待锁定池中。只有针对此对象调用notify() 通知线程,进入对象锁定池中获得对象锁进入运行状态。获得对象锁才有操作对象数据的权利。
java后台线程
Daemon(守护)线程 优先级是最低,给用户线程提供服务,守护线程创建的线程还是守护线程,线程级别是JVM,以Tomcat为例,应用暂停了,守护线程还在。 垃圾回收线程经典的守护线程,当无垃圾可回收,会回自动离开。当服务没有用户线程,守护线程也会去离开。JVM中的全部是守护线程。与系统共生死。通过设置setDaemon(true) 开启守护线程。
start 与 run 区别
start()方法来启动线程,不需要等待方法中run()执行,会往下执行。start是主线程执行,run是新开的子线程执行。
start()方法只是让线程进入准备阶段
run()让线程进入运行状态,执行run函数当中的代码。run方法结束,线程终止。
java锁
乐观锁
不会对某个数据进行上锁,只是在修改前拿到数据记录一个版本号,在修改前查询一下这个数据对比一下版本号和内容,发现版本没对上会修改失败。重新进行去最新值,在进行查询对比。 一般需要循环来做。 通过CAS操作
悲观锁
每次去拿数据都会觉得别人会修改数据,都对数据进行上锁,别人过来只能block(阻塞)着 直到拿到锁。
自旋锁
当持有锁的线程在很短时间释放,那些等待拿到锁的不需要做内核态和用户态的切换(阻塞和运行),只需要让线程等一等,等持有锁的线程释放在立即获得。所以需要一直查询锁的状态。
优点
自旋可以减少线程阻塞和再唤醒的消耗。这些操作会导致两次的上下文切换
缺点
如果线程占用锁大量的时间,会导致大量的线程在自旋。会有大量的cpu消耗
可以设置自旋的时间或次数来控制,不能一直自旋下去,在jdk1.6 上限次数10次,1.7以后由JVM来决定
非公平锁
选择线程的方式,随机 就近原则。执行效率远超公平锁。synchronized 和Reentrantlock.lock()方法采用的非公平锁
公平锁
先提出申请的线程先获得,先到先得原则
Synchronized 同步锁
可以将任意非null的对象当作锁,他属于悲观锁,同时属于可重入锁。
锁升级 偏向锁→轻量级锁→重量级锁。 锁膨胀模式 不能降级。用户态转向内核态, 并发越高性能越差。
作用范围
作为方法时候,锁住的是对象的实例
当作用是静态方法时,锁住的是calss实例
作用域一个对象实例时,锁住的是所有以该对象为锁的代码块,它有多个队列,多个线程一起访问某个对象。监视器的时候,对象监视器会将这些线程存储在不同的容器中。
synchronized 核心组件
wait set(等待组) 哪些调用wait()方法的线程会放到这里面
Contention list (竞争队列): 所有请求锁的线程都会放到这个队列里
Entry List(入口列表):当在竞争队列的线程有资格被候选就放到这个EntryList中
OnDeck (上位)在任意时刻 最多只有一个线程正在竞争锁资源,这个线程成为OnDeck
!OnDeck 当前释放锁的线程
synchronized实现
线程排队顺序 ContentionList(竞争队列) → EntryList(入口队列) → OnDeck状态
Owner(所有者)线程不会把锁传递给 Owner线程
ReentantLock 重进入锁
继承Lock接口,除了能完成synchronized所有工作以为,提供了诸如可响应中断锁,可轮询锁请求,定时锁等避免多线程死锁的方法。
Lock接口方法
lock()方法 执行方法时,如果锁定与空闲状态,当前线程获得的锁,反之禁用当前线程,直到当前线程获得锁。禁用后代码就不会往下走
tryLock() 试图去获取锁, 如果获取到返回true,否则返回false。 如果当前锁不可用,不会导致当前线程不可用,代码还是会继续走下去。
unlock()
执行此方法时,当前线程将释放持有的锁,锁只有持有线程才能释放
newCondition()
条件对象,获取等待通知用。只和当前锁绑定,只有获得锁的线程,才能调用await()方法,而调用后,当前线程将缩放锁。
getHoldCount()
获取当前线程执行lock的次数
getQueueLength
返回正等待获取锁的线程数量,不包含持有锁线程
getWaitQueueLength
当前condition对象 被多少个线程调用await()方法,十个调用,返回10。
hasWaiters(等待者)
查询现在有多少个线程等待,或者是condition对象,调用await()方法线程有多少个
hasQueuedThread(Thread thread)
查询指定线程是否在等待获取锁
hasQueuedThreads()
是否有线程等待此锁
ReentrantLock和synchronized
synchronized是由JVM自动解锁,ReentrantLock加锁需要手动解锁
ReentrantLock重入锁 相比syncharonized同步锁 优势 可中断,公平锁 多个锁。
Condition类和 Object类锁方法区别
Condition.wait()=obj.wait()
signal()方法=notify()方法
signalAll()方法=notifyAll()方法
重入锁可以唤醒制定线程,对象锁只能随机唤醒
tryLock和 lock和lockInterruptibly的区别
tryLock能获得锁就返回true,不能就立即返回false,tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回false
lock能获得锁就返回true,不能的话一直等待获得锁
lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock不会抛出异常,而lockInterruptibly会抛出异常
Semaphroe 信号量
实例设置一个计数的信号量,当设置为1 只能是一个线程获得许可,其他竞争的线程会被阻塞,常见案例 对象池,数据库连接池。
Semaphore 与 ReentrantLock
semaphore基本完成ReentrantLock的所有工作。调用acquire(获取) 方法与release(释放)方法 来获得和释放临界资源
semaphore.acquire() 默认响应中断锁,与 ReentrantLock.lockInterruptibly()作用效果一致。在等待临界资源过程中可以被Thread.interrupt()方法中断。
semaphore 实现了可轮询的锁清秋和定时锁,tryAcquire与tryLock方法名不一样,其他方法名都一样。
释放锁也需要手动进行,必须在finally代码块中完成
ActomicInteger(原子整数)
一个提供原子操作的Integer的类,也有AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同。也可以通过AtomicReference<v>将一个对象的所有操作转化为原子操作。 线程 安全 同步
可重入锁(递归锁)
当前线程在外层函数获得锁之后,在内层递归函数仍然有获得该锁的代码,但是不受影响。 ReentrantLock和synchronized 都是重入锁
ReadWriteLock 读写锁
读锁,允许同时多个线程同时读,但是不允许同时写,那就上读锁
写锁,只允许一个人写,且不允许读,就上写锁。
实现 java.util.concurrent.locks.ReadWriteLock接口
共享锁和独占锁
独占锁模式下 每次只有一个线程持有锁,是一种悲观保守的加锁策略,当某个线程在读取,其他的线程就必须等待,这样限制了不必要的并发性。读操作不会影响到数据的一致性。
共享锁运行多个线程同时获取锁, 如ReadWriteLock(读写锁) 运行一个资源被多个线程读,或允许一个线程写操作。但是读写不能一起操作。
重量级锁
阻塞同步,悲观锁。 重量级锁会导致线程从用户态切入内核态,对性能消耗非常大。
轻量级锁
为了传统重量级锁的性能消耗,通过对锁的升级,偏向锁,轻量级锁 最后升级到重量级,这个过程也叫锁膨胀。
偏向锁
为了减少线程重入(CAS)的开销。当只有一个线程 锁可以多次被获取。 但是一但有多个线程请求马上升级到CAS。
分段锁
如ConcurrentHashMap 实现就进行分区 然后进行上锁。
锁优化
减少锁持有时间 只有线程安全要求在上锁
减少锁颗粒
减少在大对象上上锁, 尽量将需要上锁的模块拆出来一个小的对象上锁。大大增加并行度,降低锁的竞争。偏向锁,轻量级锁成功概率才会提高。案例 ConcurrenHashMap.
锁分离
常见的锁分离,读写锁(ReadWriteLock) 读写只允许存在一种操作。
锁粗化
当有多个线程操作一个对象,将所有线程合并成一次请求,经常操作数据。
锁消除
在编译期间,编译器对不会发生并发的锁场景,去除掉上锁。
线程基本方法
wait 线程等待
调用这个方法的线程进入waiting状态,等待其他线程执行完通知。调用wait方法会是方法对象锁。wait方法需要在同步代码块中使用。
sleep线程休眠
让当前线程进行休眠,sleep方法不会释放当前对象锁。线程进入TIMED-WATING状态
yieId线程让步
调用yieId方法会让当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片。优先级高的线程大概率性竞争成功。
interrupt线程中断
interrupt方法会给线程通知一个通知信号,处于running状态的线程不会终止。 只有sleep()方法的线程 调用Interrupt() 会抛出中断异常,提前结束休眠状态。
抛出InterruptedException的方法,会清理中断标识。
中断状态在线程固有一个标识,通过标识线程可以安全终止。在线程的run方法内部可以 根据thread.isInterrupted()的值来优雅的终止线程。
join方法
调用join方法,线程进入阻塞状态,让线程等待其他线程终止,然后状态从阻塞状态转为就绪状态。
应用场景,当主线程需要到子线程处理结果,需要在子线程结束后再继续处理,所以在启动子线程后调用join()方法。
notify通知
调用了wait方法,线程就自动进入等待队列, 等待当前线程执行完后线程调用notify通知方法,随机唤醒一个等待的线程。调用notifyAll()方法,唤醒所有线程,进行竞争锁。
其他方法
1. isAlive(): 判断一个线程是否存活。 2. activeCount(): 程序中活跃的线程数。 3. enumerate(): 枚举程序中的线程。 4. currentThread(): 得到当前线程。 5. isDaemon(): 一个线程是否为守护线程。 6. setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线 程依赖于主线程结束而结束) 7. setName(): 为线程设置一个名称。 8. setPriority(): 设置一个线程的优先级。 9. getPriority()::获得一个线程的优先级
线程上下文切换
时间片 就是cpu线程给每个任务分配的工的处理时间段,为时间片。
线程给每个任务执行完一段时间后,将当前任务的状态保存下来,在加载下个一个任务,继续服务下个任务,任务状态继续保存下来,这个过程就是上下文切换。
上下文:指某一个时间点cpu寄存器和程序计数器的内容
寄存器:cpu中的高速缓存
程序计数器:记录代码执行到的位置,存当前执行位置和下一个执行位置。
PCB- 切换帧 用于存储上下文切换的
上下文切换活动:挂起一个进程,将当前上下文状态存入寄存器 找到下个任务,从寄存器中恢复 根据上下文信息跳转到指定程序计数器所指向的位置,以恢复程序执行中。
引起线程上下文切换的原因:
IO阻塞 会将任务挂起 执行下个。
时间片用完,系统会调用下个任务。
当前任务没有抢占到锁,被调度器挂起。
用户代码挂起。
硬件中断
同步锁与死锁
同步锁:保证线程 同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。
死锁:多个线程同时被阻塞,全部都在等待锁资源释放。事实上已经没有线程在操作资源了。
线程池原理
将创建指定数量的线程,放入缓存起来,当有任务来了,将线程放入任务队列中进行处理。特点:线程复用;控制最大并发数;管理线程
线程复用:重写Thread类,在其start方法添加不断循环从Queue(队列)中拿Runnable类。在获取下个Runnble之前可以是阻塞的。
线程池的组成
1. 线程池管理器:用于创建并管理线程池 2. 工作线程:线程池中的线程 3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行 4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
线程池配置
corePoolSize 线程核心数量
maximumPoolSize 最大线程数量
keepAliveTim空闲线程存活时间
workQueue 工作队列
threadFactory线程工厂
unit:keepAliveTime的单位。
handler 拒绝策略
handler 拒绝策略
AbortPolicy(中止策略): 直接抛出异常,阻止系统正常运行
CallerRunsPolicy(调用者运行策略):直接用调用者线程来处理当前被丢弃的任务。 这样会影响任务提交线程的性能有可能急剧下降。
DiscardOidestPolicy(丢弃最老策略): 丢弃最老的任务,就是马上要执行的任务,并重新提交任务。
DiscardPolicy(丢弃策略):这个策略丢弃无法处理的任务,不做任何处理。
以上内置拒绝策略均实现了RejectedExecutionHandler接口,若以上策略仍无法满足实际 需要,完全可以自己扩展RejectedExecutionHandler接口
java线程池工作过程
刚创建时,里面没有一个线程,任务队列也是当作参数传进去。 不会 就算队列中有任务也不会执行。
当调用execut() 线程做的判断循序
运行线程数量小于corePoolSize(核心线程数量),就创建一个线程运行任务
运行线程数量大于或等于corePoolSize,那将这个任务放入队列
队列满了,正在运行的线程处理小于maximumPoolSize(最大线程数量) ,那么创建非核心线程运行这个任务。
队列满了,当前正在运行的数量超过最大线程数量,就抛出异常RejectExecutionException
当一个线程执行完任务就取下个任务执行
当线程无事可做,在配置活跃时间后,当前运行线程大于corePoolSize,这个线程就停掉。当所有任务都完成后,最终会收缩corePoolSize的大小
阻塞队列原理
当队列的没有数据,消费端没有请求,会把线程全部挂起。
当队列数据满,生产者所有线程会挂起。
执行方法
插入
add方法 将指定元素插入队列中,插入成功返回true,没有空间就抛出。
offer() 和add方法的区别,空间不足不会报错 返回false
put()将指定元素插入此队列中,将等待可用空间
offer(E o, long timeout, TimeUnit unit),可以设置等待时间,在制定时间内,还不能往队列中加入Blockingqueue,则返回失败。 插入队列也有时间限制?
获取
poll(time)取走BlockingQueue 里排在首位的对象,如果不能取出,则等time时间过后 取不出就返回null
poll投票(long timeout,timeUnit unit) 取队列首位的对象,在规定时间内,一旦有数据就马上取出来返回,否则超时返回null。
take() 如果队列为空,阻断进入等待状态直到有数据进入。
drainTo() 将队列的数据一次性全部取出。这样就不用进行加锁或者释放锁
java中的阻塞队列
1. ArrayBlockingQueue (数组阻塞队列):由数组结构组成的有界阻塞队列。 公平队列,所有生产者线程和消费者线程,都要有序按照先后顺序访问队列。先阻塞生产者线程,就往队列插入数据。或先阻塞消费者线程,就先从队列中取数据。
2. LinkedBlockingQueue (链表阻塞队列) :由链表结构组成的有界阻塞队列。先进先出,对于生产者和消费端的线程都有独立的锁,支持同时并发操作数据。提高了整个队列的性能。
3. PriorityBlockingQueue (优先级阻塞队列):支持优先级排序的无界阻塞队列。 默认情况会对元素进行排列,可以实现compareTo()方法自定义。 相同优先级的元素无法保证顺序。
4. DelayQueue(延时队列):使用优先级队列实现的无界阻塞队列。支持延时取队列的数据,只有在延时期到后才能取数据。
缓存设计 设置延时时效,会有一个线程轮询DelayQueue,直到取到数据,说明缓存有效期到了。
定时任务调度 DelayQueue队列 保持了当天执行任务和执行时间,一旦从队列获取到任务就开始执行。
5. SynchronousQueue(同步队列):不存储元素的阻塞队列。每一个put 就必须要等待take,否则不能继续添加元素, 队列只做传递的的作用。
6. LinkedTransferQueue(链表传输队列):由链表结构组成的无界阻塞队列。
transfer方法 生产者通过方法传递给消费者,如果没有消费者就会将元素放入tail节点中,并等到该元素被消费者消费了后才返回。
traTransfer方法,会先试探看有没有消费者,有就传递,没有就立即返回false。
7. LinkedBlockingDeque(链表阻塞双向队列):由链表结构组成的双向阻塞队列,可以进行双向操作数据,比起单向多了一个入口,竞争就减少了一半。
相比其 他的阻塞队列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast, peekFirst,peekLast 等方法,以 First 单词结尾的方法,表示插入,获取(peek)或移除双端队 列的第一个元素。以 Last 单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。另 外插入方法add等同于addLast,移除方法remove 等效于removeFirst。
CyclicBarrier、CountDownLatch、Semaphore
CountDownLatch 线程计数器, 比如说A任务,需要等待开启的2个子线程任务结束才继续执行,通过计数,等待执行。
CyclicBarrier(回环栅栏-等待至barrier状态再全部同时执行) 当一组线程请求进来,等到指定的状态,在全部执行。调用await() 当前所有线程改成barrier(围栏)状态,可以设置超时时间。
Semaphore 信号量 可以控制同时访问的个数,通过acqueire() 获取一个许可,如果没有许可就等待, 调用release()释放一个许可
1. public void acquire(): 用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许 可。 2. public void acquire(int permits):获取permits个许可 3. public void release() { } :释放许可。注意,在释放许可之前,必须先获获得许可。 4. public void release(int permits) { }:释放permits个许可。 上面4个方法都会被阻塞
volatile 关键字
变量可见性,当线程去拿变量,也有其他线程在操作变量,会先进行同步最新的值
禁止重排序 java
如何在两个线程之间共享数据
共享内存关注点 可见性和有序原子性
java 内存模型 JMM 解决了可见性和有序性的问题,而锁解决了原子性和问题,理想情况下要做到同步和互斥。
同步 简单的方式使用synchronize代码块进行控制,将runnable类做完一个类的内部类,来实现这个类的数据共享。
ThreadLoadl作用 (本地线程存储)
线程内的局部在当前线程的生命周期范围,当前线程内共享。
ThreadLoadlMap 每个线程都有一个,可以将线程的对象放入其中,各管个的,线程隔离不会被其他线程影响到。通ThreadLoadl.get()方法取得自己的线程保存的类。
ThreadLoadMap是线程的一个属性。
ConcurrentHashMap(并发HashMap)
由segment数组和HashEntry数组+链表结构组成。
concurrentHashMap
segment数组 (上锁)
hashEntry数组加链表
hashEntry数组加链表
segment数组 (上锁)
hashEntry数组加链表
hashEntry数组加链表
当线程要操作hashEntry数据,需要先拿到segment的锁
java中用到的线程调度
抢占式调度 :指每条线程执行的时间,线程的切换都有系统控制,系统控制指的是在系统某种运行机制下,可能每条线程的都分同样的的执行时间片。也有可能某个线程一直得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程的堵塞。
协同式调度:接力棒式调度,当一个线程执行完了通知下一个线程进行执行。一直传递下去。 有一个弊端 当其中一个线程中断了停止 会导致整个系统瘫痪。
JVM的线程调度实现:java使用的线程调度是抢占模式,通过java中线程优先级分配时间片。优先级越高先执行,但是不等于能独占时间片。优先级越高分配的时间片越多,但是 优先级越低不会等同于分配不到时间片。
进程调度算法
先来先服务调度算法:每次调度从后背队列中取出一个或多个最先进入队列的任务,进行调入内存,分配资源,创建进程。放入就绪队列。 采用FCFS算法,从就绪队列选出一个最先进入的进程,为它分配处理机。算法比较简单,可以算的上基本上公平。
短作业(进程)优先调度算法
java基础
基本数据类型
float 浮点型
在内存占4个字节
异常分类及处理
概念:某个方法不能正常的完成业务,通过一种途径退出当前任务。
异常分类,Throwble是java语言中所有错误和异常的超类,下一层时候Error和Exception
error是指java运行时的内部错误和资源耗尽错误,如超时,内存溢出等 抛出错误是尽量告知用户,剩下做的尽力安全终止。
Exception (异常) 有两个分支,一个是运行时异常RuntimeException ,一个是CheckedException 检查异常
RuntimeException 运行时异常 空指针,转换异常。 这个错误抛出来一定是程序员写出来的错误。
CheckedException 如IO异常 sql异常
throw和throws
throws 后面跟着多个异常类,只是用来声明异常,一般定义在方法参数列表后面,这方法里面可能会发生的那些异常。
throw 后面跟着的是异常对象
两种都是消极处理异常,自己都没办法处理。将异常交给调用方进行处理。
反射机制
在运行时候可以知道任意一个类的属性和方法,动态的获取信息和动态调用对象方法。
反射应用场景
在运行时候对象有两种类型 编译时类型和运行时类型,编译时类型是声明实例对象决定,如A a=new A()。 运行时类型,是由实际赋值给对象的类型决定,如:A a=new B();
当外部系统请求进来的是Object 对象,就没办法事先知道对象信息,这个时候可以通过反射来获取对象中的内容。
反射API
Class类:反射的核心类,可以获取类的属性,方法等信息
Fieid类:表示类的成员变量 ,可以获取类的属性值
Method类
表示类的方法,它可以用来获取类的方法信息或者执行方法
Constructor(构造器)类
表示类的构造器
反射使用步骤
获得想要操作的class对象,它是反射的核心,可以通过class对象我们可以任意调用类的方法
调用class类中的方法,即就是反射的使用阶段
使用反射API来操作这些信息。
获取class对象
调用某个对象的 getClass() 方法 Person p=new Person(); Class clazz=p.getClass();
调用某个类的 class 属性来获取该类对应的 Class 对象 Class clazz=Person.class;
使用 Class类中的 forName() 静态方法 ( 最安全 / 性能最好 ) Class clazz=Class.forName("类的全路径"); (最常用)
创建对象
Person p=(Person) clazz.newInstance();
Constructor c=clazz.getDeclaredConstructor(String.class,String.class,int.class);
java注解
概念:annotation(注解) 是java 提供的一种对元程序中袁术关联信息和元数据的途径和方法,Annataion是一个接口,程序可以通过反射来获取指定程序中元素的Annotation对象,然后通过Annotation对象来获取注解的元数据信息。
4种标准注解
java5中定义了4个标准的meta-annotation类型
@Target 注解所修饰的对象范围,packages(封包),types(类,接口,枚举,类型),类型成员(方法,构造方法,成员变量,枚举值),方法参数和本地变量(如循环变量,catch参数)。 修饰后注解接口 可以定义在那些类型范围
@Documented(记录,文档) 描述-javadoc: 注释在公共API上,通过javadoc类进行工具文档化
@Retention(保留) 定义被保留的时间长短 用于扫描注解的生命周期。
@Inherited阐述了某个被标注的类型是被继承 如果一 个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个 annotation 将被用于该 class的子类。
注解处理器
注解处理主要通过反射去拿到class的注解
java内部类
可以在类内部定义一个类,类的类型有:静态内部类,成员内部类,局部内部类,匿名内部类四种。
静态内部类:可以访问外部类的所有静态方法,及时是private(私有) 也可以。
成员内部类:定义在类里面非静态,就成员内部类。成员内部类不能定义静态方法和变量,应为成员内部类不是静态的。 如果里面有静态变量,优先加载静态变量。
局部内部类
定义在方法中的类,如new Thread(){}
匿名内部类(要继承一个父类或者实现一个接口、直接使用 new 来生成一个对象的引用)
java 泛型
在编译期间安全检测机制,检测类传入对象是否符合规范。如Map<String,String> 传入int 在编译期就报错。
泛型方法<E>,该方法在调用时可以接收不同类型的参数,根据传递给泛型方法的参数 类型,编译器适当地处理每一个方法调用。
泛型类<T>泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。和泛型方法一 样,泛型类的类型参数声明部分也包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数, 也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数, 这些类被称为参数化的类或参数化的类型。
类型通配符 ?
通配符一般是?代表具体的类型参数,如list<?> 在逻辑上是List<String>
JAVA序列化
将对象状态改为持久化保存到内存和硬盘中
用于远程对象传输
序列化对象以字节数组保持-静态成员不保存
继承 Serializable 接口,生成序列化 ID ,在反序列化 校验两个类的id是否一致
序列化过程 序列化 →字节对象→反序列化
Transient (短暂,过客)通过此关键字阻止变量序列化到字节文件中。如 int 型的是 0,对象型的是 null
java复制
复制可以理解为引用
直接赋值复制: A a1=a2, 这里的复制就是a2的地址引用到a1上。当a1 里的变量发生变化 a2里面的变量也跟变化
浅复制(复制引用但不复制引用的对象),当前对象的非静态字段复制到该新对象,如果字段是值类型的, 那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。
深复制 深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象
序列化(深clone一中实现)在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对 象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
ACID
原子性 atomicity
一致性 consistency
隔离性 isolation
持久性 durability
系统
TPS 每秒处理的事务
1000毫秒/一次事务处理时间=TPS数量
QPS 每秒处理的查询
1000毫秒/一次查询处理时间=QPS数量
分布式
CAP理论
consistency(一致性)
所有节点同一时间要完全一致, 在某个节点修改了,要同步到其他节点
Availability(可用性)
既服务一直可用,而且正常响应,系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
partition Tolerance 分区容错性
分布式系统某个节点或者分区网络故障,仍然能对外提供满足一致性和可用性的服务。当一台或几台机器挂掉了,其他剩余的机器还能正常运转,对外正常提供服务,不影响用户的体验。
BASE理论
Basically Available (基本可用)
响应时间上的损失:正常情况下,处理用户请求需要0.5秒返回结果,但是系统故障了,处理用户请求的时间变成3秒
系统功能上的损失:正常情况下,用户可以使用全部功能,但是某个核心功能应该访问量剧增,导致核心功能无法使用
Soft state(软状态)
数据同步运行一定时间的延迟
Eventually consistet(最终一致性)
系统中所有的数据副本,在经过一段时间的同步,最终能够达到一个一致的状态,不要求实时。
嵌入式服务器
如入tomcat.jar,springboot内置了tomcat.jar,也不用在打war包 只需要一个JVM ,通过命令就可以启动tomcat。
面试问题
String 和 Stringbuffer ,StringBuilder区别
String 是用final修饰 不可变,每次都是新的对象
StringBuffer和StringBuilder 每次都是相同的一个对象。
StringBuffer 是线程安全的,因为方法都用synchroized修饰。
StringBuilde是线程不安全 相对于 StringBuffer 新能更快一些。
优先使用StringBuilde,在多线程的情况下使用StringBuffer
GC如何判断对象可以被回收
引用计数法:当对象引用一次就+1,引用释放时计数器减1,当计数器为0时候代表对象可以被回收了。但是这种方法会有一个问题 A引用了B B引用了A 两个对象引用计数为1 就无法被回收
可达性分析法:搜索所走过的路称为引用链,当一个对象到GC Roots 没有任何引用相连,证明这个对象是不可用的,那么虚拟机就判断可回收对象。
Final关键字
修饰的变量在声明时需要赋值或者通过构造器赋值
修饰基本数据类型 值永远不会改变
修饰对象所引用指向永远不会改变
在局部变量中修饰,可以不赋值,但是在使用前一定要赋值,不然报错。
final int[] iarr=[1,2.3] 可以更改数组里面的值,但是数组对象无法赋值。
jdk jre jvm 区别
jdk 提供给开发者使用的开发包
jer 提供给需要运行java程序的用户使用
jvm 虚拟机 解析calss文件 翻译成机器码运行
hashMap和HashTable 的区别
HashTable和HashMap实现方法基本一样,但是所有方法都加入syn代码块, 线程是安全的,但是执行效率低。
HashMap 是数组+KeyValue对象 +链表 或红黑树实现
第一次存储对象,先计算出对象的hashcode,将放入数组某个位置,hashCode同时存放
当放入的对象的hashcode已经存在,就调用equal方法比较,类型一致 覆盖原先的值,不一致 创建链表 将新旧值都放入链表。
当链表达到了一定长度后,链表就会升级红黑树存储。
当key值为null 默认放在数组下标0的位置
Arraylist和LinkedList
LinkedList
底层链表 存放的是node对象 顾名思义就是链条一样,存储是无序的, 在两个数据中间插入数据,只需要将链接断开,新值在链接上两个值即可。插入和删除 不会造成大量的数据移位,效率高。
不适合查询,因为需要逐一遍历。for 循环 通过get(i) 下标去查找 也会去遍历整个list去找这个下标
Arraylist
基于动态数组,连续存储,下标访问(随机访问)
动态数组的实现, 当list最初始的数组已经满了 会创建一个长度更长的数组,将老数据拷贝过来,将新的数据插入,然后回收老的数组。 插入速度相对慢与于linkedLIst,但是插入是进行尾部插入 效率还是能接近链表插入速度。
查询快,是通过下标查找,下标是有序的。
什么是字节码
字节码就是java类编译后的.class文件,有jvm去执行调用解释器翻译成机器码,进行程序运行。
传统编译型的语言,是编译一句在解释一句成机器码 java 先将所有代码编译好成字节码.class文件 ,jvm只需要拿到class文件就可以直接进行解释。
守护线程
主要是为非守护线程提供服务的线程,非守护线程叫用户线程,如果说GC中垃圾回收就是守护线程。守护线程 在所有用户线程关闭后就会被立即关闭。
守护线程创建的子线程也是守护线程。
它是jvm级别的线程
应用场景:为用户线程提供服务支持的情况 当成程序关闭,这个线程就立刻关闭。
session共享有什么方案
无状态服务,丢弃session , 通过用户登录信息验证完成后, 返回一个加密后的token给用户保持登录
将session放入到cookie中, 每次请求过来把cookie带过来。但是有风险。
session 同步 每台服务器之间进行相互同session,这样可以保证每台服务器有全部的session ,但是非常消耗性能,而且有网络延迟的原因会导致同步不一致。通过Tomcat 集群 实现,只要配置一下就可以了,不需要大量代码开发,上线快。问题 用户量大 会挤爆所有服务器内存
IP 绑定策略: 给每台服务器设定一个编号, 通过Nginx分发,进行哈希取模计算出在那一台服务器上。 但是当服务器宕机 session就丢失了
redis 将session放到redis中存储。每次请到服务器从redis中取。 这样 可以实现session共享,跨平台登录,服务器重启不影响
负载均衡算法
轮询法
有序 从1 2 3 ,1 2 3 编号分发请求
随机法
进行随机数取模 随机的分发到服务器上
ip哈希法
1.根据服务器数量设置对于的虚拟节点数量。
2.定义一个0到2的32次方-1的hash环。
3.对服务地址进行hash算法,得出hashcode,映射在哈希环上的某个位置上。
4.取模
加权轮询法
给服务器配置好的,增加权重,增加轮询到的次数
加权随机法
给配置好的机器编号 增加随机算法计算出来的概率,总的概率不能超出百分百
类型
通过DNS方式实现负载均衡
Nginx
跟客户端和服务端都会保持一个长连接,性能消耗比较多
使用轮询,加权轮询,ip_hsah
HAproxy
LVS
分布式生成id
雪花算法
长度为64bit的二进制
第1位0默认是0值,第2到42位为时间戳, 第43位到52位为机器码,第53到64位 为自增序号
1.生成时间戳 now左移22位
2.生成机器码 workid左移动12位
3.生成自增序列号,不需要移位,长度不能超过12位,生成最大数值为4096
判断当自增序号=4096时需要 让线程等待一毫秒 在进行生成
分布式锁
分布式锁原则
互斥性。在任意时间只有一个人持有锁
不会发生死锁,客户端上锁后 无论是程序崩溃还是中断,都要解锁
容错性,只有大部分服务正常运行,客户就能加锁解锁
解锁的人必须是上锁的人
锁不能自己失效
db
通过插入主键,插入成功获取锁 ,主键是唯一的。但是插入失败的线程 需要在代码上写循环一直重复插入。也有可能拿到锁的线程 终止了未释放锁,就变成死锁了。
zookeeper
redis锁
布隆过滤器
定义一个特定长度的字节数组,比如十万,然后对值进行3次不同hash取模,第一次100 第二次2000 第三次10000 ,对应的下标的值会改成1。如果是相同的值进行取模 结果是相同,去对应下标找值是否是1 是就存在,不是就不存在
定义一个超长的二进制数组,数组内存放的是0或1。 给这个数组添加一个数据 6。对这个6进行多次hash取模,得出多个结果下标为 5 ,10 ,21。对应的下标位的值会由0为1。
查询是否在存在数组,进行相同多次hash取模,计算出来的下标。根据下标找到对应位置,查看这些值是否全部为1,是就存在,否就是不存在。
因为hash算法 有不同值计算出来hashcode是一致的,所以出现误判,两个值计算的下标一致。
解决方案:1.增加二进制数组的长度 2.增加hash取模计算次数, 通过设置参数降低误判率来实现。
误判率设置的越低,数组长度越长,hash计算次数约多。 但是效率也会降低,建议0.01
并发容器
ContDownlatch
应用场景:使用一个主线程 创建多个子线程进行任务, 主线程调用latch.await()阻塞
int a=5; latch=new CountDownLatch(a) 底层使用AQS.state 进行计数
new Thread(B(latch)) 创建一个线程,并运行直到 执行latch.countDown()方法,对任务数量-1。 同时开5个线程去完成任务,当全部执行latch.conuntDown()方法,任务数量就变成0
任务数量变成0后就唤醒 调用latch.await()方法的主线程 继续向下走。
CyclicBarrier(循环栅栏)
应用场景:需要多个子线程阻塞等待,大家都准备好了同时执行
每个子线程都会调.await()进行等待唤醒,当线程都是等待状态就开始执行
Semaphore(信号标)
就想许可证一样,创建了10个线程,信号量只有3个,只有3个线程可以执行,否则阻塞 当某个拥有了信号标的线程执行完了,又把信号标还回去,通知其他需要的线程。
sp.acquire() 获取信号标
sp.release()释放信号标
底层使用AQS.state 进行计数
BIO ,NIO,AIO
应用场景
BIO 调用acceet() 和read()方法 会出现两次阻塞,缺点 没有连接 没有数据读取 会一直阻塞。 后者导致影响新的连接接入。单线程进行
NIO
accept()和read() 调用完后 有数据马上返回,没数据返回空,不阻塞。 优点:解决多线程问题 缺点:当有1000个连接,只有一个请求是需要操作,需要遍历整个连接查询。每次查询连接是 用户态切换到内核态,性能消耗大。
特性 new io 非阻塞io 是指调同内核非阻塞
AIO 异步非阻塞 将读 写io操作 连接都让内核监听
selector,poll,epoll区别
select poll 两者区别是 select上线1024个连接,poll无上限,概念就是 将多个连接合并成一次传输给内核,解决NIO一个一个去调用内核查询的问题。 缺点:1.重复去给内核送相同连接 内核没有存 2.内核需要重复遍历整个连接
epoll :liunx系统下selector 优先使用这种模式 与select 和poll 处理不一样。 将 accept read write事件交给内核监听。 通过whlie(selector.select(timeout)>0) 方式监听,select方法等于wait() 等待内核检测到有时间了就通知唤醒select方法,处理事件大于0就开始处理 连接,读 ,写。 当有新的连接 epoll会将新的连接存入内核中进行监测。 读 写 也是调内核进行操作。 Nginx,redis 底层使用epoll监听连接
mmap
在内存中分配一块用户空间和内核空间共享空间,用户空间将复制到共享空间 内核直接操作发送 不需要再往内核拷贝一遍。
引用
强引用: 内存溢出都不会回收
弱引用:调完就回收
软引用:内存不足了就优先被回收,适合做缓存
虚引用:管理直接内存, 直接内存指的是jvm中有一块内存的指向内核内存。 虚引用只能用于检测对象,无法主动回收,由jvm才能回收
时间复杂度
常数阶$O(1)$
值不会因为某个值增长而增长,一直保持不变
线性阶$O(n)$
随着其他值增加 也跟着增加 你加一 我加一
平方阶$O(n^2)$
以平方增加
立方阶$O(n^3)$
对数阶$O(logn)$
线性对数阶$O(nlogn)$
指数阶$O(2^n)$
双重验证
单例对象 对象加上volatile关键字
1.判断是否为空
2.synchronize 上锁
3.判断是否为空
4.new对象 返回
volatile 关键字
禁止指令重排序
对象创建过程
1.new 对象 分配内存
2.调用构造函数,init
3.引用指向
防止2 3位置互换,导致引用的对象是半初始化状态
内存屏障
StoreStoreBarrier volatile 写操作 StoreLoadBarrier 必须做完写操作才能进行 load操作
LoadLoadBarrier volatile 读操作 LoadStroreBarrier 必须做完读操作此案进行Strore操作
可见性
当值被修改了 通知内核同步最新的值
hostpot实现
ThreadLocal
用于当前线程内共享使用,线程内的局部变量
entry对象的 key是弱引用,当引用中断了 对象就会被回收
ThreadlocalMap 存放共享对象,key为当前线程
Map允许NULL值为key值,有内存泄漏的风险,调用完后需要调用remove()方法 清除null值
把entry对象的key设置为null ,因为value对entry对象是强引用,当线程一直没被回收,Map一直存在thread中。其他请求在拿到这个线程就可以访问到这个null值的entry
底层数据结构:entry[]数组
CAS 自旋锁
CAS(Compare and swap)比较和替换 1.查询拿到当前的值, 2.在查询一下最新的值 进行比较值是否被修改 ,是 就返回到1 步骤 3.没有被修改 就进行修改 通过cas实现自旋锁机制,需要对cas方法进行循环
ABA问题
在并发场景下 线程1拿到值 a=1 ,线程2进行修改 a=2 ,线程3 修改回 a=1, 线程1在进行修改前 查询 发现还是a=1 ,然后进行修改。 其实已经被其他线程修改了很多遍了,并不是最初拿到的那个对象。
解决方案:给每次修改 上版本号 +1
AtomicInteger
底层实现通过CAS原理实现,通过unsafe类 直接通过内存操作数据
unsafe类 在jdk9版本后无法被实例 或继承,仅限jvm调用 unsafe.compareAndSwapInt()方法 进行自旋
synchronize
锁升级 偏向锁 → 轻量级锁(默认10次自旋)→重量级锁
允许重入
规范:上锁对象 必须是final 不能被修改
子主题
LongAdder
通过数组 进行 分段上锁
将所有请求分配到数组上某个空间,每个空间都独立统计数量,并上锁。
最后对所有空间计算出来的值进行sum ,得出总数
ReentrantLock 重入锁
概念 允许拿到锁的当前线程,可以再次拿到锁。 重入锁每次拿到锁会记录一次重入次数 进行加1操作
公平锁
Lock lock=new ReentrantLock(true)
设置拿锁等待时间
lock.tryLock(timeout)
设置可以被打断的锁
lock.lockinterupptibly()
解锁
lock.nulock()
创建线程队列 FIFO 先进先出
底层调用AQS进行创建队列
lock.newCondition()
condition.await() 将当前线程加入队列中 并阻塞等待
condition.signal() 唤醒队列第一个线程 condition.signalAll() 唤醒默认唤醒的也是第一个线程
AQS
模板方法
volatile int state 一般计数用 公平锁 重入锁 0是没有线程占锁,1已有线程占住锁
FIFO队列 先进先出队列
存入值 使用CAS
六个操作
抢锁
释放锁
入队
出队
阻塞
释放
公平锁
等待队列
node
waitstatus 等待状态 当前入队node,在进行park前,会先将上一个node的waitstatus改成-1,告诉他释放锁以后要unpark通知自己。 waitstatus=0 后面没有节点,不需要unpark通知,一般是队列最后一个节点
放入队列流程 1.先判断是不头node ,
2.
流程
先尝试获取锁, 判断AQS.state是否=0 是,就看一下等待队列是否有线程, 队列没有 就进行修改state=1 成功就返回true 获锁成功 AQS.state=1 当前锁被占用 把自己放入队列等待
非公平锁
1.cas 判断 AQS.state是否=0,是就进行修改为1 抢锁
2.释放锁 修改state=0
exchanger 交换器
帮助两个线程进行交换参数。
什么叫做阻塞队列的有界和无界
有界队列
LinkedBlockingQueue 链表阻塞队列 容量最大 2的31次方
ArrayBlockingQueue 数组阻塞队列 容量满了就帮您扩容 不能继续放数据
存放数量有限
无界队列
ConcurrentLinkedQueue 并发链表队列 无锁队列 底层使用CAS实现
put 操作永远都不会阻塞,空间限制来源于系统资源的限制
底层都使用 CAS 无锁编程
熔断
熔断状态
强制熔断 ALL
强制放行 ALL
部分开启 PART
熔断维度
商户号
全量商户白名单 ,所有商户都支持熔断
单商户白名单,只要在白名单中的商户就支持熔断
不在白名单 强制放行
配置表
优先级:子配置表>总配置表
总配置表
子配置表
熔断配置获取 优先级从小到大
1.本地缓存 启动服务时候将配置从库中查出缓存到Map中
2.OPS配置中心缓存,本地缓存获取不到,就尝试在ops中心获取,支持灵活配置熔断
3.redis缓存 1和2都没获取到 ,从redis缓存中获取
重启
重启模式
全部重启
清除熔断计数
清除总熔断状态
清除单批次熔断状态
清除批次已熔断日志表标识
部分重启
清除熔断计数
更新单批次熔断状态为"未熔断"
清除批次熔断日志表的标识
什么幂等性?
用户在相同参数的操作,请求了多次,最终返回的是第一次操作的结果。
什么是赋值 浅拷贝 深拷贝?
赋值是将对象在栈的地址赋值给引用对象上,不会复制数据。
浅拷贝 :A a=new A(); A b=a; 当a里面的值发生改变 b也会发生改变。只是a对象的地址指向了b
深拷贝
clone方法重写
创建一个新的对象,同时将旧对象的值对新对象复制进去。
粒度 数据库概念
如时间的粒度 一天 一周,一个月,一个季度,一年。
行级锁,表锁 对操作的数据范围的粒度
netty发生TCP粘包,拆包
原因: 1.写入的数据大于套接字的缓冲区大小,会发生拆包问题 2.写入的数据小于套接字缓冲区大小,并多次发送数据到网络上,会发生粘包问题 3.进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包问题 4.接收方法不及时读取套接字缓冲区的数据,和新发生的数据发生了粘包问题
Netty解决方法:
1.行拆包器-LineBasedFreamDecoder \r\n 根据换行符号 将数据拆成一个数据包
2.基于分隔符的拆包器DelimiterBasedFrameDecoder 根据自定义符号,将数据拆成一个数据包
3.基于定长度的拆包器FixedLengthFrameDecoder 根据设置特定长度,将数据拆成一个数据包
4.httpServerCodec http编解码器
单例模式
饿汉式
懒汉式
枚举
静态内部类
双重校验
线程状态
新建 new
就绪 runnable
运行 run()
阻塞 block()
将线程放入同步队列
有限期等待 sleep()
无限期等待 wait()
将线程放入等待队列
策略模式
定义一个接口,由很多个类来实现这个接口方法,客户调用通过接口调用,只需要传入这个实现的类就可以,这样可以相互替换。不会影响到客户的调用。
责任链
是一种请求模式,它让多个处理器都有机会处理请求,直到成功。责任链模式把多个处理器串成链,然后让请求在链上传递。
MySQL日常怎么调优的?
查询关联表不能超过四个
常用字段查询,要创建索引
组合索引是否满足索引左前缀
什么是java序列化?
序列化就将java对象存到某地方 比如硬盘,网络。也就是对象流化
反序列化 就是把二进制数据反序列化达成对象数据
为什么要序列化? 答案 方便传输,存储
notify与notifyAll方法为什么是在顶层Object类中?
在 Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object 类的一部分,这样 Java 的每一个类都有用于线程间通信的基本方法。
java中有哪些实现锁的方式,有什么区别
synchronize同步代码块
重入锁 ReentrantLock
读写锁
lock接口
Condition接口
Stop-The-World jvm暂停工作状态
jvm中一种全局暂停的状态,jvm挂起
导致原因:大部分是GC垃圾回收导致 其他原因 死锁检查,dump线程 堆Dump
GC停顿,是为了更有效的找到需要会收到垃圾,并整理内存空间
危害:长期时间服务停止,没有响应 新生代gc时间段,危害小 老年代gc有时候短,但回收对象量大了会导致停止几秒或者几十秒 堆越大花的时间越长
TCP 四次挥手中 CLOSE-WAIT、TIME-WAIT 分别都发生在什么时候?作用是什么?
CLOSER-WAIT(半关闭状态) 发生在第二次挥手, 这是服务端告诉客户端,收到关闭请求,请等待。
TIME-WAIT 发生在第四次挥手,让连接继续保持连接。保证第四次挥手失败,服务端会重试第三次挥手,能接受到消息。 问题:TIME-WAIT会一直占用端口,当这个状态频繁占用,可能会导致所有端口被占满。解决方法 重用TIME-WAIT或快速回收
HashMap
面试题
为什么装填因子要去0.75
0.75 的意思就是当数组长度达到了百分之75就进行扩容。 当装填因子是1 当容量达到百分百就扩容,很容易造成hash碰撞 当装填因子是0.5 百分之五十扩容,空间利用率不高,导致频繁扩容。 0.75是比较折中的方案
扩容为什么是2的N次方?
原因1:hashMap计算下标位置,通过h & (length-1) 计算 与运算效率更高,等效h%length 取余操作,等效前提length必须是2的整数倍。 与运算速度是求余计算的十几倍。
原因2:防止哈西冲突,位置冲突 为什么要2的整数倍-1 因为 16的二进制 10000 32的二进制 100000 31的二进制 11111 进行与运算,两个同时为1,结果为1,否则为0。 得出来的结果会永远是0
数据结构
1.7jdk 键值对 数组加链表 哈希表 。 7上8下 jdk7 头插法,jdk8 将元素插入尾部
jdk1.8 数据加链表 加红黑树 链表长度达到8并且节点个数达到64 才会升级到到红黑树 红黑树退化到链表 当红黑树大小是6 就退化链表
升级红黑树 因为链表一但很大 要遍历整个链表查找,效率低下。 红黑树 及平衡 有序, 插入和查询性能的平衡点 二分查找
AVL 平衡二叉树
1.7默认长度16位 无论设置多少,一定是2的整数倍 循环capcity(容量)<<=1; 2 4 8 16
threshold(阈值) =Math.min(capcity(容量)*loadFactor(装填因子),MAXIMUM_CAPCITY(最大容量1<<30)+1) min返回两个参数最小的值
通过对hashcoed 进行二次散列后才能得到最终的存放的地址 扰动函数:增加值的的不确定性。
jdk1.8 new HashMap() 只是实例了对象,设置了装填因子。并没有创建数组
put方法, 1.对key进行hash散列算法 2. 先比较hash值, hash值一致, 3.在比较对象类型是不是一样的。 是 就替换, 否,就生成链表添加进去。 jdk1.8 put的时候创建数组
jdk1.8 hash 散列算法 高位与低位进行异或算法 1010111111000010011100100 hashcode 25位 101011111 hashcode向右位于16位 1010111111000010110111011 hashcode异或运算 异或运算 两个操作数的位中比较 相同返回0 不同返回1
jdk node数组 计算下标 通过与运算取模 h & (length-1)
jdk1.8 扩容机制
快速计算扩容后的下标: 1010111111000010011100100 hashcode 101011111 hashcode向右位于16位 1010111111000010110111011 hashcode异或运算 hashcode & (length-1) 运算后得 length=8 index=3 (length<<1) 左移1位 length=16 index=11 扩容后长度增加 11-3=8位 快速法 当前下标+当前数组长度=新数组长度 3+8=11 所以直接下标加当前容量长度,等于新数组的下标
扩容 单向链表 next记录下个元素 链表进行尾插法
Node<K,V> loHead = null, loTail = null; 低位 Node<K,V> hiHead = null, hiTail = null; 高位 与运算 hashcode &oldcap(原容量)=0 低位 否则高位 hahscode xxxx01111 原容量 xxxx10000 与运算 00000 =0 低位 hashcode xxxx11111 原容量 xxxx10000 与运算 10000 =16 高位
放值过程 第一次放值 loHead → A loTail → A 第二次 loHead →A A.next →B loTail →B 第三次 loHead →A A.next →B B.next →C loTail →C
HashMap线程不安全,因为有数据覆盖的问题: 然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失。
concurrentHashMap
MAX_ARRAY_SIZE = Integer.MAX_VALUE-8 ,减去的8位空间是为了存储 对象的头信息 和指针
put() 1.首次需要创建 node[] 2.进行key的hash散列算法 3.然后通过CAS进行替换值 4.统计count 5.达到阈值 进行扩容transfer()
扩容
和hashMap区别,hashMap是与运算hashcode & (length-1) ,concurrentHashMap: length=16 取值范围0-15(1111) ,4位 length=32 取值范围0-31(11111) ,5位 xxx01101 13 xxx11101 29 xxx01101 13 在长度16的时候,取后4位为下标,都是13。29发生了哈希碰撞 扩容到32, 根据hashcode的倒数第5位判断, 等于0就跟原数组的下标一致,当前值在低位。 等于1表示原数组的下标加上原数组的长度,就是当前值在新数组的下标位置,当前值在高位。
根据数组长度除于cpu核心数,如小于16 默认使用单线程处理,大于16个筒使用多线程,保证每个线程平均分配。 在扩容时会锁住整个hash表。
数据结构:Node数组+链表+红黑树
与hashMap的区别
1.put:hashMap比较完hashcode和对象后直接替换值 concurrentHashMap 是通过CAS机制进行替换值 2.扩容计算下标: hashMap用与运算的 concurrentHashMap 通过指定位的值 确定是高位还是地位 3.线程安全问题 :hashMap有线程安全问题 数据会被覆盖 concurrentHashMap 没有线程安全问题, 通过CAS去覆盖值
加密 加签验签
RSA 公钥 私钥
MD5 生成摘要 不可逆 验证数据是否被篡改
wait 和sleep的区别?
索引在什么场景下使用,什么场景下用不到。
Java 中变量不一定要初始化?
在方法中一定要初始化局部变量,全局变量就jvm在创建对象的过程中会给默认初始化
sleep和wait()区别
sleep 不会释放对象锁
wait会释放对象锁,将线程放入等待队列。需要通过notify方法唤醒。只能在synchronized中使用
类初始化加载顺序
静态代码块>main()>构造函数>普通代码块
重写和重载的区别?
重载 方法名一致 参数列表不一致
重写 方法一致 参数列表 返回类型都一致。但只发生在子类和父类,子类重写父类的方法。
抽象类和接口的区别
抽象类的方法可以是抽象也可以是具体的 接口所有方法都是抽象的
抽象类的方法 可以是private public 默认 protected 接口所有成员都是public
调用GC的方法
System.gc()
Runtime.getRuntime().gc()
final 有什么用法
修饰类
修饰方法
修饰变量