Bootstrap

Java 多线程:从休眠到同步的全面解析

1. 线程休眠–sleep

1.1 关于sleep方法

  1. static void sleep(long millis)

    静态方法,没有返回值,参数是一个毫秒。1秒 = 1000毫秒

  2. 这个方法作用是:

    让当前线程进入休眠,也就是让当前线程放弃占有的CPU时间片,让其进入阻塞状态

    意思:你别再占用CPU了,让给其他线程吧。

    阻塞多久呢?参数毫秒为准。在指定的时间范围内,当前线程没有权利抢夺CPU时间片了。

  3. 怎么理解“当前线程”呢?

    Thread.sleep(1000); 这个代码出现在哪个线程中,当前线程就是这个线程。

  4. run方法在方法重写的时候,不能在方法声明位置使用 throws 抛出异常。

  5. 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的调度:

  1. 让位

  2. 静态方法:Thread.yield()

  3. 与sleep一样,都是让当前线程让位。

  4. 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。

  5. 只能保证大方向上的,大概率,到了某个点让位一次。

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)

  1. 在Java语言中,线程被分为两大类:

    第一类:用户线程(非守护线程)

    第二类:守护线程(后台线程)

  2. 在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。

  3. 守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。

  4. 如何将一个线程设置为守护线程?

    线程对象.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. 什么情况下需要考虑线程安全问题?

    条件1:多线程的并发环境下

    条件2:有共享的数据

    条件3:共享数据涉及到修改的操作

  2. 一般情况下:

    局部变量不存在线程安全问题,尤其是基本数据类型不存在线程安全问题【在栈中,栈不是共享的】,如果是引用数据类型,就另说了!

    实例变量可能存在线程安全问题。实例变量在堆中。堆是多线程共享的。

    静态变量也可能存在线程安全问题。静态变量在堆中。堆是多线程共享的。

  3. 找一个现实生活中的例子,说明一下关于线程安全问题:比如同时取钱!

    现有两个线程t1和t2,当t1对账户进行取钱时,取完以后对卡中余额进行反馈数据更新,但是在这个过程中假设没有抢占到cpu,此时t2线程取钱抢到了cpu,则t2再次对帐户取出了余额。

  4. 以上多线程并发对同一个账户进行取款操作的时候,有安全问题?怎么解决?

    让线程t1和线程t2排队执行。不要并发。要排队。

    我们把线程排队执行,叫做:线程同步机制。(t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才能执行。只要t1和t2之间发生了等待,就认为是同步。)

    如果不排队,我们将其称为:线程异步机制。(t1和t2各自执行各自的,谁也不需要等对方。并发的,就认为是异步)

    异步:效率高。但是可能存在安全隐患。

    同步:效率低。排队了。可以保证数据的安全问题。

  5. 以下程序存在安全问题。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()

使用线程同步机制,来保证多线程并发环境下的数据安全问题:

  1. 线程同步的本质是:线程排队执行就是同步机制。

  2. 语法格式:

    synchronized(必须是需要排队的这几个线程共享的对象){

    ​ // 需要同步的代码

    }

  3. 原理是什么?

    synchronized(obj){

    ​ // 同步代码块

    }

    假设obj是t1 t2两个线程共享的。

    t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。

    假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。

    当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。

    同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。

  4. 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。

修改上面的代码,解决线程安全问题

  1. 找到同步代码块–getBalnce和setBlance
  2. 找到共享对象–this(即谁调用这个方法,对象就是谁)
  3. 使用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 关键字:

  1. 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。

  2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。

    这种方式相对于之前所讲的局部同步代码块的方式要差一些:

    synchronized(共享对象){

    ​ // 同步代码块

    }

    局部同步代码块这种方式优点:灵活、共享对象和同步代码块的范围可以随便调整。

  3. 什么时候用呢实例方法同步呢?

    刚好这个实力方法内容整体都是同步的内容,此时可以直接给实例方法加关键字,方便且减少代码量。

	/**
     * 取款的方法
     *
     * @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 方法的结束吗?

  1. t1 线程开始运行时,它会调用 mc.m1() 方法。由于 m1 方法是同步的,t1 线程将获得 mc 对象的锁,并进入 m1 方法。
  2. 由于 t1 线程在 m1 方法中会睡眠 5 秒,在main线程中,休眠1秒,t2开始,这时 t2 线程也开始运行并调用 mc.m2() 方法。
  3. 由于 m2 方法不是同步的,它不需要获得 mc 对象的锁,因此 t2 线程可以立即进入 m2 方法并执行它。
  4. 简而言之就是,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。 两个线程互相拥有对方希望获取的锁资源。可能会出现程序堵塞。从而造成死锁。

解决方法:

  1. 不要使用锁嵌套。
  2. 设置超时时间。–Lock类中tryLock.
  3. 使用安全java.util.concurrent下的类。

6. 线程状态(生命周期)

  1. NEW,=>新建状态。
  2. RUNNABLE,=>就绪状态和运行状态
  3. BLOCKED,=>堵塞状态
  4. WAITING,=>等待状态
  5. TIMED_WAITING,=>时间等待
  6. 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();
    }
}

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;