Java并发 - 线程基础

发布时间:2024年01月06日

1. 程序 & 线程

  • 定义:

    • 程序(Process): 一个程序是一个独立的执行单元,它包含了代码、数据、和系统资源的集合。每个程序都运行在自己独立的内存空间中,互相之间不直接共享内存。

    • 线程(Thread): 线程是程序的执行流程,是操作系统调度的最小单元。一个程序可以包含多个线程,它们共享程序的内存空间和资源。

  • 资源分配:

    • 程序(Process): 每个程序有独立的内存空间和系统资源,包括文件句柄、网络连接等。程序之间通常是相互隔离的,彼此不直接影响。

    • 线程(Thread): 线程共享所属程序的内存空间和资源。多个线程之间可以通过共享内存来进行通信,但也因此需要注意线程安全性。

  • 切换开销:

    • 程序(Process): 进程切换的开销相对较大,因为需要切换整个内存空间。

    • 线程(Thread): 线程切换的开销相对较小,因为线程共享相同的内存空间,切换时只需要保存和恢复少量的上下文信息。

  • 并发性:

    • 程序(Process): 进程之间是相互独立的,通信需要使用进程间通信(IPC)机制,如管道、消息队列等。

    • 线程(Thread): 线程之间可以直接共享内存,因此通信相对容易,但也需要注意同步和线程安全问题。

  • 创建和销毁:

    • 程序(Process): 创建和销毁一个进程相对较慢,需要分配和释放大量的资源。

    • 线程(Thread): 创建和销毁一个线程相对较快,开销较小。

  • 故障隔离:

    • 程序(Process): 进程之间是相互独立的,一个进程的崩溃不会直接影响其他进程。

    • 线程(Thread): 线程之间共享相同的内存空间,一个线程的错误可能导致整个程序崩溃。

2. JAVA中线程的生命周期

  1. 新建状态(New): 线程对象被创建但尚未启动。
  2. 就绪状态(Runnable): 线程已经被启动,等待获取CPU时间片。
  3. 运行状态(Running): 线程获取了CPU时间片,正在执行任务。
  4. 阻塞状态(Blocked): 线程被阻塞,等待某个条件的解除。
  5. 等待状态(Waiting): 线程在等待另一个线程的通知或中断。
  6. 超时等待状态(Timed Waiting): 线程在等待一个具有超时时间的条件。
  7. 终止状态(Terminated): 线程执行完毕,结束生命周期。

3. 线程的创建方式

在Java中,线程的创建和使用有多种方式,主要包括继承Thread类、实现Runnable接口、使用Callable和Future接口、使用线程池等。下面分别介绍这几种方式,并对比它们的优缺点和特性。

3.1 继承Thread类(无返回值)
class MyThread extends Thread {
    public void run() {
        // 线程执行的代码
    }
}

// 创建并启动线程
MyThread myThread = new MyThread();
myThread.start();

优点:

  • 简单,继承Thread类即可。

缺点:

  • 由于Java是单继承的,继承Thread类后无法再继承其他类。
  • 不利于共享资源,因为多个线程共享Thread的实例变量。
3.2 实现Runnable接口(无返回值)
class MyRunnable implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

// 创建线程并启动
Thread myThread = new Thread(new MyRunnable());
myThread.start();

优点:

  • 支持多继承,因为实现Runnable接口后还可以继承其他类。
  • 适合多个线程共享同一个资源的情况。

缺点:

  • 相对于继承Thread,使用稍微复杂。
3.3 使用Callable和Future接口(有返回值)
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    public String call() {
        // 线程执行的代码
        return "Task completed";
    }
}

// 创建并启动线程
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread myThread = new Thread(futureTask);
myThread.start();

// 获取线程执行结果
String result = futureTask.get();

优点:

  • 具有返回值,可以通过FutureTask获取线程执行的结果。
  • 可以抛出异常,更灵活。

缺点:

  • 相对于Runnable,使用稍微复杂。
  • 不支持直接通过Thread的start方法启动,需要借助FutureTask。
3.4 使用线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    public void run() {
        // 线程执行的代码
    }
}

// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);

// 提交任务
executorService.submit(new MyTask());

优点:

  • 提高线程的复用性和效率,避免频繁创建和销毁线程。
  • 控制并发线程数量,防止线程过多导致资源浪费。

缺点:

  • 需要了解线程池的相关概念和API,相对于简单的线程创建方式更复杂。

4. 线程的种类

4.1 用户线程(User Thread)

用户线程是程序中创建的一般线程,当所有的用户线程执行完毕后,虚拟机就会停止,不会等待守护线程执行完毕。大部分情况下,我们创建的线程都是用户线程。

Thread userThread = new Thread(() -> {
    // 线程执行的代码
});
userThread.start();
4.2 守护线程(Daemon Thread)

守护线程是为其他线程提供服务的线程。当所有用户线程执行完毕后,守护线程会被强制停止,而不管守护线程是否执行完毕。典型的守护线程包括垃圾回收线程。

Thread daemonThread = new Thread(() -> {
    // 守护线程执行的代码
});
daemonThread.setDaemon(true);
daemonThread.start();

5. 线程的基础方法

方法作用
start()启动线程,使线程进入就绪状态,等待被调度执行。
run()线程的执行体,包含线程需要执行的代码。
join()等待线程执行完毕,使当前线程进入阻塞状态,直到被等待的线程执行完毕或等待超时。
sleep()让当前线程休眠一段时间,以毫秒为单位。
yield()让出CPU的使用权,使当前线程从运行状态进入就绪状态,让其他具有相同优先级的线程有机会执行
interrupt()中断线程,使线程进入中断状态。
isAlive()判断线程是否还活着(是否启动且未终止)。

以下列出基本用法。

Thread myThread = new Thread(() -> {
    // 线程执行的代码
});
// 启动线程
myThread.start();
// 不会启动新线程,直接在当前线程中执行run()方法
myThread.run();

// ====== join start ======
myThread.start();
try {
    // 等待myThread执行完毕
    myThread.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
// ====== join end ======

// ====== sleep start ======
try {
    Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
    e.printStackTrace();
}
// ====== sleep end ======

// 让出cpu 进入就绪状态
Thread.yield();

// ====== interrupt start ======
Thread myThread = new Thread(() -> {
    while (!Thread.interrupted()) {
        // 线程执行的代码
    }
});
myThread.interrupt(); // 中断myThread
// ====== interrupt end ======

boolean alive = myThread.isAlive(); // 判断线程是否还活着

面试题:sleep()yield() 两个方法的对照

  • 影响状态
    • sleep() 会让线程从运行状态进入阻塞状态,等待指定时间后重新进入就绪状态。
    • yield() 会让线程从运行状态进入就绪状态,但并不保证立即被调度执行。
  • 时间控制
  • sleep()` 是精确的时间控制,线程会在指定时间后被唤醒。
  • yield() 并不涉及具体的时间控制,只是告诉调度器当前线程可以让出 CPU。
  • 适用场景
    • sleep() 适用于需要固定时间的休眠,如实现定时任务。
    • yield() 适用于协调多个线程的执行,避免某个线程长时间占用 CPU。
  • 异常处理
    • sleep() 方法会抛出 InterruptedException 异常,因为线程在休眠期间可能被中断。
    • yield() 方法不会抛出异常。

6. 线程通信

方法作用
wait()wait() 方法使当前线程进入等待状态,并释放对象的锁。
notify()notify() 方法用于唤醒一个等待中的线程。它会选择其中一个等待线程,通知它可以继续执行。
notifyAll()notifyAll() 方法用于唤醒所有等待中的线程。它会通知所有因调用 wait() 方法而进入等待状态的线程,让它们都有机会竞争对象的锁。
await()当前线程等待,释放锁,并进入等待状态,直到被其他线程调用 signal()signalAll() 方法唤醒,或被中断。
signal()唤醒一个等待中的线程,使其从 await() 方法中返回。
signalAll()唤醒所有等待中的线程,使它们从 await() 方法中返回。

以下是wait() 和 notify()

public class SharedResource {
    private boolean flag = false;
    public synchronized void produce() throws InterruptedException {
        while (flag) {
            wait();
        }
        // 生产操作
        flag = true;
        System.out.println("生产!!!!");
        notify();
    }

    public synchronized void consume() throws InterruptedException {
        while (!flag) {
            wait();
        }
        // 消费操作
        flag = false;
        System.out.println("消费!!!!");
        notify();
    }
    
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();
        // Producer
        Thread producerThread = new Thread(() -> {
            try {
                while (true) {
                    sharedResource.produce();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // Consumer
        Thread consumerThread = new Thread(() -> {
            try {
                while (true) {
                    sharedResource.consume();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 启动线程
        producerThread.start();
        consumerThread.start();
    }
}

运行结果

Connected to the target VM, address: '127.0.0.1:59789', transport: 'socket'
生产!!!!
消费!!!!
生产!!!!
消费!!!!
生产!!!!
消费!!!!
生产!!!!
消费!!!!
生产!!!!
消费!!!!
......

Condition 接口是Java中用于线程协作的一部分,它通常与 Lock 接口一起使用,提供了更灵活的线程同步和通信机制。Condition 接口定义了线程等待和唤醒的操作,允许线程在特定的条件下等待或唤醒。

public class SharedResource {
    private boolean flag = false;
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void produce() throws InterruptedException {
        lock.lock();
        try {
            while (flag) {
                condition.await();
            }
            // 生产操作
            flag = true;
            System.out.println("生产操作");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public void consume() throws InterruptedException {
        lock.lock();
        try {
            while (!flag) {
                condition.await();
            }
            // 消费操作
            flag = false;
            System.out.println("消费操作");
            condition.signal();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();
        // Producer thread
        Thread producerThread = new Thread(() -> {
            try {
                while (true) {
                    sharedResource.produce();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        // Consumer thread
        Thread consumerThread = new Thread(() -> {
            try {
                while (true) {
                    sharedResource.consume();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // Start the threads
        producerThread.start();
        consumerThread.start();
    }
}

运行结果

Connected to the target VM, address: '127.0.0.1:49736', transport: 'socket'
生产操作
消费操作
生产操作
消费操作
生产操作
消费操作
生产操作
消费操作
生产操作
消费操作
......

面试题:wait() sleep()

  • 使用的类和位置
    • wait() 方法是 Object 类的方法,用于等待其他线程发出通知。它必须在同步块中(使用 synchronized 关键字)被调用。
    • sleep() 方法是 Thread 类的静态方法,用于使当前线程休眠指定的时间,不需要在同步块中调用。
  • 同步锁的释放
    • 在调用 wait() 方法时,当前线程会释放持有的锁,使其他线程有机会获取锁并执行。等待期间,线程处于阻塞状态,直到被其他线程调用 notify()notifyAll() 或超时唤醒。
    • 在调用 sleep() 方法时,线程保持锁定状态,不会释放锁。线程在指定的时间内休眠,然后自动唤醒,可以通过中断来提前唤醒。
  • 被唤醒的方式
    • 调用 wait() 方法的线程需要被其他线程调用相同对象上的 notify()notifyAll() 方法来唤醒,或者等待超时。
    • sleep() 方法则会在指定的时间过后自动唤醒,或者可以通过其他线程调用该线程的 interrupt() 方法来提前唤醒。
  • 抛出的异常
    • 调用 wait() 方法的线程可能抛出 InterruptedException 异常,需要处理中断异常。
    • 调用 sleep() 方法的线程也可能抛出 InterruptedException 异常,同样需要处理中断异常。
  • 使用场景
    • wait() 方法通常用于线程间的协作,等待某个条件的满足。
    • sleep() 方法通常用于线程的暂停,实现一定的时间延迟。
文章来源:https://blog.csdn.net/qq_43678225/article/details/135428266
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。