Cursor blinking

Java 线程

Java 基础|Java线程|字数 3,433|阅读时长≈ 9 分钟

前言

进程与线程

进程和线程是多任务处理的两个重要概念:

  • 进程是操作系统分配资源的基本单位,每个进程有独立的内存空间。进程之间通常是相互独立的,彼此不会直接共享内存空间。
  • 线程是进行处理器资源调度的最基本单位,一个运行中的进程可以包含多个线程。线程共享进程的资源,如内存空间、文件句柄等。Java 中的线程是轻量级的执行单元,可以并发执行,实现多任务处理和提高程序性能。

CPU 线程和操作系统线程

在计算机领域中,CPU 线程和操作系统线程是两个不同的概念,它们之间有一些重要的区别:

  • CPU 线程
  • CPU 线程通常指的是物理处理器上的硬件线程,也称为硬件线程或逻辑核心。现代的多核处理器通常支持超线程技术,一个物理核心可以模拟出多个逻辑核心,每个逻辑核心就是一个 CPU 线程。
  • CPU 线程可以并行执行指令,提高处理器的利用率,使多个线程可以同时运行在不同的逻辑核心上,从而实现并行计算。
  • 操作系统线程
  • 操作系统线程是操作系统中用于管理程序执行的基本单位,也称为软件线程或用户线程。操作系统线程由操作系统调度和管理,用于执行程序中的代码块。
  • 操作系统线程是程序中的执行流,具有独立的堆栈、寄存器和状态。操作系统线程可以被操作系统调度到不同的 CPU 核心上执行,并且可以实现并发执行。

CPU 线程和操作系统线程区别

  • 实体
  • CPU 线程是处理器的硬件实体,用于执行指令并处理计算任务。
  • 操作系统线程是操作系统的软件实体,用于管理程序的执行流和资源分配。
  • 调度
  • CPU 线程是由处理器硬件进行调度和执行的,处理器可以同时执行多个 CPU 线程。
  • 操作系统线程是由操作系统调度器进行管理和调度的,操作系统根据调度算法决定哪个线程在哪个 CPU 核心上执行。
  • 关联
  • 一个操作系统线程可以关联到一个或多个 CPU 线程上执行,这取决于操作系统的线程调度策略和硬件支持。

Java 中的线程

创建线程

在 Java 中,创建线程通常有两种主要的方式:继承 Thread 类和实现 Runnable 接口。以下是这两种方法的示例:

  1. 通过继承 Thread 类创建线程
Code
class MyThread extends Thread {    public void run() {        System.out.println("MyThread is running");    }} public class Main {    public static void main(String[] args) {        MyThread myThread = new MyThread();        myThread.start(); // 启动线程    }}
  1. 通过实现 Runnable 接口创建线程
Code
class MyRunnable implements Runnable {    public void run() {        System.out.println("MyRunnable is running");    }} public class Main {    public static void main(String[] args) {        MyRunnable myRunnable = new MyRunnable();        Thread thread = new Thread(myRunnable);        thread.start(); // 启动线程    }}

线程的生命周期

Java线程有多个状态,包括新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed Waiting)和终止(Terminated)等状态。

未命名文件 (1).png
未命名文件 (1).png

简化生命周期的描述可以把阻塞状态,限期等待状态,无限期等待状态合称为阻塞状态。

  1. 新建状态(New)

当创建一个线程对象时,线程处于新建状态。此时线程对象已经被创建,但还没有调用 start() 方法启动线程。

  1. 就绪状态(Runnable)

当调用线程对象的 start() 方法后,线程进入就绪状态。此时线程已经准备好运行,等待获取 CPU 时间片。

  1. 运行状态(Running)

当线程获取到 CPU 时间片并开始执行时,线程处于运行状态。线程会执行 run() 方法中的代码。

  1. 阻塞状态(Blocked)

线程可能会进入阻塞状态,等待某些条件满足。线程在阻塞状态下不会消耗 CPU 时间片。线程可以进入以下几种阻塞状态:

  • 等待阻塞:线程调用 wait() 方法进入等待状态。
  • 同步阻塞:线程在获取同步锁失败时进入阻塞状态。
  • 其他阻塞:线程调用 sleep()、join() 方法或者发生 I/O 阻塞时进入阻塞状态。
  1. 无限期等待状态(Waiting)

线程进入无限期等待状态的几种情况:

  • 调用 Object.wait() 方法,线程进入等待状态,直到其他线程调用 notify() 或 notifyAll() 方法唤醒它。
  • 调用不带超时参数的 Thread.join() 方法。
  • 调用 LockSupport.park() 方法。
  1. 限期等待状态(Timed Waiting)

线程进入限期等待状态的几种情况:

  • 调用带有超时参数的 Thread.sleep() 方法。
  • 调用带有超时参数的 Object.wait() 方法。
  • 调用带有超时参数的 Thread.join() 方法。
  • 调用 LockSupport.parkNanos() 或 LockSupport.parkUntil() 方法。
  1. 死亡 (Dead)

线程执行完 run() 方法后或者调用 Thread 类的 stop() 方法终止线程,线程处死亡状态。

通过理解和控制线程的生命周期,可以更好地设计和管理多线程程序,确保线程的正确执行和资源的合理利用。

Thread.sleep() vs Object.wait()

Thread.sleep() 和 Object.wait() 是 Java 中用于线程控制的两个方法,它们之间有一些重要的区别:

  1. Thread.sleep()
  • Thread.sleep() 方法是 Thread 类的静态方法,用于使当前线程暂停执行一段时间(以毫秒为单位)。
  • 在调用 Thread.sleep() 时,当前线程会暂停执行指定的时间,但不会释放对象锁。
  • Thread.sleep() 方法是静态的,可以通过 Thread.sleep() 方法让当前线程休眠,而不需要获取任何对象的锁。
  1. Object.wait()
  • Object.wait() 方法是 Object 类的实例方法,用于使当前线程进入等待状态,同时释放对象的锁。
  • 在调用 Object.wait() 时,当前线程会释放对象锁,并进入等待状态,直到其他线程调用相同对象的 notify() 或 notifyAll() 方法唤醒它。
  • Object.wait() 方法必须在同步代码块或同步方法中调用,否则会抛出 IllegalMonitorStateException 异常。

主要区别总结:

  • Thread.sleep() 是 Thread 类的静态方法,用于线程休眠一段时间,不会释放对象锁。
  • Object.wait() 是 Object 类的实例方法,用于线程等待,会释放对象锁并进入等待状态,需要在同步代码块或同步方法中调用。

线程调度

在操作系统中,线程调度可以分为两种主要类型:协同式调度(Cooperative Scheduling)和抢占式调度(Preemptive Scheduling)。这两种调度方式在多线程环境下会对线程的执行顺序和时间片分配产生影响。

  • 协同式调度(Cooperative Scheduling):

在协同式调度中,线程自愿放弃 CPU 控制权。换句话说,线程必须显式地让出 CPU,才能让其他线程有机会执行。如果一个线程陷入无限循环或长时间运行,其他线程将无法执行,整个程序可能会因此而假死。

  • 抢占式调度(Preemptive Scheduling):

在抢占式调度中,操作系统具有控制权,可以在任何时候中断当前正在执行的线程,并将 CPU 分配给其他线程。这种方式下,操作系统可以根据线程的优先级、时间片等因素来动态调整线程的执行顺序,以确保系统的公平性和响应性。

Java 中的线程调度方式

在 Java 中,线程调度是由 JVM 和操作系统共同管理的,他们决定了哪个线程在什么时候运行。Java 线程调度采用抢占式调度方式,操作系统会根据线程的优先级和其他因素来决定线程的执行顺序。Java 提供了一些方法来帮助开发者控制线程的执行,如设置线程优先级、线程睡眠、线程等待、线程同步等。

控制线程的执行

  1. 线程优先级(Thread Priority)

Java 中的线程有优先级,优先级较高的线程会获得更多的 CPU 时间片。线程的优先级范围是从 Thread.MIN_PRIORITY(最低优先级,值为1)到 Thread.MAX_PRIORITY(最高优先级,值为10)。可以使用以下方法设置线程的优先级:

Code
Thread thread = new Thread();thread.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级为最高 
  1. 线程睡眠(Thread Sleep)

可以使用 Thread.sleep() 方法让线程暂停执行一段时间,以便让其他线程有机会执行或者实现定时执行的功能:

Code
try {    Thread.sleep(1000); // 线程睡眠1秒} catch (InterruptedException e) {    e.printStackTrace();} 
  1. 线程等待(Thread Wait)

线程可以调用 wait() 方法使自己进入等待状态,直到其他线程调用 notify() 或 notifyAll() 方法唤醒它。这通常与线程同步结合使用。

  1. 线程同步(Thread Synchronization)

Java 提供了关键字 synchronized 和 wait()、notify()、notifyAll() 方法来实现线程同步,确保多个线程之间的协调和互斥访问共享资源。

Code
synchronized (object) {    // 临界区代码}
  1. 线程 Join(Thread Join)

可以使用 join() 方法让一个线程等待另一个线程执行完毕后再继续执行。

Code
Thread thread = new Thread();thread.start();try {    thread.join(); // 等待线程执行完毕} catch (InterruptedException e) {    e.printStackTrace();} 

通过合理地使用这些线程调度机制,可以控制线程的执行顺序、优先级和互斥访问共享资源,从而实现多线程程序的正确、高效执行。

线程同步

在多线程环境下,可能会出现竞态条件(Race Condition)和资源争夺的情况。Java 提供了synchronized 关键字、Lock 接口、volatile 关键字等机制来实现线程同步。

volatile

volatile 是 Java 中的一个关键字,用于修饰变量,volatile 是 Java 虚拟机提供的最轻量级的同步机制。

  1. 保证此变量对所有线程的可见性,每次使用之前都要先刷新,执行引擎看不到不一致的情况。
  1. 禁止指令重排序,volatile 修饰的变量赋值后,执行“lock addl ”操作,相当于一个内存屏障,不能把后面的指令重排序到内存屏障之前的位置。

synchronized

synchronized 是 Java 中的关键字,用于实现同步代码块或同步方法。通过 synchronized 关键字,可以保证同一时刻只有一个线程可以访问被同步的代码块或方法。

synchronized 是基于对象锁实现的,每个对象都有一个内置的锁,当一个线程进入同步代码块时会尝试获取对象的锁,其他线程必须等待锁的释放才能进入同步代码块。

“一个变量在同一时刻只允许一条线程对其进行 lock 操作”,这个规则决定了持有同一个锁的两个同步块只能串行地进入 synchronized 关键字经过javac 编译之后,会在同步块的前后分别形成 monitorenter 和 monitorexit 两个字节码指令,这两个指令都需要一个 reference 类型的参数来指明需要锁定和解锁的对象。

Lock

  • Lock 是 Java 中的接口,用于实现更灵活的线程同步机制。与 synchronized 相比,Lock 提供了更多的功能,如尝试获取锁、超时获取锁、可中断获取锁等。
  • Lock 接口的常用实现类是 ReentrantLock,它提供了与 synchronized 类似的功能,但更加灵活。
  • 使用 Lock 接口可以更精细地控制线程的同步,避免死锁等问题,并且可以方便地实现读写分离锁等高级锁机制。

volatile vs synchronized

  • volatile 变量读操作性能消耗与普通变量几乎无差别,写操作慢一些。
  • synchronized 对锁消除或优化,不一定比 volatile 慢。
  • synchronized、final 也可实现可见性

线程通信

Java提供了wait()、notify()、notifyAll()等方法来实现线程之间的通信,可以用于线程间的协作和同步。

下面通过一个示例演示线程之间的通信:

Code
    /*     * 资源     */    class Resource {        private int orderNum = 0;        // 定义标记        private boolean flag = false;         public synchronized void produce(String name) {            if (flag) {                // 线程等待                try {		                // wait 方法必须在同步代码块或同步方法中调用                    wait();                } catch (Exception e) {                    e.printStackTrace();                }            } else {                // 将标记改为true                flag = true;                // 输出生产了哪个商品                System.out.println(Thread.currentThread().getName() + "..生产者 生产.." + (name + ++orderNum));                // 唤醒消费者                notify();            }        }         public synchronized void consume(String name) {            if (!flag) {                // 线程等待                try {                    wait();                } catch (Exception e) {                    e.printStackTrace();                }            } else {                System.out.println(Thread.currentThread().getName() + "..消费者 消费.." + (name + orderNum));                // 将标记改为false                flag = false;                // 唤醒生产者                notify();            }        }    }     /*     * 生产者     */    class Producer implements Runnable {        Resource r;         // 生产者一初始化就要有资源        public Producer(Resource r) {            this.r = r;        }         @Override        public void run() {            while (true) {                r.produce("面包");            }        }    }     /*     * 消费者     */    class Customer implements Runnable {        Resource r;         // 消费者一初始化就要有资源        public Customer(Resource r) {            this.r = r;        }         @Override        public void run() {            while (true) {                r.consume("面包");            }        }    }     @Test    public void test() {        // 创建资源对象        Resource r = new Resource();        // 创建线程任务        Producer p = new Producer(r);        Customer c = new Customer(r);        // 创建线程        Thread t1 = new Thread(p);        Thread t2 = new Thread(c);        // 启动线程        t1.start();        t2.start();         try {            Thread.sleep(100);        } catch (InterruptedException e) {            throw new RuntimeException(e);        }        t1.interrupt();        t2.interrupt();    }

方法释义:

  • wait():导致当前线程等待,直到被唤醒,通常是通过被通知或中断来唤醒
  • notify():唤醒正在该对象的监视器上等待的单个线程。如果有任何线程正在等待该对象,则选择唤醒其中一个线程。
  • notifyAll():唤醒在此对象监视器上等待的所有线程。线程通过调用 wait 方法之一来等待对象的监视器。
💡 为什么这些方法被定义在Object类中?

因为这些方法必须标识所属的锁,而锁可以是任意对象,任意对象都可以调用的方法必然是Object 类中的方法。

参考文档&资料

深入理解 JVM 虚拟机