一、背景
二、关键知识点
1. 线程启动
- 启动线程的两种方法:继承Thread类和实现Runnable接口
- run()和start()的区别
- run():直接调用Thread 对象的run方法并没有开启新线程,与调用普通方法无异;run()方法是线程运行时由 JVM 回调的方法,无需手动调用
- start():调用 Thread 对象的 start() 方法开启了新线程,此时线程处于可运行状态,需要等待JVM调度并执行
- run()和start()的区别
- 获取线程名注意事项:
- Thread.currentThread().getName():执行当前代码块的线程实例的名称
- this.getName():当前线程实例对象的线程名称
2. 线程暂停
- 使用suspend与resume方法:造成公共的同步对象的独占
3. 线程停止
(1) 两种方法
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用interrupt方法中断线程
- 建议使用“抛异常”的方法实现线程停止,使线程停止的事件得以传播
(2) 步骤
- 判断线程是否停止
- Thread.currentThread().getName():执行当前代码块的线程实例的名称
- this.getName():当前线程实例对象的线程名称
- 使用interrupt方法中断线程。
4. 线程优先级:setPriority
- 继承性:若A线程启动B线程,则B线程的优先级等于A
- 规则性:高优先级的线程总是大部分先执行完,但不代表高优先级的线程全部先执行完
- 随机性:
5. 线程安全相关
三、对象及变量
共享资源的读写访问才需要同步化
1. synchronized
- 特性:互斥性、可见性
- 可重入锁:
- 自己可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁
- 适用情况:本类、父子类(子类可以通过“可重入锁”调用父类的同步方法)
- 监视器:
- synchronized(非this对象x):将x对象本身作为“对象监视器”
- synchronized+static静态方法是给Class类上锁(Class锁),而synchronized+非static静态方法给对象上锁(Class锁),Class锁可以对类的所有对象实例起作用
- synchronized一般不使用String作为锁对象,因String会放入常量池中导致线程获取到的锁相同
- 判断同步/异步:关键在于是否获取相同的锁
- A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法
- A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步
(1) 可重入锁
- 概念:自己可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁
- 适用情况:本类、父子类(子类可以通过“可重入锁”调用父类的同步方法)
-
(1) synchronized同步方法=synchronized(this)同步代码块
- 与其他synchronized方法或synchronized代码块呈阻塞状态
2. volatile
- 使变量在多个线程间可见
- 强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值
- 不支持原子性
- 主要使用的场合是在多个线程中可以感知实例变量被更改了,并且可以获得最新的值使用,也就是用多线程读取共享变量时可以获得最新值使用
四、线程间通信
1. wait/notify
- wait使线程停止运行,而notify使停止的线程继续运行
- wait方法:使处于临界区内的线程进入等待状态,在wait所在代码行处停止,同时释放被同步对象的锁,直到接到通知或被中断为止
- 调用wait前,必须获得该对象的对象级别锁,否则抛IllegalMonitorStateException异常
- 调用wait后,释放对象锁
- notify方法:通知等待对象锁的其他线程,若有多个线程等待,由线程规划器随机挑选一个线程
- 调用notify后,需等到执行notify方法的线程将程序执行完,退出synchronized代码块后,当前线程才会释放锁;若该对象没有再次使用notify语句,其他wait状态等待的线程由于没有得到该对象的通知,会继续阻塞在wait状态,直到这个对象发出一个notify或notifyAll
2. 线程状态
- 新建状态:线程对象
- 就绪状态(Runnable):调用start方法,系统为此线程分配CPU资源,使其处于Runnable(可运行)状态
- 5种进入Runnable状态的情况:
- sleep后超过指定休眠时间
- 阻塞IO已返回,阻塞方法执行完毕。
- 获得试图同步的监视器
- 等待某个通知,其他线程发出了通知
- 处于挂起状态的线程调用了resume恢复方法。
- 运行状态
- 阻塞状态(Blocked):
- 5种出现阻塞的情况:
- 线程调用sleep方法,主动放弃占用的处理器资源。
- 线程调用了阻塞式IO方法,在该方法返回前,该线程被阻塞。
- 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
- 线程等待某个通知。
- 程序调用了suspend方法将该线程挂起。此方法容易导致死锁,尽量避免使用该方法。
- 5种出现阻塞的情况:
- 死亡状态
3. 生产者/消费者模式实现
- “假死”:呈假死状态的进程中所有线程都呈WAITING状态
- 原因:notify唤醒的不一定是异类,也许是同类,如“生产者”唤醒“生产者”。
- 解决假死:将notify()改为notifyAll()
- 注意事项:synchronized方法、while感知队列大小、notifyAll
4. 管道实现线程间通信
- 概念:管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据。一个线程发送数据到输出管道,另一个线程从输入管道中读数据。通过使用管道,实现不同线程间的通信,而无须借助于类似临时文件之类的东西
- PipedInputStream和PipedOutputStream
- PipedReader和PipedWriter
5. join
- 概念:方法join()的作用是等待线程对象销毁。主线程想等待子线程执行完成之后再结束
- sleep()与join()区别:
6. ThreadLocal
- 每个线程绑定自己的值
- 赋初值:继承ThreadLocal类,重写覆盖initialValue()方法具有初始值
7. InheritableThreadLocal
- 类InheritableThreadLocal可以在子线程中取得父线程继承下来的值
- 如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那么子线程取到的值还是旧值
五、Lock的使用
1. ReentrantLock
- 借助Condition实现等待/通知
- notify = signal
- 多路通知功能:在一个Lock对象里面可以创建多个Condition(即对象监视器)实例
- 使用notify/notifyAll方法进行通知时,被通知的线程却是由JVM随机选择的
- 使用ReentrantLock结合Condition类是可实现“选择性通知”
- 顺序执行:
- getHoldCount:查询当前线程保持此锁定的个数
- getQueueLength:返回正等待获取此锁定的线程估计数
- getWaitQueueLength:
- hasQueuedThread:查询指定的线程是否正在等待获取此锁定
- hasQueuedThreads:查询是否有线程正在等待获取此锁定
- hasWaiters(Condition condition):查询是否有线程正在等待与此锁定有关的condition条件
- isHeldByCurrentThread:查询当前线程是否保持此锁定
- isLocked:查询此锁定是否由任意线程保持
2. ReentrantReadWriteLock
- 读写锁:共享锁+排他锁
- 多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥
六、定时器
1. 指定时间/周期执行
- TimerTask:任务执行完线程即结束,Timer初始化需加true参数,以守护线程启动任务
- TimerTask类的cancel方法:将自身从任务队列中被移除,其他任务不受影响
- Timer类中的cancel()方法的作用是将任务队列中的全部任务清空
- Timer类中的cancel()方法有时若没有争抢到queue锁,则TimerTask类中的任务继续正常执行
- 延时机制:
- 使用schedule方法:如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“开始”时的时间来计算
- 如果执行任务的时间没有被延时,则下一次执行任务的时间是上一次任务的开始时间加上delay时间
- 如果执行任务的时间被延时,那么下一次任务的执行时间以上一次任务“结束”时的时间为参考来计算
- 使用scheduleAtFixedRate方法:如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“结束”时的时间来计算
- 如果执行任务的时间没有被延时,则下一次执行任务的时间是上一次任务的开始时间加上delay时间
如果执行任务的时间被延时,那么下一次任务的执行时间以上一次任务“结束”时的时间为参考来计算
- 如果执行任务的时间没有被延时,则下一次执行任务的时间是上一次任务的开始时间加上delay时间
- 使用schedule方法:如果执行任务的时间没有被延时,那么下一次任务的执行时间参考的是上一次任务的“开始”时的时间来计算
七、单例模式与多线程
- DCL双检查锁机制来实现多线程环境中的延迟加载单例设计模式
- 使用静态内置类实现单例模式
- 序列化与反序列化的单例模式实现
- 使用static代码块实现单例模式
- 使用enum枚举数据类型实现单例模式
八、补充
1. 线程状态切换
2. 常见非线程安全解决方案
- SimpleDateFormat非线程安全,解决办法有:
- 创建多个SimpleDateFormat类的实例
- 使用ThreadLocal类
- 线程组出现异常的处理
- setUncaughtExceptionHandler()给指定线程对象设置异常处理器
- setDefaultUncaughtExceptionHandler()对所有线程对象设置异常处理器
九、参考
- Java多线程编程核心技术
- https://segmentfault.com/a/1190000004962367