欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
5. 核心5:Thread和Object类中线程相关方法(wait/notify、sleep、join、yield)
5.1 方法概览
类 | 方法名 | 简介 |
---|---|---|
Thread | sleep相关 | 本表格的“相关”,指的是重载方法,如sleep有多个重载方法,但实际作用大同小异 |
join | 主线程等待ThreaA执行完毕(ThreadA.join()) | |
yield相关 | 放弃已经获取到的CPU资源 | |
currentThread | 获取当前执行线程的引用 | |
start,run相关 | 启动线程相关 | |
interrupt相关 | 中断线程 | |
stop(),suspend(),resuem()相关 | 已废弃 | |
Object | wait/notify/notifyAll相关 | 让线程暂时休息和唤醒 |
5.2 wait,notify,notifyAll方法详解
5.2.1 用法:阻塞阶段、唤醒阶段、遇到中断
1. 阻塞阶段
线程调用wait()
方法,则该线程进入到阻塞状态,直到以下4种情况之一发生时,才会被唤醒:
- 另一个线程调用这个对象的
notify()
方法且刚好被唤醒的是本线程
- 另一个线程调用这个对象的
notifyAll()
方法且刚好被唤醒的是本线程 - 过了
wait(long timeout)
规定的超时时间,如果传入0就是永久等待
- 线程自身调用了
interrupt
2. 唤醒阶段
notify
会唤起单个
在等待某对象monitor的线程
,如果有多个线程在等待,则只会唤起其中随机的一个notifyAll
会将所有等待的线程都唤起
,而唤起后具体哪个线程会获得monitor
,则看操作系统的调度notify
必须在synchronized
中调用,否则会抛出异常java.lang.IllegalMonitorStateException at java.lang.Object.notify(Native Method) at BlockedWaitingTimedWaiting.run(BlockedWaitingTimedWaiting.java:37) at java.lang.Thread.run(Thread.java:748) 复制代码
3. 遇到中断
假设线程执行了wait()
,在此期间被中断,则会抛出interruptException
,同时释放已经获取到的monitor
。
5.2.2 代码演示:4种情况
1. 普通用法
/**
* 展示wait和notify的基本用法
* 1. 研究代码执行顺序
* 2. 证明wait释放锁
*/
public class Wait {
public static Object object = new Object();
static class Thread1 extends Thread {
@Override
public void run() {
synchronized (object) {
System.out.println(Thread.currentThread().getName() + "开始执行了");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "获取到了锁");
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
synchronized (object) {
object.notify();
System.out.println("线程" + Thread.currentThread().getName() + "调用了notify()");
}
}
}
public static void main(String[] args) {
Thread thread1 = new Thread1();
Thread thread2 = new Thread2();
thread1.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.start();
}
}
//输出结果
Thread-0开始执行了
线程Thread-1调用了notify()
线程Thread-0获取到了锁
复制代码
步骤解析:
- Thread-0进入Thread1类
synchronized代码块
,获得锁,输出“Thread-0开始执行” - 然后Thread-0执行
object.wait()
,释放了锁
- Thread-1获得锁,进入Thread2类synchronized,执行object.notify(),输出“线程Thread-1调用了notify()”,同时Thread-0也被唤醒了
- Thread-0回到object.wait()的位置,执行下面的代码逻辑,输出“线程Thread-0获取到了锁”
2. notify和notifyAll展示
/**
* 3个线程,线程1和线程2首先被阻塞,线程3唤醒它们。notify,notifyAll
* start先执行不代表线程先启动
*/
public class WaitNotifyAll implements Runnable{
private static final Object resourceA = new Object();
@Override
public void run() {
synchronized(resourceA) {
System.out.println(Thread.currentThread().getName() + " get resourceA lock");
try {
System.out.println(Thread.currentThread().getName() + " wait to start");
resourceA.wait();
System.out.println(Thread.currentThread().getName() + "'s waiting end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Runnable r = new WaitNotifyAll();
Thread threadA = new Thread(r);
Thread threadB = new Thread(r);
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
resourceA.notifyAll();
//resourceA.notify();
System.out.println("ThreadC notifyed.");
}
}
});
threadA.start();
threadB.start();
Thread.sleep(200);
threadC.start();
}
}
//输出结果
Thread-0 get resourceA lock
Thread-0 wait to start
Thread-1 get resourceA lock
Thread-1 wait to start
ThreadC notifyed.
Thread-1's waiting end
Thread-0's waiting end
复制代码
3. 只释放当前monitor展示
/**
* 证明wait只释放当前的那把锁
*/
public class WaitNotifyReleaseOwnMonitor {
private static volatile Object resourceA = new Object();
private static volatile Object resourceB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("ThreadA got resourceA lock.");
synchronized (resourceB) {
System.out.println("ThreadA got resourceB lock.");
try {
System.out.println("ThreadA releases resourceA lock.");
resourceA.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceA) {
System.out.println("ThreadB got resourceA lock.");
System.out.println("ThreadB tries to resourceB lock.");
synchronized (resourceB) {
System.out.println("ThreadB got resourceB lock.");
}
}
}
});
thread1.start();
thread2.start();
}
}
//输出结果
ThreadA got resourceA lock.
ThreadA got resourceB lock.
ThreadA releases resourceA lock.
ThreadB got resourceA lock.
ThreadB tries to resourceB lock.
复制代码
没有打印ThreadB got resourceB lock
.(因为只调用了A.wait(),只释放了lockA,B还没占用着
)
5.2.3 特点、性质
- 使用的时候必须先拥有
monitor
(synchronized锁
) notify
只能唤醒其中一个- 属于
Object
类 - 类似
Condition
- 同时持有多个锁的情况
5.2.4、原理
1. wait原理
- 入口集 Entry Set
- 等待集 Wait Set
- 特殊情况
- 如果发生异常,可以直接跳到终止
TERMINATED
状态,不必再遵循路径,比如可以从WAITING
直接到TERMINATED
。 - 从
Object.wait()
刚被唤醒时,通常不能立刻抢到monitor锁
,那就会从WAITING
先进入BLOCKED
状态,抢到锁后再转换到RUNNABLE状态
。
- 如果发生异常,可以直接跳到终止
2. 手写生产者消费者设计模式
- 什么是生产者消费者模式
/**
* 用wait/notify来实现
*/
public class ProducerConsumerModel {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
public synchronized void put() {
while (storage.size() == maxSize) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库中已经有" + storage.size() + "个产品。");
notify();
}
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
}
//输出结果
仓库中已经有1个产品。
仓库中已经有2个产品。
仓库中已经有3个产品。
仓库中已经有4个产品。
仓库中已经有5个产品。
仓库中已经有6个产品。
仓库中已经有7个产品。
仓库中已经有8个产品。
仓库中已经有9个产品。
仓库中已经有10个产品。
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下9
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下8
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下7
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下6
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下5
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下4
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下3
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下2
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下1
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下0
仓库中已经有1个产品。
拿到了Sun Apr 12 11:07:50 CST 2020,现在仓库还剩下0
复制代码
5.2.6 常见面试问题
1. 两个线程交替打印0~100的奇偶数
- 基本方式:用synchronized关键字实现
/**
* @Description: 两个线程交替打印0~100的奇偶数,用synchronized关键字实现
*/
public class WaitNotifyPrintOddEvenSyn {
public static int count = 0;
public static final Object lock = new Object();
//新建2个线程
//1个只处理偶数,第二个只处理奇数(用位运算)
//用synchronized来通信
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 0) {
System.out.println((Thread.currentThread().getName() + ":" + count++));
}
}
}
}
}, "偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count < 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
}
}
}
}
}, "奇数").start();
}
}
//输出结果
//输出正确,但是实际上如果thread1(偶数线程)一直支持lock,会有不断循环做无效的操作
偶数:0
奇数:1
偶数:2
奇数:3
...
奇数:99
偶数:100
复制代码
- 更好的方法:wait/notify
/**
* 两个线程交替打印0~100的奇偶数,用wait和notify
*/
public class WaitNotifyPrintOddEvenWait {
private static int count = 0;
private static Object lock = new Object();
//1. 拿到锁,我们就打印
//2. 打印完,唤醒其他线程,自己就休眠
static class TurningRunner implements Runnable {
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
//拿到锁就打印
System.out.println(Thread.currentThread().getName() + ":" + count++);
lock.notify();
if (count <= 100) {
try {
//如果任务还没结束,就让出当前线程,并休眠
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new TurningRunner(),"偶数").start();
Thread.sleep(100);
new Thread(new TurningRunner(),"奇数").start();
}
}
//输出结果
偶数:0
奇数:1
偶数:2
奇数:3
...
奇数:99
偶数:100
复制代码
2. 手写生产者消费者设计模式
3. 为什么wait()需要在同步代码块内使用,而sleep()不需要
我们反过来想,如果不要求wait()
必须在同步块里面
,而是可以在之外调用的话,那么就会有以下代 码:
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data){
buffer.add(data);
notify(); //Since someone may be waiting in take
}
public String take()throws InterruptedException {
while (buffer.isEmpty()){
wait();
}
return buffer.remove();
}
复制代码
那么可能发生如下的错误:
- 消费者线程调用
take()
并看到了buffer.isEmpty()
。 - 在消费者线程继续
wait()
之前,生产者线程调用一个完整的give()
,也就是buffer.add(data)
和notify()
- 消费者线程现在调用
wait()
,但是错过了刚才的notify()
。 - 如果运气不好,即使有可用的数据,但是没有更多生产者生产的话,那么消费者会陷入
wait的无限期等待
。
一旦你理解了这个问题,解决方案是显而易见的:synchronized用来确保notify永远不会在isEmpty和 wait之间被调用
。如下:
- 正常逻辑是
先执行wait
,后续在执行notify唤醒
。如果wait/notify不放同步代码块,执行wait的时候,线程切换去执行其他任务如notify,导致notify先于wait
,就会导致后续切回wait的时候,一直阻塞着,无法释放,导致死锁。 - 而sleep是针对本身的当前线程的,不影响
参考资料:programming.guide/java/why-wa…
4. 为什么线程通信的方法wait(),notify()和notifyAll被定义在Object类里?而sleep定义在Thread类里?
wait、notify、notifyAll是锁级别的操作,属于Object对象的,而线程实际上是可以持有多把锁的,如果把wait定义到Thread里面,就无法做到这么灵活的控制了
经典回答:www.java67.com/2012/09/top…
5. wait方法是属于Object对象的,那调用Thread.wait会怎么样?
- 这里就把Thread当成是一个普通的类,和Object没有区别。
- 但是这样会有一个问题,那就是线程退出的时候会自动notify(),这会让我们自己设计的唤醒流程受到极大的干扰,所以十分不推荐调用
Thread类的wait()
。
6. 如何选择notify还是notifyAll?
Object.notify()
可能导致信号丢失这样的正确性问题,而Object.notifyAll()虽然效率不太高
(把不需要唤醒的等待线程也给唤醒了),但是其在正确性方面有保障。因此实现通知的一种比较流行的保守性方法是优先使用Object.notifyAll()
以保障正确性,只有在有证据表明使用Object.notify()足够的情况下才使用Object.notif()。Object.notify()只有在下列条件全部满足的情况下才能够用于替代notifyAll方法。
- 条件1:
一次通知仅需要唤醒至多一个线程
。这一点容易理解,但是光满足这一点还不足以用Object.notify()去替代Object.notifyAll()。在不同的等待线程可能使用不同的保护条件的情况下,Object.notify()唤醒的一个任意线程可能并不是我们需要唤醒的那一个(种)线程。因此,这个问题还需要通过满足条件2来排除。 - 条件2:
相应对象的等待集中仅包含同质等待线程
。所谓同质等待线程指这些线程使用同一个保护条件,并且这些线程在Object.wait()调用返回之后的处理逻辑一致。最为典型的同质线程是使用同一个Runnablef接口实例创建的不同线程(实例)或者从同一个Thread子类的new出来的多个实例。
注意:
Object.notify()唤醒的是其所属对象上的一个任意等待线程。Object.notify()本身在唤醒线程时是不考虑 保护条件的。Object.notifyAll()方法唤醒的是其所属对象上的所有等待线程。使用Object.notify()替代 Object.notifyAll()时需要确保以下两个条件同时得以满足:
- 一次通知仅需要唤醒至多一个线程。
- 相应对象上的所有等待线程都是同质等待线程。
参考资料: www.jianshu.com/p/5834de089…
7. notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
实质就跟初始状态一样,多个线程抢夺锁,抢不到的线程就等待,等待上一个线程释放锁
8. 用suspend()和resume()来阻塞线程可以吗?为什么?
这2个方法由于不安全,已经被弃用了。功能类似wait和notify,但是不释放锁,并且容易引起死锁。
5.3 sleep方法详解
5.3.1 作用
我只想让线程在预期的时间执行,其他时候不要占用CPU资源
5.3.2 不释放锁
- 包括synchronized和lock
- 和wait不同
/**
* 展示线程sleep的时候不释放synchronized的monitor,等sleep时间到了以后,正常结束后才释放锁
*/
public class SleepDontReleaseMonitor implements Runnable{
public static void main(String[] args) {
SleepDontReleaseMonitor sleepDontReleaseMonitor = new SleepDontReleaseMonitor();
new Thread(sleepDontReleaseMonitor).start();
new Thread(sleepDontReleaseMonitor).start();
}
@Override
public void run() {
syn();
}
private synchronized void syn() {
System.out.println("线程" + Thread.currentThread().getName() + "获取到了monitor");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName() + "退出了同步代码块");
}
}
//输出结果
线程Thread-0获取到了monitor
线程Thread-0退出了同步代码块(5s后出现)
线程Thread-1获取到了monitor
线程Thread-1退出了同步代码块(5s后出现)
复制代码
/**
* 演示sleep不释放lock(lock需要手动释放)
*/
public class SleepDontReleaseLock implements Runnable {
private static final Lock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "获取到了lock");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println("线程" + Thread.currentThread().getName() + "释放了lock");
}
public static void main(String[] args) {
SleepDontReleaseLock sleepDontReleaseLock = new SleepDontReleaseLock();
new Thread(sleepDontReleaseLock).start();
new Thread(sleepDontReleaseLock).start();
}
}
//输出结果
线程Thread-0获取到了lock
线程Thread-0释放了lock(5s后)
线程Thread-1获取到了lock
线程Thread-1释放了lock(5s后)
复制代码
5.3.3 sleep方法响应中断
- 抛出InterruptedException
- 清除中断状态
/**
* 每隔1s输出当前时间,被中断,观察
* Thread.sleep()
* TimeUnit.SECONDS.sleep()
*/
public class SleepInterrupted implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(new Date());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
System.out.println("我被中断了");
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new SleepInterrupted());
thread.start();
Thread.sleep(6500);
thread.interrupt();
}
}
//输出结果
Wed Apr 15 23:09:55 CST 2020
Wed Apr 15 23:09:56 CST 2020
Wed Apr 15 23:09:57 CST 2020
Wed Apr 15 23:09:58 CST 2020
Wed Apr 15 23:09:59 CST 2020
Wed Apr 15 23:10:00 CST 2020
Wed Apr 15 23:10:01 CST 2020
我被中断了
java.lang.InterruptedException: sleep interrupted
Wed Apr 15 23:10:01 CST 2020
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at ConcurrenceFolder.mooc.threadConcurrencyCore.threadobjectclasscommonmethods.SleepInterrupted.run(SleepInterrupted.java:21)
at java.lang.Thread.run(Thread.java:748)
Wed Apr 15 23:10:02 CST 2020
Wed Apr 15 23:10:03 CST 2020
复制代码
5.3.4 sleep总结
- sleep方法可以
让线程进入Waiting状态,并且不占用CPU资源
- 但是
不释放锁
,直到规定时间后再执行 - 休眠期间如果
被中断,会抛出异常并清除中断状态
5.3.5 sleep常见面试问题
1. wait/notify、sleep异同(方法属于哪个对象?线程状态怎么切换?)
-
相同
- 都会阻塞,线程状态为Waiting 或Time_Waiting
- 都可以响应中断
- 外层执行thread.interrupt()
try { wait(); Thread.sleep(); } catch (InterruptedException e) { e.printStackTrace(); } 复制代码
-
不同
- wait/notify需要在synchronized方法中,而sleep不需要
- 释放锁:wait会释放锁,而sleep不释放锁
- 指定时间:sleep必须传参时间,而wait有多个构造方法,不传时间则直到自己被唤醒
- 所属类:wait/notify是Object方法,sleep是Thread类的方法
5.4 join方法
5.4.1 作用
因为新的线程加入了“我们”,所以“我们”要等他执行完再出发
5.4.2 用法
(在main方法中thread1.join)main等待thread1执行完毕,注意谁等谁(父等待子)
5.4.3 三个例子
- 普通用法
/**
* 演示join,注意语句输出顺序,会变化
*/
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
});
thread1.start();
thread2.start();
System.out.println("开始等待子线程运行完毕");
thread1.join();
thread2.join();
System.out.println("所有子线程执行完毕");
}
}
//输出结果
开始等待子线程运行完毕
Thread-0执行完毕
Thread-1执行完毕
所有子线程执行完毕
复制代码
- 遇到中断
/**
* 演示join期间被中断的效果
*/
public class JoinInterrupt {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
mainThread.interrupt();
Thread.sleep(5000);
System.out.println("Thread1 finished.");
} catch (InterruptedException e) {
System.out.println("子线程中断");
}
}
});
thread1.start();
System.out.println("等待子线程运行完毕");
try {
thread1.join();
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "主线程中断了");
thread1.interrupt();
}
System.out.println("子线程已运行完毕");
}
}
//输出结果
等待子线程运行完毕
main主线程中断了
子线程已运行完毕
子线程中断
复制代码
- 在join期间,线程到底是什么状态?:
Waiting
/**
* 先join再mainThread.getState()
* 通过debugger看线程join前后状态的对比
*/
public class JoinThreadState {
public static void main(String[] args) throws InterruptedException {
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println(mainThread.getState());
System.out.println("Thread-0运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("等待子线程运行完毕");
thread.join();
System.out.println("子线程运行完毕");
}
}
//输出结果
等待子线程运行完毕
WAITING
Thread-0运行结束
子线程运行完毕
复制代码
可以使用封装工具类:CountDownLatch
或CyclicBarrier
5.4.4 join原理
源码:
(1)thread.join();
(2)
public final void join() throws InterruptedException {
join(0);
}
(3)
public final synchronized void join(long millis)
throws InterruptedException {
...
if (millis == 0) {
while (isAlive()) {
wait(0);
}
}
复制代码
分析:线程在run执行完成后
,JVM底层会自动调用一个notifyAll唤醒
,所以即使在join()
内没有notify显示调用
,执行完run()后,也会唤醒
。等价于下面的代码:
// thread.join(); 等价于下面synchronized的代码
synchronized (thread) {
thread.wait();
}
复制代码
5.4.5 常见面试问题
在join期间,线程处于哪种线程状态?Waiting
5.5 yield方法
- 作用:
释放我的CPU时间片
。线程状态仍然是RUNNABLE
,不释放锁,也不阻塞
- 定位:JVM不保证遵循yield逻辑
- yield和sleep区别:sleep期间线程调度器不会去调度该线程,而yield方法时只是让线程释放出自己的CPU时间片,线程依然处于
就绪状态
,随时可能再次被调度。
5.6 获取当前执行线程的引用:Thread.currentThread()方法
同一个方法,不同线程会打印出各自线程的名称
/**
* 演示打印majn, Thread-0, Thread-1
*/
public class CurrentThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
public static void main(String[] args) {
new CurrentThread().run();
new Thread(new CurrentThread()).start();
new Thread(new CurrentThread()).start();
}
}
//输出
main
Thread-0
Thread-1
复制代码
6. 核心6:线程各属性
6.1 线程各属性纵览
属性名称 | 用户 |
---|---|
编号(ID) | 每个线程有自己的ID,用于标识不同的线程 |
名称(Name) | 作用让用户或程序员在开发、调试或运行过程中,更容易区分每个不同的线程、定位问题等 |
是否是守护线程(isDaemon) | true代表该线程是【守护线程】,false代表线程是非守护线程,也就是【用户线程】 |
优先级(Priority) | 优先级这个属性的目的是告诉线程调度器,用户希望哪些线程相对多运行、哪些少运行 |
6.2 线程ID
/**
* ID从1开始,JVM运行起来后,我们自己创建的线程的ID早已不是2
*/
public class Id {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println("主线程id:" + Thread.currentThread().getId());
System.out.println("子线程id:" + thread.getId());
}
}
//输出结果
主线程id:1
子线程id:11
复制代码
getId
内部调用是nextThreadID
thread.getId = nextThreadID()
private static synchronized long nextThreadID() {
return ++threadSeqNumber;
}
复制代码
6.3 线程名字、守护线程
6.3.1 线程名字
1. 默认线程名字源码分析
- "Thread-" + 自增数
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
private static synchronized int nextThreadNum() {
return threadInitNumber++;
}
复制代码
2. 修改线程的名字(代码演示、源码分析)
Thread thread = new Thread();
System.out.println("子线程初始名字:" + thread.getName());
thread.setName("FlyThread-1");
System.out.println("子线程修改后的名字:" + thread.getName());
//输出结果
子线程初始名字:Thread-0
子线程修改后的名字:FlyThread-1
复制代码
6.3.2 守护线程
1. 概念
Java线程分为用户线程和守护线程
,线程的daemon属性为true表示是守护线程
,false表示是用户线程。
守护线程: 是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程。
用户线程: 是系统的工作线程,它会完成这个程序需要完成的业务操作。
用户线程一旦退出,守护线程也会结束,即守护线程不能单独存在,必须依赖用户线程存在
代码演示如下,当main线程执行结束退出后,a线程会自动结束,即运行按钮不会是红色了。
public class DaemonDemo
{
public static void main(String[] args)
{
Thread a = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+" come in:\t"
+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
while (true)
{
}
}, "a");
a.setDaemon(true);
a.start();
//暂停几秒钟线程
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+" ----task is over");
}
}
复制代码
重点:
- 当程序中所有
用户线程执行完毕之后
,不管守护线程是否结束,系统都会自动退出
。如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出。 - 设置守护线程,需要在start()方法之前进行
2. 三个特性
- 线程类型默认继承自父线程(守护线程的子线程也是守护线程)
- 通常守护线程都是由JVM自动启动的
- 不影响JVM退出:JVM退出只会考虑是否还有用户线程
3. 守护线程的常见面试问题
-
守护线程和普通线程的区别
- 整体无区别
- 唯一区别在于JVM的离开:用户线程会影响JVM的停止,而守护线程不影响
- 作用不同:用户线程是执行逻辑的,而守护线程是给用户线程提供服务的
-
我们是否需要给线程设置为守护线程?
thread.setDaemon(true)
- 不应该把自己的用户线程设置为守护线程。
- 例如:如果设置了用户线程为守护线程,JVM发现只有一个守护线程,就中止退出了,导致程序逻辑没有走完。其实JVM本身提供的守护线程就已经足够了
6.4 线程优先级
10个级别,默认5
引申面试题:为什么程序设计不应依赖于线程优先级?
- 由于
优先级最终是由线程调度器来决定调度方案的
,所以优先级高并不能保证就一定比优先级低的先运行:并且如果优先级设置得不合适,可能会导致线程饥饿等问题(优先级低的线程始终得不到运行),所以通常而言,我们不必要设置线程的优先级属性,保持默认的优先级就可以。
6.5、各属性总结
属性名称 | 用途 | 注意事项 |
---|---|---|
编号(ID) | 标识不同的线程 | 线程回收后,id被后续创建的线程使用;无法保证id的唯一性(之前线程id,跟后续线程id不一定是同一个线程,可能是回收后后续创建的);不允许修改id |
名称(Name) | 定位问题 | 可以设置一个清晰有意义的名字(方便跟踪定位);默认的名称是Thread-0/1/2/3 |
是否是守护线程(isDaemon) | 守护线程/用户线程 | 二选一;继承父线程;setDaemon |
优先级(Priority) | 告诉线程调度器,哪些线程相对多运行、哪些少运行 | 默认和父线程的优先级相等,共有10个等级,默认5;不应依赖优先级 |
7. 核心7:线程异常处理(全局异常处理UncaughtExceptionHandler)
7.1 为什么需要UncaughtExceptionHandler?
- 主线程可以轻松发现异常,子线程却不行
- 子线程异常无法用传统方法(try-catch)捕获(类似main方法中执行thread.start,抛出异常是在子线程的run中,而try-catch的是主线程main,所以捕获不到)
- 不能直接捕获会导致一些后果(无法捕获到异常,做相应的重试操作逻辑)
7.2 两种解决方案
方案一(不推荐):手动在每个run方法里进行try catch
public class CanCatchDirectly implements Runnable{
public static void main(String[] args) throws InterruptedException {
new Thread(new CanCatchDirectly(), "MyThread-1").start();
Thread.sleep(300);
new Thread(new CanCatchDirectly(), "MyThread-2").start();
Thread.sleep(300);
new Thread(new CanCatchDirectly(), "MyThread-3").start();
Thread.sleep(300);
new Thread(new CanCatchDirectly(), "MyThread-4").start();
}
@Override
public void run() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println("Caught Exception");
}
}
}
//输出结果
Caught Exception
Caught Exception
Caught Exception
Caught Exception
复制代码
方案二(推荐):利用UncaughtExceptionHandler
-
UncaughtExceptionHandler
接口 -
void uncaughtException(Thread t, Throwable e);
Thread.java
@FunctionalInterface
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* <p>Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
复制代码
-
异常处理器的调用策略
-
自己实现
- 给程序统一设置
- 给每个线程单独设置
- 给线程池设置
1、自己的UncaughtExceptionHandler /** * 自己的UncaughtExceptionHandler */ 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); System.out.println(name + "捕获了异常" + t.getName() + "异常" + e); } } 2、使用自己的UncaughtExceptionHandler,触发 /** * UseOwnUncaughtExceptionHandler * * @author venlenter * @Description: 使用自己的UncaughtExceptionHandler * @since unknown, 2020-04-28 */ public class UseOwnUncaughtExceptionHandler implements Runnable { public static void main(String[] args) throws InterruptedException { Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler("捕获器1")); new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-1").start(); Thread.sleep(300); new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-2").start(); Thread.sleep(300); new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-3").start(); Thread.sleep(300); new Thread(new UseOwnUncaughtExceptionHandler(), "MyThread-4").start(); } @Override public void run() { throw new RuntimeException(); } } //输出结果 捕获器1捕获了异常MyThread-1异常java.lang.RuntimeException 四月 28, 2020 11:34:06 下午 ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.MyUncaughtExceptionHandler uncaughtException 警告: 线程异常,终止了MyThread-1 java.lang.RuntimeException at ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:24) at java.lang.Thread.run(Thread.java:748) 四月 28, 2020 11:34:06 下午 ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.MyUncaughtExceptionHandler uncaughtException 捕获器1捕获了异常MyThread-2异常java.lang.RuntimeException 警告: 线程异常,终止了MyThread-2 java.lang.RuntimeException at ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.UseOwnUncaughtExceptionHandler.run(UseOwnUncaughtExceptionHandler.java:24) at java.lang.Thread.run(Thread.java:748) 四月 28, 2020 11:34:07 下午 ConcurrenceFolder.mooc.threadConcurrencyCore.uncaughtexception.MyUncaughtExceptionHandler uncaughtException ... 复制代码
7.3 线程的未捕获异常-常见面试问题
Java异常体系图:
7.3.1 为什么要全局处理?不处理行不行?
不处理是不行的,因为否则异常信息会抛给前端,这会让重要的信息泄露,不安全。只要是未处理异 常,我们返回给前端就是简单的一句话“意外错误”,而不应该把异常栈信息也告诉前端,否则会被白 帽子、黑客利用。
7.3.2 如何全局处理异常?
- 给程序统一设置
- 先自己实现UncaughtException Handler接口,在uncaughtException(Thread t,Throwable e)的实现上,根据业务需要可以有不同策略,最常见的方式是把错误信息写入日志,或者重启线程、或执行其他修复或诊断。
- 给每个线程或线程池单独设置
- 刚才我们是给整个程序设置了默认的JncaughtExceptionHandler,这是通常的做法。当然,如果业务有特殊需求,我们也可以给某个线程或者线程池指定单独的特定的UncaughtExceptionHandler,这样可以更精细化处理。
7.3.3 run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
run方法不能抛出异常,如果运行时发生异常,线程会停止运行,状态变成Terminated。
8. 核心8:线程安全-多线程会导致的问题
8.1 线程安全
8.1.1 什么是线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的————《Java并发编程实战》
8.1.2 线程不安全:get同时set
- 全都线程安全?:运行速度、设计成本、trade off
- 完全不用于多线程的代码:不过度设计
8.2 什么情况下会出现线程安全问题,怎么避免?
8.2.1 运行结果错误:a++多线程下出现消失的请求现象
/**
* 普通a++会导致count叠加错误,以下程序已优化处理
*/
public class MultiThreadsError3 implements Runnable {
int index = 0;
final boolean[] marked = new boolean[10000000];
static AtomicInteger realIndex = new AtomicInteger();
static AtomicInteger wrongCount = new AtomicInteger();
static MultiThreadsError3 instance = new MultiThreadsError3();
static volatile CyclicBarrier cyclicBarrier1 = new CyclicBarrier(2);
static volatile CyclicBarrier cyclicBarrier2 = new CyclicBarrier(2);
@Override
public void run() {
marked[0] = true;
for (int i = 0; i < 10000; i++) {
try {
cyclicBarrier2.reset();
cyclicBarrier1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
index++;
try {
cyclicBarrier1.reset();
cyclicBarrier2.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
realIndex.incrementAndGet();
//在原基础上加synchronized
synchronized (instance) {
if (marked[index] && marked[index - 1]) {
System.out.println("发生错误:" + index);
wrongCount.incrementAndGet();
}
marked[index] = true;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("表面上结果是:" + instance.index);
System.out.println("真正运行的次数:" + realIndex.get());
System.out.println("错误次数:" + wrongCount.get());
}
}
//输出结果
表面上结果是:20000
真正运行的次数:20000
错误次数:0
复制代码
8.2.2 活跃性问题:死锁、活锁、饥饿
/**
* 第二章线程安全问题,演示死锁
*/
public class MultiThreadError implements Runnable {
int flag = 1;
static Object o1 = new Object();
static Object o2 = new Object();
public static void main(String[] args) {
MultiThreadError r1 = new MultiThreadError();
MultiThreadError r2 = new MultiThreadError();
r1.flag = 1;
r2.flag = 0;
new Thread(r1).start();
new Thread(r2).start();
}
@Override
public void run() {
System.out.println("flag = " + flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
}
//输出结果
flag = 1
flag = 0
//程序一直等待不结束
复制代码
8.2.3 对象发布和初始化时的安全问题
1. 什么是发布?
public、return都算是获得对象
,发布了该对象出去
2. 什么是溢出?
- 方法返回一个private对象(定义了private对象的getXX()方法)(private的本意是不让外部访问)
- 还未完成初始化(构造函数没完全执行完毕)就把对象提供给外界,比如:
- 在
构造函数中未初始化完毕就把this赋值出去
了
/**
* 初始化未完毕,就this赋值
*/
public class MultiThreadsError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
Thread.sleep(105);
if (point != null) {
System.out.println(point);
}
}
}
class Point {
private final int x, y;
Point(int x, int y) throws InterruptedException {
this.x = x;
//这里先行给point赋值this,此时外部拿到point对象只有x,没有y的值
MultiThreadsError4.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return "Point{" +
"x=" + x +
", y=" + y +
'}';
}
}
class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//输出结果
//可能
Point{x=1, y=1}
//也可能
Point{x=1, y=0}
复制代码
2. 隐式逸出
————注册监听事件
/**
* 观察者模式
*/
public class MultiThreadsError5 {
int count;
public MultiThreadsError5(MySource source) {
source.registerListener(new EventListener() {
@Override
//这里EventListener是一个匿名内部类,实际上也用了count这个外部引用变量,当count未初始化完成,拿到的值就还是0
public void onEvent(Event e) {
System.out.println("\n我得到的数字是:" + count);
}
});
for (int i = 0; i < 10000; i++) {
System.out.print(i);
}
count = 100;
}
public static void main(String[] args) {
MySource mySource = new MySource();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
mySource.eventCome(new Event() {
});
}
}).start();
MultiThreadsError5 multiThreadsError5 = new MultiThreadsError5(mySource);
}
static class MySource {
private EventListener listener;
void registerListener(EventListener eventListener) {
this.listener = eventListener;
}
void eventCome(Event e) {
if (listener != null) {
listener.onEvent(e);
} else {
System.out.println("还未初始化完毕");
}
}
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
//输出结果
012345678910...
我得到的数字是:0
...
复制代码
- 构造函数中运行线程
/**
* 构造函数中新建线程
*/
public class MultiThreadsError6 {
private Map<String, String> states;
public MultiThreadsError6() {
new Thread(new Runnable() {
@Override
public void run() {
states = new HashMap<>();
states.put("1", "周一");
states.put("2", "周二");
states.put("3", "周三");
states.put("4", "周四");
}
}).start();
}
public Map<String, String> getStates() {
return states;
}
public static void main(String[] args) {
MultiThreadsError6 multiThreadsError6 = new MultiThreadsError6();
//在构造函数中states还未初始化完成,就get
System.out.println(multiThreadsError6.getStates().get("1"));
}
}
//输出结果
Exception in thread "main" java.lang.NullPointerException
at ConcurrenceFolder.mooc.threadConcurrencyCore.background.MultiThreadsError6.main(MultiThreadsError6.java:34)
复制代码
3. 如何解决逸出
- 返回“副本”(返回对象的deepCopy)--对应解决(1.方法返回了private对象)
- 工厂模式--对应解决(2.还没初始化就吧对象提供给外界)
8.3 各种需要考虑线程安全的情况
- 访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、共享缓存、数据库等
- 所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发问题:read-modify-write、check-then-act(a++问题)
- 不同的数据之间存在捆绑关系的时候(原子操作:要么全部执行,要么全部不执行)
- 我们使用其他类的时候,如果对方没有声明自己是线程安全的,则我们需要做相应的处理逻辑
8.4 双刃剑:多线程会导致的问题
8.4.1 性能问题有哪些体现、什么是性能问题
- 服务响应慢、吞吐量低、资源消耗(例如内存)过高等
- 虽然不是结果错误,但仍然危害巨大
- 引入多线程不能本末倒置
8.4.2 为什么多线程会带来性能问题
-
调度:上下文切换
- 什么是上下文?:线程A执行到某个地方,然后要切换到另一个线程B的时候,CPU会保存当前的线程A在CPU中的状态(上下文)到内存中的某处,等线程B执行完成后,回到线程A需要还原线程A之前保存的状态(这种切换需要耗时)
- 缓存开销(考虑缓存失效):多线程切换,从线程A切换到线程B,线程A的缓存就失效了,需要重新加载
- 何时会导致密集的上下文切换:抢锁、IO
-
协作:内存同步
- 为了数据的正确性,同步手段往往会使用禁止编译器优化、使CPU内的缓存失效(java内存模型)
8.5 常见面试问题
8.5.1 你知道有哪些线程不安全的情况
- 运行结果错误:a++多线程下出现消失的请求现象
- 活跃性问题:死锁、活锁、饥饿
- 对象发布和初始化时的安全问题
8.5.2 平时哪些情况下需要额外注意线程安全问题?
- 访问共享的变量或资源,会有并发风险,比如静态变量。
- 依赖时序的操作。
- 不同的数据之间存在捆绑关系的时候
8.5.3 为什么多线程会带来性能问题?
体现在两个方面:线程的调度和协作,这两个方面通常相辅相成,也就是说,由于线程需要协作,所 以会引起调度:
- 调度:上下文切换
- 什么时候会需要线程调度呢?当可运行的线程数超过了CPU核心数,那么操作系统就要调度线程,以便于让每个线程都有运行的机会。
- 缓存开销
- 除了刚才提到的上下文切换带来的直接开销外,还需要考虑到间接带来的缓存失效的问题。我们知道程序有很大概率会访问刚才访问过的数据,所以CPU为了加快执行速度,会根据不同算法,把常用到的数据缓存到CPU内,这样以后再用到该数据时,可以很快使用。
- 但是现在上下文被切换了,也就是说,CPU即将执行不同的线程的不同的代码,那么原本缓存的内容有极大概率也没有价值了。这就需要CPU重新缓存,这导致线程在被调度运行后,一开始的启动速度会有点慢。
8.5.4 什么是多线程的上下文切换?
参考资料:www.jianshu.com/p/0fbeee2b2…
9. 多线程八大核心总结
- 有多少种实现线程的方法?思路有5点
- 实现Runnable接口和继承Thread类哪种方式更好?
- 一个线程两次调用start()方法会出现什么情况?为什么?
- 既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
- 如何停止线程
- 如何处理不可中断的阻塞
- 线程有哪几种状态?生命周期是什么?
- 用程序实现两个线程交替打印0~100的奇偶数
- 手写生产者消费者设计模式
- 为什么wait()需要在同步代码块内使用,而sleep()不需要
- 为什么线程通信的方法wait(),notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
- wait方法是属于Object对象的,那调用Thread.wait会怎么样?
- 如何选择用notify还是notifyAll?
- notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
- 用suspend()和resume()来阻塞线程可以吗?为什么?
- wait/notify、sleep异同(方法属于哪个对象?线程状态怎么切换?)
- 在join期间,线程处于哪种线程状态?
- 守护线程和普通线程的区别
- 我们是否需要把线程设置为守护线程?
- run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
- 线程中如何处理某个未处理异常?
- 什么是多线程的上下文切换
思维导图总结:
参考资料
Java并发编程知识体系
Java多线程编程核心技术
Java并发编程的艺术
Java并发实现原理 JDK源码剖析