《Java多线程编程核心技术》知识梳理
《Java多线程编程核心技术》
@author ergwang
https://www.cnblogs.com/ergwang/
文章末尾附pdf和png下载链接
第1章 Java多线程技能
1. 进程与线程 区别? 联系?
2. 创建多线程的方式,有几种?怎么创建
- 继承Thread类 (一般不单独用)
- 实现Runnable接口 + Thread对象
- 实现Callable接口+FutureTask<>对象+Thread对象
- 线程池 + (实现Callable接口+FutureTask<>对象)或者(实现Runnable接口)
3. Thread类的常见API
-
currentThread() 获取当前线程的信息
-
isAlive() 验证当前线程是否存活
-
sleep(long millis) 线程休眠
-
线程堆栈相关
- StackTraceElement[] getStackTrace()
获取一个标识该线程堆栈跟踪元素数组 - dumpStack()
将当前线程的堆栈跟踪信息输出至标准错误流
(该方法只用于调试) - Map<> getAllStackTraces()
返回所有活动线程的堆栈跟踪的映射
- StackTraceElement[] getStackTrace()
-
getId() 获取线程的唯一标识、Id
-
停止线程的几种方式
-
使用退出标志使流程正常退出,
如在线程中用使用break、抛异常等 -
使用stop()强行终止
【可能会丢数据,已被弃用】 -
使用interrupt()方法中断线程
执行这个方法后,会将线程标记为中断,
【但还会继续执行,直到执行完毕】- interrupted()
判断线程是否已经中断
【执行后,会将中断线程状态清除!】 - this.isInterrupted()
判断this关键字所在的类的对象是否已中断
- interrupted()
-
-
线程的暂停与重启
- suspend() 暂停线程
缺点:【独占资源】【易造成数据不完整】 - resume()
重启线程
- suspend() 暂停线程
-
yield() 释放当前线程所占用的CPU资源
-
线程优先级
- getPriority() 获取当前线程的优先级
【最大10 ,最小1】 - setPriority() 设置当前线程的优先级
MIN_PRIORITY = 1;
NORM_PRIORITY = 5;
MAX_PRIORITY = 10; - 优先级具有【继承性】
A线程启动B线程,则B优先级等于A
- getPriority() 获取当前线程的优先级
-
setDaemon(true) 设置当前线程为守护线程
- Java中的线程分为:
守护线程
用户线程(非守护线程)
- Java中的线程分为:
第2章 线程同步(对象及变量的并发访问)
synchronized 同步方法
-
方法内的变量永远线程安全
-
实例变量线程不安全,怎么解决
- 同步方法
同步代码块
- 同步方法
-
synchronized在字节码指令中的原理
- 同步方法:
在class文件中标记了ACC_SYNCHRONIZED - 同步代码块:
class文件中用monitorenter和monitorexit分别表示同步代码块的开始和结束
- 同步方法:
-
脏读问题
- 发生脏读:
因为读取成员变量(实例变量)时,此值已经被其他线程修改了
- 发生脏读:
-
synchronized 锁重入
-
锁重入:
可以重复加锁,
有几个锁加几个锁,一层套一层 -
支持父子类继承
- 子类同步方法调用父类同步方法,有一层锁是一层,就当是在自己方法里面一样,不会出现线程安全问题
-
-
重写synchronized方法
- 如果重写方法不加synchronized关键字,会破坏同步方法
-
抛异常,锁自动释放
-
println() 也是同步方法,线程安全
-
【同步方法】锁是对象的锁,不同的对象是不同的锁
-
【缺点】一个对象中多个同步方法,用的同一个锁(都是对象锁),Thread1调用A方法一直没执行完毕,从而阻塞Thread2调用的B方法。
synchronized 同步代码块
-
同步和异步
-
区别和联系
- 同锁 -> 同步
异锁 -> 异步
- 同锁 -> 同步
-
各有什么优缺点
- 同步执行 成员变量线程安全 但效率低
异步执行 成员变量线程不安全 效率高
- 同步执行 成员变量线程安全 但效率低
-
什么时候用同步/异步?
- 操作同一个成员变量的不同方法,要用同一把锁,保证成员变量的线程安全,如果是不同业务,比如执行完毕后发送邮件,就异步执行
-
-
同步代码块相比于同步方法的优势
- 同步方法的锁: 同步方法所在类的对象
同步代码块的锁: 很灵活, 可以是当前方法所在类的对象,也可以是其他类对象(比如Class类对象,String类对象等)
- 同步方法的锁: 同步方法所在类的对象
-
锁对象
-
this
- 当前方法所在类的对象
-
同步方法的锁对象
- 当前方法所在类的对象
-
类名.class
- Class类的对象
-
静态同步方法的锁对象
- Class类的对象
-
其他类对象
- 如String、Object等类对象
-
-
【注1】String做锁对象时
- 受JVM常量池的影响,如果String的值被改变了,锁就变成了不同锁,会造成线程异步进而可能导致线程不安全问题
-
【注2】锁对象修改对线程同步的影响
- String常量修改 =》 会影响线程锁
- 对象属性修改 =》 不会影响线程锁
-
死锁问题
- 两个线程相互等待对方释放自己的锁
synchronized 同步写法总结
public class MyService{
public synchronized static void method1(){}
public static void method2(){
ssynchronized(MyServcie.class){}
}
public synchronized void method3(){}
public static void method4(){
ssynchronized(this){}
}
public static void method5(){
ssynchronized("abc"){}
}
}
(A) method1 和 method2 持有同一把锁,即:MyService.java 对应的 Class类对象(其实就是Class对象)
(B) method3 和 method4 持有同一把锁,即:MyService.java类的对象 (等同于this)
(C) method5 持有的锁是字符串对象
synchronized 关键字
-
原子性
- 使用synchronized实现了同步,同步实现了原子性,保证了被同步的代码段在同一时间只被一个线程操作,故实现了原子性
-
可见性
- B线程马上就能看到A线程修改的数据
【原理】用volatile或者synchronized修饰的方法,修改读取变量时,强制从公共堆栈读,不从线程私有堆栈中读取。
【注】线程修改数据,写都是写到公共堆栈中
- B线程马上就能看到A线程修改的数据
-
禁止代码重排序
- JAVA程序运行时,JIT(即时编译器)会根据代码执行时间等动态调整执行顺序,而volatile和synchronized关键字会隔断这种调整,隔断成两块后,前面可以内部调整,后面可以内部调整,但是两块之间不能相互调整了
volatile 关键字
-
原子性
- 32位系统中,未用volatile声明的long和double数据类型是非原子的,64位则要根据具体实现判断。
【注】无论32位还是64位,无论用不用volatile声明,i++ 都是一个非原子操作,除非用AtomicInteger声明变量
- 32位系统中,未用volatile声明的long和double数据类型是非原子的,64位则要根据具体实现判断。
-
可见性
- 和synchronized一样,都是强制从公共堆栈读,不从线程私有堆栈中读取。
-
禁止代码重排序
- 和synchronized一样
总结
-
synchronized关键字
- 作用:保证同一时刻,只有一个线程能执行某个方法或代码块。
修饰:可以修饰方法、代码块。
特征:可见性、原子性、禁止代码重排序。
使用场景:多个线程对同一个对象中的同一个成员变量变量操作时,为了避免出现线程安全问题时使用。
- 作用:保证同一时刻,只有一个线程能执行某个方法或代码块。
-
volatile关键字
- 主要作用:让其他线程能看到修改后的最新的值。
修饰:只能修饰变量。
特征:可见性、原子性、禁止代码重排序。
使用场景:欲实现一个变量在被A线程修改后,其他线程立马能获取到最新值时候,就用volatile修饰这个变量。
- 主要作用:让其他线程能看到修改后的最新的值。
第3章 线程间通信
wait / notify 机制
-
原理
- 持有相同锁(对象级别的)的多个线程,在wait()处暂停执行,释放锁,直到接到通知,notify() 或者 notifyAll()再获取锁,继续执行。
【注】锁必须是对象级别的,因为wait(), notify(), notifyAll()是Object类中的,不是Thread类中的方法,所以必须要对象。
- 持有相同锁(对象级别的)的多个线程,在wait()处暂停执行,释放锁,直到接到通知,notify() 或者 notifyAll()再获取锁,继续执行。
-
wait()
- 暂停当前线程,释放锁
【注】执行wait()方法后,马上暂停线程么,并释放锁
- 暂停当前线程,释放锁
-
notify()
- 发出通知,wait状态的线程可以准备获取锁,开始执行了,只能通知“一个”线程,唤醒顺序同执行wait()顺序一致
【注】执行notify()方法后,不是马上暂停当前线程,而是要将当前线程同步代码块执行完毕后,才释放锁,故wait状态的线程也要等它执行完毕才能抢到锁
- 发出通知,wait状态的线程可以准备获取锁,开始执行了,只能通知“一个”线程,唤醒顺序同执行wait()顺序一致
-
notifyAll()
- 发出通知,wait状态的线程可以准备获取锁,开始执行了,默认按照执行wait()相反的顺序依次唤醒全部线程
【注】锁释放时机同notify()
- 发出通知,wait状态的线程可以准备获取锁,开始执行了,默认按照执行wait()相反的顺序依次唤醒全部线程
-
释放锁的时机
- wait() 立即释放
- notify() / notifyAll() 同步代码块执行完毕后再释放
-
wait(long time)
- 如果线程超过time时间没有被唤醒,则自动醒来,但是要执行的前提条件是再次持有锁,没有持有锁的话,一直等待,直到拿到锁才开始执行
time的单位为毫秒,其他用法一致
- 如果线程超过time时间没有被唤醒,则自动醒来,但是要执行的前提条件是再次持有锁,没有持有锁的话,一直等待,直到拿到锁才开始执行
-
wait() 与 sleep()的区别
- wait() 线程阻塞,立即释放锁
sleep() 线程阻塞,不 释放锁
- wait() 线程阻塞,立即释放锁
-
用while替代if
-
执行wait()后,线程阻塞,当线程醒来的时候
- 用if 不会再判断条件,直接运行
- 用while 会再次判断条件,满足条件才运行
-
-
生产者消费者模型的几种实现
-
不带缓冲区
- 1生产 1消费 操作值
- 多生产 多消费 操作值
-
带缓冲区
- 1生产 1消费 操作栈
- 1生产 多消费 操作栈
- 多生产 1消费 操作栈
- 多生产 多消费 操作栈
-
-
管道流通信
- 特殊的流,用于在不同的线程间直接传输数据
- 字节流
- 字符流
-
【案例】利用wait/notify实现数据库交叉备份
join() 方法的使用
-
使用场景
- 主线程要获取子线程的结果时,要用到join(),和使用Callable接口和FutureTask效果类似
-
join() 原理
- 主线程中,子线程调用此方法,则会相当于主线程执行wait()方法,直到子线程执行完毕,会通知主线程,主线程拿到锁以后继续执行
-
join() 与 sleep() 的区别
- join() 是线程间通信,释放锁 (主线程执行wait(),子线程执行完毕后通知主线程)
sleep() 是线程内部通信,不释放锁
- join() 是线程间通信,释放锁 (主线程执行wait(),子线程执行完毕后通知主线程)
-
join(long time)
- 不管子线程是否执行完毕,到时间后,且主线程拿到锁后,主线程就继续往下执行
-
join() 和 interrupt() 同时使用出现异常
- 彼此遇到会出现 InterruptedException,终止的是主线程,子线程还在继续
-
join(long time) 后面的代码可能提前运行,其实就是锁的问题
类ThreadLocal 的使用
- 原理及作用
- get() / set()
- 隔离性验证
- 重写initialValue() 方法解决get() 方法返回null的问题
- 不能实现值继承
类InheritableThreadLocal 的使用
-
原理及其作用
-
验证
- 子线程将父线程中的table对象以 复制 的方式赋值给子线程的table
-
值继承
- 父线程新值 子线程旧值
- 父线程旧值 子线程新值
-
对象继承
- 父子统一
-
重写childValue() 方法,对值进行加工
线程的生命周期
第4章 Lock 对象的使用
使用ReentrantLook类
重进入锁
-
相比于synchronize关键字实现线程间同步,ReentrantLook更加灵活,如具有嗅探锁定、多路分支通知等功能
-
线程间通信
- ReentrantLock实现了java.util.concurrent.locks包中的Lock接口,利用ReentrantLock对象中的lock()和unlock() 方法可以实现线程间同步,ReentrantLock具有互斥排他性。
-
结合Condition对象实现线程间通信
-
利用Condition对象的await()和signal() 方法实现wait()/notify()机制
-
线程对象注册在不同的Condition中,可以实现有选择性地进行线程通知(多路分支通知)
-
【注】必须在condition.await() 之前调用lock.lock()获取锁,因为Condition对象的await()方法执行后会将线程转换为wait状态,并释放锁
-
await() 方法暂停线程运行的原理
(这一块暂时不理解)- 内部执行了Unsafe类中的park() 方法
-
-
生产者消费者模型
-
公平锁与非公平锁
-
公平锁
- 先用先得,必须排队,排队尾
-
非公平锁
- “有机会插队”,先抢锁,抢不到再排到队尾去
-
-
实现
- ReentrantLock默认是非公平锁
- 构造公平/非公平锁的构造函数
public ReentrantLock(boolean fair)
true => 公平锁
false => 非公平锁
-
API
-
public int getHoldCount()
- 查询“当前线程”保持锁定的个数,
即调用lock()方法的次数
- 查询“当前线程”保持锁定的个数,
-
public final int getQueueLength()
- 获取正等待获取此锁的估计数,
【注】已经获取锁的不算
- 获取正等待获取此锁的估计数,
-
public int getWaitQueueLength(Condition condition)
- 获取与此锁相同的condition且正等待中的线程估计数
-
public final boolean hasQueuedThread(Thread thread)
- 判断参数中的线程是否在等待获取锁的队列中
-
public final boolean hasQueuedThreads()
- 判断是否在等待获取当前锁的队列中
-
public final boolean hasWaiters(Condition condition)
- 查询是否有线程执行了参数中的condition.await() 方法而呈等待状态
-
public final boolean isFair()
- 判断当前锁是不是公平锁
-
public boolean isHeldByCurrentThread()
- 判断当前线程是否持有当前锁
-
public final boolean isLocked()
- 判断当前锁是不是已经被获取且没有释放
-
public void lockInterruptibly()
- 当某个线程尝试获得锁并且阻塞在lockInterruptibly() 方法时,该线程可以被中断
-
public boolean tryLock()
- 嗅探拿锁,判断当前锁能不能拿到(没有被持有),能就拿到返回true
-
public boolean tryLock(long timeout, TimeUnit unit)
- 嗅探拿锁,判断当前锁能不能在有限时间内(timeout)拿到(没有被持有),能就拿到返回true
-
public boolean await(long timeout, TimeUnit unit)
- 线程等待,一段时间后自动唤醒线程,单位毫秒
-
public long awaitNanos(long nanosTimeout)
- 线程等待,一段时间后自动唤醒线程,单位纳秒
-
public boolean awaitUntil(Date deadline)
- 线程等待,在deadline时自动唤醒,单位毫秒
-
public void awaitUninterruptiably()
- 线程等待过程中,不允许被中断
-
使用ReentrantReadWriteLook类
读写锁
-
原理与使用
-
读操作相关的锁——共享锁
-
写相关的锁——排他锁
-
读写分离,提高系统效率
因为读几乎不需要同步,大家都可以读,但是写必须保证数据同步,所以可以只加“写锁”,不加“读锁” -
Demo
- ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
……
lock.readLock().lock();
lock.writeLock().lock();
……
lock.readLock().unlock();
lock.writeLock().unlock();
- ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
-
-
读读共享
-
写写互斥
-
读写互斥
-
写读互斥
第5章 定时器 Timer
原理及使用
-
- MyTask继承TimerTask类,重新run()方法
- 新建Timer对象,调用其不同的schedule方法实现定时任务
-
创建Timer对象的时候,会启动一个新的非守护线程TimerThread,且线程中存在一个死循环导致线程一直运行,可以调用timer.cancel()方法终止计时器
-
timer.cancel()方法
在多任务的Timer对象中调用时,会优先清空任务队列(已经在执行的任务不受影响),然后再销毁进程 -
多任务Timer对象中执行task任务的算法
每次最后一个执行的放入队列头,
如:第一次 ABC,
第二次 CAB,
第三次 BCA -
执行情况
- 正常执行
单个TimerTask任务
多个TimerTask任务,但时间没有交集, - 立即执行
计划执行的时间早于当前时间 - 延时执行
因为前面的任务可能消耗的时间比较长,后面的任务就会被延后
- 正常执行
API
-
schedule(TimerTask task, Date time)
- 指定时间执行一次某任务
-
schedule(TimerTask task, Date firstTime, long period)
- 指定时间之后,按照间隔时间无限循环执行某一个任务
-
schedule(TimerTask task, long delay)
- 以当前时间点为基准,delay毫秒后执行一次任务
-
schedule(TimerTask task, long delay, long period)
- 以当前时间点为基准,delay毫秒后
按照间隔时间无限循环执行任务
- 以当前时间点为基准,delay毫秒后
-
scheduleAtFixedRate(TimerTask task, long firstTime, long period)
- 与schedule()方法一样,只是加了【追赶性】
【大白话】这个方法就是要把无论因为某某原因导致延迟、没执行的任务,全部补上,补上后就和schedule()一样了
- 与schedule()方法一样,只是加了【追赶性】
第6章 单例模式与多线程
单例模式特征
- 单例模式特点:
- 一个类只有一个实例
- 自行实例化,并且向整个系统提供
- 反序列化时不会重新实例化对象
- 实现关键点:
- 私有化构造函数
- 自行实例化单个对象
- 使用静态方法提供自行实例化的单个对象
单例模式的使用场景
- 最好只能有一个对象存在的场景时使用单例模式。
如:1. 日志系统,只需要一个日志系统记录全局的
2. id生成器,保证id唯一性,单例更合适
3. 计时器、计数器,都是保证数据准确
4. 多数多线程的线程池,方便线程管理
5. 数据库连接,资源重用,减少频繁开关的资源消耗
几种不同单例模式的实现
-
饿汉模式 / 立即加载
- 类加载的时候实例化
优点:线程安全
缺点:
- 类加载的时候实例化
- 资源可能浪费
- 不能出现其他实例变量(不能传参),否则可能线程不安全
-
懒汉模式 / 延迟加载
-
DCL 双检查锁
- 第一次调用时实例化
优点:延迟加载,可能节省资源
缺点:必须实现同步,否则线程不安全 - DCL——Double-Check Locking,双检查锁
保证线程安全,提高多线程下效率 - 用volatile修饰实例变量的必要性
- 第一次调用时实例化
-
-
实现实例变量线程间可见
-
禁止实例化时代码重排序
-
静态内部类
- 第一次调用时实例化
优点:
- 第一次调用时实例化
-
-
线程安全
-
延迟加载
缺点:
实例化时不能传参-
实现原理:
利用类加载的特点(即:外部类加载时,并不需要立即加载内部类,内部类不被加载则不实例化)实现单例实例化和延迟加载,又因为静态内部类线程安全的特性,保证了整体的线程安全。
【内部类线程安全的原理不理解,摘抄自网上】 -
内部类线程安全的原理:
虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行 ()方法完毕。如果在一个类的 ()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行 ()方法后,其他线程唤醒之后不会再次进入 ()方法。同一个加载器下,一个类型只会初始化一次。)
【引用】https://blog.csdn.net/mnb65482/article/details/80458571 -
static代码块
- 原理
利用静态代码块的特性(使用类时加载静态代码块)实现
优点:
- 原理
-
-
延迟加载
-
线程安全
缺点:不能传参-
【我感觉】这东西就是饿汉模式实现了延迟加载,也是一种懒汉模式的实现吧
-
enum枚举
- 原理
利用枚举类的特性(使用枚举类时,自动调用其构造方法)实现
优点:延迟加载、线程安全
缺点:不能传参 - 和静态代码块实现差不多呀。。。。
- 原理
-
序列化与反序列化
- 使用默认反序列行为
单例模式下的对象也会变成多例 - 保证序列化和反序列化后单例的条件:
- 单例模式的类必须实现Serializable接口
- 且必须在同一个类中实现序列化和反序列化操作
- 且反序列化必须调用readResolve()方法
第7章 拾遗补漏
线程的状态
-
状态信息存储在Thread.State枚举类中
-
线程的五种状态
-
NEW
- 至今尚未启动的线程状态
-
RUNABLE
- 正在Java虚拟机中执行的线程状态
-
BLOCKED
- 受阻塞,且等待某个监视器锁的线程状态
-
WAITING
- 无限等待另一个线程执行某一操作的线程状态
-
TIMED_WAITING
- 有时间限制地等待另一个线程执行某一操作的线程状态
-
TERMINATED
- 已退出的线程状态
-
-
线程的生命周期
- 根据书上的图简化了一点
线程组
-
线程组实现和特性
- ThreadGroup类
- 一级关联(常用)
多级关联(不常用) - 线程自动归组属性
-
ThreadGroup类中的API
-
activeCount()
- 获取当前线程组中子线程组的数量
-
enumerate(ThreadGroup array[])
- 将当前线程组中的子线程 复制 到数组中
- enumerate(ThreadGroup array[], boolean recurve)
true 递归复制,false 非递归
-
ThreadGroup.getParent()
- 获取父线程组
- 【根线程组】system,再往上就抛空指针了
-
-
Thread类中的API
-
activeCount()
- 返回 当前线程 的线程组 中活动线程的数目
-
enumerate(Thread tarray[])
- 调用 当前线程 的线程组 的enumerate()方法
-
-
再次实现线程执行有序性
- “没看懂这个。。。。”
SimpleDateFormat类 非线程安全
- 非线程安全原因:单例
- 解决办法:
- 多例,每次使用都new一个
- 使用ThreadLocal
保证其线程安全
线程中异常处理
-
异常处理
- 线程中处理
- 线程组内处理
-
异常处理优先级
- 调用过setUncaughtExceptionHandler()方法的优先处理,其他的不处理了