1. 线程休眠–sleep
1.1 关于sleep方法
static void sleep(long millis)
静态方法,没有返回值,参数是一个毫秒。1秒 = 1000毫秒
这个方法作用是:
让当前线程进入休眠,也就是让当前线程放弃占有的CPU时间片,让其进入阻塞状态。
意思:你别再占用CPU了,让给其他线程吧。
阻塞多久呢?参数毫秒为准。在指定的时间范围内,当前线程没有权利抢夺CPU时间片了。
怎么理解“当前线程”呢?
Thread.sleep(1000); 这个代码出现在哪个线程中,当前线程就是这个线程。
run方法在方法重写的时候,不能在方法声明位置使用 throws 抛出异常。
sleep方法可以模拟每隔固定的时间调用一次程序。
public class ThreadTest {
public static void main(String[] args) {
try {
// 让当前线程睡眠5秒
// 这段代码出现在主线程中,所以当前线程就是主线程
// 让主线程睡眠5秒
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
}
// 启动线程
Thread t = new Thread(new MyRunnable());
t.setName("t");
t.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
1.2 面试题
注意:
sleep是一个静态方法,实例对象在调用静态方法时依然使用的是“类名.方法”的方式,而不是用“引用.方法”
所以尽管在main方法中,创建了一个t分支线程,然后使用该线程调用sleep方法,依然是使当前线程也就是main线程进入休眠。
/**
* 关于sleep的面试题:以下程序中,是main线程休眠5秒,还是分支线程t休眠5秒?
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread t = new MyThread();
t.setName("t");
t.start();
try {
// 这行代码并不是让t线程睡眠,而是让当前线程睡眠。
// 当前线程是main线程。
t.sleep(1000 * 5); // 等同于:Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
}
}
}
class MyThread extends Thread {
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
}
}
}
1.3 中断休眠
线程.interrupt();
原理:调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterruptedException,然后捕捉异常,终止睡眠。
/**
* 怎么中断一个线程的睡眠。(怎么解除线程因sleep导致的阻塞,让其开始抢夺CPU时间片。)
*/
public class ThreadTest {
public static void main(String[] args) {
// 创建线程对象并启动
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "===> begin");
try {
// 睡眠一年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
} catch (InterruptedException e) {
// 打印异常信息
//e.printStackTrace();
System.out.println("知道了,这就起床!");
}
// 睡眠一年之后,起来干活了
System.out.println(Thread.currentThread().getName() + " do some!");
}
});
// 启动线程
t.start();
// 主线程
// 要求:5秒之后,睡眠的Thread-0线程起来干活
try {
Thread.sleep(5 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Thread-0起来干活了。
// 这行代码的作用是终止 t 线程的睡眠。
// interrupt方法是一个实例方法。
// 以下代码含义:t线程别睡了。
// 底层实现原理是利用了:异常处理机制。
// 当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterruptedException,然后捕捉异常,终止睡眠。
t.interrupt();
}
}
1.4 线程终止
定义一个中间变量flag,true表示继续,false表示中断
public class ThreadTest {
public static void main(String[] args) {
// 创建线程
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.setName("t");
// 启动线程
t.start();
// 5秒之后终止线程t的执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//终止线程t的执行。
mr.run = false;
}
}
class MyRunnable implements Runnable {
/**
* 是否继续执行的标记。
* true表示:继续执行。
* false表示:停止执行。
*/
boolean run = true;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if(run){
System.out.println(Thread.currentThread().getName() + "==>" + i);
try {
//每隔1秒执行一次
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}else{
return;
}
}
}
}
2. 线程让位–yield
关于JVM的调度:
让位
静态方法:Thread.yield()
与sleep一样,都是让当前线程让位。
注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
只能保证大方向上的,大概率,到了某个点让位一次。
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.setName("t1");
Thread t2 = new MyThread();
t2.setName("t2");
t1.start();
t2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 500; i++) {
//设定当线程名为t1,并且,下标能被10整除时,让位。
//注意--让位以后t1仍然能够具备竞争cpu,所以是大概率的t1让位后,t2执行。
if(Thread.currentThread().getName().equals("t1") && i % 10 == 0){
System.out.println(Thread.currentThread().getName() + "让位了,此时的i下标是:" + i);
// 当前线程让位,这个当前线程一定是t1
// t1会让位一次
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}
3. 守护线程–setDaemon(true)
在Java语言中,线程被分为两大类:
第一类:用户线程(非守护线程)
第二类:守护线程(后台线程)
在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。
守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。
如何将一个线程设置为守护线程?
线程对象.setDaemon(true);
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("t");
// 在启动线程之前,设置线程为守护线程--当main线程结束后,守护线程就结束了。
// 如果不设置守护,按照线程run方法体内所写,应该是一个死循环。
myThread.setDaemon(true);
myThread.start();
// 10s结束!
// main线程中,main线程是一个用户线程。
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
class MyThread extends Thread {
@Override
public void run() {
int i = 0;
while(true){
System.out.println(Thread.currentThread().getName() + "===>" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
4. 线程安全问题
4.1 引出
线程安全专题:
什么情况下需要考虑线程安全问题?
条件1:多线程的并发环境下
条件2:有共享的数据
条件3:共享数据涉及到修改的操作
一般情况下:
局部变量不存在线程安全问题,尤其是基本数据类型不存在线程安全问题【在栈中,栈不是共享的】,如果是引用数据类型,就另说了!
实例变量可能存在线程安全问题。实例变量在堆中。堆是多线程共享的。
静态变量也可能存在线程安全问题。静态变量在堆中。堆是多线程共享的。
找一个现实生活中的例子,说明一下关于线程安全问题:比如同时取钱!
现有两个线程t1和t2,当t1对账户进行取钱时,取完以后对卡中余额进行反馈数据更新,但是在这个过程中假设没有抢占到cpu,此时t2线程取钱抢到了cpu,则t2再次对帐户取出了余额。
以上多线程并发对同一个账户进行取款操作的时候,有安全问题?怎么解决?
让线程t1和线程t2排队执行。不要并发。要排队。
我们把线程排队执行,叫做:线程同步机制。(t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。只要t1和t2之间发生了等待,就认为是同步。)
如果不排队,我们将其称为:线程异步机制。(t1和t2各自执行各自的,谁也不需要等对方。并发的,就认为是异步)
异步:效率高。但是可能存在安全隐患。
同步:效率低。排队了。可以保证数据的安全问题。
以下程序存在安全问题。t1和t2线程同时对act一个账号进行取款操作。数据是错误的。
/**
*/
public class ThreadTest {
public static void main(String[] args) {
// 创建账户对象
Account act = new Account("act-001", 10000);
// 创建线程对象1
Thread t1 = new Thread(new Withdraw(act));
// 创建线程对象2
Thread t2 = new Thread(new Withdraw(act));
// 启动线程
t1.start();
t2.start();
}
}
// 取款的线程类
class Withdraw implements Runnable {
// 实例变量(共享数据)
private Account act;
public Withdraw(Account act) {
this.act = act;
}
@Override
public void run() {
act.withdraw(1000);
}
}
// 银行账户
class Account {
/**
* 账号
*/
private String actNo;
/**
* 余额
*/
private double balance;
public Account(String actNo, double balance) {
this.actNo = actNo;
this.balance = balance;
}
public String getActNo() {
return actNo;
}
public void setActNo(String actNo) {
this.actNo = actNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
/**
* 取款的方法
* @param money 取款额度
*/
public void withdraw(double money){
// 想要演示出多线程并发带来的安全问题,这里建议分为两步去完成取款操作。
// 第一步:读取余额
double before = this.getBalance();
System.out.println(Thread.currentThread().getName() + "线程正在取款"+money+",当前"+this.getActNo()+"账户余额" + before);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 第二步:修改余额
this.setBalance(before - money);
System.out.println(Thread.currentThread().getName() + "线程取款成功,当前"+this.getActNo()+"账户余额" + this.getBalance());
}
}
4.2 线程同步机制–synchronized()
使用线程同步机制,来保证多线程并发环境下的数据安全问题:
线程同步的本质是:线程排队执行就是同步机制。
语法格式:
synchronized(必须是需要排队的这几个线程共享的对象){
// 需要同步的代码
}
原理是什么?
synchronized(obj){
// 同步代码块
}
假设obj是t1 t2两个线程共享的。
t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。
修改上面的代码,解决线程安全问题
- 找到同步代码块–getBalnce和setBlance
- 找到共享对象–this(即谁调用这个方法,对象就是谁)
- 使用synchronized( this ) 将同步代码块包裹住
public class ThreadTest {
public static void main(String[] args) {
// 创建账户对象
Account act = new Account("act-001", 10000);
// 创建线程对象1
Thread t1 = new Thread(new Withdraw(act));
// 创建线程对象2
Thread t2 = new Thread(new Withdraw(act));
// 启动线程
t1.start();
t2.start();
}
}
// 取款的线程类
class Withdraw implements Runnable {
// 实例变量(共享数据)
private Account act;
public Withdraw(Account act) {
this.act = act;
}
@Override
public void run() {
act.withdraw(1000);
}
}
// 银行账户
class Account {
private static Object obj = new Object();
/**
* 账号
*/
private String actNo;
/**
* 余额
*/
private double balance;
public Account(String actNo, double balance) {
this.actNo = actNo;
this.balance = balance;
}
public String getActNo() {
return actNo;
}
public void setActNo(String actNo) {
this.actNo = actNo;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
/**
* 取款的方法
* @param money 取款额度
*/
public void withdraw(double money){
// this是当前账户对象
// 当前账户对象act,就是t1和t2共享的对象。
synchronized (this){
//synchronized (obj) {
// 第一步:读取余额
double before = this.getBalance();
System.out.println(Thread.currentThread().getName() + "线程正在取款"+money+",当前"+this.getActNo()+"账户余额" + before);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 第二步:修改余额
this.setBalance(before - money);
System.out.println(Thread.currentThread().getName() + "线程取款成功,当前"+this.getActNo()+"账户余额" + this.getBalance());
}
}
}
4.3 同步实例方法
在实例方法上也可以添加 synchronized 关键字:
在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
这种方式相对于之前所讲的局部同步代码块的方式要差一些:
synchronized(共享对象){
// 同步代码块
}
局部同步代码块这种方式优点:灵活、共享对象和同步代码块的范围可以随便调整。
什么时候用呢实例方法同步呢?
刚好这个实力方法内容整体都是同步的内容,此时可以直接给实例方法加关键字,方便且减少代码量。
/**
* 取款的方法
*
* @param money 取款额度
*/
public synchronized void withdraw(double money) {
// 第一步:读取余额
double before = this.getBalance();
System.out.println(Thread.currentThread().getName() + "线程正在取款" + money + ",当前" + this.getActNo() + "账户余额" + before);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 第二步:修改余额
this.setBalance(before - money);
System.out.println(Thread.currentThread().getName() + "线程取款成功,当前" + this.getActNo() + "账户余额" + this.getBalance());
}
4.4 面试题
线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
- 当
t1
线程开始运行时,它会调用mc.m1()
方法。由于m1
方法是同步的,t1
线程将获得mc
对象的锁,并进入m1
方法。- 由于
t1
线程在m1
方法中会睡眠 5 秒,在main线程中,休眠1秒,t2开始,这时t2
线程也开始运行并调用mc.m2()
方法。- 由于
m2
方法不是同步的,它不需要获得mc
对象的锁,因此t2
线程可以立即进入m2
方法并执行它。- 简而言之就是,m2没锁,m1有锁,有锁的只会影响共享该锁时的执行顺序,m2没锁,只要m2开始调用,就是直接运行到结束
综上:
m2
方法在执行的时候,不需要等待m1
方法的结束。m2
方法可以并发地执行,不受m1
方法正在执行的影响。
public class ThreadTest {
public static void main(String[] args) {
MyClass mc = new MyClass();
Thread t1 = new Thread(new MyRunnable(mc));
Thread t2 = new Thread(new MyRunnable(mc));
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
class MyRunnable implements Runnable {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){
mc.m1();
}
if("t2".equals(Thread.currentThread().getName())){
mc.m2();
}
}
}
class MyClass {
public synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("m1 over");
}
public void m2(){
System.out.println("m2 begin");
System.out.println("m2 over");
}
}
假如此时,m2也加锁,m1执行begin以后,沉睡5秒,没有释放锁,m2无法执行需要排队,故一定是按照m1,m2顺序执行。
假如此时,m2也加锁,此时创建两个任务对象,m1和m2并发执行。m1begin以后,沉睡5秒,m2begin,m2over,m1over。
假如此时,m1和m2均不加锁,也是按照m1,m2顺序执行。
假如此时,m1和m2均不加锁,且main线程中休眠1秒取消,则t1和t2变成抢占式开启,谁先抢到资源,哪个方法先执行完毕。
m1和m2均加锁,且创建两个任务对象,把m1和m2两个实例方法变为静态方法,此时运行结果如何?
答:m1执行完毕后m2执行。
本质上——等待共有资源。
静态方法加锁,使用的是类锁,类锁只有一把,所以需要等待,m1结束,m2开始。
类锁是,对于一个类来说,只有一把锁。不管创建了多少个对象,类锁只有一把。
/**
* 线程同步机制的面试题:分析以下程序 m2 方法在执行的时候,需要等待 m1 方法的结束吗?
* 需要等待。
*
* 在静态方法上添加synchronized之后,线程会占有类锁。
* 类锁是,对于一个类来说,只有一把锁。不管创建了多少个对象,类锁只有一把。
*
* 静态方法上添加synchronized,实际上是为了保证静态变量的安全。
* 实例方法上添加synchronized,实际上是为了保证实例变量的安全。
*/
public class ThreadTest {
public static void main(String[] args) {
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
Thread t1 = new Thread(new MyRunnable(mc1));
Thread t2 = new Thread(new MyRunnable(mc2));
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
class MyRunnable implements Runnable {
private MyClass mc;
public MyRunnable(MyClass mc) {
this.mc = mc;
}
@Override
public void run() {
if("t1".equals(Thread.currentThread().getName())){
mc.m1();
}
if("t2".equals(Thread.currentThread().getName())){
mc.m2();
}
}
}
class MyClass {
public static synchronized void m1(){
System.out.println("m1 begin");
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("m1 over");
}
public static synchronized void m2(){
System.out.println("m2 begin");
System.out.println("m2 over");
}
}
5. 死锁问题
线程A拥有锁资源a,希望获取锁资源b,线程B拥有锁资源b,希望获取锁资源a。 两个线程互相拥有对方希望获取的锁资源。可能会出现程序堵塞。从而造成死锁。
解决方法:
- 不要使用锁嵌套。
- 设置超时时间。–Lock类中tryLock.
- 使用安全java.util.concurrent下的类。
6. 线程状态(生命周期)
- NEW,=>新建状态。
- RUNNABLE,=>就绪状态和运行状态
- BLOCKED,=>堵塞状态
- WAITING,=>等待状态
- TIMED_WAITING,=>时间等待
- TERMINATED;=>终止。
7. 线程通信
① wait(): 线程执行该方法后,进入等待状态,并且释放对象锁。
② notify(): 唤醒优先级最高的那个等待状态的线程。【优先级相同的,随机选一个】。被唤醒的线程从当初wait()的位置继续执行。
③ notifyAll(): 唤醒所有wait()的线程
④ 需要注意的:
1.以上三个方法在使用时,必须在同步代码块中或同步方法中。
2.调用这三个方法的对象必须是共享的锁对象。
3.这三个方法都是Object类的方法。
4.调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
⑤ wait()和sleep的区别?
1.相同点:都会阻塞。
2.不同点:
① wait是Object类的实例方法。sleep是Thread的静态方法。
② wait只能用在同步代码块或同步方法中。sleep随意。
③ wait方法执行会释放对象锁。sleep不会。
④ wait结束时机是notify唤醒,或达到指定时间。sleep结束时机是到达指定时间。
⑥ 例如调用了:obj.wait(),什么效果?
obj是多线程共享的对象。当调用了obj.wait()之后,在obj对象上活跃的所有线程进入无期限等待。
直到调用了该共享对象的 obj.notify() 方法进行了唤醒。而且唤醒后,会接着上一次调用wait()方法的位置继续向下执行。
7.1 案例1–两个线程交替输出
/*
题目描述:两个线程交替输出
t1-->1
t2-->2
t1-->3
t2-->4
t1-->5
t2-->6
t1-->7
t2-->8
t1-->9
t2-->10
t1-->11
t2-->12
t1-->13
t2-->14
....
*/
public class ThreadTest {
public static void main(String[] args) {
// 共享对象
MyRunnable mr = new MyRunnable();
// 两个线程
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
// 实例变量,多线程共享的。
private int count = 0;
private Object obj = new Object();
@Override
public void run() {
while(true){
//synchronized (this){
synchronized (obj) {
// 记得唤醒t1线程
// t2线程执行过程中把t1唤醒了。但是由于t2仍然占用对象锁,所以即使t1醒了,也不会往下执行。
//this.notify();
obj.notify();
if(count >= 100) break;
// 模拟延迟
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 程序执行到这里count一定是小于100
System.out.println(Thread.currentThread().getName() + "-->" + (++count));
try {
// 让其中一个线程等待,这个等待的线程可能是t1,也可能是t2
// 假设是t1线程等待。
// t1线程进入无期限的等待,并且等待的时候,不占用对象锁。
//this.wait();
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
7.2 案例2–三个线程交替输出
/* 新题目:
* t1-->A
* t2-->B
* t3-->C
* t1-->A
* t2-->B
* t3-->C
* ....
* t1-->A
* t2-->B
* t3-->C
*/
public class ThreadTest {
// 共享对象(t1 t2 t3线程共享的一个对象,都去争夺这一把锁)
private static final Object lock = new Object();
// 给一个初始值,这个初始值表示第一次输出的时候,t1先输出。
private static boolean t1Output = true;
private static boolean t2Output = false;
private static boolean t3Output = false;
public static void main(String[] args) {
// 创建三个线程
// t1线程:负责输出A
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 10; i++) {
while(!t1Output){ // 只要不是t1线程输出
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 程序到这里说明:该t1线程输出了,并且t1线程被唤醒了。
System.out.println(Thread.currentThread().getName() + " ---> A");
// 该布尔标记的值
t1Output = false;
t2Output = true;
t3Output = false;
// 唤醒所有线程
lock.notifyAll();
}
}
}
}).start();
// t2线程:负责输出B
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 10; i++) {
while(!t2Output){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + " ---> B");
// 该布尔标记的值
t1Output = false;
t2Output = false;
t3Output = true;
// 唤醒所有线程
lock.notifyAll();
}
}
}
}).start();
// t3线程:负责输出C
new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock){
for (int i = 0; i < 10; i++) {
while(!t3Output){
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(Thread.currentThread().getName() + " ---> C");
// 该布尔标记的值
t1Output = true;
t2Output = false;
t3Output = false;
// 唤醒所有线程
lock.notifyAll();
}
}
}
}).start();
}
}