线程基础

2023/12/15 Java多线程与并发

# 1、线程状态

线程状态转换

线程的状态包含

新建(New)、可运行(Runnable)、阻塞(Blocking)、无限期等待(Waiting)、限期等待(Timed Waiting)、死亡(Terminated)。

# 1.1、新建(New)

当新创建一个线程对象时,线程即进入新建状态。此时,该线程还没有开始运行,没有分配到CPU时间片。

# 1.2、可运行(Runnable)

当调用线程的start()方法后,线程即进入运行状态。此时,该线程已经分配到了CPU时间片,并且开始执行。

# 1.3、阻塞(Blocking)

线程阻塞是指线程在执行过程中暂停或等待某个条件满足之后才能继续执行的状态。等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

线程阻塞可能发生在以下几种情况:

  • IO阻塞:线程在执行IO操作(如读写文件、网络通信等)时,如果IO操作未完成或数据未就绪,线程会被阻塞,暂停执行,直到IO操作完成或数据就绪。
  • 等待阻塞:线程调用了wait()方法,进入等待状态,直到其它线程通过notify()或notifyAll()方法唤醒它。
  • 睡眠阻塞:线程调用了sleep()方法,使自身进入睡眠状态,暂停执行指定的时间。
  • 锁阻塞:线程在执行同步代码块或同步方法时,如果获取不到同步锁(即锁已被其它线程持有),线程会被阻塞,直到获得锁。
  • 运行阻塞:处于运行状态的线程可以被调度器暂停执行,转而执行其它线程。

在线程阻塞的状态下,线程暂停执行,不会占用CPU资源。一旦解除阻塞条件,线程会从阻塞状态恢复,重新进入可运行状态,等待CPU调度执行。

# 1.4、无限期等待(Waiting)

等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。

进入方式 退出方式
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 LockSupport.unpark() 方法

# 1.5、限期等待(Timed Waiting)

无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。

  • 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用使一个线程睡眠进行描述。
  • 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用挂起一个线程进行描述。
  • 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
进入方式 退出方式
Thread.sleep() 方法 时间结束
设置了 Timeout 参数的 Object.wait() 方法 时间结束 / Object.notify() / Object.notifyAll()
设置了 Timeout 参数的 Thread.join() 方法 时间结束 / 被调用的线程执行完毕
LockSupport.parkNanos() 方法
LockSupport.parkUntil() 方法

阻塞和等待的区别

  • 阻塞状态(Blocked):线程阻塞是在某些特定情况下发生,比如线程在执行同步代码块或方法时,尝试获取一个已经被其他线程持有的锁,或者在执行IO操作时等待数据准备完成。线程阻塞是由外部因素(如锁、IO操作等)所引起的,线程会暂停执行,直到满足阻塞条件才能继续执行。
  • 等待状态(Waiting):线程等待是通过调用对象的wait()方法来实现的,线程进入等待状态时,它会主动释放所持有的对象锁,进入等待队列中,直到其他线程调用notify()或notifyAll()方法唤醒等待的线程。线程等待是与其他线程之间的协作关系,等待状态的线程需要通过其他线程的通知来唤醒。

阻塞状态是由外部因素被动引起的,线程会暂停执行,并在满足某个条件后继续执行。而等待状态是线程主动释放对象锁,并进入等待状态,直到其他线程通知其继续执行。

# 1.6、死亡(Terminated)

线程执行完了其任务或者出现了异常,即进入死亡状态。

# 2、线程创建使用

线程的创建有三种方式:实现 Runnable 接口、实现 Callable 接口、继承 Thread 类。 其实还有一种线程池创建线程

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。也就是实现 Runnable 和 Callable 接口的类创建的对象,这个对象最终还是作为参数给到 Thread 来驱动执行。

# 2.1、实现 Runnable 接口

实现 Runnable 接口,并重写 run() 方法,然后将其创建为 Thread 对象,调用 Thread 对象的 start() 方法来启动。

public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}

public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}
1
2
3
4
5
6
7
8
9
10
11

# 2.2、实现 Callable 接口

与 Runnable 相似,但 Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2.3、继承 Thread 类

创建 MyThread 类继承Thread,并重写 run() 方法,然后将其创建为 Thread 对象,调用 Thread 对象的 start() 方法来启动。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}

public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}
1
2
3
4
5
6
7
8
9
10

当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

# 2.4、实现接口、继承 Thread类对比

大多数情况下,实现Runnable接口会更好一些。

  • Java不支持多重继承,如果一个类已经继承了其他类,就不能再继承Thread类了,只能通过实现接口来创建线程。
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。
  • 实现接口的方式更符合面向对象的设计思想。

# 3、线程机制

基础的线程机制需要理解:Executor、Daemon、sleep()、yield()。

# 3.1、Executor

从线程创建使用,我们知道了怎么创建和启动一个线程,但是实际的开发过程中,如果用到了多线程,线程不可能只有一个,那如果很多线程,一个线程一个线程的创建启动,那样太麻烦也不好管理了。于是引入了Executor(线程执行器)。

  • Executor 框架是Java中用于执行任务的高级框架(线程池框架),通过使用线程池和任务调度策略,简化了线程的使用和管理,提供了更灵活、可扩展的任务执行方式。
  • Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

Executor继承关系

Executors类是Java中Executor框架的工具类,它提供了一些静态方法来创建不同类型的Executor实例。

Executors中几种常用的方法创建Executor:

方法 说明
newFixedThreadPool(int nThreads) 创建一个固定大小的线程池,该线程池中的线程数量固定为指定的nThreads个。任务会被顺序执行,多余的任务会在任务队列中等待。返回一个ThreadPoolExecutor实例。
newCachedThreadPool() 创建一个可缓存的线程池,线程池的大小会根据需要进行调整,没有活动的线程会被回收,需要执行任务时会自动创建新的线程。返回一个ThreadPoolExecutor实例。
newSingleThreadExecutor() 创建一个单线程的线程池,该线程池中只有一个线程,所有任务按照提交的顺序依次执行。返回一个ThreadPoolExecutor实例。
newFixedThreadPool(int nThreads, ThreadFactory threadFactory) 创建一个固定大小的线程池,并可以设置自定义的线程工厂。线程池中的线程数量固定为指定的nThreads个。任务会被顺序执行,多余的任务会在任务队列中等待。返回一个ThreadPoolExecutor实例。
newScheduledThreadPool(int corePoolSize) 创建一个定时执行任务的线程池,线程池的大小固定为指定的corePoolSize个。该线程池可按照设定的时间间隔或特定的执行时间来执行任务。返回一个ScheduledThreadPoolExecutor实例。
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
}
1
2
3
4
5
6
7

# 3.2、Daemon

守护线程(Daemon Thread)是一种特殊的线程,它的特性是进程结束时,所有的守护线程也会随之结束。

Java中几种常见的线程分类:

  • 用户线程(User Threads):用户线程是应用程序中最常见的线程类型,由应用程序创建和控制。它们执行应用程序的业务逻辑和任务,当所有的用户线程执行完毕后,JVM就会退出。
  • 后台线程(Daemon Threads),又称为守护线程:后台线程是一种特殊的线程,它在程序运行期间在后台提供服务。后台线程的生命周期不会影响应用程序的退出,当所有用户线程结束时,JVM会自动终止所有未结束的后台线程。后台线程通常用于执行一些不需要阻止应用程序退出的任务,如垃圾回收(GC)。
  • 主线程(Main Thread):主线程是应用程序启动时第一个被创建的线程,它执行的是main()方法。主线程通常负责初始化应用程序的环境、加载类和启动其他线程。一旦主线程执行完毕,程序可能继续执行其他用户线程或退出。

守护线程不会阻止JVM(Java虚拟机)或者其他主程序退出怎么理解?

在JVM中,当所有的非守护线程(也就是用户线程)都退出时,那么JVM就认为程序已经结束了,因此会结束运行。这时,JVM不会关心是否还有守护线程在运行,无论守护线程是否完成任务,它们都会被强制结束。因此我们说"守护线程不会阻止JVM或者主程序退出"。

将用户线程设置为守护线程:

public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
}
1
2
3
4

# 3.3、sleep()

Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。

sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

public void run() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
1
2
3
4
5
6
7

# 3.4、yield()

对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。

public void run() {
    Thread.yield();
}
1
2
3

# 4、线程中断

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

# 4.1、Thread的中断

Thread 中提供了中断相关的方法:

方法 说明
void interrupt() 中断线程,即设置线程的中断状态为true。
boolean isInterrupted() 判断线程是否被中断,如果线程被中断,返回true;否则,返回false。
static boolean interrupted() 判断当前线程是否被中断,如果线程被中断,返回true并清除中断状态;否则,返回false。

  • 当想中断某个线程时,可以调用那个线程的interrupt()方法,此时那个线程的中断标志位会被设置为true。被中断的线程可以通过isInterrupted()来检查自身是否被中断。如果需要清除中断标志位,可以使用interrupted()方法,它会返回当前的中断状态,并立即清除该状态(设为false)。
  • 通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

InterruptedException案例

在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。

public class InterruptExample {
    private static class MyThread1 extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(2000);
                System.out.println("Thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new MyThread1();
        thread1.start();
        thread1.interrupt();
        System.out.println("Main run");
    }
}

// 异常结果:java.lang.InterruptedException: sleep interrupted
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

interrupt案例

public class InterruptExample {

    private static class MyThread2 extends Thread {
        @Override
        public void run() {
            while (!interrupted()) {
                // ..
            }
            System.out.println("Thread end");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread2 = new MyThread2();
        thread2.start();
        thread2.interrupt();
    }
}

// 结果:Thread end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 4.2、Executor的中断

调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Thread run");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    executorService.shutdownNow();
    System.out.println("Main run");
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。

Future<?> future = executorService.submit(() -> {
    // ..
});
future.cancel(true);
1
2
3
4

# 5、互斥同步

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

# 5.1、synchronized

synchronized同步范围和位置有关:同步代码块、同步方法、同步类、同步静态方法。

# 5.1.1、同步代码块

同步代码块只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedExample e1 = new SynchronizedExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> e1.func1());
        executorService.execute(() -> e1.func1());
    }
}

// 输出结果:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}

// 输出结果:0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
1
2
3
4
5
6
7
8
9

# 5.1.2、同步方法

效果同同步代码块一样。

public synchronized void func () {
    // ...
}
1
2
3

# 5.1.3、同步类

作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedExample e1 = new SynchronizedExample();
        SynchronizedExample e2 = new SynchronizedExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> e1.func2());
        executorService.execute(() -> e2.func2());
    }
}

// 输出结果:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 5.1.4、同步静态方法

效果同同步类,作用于整个类。

public synchronized static void fun() {
    // ...
}
1
2
3

# 5.2、ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

public class LockExample {

    private Lock lock = new ReentrantLock();

    public void func() {
        lock.lock();
        try {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        } finally {
            lock.unlock(); // 确保释放锁,从而避免发生死锁。
        }
    }

    public static void main(String[] args) {
        LockExample lockExample = new LockExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> lockExample.func());
        executorService.execute(() -> lockExample.func());
    }
}

// 输出结果:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 5.3、synchronized、ReentrantLock对比

  • 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  • 性能:新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
  • 等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。
  • 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
  • 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象。

# 5.4、互斥锁选择

使用synchronized是首选方法,除非在需要ReentrantLock的高级功能。

synchronized是由JVM直接支持并实现的锁机制,而不是所有的JDK版本都支持ReentrantLock。此外,使用synchronized没必要担心锁未被释放而引起的死锁,因为JVM会自动确保锁被正确释放。ReentrantLock需要手动释放锁,所以需要确保在finally块中释放锁,否则容易造成线程死锁。

# 6、线程间协作

# 6.1、join()

join()方法是Thread类中的一个方法,用于等待一个线程的完成。当一个线程在另一个线程上调用join()方法时,调用线程会被阻塞,直到被调用线程执行完毕。

案例1

在thread1和thread2执行完毕之后再执行下面打印的代码

Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());

thread1.start();
thread2.start();

// 在thread1和thread2执行完毕之后再执行下面的代码
thread1.join();
thread2.join();

System.out.println("线程执行完毕");
1
2
3
4
5
6
7
8
9
10
11

案例2

等待thread1和thread2执行完毕,最多等待5秒

Thread thread1 = new Thread(new MyRunnable());
Thread thread2 = new Thread(new MyRunnable());

thread1.start();
thread2.start();

// 等待thread1和thread2执行完毕,最多等待5秒
thread1.join(5000);
thread2.join(5000);

System.out.println("线程执行完毕");
1
2
3
4
5
6
7
8
9
10
11

案例3

虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。

public class JoinExample {

    private class A extends Thread {
        @Override
        public void run() {
            System.out.println("A");
        }
    }

    private class B extends Thread {

        private A a;

        B(A a) {
            this.a = a;
        }

        @Override
        public void run() {
            try {
                a.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("B");
        }
    }

    public void test() {
        A a = new A();
        B b = new B(a);
        b.start();
        a.start();
    }

    public static void main(String[] args) {
        JoinExample example = new JoinExample();
        example.test();
    }
}

// 输出结果:A  B
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
41
42

# 6.2、wait() notify() notifyAll()

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。

  • wait() notify() notifyAll()都属于 Object 的一部分,而不属于 Thread。
  • 只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。
  • 使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
public class WaitNotifyExample {
    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        WaitNotifyExample example = new WaitNotifyExample();
        executorService.execute(() -> example.after());
        executorService.execute(() -> example.before());
    }
}

// 输出结果:before  after
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

wait() 和 sleep() 的区别?

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

# 6.3、await() signal() signalAll()

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        AwaitSignalExample example = new AwaitSignalExample();
        executorService.execute(() -> example.after());
        executorService.execute(() -> example.before());
    }
}

// 输出结果:before  after
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

# 7、Thread方法概览

以下是Thread常用方法:

方法 说明
start() 启动线程,并使其处于可执行状态。
run() 线程的执行体,定义了线程要执行的操作。
sleep(long millis) 让当前线程暂停执行指定的毫秒数。
yield() 暂停当前正在执行的线程,让其他线程有机会继续执行。
join() 等待其他线程终止。
interrupt() 中断线程。
isInterrupted() 判断线程是否被中断。
isAlive() 判断线程是否还存活。
setName(String name) 设置线程的名称。
getName() 获取线程的名称。