目录
JVM
基础概念
1.我们写出的java程序不能被机器直接识别执行,需要经过文件编译,编译顺序如下:
(1)通过JDK的javac,将java文件编译为字节码(.class)文件
(2)通过JVM将字节码文件编译成能被电脑识别的文件
2.java语言的跨平台性
java文件会先被编译成字节码文件,然后再由不同机器上不同的JAVA虚拟机(JVM)编译为可供当前机器运行的文件。
即字节码文件并不直接在机器上执行,而是由机器上的JAVA虚拟机编译后再执行,而每个系统平台都有自己的JVM,故能实现java的跨平台。
3.JDK(JAVA开发工具包)是java核心,包括了JRE(java运行环境)与jdk工具包(例如:java,javac,javadoc等),JVM则是java虚拟机
4.JVM子系统分为类加载器,运行时内存区与执行引擎
类加载器
类加载器分为启动类加载器,扩展类加载器,系统类加载器,自定义加载器。
启动类加载器
BootstrapClassLoader,用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(C++实现),无法被java程序直接引用
扩展类加载器
ExtensionsClassLoader,用来加载 Java 的扩展库,由Java语言实现的,是Launcher的静态内部类。Java 虚拟机的实现会提供一个扩展库目录,该类加载器在此目录里面查找并加载 Java 类
系统类加载器
AppClassLoader,负责在JVM启动时根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。程序可以通过ClassLoader.getSystemClassLoader()来获取系统类加载器,一般用户自定义的类加载器都以此类加载器作为父加载器,由Java语言实现。
【上述三个类加载器是JDK默认的三个类加载器,按照顺序前者是后者的父加载器,需要注意的是这里的父类并不是JAVA中的父子继承关系,而是由类加载器中有一个parentClassLoader字段设置的值指明的父加载器。】
自定义加载器
通过继承 java.lang.ClassLoader类的方式实现
类装载执行顺序
执行分为五个步骤:加载,验证,准备,解析,初始化。(也可分为加载,连接,初始化三步,其中连接包括验证,准备,解析,连接状态负责将类的二进制数据整合到JRE中)
加载:根据查找路径找到相应的 class 文件然后导入,会生成java.lang.Class对象。
验证:检查加载的 class 文件的正确性;
准备:给类中的静态变量分配内存空间;
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。
(符号引用可以理解为一个标示,而在直接引用直接指向内存中的地址)
初始化:对静态变量和静态代码块执行初始化工作,即执行执行类构造器<clinit>
() 方法。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。
双亲委派机制的好处:
1.避免重复加载
2.防止核心API库被篡改
运行时内存区
运行时内存区分为数据区与指令区
数据区
数据区为线程共享数据区,可分为方法区和堆。
方法区用于存储已被Java虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆是JVM所管理的内存中最大的一块,在虚拟机启动时创建,用于存储对象实例。
指令区
数据区为线程独立数据区,可分为程序计数器,本地方法栈,虚拟机栈。
程序计数器用于保存当前线程所正在执行的字节码指令的地址/行号,用来保证每条线程切换后能回到当前正在执行的地方,所以是线程独立数据区。
虚拟机栈中包含多个帧栈,每个帧栈中存储局部变量表、操作数栈、动态链接、方法出口等信息,帧栈在方法被执行时创建(注意java虚拟机是线程私有,生命周期和线程相同)。
本地栈和虚拟机栈大体相同,区别为虚拟机栈用来管理java方法,本地栈用来管理本地方法,在方法前会带有native关键字。
垃圾收集器
JVM中的垃圾回收线程,在正常情况下是不会执行的(线程优先级最低),在虚拟机空闲或者当前堆内存不足时会触发执行,进行垃圾对象的回收。不需要程序员手动操作,由JVM自动执行。
判断对象是否存活
GC判断对象是否存活一般有两种方法:
1.引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。它有一个缺点不能解决循环引用的问题;
2.可达性分析算法:从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的。
垃圾回收算法
一般有四种算法:
1.标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。
2.复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。
3.标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。
4.分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。(堆中一般1/3为新生代,2/3为老年代)
(下图是学习时自己列出的思维导图,有点模糊,用网页版可以大概看清)
多线程
线程状态
线程中最重要的线程状态及其转换,如下图所示:
有几点需要注意:1.线程状态分为五种:创建,就绪,运行,阻塞,死亡
顺便一提进程的状态也是这五种,进程和线程的具体区别如下:
进程:资源分配最小单位,包含至少一个线程,每个进程都有独立的代码和数据空间,进程间的切换有较大开销
线程:CPU调度的最小单位,同一类线程共享代码和数据空间,线程有独立的运行栈和程序计数器,线程间的切换开销小
2.start方法是将线程置于就绪态而非运行态
3.每个程序运行至少启动两个线程,即main线程与垃圾收集线程
4.创建一个线程有四种方式:继承thread类,实现runable接口,实现callable接口,通过线程池创建
5.线程阻塞状态分为三种:等待阻塞,同步阻塞,其他阻塞
等待阻塞:wait()方法执行,用notify() 方法或 notifyAll()方法唤醒,属于Object类
同步阻塞:线程在获取对象的同步锁时如果该同步锁正在被其他线程占用,则进入同步阻塞
其他阻塞:例如sleep()方法,join()方法,I/O请求时(注意sleep(),join(),yield()属于Thread类)
其中需要注意的是wait方法会释放锁,sleep方法不会释放锁,而I/O操作会释放,join方法会释放Thread锁而不会释放Object锁(底层调用的是wait方法)
线程创建
继承thread类
public class ThreadThread extends Thread {
private String name;
int i = 5;
public ThreadThread(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始");
while (i > 0){
System.out.println(name + "运行 : " + i);
i--;
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "结束");
}
}
实现runable接口
public class ThreadRunable implements Runnable {
private String name;
int i = 5;
public ThreadRunable(String name) {
this.name = name;
}
@Override
public void run() {
while (i > 0){
System.out.println(name + "运行 : " + i);
i--;
try {
Thread.sleep((int) Math.random() * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
继承Thread类与实现runable接口区别在于
1.后者更适合多个线程处理同一个资源
2.后者避免了JAVA只能单继承的限制
3.线程池只能放实现Runable或Callable的线程,不能放继承Thread类的线程
另外callable和runable的区别在于call()允许返回值,能抛出异常,run()则不可以
关于第一点的代码实现:
继承Thread类
public void runanle() throws InterruptedException {
Thread A = new ThreadThread("A");
Thread B = new ThreadThread("B");
A.start();
B.start();
}
运行结果
Thread-8开始
A运行 : 5
Thread-9开始
A运行 : 4
A运行 : 3
A运行 : 2
A运行 : 1
B运行 : 5
Thread-8结束
B运行 : 4
B运行 : 3
B运行 : 2
B运行 : 1
Thread-9结束
实现runable接口
public void runanle() throws InterruptedException {
ThreadRunable threadRunable = new ThreadRunable("test");
new Thread(threadRunable,"A").start();
new Thread(threadRunable,"B").start();
}
运行结果
test运行 : 5
test运行 : 4
test运行 : 3
test运行 : 2
test运行 : 3
test运行 : 1
线程方法
1.join():等待该线程结束
e.g.
public void runanle() throws InterruptedException {
Thread A = new ThreadThread("A");
Thread B = new ThreadThread("B");
A.start();
A.join();
B.start();
}
运行结果
Thread-8开始
A运行 : 5
A运行 : 4
A运行 : 3
A运行 : 2
A运行 : 1
Thread-8结束
Thread-9开始
B运行 : 5
B运行 : 4
B运行 : 3
B运行 : 2
B运行 : 1
Thread-9结束
e.g.2
public void runanle() throws InterruptedException {
Thread A = new ThreadThread("A");
Thread B = new ThreadThread("B");
A.start();
B.start();
B.join();
}
运行结果
Thread-8开始
A运行 : 5
Thread-9开始
B运行 : 5
A运行 : 4
B运行 : 4
B运行 : 3
B运行 : 2
B运行 : 1
Thread-9结束
A运行 : 3
A运行 : 2
A运行 : 1
Thread-8结束
2.yield():让当前线程回到就绪态,需要注意的是该线程可以再次被线程调度程序选中运行
3.setPriority():更改线程优先级
A.setPriority(Thread.MIN_PRIORITY);//1
A.setPriority(Thread.NORM_PRIORITY);//5
A.setPriority(Thread.MAX_PRIORITY);//10
4.interrupt():使线程在无限等待时能抛出,从而结束
e.g.
public void runanle() throws InterruptedException {
Thread A = new ThreadThread("A");
A.start();
A.interrupt();
}
运行结果
Thread-8开始
A运行 : 5
A运行 : 4
A运行 : 3
A运行 : 2
A运行 : 1
Thread-8结束
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.csdn.controller.thread$ThreadThread.run(thread.java:46)
其他常见方法:
isAlive(): 判断一个线程是否存活。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。
setName(): 为线程设置一个名称。
线程分类
主线程:main()线程
当前线程:当前线程
后台线程:即守护线程,为其他线程提供服务的线程,也称为守护线程。(比如GC线程)“用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束”
前台线程:是指接受后台线程服务的线程
synchronized
1.synchronized即Java中的同步锁,可以修饰的对象有代码块,方法,类。
2.synchronized的作用通俗讲即是:当一个线程访问被synchronized修饰的代码时,其他想要访问这段代码的线程将被阻塞。
3.在使用中synchronized一般与wait(),notify()方法一起使用,即wait(),notify()方法必须存在于synchronized修饰的对象中。
4.synchronized不可继承:这里是指子类重写父类被synchronized修饰的方法时必须加上synchronized关键字否则synchronized不生效。
synchronized实例:多线程实现循环打印ABC
线程:
public class SynchronizedThread implements Runnable {
private String name;
private Object prev;
private Object self;
private SynchronizedThread(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
@Override
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
}
try {
prev.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
调用:
public void runanle() throws InterruptedException {
Object a = new Object();
Object b = new Object();
Object c = new Object();
SynchronizedThread pa = new SynchronizedThread("A", c, a);
SynchronizedThread pb = new SynchronizedThread("B", a, b);
SynchronizedThread pc = new SynchronizedThread("C", b, c);
new Thread(pa).start();
Thread.sleep(100);
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
Thread.sleep(100);
}
}
结果:
ABCABCABCABCABCABCABCABCABCABC
线程池
优势:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池工作流程
创建线程池
1.通过ThreadPoolExecutor创建,构造方法如下:
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize,//最大线程数
long keepAliveTime, //最大空闲时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler//饱和处理机制
)
构造函数有四种:
其中线程池的拒绝策略分为四种,即:
1 AbortPolicy(默认) : 丢弃任务并抛出RejectedExecutionException异常
2 CallerRunsPolicy: 由调用线程处理该任务
3 DiscardPolicy: 丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的方式
4 DiscardOldestPolicy: 丢弃队列中最早的未被处理任务,然后重新尝试执行任务
2.通过Executors静态类的方法创建,其中包含方法如下:
1 newSingleThreadExecutor:创建一个单线程的线程池,这个线程池只有一个线程在工作。
2 newFixedThreadPool :创建一个固定大小的线程池,每次提交一个任务就会创建一个线程,直到线程达到线程池的大小
3 newCachedThreadPool:创建一个可以缓存的线程池,如果线程池的大小超过了处理任务的线程,那么就会回收部分空闲线程
4 newScheduleThreadPool:创建一个大小无线的线程,线程池可以定时的以及周期的执行任务
线程池具体实现举例
这里使用经典的淘宝秒杀系统举例,例如:现在库存中有10件商品可供出售,有20个客户抢购,使用线程池实现这一例子。
任务类:
//任务类
public static class ThreadTask implements Runnable {
private static int count = 10;
private String name;
public ThreadTask(String name) {
this.name = name;
}
//线程具体实现
public void run() {
synchronized (ThreadTask.class) {
if (count > 0) {
System.out.println(name + "使用" + Thread.currentThread().getName() + "秒杀成功,剩余" + count--);
} else {
System.out.println(name + "使用" + Thread.currentThread().getName() + "失败,剩余" + count);
}
}
}
}
线程池实现分别使用上述的两种方式实现:
1.ThreadPoolExecutor实现
//线程池
@RequestMapping("/runanle")
public void Pool() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(15));
for (int i = 0; i < 20; i++) {
ThreadTask threadTask = new ThreadTask("客户" + i);
pool.submit(threadTask);
}
pool.shutdown();
}
结果:
客户0使用pool-1-thread-1秒杀成功,剩余10
客户3使用pool-1-thread-4秒杀成功,剩余9
客户1使用pool-1-thread-2秒杀成功,剩余8
客户2使用pool-1-thread-3秒杀成功,剩余7
客户4使用pool-1-thread-5秒杀成功,剩余6
客户5使用pool-1-thread-6秒杀成功,剩余5
客户6使用pool-1-thread-7秒杀成功,剩余4
客户7使用pool-1-thread-8秒杀成功,剩余3
客户8使用pool-1-thread-9秒杀成功,剩余2
客户9使用pool-1-thread-10秒杀成功,剩余1
客户10使用pool-1-thread-11失败,剩余0
客户11使用pool-1-thread-12失败,剩余0
客户12使用pool-1-thread-13失败,剩余0
客户13使用pool-1-thread-14失败,剩余0
客户14使用pool-1-thread-15失败,剩余0
客户15使用pool-1-thread-16失败,剩余0
客户16使用pool-1-thread-17失败,剩余0
客户17使用pool-1-thread-18失败,剩余0
客户18使用pool-1-thread-19失败,剩余0
客户19使用pool-1-thread-20失败,剩余0
2.使用Executors的newFixedThreadPool方法实现
//线程池
@RequestMapping("/Exrunanle")
public void ExPool() {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
ThreadTask threadTask = new ThreadTask("客户" + i);
pool.submit(threadTask);
}
pool.shutdown();
}
结果:
客户0使用pool-1-thread-1秒杀成功,剩余10
客户2使用pool-1-thread-3秒杀成功,剩余9
客户1使用pool-1-thread-2秒杀成功,剩余8
客户3使用pool-1-thread-4秒杀成功,剩余7
客户4使用pool-1-thread-5秒杀成功,剩余6
客户5使用pool-1-thread-6秒杀成功,剩余5
客户6使用pool-1-thread-7秒杀成功,剩余4
客户7使用pool-1-thread-8秒杀成功,剩余3
客户8使用pool-1-thread-9秒杀成功,剩余2
客户9使用pool-1-thread-10秒杀成功,剩余1
客户10使用pool-1-thread-11失败,剩余0
客户11使用pool-1-thread-12失败,剩余0
客户12使用pool-1-thread-13失败,剩余0
客户13使用pool-1-thread-14失败,剩余0
客户14使用pool-1-thread-15失败,剩余0
客户15使用pool-1-thread-16失败,剩余0
客户16使用pool-1-thread-17失败,剩余0
客户17使用pool-1-thread-18失败,剩余0
客户18使用pool-1-thread-19失败,剩余0
客户19使用pool-1-thread-20失败,剩余0
REDIS
REDIS是使用C语言编写的,开源的高性能非关系型(NOSQL)key-value键值对数据库。
快速原因
1.完全基于内存
2.数据结构简单
3.使用多路 I/O 复用模型,非阻塞 IO
4.采用单线程,避免了多线程中切换,锁,死锁,竞争等一系列可能造成的性能消耗的行为
5.使用底层模型不同,Redis 直接构建了 VM 机制,不同于其他客户端之间通信的应用协议(一般的系统调用系统函数的话,会浪费一定的时间去移动和请求)
数据类型
Redis键的类型只能为字符串,值支持五种数据类型:
1.STRING 字符串,整数,浮点数
2.LIST 列表
3.SET 无序集合
4.HASH 包括键值对的无序散列表
5.ZSET 有序集合
优缺点
优点
1.读写性能优异
2.支持数据持久化
3.支持事务
4.数据结构丰富
5.支持主从复制
缺点
1.数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上
2.Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复
3.主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性
54.Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂
高性能高并发
高性能:用户第一次访问数据从硬盘上读取,会比较慢。然后可以将用户访问的这些数据存储到缓存上,之后的操作都是直接操作内存,速度会非常快。
高并发:如果用户操作请求很大,可以考虑将一部分数据转移至缓存,直接操作缓存能承受的请求量要远大于直接访问数据库的
持久化
内存淘汰策略
过期键删除策略
(下图是学习时自己列出的思维导图,有点模糊,用网页版可以大概看清)