计算机的基本组成
计算机存储模型(CPU、寄存器、高速缓存、内存、外存)
现代计算机系统CPU和内存之间其实是有一个cache的层级结构的。比内存速度更快的存储介质(SRAM),普通内存一般是DRAM,这种读写速度更快的介质充当CPU和内存之间的Cache,这就是缓存。当CPU写cache时,cache中没有相应的内存数据需要把该数据从内存加载到cache中,然后更新cache,再更新内存。
寄存器
CPU的一部分,空间在kb级别,用来暂存指令、数据和地址。CPU处理数据时先把数据取到寄存器中,再作处理。
内存
计算机运行过程中的存储主力,存储指令(编译好的代码段),运行中的各个静态,动态,临时变量,外部文件的指针等。程序的运行都是在内存中进行,内存的大小决定可运行程序的大小。
硬盘
机械硬盘(HDD): 磁性碟片 机械硬盘主要由磁盘盘片、磁头、主轴与传动轴等组成,数据就存放在磁盘盘片中,与留声机上的唱片相似(硬盘是上下双磁头)。 固态硬盘: 闪存颗粒、DRAM 闪存的固态硬盘(SSD): 可以移动,数据保护不受电源控制,寿命较长,能适应于各种环境:笔记本硬盘、微硬盘、存储卡、U盘等。 DRAM: 仿效传统硬盘的设计,高性能的存储器,理论上可以无限写入,需要独立电源来保护数据安全。可被大部分操作系统的文件系统工具进行卷设置和管理,非主流的设备。
数据与二进制的对应规则
计算机编码: ASCII: 一个字节byte有八个二进制位可组合出256种状态,每个状态对应一个符号,有256个符号。ASCII规定了128个字符(对英语字符与二进制位间做了统一规定) 非ASCII码: 不同国家,使用的字符不一致,仅128个字符不够用,所以针对本国字符特点,使用了256个字符。 Unicode: 该数字码全球统一,属于字符编码的标准,即字符集。nicode并未明确编码规则,根据Unicode字符集有常用的编码规则: UTF-8、UTF-16等。 UTF-8: 作为Unicode一种实现方式,使用1-4个字节进行编码。UTF-8与ASCII码表示的二进制数完全一致。
JVM
JVM内存模型
堆:
堆: 新生代、老年代。
老年代(Old)占2/3,年轻代(Young)占1/3,
年轻代中包含Eden区和Survivor区,Survivor区包含From(S0区)区和To(S1区),默认Eden区、From区、To区的比例为8:1:1,
当Eden区内存不足时会触发Minor gc,没有被回收的对象进入到Survivor区,同时分代年龄+1,
再次触发Minor gc时,From区中的对象会移动到To区,Minor gc会回收Eden区和From区中的垃圾对象,对象的分代年龄会再次增加,
当分代年龄增加到15以后,对象会进入到老年代。当老年代内存不足时,会触发Full gc,
如果Full gc无法释放足够的空间,会触发OOM内存溢出,在进行Minor gc或Full gc时,会触发STW(Stop The World)即停止用户线程。
非堆(永久代/元空间区):
常量(final修饰)、静态变量(static修饰)、类信信息(版本、字段等)、运行时常量池,操作的是直接内存。
运行时数据区(方法区、堆的生命周期与JVM一致,与线程无关)
1.程序计数器
存放下一条指令所在单元的地址的地方。
2.线程栈(虚拟机栈)
JVM的每一个线程对应一个线程栈,一个线程的每个方法会分配一块栈帧内存空间。栈帧超过了栈的深度,会报StackOverflowError。
局部变量表: 存储基本数据类型(int、float、byte等),如果是引用数据类型,则存储的是其在堆中的内存地址,也就是指向对象的一个指针。
操作数栈: 主要用来存计算过程的中间结果,计算过程中变量临时的存储空间。CPU会从操作数栈中弹出所需的操作数,计算后压入到操作数栈顶。
i++ 和 ++i 的区别:
1、i++: 从局部变量表取出 i 并压入操作栈(load memory),对局部变量表中的 i 自增 1(add&store memory),将操作栈栈顶值取出使用,如此线程从操作栈读到的是自增之前的值。
2、++i: 先对局部变量表的 i 自增 1(load memory&add&store memory),然后取出并压入操作栈(load memory),再将操作栈栈顶值取出使用,线程从操作栈读到的是自增之后的值。
i++ 不是原子操作, 1、i 从局部变量表(内存)取出,2、压入操作栈(寄存器),操作栈中自增,3、使用栈顶值更新局部变量表(寄存器更新写入内存),这3步可能被另一个线程打断,产生数据互相覆盖问题。
动态链接: 在Java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用存到class文件的常量池中。通过常量池中指向方法的符号引用来表示一个方法调用了其他方法时。动态链接是将符号引用转换为直接引用。
方法出口:
方法执行时有两种退出情况:
1.正常退出,即正常执行到任何方法的返回字节码指令,如 return、ireturn、areturn 等;
2.异常退出。
方法退出就返回至方法当前被调用的位置。
1.返回值压入上层调用栈帧。
2.异常信息抛给能够处理的栈帧。
3.PC计数器指向方法调用后的下一条指令。
3.本地方法栈
与虚拟机栈结构一致,本地方法栈执行的是Java底层由C++编写的native方法。
4.方法区
在JDK8之前Perm Space(永久代),JDK8中就是Metaspace(元空间)。
方法区中存储的有: 常量、静态变量、类信息、运行时常量池,操作的是直接内存。
方法区内存不足,会报OutOfMemoryError。
5.堆
堆是JVM所管理内存的最大一部分,在虚拟机启动时启动。
对象实例和数组都在堆上分配。堆内存不足,也会报OutOfMemoryError。
JVM中对象及常量、局部变量、全局变量的存储位置
1.局部变量
基本数据类型: 变量名和变量值存储在方法栈中。
引用数据类型: 变量值存在栈中(堆中对象的地址),指向的对象存储在堆中(如new出来的对象)。
2.全局变量
基本数据类型: 变量名和变量值存储在堆内存中。
引用数据类型: 变量名存储的是所引用对象的内存地址,变量名和变量值存储在堆内存中。
static关键字小结:
(1)特点:
1、static是一个修饰符,用于修饰成员(成员变量,成员函数)static修饰的成员变量 称之为静态变量或类变量.
2、static修饰的成员被所有的对象共享.
3、static优先于对象存在, 因为static的成员随着类的加载就已经存在.
4、static修饰的成员多了一种调用方式, 可以直接被类名所调用,(类名.静态成员).
5、static修饰的数据是共享数据, 对象中的存储的是特有的数据.
(2)成员变量和静态变量的区别:
1、生命周期的不同:
成员变量随着对象的创建而存在随着对象的回收而释放.
静态变量随着类的加载而存在随着类的消失而消失.
2、调用方式不同:
成员变量只能被对象调用.
静态变量可以被对象调用,也可以用类名调用(推荐用类名调用).
3、别名不同:
成员变量也称为实例变量.
静态变量称为类变量.
4、数据存储位置不同:
成员变量数据存储在堆内存的对象中, 所以也叫对象的特有数据.
静态变量数据存储在方法区(共享数据区)的静态区, 所以也叫对象的共享数据.
(3)静态使用时需要注意的事项:
1、静态方法只能访问静态成员(非静态既可以访问静态,又可以访问非静态).
2、静态方法中不可以使用this或者super关键字.
3、主函数是静态的.
类的初始化顺序: 静态代码 --> 父类构造函数 --> 变量初始化 --> 实例代码块 --> 自身构造函数
JVM的加载
Java文件的编译
Java文件通过JDK中的javac指令进行编译成成一个字节码(.class)文件
.java → 词法分析器 → tokens流 → 语法分析器 → 语法树/抽象语法树 → 语义分析器→ 注解抽象语法树 → 字节码生成器 → .class文件
类加载器的分类
启动类加载器(Bootstrap ClassLoader):负责加载%JAVA_HOME%/lib目录下的 jar 包和类。它只能加载自己能够识别的类。
扩展类加载器(Extendtion ClassLoader):它负责加载<JAVA_HOME>\lib\ext目录中的或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以使用扩展类加载器。
应用程序类加载器(Application ClassLoader):负责加载ClassPath上所指定的类库,如果应用程序没有自定义过自己的类加载器,一般情况下这就是程序的默认类加载器。
自定义类加载器(CustomerClassLoader):自己定义的类加载器,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader。
类加载机制:
JVM 把.class文件加载到虚拟机的内存中,并对数据进行验证、准备、解析和初始化,将这些内容转换成方法区中的运行时数据结构, 可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 装载–>链接(验证、准备、解析)–>初始化
1.装载(运行期完成) 通过全限定名获取类的二进制字节流。 将二进制字节流转化成方法区的运行时数据结构。 实例化对象,放入堆中作为这个类的访问入口。 2.连接 验证: 保证不会危害虚拟机的安全,包括文件格式验证、元数据验证、字节码验证、符号引用验证。 准备: 给类变量(static修饰的变量)在方法区中分配内存,初始化默认值,各个数据类型的默认值如下。 byte (byte)0 short (short)0 int 0 long 0L float 0.0f double 0.0d char '\u0000' boolean false reference null 解析: 将符号引用替换为直接引用 符号引用: 用一组符号来描述目标,可以是任何的字面量。 直接引用: 直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 3.初始化: 为静态变量赋值,执行静态代码块。
Java线程
线程(thread): 操作系统能够进行运算调度的最小单位。
Java线程的创建:
Java创建线程是采用内核线程: 由操作系统内核支持的线程,通过操纵调度器对线程进行调度(内核切换线程),将线程的任务映射到各个处理器上。 1.new Thread: 创建了一个对象 2.运行start()方法: 调native方法 —> 调C++ -> 调操作系统(通过操作系统的库函数创建的线程)。 Thread.c jvm.h jvm.cpp thread.cpp os_linux.cpp 优点:一个线程阻塞,不影响另一个线程的执行; 缺点: 1、由于是基于内核线程实现的,所以各种线程操作,如创建、休眠及同步,都需要进行系统调用,而系统调用的代价相对较高; 2、操作系统内核支持的线程数量是有限的,不能无限制地创建线程;
Go语言采是用户线程: 内核线程,都可认为是用户线程。用户线程的创建、同步、销毁和调度完全在用户自己的程序代码中完成,不需要内核的帮助,系统内核不能感知到用户线程的存在。 优点:在用户自己的程序中实现,不需要调用内核,操作非常快速且低消耗,能够支持规模更大的线程数量; 缺点:所有的线程操作都要由用户自己处理,线程的创建、销毁、切换和调度等,实现起来较复杂;
线程基本信息
线程ID(Thread ID): 线程的唯一标识符,同一个进程内不同线程ID不会重叠.
线程名称: 方便用户识别,系统会自动分配名称,也可以用户指定.
线程优先级: 表示线程调度的优先级,优先级越高活的CPU执行的机会就越大.
线程状态: 标识线程的执行状态,为新建/就绪/运行/阻塞/结束等状态的一种.
其他: 是否为守护线程等等.
程序计数器: 在线程的结构中,程序计数器很重要,它记录着线程下一条指令的代码段内存地址.
栈内存信息: 在线程的结构中,程序计数器很重要,它记录着线程下一条指令的代码段内存地址.
public class Thread implements Runnable {
private long tid; 线程的ID, 由线程自己分配
private volatile String name; 线程的名称, 可以手动设置(setName())
private int priority; 线程的优先级, 默认5, 最小1, 最大10
private ThreadGroup group; 线程所属的线程组
private ClassLoader contextClassLoader; 次线程上下文ClassLoader
private boolean daemon = false; 是否是守护线程
private volatile int threadStatus = 0; 线程的状态. 对应静态枚举类Stete
public void run(){} 线程的功能方法
public synchronized void start(){} 线程准备就绪,等待程序调用run()方法. 异步操作, 调用顺序不代表实际启动的顺序.
public final void stop(){} 终止当前线程, 不建议使用
public static native void sleep(long var0){} 指定毫秒数让正在执行的线程暂停, 具体取决于系统定时器和调度程序的精度和准确性
public final void setPriority(int var1){} 设置优先级, 不一定有用, 影响线程执行优先级的因素很多
public final void setDaemon(boolean var1){} 设置守护线程标识
public static native void yield(){} 放弃当前CPU执行资源,放弃时间不确定
public final native boolean isAlive(){} 判断线程是否处于活动状态. 活动状态: 启动未终止
public final void resume(){} 恢复线程, 不建议使用
public final void suspend(){} 暂停线程, 不建议使用
public void interrupt(){} 中断线程
public static boolean interrupted(){} 测试当前线程是否中断, 并且具备清除状态的功能
public boolean isInterrupted(){} 测试当前线程是否中断
public final void join(){} 等待调用的这个线程终止
public Thread.State getState(){} 获取线程状态
}
线程状态
新建(New)状态: 创建后尚未启动(未调用start()方法)的线程处于这种状态。 就绪(Ready)状态: 当调用线程对象的start()方法,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行。 运行(Running)状态: 运行状态(统线程状态中的 Ready 和 Running),该状态可能正在CPU执行,也可能在等待CPU执行。 注意: 线程要想进入运行状态执行,首先必须处于就绪状态中。 无限期等待(Waiting)状态: 处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态。 没有设置Timeout参数的Object.wait()方法 没有设置Timeout参数的Object.wait()方法 限期等待(Timed Waiting)状态: 处于这种状态的线程也不会被分配CPU执行时间,不过无须等待其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态: 设置了Timeout参数的Object.wait(long millis)方法 设置了Timeout参数的Object.wait(long millis)方法 阻塞(Blocked)状态: 线程在获取synchronized排他锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 死亡(Dead)状态: 线程执行完了或者因异常退出了run()方法,该线程生命周期结束。
public static enum State {
NEW, 新建
RUNNABLE, 就绪, 运行
BLOCKED, 阻塞
WAITING, 等待
TIMED_WAITING, 计时等待
TERMINATED; 结束
private State() {
}
}
线程池
Java线程的设计
1、工作任务/逻辑单元:(Runnable 和 Callable)
2、执行机制/载体:(Thread、Executor 框架)
线程池的好处
一: 降低资源消耗.
二: 提高性能.
三: 方便线程管理.
线程池七参数:
1.核心线程数(corePoolSize): 线程池中的核心线程数量(最少的线程个数),即使这些线程处理空闲状态,它们也不会被销毁,除非设置了 allowCoreThreadTimeOut; 默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
2.任务队列(workQueue): 任务队列,当核心线程全部繁忙时,由 execute/submit 方法提交的 Runnable 任务存放到该任务队列中,等待被核心线程来执行。
3.最大线程数(maximumPoolSize): 线程池中允许的最大线程数,当核心线程全部繁忙且任务队列存满之后,线程池会临时追加线程,直到总线程数达到 maximumPoolSize。
4.线程空闲超时时间(keepAliveTime): 如果一个线程处于空闲状态,并且当前的线程数量大于 corePoolSize,那么在指定时间后,这个空闲线程会被销毁。
5.时间单位(unit): keepAliveTime 的时间单位(天、小时、分、秒......)。
6.线程工厂(threadFactory): 用于创建线程,可默认也可以自定义实现。
7.拒绝策略(handler): 核心线程 corePoolSize 正在执行任务、任务队列workQueue 已满、并且线程池中的线程数达到 maximumPoolSize 时,就需"拒绝"掉新提交过来的任务。
JDK提供的四种内置的拒绝策略
AbortPolicy: 异常中止策略,异常中止,无特殊场景;
DiscardPolicy: 丢弃策略,无关紧要的任务(文章点击量、商品浏览量等);
DiscardOldestPolicy: 弃老策略,允许丢掉老数据的场景;
CallerRunsPolicy: 调用者运行策略,不允许失败场景(对性能要求不高、并发量较小的场景);
线程池的大小
1、CPU 密集型: 任务需要大量使用 CPU 进行运算,但是没有阻塞,CPU一直高速运行;该情况下应尽可能减少 CPU 对线程的切换,所以要使线程数尽可能少;
线程数 = CPU 核心数 + 1;
2、IO 密集型: 任务需要进行大量的 IO 操作,那么就会有大量的阻塞等待,CPU大部分时间是在阻塞,该情况下应尽可能减少阻塞所带来的消耗,所以要使线程数尽可能多;
线程数 = CPU 核心数 * 2;
线程池的底层状态切换(位运算)
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
COUNT_BITS = 29
CAPACITY = (1 << COUNT_BITS) - 1
线程池源码-线程池状态值
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
RUNNING = -1 << COUNT_BITS;
线程池关闭
shutdownNow() 1、停止接收新的任务请求; 2、将正在执行的任务 interrupt 中断; 3、忽略任务队列里面的任务,也就是任务队列里面的任务不会执行了;4、返回没有执行的任务集合; shutdown() 1、停止接收新的任务请求; 2、内部正在执行的任务和任务队列中等待的任务会继续执行直至完成; 3、等所有任务都执行完成后才会关闭线程池;
execute(Runnable command)方法:
/*
* 任务提交执行,在未来的某个时间执行给定的任务,任务可以在新线程或现有池线程中执行;
* 如果任务无法提交执行,要么因为此执行器已关闭或因为其容量已达到,则该任务由当前RejectedExecutionHandler处理;
*/
public void execute(Runnable command) {
//如果command是null,则抛出空指针异常;
if (command == null)
throw new NullPointerException();
//获取clt控制变量的值,clt控制变量记录着runState和workerCount的值;
int c = ctl.get();
/*
* workerCountOf方法获取控制变量ctl低29位的值,表示当前活动的线程数;
* 如果当前活动线程数小于核心线程数corePoolSize,则新建一个线程放入线程池中,并把任务添加到该线程中运行;
*/
if (workerCountOf(c) < corePoolSize) {
/*
* addWorker()方法:
* 第一个参数是要提交的工作任务;
* 第二个参数:
* 如果是true,根据corePoolSize来判断,表示添加核心线程;(保持稳定的线程数来处理任务)
* 如果是false,根据maximumPoolSize来判断,表示添加非核心线程;(应对突发的任务处理)
*/
if (addWorker(command, true)) //addWorker()方法会检查运行状态和工作线程数,如果返回false则说明线程没有创建成功;
//添加成功则返回;
return;
//如果添加失败,则重新获取控制变量ctl的值;
c = ctl.get();
}
//到这里了,说明workerCountOf(c) >= corePoolSize,并且如果当前线程池是运行状态并且工作任务添加到任务队列成功
if (isRunning(c) && workQueue.offer(command)) {
// 重新获取ctl值
int recheck = ctl.get();
// 再次判断线程池是否是运行状态,如果不是运行状态,由于之前已经把command添加到workQueue中了,此时需要移除该command;
if (! isRunning(recheck) && remove(command))
//线程池不是运行状态并且移除该command成功,则使用拒绝策略对该任务进行处理;
reject(command);
/*
* 线程池是运行状态,获取一下线程池中的有效线程数,如果是0,则执行addWorker()方法创建一个线程;
* addWorker()方法:
* 第一个参数为null,表示在线程池中创建一个线程,但不启动;
* 第二个参数为false,表示是非核心线程;
* 接下来这里没有写else,表示如果判断workerCount大于0,则不需要做什么处理,直接返回,
* 加入到workQueue中的command会在将来的某个时刻被执行;
*/
else if (workerCountOf(recheck) == 0)
//此处是创建一个线程,但并没有传入任务,因为任务已经被添加到workQueue中了,到时候线程会从从workQueue中获取任务来执行;
//所以当workerCountOf(recheck) == 0时执行addWorker(null, false);
//是为了保证线程池在RUNNING状态下必须要有一个线程来执行任务;
addWorker(null, false);
}
/*
* 如果执行到这里,有两种情况:
* 1. 线程池已经不是RUNNING状态;
* 2. 线程池是RUNNING状态,但往workQueue已经放不进去,即workerCount >= corePoolSize,并且workQueue已满;
* 此时再次调用addWorker()方法,第二个参数为false,表示非核心线程,如果失败则拒绝该任务;
*/
else if (!addWorker(command, false))
reject(command);
}
addWorker(Runnable firstTask, boolean core)方法:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
// 外层for循环
for (;;) {
// 获取线程池控制变量的值
int c = ctl.get();
// 获取线程池运行状态
int rs = runStateOf(c);
// if判断,如果rs >= SHUTDOWN 并且 (判断3个条件,只要有1个不满足),返回false;
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
// 内层for循环
for (;;) {
// 获取线程池线程数
int wc = workerCountOf(c);
// 如果wc超过CAPACITY,也就是ctl的低29位的最大值(二进制是29个1),返回false;
// core为addWorker方法的第二个参数,若为true根据corePoolSize比较,若为false根据maximumPoolSize比较;
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 尝试增加workerCount,如果成功,则跳出外层for循环
if (compareAndIncrementWorkerCount(c))
break retry;
// 如果增加workerCount失败,则重新获取控制变量ctl的值
c = ctl.get(); // Re-read ctl
// 如果当前线程池的运行状态不等于rs,说明线程池运行状态已被改变,返回外层for循环继续执行
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// Worker线程是否启动
boolean workerStarted = false;
// Worker线程是否添加
boolean workerAdded = false;
// 是一个线程类,该类实现了Runnable接口的
Worker w = null;
try {
// 根据firstTask来创建Worker对象
w = new Worker(firstTask);
// 每一个Worker对象都会创建一个线程
final Thread t = w.thread; // Thread t = new Thread(worker);
if (t != null) {
// 创建一个lock锁
final ReentrantLock mainLock = this.mainLock;
// 上锁
mainLock.lock();
try {
// 检查线程池运行状态
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN表示是RUNNING状态;
// 如果rs是RUNNING状态或者rs是SHUTDOWN状态并且firstTask为null,向线程池中添加线程。
// 因为在SHUTDOWN时不会在添加新的任务,但还是会执行workQueue中的任务
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 检查线程已经是运行状态,抛出非法线程状态异常
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// workers是一个HashSet
workers.add(w);
int s = workers.size();
// largestPoolSize记录着线程池中出现过的最大线程数量
if (s > largestPoolSize)
// 把历史上出现过的最大线程数的值更新一下
largestPoolSize = s;
// Worker线程添加成功
workerAdded = true;
}
} finally {
// 释放ReentrantLock锁
mainLock.unlock();
}
if (workerAdded) {
// 启动线程
t.start(); //因为这个t线程里面包装一个worker对象,所以启动后就会执行worker的run()方法
// Worker线程已经启动
workerStarted = true;
}
}
} finally {
// Worker线程没有启动成功
if (! workerStarted)
addWorkerFailed(w);
}
// 返回Worker线程是否启动成功
return workerStarted;
}
//Worker类
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
//Thread t = new Thread(worker);
//t.start(); ==> worker.run()
}
@Override
public void run() {
runWorker(this);
}
//......................... 省略
}
getTask()方法:
private Runnable getTask() {
// 表示上一次从任务队列中取任务时是否超时
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
/*
如果线程池为`SHUTDOWN`状态且任务队列为空(线程池shutdown状态可以处理任务队列中的任务,不再接受新任务)
或者 线程池状态>=STOP,则意味着线程池不必再获取任务了,将当前工作线程数量-1并返回null;
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//线程池的线程个数
int wc = workerCountOf(c);
// Are workers subject to culling?
/*
timed变量用于判断是否需要进行超时控制;
allowCoreThreadTimeOut默认是false,也就是核心线程不允许进行超时;
wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
表示对于超过核心线程数量的这些线程,需要进行超时控制(默认情况)
*/
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
* 两个条件全部为true,则通过CAS使工作线程数-1,即去除工作线程:
* 条件1:工作线程数大于maximumPoolSize,或(工作线程需要超时控制且上次在任务队列拉取任务超时)
* 条件2:wc > 1或任务队列为空
* 如果减1失败,则返回重试;
*/
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/*
* 执行到这里,说明已经经过前面的校验,开始真正获取task;
* 根据timed来判断,如果工作线程有超时时间,则通过任务队列的poll方法进行超时等待方式获取任务,
* 如果在keepAliveTime时间内没有获取到任务,则返回null,否则通过take方法;
* take方法表示如果这时任务队列为空,则会阻塞直到任务队列不为空;
* 一般poll()用于普通线程、take()用于核心线程
*/
Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
// r不为空,则返回该Runnable
if (r != null)
return r;
// 如果 r == null,说明已经超时得不到任务,timedOut设置为true
timedOut = true;
} catch (InterruptedException retry) {
// 如果获取任务时当前线程发生了中断,则设置timedOut为false并返回循环重试
timedOut = false;
}
}
}
processWorkerExit(Worker w, boolean completedAbruptly)方法:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//completedAbruptly为true表示线程异常执行结束
//completedAbruptly为false表示线程正常执行结束
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
//从线程set集合中移除工作线程,该过程需要加锁,因为HashSet是线程不安全的集合
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//统计完成的任务数:将该worker已完成的任务数追加到线程池已完成的任务数
completedTaskCount += w.completedTasks;
//从HashSet<Worker>中移除该worker
workers.remove(w);
} finally {
//释放锁
mainLock.unlock();
}
//根据线程池状态进行判断是否结束线程池
tryTerminate();
int c = ctl.get();
//当线程池是RUNNING或SHUTDOWN状态时
if (runStateLessThan(c, STOP)) {
//如果worker不是异常结束:
if (!completedAbruptly) {
//如果allowCoreThreadTimeOut=true,最小线程个数就可以变为0;
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
//但是,如果等待队列有任务,至少保留一个worker来处理任务;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
//如果工作线程大于等于核心线程,直接return就行了,否则就需要添加一个线程;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
//是异常执行结束的,添加一个线程去执行任务
addWorker(null, false);
}
}
runWorker(Worker w)方法:
final void runWorker(Worker w){
//获取当前线程
Thread wt = Thread.currentThread();
//Worker的一个成员变量,第一个任务firstTask,是用户提交过来的
Runnable task = w.firstTask;
//然后把w.firstTask置为null
w.firstTask = null;
//允许响应中断
w.unlock();
// 线程退出的原因,true是任务导致,false是线程正常退出
boolean completedAbruptly = true;
try{
// 当前任务为空,且当前任务队列为空,停止循环
while (task != null || (task = getTask()) != null) {
// 上锁处理并发问题,防止在shutdown()时终止正在运行的worker
w.lock();
// 如果线程池是stop状态,并且线程没有被中断,就要确保线程被中断,如果线程池不是,确保线程池没有被中断;
// 清除当前线程的中断标志,做一个recheck来应对shutdownNow方法;
if ((runStateAtLeast(ctl.get(),STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(),STOP)))
&& !wt.isInterrupted())
wt.interrupt(); //中断
try {
// 执行前(空方法,由子类重写实现)
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行Runnable类的run()方法
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 执行后(空方法,由子类重写实现)
afterExecute(task, thrown);
}
} finally {
task = null;
// 完成的任务数+1
w.completedTasks++;
// 释放锁
w.unlock();
}
}
// 到此,线程是正常退出
completedAbruptly = false;
} finally {
// 处理worker的退出
processWorkerExit(w,completedAbruptly);
}
}
锁
synchronized
加锁:线程在进入 synchronized 代码块前会自动获取内部锁,其它线程访问该代码块时会被阻塞。 解锁:拿到内部锁的线程在正常退出代码块、抛出异常后、在同步代码块内调用了该内置锁资源的 wait 系列方法时,释放该内置锁。 1.作用在方法上: 修饰非静态方法(普通方法) 修饰静态方法 2.作用在代码块上(更细,更灵活): synchronized(this|object) {...} synchronized(类.class) {...} 对象锁 1、两个线程同时访问同一个对象的同步方法,会互斥; 2、两个线程同时访问同一个对象的同步方法和非同步方法,不互斥; 3、两个线程同时访问两个对象的同步方法,不互斥; 4、两个线程同时访问两个对象的同步方法和非同步方法,不互斥; 类锁 1、两个线程同时访问同一个类的静态同步方法,会互斥; 2、两个线程同时访问不同类的静态同步方法,不互斥; 3、两个线程同时访问同一个类的静态同步方法和静态非同步方法,不互斥; 4、两个线程同时访问同一个类的静态同步方法和非静态同步方法,不互斥; 偏向锁: 通过CAS 替换MarkWord中的偏向锁标记和锁标记,并设置线程 ID;当其他线程尝试竞争偏向锁时,发生锁的升级/膨胀. 轻量级锁: 没有抢占到锁的线程,进行一定次数的重试,也叫自旋(while(...)),不阻塞,不要唤醒。 轻量级锁加锁: 1.在线程进入同步块时,如果同步对象锁状态为无锁状态(偏向锁为0,锁标志位为01),虚拟机先将在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象目前的 Mark Word 拷贝, 2.复制对象头中的 Mark Word 到锁记录(Lock Record)中. 3.复制完后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向LockRecord的指针,并将 Lock Record 里的 owner 指针指向对象头的 mark word. 4.如果1-3成功了,那么这个线程就拥有了该对象的锁,并将对象MarkWord的锁标记设置为 00,即表示此对象处于轻量级锁定状态. 5.如果1-3失败了,虚拟机首先会检查对象的Mark Word 是否指向当前线程的栈帧,不是则有多个线程竞争锁。进行自旋,等待占用锁的线程释放锁,通过 CAS 后即可立即获取锁,否则轻量级锁就要升级为重量。 轻量级锁释放: 轻量级锁释放锁(解锁)时,会使用 CAS 将之前复制在栈桢中的Displaced Mard Word替换回 Mark Word 中。 重量级锁: 把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗 CPU。阻塞或者唤醒一个线程时,要操作系统来调度,用户态转换到内核态比较耗时。 重量级锁底层实现原理: synchronized(this){ ...... }膨胀为重量级锁,那么 this 对象会与一个monitor关联,this 对象头里面的 mark word 指向 monitor 的指针。monitor(管程,就是锁)提供了一种排它互斥访问机制,保证在每个时间点上最多只有一个线程会执行同步方法。 重量级锁加锁 重量级锁是通过对象内部的监视器(monitor)来实现,也就是通过ObjectMonitor 实现,ObjectMonitor.hpp 有几个重要属性: _Owner:保存当前持有锁的线程; _EntryList:等待获取锁的线程; _WaitSet:调用 Object 的 wait()方法等待时,此时将该等待的线程保存到_WaitSet中; _cxq:存储没有获取到锁的线程; _recursions:记录重入次数; 当多个线程同时访问某段同步代码时: 1、首先会进入_EntryList 集合; 2、当线程获取到对象的 monitor 之后,就会进入_Owner 区域,并把ObjectMonitor对象的_Owner 指向为当前线程,并将_count + 1; 3、如果调用了释放锁(比如 wait()方法)操作,就会释放当前持有的monitor,即_owner= null,_count - 1,同时这个线程会进入到_WaitSet 列表中等待被唤醒; 4、如果当前线程执行完毕,则释放 monitor 锁,复位_count 的值(_count-1),不过此时不会进入_WaitSet 列表; 重量级锁释放 锁释放是同步代码块执行结束后触发,ObjectMonitor::exit; 1、ObjectMonitor 中持有的锁的_owner 对象置为 null; 2、从_cxq 队列中唤醒一个被挂起的线程:根据 QMode 模式判断是从_cxq 还是 EntryList 中获取头节点的线程进行唤醒,通过ObjectMonitor::ExitEpilog 方法唤醒该节点封装的线程,唤醒操作最终由unpark完成:_cxq(竞争列表)EntryList(锁候选者列表) 3、被唤醒的线程重新竞争重量级锁,被唤醒的线程不一定能抢到锁,未抢到锁的线程将继续挂起,synchronized 是非公平锁; 用户态->内核态->用户态 因为每个进程的栈有两个,用户态栈、内核态栈。从用户态栈进入内核态栈的时,要保存用户态的寄存器,在内核态返回用户态的时候会恢复这些寄存器的内容,相对而言这是一个很大的开销且耗时.
死锁
产生死锁的四个条件
1、互斥条件: 当资源被一个线程使用或者占用时,别的线程就不能使用该资源,比如资源X已经被 T1 线程占用,那么 T2 线程就不能再占用 X 资源;(系统条件)
2、请求和保持条件: 资源请求者在请求别的资源时,同时保持对已有资源的占有,比如T1线程已经取得 X 资源,在等待获取 Y 资源的时候,不释放对X 资源的占用;
3、不可抢占条件: 获取资源的一方,不能从正在使用资源的一方强行抢占资源,资源只能被使用者主动释放,比如 T1 线程已经取得 X 资源并正在使用,那么T2 线程不能强行抢占X资源;
4、循环等待条件: 一个线程正在等待另一个线程占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况,比如 T1 线程等待 T2 线程占用的资源,T2 线程等待T1线程占用的资源;
解决死锁
1、互斥条件: 不能破坏;
2、请求和保持条件: 可破坏,一次性获取所有锁资源,避免等待;
3、不可抢占条件: 可破坏,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,ReentrantLock: lock.unlock();
4、循环等待条件: 可破坏,按顺序获取锁;
volatile:
只能修饰类变量、实例变量
Java线程内存模型会确保所有线程看到这个变量的值都是一致的;
当线程更新数据时(不能保证原子性,不加锁),会把主存数据加到缓存中,更新完将数据从缓存加到主存中。
单线程读取数据时,会把数据从主存加到缓存中。
程序重排序: Java在编译和运行期对代码的优化,以提高代码执行效率;
重排序只会对不存在数据依赖性的指令重排序,在单线程下无论怎么重排序可以保证最终执行的结果与程序顺序执行的结果一致,在多线程下就可能出现问题;
被 volatile 修饰的共享变量相当于屏障,屏障的作用是不允许指令随意重排;