# Java 多线程
# 1. Thread 类
Java 语言的 JVM 允许程序运行多个线程,它通过 java.lang.Thread 类来体现。
# 1.1 Thread 类的特性
- 每个线程都是通过某个特定 Thread 对象的 run() 方法来完成操作的,经常把 run() 方法的主体称为线程体。
- 通过该 Thread 对象的 start() 方法来启动这个线程,而非直接调用 run()。
# 1.2 Thread 类的构造器
- Thread():创建新的 Thread 对象。
- Thread(String threadname):创建线程并指定线程实例名。
- Thread(Runnable target):指定创建线程的目标对象,它实现了 Runnable 接口中的 run() 方法。
- Thread(Runnable target, String name):创建新的 Thread 对象。
# 1.3 Thread 类常用方法
- void start():启动线程,并执行对象的 run() 方法
- run():线程在被调度时执行的操作
- String getName():返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread():返回当前线程
- static void yield():线程让步
- 释放时间片,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程;
- 若队列中没有同优先级的线程,忽略此方法;
- 调用 yield() 后还是 RUNNABLE 状态,并不一定会释放 CPU,也不会释放锁。
- join()
当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
低优先级的线程也可以获得执行
join() 底层调用了 wait(long timeout);
thread.join()
等价于
synchronized(thread){
thread.wait();
};
join() 没传参的时候会执行 wait(0),但是没有 notify(),那谁来 notify() 的呢?
每一个 Thread 对象执行完成后,JVM 会自动执行 notifyAll() 方法。
- static void sleep(long millis)
- 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队;
- 不会释放锁(synchronized 和 lock);
- 可以响应 interrupt() 中断,并抛出 InterruptedException 异常,然后清除中断标志。
- stop():强制线程生命期结束,不推荐使用
- boolean isAlive():返回 boolean,判断线程是否还活着
线程的优先级
- getPriority():返回线程优先值
- setPriority(int newPriority):改变线程的优先级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5
说明:
- 线程创建时继承父线程的优先级;
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。
守护线程
- setDaemon(Boolean on):是否为守护线程
Java 中的线程分为两类:一种是守护线程,一种是用户线程。
- 它们在几乎每个方面都是相同的,唯一的区别是判断 JVM 何时离开;
- 守护线程是用来服务用户线程的,通过在 start() 法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程;
- Java 垃圾回收就是一个典型的守护线程;
- 若 JVM 中都是守护线程,当前 JVM 将退出;
- 形象理解:兔死狗烹,鸟尽弓藏。
# 2. 线程的创建
# 2.1 创建方法一:继承 Thread 类
# 步骤
1. 定义子类继承 Thread 类
2. 子类中重写 Thread 类的 run() 方法
3. 创建 Thread 子类对象,即创建了线程对象。
4. 调用线程对象 start() 方法:启动线程,调用 run() 方法。
class ThreadWayOne extends Thread {
//构造方法
public ThreadWayOne() {
super();
}
//线程体 => 重写 run() 方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("线程输出:"+i);
}
}
}
测试:
public class ThreadWayOneTest {
public static void main(String[] args) {
ThreadWayOne threadWayOne1 = new ThreadWayOne();
ThreadWayOne threadWayOne2 = new ThreadWayOne();
ThreadWayOne threadWayOne3 = new ThreadWayOne();
//调用的是start()方法而不是run()方法
threadWayOne1.start();
threadWayOne2.start();
threadWayOne3.start();
}
}
输出:
# 注意点:
1. 如果自己手动调用 run() 方法,那么就只是普通方法,没有启动多线程模式。
2. run() 方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU 调度决定。
3. 想要启动多线程,必须调用 start() 方法。
4. 一个线程对象只能调用一次 start() 方法启动,如果重复调用了,则将抛出以上的异常 `IllegalThreadStateException`。
# 2.2 创建方法二:实现 Runnable 接口
# 步骤:
1. 定义类实现 Runnable 接口
2. 子类中重写 Runnable 接口中的 run() 方法。
3. 通过 Thread 类含参构造器创建线程对象。
4. 将 Runnable 接口的子类对象作为实际参数传递给 Thread 类的构造器中。
5. 调用 Thread 类的 start() 方法开启线程,调用 Runnable 子类接口的 run() 方法。
//1. 实现 Runnable 接口
class ThreadWayTwo implements Runnable{
//2. 子类中重写 Runnable 接口中的run方法 => 编写线程体
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0)
System.out.println("这是继承 Runnable 接口实现的多线程->"+Thread.currentThread().getName()+":"+i);
}
}
}
public class ThreadWayTwoTest {
public static void main(String[] args) {
//4.将 Runnable 接口的子类对象作为实际参数传递给Thread类的构造器中。
ThreadWayTwo threadWayTwo = new ThreadWayTwo();
//3. 通过 Thread 类含参构造器创建线程对象
Thread thread = new Thread(threadWayTwo);
//5. 调用 Thread 类的 start() 方法开启线程,调用 Runnable 子类接口的 run() 方法。
thread.start();
for (int i = 0; i < 100; i++) {
if (i%2 == 1)
System.out.println("主线程:"+i);
}
}
}
上述两种创建线程方式的比较:
# Thread 类与 Runnable 类的联系:
public class Thread implements Runnable
Thread 类其实也是实现了 Runnable 接口然后重写了 run() 方法
# 区别:
继承 Thread:线程代码存放在 Thread 子类 run() 方法中。
实现 Runnable:线程代码存在接口的实现类的 run() 方法。
# 实现 Runnable 接口的好处:
避免了单继承的局限性;
多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
使用 Runnable 可以一直利用同一个 Thread 对象,不断更改其 target 即可,这样减少了新建线程的消耗。
# 同时用 Thread 和 Runnable 两个方法会怎么样?
继承了 Thread 类的会重写 Thread 中的 run() 方法,所以 Thread 中的 run() 方法不会去判断 target 是否为空,而是被直接覆盖了,所以只会执行继承了 Thread 类的线程类。
# 2.3 创建方法三:实现 Callable 接口
# 步骤
1. 创建一个实现 Callable 的实现类
2. 重写 call() 方法,将此线程需要做的事情放在 call() 里面
3. 创建 Callable 实现类的对象
4. 将上述对象作为参数传递到 FutureTask 构造器中,创建 FutureTask 对象
5. 将 FutureTask 对象作为参数传递到 Thread 构造器中,创建 Thread 对象
6. 调用 Thread 对象的 start() 启动线程
7. 可以通过 FutureTask 对象获取 call() 方法的返回值等信息
# 与实现 Runnable 相比,Callable 功能更强大些
- ① call() 方法相比 run() 方法,可以有返回值
- ② call() 方法可以抛出异常
- ③ 支持泛型的返回值
- ④ 需要借助 FutureTask 类,比如获取返回结果
# Future 接口
- 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等;
- FutureTask 是 Future 接口的唯一的实现类;
- FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返 回值。
class ThreadNew implements Callable{
/**
* 实现 Callable 是重写 call() 方法
* @return 有返回值
* @throws Exception 可以抛异常
*/
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
if( i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadByCallable{
public static void main(String[] args) {
ThreadNew threadNew = new ThreadNew();
FutureTask futureTask = new FutureTask(threadNew);
//还是得 new 一个 Thread
Thread thread = new Thread(futureTask);
//启动线程
thread.start();
try {
//通过 get() 方法获取 call() 返回的值
Object sum = futureTask.get();
System.out.println("sum = "+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
# 2.4 创建方法四:使用线程池
# 背景
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程, 对性能影响很大。
# 思路
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
# 好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize: 核心池的大小
- maximumPoolSize: 最大线程数
- keepAliveTime: 线程没有任务时最多保持多长时间后会终止
- ...
# Executors
工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n): 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor(): 创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
# ExecutorService
真正的线程池接口。常见子类 ThreadPoolExecutor。
- void execute(Runnable command): 执行任务/命令,没有返回值,一般用来执行 Runnable
- <T> Future<T> submit(Callable<T> task): 执行任务,有返回值,一般用来执行 Callable
- void shutdown(): 关闭连接池
- 示例
public class ThreadPoolTest {
public static void main(String[] args) throws Exception{
//创建使用单个线程的线程池
ExecutorService es1 = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
es1.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建使用固定线程数的线程池
ExecutorService es2 = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
//execute 适用于 Runnable
es2.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
//submit 适用于 Callable (Runnable 也可以)
es2.submit(new Callable() {
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
return null;
}
});
}
//创建一个会根据需要创建新线程的线程池
ExecutorService es3 = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
es3.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行任务");
}
});
}
//创建拥有固定线程数量的定时线程任务的线程池
ScheduledExecutorService es4 = Executors.newScheduledThreadPool(2);
System.out.println("时间:" + System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
es4.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+"--"+Thread.currentThread().getName() + "正在执行任务");
}
},3, TimeUnit.SECONDS);
}
//创建只有一个线程的定时线程任务的线程池
ScheduledExecutorService es5 = Executors.newSingleThreadScheduledExecutor();
System.out.println("时间:" + System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
es5.schedule(new Runnable() {
@Override
public void run() {
System.out.println("时间:"+System.currentTimeMillis()+"--"+Thread.currentThread().getName() + "正在执行任务");
}
},3, TimeUnit.SECONDS);
}
//关闭线程池
es1.shutdown();
es2.shutdown();
es3.shutdown();
es4.shutdown();
es5.shutdown();
}
}
# 3. 线程的停止
核心:使用 interrupt() 来通知线程自己去停止,而不是强制线程停止。
# 3.1 线程停止的情况
# ① 正常情况下停止
- 主线程使用
子线程.interrupt()
通知子线程停止线程; - 子线程使用
Thread.currentThread().isInterrupted()
检测当前线程是否被中断,如果 true,就中断当前线程。
/**
* run() 方法内没有 sleep() 或 wait() 方法时,停止线程的正确方式
*
* @author Hedon Wang
* @create 2021-02-19 9:52 PM
*/
public class RightStopThreadWithoutSleep implements Runnable{
@Override
public void run() {
//这里需要对 interrupt() 方法进行响应,否则不会停止线程
for (int i=0;i<=Integer.MAX_VALUE/2 && !Thread.currentThread().isInterrupted();i++){
if( i % 10000 == 0){
System.out.println(i + "是10000的倍数");
}
}
//下面这句使用 interrupt() 的话会输出,采用 thread.stop() 的话不会
System.out.println("over....");
}
public static void main(String[] args) throws InterruptedException {
//新建线程
Thread thread = new Thread(new RightStopThreadWithoutSleep());
//启动线程
thread.start();
//阻塞main线程
Thread.sleep(100);
//通知子线程停止
thread.interrupt();
// thread.stop();
}
}
# ② 线程阻塞情况下停止
/**
* 在阻塞情况下如何正确停止线程
*
* @author Hedon Wang
* @create 2021-02-19 9:59 PM
*/
public class RightStopThreadWithSleep implements Runnable{
@Override
public void run() {
int num = 0;
try {
//这里希望子线程快速执行然后进入阻塞状态
while (num <= 300 && !Thread.currentThread().isInterrupted()){
if (num % 100 == 0){
System.out.println(num + "是100的倍数");
}
num++;
}
//子线程睡眠,等待
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException{
//新建线程
Thread thread = new Thread(new RightStopThreadWithSleep());
//启动子线程
thread.start();
//阻塞当前线程,这里阻塞时间比子线程阻塞时间短,确保会执行到子线程阻塞的过程中
Thread.sleep(500);
//通知子线程停止
thread.interrupt();
}
}
输出结果:
会抛出异常的原因:
- 线程在 sleep() 过程中如果被通知要中断的时候,就会报 InterruptedException 异常。
# ③ 线程在每次迭代后都阻塞的情况下停止
【情况一: try/catch 包围 while】
/**
* 执行过程中每一次循环都阻塞(sleep或wait),该如何正确停止线程:情况一
*
* @author Hedon Wang
* @create 2021-02-19 10:10 PM
*/
public class RightStopThreadWithSleepEveryLoop implements Runnable {
@Override
public void run() {
int num = 0;
try {
/**
* 这里不需要判断 Thread.currentThread().isInterrupted()
* 原因:代码大部分实现都在 sleep(),而 sleep 过程中如果被 interrupt() 的话自然会捕获异常然后抛出
*/
while (num < 10000) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
//每次迭代都阻塞子线程
Thread.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightStopThreadWithSleepEveryLoop());
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
输出结果:
【情况二:try/catch 在 while 中】
/**
* 执行过程中每一次循环都阻塞(sleep或wait),该如何正确停止线程:情况二
*
* @author Hedon Wang
* @create 2021-02-19 10:10 PM
*/
public class RightStopThreadWithSleepEveryLoop implements Runnable {
@Override
public void run() {
int num = 0;
while (num < 10000) {
//将 try/catch 放在 while 中
try {
//不管使不使用Thread.currentThread().isInterrupted()结果都一样
if (num % 100 == 0 && !Thread.currentThread().isInterrupted()) {
System.out.println(num + "是100的倍数");
}
num++;
//每次迭代都阻塞子线程
Thread.sleep(10);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightStopThreadWithSleepEveryLoop());
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
输出结果:
报错后子线程并没有停止,而是继续执行的原因:
interrupt()
只是通知线程中断,而不是强制线程中断,线程何时中断以及是否要中断的决定权都在线程本身。- 所以
try/catch
后还是会进入下一轮循环。而 Java 中如果捕获到InterruptedException
异常并处理后,会将中断标记位清除,所以在下一轮循环中,线程检测不到被中断,就会继续执行。
# 3.2 正确的两种方式
# ① 优先选择:传递中断
- 业务方法捕获到中断信息后不要 try/catch,而是 throws 给调用方。
- 调用方感知到线程被中断后再进行相应的处理。
/**
* 优先选择:传递中断
* 说明:
* catch 了 InterruptedException 后在方法前面中抛出异常
* 那么在子线程中的 run() 方法就会强制 try/catch
*
* @author Hedon Wang
* @create 2021-02-20 9:59 PM
*/
public class RightWayStopThreadByDelivery implements Runnable{
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()){
//模拟业务
System.out.println("go");
//因为方法传递了中断,所以调用方可以感知到,然后可以进行相应的处理
try {
throwInMethod();
} catch (InterruptedException e) {
System.out.println("调用方知道中断后进行了一系列操作....[保存日志/停止程序/...]");
e.printStackTrace();
}
}
}
/**
* 一次会抛出异常的方法
*/
private void throwInMethod() throws InterruptedException{
try {
Thread.sleep(1000);
}catch (InterruptedException e){
//e.printStackTrace()打印日志信息,看似处理了异常
//但是其实没有处理,因为程序运行到服务器上,你打印的信息一般是看不到的
//所以这里实际上是把中断给吞了,调用方是感知不到的
e.printStackTrace();
//所以最好是把中断传递给调用方
throw e;
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new RightWayStopThreadByDelivery());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
注意:run() 不允许抛出异常,只能 try/catch。
# ② 不想或无法传递:恢复中断
- 使用 try/catch 来捕获中断异常
- 在 catch 中使用
Thread.currentThread().interrupt()
恢复中断
/**
* 不想或无法传递中断:恢复中断
* 说明:
* 当捕获到中断信息后,由于方法限制无法 throws InterruptedException 或者不想 throws 的话
* 可以在 try/catch 语句中的 catch 块中调用 Thread.currentThread().interrupt() 来中断当前线程
* 这样就可以一直保持一个中断的情况,就不会吞掉中断信息,这样后续执行过程中还是可以检查到中断的
*
*
* @author Hedon Wang
* @create 2021-02-20 10:29 PM
*/
public class RightWayStopThreadByRecovery implements Runnable{
@Override
public void run() {
while (true && !Thread.currentThread().isInterrupted()){
System.out.println("go");
reInterrupt();
}
System.out.println("程序已被中断");
}
private void reInterrupt(){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
//恢复中断
Thread.currentThread().interrupt();
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new RightWayStopThreadByRecovery());
thread.start();
Thread.sleep(500);
thread.interrupt();
}
}
输出结果:
# 3.3 能够响应中断的方法总结
- Object.wait() / wait(long) / wait(long, int)
- Thread.sleep(long) / sleep(long, int)
- Thread.join() / join(long) / join(long, int)
- java.util.concurrent.BlockingQueue.take() / put(E)
- java.util.concurrent.locks.Lock.lockInterruptibly()
- java.util.concurrent.CountDownLatch.await()
- java.util.concurrent.CyclicBarrier.await()
- java.util.concurrent.Exchanger.exchange(V)
- java.nio.channels.InterruptibleChannel 的相关方法
- java.nio.channels.Selector 的相关方法
# 3.4 几个错误的方式
# ① 被弃用的 stop()、suspend() 和 resume() 方法
- stop()
package stopthreads;
/**
* 停止线程的错误方法:stop()
* 说明:
* 会导致线程运行一半突然停止,没办法完成一个基本单位的操作;
* 举例:
* 下面模拟一个军队领物资的请教,调用 stop() 瞬间停止线程会导致有的连队多领取,而有的连队少领取装备,
* 这就产生了脏数据
*
* @author Hedon Wang
* @create 2021-02-20 10:48 PM
*/
public class WrongWayStopThreadByStop implements Runnable{
@Override
public void run() {
//模拟指挥军队:5个连队,每个连队10人,以连队为单位发放弹药,叫到号的士兵前去领取
for (int i = 0; i < 5; i++) {
System.out.println("连队"+i+"开始领取武器");
for (int j = 0; j < 10; j++) {
System.out.println("连队"+i+"中的士兵"+j+"开始领取武器>>>>");
try {
Thread.sleep(50);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("连队"+i+"中的士兵"+j+"领取完武器<<<<<<");
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(new WrongWayStopThreadByStop());
thread.start();
Thread.sleep(1000);
//强制停止线程
thread.stop();
}
}
输出结果:
# 为什么 Thread.stop() 会被弃用?
因为它本质上是不安全的。
停止线程会导致它解锁已锁定的所有监视器。(当 ThreadDeath 异常传播到堆栈中时,监视器将被解锁)。
如果先前受这些监视器保护的任何对象处于不一致状态,则其他线程现在可以以不一致的状态查看这些对象。
当线程对受损对象进行操作时,可能会导致任意行为。这种行为可能很微妙并且难以检测,或者可能是明显的。
与其他未经检查的异常不同,它会 ThreadDeath 默默地杀死线程。
因此,用户没有警告他的程序可能被破坏。腐败可以在实际损害发生后的任何时间显现,甚至在未来几小时或几天。
suspend()、resume()
- suspend() 是挂起,resume() 是唤醒线程
- supsend() 是带着锁去挂起线程,所以它需要另一个线程来唤醒它。如果它需要的那个线程被它给锁住了,就永远不可能来唤醒它了,就造成了死锁。
# ② 用 volatile 设置 boolean 标记位
/**
* 生产者
*/
class Producer implements Runnable{
public volatile boolean canceled = false;
BlockingQueue<Integer> storage;
public Producer(BlockingQueue<Integer> storage){
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !canceled){
//生产者生产特别快
if (num % 100 == 0){
//这里如果 storage 满了,就会被阻塞
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了...");
}
num++;
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
System.out.println("生产者停止运行..");
}
}
}
/**
* 消费者
*/
class Consumer {
BlockingQueue<Integer> storage;
public Consumer(BlockingQueue<Integer> storage){
this.storage = storage;
}
/**
* 是否还需要数字
*/
public boolean needMoreNums(){
double c = Math.random();
if (c > 0.95){
return false;
}else{
return true;
}
}
}
public class VolitileOne{
public static void main(String[] args) throws InterruptedException {
//仓库
BlockingQueue<Integer> storage = new ArrayBlockingQueue<>(10);
//生产者
Producer producer = new Producer(storage);
Thread thread = new Thread(producer);
thread.start();
Thread.sleep(1000);
//消费者
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()){
System.out.println(storage.take() + "被消费了....");
//消费者消费速度远小于生产者生产速度
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了....");
//让生产者停下来
producer.canceled = true;
System.out.println(producer.canceled);
}
}
原因分析:
- 生产者在
storage.put(num)
那个地方阻塞的,不会执行到while (num <= 100000 && !canceled)
的判断语句中,所以不会检测到 cancel 已经被置为 true 了,导致程序一直处于阻塞状态没有停止。
用 interrupt() 改进:
/**
* 生产者
*/
class Producer implements Runnable{
BlockingQueue<Integer> storage;
public Producer(BlockingQueue<Integer> storage){
this.storage = storage;
}
@Override
public void run() {
int num = 0;
try {
while (num <= 100000 && !Thread.currentThread().isInterrupted()){
if (num % 100 == 0){
storage.put(num);
System.out.println(num + "是100的倍数,被放到仓库中了...");
}
num++;
}
}catch (InterruptedException e){
e.printStackTrace();
}finally {
System.out.println("生产者停止运行..");
}
}
}
/**
* 消费者
*/
class Consumer {
BlockingQueue<Integer> storage;
public Consumer(BlockingQueue<Integer> storage){
this.storage = storage;
}
/**
* 是否还需要数字
*/
public boolean needMoreNums(){
double c = Math.random();
if (c > 0.95){
return false;
}else{
return true;
}
}
}
public class VolitileOne{
public static void main(String[] args) throws InterruptedException {
//仓库
BlockingQueue<Integer> storage = new ArrayBlockingQueue<>(10);
//生产者
Producer producer = new Producer(storage);
Thread thread = new Thread(producer);
thread.start();
Thread.sleep(1000);
//消费者
Consumer consumer = new Consumer(storage);
while (consumer.needMoreNums()){
System.out.println(storage.take() + "被消费了....");
Thread.sleep(100);
}
System.out.println("消费者不需要更多数据了....");
//让生产者停下来
thread.interrupt();
}
}
结果:线程可以正确结束
# 3.5 停止线程相关的重要方法解析
# interrupt()
# static boolean interrupted()
返回当前线程是否被中断,返回后会清除中断标志。
# boolean isInterrupted()
返回当前线程是否被中断。
# Thread.interrupted()
目标对象是当前运行的线程。
# 4. 线程的生命周期
# 4.1 六种状态 —— Thread.State
JDK 中用 Thread.State 类定义了线程的几种状态。
要想实现多线程,必须在主线程中创建新的线程对象。Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的六种状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
# 4.2 示意图
# 特殊情况
从 Object.wait() 状态刚被唤醒时,通常不能立刻抢到 monitor 锁,那就会从 WAITING 先进入 BLOCKED 状态,抢到锁后再转换为 RUNNABLE 状态。如果发生异常,可以直接跳到终止 TERMINATED 状态,不必再遵循路径,比如可以从 WAITING 直接到 TERMINATED。
# 5. 线程的属性
ID
- 线程 ID 是自增的,而且每次重启应用后会重新从 1 开始递增。
Name
- 便于开发者识别各个线程。
守护线程
- 守护线程是作用是给用户线程提供服务。
- 如果所有线程都是守护线程,那么所有的守护线程和 JVM 会一起停止工作。
优先级
- 表明开发者希望哪些线程多被执行,哪些线程少被执行。
- 默认和父线程的优先级相等,共有 10 个等级,默认值是 5。
- 不同操作系统对优先级的定义是不一样的,Java 线程最终是由操作系统来执行的,而 Java 中的优先级映射到不同的操作系统的线程优先级是不一样的。
- 所以最好是不要设置线程的优先级,因为 Java 可以运行在不同的操作系统中,这就会导致线程优先级映射结果不同,这样就造成了我们程序的不可靠。
# 6. 线程的异常
# 6.1 为什么需要 UncaughtExceptionHandler?
- 主线程可以轻松发现异常,子线程却不行(子线程会有堆栈信息输出,但是主线程还是畅通无阻地执行)。
- 子线程的异常无法在主线程中用传统方法捕获(try/catch 只能捕获当前线程的异常)。
- 不捕获子线程的异常的话,子线程会直接停止,会影响程序的正常执行。
# 6.2 两种解决方案
① 手动在每个 run() 方法中进行 try/catch —— 不推荐
- 无法预料线程中是否有异常及其异常类型。
- 每个线程类都 try/catch 的话代码太冗余。
② 利用 UncaughtExceptionHandler —— 推荐
- 可以捕获线程由于未捕获异常而终止的情况,并可以对其进行处理。
- void uncaughtException(Thread t, Throwable e)
# 6.3 UncaughtException 异常处理器的调用策略
public class ThreadGroup implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
//默认情况下 parent 是 null
if (parent != null) {
parent.uncaughtException(t, e);
} else {
//调用 Thread.getDefaultUncaughtExceptionHandler() 方法设置的全局 handler 进行处理
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
//全局 handler 存在就处理异常
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
//全局 handler 不存在就输出异常
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
}
- 所以我们需要做的就是定义一个全局异常处理器,让它来处理子线程未捕获的异常并对其进行处理。
# 6.4 自定义 UncaughtExceptionHandler
自定义 UncaughtExceptionHandler
/**
* 自定义 UncaughtExceptionHandler
*
* @author Hedon Wang
* @create 2021-03-10 3:42 PM
*/
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name){
this.name = name;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING,t.getName() +"线程异常终止啦",e);
}
}
测试:
/**
* 演示使用自定义的 UncaughtExceptionHandler
*
* @author Hedon Wang
* @create 2021-03-10 3:46 PM
*/
public class UseOwnUncaughtExceptionHandler implements Runnable{
@Override
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) throws Exception{
//设置全局异常处理器
Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1"));
new Thread(new UseOwnUncaughtExceptionHandler()).start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler()).start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler()).start();
Thread.sleep(300);
new Thread(new UseOwnUncaughtExceptionHandler()).start();
}
}
输出:
三月 10, 2021 3:47:58 下午 threadException.MyUncaughtExceptionHandler uncaughtException
警告: Thread-0线程异常终止啦
java.lang.RuntimeException
at threadException.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:13)
at java.lang.Thread.run(Thread.java:748)
三月 10, 2021 3:47:58 下午 threadException.MyUncaughtExceptionHandler uncaughtException
警告: Thread-2线程异常终止啦
java.lang.RuntimeException
at threadException.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:13)
at java.lang.Thread.run(Thread.java:748)
三月 10, 2021 3:47:58 下午 threadException.MyUncaughtExceptionHandler uncaughtException
警告: Thread-3线程异常终止啦
java.lang.RuntimeException
at threadException.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:13)
at java.lang.Thread.run(Thread.java:748)
三月 10, 2021 3:47:59 下午 threadException.MyUncaughtExceptionHandler uncaughtException
警告: Thread-4线程异常终止啦
java.lang.RuntimeException
at threadException.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:13)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
# 7. 线程的弊端
# 7.1 线程安全问题
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在掉方法进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象是线程安全的。
# 7.1.1 临界资源
a++
# 7.1.2 死锁
多个线程对同一临界资源的争夺有可能造成死锁而导致整个系统终止。
# 7.1.3 对象发布和初始化
# 对象溢出
方法返回一个 private 对象(private 的本意是不让外部访问);
解决:返回一个副本,而不是返回对象本身。
还未完全初始化就把对象提供给外界,如:
- 在构造函数中未初始化完毕就 this 赋值;
- 隐式溢出 —— 注册监听事件;
- 构造函数中运行线程;
解决:利用工厂模式。
# 7.2 性能问题
# 7.2.1 调度:上下文切换
- 挂起当前线程
- 保存现场
- 切换线程
- 回到现场
- 切换回当前线程
- 保存现场消耗 CPU 时间片
- 缓存开销:缓存失效
- 抢锁、IO 会导致密集的上下文切换
# 7.2.2 协作:内存同步
# 8. Java 内存模型
# 8.1 JVM 内存结构
# 程序计数器
- 每个线程都拥有一个 PC 寄存器,是线程私有的。用来存储指向下一条指令的地址。
- 在创建线程的时候,同时会创建相应的 PC 寄存器。
- 执行本地方法(JNI)时,PC 寄存器的值为 undefined。
- PC 寄存器是一块较小的内存空间,是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的内存区域。
# 虚拟机栈
- 栈由一系列帧(Frame)组成,是线程私有的。
- 帧用来保存一个方法的局部变量、操作数栈(Java 没有寄存器,所有参数传递使用操作数栈)、常量池指针、动态链接、方法返回值等。
- 每一次方法调用创建一个帧,并压栈。退出方法的时候,修改栈顶指针就可以把栈帧中的内容销毁。
- 局部变量表存放了编译期可知的各种基本数据类型和引用类型,每个 slot 存放 32 位数据,long、double 占 2 个槽位。
- 栈的优点:存取速度比堆快,仅次于寄存器。
- 栈的缺点:存在栈中的数据大小、生存期是在编译期决定的,缺乏灵活性。
# 堆
- 用来存放应用系统创建的对象和数组,所有线程共享 Java 堆。
- GC 主要就管理堆空间,对分代 CG 来说,堆也是分代的。
- 堆的优点:运行期动态分配内存大小,自动进行垃圾回收。
- 堆的缺点:效率相对较慢。
# 方法区
- 方法区是线程共享的,通常用来保存装载的类的结构信息。
- 常量池
- 字段、方法
- 特殊方法
- 永久引用(static修饰的)
- 通常和元空间关联在一起,但具体的跟 JVM 实现和版本有关。
- 🔺 String 在 JDK7 之前是放在方法区的,但是 JDK7 以后就移到堆了。
- JVM 规范把方法区描述为堆的一个逻辑部分,但它有一个别名称为 Non-heap(非堆),主要是为了与 Java 堆区分开。
# 运行时常量池
- 是 Class 文件中每个类或接口的常量池表,在运行期间的表示形式。通常包括:
- 类的版本
- 字段
- 方法
- 接口
- ...
- 在方法区中分配。
- 通常在加载类和接口到 JVM 后,就创建相应的运行时常量池。
# 本地方法栈
- 在 JVM 中用来支持 native 方法执行的栈就是本地方法栈。
# 8.2 Java 内存模型
# 8.2.1 重排序
在某一线程内部的两行代码的实际执行顺序和代码在 Java 文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。
好处:提高处理速度。
# 8.2.2 可见性
# 8.2.2.1 可见性演示
/**
* 描述: 演示可见性带来的问题
*/
public class FieldVisibility {
int a = 1;
int b = 2;
private void change() {
a = 3;
b = a;
}
private void print() {
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
while (true) {
FieldVisibility test = new FieldVisibility();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
3 个非常容易分析出来的情况:
- b=2;a=1;
- b=3;a=3;
- b=2;a=3;
但是还有1个比较隐秘的结果:
- b=3;a=1
# 为什么会出现这种情况?
原因是线程1和线程2看到的东西是不一样的,比如线程1执行了 a=3,线程1也执行了 b=a,但是线程2看到了真正的b是3,但是看到a只看到了自己那份a是1,所以就出现了b=3;a=1。
# 8.2.2.2 为什么会有可见性?
操作系统层
- 从内存到 CPU 是存在多层缓存的,每一片缓存都是数据的一份拷贝,所以不同缓存之间数据不一定的一致的。
JVM 层
- Java 作为高级语言,屏蔽了内存到 CPU 之间的多层缓存这些底层细节,用 JMM 定义了一套读写内存数据的规范。虽然我们不再需要关心一级缓存和二级缓存的问题,但是 JMM 抽象了主内存和本地内存的概念。
- 这里说的本地内存并不是真的是一块给每个线程分配的内存,而是 JMM 的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。
- 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内存是主内存中的拷贝。
- 线程不能直接读写主内存中的变量,而只能操作自己工作内存中的变量,然后再同步到主内存中。
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成,这个中转不是实时的,所以才存在了可见性的问题。
# 8.2.2.3 Happens-Before 原则
Happens-Before 原则是用来解决可见性问题的:在时间上,动作 A 发生在动作 B之 前,B 保证能看到 A,这就是 Happens-Before。
单线程原则
后一条语句一定能看到前一条语句执行的结果。
锁操作(synchronized 和 Lock)
volatile 变量
线程启动
子线程启动之后一定能看到主线程之前所做的所有事情。
线程 join()
主线程在子线程 join() 语句之后一定可以看到子线程执行的所有事情。
传递性
如果 hb(A,B) 且 hb(B,C),那么可以推出 hb(A,C)。
中断
一个线程被其他线程 interrupt 时,那么检查中断(isInterrupted)或者抛出 InterruptedException 一定能看到。
构造方法
对象构造方法的最后一行指令一定先与 finalize() 方法的第一行指令。
工具类:
- 线程安全的容器 get 一定能看到在此之前的 put 等存入操作;
- CountDownLatch
- Semaphore
- Future
- 线程池
- CyclicBarrier
volatile 关键字
# ① 是什么
- volatile 是一种同步机制,比 synchronized 和 Lock 相关类更轻量,因为适用 volatile 并不会发生上下文切换等开销很大的行为。
- 如果一个变量被修饰成 volatile,那么 JVM 就知道了这个变量可能会被并发修改。
- 但是开销小,相应能力也小。虽然说 volatile 是用来同步的保证线程安全的,但是 volatile 做不到 synchronized 那样的原子保护。
# ② 适用场景
不适用:a++
boolean flag,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,因为赋值自身是带有原子性的,而 volatile 又保证了可见性,所以就足以保证线程安全。
作为刷新之前变量的触发器。
int a = 0; volatile int b = 0; public void change(){ a = 3; b = a; //这个时候,因为 b 被 volatile 修饰,所以可以保证其他线程看到的 b=a,这个时候又同时保证了其他线程看到的 a 也是真实的 a,也就是 a =3。所以 volatile 不仅保证了 b 的可见性,也作为触发线刷新了 b=a 之前的所有语句。 }
# ③ 作用
- 可见性
- 禁止重排序
- 使得 long 和 double 的赋值是原子的
# ④ 与 synchronized 的关系
(1)使用上的区别
volatile 只能修饰变量,synchronized 只能修饰方法和语句块
(2)对原子性的保证
synchronized 可以保证原子性,volatile 不能保证原子性
(3)对可见性的保证
都可以保证可见性,但实现原理不同
volatile 对变量加了 lock,synchronized 使用了 monitorenter 和 monitorexit
(4)对有序性保证
volatile 能保证有序性,synchronized 可以保证有序性,但是代价(重量级)大,并发退化到串行
(5)其他
synchronized 引起阻塞,volatile 不会引起阻塞
# 8.2.3 原子性
一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
Java 中的原子操作:
- 除了 long 和 double 之外的基本类型的赋值操作。
- 所有引用 reference 的赋值操作,不管是 32 位还是 64 位的机器。
- java.concurrent.Atomic.* 包中所有类的原子操作
# long 和 double 的原子性问题
出于 Java 编程语言存储器模型的目的,对 long 和 double 值的单个写入被视为两个单独写入:每个 32 位写一次,总共写 2 次。这可能导致线程从一次写入看到 64 位值的前 32 位,而从另一次写入看到第二次 32 位的情况。
用 volatile 可以解决 long 和 double 的原子性问题。
在 32 位的 JVM 上,long 和 double 操作不是原子性的。
在 64 位的 JVM 上,long 和 double 操作是原子性的。
商用 JVM 中都已经解决了 long 和 double 的原子性问题,我们不需要专门去考虑。
# 8.3 Java 对象模型
# 8.3.1 对象组成成分
以 HotSpot 虚拟机为例,对象在内存中的布局分为以下 3 个部分:
- 对象头:
- Mark Word:存储对象自身的运行数据。如:Hashcode、GC 分代年龄、锁状态标志等。
- 类型指针:对象指向它的类元数据的指针。
- 实例数据:
- 真正存放对象实例数据的地方。
- 对齐填充:
- 这部分不一定存在,也没有什么特别含义,仅仅是占位符。因为 HotSpot 要求对象起始地址都是 8 字节的整数倍,如果不是,就对齐。
# 8.3.2 对象的访问定位
# ① 使用句柄
Java 堆中会划分一块内存来作为句柄池,reference 中存放句柄的地址,句柄中存储对象的实例数据和类元数据的地址。
- 间接指针,效率慢
- 修改对象引用的时候只需要修改到对象实例数据的指针即可。
# ② 使用指针
Java 堆中会存放访问类元数据的地址,reference 存储的就直接是对象的地址。
HotStop 用的是使用指针。
- reference 直接指向对象,效率快。
# 9. 线程的同步
# 9.1 线程安全问题
代码
package com.hedon.window; import sun.plugin2.os.windows.Windows; /** * @author Hedon Wang * @create 2020-09-21 15:51 */ class Window2 implements Runnable{ public int ticket = 100; @Override public void run() { while (true){ if (ticket > 0){ System.out.println(Thread.currentThread().getName()+":卖票,票号为 "+ ticket); //延迟执行过程,提高线程出现安全问题的概率 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } ticket -- ; }else{ break; } } } } public class WindowTest2 { public static void main(String[] args) { Window2 window2 = new Window2(); Thread thread = new Thread(window2); thread.setName("窗口1"); Thread thread1 = new Thread(window2); thread1.setName("窗口2"); Thread thread2 = new Thread(window2); thread2.setName("窗口3"); thread.start(); thread1.start(); thread2.start(); } }
问题
# 问题描述 上述代码模仿了一个窗口卖票的过程,三个线程共用同一个 windows 对象的 ticket 变量,按道理是不会卖出同一张票的,但是实际结果如上图,卖出了 3 张100号的票,这是不应该出现的状况。 事实上,还有一定概率卖出票号为0或者-1的票,这也是不对的。
# 9.2 示意图
# 9.3 原因分析
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
# 9.4 解决措施
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,不管有没有阻塞,其他线程都不可以参与执行。
# 9.4.1 法1:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
- 需要被同步的代码:操作共享数据的代码
- 共享数据:多个线程共同操作的变量,如上述代码的 ticket
- 同步监视器:俗称,锁。任何一个类的对象,都可以充当锁,==但是所有线程要共用同一个锁==。
修改上述代码中的 Window2 类,加入同步代码块,如下:
class Window2 implements Runnable{
public int ticket = 100;
//同步监视器
Object object = new Object();
@Override
public void run() {
while (true){
synchronized(object){
//需要被同步的代码
if (ticket > 0){
System.out.println(Thread.currentThread().getName()+":卖票,票号为 "+ ticket);
//延迟执行过程,提高线程出现安全问题的概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket -- ;
}else{
break;
}
}
}
}
}
再次运行测试方法,会发现线程安全问题已经得到解决了。
△ 注意点:
每一个线程都要共用同一个锁,比如如下代码:
class Window extends Thread{
//得声明成静态的
private static int ticket = 100;
//同步监视器
Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {
//需要被同步的代码
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
Window window = new Window();
Window window1 = new Window();
Window window2 = new Window();
window.setName("柜台1");
window1.setName("柜台2");
window2.setName("柜台3");
window.start();
window1.start();
window2.start();
}
}
上述代码我们在测试的时候,其实是 new 了 3 个 Window 对象的,所以是有 3 个不同的 object 对象的,这样锁是起不到作用的。
这时候改成 private static Object object = new Object(),就可以起到作用了!
# 好处
解决了线程的安全问题。
# 缺点
- 操作同步代码时,只能有一个线程参与,其他线程等待,这相当于是一个单线程的过程了,效率会比较低下;
- 死锁
# 重点重点重点
所有线程要共用同一个锁
所有线程要共用同一个锁
所有线程要共用同一个锁
# 9.4.2 法2:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步的。
# 9.4.2.1 Runnable 版本
把售票的过程提取出来作为一个方法 show(),加上 synchronized 表明这是一个同步方法,这样线程安全问题也得到了解决。
class Window3 implements Runnable{
public int ticket = 100;
//同步监视器
Object object = new Object();
@Override
public void run() {
while (true){
show();
}
}
//把售票的过程提取出来作为一个方法,加上 synchronized 表明这是一个同步方法
public synchronized void show(){
//需要被同步的代码
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为 " + ticket);
//延迟执行过程,提高线程出现安全问题的概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}
}
}
# 5.4.2.2 Thread 版本
class Window4 extends Thread{
//得声明成静态的
private static int ticket = 100;
//同步监视器
private static Object object = new Object();
@Override
public void run() {
while (true) {
show();
}
}
//必须声明为静态的
public static synchronized void show(){
//需要被同步的代码
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
# 总结
# 同步监视器
同步方法仍然涉及到同步监视器的问题,只是不需要我们显示的声明:
- 非静态的同步方法,同步监视器是 this
- 静态的同步方法,同步监视器是当前类本身,如 Windows4.class
# 9.4.3 法3:Lock 锁
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
# 9.4.3.1 Lock 接口
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
# 9.4.3.2 ReentrantLock 类
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和 内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
# 9.4.3.3 代码
- 声明 ReentrantLock 对象
- 上锁 => lock()
- 解锁 => unlock()
class Window5 implements Runnable{
public int ticket = 100;
/*
fair 参数:
- true:公平的,符合先来后到
- false:谁抢得到就是谁的
*/
//1. 声明 ReentrantLock 对象
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try{
//2. 上锁 => lock()
lock.lock();
//需要被同步的代码块
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + ":卖票,票号为 " + ticket);
//延迟执行过程,提高线程出现安全问题的概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
}else{
break;
}
}finally {
//3. 解锁 => unlock()
lock.unlock();
}
}
}
}
# 9.4.3.4 面试题:synchronized 与 Lock 的异同
# 相同
二者都可以解决线程的安全问题;
----------------------------------------------------------------------------------------------------------
# 不同
## ① 用法
在需要同步的对象中加入synchronized控制,synchronized既可以加在方法上,也可以加在特定代码中,括号中表示需要锁的对象。而Lock需要显示的指定起始位置和终点位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现的,它有比synchronized更精确的线程定义。
## ② 性能
在JDK 5中增加的ReentrantLock。它不仅拥有和synchronized相同的并发性和内存语义,还增加了锁投票,定时锁,等候和中断锁等。它们的性能在不同情况下会不同:在资源竞争不是很激励的情况下,synchronized的性能要优于ReentrantLock,带在资源紧张很激烈的情况下,synchronized的性能会下降的很快,而ReentrantLock的性能基本保持不变。
## ③ 锁机制
锁机制不一样。synchronized获得锁和释放锁的机制都在代码块结构中,当获得锁时,必须以相反的机制去释放,并且自动解锁,不会因为异常导致没有被释放而导致死锁。而Lock需要开发人员手动去释放,并且写在finally代码块中,否则会可能引起死锁问题的发生。
此外,Lock还提供的更强大的功能,可以通过tryLock的方式采用非阻塞的方式取获得锁。
而且,Lock还提供公平锁,即锁的获得按照线程先来后到的顺序依次获得,不会产生饥饿现象。默认是非公平锁,可通过传入构造方法的参数实现公平锁。synchronized提供的是非公平锁。
----------------------------------------------------------------------------------------------------------
# 用哪个
Lock -> 同步代码块 -> 同步方法
# 10. 单例模式
单例的好处:
- 节省内存和计算
- 保证结果正确
- 方便管理
适用场景:
- 无状态的工具类
- 全局信息类
# 10.1 饿汉式 - 静态常量[可用]
- 简单
- 类装载时便完成了实例化,类加载由 JVM 保证线程安全,所以没有线程安全问题。
- 效率低,可能不需要,但是还是加载了。
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1(){
}
public static Singleton1 getInstance(){
return INSTANCE;
}
}
# 10.2 饿汉式 - 静态代码块[可用]
- 优缺点同静态变量
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2(){
}
public static Singleton2 getInstance(){
return INSTANCE;
}
}
# 10.3 懒汉式 - 无处理[不可用]
- 线程不安全。
public class Singleton3 {
private static Singleton3 INSTANCE;
private Singleton3(){
}
public static Singleton3 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton3();
}
return INSTANCE;
}
}
# 10.4 懒汉式 - 同步方法[不推荐用]
- 线程安全。
- synchronized 加锁,效率低。
public class Singleton4 {
private static Singleton4 INSTANCE;
private Singleton4(){
}
public synchronized static Singleton4 getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton4();
}
return INSTANCE;
}
}
# 10.5 懒汉式 - 同步代码块[不可用]
- 线程不安全。
public class Singleton5 {
private static Singleton5 INSTANCE;
private Singleton5(){
}
public static Singleton5 getInstance(){
if (INSTANCE == null){
synchronized (Singleton5.class){
INSTANCE = new Singleton5();
}
}
return INSTANCE;
}
}
# 10.6 懒汉式 - 双重检查[推荐用]
- 懒汉式,延迟加载,效率高。
- 线程安全。
- volatile 禁用了重排序,保证了可见性。
public class Singleton6 {
private volatile static Singleton6 INSTANCE;
private Singleton6(){
}
public static Singleton6 getInstance(){
if (INSTANCE == null){
synchronized (Singleton6.class){
if (INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
思考:
# 为什么要 volatile?
1. 新建对象不是原子操作,新建对象实际上有 3 个操作
① 新建空的对象
② 执行构造方法
③ 赋值给 INSTANCE
2. CPU 和 JVM 具有重排序功能,这 3 行有可能被 CPU 或者 JVM 进行重排序。有可能在第一个线程构造方法还没执行的时候已经将对象赋值给 INSTANCE 了,那第二个对象判断 INSTANCE != NULL,然后就直接拿去用了。用的时候发现里面的属性其实是 NULL 的,还不可用,这就造成了线程不安全。
3. 有 volatile,对象一实例好,就会被 flush 到主内存中,所有线程可见。
# 10.7 懒汉式 - 静态内部类[可用]
- SingletonInstance 是内部类,JVM 在加载类 Singleton7 的时候是不会将 SingletonInstance.INSTANCE 进行实例化的,只有当调用 SingletonInstance.INSTANCE 才会去实例化。
public class Singleton7 {
private Singleton7(){
}
private static class SingletonInstance{
private static Singleton7 INSTANCE = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonInstance.INSTANCE;
}
}
# 10.8 懒汉式 - 枚举[最佳]
- 写法简单
- 线程安全有保证(JVM 会保证)
- 懒加载
- 避免反序列化破坏单例
public enum Singleton8 {
INSTANCE;
//随便做点事
public void whatever(){
}
}
# 11. 线程的死锁
# 11.1 现象
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
# 11.2 演示死锁
/**
* Thread-0 有 o1 想要 o2,Thread-1 有 o2 想要 o1,互相等待,成死锁。
* @author Hedon Wang
* @create 2021-03-16 11:20 AM
*/
public class MustDeadLock {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new DeadLockModel(1));
Thread t2 = new Thread(new DeadLockModel(2));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("两个线程都已经运行完毕~");
}
}
class DeadLockModel implements Runnable{
//两把锁
static Object o1 = new Object();
static Object o2 = new Object();
//标志,一个线程运行一种情况
int flag;
public DeadLockModel(int flag){
this.flag = flag;
}
@Override
public void run() {
if (flag == 1){
//申请第一把锁
synchronized (o1){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o1 锁");
//故意等待5s,让第二个线程拿到o2锁
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//想要第二把锁
synchronized (o2){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o2 锁");
}
}
System.out.println("线程" + Thread.currentThread().getName() + "已经运行完毕");
}else{
//申请第二把锁
synchronized (o2){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o2 锁");
//故意等待2s,让第一个线程拿到o1锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o1 锁");
}
}
System.out.println("线程" + Thread.currentThread().getName() + "已经运行完毕");
}
}
}
结果:
# 11.3 必要条件
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
# 11.4 如何定位死锁
# [法1-jstack]
jps:找到在运行的程序的 PID
jstack:打印出线程堆栈信息
# [法2-ThreadMXBean]
package deadlock;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
/**
* @author Hedon Wang
* @create 2021-03-16 11:20 AM
*/
public class MustDeadLock {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new DeadLockModel(1));
Thread t2 = new Thread(new DeadLockModel(2));
t1.start();
t2.start();
Thread.sleep(6000);
//ThreadMXBean帮助找到死锁
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
//发现死锁
if (deadlockedThreads != null && deadlockedThreads.length > 0){
for (int i = 0; i < deadlockedThreads.length; i++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);
System.out.print("发现死锁了 ---> ");
System.out.println(threadInfo.getThreadName());
}
}
System.out.println("两个线程都已经运行完毕~");
}
}
class DeadLockModel implements Runnable{
//两把锁
static Object o1 = new Object();
static Object o2 = new Object();
//标志,一个线程运行一种情况
int flag;
public DeadLockModel(int flag){
this.flag = flag;
}
@Override
public void run() {
if (flag == 1){
//申请第一把锁
synchronized (o1){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o1 锁");
//故意等待5s钟,让第二个线程拿到o2锁
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//想要第二把锁
synchronized (o2){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o2 锁");
}
}
System.out.println("线程" + Thread.currentThread().getName() + "已经运行完毕");
}else{
//申请第二把锁
synchronized (o2){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o2 锁");
//故意等待2s钟,让第一个线程拿到o1锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1){
System.out.println("线程" + Thread.currentThread().getName() + "已经拿到 o1 锁");
}
}
}
}
}
结果:
# 11.5 解决措施
# 线上发生死锁怎么办?
1. 线程问题都需要防患于未然,不造成损失地扑灭几乎是不可能的;
2. 保存案发现场然后立刻重启服务器;
3. 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版。
# ① 避免策略
- 哲学家就餐的换手方案、转账换序方案。
# ② 检测与恢复策略
一段时间检测是否有死锁,如果有就剥夺某一个资源来打开死锁。
# 死锁检查算法 1. 每次调用锁都有记录,用一个图来记录; 2. 定期检查“锁的调用链路图”中是否存在环路; 3. 存在环路的话:①逐个终止线程,直到死锁消除;② 资源抢占:让部分线程回退几步(可能会造成饥饿)。
# ③ 鸵鸟策略
- 如果我们发送死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复。
# 11.6 修复死锁
# ① 换序解决转账问题
规定顺序,一定先拿到 hashCode 小的那个对象的锁,然后再去拿大的锁。这样就不会造成我拿着自己的锁在等你的锁,而你也拿着自己的锁在等我的锁这样的死锁情况。
package deadlock;
/**
* 修复死锁:换序解决转账问题
*
* @author Hedon Wang
* @create 2021-03-16 2:37 PM
*/
public class SolveDeadLockByChangeOrder implements Runnable{
int flag = 1;
static Account a = new Account(500);
static Account b = new Account(500);
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
SolveDeadLockByChangeOrder s1 = new SolveDeadLockByChangeOrder();
SolveDeadLockByChangeOrder s2 = new SolveDeadLockByChangeOrder();
s1.flag = 1;
s2.flag = 0;
Thread t1 = new Thread(s1);
Thread t2 = new Thread(s2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
@Override
public void run() {
if (flag == 1){
transfer(a, b, 200);
}
if (flag == 0){
transfer(b, a, 200);
}
}
/**
* 转账
*
* @param from 从哪来
* @param to 转哪去
* @param amount 转多少
*/
public void transfer(Account from, Account to, int amount){
class Helper{
public void transfer(){
if (from.balance - amount >= 0){
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账 " + amount + "元。");
}else{
System.out.println("余额不足,转账失败。");
}
}
}
//根据 hash 值来进行排序,小的排前面
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash){
synchronized (from){
synchronized (to){
new Helper().transfer();
}
}
}else if(fromHash > toHash){
synchronized (to){
synchronized (from){
new Helper().transfer();
}
}
}else{
//相等的话,引入第三个锁,让他们竞争一下
synchronized (lock){
synchronized (to){
synchronized (from){
new Helper().transfer();
}
}
}
}
}
/**
* 账户类
*/
static class Account{
int balance; //余额
public Account(int balance){
this.balance = balance;
}
}
}
# ② 哲学家换手解决死锁
演示死锁:五位哲学家都拿起了左边的筷子,都在等右边的筷子,形成闭环,这就导致了死锁。
/**
* 哲学家问题导致的死锁
*
* @author Hedon Wang
* @create 2021-03-16 2:52 PM
*/
public class DiningPhilosophers {
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
//初始化筷子
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
//初始化哲学家
for (int i = 0; i < philosophers.length; i++) {
//左筷子
Object leftChopstick = chopsticks[i % chopsticks.length];
//右筷子
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
philosophers[i] = new Philosopher(leftChopstick,rightChopstick);
//启动
Thread thread = new Thread(philosophers[i],"" +(i+1));
thread.start();
}
}
/**
* 哲学家实体类
*/
public static class Philosopher implements Runnable{
private Object leftChopstick; //左边的筷子
private Object rightChopstick; //右边的筷子
public Philosopher(Object leftChopstick, Object rightChopstick){
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
while (true){
//思考
thinking();
//吃饭
eat();
}
}
/**
* 吃饭
*/
private void eat() {
//拿起左边筷子
synchronized (leftChopstick){
System.out.println("哲学家 " + Thread.currentThread().getName() + " 拿起左边筷子");
try {
Thread.sleep(new Random().nextInt(4000) + 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//拿起右边筷子
synchronized (rightChopstick){
System.out.println("哲学家 " + Thread.currentThread().getName() + " 拿起右边筷子");
//吃饭
System.out.println("哲学家 " + Thread.currentThread().getName() + " 正在吃饭");
try {
Thread.sleep(new Random().nextInt(1000) + 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("哲学家" + Thread.currentThread().getName() +" 放下右边筷子");
}
System.out.println("哲学家" + Thread.currentThread().getName() +" 放下左边筷子");
}
}
/**
* 思考
*/
public void thinking(){
try {
System.out.println("哲学家 " + Thread.currentThread().getName() + " 正在思考");
Thread.sleep(new Random().nextInt(1000) + 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果:
解决:其中一位哲学家换手,也就是先拿右边的,再拿左边的,这样就永远不可能出现循环等待条件
。
/**
* 哲学家问题解决
*
* @author Hedon Wang
* @create 2021-03-16 2:52 PM
*/
public class DiningPhilosophers {
public static void main(String[] args) {
Philosopher[] philosophers = new Philosopher[5];
Object[] chopsticks = new Object[philosophers.length];
//初始化筷子
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
//初始化哲学家
for (int i = 0; i < philosophers.length; i++) {
//左筷子
Object leftChopstick = chopsticks[i % chopsticks.length];
//右筷子
Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];
//最后一位哲学家换手
if ( i == philosophers.length -1 ){
philosophers[i] = new Philosopher(rightChopstick,leftChopstick);
}else{
philosophers[i] = new Philosopher(leftChopstick,rightChopstick);
}
//启动
Thread thread = new Thread(philosophers[i],"" +(i+1));
thread.start();
}
}
/**
* 哲学家实体类
*/
public static class Philosopher implements Runnable{
private Object leftChopstick; //左边的筷子
private Object rightChopstick; //右边的筷子
public Philosopher(Object leftChopstick, Object rightChopstick){
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
while (true){
//思考
thinking();
//吃饭
eat();
}
}
/**
* 吃饭
*/
private void eat() {
//拿起左边筷子
synchronized (leftChopstick){
System.out.println("哲学家 " + Thread.currentThread().getName() + " 拿起左边筷子");
try {
Thread.sleep(new Random().nextInt(4000) + 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
//拿起右边筷子
synchronized (rightChopstick){
System.out.println("哲学家 " + Thread.currentThread().getName() + " 拿起右边筷子");
//吃饭
System.out.println("哲学家 " + Thread.currentThread().getName() + " 正在吃饭");
try {
Thread.sleep(new Random().nextInt(1000) + 200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("哲学家" + Thread.currentThread().getName() +" 放下右边筷子");
}
System.out.println("哲学家" + Thread.currentThread().getName() +" 放下左边筷子");
}
}
/**
* 思考
*/
public void thinking(){
try {
System.out.println("哲学家 " + Thread.currentThread().getName() + " 正在思考");
Thread.sleep(new Random().nextInt(1000) + 100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
其他思路:
- 服务员检查(避免策略):拿筷子之前询问一下服务员,服务员决定是否给筷子。
- 餐票(避免策略):吃饭之前必须拿到餐票才能车,n 个人只有 n-1 张餐票,保证一定会有人能拿到 2 支筷子。
- 领导调节(检测与恢复策略):领导定期巡视,如果出现死锁,命令一个哲学家放下筷子。
# 11.7 避免死锁
- 设置超时时间(ReentrantLock)
- 使用并发类而不是自己设计锁
# 12. 线程的活锁
虽然线程并没有阻塞,也始终在运行,但是程序却得不到进展,因为线程始终重复做相同的事情。
在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。—— 缺乏共享资源
程序一直在运行,但是做的事情没有意义。
# 12.1 活锁演示
/**
* 活锁问题
*
* @author Hedon Wang
* @create 2021-03-16 3:35 PM
*/
public class LiveLock {
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("织女");
husband.isHungry = true;
wife.isHungry = true;
Spoon spoon = new Spoon();
spoon.setDiner(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.eatWith(spoon,wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon,husband);
}
}).start();
}
/**
* 勺子
*/
static class Spoon{
private Diner diner;
public Diner getDiner() {
return diner;
}
public void setDiner(Diner diner) {
this.diner = diner;
}
public synchronized void use(){
System.out.printf("%s has eaten!",diner.name);
}
}
/**
* 吃饭者
*/
static class Diner{
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
}
/**
* @param spoon 餐具
* @param diner 一起就餐的人
*/
public void eatWith(Spoon spoon, Diner diner){
while (isHungry){
if (spoon.diner != this){
try {
//等待对方吃完
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}else{
//对方饿了就把餐具让给对方
if (diner.isHungry){
System.out.println(this.name + "让出了勺子");
spoon.diner = diner;
continue;
}
}
//对方不饿的话就自己吃
spoon.use();
//吃完了
this.isHungry = false;
System.out.println(this.name + "吃完了");
spoon.setDiner(diner);
}
}
}
}
结果:互相谦让,一直在让,从未在吃。
# 12.2 解决措施
- 以太网的指数退避算法
- 引入随机因素(就不是始终都在谦让)
public class LiveLock {
public static void main(String[] args) {
Diner husband = new Diner("牛郎");
Diner wife = new Diner("织女");
husband.isHungry = true;
wife.isHungry = true;
Spoon spoon = new Spoon();
spoon.setDiner(husband);
new Thread(new Runnable() {
@Override
public void run() {
husband.eatWith(spoon,wife);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
wife.eatWith(spoon,husband);
}
}).start();
}
/**
* 勺子
*/
static class Spoon{
private Diner diner;
public Diner getDiner() {
return diner;
}
public void setDiner(Diner diner) {
this.diner = diner;
}
public synchronized void use(){
System.out.printf("%s has eaten!",diner.name);
}
}
/**
* 吃饭者
*/
static class Diner{
private String name;
private boolean isHungry;
public Diner(String name) {
this.name = name;
}
/**
* @param spoon 餐具
* @param diner 一起就餐的人
*/
public void eatWith(Spoon spoon, Diner diner){
while (isHungry){
if (spoon.diner != this){
try {
//等待对方吃完
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}else{
//对方饿了就把餐具让给对方
if (diner.isHungry){
//引入随机因素,一半的几率会让出勺子,一半的几率不让
if (new Random().nextBoolean()){
System.out.println(this.name + "让出了勺子");
spoon.diner = diner;
continue;
}
System.out.println(this.name + "决定不让出勺子=================>>>>");
}
}
//对方不饿的话就自己吃
spoon.use();
//吃完了
this.isHungry = false;
System.out.println(this.name + "吃完了");
spoon.setDiner(diner);
}
}
}
}
结果:
# 13. 线程的饥饿
当线程需要某些资源(例如 CPU),但是始终得不到。
别人要插队,一直谦让,就会源源不断有人来插队,自己一直饿着。
# 14. 线程的通信
# 14.1 Object 类的三个重要方法
# 14.1.1 wait()
- 在当前线程中调用方法: 对象名.wait();
- wait() 方法使当前线程进入等待(某对象)状态 ,直到:
- 另一线程对该对象发出 notify(或 notifyAll ))
- 过了 wait(long timeout) 规定的超时时间,如果传入 0 就是永久等待
- 线程自身调用了 interrupt(),会报 InterruptedException 异常,然后释放 monitor
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁) ;
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待;
- 在当前线程被 notify 后,要重新获得监控权,然后从断点处继续代码的执行。
# 14.1.2 notify()
- 唤醒正在排队等待同步资源的线程中优先级最高者结束等待。
# 14.1.3 notifyAll()
- 唤醒正在排队等待资源的所有线程结束等待。
# 注意点
① 这三个方法只有在 synchronized 方法或 synchronized 代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException 异常。
② 因为这三个方法必须由锁对象调用,而任意对象都可以作为 synchronized 的同步锁,因此这三个方法只能在 Object 类中声明。
③ 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会报 IllegalMonitorStateException 异常。
# sleep() 和 wait() 的异同
同:
- ① 一旦执行方法,都可以使得当前的线程进入阻塞状态;
异:
- ① 两个方法声明的位置不同:Thread 类中声明 sleep(),Object 类中声明 wait()
- ② 调用的要求不同:sleep() 可以在任何需要的场景下调用,wait() 必须是在用同步代码中
- ③ sleep() 不会释放同步监视器, wait() 会释放同步监视器,而且会加入到等待队列中
- ④ sleep() 不需要被唤醒(休眠之后推出阻塞),但是 wait() 需要(不指定时间需要被别人中断)
# 14.2 生产者/消费者问题
# 14.2.1 题目
生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
- 生产者比消费者快时,消费者会漏掉一些数据没有取到;
- 消费者比生产者快时,消费者会取相同的数据。
# 14.2.2 法一:synchronized 搭配 wait/nofity
//产品
class Product{
//产品个数
private Integer count;
public Product(){
this.count = 0;
}
//生产
public synchronized void add(){
//超过20个,先等等
if (this.count >= 20){
try {
System.out.println("满20个了!消费者快来!================");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
this.count++;
System.out.println("生产者生产了第 "+count+" 个产品。");
//现在肯定有产品了,赶紧通知人来消费
notifyAll();
}
}
//消费
public synchronized void reduce(){
//没产品了,咱就先候着
if (this.count <= 0){
try {
System.out.println("*******************没货啦!生产者在哪里!");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
System.out.println("消费者消费了第:"+count+" 个产品。");
this.count--;
//消费了1个,现在肯定还有位置可以放,通知生产者可以继续生产了
notifyAll();
}
}
}
//生产者
class Producer extends Thread {
private Product product;
public Producer(Product product){
this.product = product;
}
@Override
public void run() {
Random random = new Random();
while (true){
try {
//随机生产速度
sleep(random.nextInt(100)+100);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.add();
}
}
}
//消费者
class Consumer extends Thread{
private Product product;
public Consumer(Product product){
this.product = product;
}
@Override
public void run() {
Random random = new Random();
while (true){
try {
//随机消费速度
sleep(random.nextInt(100)+100);
} catch (InterruptedException e) {
e.printStackTrace();
}
product.reduce();
}
}
}
public class ProductTest {
public static void main(String[] args) {
Product product = new Product();
Producer producer = new Producer(product);
Consumer consumer = new Consumer(product);
producer.setName("生产者");
consumer.setName("消费者");
producer.start();
consumer.start();
}
}
# 14.2.3 法二:BlockingQueue
# 15. 面试问题
# 1. start() 和 run() 的区别?
- 调用 run() 的话就只是调用普通调用;
- 调用 start() 的话,会由 JVM 去调用 run() 方法,JVM 在调用 run() 的时候就去创建一个新的线程,让它来调用 run() 方法;
- start() 做三件事:① 启动新线程检查线程状态;② 加入线程组;③ 调用 start0()
# 2. 一个线程两次调用 start() 方法会出现什么情况?为什么?
会报 IllegalMonitorStateException 异常。
因为调用 start() 方法会对当前线程的状态进行检查,一个线程的状态先从 new,再到 runnable,最后到 terminated。一个线程调用了 start() 方法后就进入这些状态中的一个了,所以检查不通过,就报错了。
# 3. 既然 start() 方法会调用 run() 方法,为什么不直接调用 run() 呢?
因为调用 start() 方法开始真正意义上创建一个新的线程,run() 只是普通调用。
# 4. Thread.sleep(1000), 1000ms后是否立即执行?
不一定,在未来的 1000 毫秒内,线程不想再参与到 CPU 竞争。那么 1000 毫秒过去之后,这时候也许另外一个 线程正在使用C PU,那么这时候操作系统是不会重新分配 CPU 的,直到那个线程挂起或结束; 况且,即使这个时候恰巧轮到操作系统进行CPU分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。
# 5. Thread.sleep(0) 是否有用?
Thread. Sleep(0) 的作用,就是触发操作系统立刻重新进行一次 CPU 竞争,重新计算优先级。竞争的结果也许 是当前线程仍然获得 CPU 控制权,也许会换成别的线程获得 CPU 控制权。这也是我们在大循环里面经常会写一 句 Thread.sleep(0),因为这样就给了其他线程比如 Paint 线程获得 CPU 控制权的权力,这样界面就不会假死在 那里。
# 6. 线程交替打印
/**
* 四个线程(ABCD),写一个程序,按照以下要求输出字符串,4个线程总共输出1000次即可
* <p>
* 线程A=1
* 线程B=2
* 线程C=3
* 线程D=4
* 线程A=1
* 线程B=2
* 线程C=3
* 线程D=4
* (以下重复)
*
*/
public class FourThreadPrintThousand {
public static void main(String[] args) throws InterruptedException {
//声明4把锁
Object a = new Object();
Object b = new Object();
Object c = new Object();
Object d = new Object();
ThreadPrinter threadA = new ThreadPrinter("A", 1, d, a);
ThreadPrinter threadB = new ThreadPrinter("B", 2, a, b);
ThreadPrinter threadC = new ThreadPrinter("C", 3, b, c);
ThreadPrinter threadD = new ThreadPrinter("D", 4, c, d);
new Thread(threadA).start();
Thread.sleep(10); //确保 ABCD 顺序运行
new Thread(threadB).start();
Thread.sleep(10);
new Thread(threadC).start();
Thread.sleep(10);
new Thread(threadD).start();
Thread.sleep(10);
}
}
class ThreadPrinter implements Runnable {
private String threadName; //线程名称
private int number; //线程要打印的数字
private Object preLock; //前一个线程的锁
private Object selfLock; //当前线程的锁
public ThreadPrinter(String threadName, int number, Object preLock, Object selfLock) {
this.threadName = threadName;
this.number = number;
this.preLock = preLock;
this.selfLock = selfLock;
}
@Override
public void run() {
int count = 1000 / 4;
while (count > 0) {
//先获取 preLock,因为必须要前一个线程释放了其对应的对象锁,本线程才可以操作
synchronized (preLock) {
//然后再获取当前线程锁
synchronized (selfLock) {
//打印
System.out.println("线程" + this.threadName + "=" + this.number);
count--;
//当前线程操作完后,唤醒其他被当前线程锁住的线程
selfLock.notifyAll();
}
try {
//如果count为0,说明这是最后一次打印操作,通过 notifyAll 操作释放对象锁。
if (count == 0) {
preLock.notifyAll();
} else {
//释放上一个锁
preLock.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
# 9. 为什么 wait() 需要放在 synchronized 代码块中而 sleep() 不需要?
为了避免死锁。设计者的思路是先 wait() 在 notify()。
限制 wait() 只能在 synchronized 代码块中可以保证调用 wait() 的对象拥有本身这把锁,而不会出现还没 wait() 的时候其他地方就已经调用完 notify() 了,导致后面没人来 notify() 而一直处于等待状态。
# 10. 为什么 wait()、notify() 和 notifyAll() 放在 Object 类中而 sleep() 放在 Thread 中?
wait()、notify() 和 notifyAll() 是一个锁级别的操作,该锁是绑定到某一个对象中的,而不是绑定到线程中。
这样还有另外一个好处,一个线程可以拥有多把锁,更加灵活。
# 11. 守护线程和普通线程的区别
二者整体上没有什么区别。
主要是区别点是如果所有线程都是守护线程的话,那么所有守护线程会和 JVM 一起停止工作。
同时普通线程是执行我们逻辑的,而守护线程的服务于我们的普通线程的。
# 12. 我们是否需要给线程设置为守护线程?
不是需不需要,而是应不应该。
这里是不应该的,因为如果这个线程还有工作没有完成,而其他普通线程都已经完成了,那么所有守护线程和 JVM 会一起停止工作,这样就会导致有部分工作没有完成。
# 13. Java 异常体系图
# 14. 实际工作中,如何全局处理异常?为什么要全局处理?不处理行不行?
用一个 UncaughtExceptionHandler,然后在主线程中设置其为全局异常处理器。
# 15. 写一个必然死锁的例子,生产者什么场景下回发生死锁?
一个方法中占有多种锁,锁存在循环。
# 16. 死锁的四个必要条件
互斥条件
请求与保持条件
不剥夺条件
循环等待条件
# 17. 如何定位死锁?
jps + jstack
ThreadMXBean
# 18. 有哪些解决死锁问题的策略?
① 避免策略:哲学家就餐换手、转账换序方案
② 检测与恢复策略
③ 鸵鸟策略
# 19. 哲学家就餐问题
# 20. 实际工程中如何避免死锁?
① 设置超时时间
② 使用并发工具类
③ 降低锁的粒度
④ synchronized
⑤ 线程起名字,便于 debug
⑥ 避免锁的嵌套
⑦ 专锁专用
# 21. 什么是活跃性问题?活锁、饥饿和死锁有什么区别?
活锁:一直在运行,但是一直没进展
饥饿:一个人一直得不到 CPU 资源,一直在等待
死锁:一群人一直在互相等待,互不谦让
← Synchronized 一、线程池 →