当前位置: 首页 > article >正文

【Java 多线程基础 - 上篇】

Java 多线程基础 - 上篇

多线程编程是现代软件开发中不可或缺的一部分,尤其是在需要处理大量数据或进行 I/O 密集型操作时,合理使用多线程可以显著提高程序的效率和响应性。本文将深入解析 Java 中的多线程概念、创建线程的方法、以及多线程的优势。

1. 认识线程(Thread)

1.1 线程的概念

线程是程序执行的最小单位。一个线程就是一个"执行流",程序中的每一条执行路径都可以看作一个线程。多个线程可以在同一时刻并发执行各自的任务,从而提高程序的效率和响应性。

为了帮助理解线程,我们可以用一个银行业务办理的例子来说明。假设某公司需要去银行办理业务,包括财务转账、福利发放和社保缴纳等。若只有一个会计(张三)来处理这些业务,那么他需要排队等待,并且需要花费大量时间完成所有任务。为了提高效率,张三可以找两位同事(李四和王五)一起来帮忙。每个人负责一项任务,最终三个人共同完成任务,这样就大大减少了等待时间。

这里的张三、李四、王五就可以看作是多个线程。一个大的任务被分解成了多个小任务,每个线程分别负责执行其中的一部分任务。这种方式被称为多线程

1) 为什么要使用线程
  1. 并发编程的需求:多核 CPU 的普及使得并发编程成为了提高性能的必然需求。多个线程可以同时在不同的 CPU 核心上执行任务,提升整体计算能力。
  2. 提高资源利用率:有些任务,如等待文件 I/O 或数据库操作,可能会导致 CPU 处于空闲状态。通过使用线程,可以在等待期间执行其他任务,从而提升资源利用率。
  3. 线程比进程更轻量:线程创建和销毁的开销远小于进程。线程之间的上下文切换也更高效。
2) 进程与线程的区别
  • 进程:进程是操作系统分配资源的最小单位,每个进程有独立的内存空间。进程之间是相互隔离的。
  • 线程:线程是操作系统调度的最小单位。一个进程内的多个线程共享内存空间。线程之间的通信和共享数据比进程更高效。

线程和进程的主要区别在于,进程之间不共享内存空间,而线程共享内存空间

进程
线程
共享内存
3) Java中的线程与操作系统线程的关系

在 Java 中,线程的实现是通过对操作系统线程的封装来完成的。Java 提供了 Thread 类,它封装了操作系统提供的线程 API,使得我们可以通过该类来创建、管理和控制线程。每个 Java 线程对应操作系统中的一个线程,JVM 会管理这些线程的调度。

1.2 创建线程

Java 中有多种方法来创建线程,常见的两种方式是:继承 Thread 类和实现 Runnable 接口。

1) 继承 Thread

通过继承 Thread 类,我们可以重写其 run() 方法,并调用 start() 启动线程。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程正在执行");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();  // 启动线程
    }
}
2) 实现 Runnable 接口

另一种创建线程的方式是实现 Runnable 接口。实现 Runnable 接口后,我们可以将其传递给 Thread 构造函数。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程正在执行");
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t = new Thread(myRunnable);
        t.start();  // 启动线程
    }
}
3) 匿名内部类与 Lambda 表达式

Java 还允许使用匿名内部类和 Lambda 表达式来创建线程。下面是两种方式的示例:

// 使用匿名内部类
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建线程");
    }
};

// 使用 Lambda 表达式
Thread t2 = new Thread(() -> System.out.println("使用 Lambda 表达式创建线程"));

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

1.3 多线程的优势

多线程的最大优势之一是能够在多个 CPU 核心上并行执行任务,从而加快计算速度。下面我们通过一个简单的例子来对比并行执行与串行执行的性能。

public class ThreadAdvantage {
    private static final long count = 10_0000_0000;

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();
        
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < count; i++) {
                a--;
            }
        });
        thread.start();

        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        thread.join();  // 等待线程执行完毕
        
        long end = System.nanoTime();
        double ms = (end - begin) / 1000.0 / 1000.0;
        System.out.printf("并发: %f 毫秒%n", ms);
    }

    private static void serial() {
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a--;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long end = System.nanoTime();
        double ms = (end - begin) / 1000.0 / 1000.0;
        System.out.printf("串行: %f 毫秒%n", ms);
    }
}

在该示例中,concurrency() 使用了多线程执行,而 serial() 在主线程中串行执行。通过对比执行时间,我们可以看到,使用多线程能够有效地减少任务执行时间。

1.4 线程的生命周期

线程在执行过程中会经历不同的状态,包括新建、就绪、运行、阻塞和死亡状态。我们可以通过以下状态图来理解线程的生命周期:

start()
CPU 时间片
等待资源
获得资源
完成任务
New
Runnable
Running
Blocked

2. Thread 类及常见方法

在 Java 中,Thread 类是一个非常重要的类,它用于管理和调度线程。每个线程都有一个唯一的 Thread 对象与之关联,而 JVM 会用这些 Thread 对象来管理所有的线程调度和状态。通过了解 Thread 类,我们可以更好地控制线程的生命周期、状态、以及与其他线程的交互。

2.1 Thread 的常见构造方法

Thread 类有多个构造方法,我们可以通过这些方法来创建线程对象并指定线程执行的任务。下面是几种常见的线程构造方法:

Thread t1 = new Thread(); // 默认构造方法
Thread t2 = new Thread(new MyRunnable()); // 通过传入Runnable对象创建线程
Thread t3 = new Thread("自定义线程名称"); // 使用自定义名称创建线程
Thread t4 = new Thread(new MyRunnable(), "自定义线程名称"); // 同时传入Runnable和名称

这几种构造方法可以创建不同的线程,并为线程提供任务执行内容以及线程名称。使用线程时,线程并不会立即启动,必须通过调用 start() 方法才能真正开始执行。

2.2 Thread 的几个常见属性

Thread 类有一些常见的属性和方法,可以帮助我们获取和控制线程的状态:

  • ID:线程的唯一标识,通过 getId() 方法获取。
  • 名称:线程的名称,用于调试时识别线程,通过 getName() 获取。
  • 状态:线程当前的状态(如新建、就绪、运行、阻塞等),通过 getState() 获取。
  • 优先级:线程的优先级,影响线程的调度优先级,通过 getPriority() 获取。
  • 是否后台线程:通过 isDaemon() 方法判断线程是否为后台线程。后台线程在所有前台线程结束后会自动退出。
  • 是否存活:判断线程是否还在运行,通过 isAlive() 获取。
  • 是否被中断:通过 isInterrupted() 判断线程是否被中断。

示例

public class ThreadDemo {
    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(1000);  // 让线程休眠1秒
                } 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());
        
        thread.start();  // 启动线程
        
        while (thread.isAlive()) {}  // 等待线程结束
        System.out.println(Thread.currentThread().getName() + ": 状态: " + thread.getState());
    }
}

2.3 启动一个线程 - start()

在我们创建了一个 Thread 对象后,线程并不会自动执行。为了启动线程,我们需要调用 start() 方法。start() 方法会让线程进入就绪状态,并且 JVM 会为其分配时间片,在合适的时机执行线程中的 run() 方法。

示例

Thread thread = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
        System.out.println(Thread.currentThread().getName() + ": 正在工作...");
    }
});
thread.start();  // 启动线程

调用 start() 方法后,线程会在后台执行,且其执行逻辑由 run() 方法定义。值得注意的是,不能直接调用 run() 方法,否则它将作为普通的方法被执行,而不会开启新的线程。

2.4 中断一个线程

有时我们需要中断一个正在执行的线程。线程的中断是一个标记位,它并不强制线程立即停止,而是告知线程它应该停止。

我们可以通过以下两种方式来中断线程:

  1. 使用共享标志位:通过设置一个标志位来控制线程的停止。
  2. 调用 interrupt() 方法:调用 interrupt() 方法通知线程中断,线程会抛出 InterruptedException,然后可以根据需要决定是否终止执行。

示例 1:使用共享标志位

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        public volatile boolean isQuit = false;  // 共享标志位
        
        @Override
        public void run() {
            while (!isQuit) {
                System.out.println(Thread.currentThread().getName() + ": 正在工作...");
                try {
                    Thread.sleep(1000);  // 模拟线程工作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 任务已终止");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "线程A");
        thread.start();  // 启动线程
        
        Thread.sleep(5000);  // 模拟5秒后中断线程
        System.out.println("通知线程停止工作");
        target.isQuit = true;  // 设置标志位
    }
}

示例 2:使用 interrupt() 方法

public class ThreadDemo {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (!Thread.interrupted()) {
                System.out.println(Thread.currentThread().getName() + ": 正在工作...");
                try {
                    Thread.sleep(1000);  // 模拟线程工作
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName() + ": 被中断了!");
                    break;  // 线程中断后跳出循环
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 任务已终止");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "线程B");
        thread.start();  // 启动线程
        
        Thread.sleep(5000);  // 模拟5秒后中断线程
        System.out.println("通知线程停止工作");
        thread.interrupt();  // 中断线程
    }
}

2.5 等待一个线程 - join()

有时我们需要等待一个线程执行完毕后再继续执行当前线程的操作。此时可以使用 join() 方法。join() 方法会使当前线程等待,直到调用该方法的线程执行完毕。

join() 方法有几个重载版本,支持设置最大等待时间:

public void join() throws InterruptedException;  // 无限期等待
public void join(long millis) throws InterruptedException;  // 等待指定时间(毫秒)
public void join(long millis, int nanos) throws InterruptedException;  // 高精度等待

示例

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable target = () -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我结束了!");
        };
        
        Thread thread1 = new Thread(target, "线程A");
        Thread thread2 = new Thread(target, "线程B");
        
        thread1.start();
        thread1.join();  // 等待线程A完成
        thread2.start();
        thread2.join();  // 等待线程B完成
        
        System.out.println("所有线程完成,主线程继续执行");
    }
}

2.6 获取当前线程引用 - currentThread()

Thread 类提供了 currentThread() 方法来获取当前正在执行的线程对象引用。通过这个方法,我们可以获取当前线程的信息。

Thread current = Thread.currentThread();
System.out.println("当前线程名称:" + current.getName());

2.7 休眠当前线程 - sleep()

sleep() 方法让当前线程进入休眠状态,休眠时间结束后,线程会自动重新进入就绪状态。需要注意,线程的调度是不可控的,因此实际休眠的时间可能大于等于我们设置的时间。

Thread.sleep(1000);  // 使当前线程休眠1000毫秒

通过合理使用 Thread 类的各种方法

,我们可以有效地管理线程的执行,确保多线程程序的高效和稳定运行。

2.8 线程的状态及线程安全详解

在 Java 中,线程的生命周期受线程状态的影响。线程的状态是由 Thread.State 枚举类定义的,并且线程的状态会随着线程的执行过程而发生变化。了解这些状态及其转移是多线程编程中的重要基础,同时,掌握如何处理线程安全问题也至关重要。在这篇文章中,我们将深入探讨线程的各种状态及其转移,并解决线程不安全的问题。


3. 线程的状态

线程在生命周期中会经历多个不同的状态。Thread.State 枚举类为我们提供了以下几种状态来描述线程的运行状态:

3.1 线程的所有状态

Thread.State 枚举包含以下几种状态:

  • NEW:线程已被创建,但尚未开始执行。此时,线程的 start() 方法还未被调用。
  • RUNNABLE:线程已开始执行,且处于可以被操作系统调度的状态。请注意,这个状态不一定表示线程正在运行,而是表示线程可以被调度运行。
  • BLOCKED:线程正在等待获取锁。通常发生在多个线程竞争同一资源时,线程会被阻塞,直到能够获取到锁。
  • WAITING:线程正在等待其他线程的通知或者事件的发生。线程在此状态下不会占用 CPU 时间,直到条件满足为止。
  • TIMED_WAITING:线程在等待某个事件的发生,但它的等待是有时间限制的。线程通过 Thread.sleep(time)Object.wait(time) 等方法进入该状态。
  • TERMINATED:线程执行完成或由于异常终止时进入该状态。线程的 run() 方法执行完毕或者线程被强制终止后,线程进入此状态。

3.2 线程状态与状态转移

线程的状态是多线程程序中不可忽视的一部分,它描述了线程生命周期中的不同阶段,并且线程会根据不同的条件在这些状态之间进行转移。

线程状态的转移通常会发生在以下几种情况:

  1. NEW → RUNNABLE:线程调用了 start() 方法后,线程进入 RUNNABLE 状态,表示线程已准备好执行。
  2. RUNNABLE → BLOCKED:线程尝试获取已经被锁定的资源时,会进入 BLOCKED 状态。
  3. RUNNABLE → WAITING/TIMED_WAITING:线程调用了 wait()join() 等方法后,会进入 WAITINGTIMED_WAITING 状态。
  4. RUNNABLE → TERMINATED:线程的 run() 方法执行完毕,线程进入 TERMINATED 状态,表示线程已经结束。

线程的状态转移有助于我们理解程序在多线程环境下的行为。

start()
trying to acquire lock
wait(), join()
sleep(), wait(time), join(time)
run() ends
notify(), notifyAll()
timeout
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED

3.3 观察线程的状态与转移

我们可以通过代码监控线程的状态变化,理解不同状态之间的转移。

观察 1: 关注 NEW、RUNNABLE、TERMINATED 状态的转换

可以使用 Thread.isAlive() 方法来判断一个线程是否处于非 NEWTERMINATED 状态。

示例代码:

public class ThreadStateTransfer {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 10000000; i++) {
                // 模拟任务
            }
        }, "李四");

        System.out.println(t.getName() + ": " + t.getState());  // 输出 NEW
        t.start();

        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());  // 输出 RUNNABLE 状态
        }

        System.out.println(t.getName() + ": " + t.getState());  // 输出 TERMINATED
    }
}

观察 2: 关注 WAITING、BLOCKED、TIMED_WAITING 状态的转换

这些状态通常与线程的锁和等待机制有关。我们可以通过 wait()sleep() 方法进入 WAITINGTIMED_WAITING 状态,且 BLOCKED 状态通常发生在多个线程竞争同一个资源时。

示例代码:

public class ThreadStateTransfer {
    public static void main(String[] args) throws InterruptedException {
        final Object object = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    try {
                        Thread.sleep(1000);  // 模拟线程在等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("t2 is working");
                }
            }
        }, "t2");

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

在上面的例子中,t1 进入 TIMED_WAITING 状态,而 t2 因为 t1 持有锁,进入 BLOCKED 状态。

观察 3: yield() 方法

yield() 方法会让当前线程放弃 CPU 使用权,并使当前线程重新进入就绪队列。yield() 不会改变线程的状态,只是影响线程的调度顺序。

示例代码:

public class YieldExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("张三");
                Thread.yield();  // 让出 CPU
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("李四");
            }
        }, "t2");

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

通过使用 yield(),可以观察到张三和李四的输出顺序发生变化,说明线程的调度顺序可能会有所调整。


4. 多线程带来的风险——线程安全

4.1 观察线程不安全

多线程环境下,多个线程同时操作共享资源时,往往会出现线程安全问题。以计数器为例,当多个线程同时操作同一个共享变量时,如果没有同步机制,结果可能不一致。

示例代码:

static class Counter {
    public int count = 0;
    void increase() {
        count++;  // 非原子操作,可能会引发线程安全问题
    }
}

public class ThreadUnsafeExample {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

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

        t1.join();
        t2.join();
        System.out.println(counter.count);  // 可能不是预期的 100000
    }
}

由于没有同步机制,count++ 操作实际上是非原子的,可能导致多个线程同时修改 count,最终结果并不符合预期。

4.2 线程安全的概念

线程安全的程序能够在多线程环境下按预期工作,不会产生不一致的结果。简单来说,如果在单线程环境下代码能够正确运行,那么在多线程环境下代码也应当能够正确运行。

4.3 线程不安全的原因

线程不安全通常发生在共享数据被多个线程同时修改时,造成了数据竞争。具体原因包括:

  1. 原子性:操作不是原子性的,比如 count++,它实际上是由多个步骤组成的。
  2. 可见性:线程对共享数据的修改可能没有及时更新到其他线程的工作内存中。
  3. 指令重排序:编译器或 CPU 可能会对指令进行重排序,导致线程执行的顺序与预期不符。

4.4 解决线程不安全问题

可以通过使用 synchronized 来保证线程安全,确保共享资源的操作在同一时刻只有一个线程能够执行。

示例代码:

static class Counter {
    public int count = 0;

    synchronized void increase() {  // 使用 synchronized 保证线程安全
        count++;
    }
}

public class ThreadSafeExample {
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });

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

        t1.join();
        t2.join();
        System.out.println(counter.count);  // 现在的结果是预期的 100000
    }
}

通过给 increase() 方法加上 synchronized 关键字,我们确保了每次只有一个线程能够访问该方法,从而避免了线程安全问题。这样就保证了共享变量 count 的原子性操作,最终输出的结果是预期的 100000


http://www.kler.cn/a/561716.html

相关文章:

  • STM32MP157A-FSMP1A单片机移植Linux系统SPI总线驱动
  • 【设计师专属】智能屏幕取色器Pro|RGB/HEX双模式|快捷键秒存|支持导出文档|C++ QT
  • C++的三种对象模型
  • vmware系统磁盘扩容
  • 【Java项目】基于Spring Boot的交流互动系统
  • 代码随想录|01背包理论基础,01背包之滚动数组,416.分割等和子集
  • Python入门12:面向对象的三大特征与高级特性详解
  • SOME/IP-SD -- 协议英文原文讲解3
  • WPS中如何对word表格中数据进行排序,就像Excel排序那样简单
  • MVC MVP MVVM架构 在Android中的体现
  • javascript-es6 (五)
  • pytest下allure
  • MySQL索引失效
  • ROS的action通信——实现阶乘运算(三)
  • openstack ironic ipa 以及用户镜像制作
  • 企业如何通过云计算提高数据的可访问性
  • Siddon算法中对参数值α的解释
  • java给钉钉邮箱发送邮件
  • RK3399 Android7双WiFi功能实现
  • 灵犀互娱游戏测试开发一面面经