主要来自于《尚硅谷Java教程》

目录

异常处理

异常概述与异常体系结构

在Java语言中,将程序执行中发生的不正常情况称为异常(开发过程中的语法错误和逻辑错误不是异常)。

Java程序在执行过程中所发生的异常事件可分为两类:

  • Error: Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况,例如StackoverflowErrorOOM。一般不编写针对性的代码进行处理。
public static void main(String[] args) {
    // 1. 栈溢出: java.lang.StackOverflowError
    // main(args);

    // 2. 堆溢出: java.lang.OutOfMemoryError
    // Integer[] arr = new Integer[1024*1024*1024];
}
  • Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码讲行处理。例如
    • 空指针访问
    • 试图读取不存在的文件
    • 网络连接中断
    • 数组角标越界

体系结构

java.lang.Throwable父类

  • java.lang.Error
  • java.lang.Exception
    • 编译时异常(checked)
      • IOException
        • FileNotFoundException
      • ClassNotFoundException
    • 运行时异常(unchecked)
      • NullPointerExcepition
      • ArrayIndexOutOfBoundsException
      • NumberFormatException

常见异常

常见运行时异常有:

@Test
public void test() {
    /* NullPointerException */
    // String str = null;
    // str.length();

    /* ArrayIndexOutOfBoundsException */
    // int[] arr = new int[5];
    // arr[5] = 1;

    /* ClassCastException */
    // Object obj = new Date();
    // String str = (String) obj;

    /* NumberFormatException */
    // String str = "abc";
    // int num = Integer.parseInt(str);

    /* ArithmeticException */
    // int a = 10 / 0;
}

常见编译时异常:

@Test
public void testRuntimeException() {
    // 编译时异常
    // 如果文件操作时没有写try-catch结构,javac编译不通过
    File file = new File("Hello.txt");
    try {
        FileInputStream fis = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
}

异常处理机制

程序正常执行的过程中,一旦出现异常,就会在异常代码处生成一个对应异常类对象,并将对象抛出,其后的代码不再执行。我们可以使用try-catch-finally结构或throws结构处理异常。

try-catch-finally

  1. 一旦try中的异常对象匹配到某一个catch时,就进入该catch中进行处理,一旦处理完成,就跳出当前的结构,执行之后的代码。
  2. catch中的异常类型没有子父类关系时,谁声明在上无所谓;当catch中的异常类型满足子父类关系时,则子类必须声明在父类上面,否则报错。
  3. getMessage()错误信息,printStackTrace()打印完整的错误信息。
  4. try结构声明的变量,出了try结构之后,就不能再被调用。
@Test
public void testException() {
    int num = 0;
    String str = "123a";

    try {
        num = Integer.parseInt(str);
        System.out.println("Test1");  // 不会输出
    } catch (NumberFormatException e) {
        System.out.println(e.getMessage());  // For input string: "123a"
        e.printStackTrace();  // 打印错误信息
    } catch (NullPointerException e) {
        e.printStackTrace();
    } catch (Exception e) {
        System.out.println("Exception!");
    }
}

有关finally

  • finally是可选的。
  • finally中的代码一定会被执行,即使catch中又出现了异常、trycatch中有return语句。
@Test
public void testFinally() {
    int n = method();  // Finally.
    System.out.println(n);  // 30
}

public int method() {
    try {
        int[] arr = new int[10];
        arr[10] = 1;
        return 10;
    } catch (ArrayIndexOutOfBoundsException e) {
        return 20;
    } finally {
        System.out.println("Finally.");
        return 30;
    }
}
  • 像数据库连接、输入输出流、网络编程Socket等资源,JVM是不能自动的回收的,我们需要自己手动的进行资源的释放。此时的资源释放,就需要声明在finally中。
@Test
public void testFinallyFileIO() {
    FileInputStream fis = null;  // 由于在try中声明的变量仅在try中有效,这里提前声明fis
    try {
        File file = new File("Hello.txt");  // 打开Hello.txt
        fis = new FileInputStream(file);

        // 读文件
        int data = fis.read();
        while (data != -1) {
            System.out.println((char) data);
            data = fis.read();
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (fis != null) {
            try {
                fis.close();  // 资源释放
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

throws

  • throws+异常类型,写在方法声明处,表示该方法执行时,可能会抛出的异常对象。
  • 一旦方法体执行时,出现异常,仍会生成一个异常类对象。
  • 异常代码后续的代码不再执行。
  • try-catch-finally真正处理了异常,而throws只是将异常抛给了方法的调用者。
@Test
public void testThrows() {
    try {
        method2();
    } catch (FileNotFoundException e) {
        System.out.println("FileNotFoundException!");
        e.printStackTrace();
    } catch (IOException e) {
        System.out.println("IOException!");
        e.printStackTrace();
    }
}

public void method2() throws FileNotFoundException, IOException {
    method1();
}

public void method1() throws FileNotFoundException, IOException {
    File file = new File("Hello.txt");  // 打开Hello.txt
    FileInputStream fis = new FileInputStream(file);

    // 读文件
    int data = fis.read();
    while (data != -1) {
        System.out.println((char) data);
        data = fis.read();
    }
}

方法重写规则

  • 子类重写的方法所抛出的异常不大于父类被重写方法抛出的异常。
    • 如父类方法抛出IOException,子类重写该方法,可以抛出FileNotFoundException,但不能抛出Exception

手动抛出异常

使用throw来手动抛出异常。

public static void testThrow() throws Exception {
    Scanner scanner = new Scanner(System.in);
    int i = scanner.nextInt();
    if (i > 0) {
        System.out.println(i);
    }
    else {
        throw new Exception("i <= 0!!");
    }
}

public static void main(String[] args) throws Exception {
    testThrow();
}

用户自定义异常类

  1. 继承现有的异常结构:RuntimeException, Exception等。
  2. 提供全局常量: serialVersionUID
  3. 提供重载的构造器。
class MyException extends RuntimeException {
    static final long serialVersionUID = -7034897190745766939L;

    public MyException() {
    }

    public MyException(String msg) {
        super(msg);
    }
}
多线程

基本概念:程序、进程、线程

  • 程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程,即生命周期。
    • 如:运行中的QQ,运行中的MP3播放器。
    • 程序是静态的,进程是动态的。
    • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
  • 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
    • 若一个进程同一时间并行执行多个线程,就是支持多线程的。
    • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),线程切换的开销小。
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间:它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。

一个Java程序至少有3个线程:main()主线程、gc()垃圾回收线程、异常处理线程,如果发生异常,会影响主线程。

并行与并发

  • 并行:多个CPU同时执行多个任务。
  • 并发:一个CPU(采用时间片)同时执行多个任务,比如商品秒杀。

多线程的优点

背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?多线程程序的优点:

  1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
  2. 提高计算机系统CPU的利用率。
  3. 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。

何时需要多线程

  • 程序需要同时执行两个或多个任务。
  • 程序需要实现--些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
  • 需要一些后台运行的程序时。

线程的创建和使用

方式一:继承Thread类

  1. 创建一个继承自Thread的子类。
  2. 重写Thread类的run()方法。
  3. 创建该子类的对象,并调用该对象的start()方法。
public class CreateThreadTest {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("t1");
        MyThread t2 = new MyThread("t2");
        t1.start();
        t2.start();
        /*
        t2:0. t1:0. t2:1. t1:1. t2:2. t1:2. t1:3. t1:4. t1:5. t1:6. t1:7. t1:8. t1:9. t2:3. t2:4. t2:5. t2:6. t2:7. t2:8. t2:9.
         */
    }


}

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.print(getName() + ":" + i + ". ");
        }
    }
}

注意事项

  1. 不能通过直接调用run()来启动线程,因为这就是普通的方法调用。
  2. 不能让已经start()的线程再次启动,需要重新创建一个对象。

线程的常用方法

  • void start(): 启动线程,调用run()方法。
  • run(): 线程在被调度时执行的操作。
  • String getName(): 返回线程的名称。
  • void setName(String name): 设置该线程的名称。
  • static Thread currentThread(): 返回执行当前代码的线程。
  • yield(): 释放当前CPU的执行权。
public class CreateThreadTest {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("t1");
        MyThread t2 = new MyThread("t2");
        t1.start();
        t2.start();
        /*
         * 当线程t1或t2数到10的倍数时,会放弃CPU执行权,交给其他线程执行
         * t2:0. t1:0. t2:1. t2:2. t2:3. t2:4. t2:5. t2:6. t2:7. t2:8. t2:9. t2:10. t1:1. t1:2. t1:3. t1:4. t1:5. t1:6. t1:7. t1:8. t1:9. t1:10. t2:11...
         */
    }


}

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.print(getName() + ":" + i + ". ");

            if (i % 10 == 0) {
                yield();
                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • join(): 在线程a调用线程b的join()方法后,此时线程a就进入了阻塞状态,直到线程b完全执行完毕后,线程a才结束阻塞状态。
public class JoinTest {
    public static void main(String[] args) {
        Thread.currentThread().setName("Main");

        Thread t1 = new CountThread();
        t1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i + ". ");
			
            // 当Main线程数到10之后,会等待线程t1结束后再执行
            if (i == 10) {
                try {
                    t1.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        
         /*
         * Main:10. 
         * Thread-0:0. Thread-0:1. Thread-0:2. Thread-0:3. Thread-0:4. Main:11. 
         * Main:12. 
         */
    }
}

class CountThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.print(getName() + ":" + i + ". ");
        }
    }
}
  • static void sleep(long millis): 毫秒为单位,令当前活动线程在指定时间内对CPU放弃控制,使其他线程有机会被执行,时间到后重新排队。
// 实现简单的倒数线程
public class CreateThreadTest {
    public static void main(String[] args) {
        CountDownThread t1 = new CountDownThread("t1", 10);
        t1.start();
    }
}

class CountDownThread extends Thread {
    private int second;

    public CountDownThread(String name, int second) {
        super(name);
        this.second = second;
    }

    @Override
    public void run() {
        for (int i = 0; i < second; i++) {
            System.out.print((second - i) + "... ");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • stop(): 强制结束线程生命周期,已过时,不推荐使用。
  • boolean isAlive(): 返回当前线程是否还活着。

线程的调度

调度策略一般分为:

  • 时间片。
  • 抢占式:高优先级的线程抢占CPU。

Java的调度方法:

  • 同优先级线程组成FIFO队列,使用时间片策略。
  • 对高优先级,使用优先调度的抢占式策略。
  • 线程的优先级等级:MAX_PRIORITY=10MIN_PRIORITY=1、默认为NORM_PRIORITY=5
  • 使用getPriority()返回线程优先级,使用setPriority()设置优先级。

需要注意:

  • 线程创建时继承父类的优先级。
  • 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用。

方式二:实现Runnable接口

  1. 创建一个实现了Runnable接口的类,并实现抽象方法run()
  2. 创建实现类的对象。
  3. 将此对象作为参数传入到Thread类的构造器中,创建Thread类对象。
  4. 调用Thread类对象的start()方法。
public class RunnableTest {
    public static void main(String[] args) {
        Window w1 = new Window();

        // 同一个对象创建多个线程
        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

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

        /*
        Thread-1: Sell 10.
        Thread-1: Sell 9.
        Thread-1: Sell 8.
        Thread-1: Sell 7.
        Thread-2: Sell 7.
        Thread-0: Sell 10.
        Thread-2: Sell 5.
        Thread-0: Sell 4.
        Thread-1: Sell 6.
        Thread-0: Sell 2.
        Thread-2: Sell 3.
        Thread-1: Sell 1.
         */
    }
}

// 卖票线程
class Window implements Runnable {
    // 实现Runnable接口的时候,不用声明为static,因为可以用同一个对象创建多个线程
    private int ticket = 10;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + ": Sell " + ticket + ".");
                ticket--;
            }
            else {
                break;
            }
        }
    }
}

实现Runnable接口优点

  1. 实现接口没有类的单继承局限性。
  2. 实现的方式更适合处理多个线程有共享数据的情况。

方式三:实现Callable接口

JDK5.0新增创建线程方法,与Runnable接口相比,功能更强大:

  • 相比run()方法,call()方法可以有返回值。
  • 方法可以抛出异常。
  • 支持泛型的返回值。
  • 需要借助FutureTask类,比如获取返回结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableTest {
    public static void main(String[] args) {
        // 创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        // 将此对象作为参数出入FutureTask构造器中
        FutureTask futureTask = new FutureTask(numThread);
        // FutureTask对象作为参数,传入Thread类构造器中,并调用start()方法
        new Thread(futureTask).start();

        try {
            // 获取Callable中call()方法的返回值
            Object sum = futureTask.get();
            System.out.println("Summation = " + sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class NumThread implements Callable {
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100 ; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}

Future接口

  • 可以对具体RunnableCallable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutrueTaskFutrue接口的唯一的实现类。
  • FutureTask同时实现了RunnableFuture接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

方式四:线程池

  • 背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处:
    • 提高响应速度(减少了创建新线程的时间)。
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)。
    • 便于线程管理:
      • corePoolSize:核心池的大小。
      • maximumPoolSize:最大线程数。
      • keepAliveTime:线程没有任务时最多保持多长时间后会终止。

相关API

JDK5.0起提供了线程池相关API:ExecutorServiceExecutors

  • ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
    • void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
    • <T>Future<T>submit(Callable<T>task):执行任务,有返回值,一般又来执行Callable
    • void shutdown():关闭连接池。
  • Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程。
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
    • Executors.newFixedThreadPool(n):创建一个可重用固定线程数的线程池。
    • Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池。
    • Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class ThreadPoolTest {
    public static void main(String[] args) {
        // 1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        // 设置线程池属性
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        service1.setCorePoolSize(15);

        service.execute(new NumberThread());  // 适用于Runnable
        service.submit(new NumThread());  // 适用于Callable

        service.shutdown();  // 关闭线程池
    }
}

class NumberThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 100 ; i++) {
            if (i % 2 == 1) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

线程的生命周期

JDK中用Thread.State类定义了线程的几种状态。要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  1. 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  2. 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
  3. 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
  4. 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
  5. 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

JDK源码如下:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

3. Java异常处理与多线程_线程池

线程的同步

线程安全

参考上面的卖票线程,同一张票被售卖了多次,这就是线程不安全的。问题出现的原因是:当某个线程操作车票的过程中,尚未完成时,有其他线程参与进来,也操作了车票。Java中,通过同步机制来解决线程安全问题。

方式一:同步代码块

操作共享数据(多个线程共同操作的数据)的代码,即为需要被同步的代码。

  • 通过synchronized (同步监视器)包裹需要被同步的代码。
  • 同步监视器就是锁,可以是任何一个对象。要求多线程必须共用同一把锁。
  • 在操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率较低。

上面的Window类可以做出如下修改:

class Window implements Runnable {
    // 实现Runnable接口的时候,不用声明为static,因为可以用同一个对象创建多个线程
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ": Sell " + ticket + ".");
                    ticket--;
                }
                else {
                    break;
                }
            }
        }
    }
}

主要注意:

  • 在实现Runnable接口时,由于可以共用对象,则可以使用this充当同步监视器。
  • 而在继承Thread类创建多线程的方式中,可以考虑使用当前类充当同步监视器,如Window.class

方式二:同步方法

如果操作共享数据代码的完整声明在一个方法中,可以将此方法声明同步。同步方法仍然涉及到同步监视器,只是此时我们不需要显式声明。

  • 实现Runnable接口中使用同步方法:
class Window2 implements Runnable {
    private int ticket = 100;
	
    // 实际上就是使用this充当同步监视器
    private synchronized void sell() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ": Sell " + ticket + ".");
            ticket--;
        }
    }

    @Override
    public void run() {
        while (true) {
            sell();
        }
    }
}
  • 继承Thread类中使用同步方法:
class Window3 extends Thread {
    private static int ticket = 100;

    // 需要在同步方法时,同时声明为static
    // 此时同步监视器为当前类本身
    private static synchronized void sell() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + ": Sell " + ticket + ".");
            ticket--;
        }
    }

    @Override
    public void run() {
        while (true) {
            sell();
        }
    }
}

使用同步机制将懒汉式单例模式改写为线程安全的

class Bank {
    private Bank() {
    }

    private static Bank instance = null;

    public static Bank getInstance() {
        /*
         * 方式一:效率较低
         * 这是因为只有第一次new创建对象时是共享操作
         * 当对象成功创建后,就不需要同步了
         */
        // synchronized (Bank.class) {
        //     if (instance == null) {
        //         instance = new Bank();
        //     }
        //     return instance;
        // }

        /*
         * 方式二
         */
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}

死锁

  • 出现原因:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常、提示,只是所有线程都处于阻塞状态。
  • 为了避免出现死锁,需要设计专门的算法、原则,尽量减少同步资源的定义、避免嵌套同步。

方式三:Lock锁

  • 从JDK5.0开始,Java提供了更强大的线程同步机制:通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
class Window5 implements Runnable {
    private int ticket = 100;

    // 1. 实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                // 2. 调用lock方法
                lock.lock();

                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ": Sell " + ticket + ".");
                    ticket--;
                }
                else {
                    break;
                }
            } finally {
                // 3. 调用unlock
                lock.unlock();
            }
        }
    }
}

与synchronized区别

  • synchronized机制在执行完相应的同步代码后,自动释放同步监视器。而Lock需要使用lock()方法手动启动同步,使用unlock()方法手动结束同步。
  • Lock只有代码块锁,synchronized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多子类)。

优先顺序:Lock、同步代码块(已经进入了方法体,分配了相应资源)、同步方法体(在方法体之外)。

线程的通信

  • wait()方法:当前线程进入阻塞状态,并释放同步监视器。
  • notify()方法:唤醒一个wait的线程,如果有多个线程wait,唤醒优先级最高的。
  • notifyAll()方法:唤醒所有wait的线程。

两个线程交替打印1-100代码:

public class CommunicationTest {
    public static void main(String[] args) {
        // 2个线程交替打印1-100
        Number num = new Number();

        Thread t1 = new Thread(num);
        Thread t2 = new Thread(num);

        t1.start();
        t2.start();
        /*
         * Thread-0:1
         * Thread-1:2
         * Thread-0:3
         * Thread-1:4 ...
         */
    }
}

class Number implements Runnable {
    private int number = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                // 唤醒wait中的线程
                notifyAll();

                if (number <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    // 调用wait()方法的线程进入阻塞状态(会释放锁)
                    try {
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

线程通信注意事项

  1. wait()notify()notifyAll()必须使用在同步代码块或同步方法中。
  2. wait()notify()notifyAll()的调用者必须是同步代码块中的同步监视器,否则会出现IllegalMonitorStateException异常。在上面的代码中,由于同步监视器为this,所以在调用时省略了调用对象,实际上是this.notifyAll()this.wait()
  3. wait()notify()notifyAll()定义在java.lang.Object类中,这是因为同步监视器用任何对象都可以,这就表明任何一个对象都要有这3种方法。

sleep和wait的区别

sleep()wait()方法都可以使当前线程进入阻塞状态,但是区别在于:

  1. 声明位置:Thread类中声明sleep()方法,Object类中声明wait()方法。
  2. 调用要求不同:sleep()可以在任意场景下调用,而wait()必须在同步代码块或同步方法中调用。
  3. 同步监视器:如果二者都在同步代码块或者同步方法中,sleep()方法不会释放锁,而wait()方法会释放锁。