admin 管理员组文章数量: 1086019
Java核心技术汇总
文章目录
- 1、Java并发编程篇
- 必考点汇总
- 面试题汇总
- 线程的生命周期与状态流转
- Java线程的6种状态
- 线程的状态流转
- sleep、wait、notify、yield、join的区别
- 线程同步与锁
- 多线程同步与锁由来
- 线程同步解决方案
- Synchronized简介和用法
- Synchronized源码实现
- Synchronized的锁存储位置
- Synchronized的锁升级
- ReentrantLock可重入锁
- ReentrantLock的实现原理
- ReentrantLock的公平锁源码实现
- ReentrantLock的非公平锁源码实现
- ReentrantLock的使用
- Synchronized与ReentrantLock区别
- AQS
- AQS简介
- AQS资源共享方式
- AQS核心成员
- AQS数据模型
- AQS实现原理
- AQS独占式模式:获取锁过程
- Java内存模型
- 为什么需要JMM?JMM解决什么问题硬件内存结构?
- 谈谈并发问题:缓存一致性?
- 谈谈并发问题:指令重排?
- JMM的组成:工作内存和主内存
- JMM的happens-before(先行发生原则)
- JMM依靠那些关键字来实现?
- volatile的实现
- Volatile关键字
- Volatile的工作原理
- 指令重排
- 线程不安全的双重检查单例模式(DCL,Double Check Lock)
- 线程安全核心:CAS、ThreadLocal
- 线程安全核心
- CAS实现原理
- CAS的优缺点
- CAS的ABA问题
- CAS的自旋
- ThreadLocal概述
- ThreadLocal的内部结构
- ThreadLocal的内存泄漏
- 线程池
- 为什么需要线程池
- 线程池的组成结构
- 线程池的核心参数
- 线程池的创建种类与使用场景
- 线程池的任务处理流程
- 2、JVM篇
- JVM面试考点
- JVM体系结构
- JVM类加载
- JVM运行时数据区
- JVM的堆内存结构
- JVM垃圾回收算法
- JVM垃圾回收知识点
- JVM垃圾回收机制
- GC判断策略
- 标记-清除算法(Mark-Sweep)
- 标记-复制算法(Coping)
- 标记-整理算法(Mark-Compact)
- 分代收集算法(Generational Collection)
- 垃圾回收总结
- JVM垃圾收集器
- JVM垃圾收集器
- JVM收集器类别
- Serial串行收集器
- ParNew并行收集器
- Parallel Scavenge并行收集器
- CMS 并发收集器
- G1收集器
- ZGC并发收集器
- JVM垃圾收集器总结
- JVM性能调优
- 性能调优思路
- JVM内存泄漏分析
- JVM性能调优目标
- JVM调优参数
- JVM调优工具
- 3、集合篇
- Java集合类知识体系
- Java List必考点
- Java Map必考点
- Java List使用场景和区别
- ArrayList源码解析
- LinkedList源码解析
- Vector源码解析
- CopyOnWriteArrayList源码解析
- ArrayList、LinkedList、Vector、CopyOnWriteArrayList比较
- HashMap的底层结构
- 哈希表
- HashMap底层结构
- HashMap的put流程详解
- HashMap的面试考点解答
- Java Map的底层结构和使用
- HashMap
- HashTable
- ConcurrentHashMap的底层结构
- ConcurrentHashMap底层结构(JDK1.7版)
- ConcurrentHashMap底层结构(JDK1.8版)
- TreeMap的底层结构
- LinkedHashMap的底层结构
- Java Map面试专题
- 4、 MySQL篇
- 索引的类型与实现原理
- 索引原理
- 索引类别
- 索引数据结构
- B+树索引
- 聚集索引和非聚集索引
- MyISAM与InnoDB的索引实现
- 事务的特征与隔离级别
- 事务
- 事务的ACID
- 事务带来的并发问题
- 脏读
- 不可重复读
- 幻读
- 事务的隔离级别
- 数据库锁
- 数据库锁的类型
- 行锁的类型
- 表锁的类型
- 行锁的实现方式
- 乐观锁和悲观锁
- MySQL性能优化
- 性能优化方案
- 表优化
- 单表
- 架构
- 索引优化
- SQL优化
- SQL优化步骤
- 数据库拆分设计
- 数据拆分原则
- 垂直纵向拆分
- 水平横向拆分(分库分表)
- 垂直水平拆分
- MySQL主从同步与读写分离
- MySQL主从复制由来
- MySQL主从复制结构
- MySQL主从复制原理
- MySQL主从复制模式
- MySQL读写分离设计
- 分布式Session解决方案
- Session概述
- 分布式Session
- 分布式Session方案
- Session复制同步
- Session存储在Cookie
- Session粘性管理
- Session集中管理
- Session集中管理方案
- 分布式锁解决方案
- 分布式锁
- 分布式锁的特点
- 分布式锁解决方案
- 数据库分布式锁
- Redis分布式锁
- Zookeeper分布式锁
- 总结
- 分布式事务解决方案
- 分布式事务
- 分布式事务理论基石
- 分布式事务解决方案
- XA协议两阶段提交
- 事务补偿TCC模式
- 消息队列最终一致性
- 5、Redis篇
- Redis特征
- Redis高并发
- Redis数据结构
- Redis持久化
- Redis高可用
- Redis应用场景
- 6、缓存篇
- Redis VS Memcached
- 缓存雪崩
- 缓存穿透
- 缓存击穿
- 缓存与数据库一致性
1、Java并发编程篇
必考点汇总
- 掌握基本的线程的状态转换
- 线程状态转换的关系与区别:sleep、yield、wait、notify、notifyAll
- 掌握线程的同步的原因
- 掌握线程同步的实现方案,比如:线程锁、Synchronized与Lock
- 掌握AQS的实现原理
- 理解线程安全与核心:Volatile、CAS、ThreadLocal
- 掌握线程池的实现原理:核心参数、核心流程
- 了解JUC工具类的适用场景与实现原理
面试题汇总
线程的生命周期与状态流转
Java线程的6种状态
Thread.class定义了线程相关状态枚举
public enum State {/*** 新建*/NEW,/*** 可运行的*/RUNNABLE,/*** 阻塞*/BLOCKED,/*** 等待*/WAITING,/*** 有限等待*/TIMED_WAITING,/*** 终止*/TERMINATED;}
线程的状态流转
sleep、wait、notify、yield、join的区别
关键字 | 作用 |
---|---|
volatile | 线程操作变量可见 |
Lock | Java5.0增加的线程同步锁 |
synchronized | 线程同步锁 |
wait() | 让该线程处于等待状态 |
notify() | 唤醒处于wait()的线程 |
notifyAll() | 唤醒所有处于wait状态的线程 |
sleep() | 线程休眠 |
join() | 使当线程处于阻塞状态 |
yield() | 让出该线程的时间片给其他线程 |
- sleep(time)
- 使一个正在运行的线程处于睡眠状态,不会释放锁资源
- 就是说如果有synchronized同步块,其他线程仍然不能访问共享数据
- wait()、notify()、notifyAll()
- wait:使一个线程处于等待状态,释放CPU和锁资源
- notify:唤醒线程
- 这三个方法必须在
synchronized语句块内
使用 - wait和notify必须配套使用,即必须使用同一把锁调用
- wait与sleep的不同之处在于:wait会释放对象的"锁标志"
- yield()
- yield方法仅释放CPU执行权,锁仍然占用,从而让其他具有相同优先级的等待线程获取执行权,线程会被放入就绪队列,会在短时间内再次执行
- yield()方法不会释放锁
- Join()
- 调用线程等待该线程完成后,才能继续向下运行
线程同步与锁
多线程同步与锁由来
在Java并发编程中,经常遇到多个线程访问同一个共享资源,必须考虑如何维护数据一致性
多线程不安全案例:多线程中的i++
- 从内存中取出i的值
- 计算i+1
- 将结果写回内存
i++不是原子性操作。i++分为读取i值,对i值加1,再赋值给i,执行期中任何一步都是有可能被其他线程抢占的
Java内存模型中的可见性、原子性和有序性
(1)可见性
一个线程对共享变量值的修改,能够及时的被其他线程看到
(2)原子性
提供了互斥访问,同一时刻只能有一个线程来对它操作
(3)有序性
程序执行的顺序按照代码的先后顺序执行
线程同步解决方案
- Synchronized
- ReentrantLock
- Volatile
- ThreadLocal
- AtomicInteger
Synchronized简介和用法
- 同步、同步锁
(1)synchronized的作用
- 是保证在同一时刻,被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果
- 用synchronized修饰的代码具有原子性和可见性
(2)synchronized用法
-
实例方法:锁的是类的实例对象
public synchronized void method(){//代码 }
-
静态方法:锁的是类对象
public static synchronized void method(){//代码 }
-
代码块:锁的synchronized括号里配置的对象
synchronized(this){//代码 }
Synchronized源码实现
(1)同步方法
使用ACC_SYNCHRONIZED
标记符隐式的实现
(2)同步代码块采用
monitorenter(获取锁)、monitorexit(释放锁)两者虽然实现细节不同,但本质上都是对一个对象
(3)Synchronized实现同步的原理
- Entry List:那些有资格成为候选人的线程被移到Entry List
- Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
- Ower:获得锁的线程成为Owner
Monitor(对象监视器)获取锁
- 弱monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1.当前线程成为monitor的owner(所有者)
- 若线程已拥有monitor的所有权,允许它重入monitor,并递增monitor的进入数
- 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权
Synchronized的锁存储位置
- 类型指针:是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 标记字段:用于存储对象自身的运行时数据,如哈希码(hashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,它是实现轻量级锁和偏向锁的关键
Synchronized的锁升级
- 偏向锁:JDK6中引入的一项锁优化,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
-
轻量级锁:是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
-
重量级锁:是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁
ReentrantLock可重入锁
(1)ReentrantLock
- 可重入锁是指同一线程可以多次获取同一把锁
- 是一个基于AQS并发框架的并发控制类
(2)ReentrantLock引入了两个核心的概念
-
公平锁:类似于排队,多个线程之间讲究先来后到
-
非公平锁:进行锁的争抢,抢到就执行,没抢到就阻塞
// 构造器默认非公平锁,效率比公平锁高public ReentrantLock() {sync = new NonfairSync();}public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
ReentrantLock的实现原理
-
ReentrantLock是一个基于AQS并发框架的并发控制类
-
ReentrantLock内部实现了3个类,Sync继承AQS,FairSync和NoFairSync都继承自Sync,实现各种获取锁的方法tryAcquire方法
基本实现
- 先通过CAS尝试获取锁
- 如果此时已经有线程占据了锁,那就假如CLH队列并且被挂起
- 当锁被释放时,排在CLH队列对首的线程会被唤醒,然后CAS再次尝试获取锁
ReentrantLock的公平锁源码实现
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
获取当前线程步骤,acquires传值1:
- 获取AQS中的state,如果state为0,表示此时没有线程获得锁
- 在if判断中,先要判断AQS的有没有前置等待的Node队列
ReentrantLock的非公平锁源码实现
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
公平锁与非公平锁的区别
- 没有调用hasQueuedPredecessors判断队列是否有排在前面的线程在等待锁,这点体验出公平锁和非公平锁的不同,公平锁会关注队列里排队的情况,老老实实按照FIFO的次序,非公平锁只要有机会就抢占,才不管排队的事
ReentrantLock的使用
利用lock和unlock来实现线程同步
ReentrantLock reentrantLock = new ReentrantLock(true);
//设置为true为公平锁,默认为非公平锁
reentrantLock.lock();
try{//代码
}finally{reentrantLock.unlock();
}
ReentrantLock只能修饰代码块,使用ReentrantLock必须手动unlock释放锁,不然锁永远会被占用
Synchronized与ReentrantLock区别
-
用法不同
- Lock使用起来更灵活,但是必须有释放锁的配合工作
- Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁
- Lock只适用于代码块锁,而synchronized可以加在方法上,也可以加在代码块中,由JVM执行
-
性能不一样
- 在竞争不激烈时Synchronized关键字效率较高,原因在于,编译程序会通过锁升级优化synchronize,另外可读性非常好
- 同步非常激烈的时候,synchronized的性能一下子讲好了几十倍,而ReentrantLock确还能维持常态
- 高并发情况下使用ReentrantLock
-
ReentrantLock支持公平锁
- ReentrantLock支持公平锁和非公平锁
- Synchronized仅支持非公平锁
AQS
AQS简介
- AQS(Abstract Queued Synchronizer)就是一个抽象的队列同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它AQS是一个Java提供的底层同步工具类,用一个
volatile int
类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态 - AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,比如:
- ReentrantLock(重入锁)
- Semaphore(信号量)
- CountDownLatch(倒计时)
- 它维护了一个
volatile int state
来表示同步状态,通过内置的FIFO队列,该队列就是CLH同步队列,来完成线程等待排队 - CLH(Craig,Landin,andHagersten)根据三个人的名字命名的队列
AQS资源共享方式
AQS定义两种资源共享方式:
-
独占锁Exclusive
独占模式下时,其他线程试图获取该锁将无法取得成功,只有一个线程能执行,如ReentrantLock采用独占模式
-
共享锁shared
多个线程获取某个锁可能会获得成功,多个线程可同时执行,如Semaphore、CountDownLatch
AQS核心成员
- 核心的3个变量
//队头结点private transient volatile Node head;//队尾结点private transient volatile Node tail;//代表共享资源private volatile int state;
protected final int getState() {return state;}protected final void setState(int newState) {state = newState;}protected final boolean compareAndSetState(int expect, int update) {// See below for intrinsics setup to support thisreturn unsafe.compareAndSwapInt(this, stateOffset, expect, update);}
- head、tail、state三个变量都是volatile的,volatile是轻量级的synchronized,保证共享变量的可见性
- 通过CAS设置当前状态,保证状态更新的原子性
AQS数据模型
AQS的实现依赖于它维护了:
-
一个volatile int state(代表共享状态)
-
一个FIFO(双向队列)线程等待队列(多线程争用资源被阻塞时会进入此队列)
AQS实现原理
-
线程获取锁(同步状态)流程
-
线程A获取锁,state将0置为1,线程A占用
-
在A没有释放锁期间,线程B也来获取锁,线程B获取state为1,表示线程被占用,线程B创建Node节点放入队尾(tail),并且阻塞线程B
-
同理线程C获取state为1,表示线程被占用,线程C创建Node节点,放入队尾,且阻塞线程
-
-
线程释放锁(同步状态)流程
- 线程A执行完,将state从1置为0
- 唤醒下一个Node B线程节点,然后再删除线程A节点
- 线程B占用,获取state状态位,执行完后唤醒下一个节点Bode C,再删除线程B节点
AQS独占式模式:获取锁过程
获取锁的流程
-
当线程调用
acquire()
申请获取锁资源,如果成功,则进入临界区 -
没成功,则
addWaiter()
进入一个FIFO等待队列,然后被挂起等待唤醒 -
acquireQueued()
阻塞或者循环尝试获取锁
Java内存模型
为什么需要JMM?JMM解决什么问题硬件内存结构?
-
计算机处理器处理绝大多数运行任务都不可能只靠处理器计算,就能完成,处理器至少需要与内存交互,如读取运算数据、存储运算结果,无法仅靠寄存器完成所有的运算任务
-
随着CPU技术的发展,CPU的执行速度越来越快,但是内存的技术并没有太大的改进
-
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,为了避免处理器等待缓慢的内存读写操作完成,现代计算机系统通过加入一层读写速度尽可能接近处理器运算速度的高速缓存
-
缓存作为内存和处理器之间的缓冲,将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中
谈谈并发问题:缓存一致性?
谈谈并发问题:指令重排?
-
为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理,这就是处理器优化
-
除了现在很多流程的处理器会对代码进行优化乱序处理,很多编程语言的编译期也会有类似的优化,比如**Java虚拟机的即时编译期(JIT)**也会做指令重排
JMM的组成:工作内存和主内存
-
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量,也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题
-
工作内存
每条线程都有自己的工作内存(Working Memory,又称为本地内存),线程的工作内存中保存了该线程使用到的变量的主内存中共享变量的副本拷贝。工作内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
JMM的happens-before(先行发生原则)
- 程序顺序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
JMM依靠那些关键字来实现?
-
在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurrent包等,其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字等
-
在开发多线程的代码的时候,可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题
-
所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用
- Java的内存模型,要解决两个主要的问题:可见性和有序性
volatile的实现
Volatile关键字
- volatile是一个Java语言的类型修饰符
- 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证多线程下的可见性
- 禁止进行指令重排序(即保证有序性)
Volatile的工作原理
主内存和工作内存之间的交互有具体的交互协议,JMM定义了8种操作来完成,这8种操作都是原子的、不可再分的,分别是:lock、unlock、read、load、use、assign、store、write,其中:
- lock、unlock、read、write作用于主内存,load、use、assign、store作用于工作内存
- 从主内存复制变量到当前工作内存(read and load)
- 执行代码,改变共享变量值(use and assign)
- 用工作内存数据刷新主内存相关内容(store and write)
指令重排
-
为了优化程序性能,编译器和处理器会对Java编译后的字节码和机器指令进行重排序,通俗的说代码的执行顺序和在程序中定义的顺序会有些不同,只要不改变单线程环境下的执行结果就行
-
但是在多线程环境下,这么做却可能出现并发问题
线程不安全的双重检查单例模式(DCL,Double Check Lock)
获得的单例对象是未初始化的
前后对比
线程顺序
线程安全核心:CAS、ThreadLocal
线程安全核心
- CAS
- ThreadLocal
- Synchronized
- ReentrantLock
- Volatile
CAS实现原理
- CAS(Compare and Swap),比较并交换,也是实现平时所说的自旋锁或乐观锁的核心操作
- 它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回true。否则,返回false
- CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B
- 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B
实现原理如下所示
- 在内存地址V当中,存储着值为7的变量
- 线程1想要把变量的值增加1,对线程1来说,旧的预期值A=7,要修改的新值B=8
- 线程2抢先一步,把内存值V修改:8
- 线程1开始提交更新,首先对比了预期值A=7和实际值V的比较8,发现A不等于V的实际值,提交失败
CAS的优缺点
优点
- 非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高
缺点
- 自旋(循环)时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源
- 只能保证一个共享变量的原子操作,不能保证代码块的原子性。比如需要保证3个变量共同进行原子性的更新,需要synchronized解决。从Java1.5开始JDK提供了
AtomicRefernece
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作 - ABA问题
CAS的ABA问题
-
线程1准备用CAS修改变量值A
-
在此之前,线程2将变量的值由A替换为B,又由B替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功,但实际上这时的现场已经和最初不同了
解决方案
Java的原子类AtmoicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就会变成1A-2B-3A
CAS的自旋
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}
- AtomicInteger:递增计数器实现
- 该方法采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止
ThreadLocal概述
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
- 对于多线程资源共享的问题,同步机制采用了“遗失件换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而不影响
- 线程数据隔离的秘诀:Thread有个ThreadLocalMap类型的属性,叫做threadLocals,该属性用来保存线程本地变量,这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全
- ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal
ThreadLocal的内部结构
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}
static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}
- ThreadLocalMap是ThreadLocal的静态内部类,是Map类型
- 每个Thread线程内部都有一个Map
- Map里面存储线程本地对象(key)和线程的变量副本(value)
ThreadLocal的内存泄漏
- 因为ThreadLocalMap中的Entry的键ThreadLocal是
WeakReference
,即弱引用 - JVM中弱引用在垃圾回收时,不管内存有没有占满,都会被GC回收
- 因此很有可能在某次GC之后,某个线程的某个ThreadLocal变量变成了null,那么在Entry中,key也变成了null,在查找时将永远不会被炸到,这个Entry的Value将永远不会被用到,这就是内存泄漏
- 每次使用完ThreadLocal,都调用它的remove()方法,清除数据,使用ThreadLocal就跟加锁完要解锁一样,用完就清理
线程池
为什么需要线程池
线程池主要用来解决:线程生命周期开销问题和资源不足问题
- Web服务器完成网页请求的任务
- 用户的每一个请求到达就需要创建一个新线程
- 服务器在创建和销毁线程上的花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多
- JVM线程太多导致内存资源不足
- 如果在一个JVM里面创建太多的线程,可能会使系统由于过度消耗内存或切换过度而导致系统资源不足,每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大
为了防止资源不足,减少创建和销毁线程的次数,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因
线程池的组成结构
一个简单的线程池至少包括以下四个基本组成部分
-
线程池管理器(ThreadPool)
创建并管理线程池,包括:创建线程池、销毁线程池、添加新任务
-
工作线程(PoolWorker)
线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务
-
任务接口(Task)
每个任务必须实现接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
-
任务队列(taskQueue)
存放没有处理的任务,提供一种缓存机制
线程池的核心参数
线程池的创建种类与使用场景
线程池的任务处理流程
2、JVM篇
JVM面试考点
JVM体系结构
JVM类加载
JVM类加载器
-
启动类加载器(Bootstrap ClassLoader)
- 负责加载JVM虚拟机运行时所需的系统级别的类
- 会读取JAVA_HOME\lib下的jar包(如rt.jar)和配置,然后讲这些系统类加载到方法区内
-
扩展类加载器(Extension ClassLoader)
JAVA_HOME\lib\ext
-
应用程序类加载器(Application ClassLoader)
加载用户路径(classpath)上类库
-
用户自定义类加载器(Customized ClassLoader)
用户可以自己定义类加载器来加载类
JVM运行时数据区
-
JVM的内存空间分为3大部分
- 堆内存
- 方法区
- 栈内存
-
堆内存(新生代和老年代)
eden、Survivor Space(幸存者区)、老年代
-
方法区
存储类信息、常量(final)、静态变量(static)、JIT编译后的代码等不会变更的数据
-
栈内存(Java虚拟机栈和本地方法栈)
Java虚拟机栈:基本数据类型、局部变量、引用
JVM的堆内存结构
JVM垃圾回收算法
JVM垃圾回收知识点
JVM垃圾回收机制
GC判断策略
标记-清除算法(Mark-Sweep)
标记-复制算法(Coping)
标记-整理算法(Mark-Compact)
分代收集算法(Generational Collection)
垃圾回收总结
分类 | 标记-清除算法 | 复制算法 | 标记-整理算法 | 分代收集算法 |
---|---|---|---|---|
进行整理 | 否 | 是 | 是 | 是 |
算法实现过程 | 该算法分为两个过程:标记和清除。先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的要回收的对象 | 将内存按容量分为大小相等的两块区域,每次使用其中的一块,当一块的内存用完了,执行GC算法时将还存活的对象整理复制到另一块上,然后清理所有的内存块 | 该算法分为两个过程:标记和整理。首先标记出所有需要回收的对象,然后让存活的对象都向内存的一端移动,然后直接清除掉端边界以外的内存 | 根据对象存活周期的不同将内存划分为几块,一般是划分为新生代和老年代,然后根据各个年代的特点采用不同的最适当的收集算法 |
优点 | 简单,易于实现 | 内存分配时算法不产生内存碎片 | 内存分配时算法不产生内存碎片,也比较易于实现 | 分代收集,效率较高 |
缺点 | 效率低;会产生大量不连续的内存碎片 | 空间消耗太大,内存被压缩为原来的一半 | 算法复杂度大,执行步骤较多 | 算法复杂度大,执行步骤较多 |
适用情况 | 老年代 | 新生代 | 老年代 | 新生代、老年代 |
JVM垃圾收集器
JVM垃圾收集器
JVM收集器类别
Serial串行收集器
- 单线程收集器
- 用户线程STW
- 适用于单处理器
- 响应时间无要求的小型应用
ParNew并行收集器
-
Serial多线程版本
-
新生代收集器
-
复制算法
-
只有Serial与ParNew可以搭档CMS
Parallel Scavenge并行收集器
-
新生代收集器
-
复制算法
-
追求高吞吐量
-
适用于后台计算
CMS 并发收集器
G1收集器
ZGC并发收集器
Java11版本包含一个全新的垃圾收集器ZGC,一款低停顿、高并发的收集器
目标
- 在数TB的堆上具有非常低的暂停时间
- 垃圾回收停顿时间不超过10ms
- 无论是相对小的堆(几百MB)还是大堆(TB级)都能应对自如
- 与G1相比,吞吐量下降不超过15%
核心设计
- 读屏障
- 彩色指针
- 基于region
- 内存压缩整理
JVM垃圾收集器总结
收集器 | 类别 | 新生代/老年代 | 算法 | 目标 | 应用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制 | 响应速度 | 单CPU Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度 | 单CPU Client模式 |
ParNew | 并行 | 新生代 | 复制 | 响应速度 | 多CPU与CMS搭档 |
Parallel Scavenge | 并行 | 新生代 | 复制 | 追求吞吐量 | 后台计算不需要交互 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 响应速度 | 后台计算不需要交互 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度 | 互联网B/S用户交互 |
G1 | 并发 | 新生+老年代 | 复制+标记-整理 | 响应速度 | 替换CMS |
JVM性能调优
性能调优思路
JVM内存泄漏分析
JVM性能调优目标
JVM调优参数
Btrace:Java线上问题排查神器
JVM调优工具
3、集合篇
Java集合类知识体系
Java List必考点
Java Map必考点
Java List使用场景和区别
ArrayList源码解析
LinkedList源码解析
Vector源码解析
CopyOnWriteArrayList源码解析
ArrayList、LinkedList、Vector、CopyOnWriteArrayList比较
- 线程是否安全
- 线程安全:Vector、CopyOnWriteArrayList、Collections.synchronizedList()
- 线程非安全:ArrayList、LinkedList
- 数据类型
- 数组:ArrayList、Vector、CopyOnWriteArrayList
- 双向链表:LinkedList
- 容量
- ArrayList默认容量为10,LinkedList、CopyOnWriteArrayList默认容量为0
- ArrayList扩容1.5倍,Vector扩容默认为2倍
- 使用场景
- ArrayList底层为数组,适合随机访问,不适合频繁的在数组中间进行插入、删除的场景
- LinkedList底层为双向链表,适合频繁删除新增的场景,不适合随机访问,需要遍历
- Vector,线程安全,所有的方法都加锁,导致性能较差
- CopOnWriteArrayList线程安全,只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector
- 支持读多写少的并发情况,替代Vector
HashMap的底层结构
哈希表
HashMap底层结构
HashMap数据底层存储的是什么
HashMap的put流程详解
HashMap的面试考点解答
Java Map的底层结构和使用
HashMap
HashTable
ConcurrentHashMap的底层结构
ConcurrentHashMap底层结构(JDK1.7版)
ConcurrentHashMap底层结构(JDK1.8版)
TreeMap的底层结构
LinkedHashMap的底层结构
Java Map面试专题
4、 MySQL篇
索引的类型与实现原理
索引原理
- 索引在MySQL中也叫做“键”,是存储引擎用于快速找到记录的一种数据结构
- 索引的目的在于提高查询效率,减少查找过程中磁盘IO的存取次数
索引类别
索引数据结构
- R树索引
- B+树索引(InnoDB的索引)
- Fulltext索引
- Hash索引
B+树索引
特征
- 多叉树
- 非叶子节点存储key
- 叶子节点存储key和数据
- 叶子节点从小到大顺序两两相连
优点
- 非叶子节点只是存储key,占用空间非常小
- 可以容纳更多的节点元素,IO查询次数更少
- 叶子节点两两相连,符合磁盘的预读特性
- 支持范围查询,而且部分范围查询非常高效
聚集索引和非聚集索引
聚集索引
-
数据的物理存放顺序与索引顺序是一致
-
一个表只能包含一个聚集索引
-
聚集索引类似于电话簿
非聚集索引
- 索引的逻辑顺序与磁盘上行的物理存储顺序不一致
- 一个表有多个非聚集索引
MyISAM与InnoDB的索引实现
事务的特征与隔离级别
事务
- 事务就是要保证一组数据库操作(一组原子性的SQL集合),要么全部成功,要么全部失败
- MySQL中,事务支持是在引擎层实现,InnoDB
事务的ACID
-
原子性(Atomicity)
所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节
-
一致性(Correspondence)
数据必须保证从一种一致性的状态转换为另一种一致性状态
-
隔离性(Isolation)
指当多个用户并发操作数据库时,数据库为每一个用户开启不同的事务,这些事务之间相互不干扰,相互隔离
-
持久性(Durability)
事务一旦commit,则数据就会保存下来,即使提交完之后系统崩溃,数据也不会丢失
事务带来的并发问题
- 脏读
- 不可重复读
- 幻读
脏读
-
一个事务读取到另一个事务未提交的更新数据
-
如果另一个事务出现了异常回滚了事务,结果造成读取的数据不一致
不可重复读
-
在同一事务中,多次读取统一数据返回的结果不同
-
不可重复读和脏读不同的是这里读取的是已经提交过后的数据
- 事务A第二次查询之前读到了事务B的更新结果
- 破坏了事务的隔离性
幻读
- 一个事务读到另一个事务已提交的insert数据
事务的隔离级别
数据库锁
数据库锁的类型
(1)表级锁
- 开销小,加锁快,不会出现死锁
- 锁定粒度大,发生锁冲突的概率最高,并发度最低
- 使用表级锁定的主要是MyISAM
- 适用于查询为主,更新少的情况
(2)行级锁
- 好处是锁定对象的颗粒度很小
- 缺点是开销大、加锁慢
- 行级锁容易发生死锁
- InnoDB存储引擎采用行级锁
(3)页面锁
开销和加锁时间介于表锁和行锁之间,并发一般
BerkeleyDB存储引擎使用
行锁的类型
(1)共享锁(读锁)
- 多个事务对于同一数据可以共享一把锁,都能访问到数据
- 只能读不能修改
- 语法是:
select * from table lock inshare mode;
(2)排他锁(写锁)
- 对某一资源加排他锁,自身可以进行CRUD,其他人无法进行任何操作
- 语法:
select * from table for update;
- InnoDB引擎中update、delete、insert语句自动加排他锁
表锁的类型
意向锁
- 意向共享锁:事务想要对表中的某些记录加上共享锁
- 意向排他锁:事务想要对表中的某些记录加上排他锁
行锁的实现方式
MySQL InnoDB行锁是通过给索引上的索引加锁来实现的
- 记忆锁(Record Lock):单个记录上的锁
- 间隙锁(Grap Lock):锁定一个范围,但不包括记录本上
- Next-Key Lock:间隙锁+记忆锁,锁定一个范围,并锁定记录本身
乐观锁和悲观锁
(1)乐观锁
- 使用版本号等机制
- 适用于多读的应用类型,这样可以提高吞吐
(2)悲观锁
- 依靠数据库的锁机制实现
- 共享锁与排他锁数据悲观锁
MySQL性能优化
性能优化方案
表优化
单表
- 表设计时尽量符合第三范式
- 字段选择最合适的数据类型
- 字符:Char(固定长度)替换Varchar
- 性别:使用枚举或者整数代替字符串类型
- 年龄:尽量使用tinyint作为整数类型而非int
- 避免使用NULL字段,很难查询优化占用额外索引空间
- 单表不要有太对字段,建议在20以内
架构
- 读写分离
- 垂直拆分
- 水平拆分
- 缓存的使用
索引优化
- 索引,并不是越多越好
- where及order by涉及的列上建立索引
- 值分布很稀少的字段不适合建索引
- 字符字段最好不要做主键
- 避免在WHERE子句中对字段进行NULL值判断
- 联合索引最左前缀原则
- like以通配符开始无法使用索引
- 业务上具有唯一特性的字段,建成唯一索引
- 单表索引建议控制在5个以内
- 单索引字段数不允许超过5个
SQL优化
- 禁止使用Select *,只查询需要的字段
- 少用JOIN
- 尽量使用INNER JOIN,避免LEFT JOIN
- 多表连接时,尽量小表驱动大表,即小表JOIN大表
- 避免隐式类型转换
- 避免使用in not in
- 避免where子句中使用!=或者<>操作符
- 避免在where子句中对字段进行null值判断
- 避免大事务操作,提高系统并发能力
SQL优化步骤
数据库拆分设计
数据拆分原则
(1)优先使用缓存
- 降低对数据库的读操作
(2)其次数据库读写分离
- 降低对数据库的写操作
(3)最后考虑数据拆分
- 切分模式:垂直和水平
垂直纵向拆分
水平横向拆分(分库分表)
将同一个表按不同的条件分散到多个数据库或多个表中
(1)水平分表
(2)水平分库
垂直水平拆分
MySQL主从同步与读写分离
MySQL主从复制由来
(1)优先使用缓存
- 降低对数据库的读操作
(2)其次数据库读写分离
- 降低对数据库的写操作
(3)最后考虑数据拆分
- 切分模式:垂直和水平
MySQL主从复制结构
MySQL主从复制原理
MySQL主从复制模式
MySQL读写分离设计
分布式Session解决方案
Session概述
Session是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息
分布式Session
分布式Session方案
- Session粘性管理
- Session集中管理
- Session存储在Cookie
- Session复制同步
Session复制同步
设计思路
- 多个Web服务器之间相互同步session
- 每个web服务器包含全部session
优点
- 应用程序不需要修改代码
缺陷
- 同步合并过程复杂
- 广播式复制容易造成同步延迟
- 数据量受内存限制
- 无法水平扩展
Session存储在Cookie
设计思路
- session数据做加密,然后存储到cookie
优点
- 简单
- 不用考虑数据同步
- 服务器不需要存储
缺点
- 不安全,数据有被破解的风险
- cookie的存储容量比较小,只适用于session数据量小的场景
Session粘性管理
设计思路
- 负载均衡器的分发能力,将同一浏览器上同一用户的请求,都定向发送到固定服务器上
优点
- 只需要改Nginx配置
- 不需要修改应用代码
缺点
- 机器Down掉时用户Session会丢失
- 容易造成单点故障
Session集中管理
设计思路
- Session存储在数据库端或者缓存
优点
- 实现起来相对简单
- 效率很高
- 安全性也不错
- 中大型网站的首选方案
缺点
- 需要修改应用代码
Session集中管理方案
-
Memcache-Tomcat-Session
- 基于Memcache和Tomcat实现Session集中管理
- 实现起来比较简单,但与Tomcat耦合
- 不适用于其他的Web服务器
-
spring-boot+spring-session
- Spring提供的一套Session管理方案
- 通过一个SessionFilter将所有请求进行拦截
- 需要配合redis使用
分布式锁解决方案
分布式锁
分布式锁的特点
-
互斥性
任意时刻,只能有一个客户端获取锁,不能有多个获取锁
-
高性能
高性能的获取锁与释放锁
-
高可用
当部分节点down机时,客户端仍然能够获取锁和释放锁,具备锁失效机制,防止死锁
-
自解锁
加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给解了
分布式锁解决方案
- 数据库实现
- 缓存实现
- Zookeeper实现
数据库分布式锁
(1)悲观锁
select id from table for update(行级锁—>排他锁)
(2)乐观锁
版本控制,在表中引入一个版本号(version)字段
Redis分布式锁
(1)获取锁
基于Redis命令:SET lock_key lock_value NX PX timeout
- lock_key:获取的锁
- lock_value:锁的值
- NX:保证只有键不存在的时候插入
- PX timeout:设置键的过期毫秒数
(2)释放锁
使用Lua脚本释放锁:先进行get,再进行del
if redis.call("get",KEYS[1])==ARGV[1] thenreturn redis.call("dell",KEYS[1])elsereturn 0end
(3)利用客户端实现
- Jedis
- redisson
Zookeeper分布式锁
总结
-
实现的复杂度
Zookeeper > 缓存 > 数据库
-
高性能角度
缓存 > Zookeeper > 数据库
-
高可用角度
Zookeeper > 缓存 > 数据库
分布式事务解决方案
分布式事务
(1)按照业务为单位进行数据拆分
- 垂直拆分
- 水平拆分
(2)按照业务为单位提供服务
- 商品中心
- 交易中心
- 积分中心
(3)业务场景
比如:购买商品后扣款,有可能需要横跨多个服务,涉及订单、商品库存、积分等多个数据库,而这些操作又需要在同一事务中完成,这就涉及到了分布式事务
分布式事务理论基石
(1)CAP理论(强一致性)
-
一致性(Consistency)
更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致
-
可用性(Availability)
服务一直可用,而且是正常响应时间
-
分区容错性(Partition tolerance)
分布式系统在遇到任何网络分区故障的时候,仍然能够保证对外提供满足一致性或可用性的服务
(2)BASE理论
-
Basically Available基本可用
基本可用是指分布式系统在出现任何不可预知故障的时候,允许损失部分可用性
-
Soft state 软状态
允许系统中的数据存在中间状态,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
-
Eventually Consistent 最终一致性
-
系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性
分布式事务解决方案
分布式事务常用的解决方案如下所示:
- XA两阶段提交
- 消息队列最终一致性
- 事务补偿TCC模式
XA协议两阶段提交
事务补偿TCC模式
消息队列最终一致性
5、Redis篇
Redis特征
- 高并发性能
- 多数据结构类型
- 高可用集群模式
- 支持持久化
Redis高并发
(1)单线程
优点
- 代码更清晰,处理逻辑更简单
- 避免了不必要的上下文切换和竞争条件
- 不存在加锁释放锁操作
缺点
- 无法发挥多核CPU性能
方案
- 单线程多进程的集群
(2)IO多路复用
- Redis采用网络IO多路复用技术来保证在多连接的时候,系统的 高吞吐量
- “多路”指的是多个网络连接,“复用”指的是复用同一个线程
- 采用了epoll+自己实现的事件框架
- 复用技术可以让单个线程高效的处理多个连接请求
Redis数据结构
Redis持久化
Redis高可用
Redis应用场景
- 页面缓存
- 排行榜
- 计数器
- 分布式Session
- 分布式锁
6、缓存篇
Redis VS Memcached
- Redis是MC的一个加强版本
对比维度 | Redis | Memcached |
---|---|---|
性能 | 单线程,每一个核上Redis在存储小数据时比MC性能更高 | MC支持多核多线程,在100k以上的数据中心,MC性能要高于Redis |
数据存储 | 不仅支持KV类型,还提供了list、set、sorted set、hash数据结构 | MC支持简单的key-value存储 MC一个value最大只支持1MB,而Redis最大支持512MB |
可靠性 | 支持数据持久化,提供RDB和AOF两种持久化,允许单点故障,可以快速恢复数据 | MC只是内存缓存,对可靠性无要求,不支持数据持久化 |
分布式拓展性 | Redis偏向在服务器端构建分布式存储,比如:Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本 | 本身并不支持分布式,而是在客户端通过一致性哈希这样的分布式算法实现 |
应用场景 | 适用于对读写要求很高,对安全性要求较高的系统 | 适用于多读少写,大数据量的情况 |
缓存雪崩
缓存穿透
缓存击穿
缓存与数据库一致性
本文标签: Java核心技术汇总
版权声明:本文标题:Java核心技术汇总 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1686559693a10281.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论