线程间通信
同步机制
生产者线程 ProducerThread:依次对共享数据 Student 进行赋值。
消费者线程 ConsumerThread:打印输出共享数据 Student。
共享资源 Student
Student:
public class Student {
String name;
int age;
}
ProducerThread:
public class ProducerThread implements Runnable {
private Student s;
private int x = 0;
public ProducerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
if (x % 2 == 0) {
s.name = "lili";//刚走到这里,就被别人抢到了执行权
s.age = 18;
} else {
s.name = "jack"; //刚走到这里,就被别人抢到了执行权
s.age = 20;
}
x++;
}
}
}
ConsumerThread:
public class ConsumerThread implements Runnable {
private Student s;
public ConsumerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
System.out.println(s.name + "---" + s.age);
}
}
}
StudentDemo:
public class StudentDemo {
public static void main(String[] args) {
//创建共享资源
Student s = new Student();
//设置和获取的类
ProducerThread st = new ProducerThread(s);
ConsumerThread gt = new ConsumerThread(s);
//线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
//启动线程
t1.start();
t2.start();
}
}
输出:
...
jack---18
lili---18
lili---18
lili---18
jack---20
lili---18
lili---20
结果输出有jack---18
、lili---20
,显然不符合预期。那什么原因导致的呢?
- 同一个数据打印多次:CPU的一点点时间片的执行权,就足够你执行很多次。
- 姓名和年龄不匹配:线程运行的随机性。
首先,我们分析下代码:
- 是否有多线程环境? 是。
- 是否有共享数据? 是。共享对象 s。
- 是否有多条语句操作共享数据? 是。s.name = “lili”;s.age = 18;
那么,解决方案是:加锁。
ProducerThread:
public class ProducerThread implements Runnable {
private Student s;
private int x = 0;
public ProducerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
// 增加同步代码块
synchronized (s) {
if (x % 2 == 0) {
s.name = "lili";
s.age = 18;
} else {
s.name = "jack";
s.age = 20;
}
x++;
}
}
}
}
ConsumerThread:
public class ConsumerThread implements Runnable {
private Student s;
public ConsumerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
// 增加同步代码块
synchronized (s) {
System.out.println(s.name + "---" + s.age);
}
}
}
}
输出:
...
lili---18
lili---18
jack---20
jack---20
...
输出正常。
等待唤醒机制
需求:让lili---18
、jack---20
交替打印,其实就是生产者 ProducerThread 执行一次后等待,然后消费者 ConsumerThread 执行一次后等待,然后生产者 ProducerThread 再执行一次,交替执行。
Java提供的等待唤醒机制
解决。
- public final void
wait()
:当前线程堵塞等待。除非调用此对象的notify()
方法或notifyAll()
方法。 - public final void
wait
(long timeout):当前线程堵塞等待。除非调用此对象的notify()
方法或notifyAll()
方法,或者超过指定的时间量前。 - public final void
notify()
:唤醒在此对象监视器上等待的单个线程。 - public final void
notifyAll()
:唤醒在此对象监视器上等待的所有线程。
只有获取到锁才能调用wait()(在同步块内使用),未获得锁时调用wait()
抛出异常IllegalMonitorStateException,调用wait()
会使当前线程进入阻塞等待状态,并且会释放执行 wait() 的锁资源。
被notify,notifyAll唤醒后,阻塞等待才会结束,线程进入就绪状态,抢占cpu执行权后程序才会往下执行。
public class WaitDemo {
public static void main(String[] args) {
Object lock = new Object();
long startTime = System.currentTimeMillis();
Runnable rs1 = new Runnable() {
@Override
public void run() {
System.out.println("【rs1】启动:" + (System.currentTimeMillis() - startTime));
synchronized (lock) {
System.out.println("【rs1】拿到了锁:" + (System.currentTimeMillis() - startTime));
try {
// wait会堵塞等待,并释放锁。
System.out.println("【rs1】开始wait,释放了锁:" + (System.currentTimeMillis() - startTime));
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 接收notify()唤醒后,才往下接着执行。
System.out.println("【rs1】 wait结束:" + (System.currentTimeMillis() - startTime));
}
}
};
Runnable rs2 = new Runnable() {
@Override
public void run() {
System.out.println("【rs2】启动:" + (System.currentTimeMillis() - startTime));
synchronized (lock) {
System.out.println("【rs2】拿到了锁:" + (System.currentTimeMillis() - startTime));
lock.notify();
}
System.out.println("【rs2】同步代码块执行完成,【rs2】释放了锁:" + (System.currentTimeMillis() - startTime));
}
};
Thread ts1 = new Thread(rs1);
Thread ts2 = new Thread(rs2);
ts1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ts2.start();
}
}
分析:rs1 先获取到锁,调用wait()
会使得当前线程进入堵塞等待状态,并释放执行wait()
的锁资源。此时,rs2拿到了锁,调用notify()
唤醒堵塞的线程,rs1被唤醒结束堵塞等待状态,程序继续往下执行。
等待唤醒改进
改进如下:
- 生产者线程 ProducerThread:判断是否有数据,如果有,等待;如果没有,进行赋值并且修改数据标识为true,并唤醒消费者线程。
- 消费者线程 ConsumerThread:判断是否有数据,如果没有,等待;如果有,打印输出并且修改数据标识为false,并唤醒生产者线程。
- 共享资源 Student:增加数据是否存在标识 flag。
Student:
public class Student {
String name;
int age;
boolean flag; // 默认情况是没有数据,如果是true,说明有数据
}
ProducerThread:
public class ProducerThread implements Runnable {
private Student s;
private int x = 0;
public ProducerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (s.flag) {
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (x % 2 == 0) {
s.name = "lili";
s.age = 18;
} else {
s.name = "jack";
s.age = 20;
}
x++;
//修改标记
s.flag = true;
//唤醒线程
s.notify();
}
}
}
}
ConsumerThread:
public class ConsumerThread implements Runnable {
private Student s;
public ConsumerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
synchronized (s) {
if (!s.flag){
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(s.name + "---" + s.age);
//修改标记
s.flag = false;
//唤醒线程
s.notify();
}
}
}
}
StudentDemo:
public class StudentDemo {
public static void main(String[] args) {
//创建资源
Student s = new Student();
//设置和获取的类
ProducerThread st = new ProducerThread(s);
ConsumerThread gt = new ConsumerThread(s);
//线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
//启动线程
t1.start();
t2.start();
}
}
输出:
...
lili---18
jack---20
lili---18
jack---20
...
线程通信的方法 wait()、notify() 为什么定义在 Object 类中?而不定义在Thread类中呢?
因为这些方法的调用必须通过锁对象调用,比如,s.wait()
、s.notify()
,而使用的锁对象是任意锁对象。所以,这些方法必须定义在 Object 类中。
共享资源类线程安全化
把共享资源 Student 的成员变量私有化。
把设置和获取的操作封装成共享资源功能,并加上同步。使得共享资源类成为线程安全类。
设置或者获取的线程只需要调用方法即可。
Student:
public class Student {
private String name;
private int age;
private boolean flag; // 默认情况是没有数据,如果是true,说明有数据
public synchronized void set(String name, int age) {
// 如果有数据,就等待
if (this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 设置数据
this.name = name;
this.age = age;
// 修改标记
this.flag = true;
this.notify();
}
public synchronized void get() {
// 如果没有数据,就等待
if (!this.flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 获取数据
System.out.println(name + "---" + age);
// 修改标记
this.flag = false;
this.notify();
}
}
ProducerThread:
public class ProducerThread implements Runnable {
private Student s;
private int x = 0;
public ProducerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
if (x % 2 == 0) {
s.set("lili", 18);
} else {
s.set("jack", 20);
}
x++;
}
}
}
ConsumerThread:
public class ConsumerThread implements Runnable {
private Student s;
public ConsumerThread(Student s) {
this.s = s;
}
@Override
public void run() {
while (true) {
s.get();
}
}
}
StudentDemo:
public class StudentDemo {
public static void main(String[] args) {
//创建资源
Student s = new Student();
//设置和获取的类
ProducerThread st = new ProducerThread(s);
ConsumerThread gt = new ConsumerThread(s);
//线程类
Thread t1 = new Thread(st);
Thread t2 = new Thread(gt);
//启动线程
t1.start();
t2.start();
}
}
输出:
...
lili---18
jack---20
lili---18
jack---20
...
共享资源类成为线程安全类,线程只需要直接调用同步方法即可,调用线程不需要编写同步代码逻辑。
定时器的使用
定时器:可以让我们在指定的时间做某件事情,还可以重复的做某件事情。依赖Timer
和TimerTask
这两个类。
- Timer:定时执行
- public Timer()
- public void
cancel()
- public void
schedule
(TimerTask task, long delay):延时 delay 执行。 - public void
schedule
(TimerTask task, long delay, long period):延时 delay 执行,且每间隔 period 执行一次。 - public void
schedule
(TimerTask task, Date time):指定时间执行。
- TimerTask:执行任务
- public abstract class TimerTask extends Object implements Runnable:继承 Runnable 接口
public class TimerDemo {
public static void main(String[] args) {
// 创建定时器对象
Timer t = new Timer();
// 创建任务
t.schedule(new MyTask(t), 1000, 2000);
}
}
// 做一个任务
class MyTask extends TimerTask {
private Timer t;
public MyTask(){}
public MyTask(Timer t){
this.t = t;
}
@Override
public void run() {
System.out.println("beng,爆炸了");
}
}
输出:
beng,爆炸了
beng,爆炸了
beng,爆炸了
...
等待1秒输出,然后每隔2秒输出一次。
需求:在指定的时间删除我们的指定目录
class DeleteFolder extends TimerTask {
@Override
public void run() {
File srcFolder = new File("demo");
deleteFolder(srcFolder);
}
// 递归删除目录
public void deleteFolder(File srcFolder) {
File[] fileArray = srcFolder.listFiles();
if (fileArray != null) {
for (File file : fileArray) {
if (file.isDirectory()) {
deleteFolder(file);
} else {
System.out.println(file.getName() + ":" + file.delete());
}
}
System.out.println(srcFolder.getName() + ":" + srcFolder.delete());
}
}
}
public class TimerTest {
public static void main(String[] args) throws ParseException {
Timer t = new Timer();
String s = "2021-06-29 15:45:00";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d = sdf.parse(s);
t.schedule(new DeleteFolder(), d);
}
}
Timer 负责定时执行计划;TimerTask 的run()
方法负责具体的执行任务。
sleep与wait在释放锁上的区别
sleep期间不会释放锁资源
public class SleepDemo {
public static void main(String[] args) {
Object lock = new Object();
long startTime = System.currentTimeMillis();
Runnable rs1 = new Runnable() {
@Override
public void run() {
System.out.println("rs1启动:" + (System.currentTimeMillis() - startTime));
synchronized (lock) {
System.out.println("rs1拿到了锁:" + (System.currentTimeMillis() - startTime));
try {
System.out.println("rs1睡眠3s...");
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("rs1 睡眠结束:" + (System.currentTimeMillis() - startTime));
}
System.out.println("rs1释放了锁:" + (System.currentTimeMillis() - startTime));
}
};
Runnable rs2 = new Runnable() {
@Override
public void run() {
System.out.println("rs2启动:" + (System.currentTimeMillis() - startTime));
synchronized (lock) {
System.out.println("rs2拿到了锁:" + (System.currentTimeMillis() - startTime));
try {
System.out.println("rs2睡眠1s...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("rs2睡眠结束:" + (System.currentTimeMillis() - startTime));
}
System.out.println("rs2释放了锁:" + (System.currentTimeMillis() - startTime));
}
};
Thread ts1 = new Thread(rs1);
Thread ts2 = new Thread(rs2);
ts1.start();
ts2.start();
}
}
输出:
【rs1】启动:1
【rs2】启动:1
【rs1】拿到了锁:2
【rs1】睡眠3s...
【rs1】睡眠结束:3015
【rs1】释放了锁:3015
【rs2】拿到了锁:3015
【rs2】睡眠1s...
【rs2】睡眠结束:4030
【rs2】释放了锁:4030
分析:rs1 和 rs2 在 1ms 同时启动,然后 rs1 在 2ms 拿到锁并开始睡眠,在 3015ms 释放了锁,rs2直到 3015ms 才拿到了锁,说明 rs1 在睡眠期间并没有释放锁。
sleep() 是 Thread 类中的静态方法,调用 sleep() 会使得当前线程睡眠一段时间。睡眠状态开始时放弃对 CPU 的掌控,并在睡眠持续期间不再抢夺 CPU 计算资源,但是睡眠状态并不会释放持有的锁资源。
调用 sleep() 需要捕获 InterruptedException,避免数据期间设置中断标识。
注:wait() 是 object 类的方法,sleep() 是 Thread 类的方法。
线程的生命周期和线程的状态
Thread.state
枚举类有6个状态:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- 新建(new):新建一个线程对象,JVM为其分配内存。
- 就绪(runnable):调用线程的
start()
方法后,线程处于等待 CPU 分配资源阶段,谁先抢到 CPU 资源,谁开始执行。JVM会为其创建方法调用栈和程序计数器。 - 运行(running):当就绪的线程被调度并获得 CPU 资源时,便进入运行状态。
- 阻塞(blocked):在运行期间,因为某些原因导致运行状态的线程变成了阻塞状态,比如
sleep()
、wait()
之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify()
或者notifyAll()
方法。唤醒的线程不会立刻执行run()
方法,它们要再次等待 CPU 分配资源再进入运行状态。 - 死亡(terminated):线程执行完成或者因异常退出了 run 方法,该线程结束生命周期。
1. RUNNABLE
因为CPU时间片抢占很快,所以,就绪
和运行
状态,都表现为RUNNABLE
。
2. 阻塞的三种情况
- 同步阻塞
运行的线程在获取对象的同步锁时(比如进入synchronized
同步代码块或同步方法时),若该同步锁被别的线程占用,线程就进入了BLOCKED
状态,并且 JVM 会把该线程放入“锁池”中。 - 等待阻塞
运行的线程执行wait()
方法,该线程会释放占用的所有资源,线程就进入了WAITING
状态。必须依靠其他线程调用notify()
或notifyAll()
方法才能被唤醒。 - 其他阻塞
运行的线程执行sleep()
、join()
、等待I/O流的输入输出
、等待网络资源
时,JVM 会把该线程置为阻塞状态。当 sleep 状态超时、join 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
当线程调用
sleep(time)
,或者wait(time)
时,进入TIMED_WAITING
状态,当休眠时间结束后,或者调用notify()
或notifyAll()
方法时会重新进入RUNNABLE
状态。
BLOCKED、WAITING、TIMED_WAITING 我们都称为阻塞状态。
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
/* 定义锁 */
Object obj = new Object();
/* sleep() */
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
/* 抢占锁后wait() */
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
try {
Thread.sleep(1000);
obj.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
t2.start();
/* 抢占已经占用的锁 */
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (obj) {
}
}
});
t3.start();
System.out.println(t1.getState());//TIMED_WAITING
System.out.println(t2.getState());//TIMED_WAITING
System.out.println(t3.getState());//BLOCKED
Thread.sleep(3000);
System.out.println("++++++");
System.out.println(t1.getState());//TERMINATED
System.out.println(t2.getState());//WAITING
System.out.println(t3.getState());//TERMINATED
}
}