Java高并发编程详解:多线程与架构设计
多线程基础
快速认识线程
-
线程的介绍
线程是CPU调度的基本单位,每个线程都有自己的局部变量表、程序计数器、声明周期等
-
快速创建并启动一个线程
- 创建方式
- 继承java.lang.Thread类
- 实现java.lang.Runnable接口
- 基于Java 8 Lambda简化创建
- ()->{具体内容}
- 方法引用
- 创建具体操作
- 重写run方法
- 启动方式
- 调用Thread的start方法
- 注意
- 启动新的线程,只有调用了Thread的start方法,才代表派生了一个新的线程,否则Thread和其他普通的Java对象没有什么区别,start方法是立即返回方法,并不会让程序陷入阻塞
- 使用Jconsole或jstack观察线程
- 操作系统启动一个JVM时,其实是启动一个进程,而在该进程里面启动了一个以上的线程,其中Thread-0这个线程是示例中创建的。
- jconsole展示的线程截图
- 打开jstack一闪而过,无法查看
- 创建方式
-
线程的声明周期
- 状态图
- 问题
- 执行了Thread的start方法就代表该线程已经开始执行了吗?
- 线程声明周期的5个主要阶段
- NEW
- 用关键字NEW创建一个Thread对象时,此时它并不处于执行状态,因为没有调用start方法启动该线程,那么线程的状态为NEW状态
- 可以切换的状态
- 通过start方法进入RUNNABLE状态
- RUNNABLE
- 线程对象调用start方法进入RUNNABLE状态后,才真正的在JVM进程中创建一个线程,线程启动后不会立刻得到执行,取决于CPU的调度
- 线程具备的资格,但是并没有真正的执行,而是等待CPU的调度
- 由于存在Running状态,所以不会直接进入BLOCKED和TERMINATED状态,即使在线程的执行逻辑中调用wait、sleep或者其他block的IO操作等,也必须先获得CPU才可以。
- 可以切换的状态
- 意外终止进入TERMINATED状态
- RUNNING状态
- RUNNING
- 一旦获得CPU,那么此时线程才能执行自己的代码逻辑
- RUNNING状态的线程事实上也是RUNNABLE的,反之不成立。
- 可以切换的状态
- 直接进入TERMINATED状态,比如调用JDK的stop方法或者判断某个逻辑标识
- 进入BLOCKED状态,比如调用了sleep,或者wait方法而加入waitSet中;进行某个阻塞的IO操作,比如网络数据读写;获取某个锁资源,从而加入到该锁的阻塞队列中
- 进入RUNNABLE状态,由于CPU调度使该线程放弃执行或者线程主动调用yield方法,放弃CPU执行权
- BLOCKED
- 线程进入BLOCKED状态的原因参考上面
- 可以切换的状态
- 直接进入TERMINATED状态,比如调用stop方法或者意外死亡(jvm crash)
- 进入RUNNABLE状态,比如线程阻塞的操作结束、线程完成指定时间的休眠、wait中的线程被其他线程notify/notifyAll唤醒、线程获得某个锁资源、线程在阻塞过程中被打断(比如其他线程调用了interrupt方法)
- TERMINATED
- TERMINATED是线程的最终状态,在该状态中线程将不会切换到其他任何状态。线程进入TERMINATED状态,意味着该线程的整个生命周期结束
- 使线程进入TERMINATED状态的情况
- 线程正常运行结束
- 线程运行出错意外结束
- JVM crash,导致所有的线程都结束
- NEW
-
线程的start方法剖析
- 在start0方法中调用了线程的run方法
- 总结
- Thread被构造后使NEW状态,threadStatus这个内部属性为0
- 不能启动线程超过一次,否则就会出现IllegalThreadStateException异常
- 线程启动后将会被加入到一个ThreadGroup中
- TERMINATED状态的线程再次调用start方法是不允许的,即TERMINATED状态没有办法回到RUNNABLE/RUNNING状态
- 演示
- 重复启动
- 线程运行结束后再启动
- 都会抛出IllegalThreadStateException异常,但是第一种情况中该线程处于运行状态,而第二种情况是没有该线程
-
Runnable接口的引入
- Runnable与Thread的优缺点比较
- Runnable是接口,无需子类化
- Runnable与Thread的优缺点比较
Thread构造函数
- 构造函数概览
- 线程的名称
- 为线程取特殊意义名字有助于问题的排查和线程的跟踪
- 线程的默认名称
- "Thread-"+nextThreadNum()
- 给线程设置名称
- 通过构造函数
- 修改线程的名称
- 在线程启动之前还可以对其进行修改,一旦线程启动,名字将不再被修改
- 没明白NEW非NEW状态的处理
- 线程的父子关系
- 在构造函数的init方法中有体现
- 一个线程的创建肯定是由另一个线程完成的
- 被创建线程的父线程是创建它的线程
- ThreadGroup
- 如果在构造Thread的时候没有显示的指定一个ThreadGroup,那么子线程将被加入父线程所在的线程组
- main线程所在的ThreadGroup称为main
- 构造一个线程的时候如果没有显示地指定ThreadGroup,那么他将会和父线程同属于一个ThreadGroup
- Thread与Runnable
- Thread负责线程本身相关地职责和控制,而Runnable负责逻辑执行单元的部分
- Thread与JVM虚拟机栈
- Thread与stackSize
- 影响递归的深度,采用默认值极客
- JVM内存结构
- 内存结构图
- 程序计数器
- 书上是错的吧
- 程序计数器是线程私有的
- Java虚拟机栈
- Java虚拟机栈是线程私有的,它的生命周期与线程相同,是在JVM运行时锁创建的,在线程中,方法在执行的时候都会创建一个名为栈帧的数据结构,栈帧中主要存放局部变量表、操作栈、动态链接、方法出口等信息
- 本地方法栈
- JVM为本地方法所划分出的内存区域
- Java中提供了调用本地方法的接口(Java Native Interface),也就是C/C++程序,比如网络通信、文件操作的底层,JVM为本地方法所划分的内存区域便是本地方法栈,这块内存区域完全由不同的JVM厂商实现
- 本地方法栈也是线程私有的
- 堆内存
- 堆内存是JVM中最大的一块内存区域,被所有的线程共享,Java在运行期间创建的所有对象几乎都存放在该内存区域,该该内存区域也是垃圾回收的重点区域
- 堆区的分代划分
- 方法区
- 方法是被多个线程所共享的内存区域,它主要用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据
- 不同的JVM会有不同的实现
- Java 8元空间
- jstat
- Thread与虚拟机栈
- 程序计数器是比较小的一块内存,而且该部分内存是不会出现任何溢出异常的。与线程创建、运行、销毁等关系比较大的是虚拟机栈内存了,而且占内存划分的大小将直接决定在一个JVM进程中可以创建多少个线程
- 线程的创建数量是随着虚拟机栈内存的增多而减少的
- 虚拟机栈内存是线程私有的,就是说每个线程都会占有指定的内存大小,粗略认为一个Java进程的内存大小为:堆内存+线程数量*栈内存
- 操作系统中,一个进程的最大内存是有限制的,32为的Windows操作系统所允许的最大进程内存为2GB。可以根据上面公式可以得出,线程数量与占内存的大小是反比关系,线程数量与堆内存也是反比关系
- linux下还没查到资料
- 计算线程数量的公式
- 线程数量=(最大地址空间)-JVM堆内存-ReservedOsMemory)/ThreadStackSize
- ReservedOsMemory为系统保留内存,32位Windows下一般位136MB左右
- 线程数量与操作系统的一些内核配置有关
- linux下
- /proc/sys/kernel/threads-max
- /proc/sys/kernel/pid_max
- /proc/sys/vm/max_map_count
- linux下
- 程序计数器是比较小的一块内存,而且该部分内存是不会出现任何溢出异常的。与线程创建、运行、销毁等关系比较大的是虚拟机栈内存了,而且占内存划分的大小将直接决定在一个JVM进程中可以创建多少个线程
- Thread与stackSize
- 守护线程
- 守护线程一般用于处理一些后台的工作,比如垃圾回收
- The Java Virtual Machine exits when the only threads running are all daemon threads
- 是说在正常退出的情况下,而不是调用System.exit()
- 调用setDaemon方法设置守护线程,true代表守护线程,false代表正常线程
- 只在线程启动之前才能生效
- isDaemon方法判断线程是不是守护线程
Thread API
-
sleep
- sleep介绍
- 构造方法
- public static native void sleep(long millis) throws InterruptedException
- public static void sleep(long millis, int nanos)
throws InterruptedException
- sleep方法会使当前线程进入指定毫秒数的休眠,暂停执行,但是线程不会放弃monitor所的所有权
- Thread.sleep只会导致当前线程进入指定时间的休眠
- 构造方法
- 使用TimeUnit替代Thread.sleep
- 可以省区时间单位的换算
- sleep介绍
-
yield
- yield方法介绍
- 提示调度器当前线程愿意放弃处理器的当前使用。调度器可以自由的忽略这个提示
- 调用yield方法会使当前线程从RUNNING切换到RUNNABLE状态
- yield与sleep的区别
- sleep会导致当前线程暂停指定的时间,没有CPU时间片的消耗
- yield只是堆CPU调度器的一个提示,如果CPU调度器没有忽略这个提示,它会导致线程上下文的切换
- sleep会使线程短暂block,会在给定的时间内释放CPU资源
- yield会使RUNNING状态的线程进入RUNNABLE状态(如果CPU调度器没有忽略这个提示的话)
- sleep几乎百分之百的完成给定时间的休眠,而yield的提示并不能一定担保
- 最后一个没明白
- yield方法介绍
-
设置线程优先级
- 相关方法
- setPriority(int)
- getPriority()
- 线程优先级介绍
- 优先级搞的线程不一定有优先被CPU调度的机会,因为它只是一个提示操作
- 一般情况下不会对线程设定优先级别
- 相关方法
-
获取线程ID
- getId()获取线程的唯一ID,线程的ID在整个JVM进程中都是唯一的,并且是从0开始逐次递增
- 如果在main线程中创建一个唯一的线程,并且调用getId()后发现其并不等于0,是因为一个JVM进程启动的时候,实际上是开辟了很多个线程
-
获取当前线程
- currentThread()用于返回当前执行线程的引用
-
设置线程上下文类加载器
- getContextClassLoader()获取线程上下文的类加载器,即这个线程是由哪个类加载器加载的,如果是在没有修改线程上下文类加载器的情况下,则保持与父线程同样的类加载器
- setContextClassLoader(ClassLoader)设置该线程的类加载器
-
线程interrupt
中断这个线程
-
相关方法
- public void interrupt()
- public static boolean interrupted()
- public boolean isInterrupted()
-
interrupt
-
让当前线程进入阻塞状态的方法
Object的wait方法
Object的wait(long)方法
Object的wait(long, int)方法
Thread的sleep(long)方法
Thread的sleep(long,int)方法
Thread的join方法
Thread的join(long)方法
Thread的join(long, int)方法
InterruptibleChannel的io操作
Selector的wakeup方法
其他方法 调用当前线程的interrupt方法可以打断阻塞
一旦线程在阻塞的情况下被打断,都会抛出一个成为InterruptedException的异常,这个异常作为信号会通知当前线程被打断了
在一个线程内部存在着名为interrupt flag的标识,如果一个线程被interrupt,那么它的flag将被设置,但是如果当前线程正在执行可中断方法被阻塞时,调用interrupt方法将其中断,反而会导致flag被清除
-
-
isInterrupted
- 判断当前线程是否被中断,只是对interrupt标识的一个判断
-
interrupted
- 用于判断当前线程是否被中断,并且直接擦掉线程的interrupt标识。如果当前线程被打断了,那么第一次调用interrupted方法会返回true,并且立即擦除interrupt标识;第二次包括以后的调用永远都会返回false
可中断方法和不可中断方法
-
join
等待这个线程死亡
join方法会使当前线程永远等待下去,直到期间被另外的线程中断,或者join的线程执行结束
-
重载方法
public final void join() throws InterruptedException
public final synchronized void join(long millis)
throws InterruptedException
public final synchronized void join(long millis, int nanos)
throws InterruptedException
-
如何关闭线程
- 弃用的stop方法在关闭线程时可能不会释放掉monitor的所
- 几种关闭线程的方法
- 正常关闭
- 线程结束生命周期而正常结束
- 捕获中断信号关闭线程
- 使用volatile开关控制?
- 异常退出
- 进程假死
- 线程阻塞
- 死锁
- 借助jstack、jconsole、jvisualvm等工具诊断
- 正常关闭
线程安全与数据同步
- 基本概念
- 共享资源是指多个线程同时访问的资源
- 数据同步是指如何保证毒功而线程访问到的数据是一致的
- 数据同步
- a++问题
- synchronized
- 初始synchronized关键字
- 什么是synchronized
- 定义
- synchronized关键字实现一种简单的策略,用于防止线程干扰和内存一致性错误:如果一个对象对超过一个线程可见,对那个独享的变量的读写会通过同步的方法完成
- 具体表现
- synchronized关键字提供了一种锁的机制,能确保共享变量的互斥访问,从而防止数据不一致问题的出现
- synchronized关键字包括monitor enter和monitor exit两个JVM指令,它能保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功后,共享变量被更新后的值必须刷入主存
- synchronized的指令严格遵守java happens-before规则,一个monitor exit指令之前必定要由一个monitor enter
- 定义
- synchronized关键字的用法
- synchronized可以用于对代码块或方法进行修饰,而不能够用于对class以及变量进行修饰
- 同步方法
- [default|public|private|proteced] synchronized [static] type method()
- public synchronized void synch(){}
- 同步代码块
- private final Object MUTEX = new Object(); public void sync(){ synchronized(MUTEX){}}
- 同步方法
- synchronized可以用于对代码块或方法进行修饰,而不能够用于对class以及变量进行修饰
- 什么是synchronized
- 深入synchronized关键字
- synchronized(mutex)是某线程获取与mutext关联的monitor锁
- 线程堆栈分析
- jconsole检查线程的状态
- JVM指令分析
- javap -c查看汇编代码
- monitorenter
- 每个对象都与一个monitor的lock锁关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下几件事情
- 如果monitor的计数器位0,则意味着该monitor的lock还没有被获得,某个线程获得之后立即对该计数器加一,从此该线程就是这个monitor的所有者了
- 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加
- 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor的计数器变为0,才能再次尝试获取对monitor的所有权
- 每个对象都与一个monitor的lock锁关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下几件事情
- monitorexit
- 释放monitor的所有权,就是将monitor的计数器减一,如果计数器的结果为0,该线程不再拥有对该monitor的所有权,即解锁。与此同时被该monitor blcok的线程将再次尝试获得对该monitor的所有权
- monitorenter
- javap -c查看汇编代码
- 使用synchronized需要注意的问题
- 与monitor关联的对象不能位空,即mutext不能位null
- synchronized作用域太大。因为synchronized关键字具有互斥行为,作用域越大,会失去并发优势。synchronized关键字应该尽可能地只作用域共享资源(数据)地读写作用域
- 不同monitor企图所相同的方法。即声明mutext要static,不能忘掉
- 正确:private final Object mutex = new Object();
- 错误:private final Object MUTEX = new Object();每个线程会独立持有锁
- 多个锁的交叉导致死锁
- 初始synchronized关键字
- This Monitor和Class Monitor
- 使用synchronized关键字同步类的不同实例方法,争抢的是同一个monitor的lock,与之关联的引用则是ThisMonitor的实例引用
- 用synchronized同步某个类的不同静态方法争抢的也是同一个monitor的lock
- 程序死锁的原因以及如何诊断
- 程序死锁
- 交叉锁可能导致程序出现死锁
- 内存不足
- 一问一答式的数据交换
- 数据库锁
- 文件锁
- 死循环引起的锁
- 难排查
- 程序死锁
线程间通信
- 线程间的通信与进程间通信方式不同
- 同步阻塞与异步非阻塞
- 同步阻塞消息处理设计的缺点
- 客户端等待时间过长(提交Event时长+接收Event创建thread时长+业务处理时长+返回结果时长)会陷入阻塞
- 由于客户端提交的Event数量不多,导致系统同时受理业务数量有限,也就是系统整体的吞吐量不高
- 在业务达到峰值时,大量的业务处理线程阻塞会导致频繁的CPU上下文切换,从而降低系统性能
- 频繁的创建于销毁线程,增加系统额外开销
- 异步非阻塞消息处理
- 优点
- 客户端不用等结果处理结束就可以返回,提供系统的吞吐量和并发量
- 服务端线程可以重复使用,减少了不断创建线程带来的资源浪费
- 服务端的线程数量在一个可控制的范围之内,不会导致太多的CPU上下文切换
- 缺点
- 客户端想要得到结果需要再次调用接口方法进行查询
- 优点
- 同步阻塞消息处理设计的缺点
- 单线程间通信
- wait和notify实现生产者-消费者模式
- wait和notify方法
- wait和notify方法并不是Thread特有的方法,而是Object中的方法,即在JDK中每个类都拥有这两个方法
- wait方法
- wait的重载方法
- public final void wait() throws InterruptedException
- public final native void wait(long timeout) throws InterruptedException
- public final void wait(long timeout, int nanos) throws InterruptedException
- wait的说明
- wait(0)代表着永不超时
- Object的wait(long timeout)方法会导致当前线程进入阻塞,直到其他线程调用了Object的notify或者notifyAll方法才将其唤醒,或者阻塞时间到达了timeout时间而自动唤醒
- wait方法必须拥有该对象的monitor,也就是必须在同步方法中使用
- 没明白
- 当前线程执行了该对象的wait方法之后,将会放弃该monitor的所有权,并且进入于该对象关联的wait set中,即一旦线程执行了某个object的wait方法之后,它就会释放该对象的monitor的所有权,其他线程也有机会继续争抢该monitor的所有权
- wait的重载方法
- notify方法
- public final native void notify()
- noitfy的说明
- 唤醒单个正在执行该对象wait方法的线程
- 如果有某个线程由于执行该对象的wait方法而进入阻塞则会被唤醒,如果没有则会忽略
- 被唤醒的i安城需要重新获取该对象所关联monitor的lock才能继续执行
- wait和notify的注意事项
- wait方法是可中断方法
- 线程执行了某个对象的wait方法以后,会加入与之对应的wait set中,每个对象的monitor都有一个与之关联的wait set
- 当线程进入wait set之后,notify方法可以将其唤醒,也就是从wait set中弹出,同时中断wait中的线程也会将其唤醒
- 必须在同步方法中使用wait和notify方法,因为执行wait和notify的前提条件是必须持有同步方法的monitory的所有权,否则会抛出IllegalMonitorStateException
- 同步代码的monitor必须于执行wait notify方法的对象一致,即使用哪个对象的monitor进行同步,就只能用哪个对象进行性wait核notify。否则抛出IllegalMonitorStateExcpetion异常信息
- wait和sleep
- 类似点
- wait和sleep方法都可以使线程进入阻塞状态
- wait和sleep方法均是可中断方法,被中断后都会收到中断异常
- 区别
- wait是Object的方法,而sleep是Thread特有的方法
- wait方法的执行必须在同步方法中进行,而sleep则不需要
- 线程在同步方法中执行sleep方法时,并不会释放monitor的锁,而wait方法则会释放monitor的锁
- sleep方法短暂休眠之后主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞
- 类似点
- 多线程间通信
- 生产者消费者
- notifyAll方法
- 多线程间线程通信需要用Object的notifyAll方法,可以唤醒由于调用wait方法而阻塞的线程,但是noitfy方法只能唤醒其中的一个线程,而noitfyAll方法则可以同时唤醒全部的阻塞线程,同样被唤醒的线程仍需要继续争抢monitor的锁
- 生产者消费者
- 前面代码中会出现两类问题
- LinkedList中没有元素的时候仍旧调用removeFirst方法
- 当LinkedList中的元素超过10个的时候仍旧执行addLast方法
- 改进
- 使用while和noitfyAll
- 前面代码中会出现两类问题
- notifyAll方法
- wait set
- 线程调用了某个对象的wait方法之后都会被加入与该对象monitor关联的wati set中,并且释放monitor的所有权
- 另外一个线程调用该monitor的notify或者notifyAll后,就会有thread从wait set中弹出
- 生产者消费者
- 自定义显示锁BooleanLock
- synchronized关键字的缺陷
- synchronized提供一种互斥的数据同步机制,某个线程在获取monitor lock的时候可能会被阻塞
- 无法控制阻塞时长
- 阻塞不可被中断
- 被synchronized同步的线程不可被中断
- synchronized提供一种互斥的数据同步机制,某个线程在获取monitor lock的时候可能会被阻塞
- synchronized关键字的缺陷
ThreadGroup详解
- ThreadGroup并不是用来管理Thread的,而是针对Thread的一个组织结构
- ThreadGroup与Thread
- 在Java程序中,默认情况下,新的线程都会被加入到main线程所在的group中,main线程的group名字同线程名字
- 创建ThreadGroup
- public ThreadGroup(String name)
- public ThreadGroup(ThreadGroup parent, String name)
- 复制Thread数组和ThreadGroup数组
- ThreadGroup操作
- ThreadGroup的基本操作
- activeCount()获取group中活跃的线程,这只是估值,不能百分百保证数字一定正确
- activeGroupCount()获取group中活跃的子group
- getName()获取group的名字
- getParent()
- list()该方法没有返回值,执行该方法会将group中所有的活跃线程信息去拿不输出到控制台
- ThreadGroup的interrupt
- interrupt一个thread group会导致该group中所有的active线程都被interrupt
- ThreadGroup的基本操作
Hook线程以及捕获线程执行异常
- 获取线程运行时异常
- Thread类中关于处理运行时异常的4个API
- public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh):为某个特定线程指定UncaughtExceptionHandler
- public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh):设置全局的UncaughtExceptionHandler
- public UncaughtExceptionHandler getUncaughtExceptionHandler():获取特定线程的UncaughtExceptionHandler
- public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler():获取全局的UncaughtExceptionHandler
- UncaughtExceptionHandler介绍
- 线程在执行单元中是不允许抛出受检异常的,派生它的线程将无法直接获得它运行中出现的异常信息
- 当线程在运行过程中出现异常时,JVM会调用dispatchUncaughtException方法,该方法会将对应线程实例以及异常信息传递给回调接口
- Thread类中关于处理运行时异常的4个API
- 注入钩子线程
- Hook线程介绍
- JVM进程的退出是由于JVM进程中没有活跃的非守护线程,或者收到了系统中断信号,向JVM程序注入一个Hook线程,在JVM进程突出的时候,Hook线程会启动执行,通过Runtime可以为JVM注入多个Hook线程
- 利用Hook防止重复启动
- Hook线程介绍
线程池原理以及自定义线程池
- 线程池原理
- 线程池实现
- 线程池的应用
Java ClassLoader
ClassLoader主要负责加载class文件到JVM中,给定一个class的二进制文件名,ClassLoader会尝试加载并且在JVM中生成构成这个类的各个数据结构,然后使其分布在JVM对应的内存区域中
类的加载过程
- 类的加载过程简介
- 也有说5个阶段的
- 加载阶段
- 主要负责查找并且加载类的二进制数据文件,即class文件
- 连接阶段
- 验证
- 主要是确保类文件的正确性,比如class的版本,class文件的魔术因子是否正确
- 准备
- 为类的静态变量分配内存,并且为其初始化默认值
- 解析
- 把类中的符号引用转换为直接引用
- 验证
- 初始化阶段
- 为类的静态变量赋予正确的初始值(代码编写阶段给定的值)
- 类的主动使用和被动使用
- JVM规范的6种主动使用类的场景
- 通过new关键字会导致类的初始化
- 访问类的静态变量,包括读取和更新会导致的初始化
- 访问类的静态方法,会导致类的初始化
- 对某个类进行反射操作,会导致类的初始化
- 初始化子类会导致父类的初始化
- 启动类,就是执行main函数所在的类会导致该类的初始化
- 6种情况外的都称为被动引用,不会导致类的加载和初始化
- 注意事项
- 构造某个类的数组时并不会导致该类的初始化
- 应用类的静态常量不会导致类的初始化
- JVM规范的6种主动使用类的场景
- 类的加载过程详解
- 类的加载阶段
- 类的加载就是将class文件种的二进制数据读取到内存中,将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class对象,作为访问该方法去数据结构的入口
- 类加载的最终产物就是堆内存中的class对象,堆同一个ClassLoader来讲,不管某个类被加载了多少次,对应到堆内存中的class对象始终是通一个
- 获取类二进制数据流的形式
- class二进制文件
- 运行时生成,比如通过开源的ASM包可以生成一些class,或者通过动态代理java.lang.Proxy也可以生成代理类的二进制字节流
- 通过网络获取,比如Applet小程序,以及RMI动态发布等
- 通过读取zip文件获得类的二进制字节流,比如jar、war(其实,jar和war使用的是和zip同样的压缩算法)
- 将类的二进制数据存储在数据库的BLOB字段类型中
- 运行时生成class文件,并且动态加载,比如使用Thrift、AVRO等都可以在运行时将某个Schema文件生成对应的若干个class文件,然后进行加载
- 类的连接阶段
- 验证
- 确保class文件的字节流所包含的内容复合当前JVM的规范要求,并且不会出现危害JVM自身安全的代码,否则会抛出VerifyError之类的异常
- 验证的信息包括
- 验证文件格式
- 文件头部存在着魔术因子,该因子确定这个文件到底是什么类型
- 主次版本号
- 构成class文件的字节流是否存在缺失或者其他附加信息,主要是看class的MD5指纹
- 常量池中的常量是否存在不被支持的变量类型,比如int64
- 指向常量中的引用是否指向了不存在的常量或者该常量的类型不被支持
- 元数据的验证
- 元数据的验证是对class的字节流进行语义分析的过程,整个语义分析就是为了确保class字节流复合JVM规范的要求
- 具体验证
- 检查这个类是否存在父类,是否实现了某个接口,这些父类何接口是否合法,或者是否存在
- 检查该类是否继承了final修饰的类,被修饰的类是不允许被继承并且其中的方法是不允许被ovrride的
- 检查该类是否为抽象类,如果不是抽象类,那么它是否实现了父类的重选ing方法或接口中的所有方法
- 检查方法重载的合法性,比如相同的方法名称、相同的参数但是返回类型不相同,这都不允许
- 字节验证码
- 验证程序的控制流程,比如循环、分支等
- 具体验证
- 保证当前线程在程序计数器中的指令不会跳转到不合法的字节码指令中去
- 保证类型的转换是合法的,比如用A声明的引用,不能用B进行强制类型转换
- 保证任意时刻,虚拟机栈中的操作栈类型与指令代码都能正确的被执行
- 符号引用验证
- 验证符号引用转换为直接引用时的合法性
- 具体验证
- 通过符号引用描述的字符串全限定名称是否能够顺利地找到相关合法性
- 符号引用中的类、字段、方法,是否对当前类可见,比如不能访问引用类的私有方法
- 验证文件格式
- 准备
- 当一个class的字节流通过了所有的验证过程之后,就开始为该对象的类变量(静态变量)分配内存并且设置初始值了,类变量的内存会被分配方法区中,不同于实例变量会被分配到堆内存之中
- 初始值就是相应类型在没有被设置时的默认值,不同的数据类型其初始值不同
- 例子是细节
- 解析
- 在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接用用的过程
- 验证
- 类的初始化阶段
- 执行<climit>()方法
- 只有当接口中有变量的初始化操作时才会生成<clint>()方法
- 静态语句块只能对后面的静态变量进行赋值,但不能对其进行访问
- 执行<climit>()方法
- 类的加载阶段
JVM类加载器
- JVM内置三大类加载器
- 类加载器的父亲委托机制
- 根类加载器介绍
- 根加载器又称为Bootstrap类加载器,该类加载器是最为顶层的加载器,它没有任何父加载器,它是由C++编写的,主要负责虚拟机河西类库的加载,比如整个java.lang包都是由根加载器所加载的,可以通过-Xbootclasspath来指定根加载器的路径,也可以通过系统属性来得知当前JVM的根加载器都加载了哪些资源
- 扩展类加载器介绍
- 扩展类加载器的父加载器是根加载器,它主要用于加载JAVA_HOME下的jre\lb\ext子目录里面的类库。扩展类加载器是由纯Java语言实现的,它是java.lang.URLClassLoader的子类,它的完整类名是sun.misc.Launcher$ExtClassLoader。扩展类记载其所加载的类库可以通过系统属性java.ext.dir获得
- 系统类加载器介绍
- 系统类加载器负责加载classpath下的类库资源。我们在进行项目开发的时候引入的第三方jar包,系统类加载器的父加载器是扩展类加载器,同时它也是自定义类加载器的默认父加载器,系统类加载器的加载路径一般通过-classpath或者-cp指定,可以通过系统属性java.class.path获取
- 自定义类加载器
- 所有的自定义类加载器都是ClassLoader的直接子类或者间接子类,java.lang.ClassLoader是一个抽象类,它里面没有抽象方法,但是有findClass方法,务必实现该方法,否则抛出ClassNotFoundException
- 类的全路径几种格式
- 双亲委托机制(父亲委托机制)
- 当一个类加载器被调用了loadClasss之后,它并不会直接将其加载,它并不会直接将其加载,而是先交给当前类加载器的父加载器尝试加载知道最顶层的父加载器,然后再一次向下进行加载
- 子主题 2
- 类被加载后的内存情况
- 子主题 1
线程上下文类加载器
- 为什么需要线程上下文类加载器
- 数据库驱动的初始化源码分析
深入理解volatile关键字
volatile关键字介绍
- volatile关键字
- static修饰的字段不保证内存可见性
- volatile关键字只能修饰类变量和实例变量,对于方法参数、局部变量以及实例变量,类常量都不能进行修饰
- 机器硬件CPU
- CPU Cache模型
- 程序在运行的过程中,会将所需要的数据从贮存复制一份到CPU Cache中,这样CPU进行计算时就可以直接对CPU Cache中的数据进行读取和写入,当运算结束之后,再将CPU Cache中的最新数据刷新到主内存当中
- CPU通过Cache与主内存进行交互
- CPU缓存一致性问题
- 多线程i++的问题
- 多线程下,每个线程都有自己的工作内存(本地内存,对应于CPU中的Cache
- 解决方案
- 通过总线加所的方式
- 通过缓存一致性协议
- 标志Cache中的行无效
- CPU Cache模型
- Java内存模型
- Java的内存模型(JMM)指定了Java虚拟机如何与计算机的主内存进行工作
- Java的内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主内存之间的抽象关系
- 共享变量存储与主内存中,每个线程都可以范文
- 每个线程都有私有的工作内存或者称为本地内存
- 工作内存只存储该线程对共享变量的副本
- 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
- 工作内存和Java内存模型都是抽象概念,它涵盖了缓存、寄存器、编译器优化以及以减等
- 子主题 3
- Java内存模型与CPU硬件架构交互图
深入volatile关键字
- 并发编程的三个重要特性
- 原子性
- 再一次的操作或者多次操作中,要么所有的操作全部都得到了执行并且不会收到任何因素的干扰而中断,要么所有的操作都不执行
- 银行转账
- 两个原子性的操作结合在一起未必还是原子性的
- volatile关键字不保证数据的原子性,synchronized关键字保证,自JDK 1.5版本起,提供的原子类型变量也可以保证原子性
- 再一次的操作或者多次操作中,要么所有的操作全部都得到了执行并且不会收到任何因素的干扰而中断,要么所有的操作都不执行
- 有序性
- 程序代码在执行过程中的先后顺序
- 由于Java在编译器以及运行期的优化,处理器优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序
- 指令重排
- 在单线程情况下,无论怎样的重排序最终都会保证程序的执行结果和代码顺序执行的结果是完全一致的,但是在多线程的情况下,如果有序性得不到保证,那么很有可能就会出现非常大的问题
- 程序代码在执行过程中的先后顺序
- 可见性
- 当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值
- 读线程和写线程的例子
- 当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值
- 原子性
- JMM如何保证三大特性
- JVM采用内存模型的机制来屏蔽各个平台和操作系统之间内存访问的差异,以实现让Java程序在各种平台下达到一致的内存访问效果
- JMM与原子性
- 对基本数据类型的变量读取、赋值操作都是原子性的
- 对引用类型变量的读取和赋值操作也是原行星的
- x=4;y=x;y++;z=z+1
- 说明
- volatile关键字不具备保证原子性的语义
- JMM与可见性
- Java提供了三种方式来确保可见性
- volatile关键字具有保证可见性的语义
- JMM与有序性
- 在Java的内存模型中,允许编译器和处理器对指令进行重排序
- Java提供了三种保证有序性的方式
- Java内存模型提供了Happens-before原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就无法保证有序性,就是说虚拟机或者处理器可以随意对它们进行重排序处理
- happens-before原则
- 程序次序规则:在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生于编写在前面的从左之后
- 锁定规则:一个unlock操作要先行发生于对同一个锁的lock操作
- volatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作
- volatile关键字具有保证有序性的语义
- 传递规则:如果操作A先于操作B,而操作B又现与操作C,则可以得出操作A肯定要现与操作C
- 线程启动规则:Thread对象的start()方法线性发生于该对象的任何动作
- 线程中断规则:对线程执行interrupt()方法肯定要优先于捕获到中断信号
- 线程终结规则:线程中所有的操作都要先行发生于线程的终止检测
- 对象的终结规则:一个对象初始化的完成先行发生于finalize()方法之前
- happens-before原则
- volatile关键字深入解析
- volatile关键字的语义
- 保证了不同线程之间对共享变量操作时的可见性。当一个线程修改volatile修饰的变量,另外一个线程会立即看到最新值
- 禁止对指令进行重排序
- volatile的原理和实现机制
- 伪代码
- "lock;"前缀实际上相当于一个内存屏障,该内存屏障会为指令的执行提供如下几个保障
- 子主题 1
- volatile的使用场景
- 开关控制
- 状态标记利用顺序性
- Singleton涉及模式的double-check
- volatiel和synchronized
- 区别
- 使用上的区别
- volatile关键字只能用于修饰实例变量和类变量,不能用于修饰方法以及方法参数和局部变量、常量等
- synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块
- volatile修饰的变量可以为null,synchronized关键字同步语句块的monitor对象不能为null
- 对原子性的保证
- volatile无法保证原子性
- 由于synchronized是一种排他的机制,因此synchronized关键字修饰的同步代码是无法被中途打断的,因此它能保证代码的原子性
- 对可见性的保证
- 两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同
- synchronized借助于JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中
- volatile使用机器指令"lock;"的方式迫使其他线程刚工作内存中的数据失效,必须到主内存中进行再次加载
- 对有序性的保证
- volatile关键字进行JVM编译器以及处理器对其进行重排序,所以他能保证有序性
- synchronized关键字锁修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的穿行化执行换来的,在synchronized关键字锁修饰的代码块中代码指令也会发生指令重排序的
- 其他
- volatile不会使线程陷入阻塞
- synchronized关键字会使线程进入阻塞状态
- 使用上的区别
- 区别
- volatile关键字的语义
7种单例设计模式
- 思考维度
- 线程安全
- 高性能
- 懒加载
- 各种实现
- 饿汉模式
- 懒汉模式
- 懒汉+同步
- Double-Check
- Volatile+Double-Check
- Holder方式
- 枚举方式
多线程设计架构模式
监控任务的生命周期
- 场景叙述
- Thread可以获取状态,但都是针对线程本身的,无法获取提交的Runnable在运行过程种所处的状态,如开始、结束、获取结果。用传入共享变量的方式会导致资源竞争
- 当观察模式遇到Thread
- 当某个对象发生状态改变需要通知第三方的时候,观察者模式特别适合。观察者模式需要有事件源,也就是引发状态改变的源头 ,Thread负责执行任务的逻辑单元,它清楚整个过程的始末周期,而事件的接收者则是通知接收者一方。只需要将执行任务的每一个阶段都通知给观察者即可
- 子主题 2
- 没发现与硬编码有啥区别
Single Thread Executor设计模式
- 机场过安检
- 吃面问题
- 和只有一个线程的线程池有啥区别?
- 被synchronized侮辱了智商?
读写锁分离设计模式
- 场景描述
- 读写分离程序设计
- 读写锁的使用
- jdk中自带的读写锁、Stamped锁
- 乐观锁和悲观锁
不可变对象设计模式
- 线程安全性
- 不可变对象的设计
- String的实现
Future设计模式
- Future设计模式实现
- Future的使用以及技巧总结
- 增强FutureService使其支持回调
Guarded Suspension设计模式
- 什么是Guarded Suspension设计模式
- 示例
线程上下文设计模式
- 什么是上下文
- 线程上下文设计
- ThreadLocal详解
- 使用ThreadLocal设计线程上下文
- VisualVM监控堆内存的代销
- ThreadLocal内存泄漏问题
balking设计模式
- 什么是Balking设计
- Balking模式只文档编辑
Latch设计模式
- 什么是Latch
- CountDownLatch程序实现
Thread-Per-Message设计模式
- 什么是Thread-Per-Message模式
- 每个任务一个线程
- 多用户的网络聊天
Two Phase Termination设计模式
- 什么是Two Phase Termination模式
- Two Phase Termination示例
- 知识扩展
Worker-Thread设计模式
- 什么是Worker-Thread模式
- Worker-Thread模式实现
Active Objects设计模式
- 接受异步消息的主动对象
- 标准Active Objects模式设计
- 通用Active Objects框架设计
Event Bus设计模式
- Event Bus设计
- Event Bus实战
Event Driven设计模式
- Event-Driven Architecture基础
- Event-Driven框架
- Event-Driven的使用