多线程
1. 认识线程
1.1. 概念
- 什么是线程?
一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间就可以 “同时”执行多份代码。
举个例子就是造一台电脑,一个人造的话就是单线程,如果将电脑的每个部位分给多个人,每个人干自己的活,这就是多线程。
- 为什么要有多线程?
并发变成成为现代所需
- 单核CPU的发展遇到了瓶颈,要想提高算力,就需要多核CPU。而并发变成能更充分利用多核CPU资源
- 有些任务场景需要“等待IO”,为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。
其实多进程也能实现并发变成,但是线程要比进程更轻量级。
- 创建线程要比创建进程更快
- 销毁线程比销毁进程更快
- 调度线程比调度进程更快
在拥有了线程之后,为了方便管理,又有了“线程池”、“协程”。
- 进程和线程的区别
- 进程是包含线程的,每个进程至少有一个线程的存在,即主线程(管理其他线程)。
- 进程和进程之间不共享内存,在同一进程下的线程共享同一内存空间。
- 进程是分配资源的最小单位,线程是系统调度的最小单位
1.2. 体验多线程编程
编写一个简单的并发编程程序,我们发现
- 每个线程都是一个独立的执行流
- 多个线程之间是“并发”执行的
class ThreadDemo extends Thread {
@Override
public void run() {
while (true) {
System.out.println("ThreadDemo");
}
}
}
public class Demo1 {
public static void main(String[] args) {//主线程
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
while (true) {
System.out.println("main");
}
}
}
1.3. 创建线程
1.3.1. 继承Thread
- 继承Thread来创建一个线程类
class ThreadDemo extends Thread {
@Override
public void run() {
//编写线程运行代码
}
}
- 创建线程类实例
ThreadDemo threadDemo = new ThreadDemo();
- 启动调用start()方法启动线程
threadDemo.start();
优点
- 实现简单
- 直接使用线程方法:可以直接调用Thread类中的方法,如
start()
,sleep()
,getId()
。
缺点
- 不适合多继承:Java中类不能多继承。
- 因为直接继承自Thread类,创建的对象会包含Thread类的一些额外特性和数据,可能会比实现
Runnable
接口更重。
1.3.2. 实现Runnable接口
- 创建线程类ThreadDemo2并实现Runnable接口
class ThreadDemo2 implements Runnable {
@Override
public void run() {
//编写线程运行代码
}
}
- 创建Thread实例,调用Thread的构造返方法并传递线程类实例(ThreadDemo2)
Thread thread = new Thread(new ThreadDemo2());
- 调用start()方法
thread.start();
优点
- 适合多继承
- 资源分离:通过将线程的任务与线程控制分离(任务在
Runnable
实现类中,控制在Thread
类中),可以更清晰地管理线程的行为和业务逻辑。 - 更轻的类: 实现
Runnable
接口的类不需要继承Thread
,所以不会有额外的线程控制数据,类的重量较轻。
缺点
- 需要额外的Thread实例: 必须创建一个
Thread
对象来运行实现Runnable
接口的任务,略显冗余。
1.4. 多线程的优势
多线程是可以提高程序整体运行效率的,例如下面我们使用计时来观察单线程和多线程的效果。
public class Demo3 {
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使用并发方式
concurrency();
// 使用串行方式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利用一个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运行结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = end - begin;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long end = System.nanoTime();
double ms = end - begin;
System.out.printf("串行: %f 毫秒%n", ms);
}
}
2. Thread类
2.1. Thread的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
Thread thread1 = new Thread();
Thread thread2 = new Thread(new ThreadDemo2());
Thread thread3 = new Thread("线程3");
Thread thread4 = new Thread(new ThreadDemo2(),"线程4");
2.2. Thread的常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID是线程的唯一标识(不会重复)
- 名称在调试时候常用
- 状态标识线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
- run方法运行结束后线程结束
2.3. start()
run方法并不是启动线程,之前我们重写run方法是书写线程执行内容。调用start()
方法才是正真执行此线程
2.4. 中断一个线程
线程进入到执行状态之后如何进行中断?
目前常见的有两种方法:
- 通过共享的标记来进行沟通
- 调用interrupt()方法中断
示例一:使用自定义的变量来作为标志位
- 需要给标志位加上volatile关键字
public class Demo5 {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ "正在运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "执行中断");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "thread线程");
System.out.println(Thread.currentThread().getName()
+ ": 开始执行。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 终止执行!");
target.isQuit = true;
}
}
示例二:使用Thread.Interrupted()
或者Thread.currentThread().isInterrupted()
代替自定义标志位
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
- 使用thread对象的
interrupted()
方法通知线程结束
public class Demo6 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName()
+ "正在运行中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ "执行中断");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "thread线程");
System.out.println(Thread.currentThread().getName()
+ ":thread开始执行。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ":thread终止执行!");
thread.interrupt();
}
}
thread收到通知的方式有两种:
- 如果线程因为调用wait/join/sleep等方法阻塞挂起,则以interruptedException异常的形式通知,清除中断标志
- 当出现InterruptedException的时候,要不要结束线程取决于catch中代码的写法,可以选择忽略这个异常,也可以跳出循环结束线程。
- 否则只是内部的一个中断标志位被设置,thread可以通过
- thread.interrupted()判断当前线程的中断标志位被设置,清除中断标志
- thread.currentThread().isInterrupted()判断指定线程的中断标志位被设置,不清除中断标志位这种方式通知收到的更及时,即使线程正在sleep也可以马上收到。
示例三 :观察标志位是否清除
标志位就类似于一个开关,Thread.isInterrupted()
相当于按下开关,然后开关自动复位
Thread.currentThread().isInterrupted()
按下开关,但是不复位
- 使用Thread.isInterrupted(),线程中断会清除标志位
public class Demo7 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "Demo7");
thread.start();
thread.interrupt();
}
}
- 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除
public class Demo7 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "Demo7");
thread.start();
thread.interrupt();
}
}
2.5. 等待线程
有时候代码需要一个程序执行完毕之后再继续往下执行。就需要用到join()方法
public class Demo8 {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在工作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始工作");
thread1.start();
thread1.join();
System.out.println("李四工作结束了,让王五开始工作");
thread2.start();
thread2.join();
System.out.println("王五工作结束了");
}
}
上面这代码就是李四先工作完,然后王五在开始工作。
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
2.6. 获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class Demo9 {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7. 休眠线程
这个方法我们上面已经用过了,就是让线程停止运行一段时间。
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
3. 线程的状态
3.1. 观察线程的所有状态
public class Demo1 {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values())
System.out.println(state);
}
}
NEW //安排了工作, 还未开始行动
RUNNABLE //可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED //表示排队等着其他事情
WAITING //表示排队等着其他事情
TIMED_WAITING //表示排队等着其他事情
TERMINATED //工作完成了
3.2. 线程状态和状态转移的意义
网图(侵权删)
3.3. 线程状态转换
使用isAlive方法判定线程的存活状态
- 观察new、runnable、terminated
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1_0000; i++) {
}
}, "李四");
System.out.println(t.getName() + ": " + t.getState());
t.start();
while (t.isAlive()) {
System.out.println(t.getName() + ": " + t.getState());
}
System.out.println(t.getName() + ": " + t.getState());
}
}
李四: NEW
李四: RUNNABLE
李四: RUNNABLE
李四: RUNNABLE
李四: RUNNABLE
李四: TERMINATED
- 观察WAITING、BLOCKED、TIMED_WAITING状态的转换
public class Demo3 {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
}
我们直接使用jconsole看到t1的状态是TIMED_WAITING 、t2的状态是BLOCKED,阻塞者是t1
接着我们将t1中的sleep换成wait
public class Demo4 {
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
}
hehe //输出打印
总结一下
- BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知
- TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
- yield()让出cpu
public class Demo5 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("t1");
// 先注释掉, 再放开
// Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("t2");
}
}
}, "t2");
t2.start();
}
}
- 不使用yield时t1打印数量与t2相差无几
- 使用yield时t1数量大于t2
yield不会改变线程的状态,但是会重新去排队
4. 线程安全
4.1. 举例线程不安全
public class Demo6 {
static class Counter {
public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
54111 //运行结果
为什么会出现这种情况?
假设当i = 10000的时候,t1、t2并行同时判断i都不小于50000,所以会同时调用increase方法,假设此时内存中的count = 20000,但是t1 执行完count++操作之后,得20001,此时内存的count还未及时更新,但是t2线程获取到的count也为20000,执行完count++操作也是20001,2次执行只加了一次值。所以所以如果程序运行越快,最终结果与预期结果相差越多。
4.2. 线程安全的概念
具体来说,线程安全意味着:
- 数据完整性:多个线程同时操作共享数据时,不会导致数据的不一致或破坏。
- 原子性:线程执行的关键操作是不可分割的,即使有多个线程并发执行,也不会出现中间状态。
- 可见性:一个线程对共享数据的修改,对于其他线程是可见的,即其他线程能够看到最新的修改结果。
- 有序性:操作执行的顺序是可预期的,不会因为线程调度的不确定性而导致错误的执行顺序。
简单来说就是如果多线程环境下的代码运行的结果符合我们的预期,也就是多线程运行的结果与单线程运行的结果一致,那么就是线程安全的。
4.3. 线程不安全的原因
4.3.1. 修改共享数据
对于上面的线程不安全的示例中,counter.count变量是一个共享变量,它在堆上,所以可以被多个线程访问。
4.3.2. 原子性
- 什么是原子性?
如果我们把一段代码比喻为一间房,假设房间不上锁,那么在A进入房间之后,B也可以进入房间。如果加锁且这个锁只有A有钥匙,那么在A出来之前其他人是进不去的,这就是原子性。
- 一条Java语句不一定是原子性的,也不一定是一条指令
比如我们代码中的n++,其实是由三步组成的:
-
- 从内存中把数据读到CPU
- 进行数据更新
- 把数据写回到CPU
- 不保证原子性会出现什么问题?
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果可能是错误的。
4.3.3. 可见性
一个线程对共享变量值的修改,能够及时的被其他线程看到,就是可见性。
- Java 内存模型(JMM):java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内容访问差异,以实现让Java程序在各种平台下都能达到一致并发效果。
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每一个线程都有自己的 "工作内存" (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存
由于每个线程都有自己的工作内存,这些内存中的内容相当于同一个共享变量的“副本”。此时修改线程1的工作内容中的值,线程2的工作内存不一定会即使发生变化。
- 初始化情况下,两个线程的工作内容一致。
- 一旦线程1修改了a的值,此时主内存中不一定能及时更新。对应的线程2的工作内容的a的值也不一定能及时同步
此时引入了两个问题: 为啥要整这么多内存?为啥要这么麻烦的拷来拷去?
- 为啥整这么多内存?实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法.所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.
- 为啥要这么麻烦的拷来拷去?因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍)
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了
4.3.4. 代码顺顺序性
例如程序是这样的:
- 去小卖部买一瓶饮料
- 去教室拿本书
- 去小卖部买支笔
如果是在单线程的情况下,JVM、CPU指令集会对器进行优化,比如按照1->3->2执行,也是可以的,可以少跑一趟小卖部,这样叫做指令重排序。
编译器对于指令重排序的前提是“保持逻辑不发生变化”,这一点在单线程环境下比较容易判断。但是在多线程环境下就没那么容易了,多线程的代码执行复杂度很高,编译器很难在编译阶段对代码的执行效果进行预判,因此重排序很容易导致优化后的逻辑和之前不等价。
4.4. 解决之前线程不安全的问题
public class Demo6 {
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
5. synchronized关键字-监视器锁monitor lock
5.1. synchronized的特征
- 互斥
synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待。
- 进入synchronized修饰的代码块,相当于加锁。
- 退出synchronized修饰的代码块,箱单故意解锁。
- synchronized用的锁时存在Java对象里的。
例如t1先进入count++程序,那么t1就对此程序加锁了,只有它执行完毕,才会解锁。t2在此过程只能等待。
- 刷新内存
synchronized的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更新后的共享变量的值刷新到主内存
- 释放互斥锁
所以synchronized也能保证内存可见性
- 可重入
什么是可重入的?
本来上过锁的程序线程想再次获取的话应该是阻塞等待释放。可重入指可重复获得锁,例如线程t1已经获取一个加锁对象,他想再次获取此加锁对象是可以的。
内部机制
当一个线程第一次进入同步方法时,它获取对象的锁并持有它。每当它再次进入这个或其他同步方法时,Java 允许线程再次获取这个锁,而不会阻塞自己。这是通过一个计数器来实现的:
- 第一次获取锁时,计数器设为 1。
- 每次重入(即再次获取锁)时,计数器递增。
- 每次退出同步方法时,计数器递减。
- 当计数器为 0 时,锁被释放。
5.2. synchronized使用示例
synchronized本质上要修改指定对象的“对象头”。从使用角度来看,synchronized也势必要搭配一个具体的对象来使用。
- 修饰普通方法:锁Demo1中的静态方法fun
public class Demo1 {
public synchronized void fun() {
//方法内容
}
}
- 修饰静态方法:
public class Demo1 {
public synchronized static void fun() {
//方法内容
}
}
- 修饰代码块:指定锁哪个对象
锁当前对象
class Demo2 {
public void fun() {//此时fun方法不可为静态方法
synchronized (this) {
}
}
}
锁类对象
class Demo2 {
public void fun() {
synchronized (Demo2.class) {
}
}
}
5.3. 线程安全类举例
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
线程安全类
- Vector
- HashTable
- ConcurrentHashMap
- StringBuffer
还有String虽然没有加锁,但是String是不能修改的,所以仍然是线程安全的
6. volatile关键字
volatile能保证内存可见性
代码在写入volatile修饰的变量的时候
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取volatile修饰的变量的时候
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
但是volatile不保证原子性
这个很好验证,我们只要将synchronized拿掉,并给变量加上volatile。
public class Demo6 {
static class Counter {
public volatile int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
55566 //输出结果
看到结果不等于10_0000就知道了
synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.
对上面的代码进行调整:
- 去掉 flag 的 volatile
- 给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.
public class Demo6 {
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
}
7. wait和notify
由于线程之间是抢占式的,因此线程之间执行的先后顺序难以预知。
想合理协调多个线程之间的执行顺序怎么办呢?
- wait() /wait(long timeout):让当前线程进入等待状态。
- notify() /notifyAll():唤醒在当前对象上等待的线程。
wait,notify,notifyAll都是Object类的方法
7.1. wait()
- 使当前执行代码的线程进行等待
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
wait结束等待的条件:
- 其他线程调用该对象的notify方法
- wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
- 其他线程调用该等待线程的interrupted方法,导致wait抛出
InterruptedException
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify() 。
7.2. notify()
notify 方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public class Demo1 {
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
}
7.3. notifyAll()
notify方法唤醒一个等待线程,使用notifyAll方法可以一次性唤醒所有的等待线程,在这里就不细讲了。
转载自:https://juejin.cn/post/7376276140595068937