Java多线程

12/11/2021 Java多线程

image-20211116212038100

Java高级是面向高级语法部分,主要是服务于一些高级的操作,例如文件、异常、注解、数据结构、多任务、网络通信和数据库等。

多线程技术是Java实现多任务的一个手段,能够用于提高程序运行的效率。

# 多线程

# 多线程介绍

进程:指正在运行的程序。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

  • 那么什么是多线程呢?

    即就是一个程序中有多个线程在同时执行。

通过以下这个例子来区别单线程程序和多线程程序的不同:

  • 单线程程序:若有多个任务只能依次执行。当上一个任务执行结束后,下一个任务开始执行。

    比如:去网吧上网,网吧只能让一个人上网,当这个人下机后,下一个人才能上网。

  • 多线程程序:若有多个任务可以同时执行。

    比如:去网吧上网,网吧能够让多个人同时上网。

image-20211128195927612

线程在CPU内是使用以下两种策略来进行调度。

  • 分时调度:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
  • 抢占式调度:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性)。

在Java内使用的是抢占式调度。大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。

image-20211128200231811

实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

# 主线程

回想我们以前学习中写过的代码,当我们在dos命令行中输入java空格类名回车后,启动JVM,并且加载对应的class文件。虚拟机并会从main方法开始执行我们的程序代码,一直把main方法的代码执行结束。如果在执行过程遇到循环时间比较长的代码,那么在循环之后的其他代码是不会被马上执行的。如下代码演示:

class Demo{
	String name;
	Demo(String name){
		this.name = name;
	}
	void show()	{
		for (int i=1;i<=10000 ;i++ )		{
			System.out.println("name="+name+",i="+i);
		}
	}
}

class ThreadDemo {
	public static void main(String[] args) 	{
	    Demo d = new Demo("小强");
         Demo d2 = new Demo("旺财");
		d.show();		
		d2.show();
		System.out.println("Hello World!");
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

若在上述代码中show方法中的循环执行次数很多,这时在d.show();下面的代码是不会马上执行的,并且在dos窗口会看到不停的输出name=小强,i=值,这样的语句。为什么会这样呢?

原因是:jvm启动后,必然有一个执行路径(线程)从main方法开始的,一直执行到main方法结束,这个线程在java中称之为主线程。当程序的主线程执行时,如果遇到了循环而导致程序在指定位置停留时间过长,则无法马上执行下面的程序,需要等待循环结束后能够执行。

那么,能否实现一个主线程负责执行其中一个循环,再由另一个线程负责其他代码的执行,最终实现多部分代码同时执行的效果?

能够实现同时执行,通过Java中的多线程技术来解决该问题。

# 创建线程方式——继承Thread类

在Java内创建线程的其中一个方式是继承Thread类,子类重写Thread类的run方法,创建对象,开启线程。Thread类是程序中的执行线程。Java虚拟机运行应用程序并发地运行多个执行程序。

image-20211128200654499

  • 构造方法

    image-20211128200709976

  • 常用方法

    image-20211128200724333

创建线程的步骤:

  1. 定义一个类继承Thread。
  2. 重写run方法。
  3. 创建子类对象,就是创建线程对象。
  4. 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
public class MyThread extends Thread {
	//定义指定线程名称的构造方法
	public MyThread(String name) {
		//调用父类的String参数的构造方法,指定线程的名称
		super(name);
	}
	/**
	 * 重写run方法,完成该线程执行的逻辑
	 */
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println(getName()+":正在执行!"+i);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadDemo01 {
	public static void main(String[] args) {
		//创建自定义线程对象
		MyThread mt = new MyThread("新的线程!");
		//开启新线程
		mt.start();
		//在主方法中执行for循环
		for (int i = 0; i < 10; i++) {
			System.out.println("main线程!"+i);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

思考:线程对象调用 run方法和调用start方法区别?

线程对象调用run方法不开启线程。仅是对象调用方法。线程对象调用start开启线程,并让jvm调用run方法在开启的线程中执行。

每个线程都有标识自己的名字和引用,可用以下方法来获取相应的信息。

image-20211128201212913

public class MyThread extends Thread {  //继承Thread
	MyThread(String name){
		super(name);
	}
	//复写其中的run方法
	public void run(){
		for (int i=1;i<=20 ;i++ ){
			System.out.println(Thread.currentThread().getName()+",i="+i);
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
public class ThreadDemo02 {
    public static void main(String[] args) {
        // 创建两个线程任务
        MyThread d1 = new MyThread("Thread01");
        MyThread d2 = new MyThread("Thread02");

        d1.run(); // 没有开启线程,在主线程调用run方法
        d2.start(); // 开启一个新线程,新线程调用run方法
    }
}
1
2
3
4
5
6
7
8
9
10

# 创建线程方式——实现Runnable接口

创建线程的另一种方式是声明实现Runnable接口的类,然后实现run方法,接着创建Runnable的子类对象,传入到某个线程的构造方法中,开启线程。

image-20211128201332542

  • 接口中的方法

    image-20211128201346865

  • Thread类构造方法

    image-20211128201427678

创建线程的步骤。

  1. 定义类实现Runnable接口。
  2. 覆盖接口中的run方法。
  3. 创建Thread类的对象。
  4. 将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
  5. 调用Thread类的start方法开启线程。
public class MyRunnable implements Runnable{

    // 定义线程要执行的run方法逻辑
    @Override
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.println("我的线程:正在执行 ! " + i);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
public class ThreadDemo03 {
    public static void main(String[] args) {
        // 创建线程执行目标类对象
        Runnable runn = new MyRunnable();
        // 将Runnable接口的子类作为参数传递给THread类的构造函数
        Thread t1 = new Thread(runn);
        Thread t2 = new Thread(runn);
        // 开启线程
        t1.start();
        t2.start();
        for (int i = 0; i< 10; i++) {
            System.out.println("main线程:正在执行 " + i);
        }

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

实现Runnable的好处:第二种方式实现Runnable接口避免了单继承的局限性,所以较为常用。

  • 继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,有又有线程任务。

  • 实现Runnable接口,更加的符合面向对象。线程分为两部分,一部分线程对象,一部分线程任务。

  • 实现Runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。

# 线程的匿名内部类使用

使用线程的匿名内部类,可以方便的实现每个线程执行不同的线程任务操作。

  • 方式1:创建线程对象时,直接重写Thread类中的run方法

    /**
     * 线程演示——线程匿名内部类
     */
    public class ThreadDemo04 {
        public static void main(String[] args) {
            // 创建线程对象,直接重写Thread类中的run方法
            new Thread() {
                public void run() {
                    for(int x = 0; x < 40; x++){
                        System.out.println(Thread.currentThread().getName() + " : " + x);
                    }
                }
            }.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 方式2:使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法

    /**
     * 线程演示——线程匿名内部类
     */
    public class ThreadDemo05 {
        public static void main(String[] args) {
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    for(int x = 0; x < 40; x++) {
                        System.out.println(Thread.currentThread().getName() + " : " +x);
                    }
                }
            };
            new Thread(r).start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# 线程池

# 线程池介绍

线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

image-20211128202052859

我们详细的解释一下为什么要使用线程池?

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

# 使用线程池——Runnable接口

通常,线程池都是通过线程池工厂创建,再调用线程池中的方法获取线程,再通过线程去执行任务方法。

  • Executors:线程池创建工厂类

    • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
  • ExecutorService:线程池类

    • Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行。

    • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  1. 创建线程池对象
  2. 创建Runnable接口子类对象
  3. 提交Runnable接口子类对象
  4. 关闭线程池
public class SwimRunnable implements  Runnable{

    @Override
    public void run() {
        System.out.println("我要一个教练");
        try{
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("结束! 教练回到游泳池");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 线程演示——线程池
 */
public class ThreadDemo06 {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(2); // 包含2个线程对象
        // 创建Runnable实例对象
        SwimRunnable sr = new SwimRunnable();

        //自己创建线程对象的方式
        //Thread t = new Thread(sr);
        //t.start(); ---> 调用MyRunnable中的run()

        //从线程池中获取线程对象,然后调用SwimRunnable中的run
        threadPool.submit(sr);
        //再获取个线程对象,调用MyRunnable中的run()
        threadPool.submit(sr);
        threadPool.submit(sr);
        //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
        //关闭线程池
        threadPool.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 使用线程池——Callable接口

Callable接口与Runnable接口功能相似,用来指定线程的任务。其中的call()方法,用来返回线程任务执行完毕后的结果,call方法可抛出异常。

  • ExecutorService:线程池类

    • <T> Future<T> submit(Callable<T> task):获取线程池中的某一个线程对象,并执行线程中的call()方法

    • Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用

使用线程池中线程对象的步骤:

  1. 创建线程对象
  2. 创建Callable接口子类对象
  3. 提交Callable接口子类对象
  4. 关闭线程池
import java.util.concurrent.Callable;

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        System.out.println("我要一个教练: call");
        Thread.sleep(2000);
        System.out.println("教练来了:" + Thread.currentThread().getName());
        System.out.println("结束! 教练回到游泳池");
        return null;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo07 {
    public static void main(String[] args) {
        //创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        //创建Callable对象
        MyCallable c = new MyCallable();

        //从线程池中获取线程对象,然后调用MyCallable中的call()
        service.submit(c);
        //再获取个教练
        service.submit(c);
        service.submit(c);
        //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中

        //关闭线程池
        service.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 线程安全问题

所谓线程安全是指如果有多个线程在同时运行,而这些线程可能会同时运行这段代码,若程序运行结果和单线程运行的结果是一样的,而且其他变量的值也和预期的是一样的,那就是线程安全。

通过一个案例,演示线程的安全问题。

# 案例演示

电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “功夫熊猫3”,本次电影的座位共100个(本场电影只能卖100张票)。我们来模拟电影院的售票窗口,实现多个窗口同时卖 “功夫熊猫3”这场电影票(多个窗口一起卖这100张票)。需要窗口,采用线程对象来模拟;需要票,Runnable接口子类来模拟。

public class Ticket implements Runnable {
	//共100票
	int ticket = 100;

	@Override
	public void run() {
		//模拟卖票
		while(true){
			if (ticket > 0) {
				//模拟选坐的操作
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
			}
		}
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 线程安全问题演示
 */
public class ThreadDemo08 {
    public static void main(String[] args) {
        // 创建票对象
        Ticket ticket = new Ticket();

        // 创建3个窗口
        Thread t1 = new Thread(ticket , "窗口1");
        Thread t2 = new Thread(ticket , "窗口2");
        Thread t3 = new Thread(ticket , "窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

// 输出内容
窗口3正在卖票:33
窗口1正在卖票:32
窗口2正在卖票:33
......
窗口2正在卖票:4
窗口3正在卖票:3
窗口1正在卖票:2
窗口2正在卖票:1
窗口3正在卖票:0
窗口1正在卖票:-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • 票数出现了重复的票
  • 出现了错误的票,例如0和-1

这就是线程安全的问题。线程安全问题都是由全局变量以及静态变量引起的。

若每个线程中对全局变量和静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;

若有多个线程同时执行写操作,一般都需要考虑线程安全,否则可能出现类似于上面的异常的结果。

# 线程同步(线程安全处理Synchronized)

Java中提供了线程同步机制,它能够解决上述的线程安全问题。

线程同步的方式有两种:

  • 方式1:同步代码块
  • 方法2:同步方法

# 同步代码块

在代码块声明上加上synchronized

synchronized (锁对象) {
	可能会产生线程安全问题的代码
}
1
2
3

同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。

使用同步代码块,对电影院卖票案例中Ticket类进行如下代码修改:

public class TicketSafe01 implements  Runnable{

    // 票数
    int ticket = 100;
    // 定义锁对象
    Object lock = new Object();

    @Override
    public void run() {
        // 模拟卖票
        while (true) {
            // 同步代码块
            synchronized (lock) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                        // 休眠时间长一些,不然单个线程抢占锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

当使用了同步代码块后,上述的线程的安全问题,解决了。

# 同步方法

在方法声明上加上synchronized

public synchronized void method(){
   	可能会产生线程安全问题的代码
}
1
2
3

同步方法中的锁对象是this。

public class TicketSafe02 implements  Runnable{

    // 票数
    int ticket = 100;
    // 定义锁对象
    Object lock = new Object();

    @Override
    public void run() {
        // 模拟卖票
        while (true) {

        }
    }

    // 同步方法,锁对象this
    public synchronized void method() {
        if (ticket > 0) {
            //模拟选坐的操作
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 静态同步方法

在方法声明上加上static synchronized

public static synchronized void method(){
	可能会产生线程安全问题的代码
}
1
2
3

静态同步方法中的锁对象是 类名.class

# 线程死锁问题

同步锁使用的弊端:当线程任务中出现多个同步(多个锁)时,如果同步中嵌套了其他的同步。这时容易引起一种现象:程序出现无限等待,这种现象我们称为死锁。

synchronzied(A){
	synchronized(B){        
	}
}
1
2
3
4
  • 定义锁对象类

    public class MyLock {
    	public static final Object lockA = new Object();
    	public static final Object lockB = new Object();
    }
    
    1
    2
    3
    4
  • 线程任务类

    public class ThreadTask implements Runnable {
    	int x = new Random().nextInt(1);//0,1
    	//指定线程要执行的任务代码
    	@Override
    	public void run() {
    		while(true){
    			if (x%2 ==0) {
    				//情况一
    				synchronized (MyLock.lockA) {
    					System.out.println("if-LockA");
    					synchronized (MyLock.lockB) {
    						System.out.println("if-LockB");
    						System.out.println("if大口吃肉");
    					}
    				}
    			} else {
    				//情况二
    				synchronized (MyLock.lockB) {
    					System.out.println("else-LockB");
    					synchronized (MyLock.lockA) {
    						System.out.println("else-LockA");
    						System.out.println("else大口吃肉");
    					}
    				}
    			}
    			x++;
    		}
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
  • 测试类

    /**
     * 死锁演示
     */
    public class ThreadDemo10 {
        public static void main(String[] args) {
            // 创建线程任务对象
            ThreadTask task = new ThreadTask();
            // 创建两个线程
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
            // 启动线程
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

# Lock接口

Lock实现提供了比使用synchronized方法和语句更为广泛的锁操作。

  • Lock接口中的常用方法

    image-20211128210644818

Lock提供了一个更加面向对象的锁,在该锁中提供了更多的操作锁的功能。

我们使用Lock接口,以及其中的lock()方法和unlock()方法替代同步,对电影院卖票案例中Ticket类进行如下代码修改:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketLock implements Runnable{
    // 票数
    int ticket = 100;

    // 创建Lock锁对象
    Lock ck = new ReentrantLock();

    @Override
    public void run() {
        // 模拟卖票
        while (true) {
            ck.lock(); // 获取锁
            if(ticket > 0) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "正在卖票:" + ticket--);
            }
            ck.unlock(); // 释放锁
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 等待唤醒机制

在开始讲解等待唤醒机制之前,有必要搞清一个概念——线程之间的通信。多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。通过一定的手段使各个线程能有效的利用资源。

这种手段即——等待唤醒机制。

等待唤醒机制所涉及到的方法:

  • wait():等待,将正在执行的线程释放其执行资格和执行权,并存储到线程池中。
  • notify():唤醒,唤醒线程池中被wait()的线程,一次唤醒一个,而且是任意的。
  • notifyAll():唤醒全部,可以将线程池中的所有wait()线程全部唤醒

其实,所谓唤醒的意思就是让线程池中的线程具备执行资格。必须注意的是,这些方法都是在同步中才有效。同时这些方法在使用时必须标明所属锁,这样才可以明确出这些方法操作的到底是哪个锁上的线程。

仔细查看JavaAPI之后,发现这些方法 并不定义在 Thread中,也没定义在Runnable接口中,却被定义在了Object类中,为什么这些操作线程的方法定义在Object类中?

因为这些方法在使用时,必须要标明所属的锁,而锁又可以是任意对象。能被任意对象调用的方法一定定义在Object类中。

image-20211128211211014

接下里,我们先从一个简单的示例入手:

image-20211128211304280

如上图说示,输入线程向Resource中输入name ,sex , 输出线程从资源中输出,先要完成的任务是:

  • 当input发现Resource中没有数据时,开始输入,输入完成后,叫output来输出。如果发现有数据,就wait();
  • 当output发现Resource中没有数据时,就wait() ;当发现有数据时,就输出,然后,叫醒input来输入数据。

下面代码,模拟等待唤醒机制的实现:

  • 模拟资源类

    public class Resource {
        private String name;
        private String sex;
        private boolean flag = false;
    
        public synchronized  void set(String name, String sex) {
            if(flag) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 设置成员变量
            this.name = name;
            this.sex = sex;
            System.out.println("Resource设置值——姓名:"+name + ", 性别:"+sex);
            // 若Resource中有值,则标记为true
            flag = true;
            // 唤醒output
            this.notify();
        }
    
        public synchronized void out() {
            if(!flag) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 输出数据
            System.out.println("Resource输出值——姓名:" + name + "," + "性别:" + sex);
            // 改变标记
            flag = false;
            // 唤醒input
            this.notify();
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
  • 输入线程任务类

    public class InputThread implements Runnable{
    
        private Resource r;
    
        public InputThread(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            int count = 0;
            while (true) {
                if(count == 0) {
                    r.set("小米" , "boy");
                } else {
                    r.set("小花", "gril");
                }
                // 切换
                count = (count + 1) % 2;
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
  • 输出线程任务类

    public class OutputThread implements Runnable{
    
        private Resource r;
    
        public OutputThread(Resource r) {
            this.r = r;
        }
    
        @Override
        public void run() {
            while (true) {
                r.out();
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • 测试类

    /**
     * 等待唤醒机制演示
     */
    public class ThreadDemo12 {
        public static void main(String[] args) {
            // 创建资源对象
            Resource r = new Resource();
    
            // 任务对象
            InputThread in = new InputThread(r);
            OutputThread out = new OutputThread(r);
    
            // 线程对象
            Thread t1 = new Thread(in);
            Thread t2 = new Thread(out);
    
            //开启线程
            t1.start();
            t2.start();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
Last Updated: 11/21/2022, 10:03:43 PM