【JavaEE】多线程基础

1. 认识线程(Thread)

1.1 概念

(1) 进程与线程

进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

(2) 为什么要有线程

首先, “并发编程” 成为 “刚需”.

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核CPU. 而并发编程能更充分利用多核 CPU 资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量:

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)

(3) 进程和线程的区别

  1. 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  2. 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  3. 是系统分配资源的最小单位,线程是系统调度的最小单位。


4) Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的pthread 库).
Java 标准库中Thread类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

1.2 创建线程

方法1 继承 Thread 类

public class Demo01_Thread {
    // 继承Thread类并实现run方法
    public static class MyThread extends Thread{
        // run方法中的代码,就表示线程要执行的任务
        @Override
        public void run() {
            System.out.println("hello thread...");
        }

    }

    public static void main(String[] args) {
        // 创建自己定义的线程对象
        MyThread thread = new MyThread();
        // 执行这个线程 start方法是启动线程,并通知操作系统加入CPU调度
        //调用 start 方法, 才真的在操作系统的底层创建出一个线程.
        thread.start();
    }
}

方法2 实现 Runnable 接口

public class Demo03_Runnable {
    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (true){
                System.out.println("金币+1......");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public static void main(String[] args) {
        // 创建Runnable对象
        MyRunnable runnable = new MyRunnable();
        // 创建线程
        Thread thread = new Thread(runnable);
        // 启动线程,参与CPU调度
        thread.start();
    }
}

其他变形

匿名内部类创建 Thread 子类对象

public class Demo05_Thread_Anon {
    public static void main(String[] args) {
//        匿名内部类
        Thread thread = new Thread(){
            @Override
            public void run(){
                while (true) {
                    // 打印线程名称
                    System.out.println(Thread.currentThread().getName() + " running");
                    try {
                        // 随机停止运行 0-9 秒
//                    Thread.sleep(random.nextInt(10));
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        thread.start();
    }
}

匿名内部类创建 Runnable 子类对象

public class Demo06_Runnable_Anon {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("金币+1......");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        thread.start();
    }
}

lambda 表达式创建 Runnable 子类对象

public class Demo07_Lambda {
    public static void main(String[] args) {
//        @FunctionalInterface,函数式接口,接口里只有一个定义方法
//        (传入参数) -> {方法体}
        Thread thread = new Thread(() -> {
            while (true) {
                // 打印线程名称
                System.out.println(Thread.currentThread().getName());
                try {
                    // 随机停止运行 0-9 秒
//                    Thread.sleep(random.nextInt(10));
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread.start();
    }
}

1.3 多线程的优势-增加运行速度

可以观察多线程在一些场合下是可以提高程序的整体运行效率的。

  • 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
  • serial 串行的完成一系列运算.
  • concurrency 使用两个线程并行的完成同样的运算
public class Demo08_1B {
//    次数 十亿 = 1B
    private static long COUNT = 10_0000_0000L;
    public static void main(String[] args) throws InterruptedException {
        // 串行操作,单线程
        serial ();
        // 并行操作,多线程
        concurrency ();
    }

//    线程累加
    private static void concurrency() throws InterruptedException {
        // 记录开始时间
        long begin = System.currentTimeMillis();
        // 创建第一个线程
        Thread t1 = new Thread(() -> {
            long a = 0l;
            for (int i = 0; i < COUNT; i++) {
                a++;
            }
//            System.out.println("t1结束");
        });
        // 第二个线程
        Thread t2 = new Thread(() -> {
            long b = 0l;
            for (int i = 0; i < COUNT; i++) {
                b++;
            }
//            System.out.println("t2结束");
        });
        // 启动线程
        t1.start();
        t2.start();
        // 等待线程执行完成
        t1.join();
        t2.join();

        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("并行总耗时:" + (end - begin));
//        System.out.println("主线程结束");
    }

//    普通累加
    private static void serial() {
// 记录一下开始时间
        long begin = System.currentTimeMillis();
        // 第一个变量
        long a = 0l;
        for (int i = 0; i < COUNT; i++) {
            a++;
        }

        // 第二个变量
        long b = 0l;
        for (int i = 0; i < COUNT; i++) {
            b++;
        }

        // 记录结束时间
        long end = System.currentTimeMillis();
        System.out.println("串行总耗时:" + (end - begin));
    }
}

串行总耗时:1250
并行总耗时:668

2. Thread 类及常见方法

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象 就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
image.png

2.1 Thread 的常见构造方法

image.png

2.2 Thread 的几个常见属性

image.png

  • ID 是线程的 唯一标识 ,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
public class Demo10 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                        System.out.println(Thread.currentThread().getName() + ": 我还 活着");
                        Thread.sleep(1 * 1000);
                 } catch (InterruptedException e) {
                        e.printStackTrace();
                 }
             }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName() + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName() + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName() + ": 优先级: " + thread.getPriority());
//        后台线程处理容错率比较高的业务,或者辅助功能
        System.out.println(Thread.currentThread().getName() + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName() + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName() + ": 被中断: " + thread.isInterrupted());
        thread.start();
//        isAlive()是指系统的线程是否存活
        while (thread.isAlive()) {
        //这里先让子线程结束,再让主线程结束
//            没有while循环的话,主线程会先结束
        }
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
    }
}

2.3 中断一个线程

目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
  2. 调用interrupt() 方法来通知

示例-1: 使用自定义的变量来作为标志位.
需要给标志位上加volatile关键字(这个关键字的功能后面介绍).

public class Demo13_Interrupted01 {
    // 定义一个中断标识
    private static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (!isQuit) {
                System.out.println("hello thread...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程任务结束");
        });
        // 启动线程
        thread.start();
        // 主线程休眠3秒,模拟子线程正在处理任务
        Thread.sleep(3000);
        // 设置中断标志位为true
        isQuit = true;
        // 让子线程先结束
        Thread.sleep(1000);
        System.out.println("主线程结束");

    }
}

示例-2: 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
image.png

public class Demo14_Interrupted02 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            // 注意判断条件
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 异常处理来中断线程
                    // 方式一:啥也不做
                    // 方式二:处理具体的逻辑
                    // 方式三:真正的中断
                    break;
                }
            }
            System.out.println("是否中断:" + Thread.currentThread().isInterrupted());
            System.out.println("线程任务结束");
        });
        // 启动线程
        thread.start();
        // 主线程休眠3秒,模拟子线程正在处理任务
        Thread.sleep(3000);
        // 中断线程,修改Thread中的中断标志
        thread.interrupt();
        // 让子线程先结束
        Thread.sleep(1000);
        System.out.println("主线程结束");
    }
}

thread 收到通知的方式有两种:

  1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以InterruptedException异常的形式通知,清除中断标志
  • 当出现InterruptedException的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
  1. 否则,只是内部的一个中断标志被设置,thread 可以通过
  • Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
  • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志

这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

2.4 等待一个线程-join()

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
image.png

3. 多线程带来的的风险-线程安全 (重点)

3.1 观察线程不安全

public class Demo18_Insecurity {
    // 定义自增操作的对象
    private static Counter counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        // 定义两个线程,分别自增5万次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();
        // 等待自增完成
        t1.join();
        t2.join();
        // 打印结果
        System.out.println("count = " + counter.count);

    }
}

class Counter {
    public int count = 0;
    // 自增方法
    public void increment () {
        count++;
    }
}

count = 98937

4.2 线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.3 线程不安全的原因

修改共享数据
上面的线程不安全的代码中, 涉及到多个线程针对 counter.count 变量进行修改.
此时这个 counter.count 是一个多个线程都能访问到的 “共享数据
image.png
counter.count 这个变量就是在堆上. 因此可以被多个线程共享访问.

原子性

image.png
**什么是原子性 **

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 n++,其实是由三步操作组成的:

  1. 从内存把数据读到 CPU
  2. 进行数据更新
  3. 把数据写回到 CPU

不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。
这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大.

可见性

可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.
image.png

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 “工作内存” (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程 2 的工作内存不一定会及时变化.

此时引入了两个问题:
1) 为啥整这么多内存?

实际并没有这么多 “内存”. 这只是 Java 规范中的一个术语, 是属于 “抽象” 的叫法.
所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存.

2) 为啥要这么麻烦的拷来拷去?

因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 ~ 4 个数量级, 也就是几千倍, 上万倍).
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到
CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了.

那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??
答案就是一个字: 贵
值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘. 对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜.

代码顺序性

什么是代码重排序
一段代码是这样的:

  1. 去前台取下 U 盘
  2. 去教室写 10 分钟作业
  3. 去前台取下快递

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论

3.4 解决之前的线程不安全问题

这里用到的机制,马上会给大家解释。

public class Demo19_synchronized {
    // 定义自增操作的对象
    private static Counter19 counter = new Counter19();

    public static void main(String[] args) throws InterruptedException {
        // 定义两个线程,分别自增5万次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();
        // 等待自增完成
        t1.join();
        t2.join();
        // 打印结果
        System.out.println("count = " + counter.count);

    }
}

class Counter19 {
    public int count = 0;
    // 自增方法
    public synchronized void increment () {
        count++;
    }
}

4. synchronized 关键字-监视器锁monitor lock

4.1 synchronized 的特性

(1) 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
synchronized用的锁是存在Java对象头里的。
image.png

理解 “阻塞等待”.
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.

synchronized的底层是使用操作系统的mutex lock实现的.

(2) 刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.

(3) 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

理解 “把自己锁死”
一个线程没有释放锁, 然后又尝试再次加锁.

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁.
image.png
这样的锁称为 不可重入锁.
Java 中的 synchronized可重入锁, 因此没有上面的问题.
代码示例
在下面的代码中,

increaseincrease2两个方法都加了 synchronized, 此处的 synchronized 都是针对 this当前对象加锁的. 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁.
(上个锁还没释放, 相当于连续加两次锁) 这个代码是完全没问题的. 因为 synchronized 是可重入锁.

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

4.2 synchronized 使用示例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用
(1) 直接修饰普通方法:
锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

(2) 修饰静态方法:
锁的 SynchronizedDemo 类的方法

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

(3) 修饰代码块:

当前对象

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

类对象

public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

我们重点要理解,synchronized 锁的是什么.两个线程竞争同一把锁, 才会产生阻塞等待

5. volatile 关键字

volatile 能保证内存可见性

volatile 修饰的变量, 能够保证 “内存可见性”.
image.png
代码在写入 volatile 修饰的变量的时候,

  • 改变线程工作内存中volatile变量副本的值
  • 改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

volatile 不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

public class Demo30_volatile {
    // 定义自增操作的对象
    private static Counter30 counter = new Counter30();

    public static void main(String[] args) throws InterruptedException {
        // 定义两个线程,分别自增5万次
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 调用加锁的方法
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                // 调用没有加锁的方法
                counter.increment();
            }
        });

        // 启动线程
        t1.start();
        t2.start();
        // 等待自增完成
        t1.join();
        t2.join();
        // 打印结果
        System.out.println("count = " + counter.count);

    }
}

class Counter30 {
    // 加入volatile修饰变量,观察结果,验证是否具备原子性
    public volatile int count = 0;

    // 静态方法,这时的锁对象
    public void increment() {
        // 要执行的修改逻辑
        count++;
    }

}

此时可以看到, 最终 count 的值仍然无法保证是 100000.

synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.

6. wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序
完成一个协调工作, 主要涉及到三个方法

wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是Object类的方法.

6.1 wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的interrupted方法, 导致 wait 抛出InterruptedException 异常.

代码示例: 观察wait()方法使用

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()。

6.2 notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

代码示例:
使用notify()方法唤醒线程

  • 创建WaitTask类, 对应一个线程, run 内部循环调用 wait.
  • 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次notify
  • 注意:WaitTask NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object.
public class Demo31_Wait_Notify {

    private static Object locker = new Object();

    public static void main(String[] args) {
        // 买包子线程
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " wait 之前");
                try {
                    // 等待资源,线程会被阻塞
                    synchronized (locker) {
                        locker.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " wait 之后");
                System.out.println("=============================");
            }
        }, "t1");
        // 启动线程
        t1.start();

        // 包子铺老板线程
        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println(Thread.currentThread().getName() + " notify 之前");
                // 唤醒时也使用同一个锁对象
                synchronized (locker) {
                    locker.notify();
                }
                System.out.println(Thread.currentThread().getName() + " notify 之后");
                // 等待一会
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2");
        // 启动线程
        t2.start();

    }
}

6.3 notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
被唤醒的线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

其余与notify一致。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值