Java多线程基础(二)

164 阅读18分钟

概念图

java多线程(二).png

多线程安全问题

多线程安全问题是指在多线程环境下,多个线程同时访问共享资源(如变量、对象等)时,可能导致数据不一致、错误或异常等问题。这主要是由于线程之间的并发执行导致的资源竞争和执行顺序的不确定性。

  1. 数据竞争(Data Race) : 当两个或多个线程同时读写同一个变量而没有任何同步机制时,可能导致数据的状态不一致。例如,一个线程在更新一个共享变量,而另一个线程可能在读取这个变量,导致读取到的是一个中间状态的值。
  2. 不可重入(Reentrancy Issues) : 如果一个线程在执行某个操作时被中断并再进入同一个操作,可能导致不一致的状态。这个问题通常需要通过锁或其他同步手段来解决。
  3. 死锁(Deadlock) : 当两个或多个线程互相等待对方释放资源时,会导致所有相关的线程都处于阻塞状态,从而无法继续执行。死锁是一种常见的多线程安全问题,需要采取措施避免。
  4. 活锁(Livelock) : 活锁与死锁相似,但线程并没有被完全阻塞,而是因为不断地尝试执行某个操作而无法完成,导致线程之间持续相互影响,无法继续前进。
  5. 饥饿(Starvation) : 一些线程可能因为缺乏必要的资源而无法获得执行机会,从而长时间得不到服务。这通常是由于线程调度策略或资源分配不当导致的。

如和解决多线程安全的问题(就是线程如何同步)

核心思想:上锁 分布式上锁

在同一个jvm中,多个线程需要竞争锁的资源,最终只有一个线程能够获取到锁,多个线程同时抢一把锁,谁(哪个线程)能够获取到锁,谁就可以执行该代码,如果没有获取锁成功,中间需要经历锁升级过程如果一直没有获取到锁则会一直阻塞等待。

Java中的多线程安全问题可以通过多种方式来解决,主要包括以下几种常用的方法:

  1. 使用同步块(synchronized)【底层:锁升级过程:偏向锁 -> 轻量级锁 -> 重量锁 : 使用synchronized关键字可以确保同一时间只有一个线程可以执行被标记的方法或代码块,从而避免数据竞争和一致性问题。例如:

    public synchronized void method() {
        // 线程安全的代码
    }
    
  2. 使用锁(Lock) : Java提供了java.util.concurrent.locks包中的显式锁(如ReentrantLock),可以通过这种锁来替代synchronized,提供更灵活的线程控制: 【JUC 底层基于aqs+cas实现】

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
        // 线程安全的代码
    } finally {
        lock.unlock();
    }
    
  3. 使用原子变量(Atomic Variables) : java.util.concurrent.atomic包中的原子变量类【CAS非阻塞式】(如AtomicIntegerAtomicReference等)允许在多个线程之间安全地更新变量而无需使用同步:

    AtomicInteger atomicInteger = new AtomicInteger(0);
    atomicInteger.incrementAndGet(); // 安全的递增操作
    
  4. 使用并发集合(Concurrent Collections) : Java提供了一些线程安全的集合类,如ConcurrentHashMapCopyOnWriteArrayList等。这些集合类内部实现了必要的同步机制,使得它们可以在多线程环境中安全地使用。

  5. 使用线程局部变量(ThreadLocal) : ThreadLocal类可以为每个线程提供一个独立的变量拷贝,避免了多个线程之间的共享,从而提高了线程安全性:

    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    threadLocal.set(1); // 每个线程可以独立存储数据
    
  6. 避免共享可变状态: 设计时尽量避免多个线程共享可变的数据,可以考虑使用不可变对象,或者通过消息传递的方式(如使用队列)来减小线程间的耦合。

使用 synchronized 关键字进行线程同步时,可能会遇到死锁问题。死锁是指两个或多个线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,导致它们都无法继续执行。

死锁产生的条件

为了产生死锁,必须满足以下四个条件:

  1. 互斥条件:至少有一个资源是非共享的,即同一时间只有一个线程可以获得对资源的独占访问权。
  2. 占有并等待:一个线程已经持有了一个资源,并且还在等待获取其他资源。
  3. 不剥夺条件:已经获得的资源在使用完之前不能被其他线程强行剥夺,只能由持有该资源的线程自行释放。
  4. 循环等待:存在一种线程资源的循环等待关系,即线程 A 等待线程 B 持有的资源,而线程 B 又在等待线程 A 持有的资源,从而形成一个环路。

synchronized死锁 示例

以下是一个简单的示例,演示了如何使用 synchronized 导致死锁的情况:

public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            System.out.println("Locked lock1");
            try {
                Thread.sleep(100); // 为了确保另一个线程获得执行机会
            } catch (InterruptedException e) {}
            synchronized (lock2) {
                System.out.println("Locked lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("Locked lock2");
            try {
                Thread.sleep(100); // 为了确保另一个线程获得执行机会
            } catch (InterruptedException e) {}
            synchronized (lock1) {
                System.out.println("Locked lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();
        
        new Thread(example::method1).start();
        new Thread(example::method2).start();
    }
}

在上面的示例中,method1 在获得 lock1 后尝试获得 lock2,而 method2 在获得 lock2 后尝试获得 lock1。在这两种方法并发执行时,就会产生死锁。

预防死锁的方法

  1. 资源顺序:确保所有线程在请求多个资源时,都按照相同的顺序进行请求。例如,始终先请求 lock1 再请求 lock2
  2. 定时锁(TryLock) :使用 ReentrantLock 提供的 tryLock() 方法,可以尝试在一定时间内获取锁,如果未能获取,则可以做其他处理,避免死锁。
  3. 减少锁的粒度:将锁的范围减小,避免长时间持有锁,从而减少死锁的机会。
  4. 死锁检测:定期检查系统是否存在死锁,如果发现死锁,可以适时终止某些线程来解除死锁状态。

多线程通讯

  1. 共享变量:多个线程通过共享变量进行通信。可以使用synchronized关键字来保证线程安全。
  2. wait/notify机制:通过Object类的wait()notify()方法实现线程间的通信。线程可以在某个条件下等待,另一个线程可以通知它继续执行。
  3. Condition:使用java.util.concurrent.locks.Condition接口,与锁(Lock)结合使用,提供了更灵活的线程间通知机制。
  4. 信号量(Semaphore) :使用java.util.concurrent.Semaphore类控制对共享资源的访问,允许多个线程同时访问资源。
  5. 阻塞队列(BlockingQueue) :使用java.util.concurrent包中的BlockingQueue,例如ArrayBlockingQueue,在生产者-消费者模型中非常有效,提供了线程安全的入队和出队操作。
  6. CountDownLatch:一种同步工具类,允许一个或多个线程等待其他线程完成某些操作。
  7. CyclicBarrier:使一组线程在执行到某个点时相互等待,直到所有线程都达到这个点。
  8. Future和Callable:使用FutureCallable接口实现异步任务处理,通过ExecutorService来管理线程。

1. 共享变量

class SharedData {
    int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

class IncrementThread extends Thread {
    SharedData data;
    
    IncrementThread(SharedData data) {
        this.data = data;
    }
    
    public void run() {
        for (int i = 0; i < 1000; i++) {
            data.increment();
        }
    }
}

public class SharedVariableExample {
    public static void main(String[] args) throws InterruptedException {
        SharedData data = new SharedData();
        Thread t1 = new IncrementThread(data);
        Thread t2 = new IncrementThread(data);
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Final Count: " + data.getCount());
    }
}

2. wait/notify机制

class Data {
    private String message;
    private boolean empty = true;

    public synchronized String getMessage() throws InterruptedException {
        while (empty) {
            wait(); 
        }
        empty = true;
        notifyAll(); 
        return message;
    }

    public synchronized void setMessage(String message) throws InterruptedException {
        while (!empty) {
            wait(); 
        }
        this.message = message;
        empty = false;
        notifyAll(); 
    }
}

// Producer and Consumer classes would be similar to the example in the previous message.

3. Condition

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class ConditionData {
    private String message;
    private boolean empty = true;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public String getMessage() throws InterruptedException {
        lock.lock();
        try {
            while (empty) {
                condition.await(); 
            }
            empty = true;
            condition.signalAll(); 
            return message;
        } finally {
            lock.unlock();
        }
    }

    public void setMessage(String message) throws InterruptedException {
        lock.lock();
        try {
            while (!empty) {
                condition.await(); 
            }
            this.message = message;
            empty = false;
            condition.signalAll(); 
        } finally {
            lock.unlock();
        }
    }
}

4. 信号量(Semaphore)

import java.util.concurrent.Semaphore;

class SemaphoreExample {
    private static Semaphore semaphore = new Semaphore(1);

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println("线程1获取了信号量");
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
                System.out.println("线程1释放了信号量");
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println("线程2获取了信号量");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
                System.out.println("线程2释放了信号量");
            }
        });

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

5. 阻塞队列

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class Producer extends Thread {
    private BlockingQueue<String> queue;

    public Producer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                String message = "Message " + i;
                queue.put(message);
                System.out.println("生产者生产: " + message);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Consumer extends Thread {
    private BlockingQueue<String> queue;

    public Consumer(BlockingQueue<String> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            for (int i = 0; i < 5; i++) {
                String message = queue.take();
                System.out.println("消费者消费: " + message);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class BlockingQueueExample {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        producer.start();
        consumer.start();
    }
}

6. CountDownLatch

import java.util.concurrent.CountDownLatch;

class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        for (int i = 0; i < 3; i++) {
            int finalI = i;
            new Thread(() -> {
                System.out.println("线程" + finalI + "完成任务");
                latch.countDown();
            }).start();
        }

        latch.await(); // 等待所有线程完成
        System.out.println("所有线程都完成任务");
    }
}

7. CyclicBarrier

import java.util.concurrent.CyclicBarrier;

class CyclicBarrierExample {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程已到达,开始执行"));

        for (int i = 0; i < 3; i++) {
            final int threadId = i;
            new Thread(() -> {
                try {
                    System.out.println("线程" + threadId + "到达");
                    barrier.await(); // 等待其他线程
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

8. Future和Callable

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

class CallableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        
        Callable<Integer> task = () -> {
            Thread.sleep(2000);
            return 123;
        };
        
        // 用于提交一个 Callable 或 Runnable 任务以进行执行。这个方法会返回一个 `Future` 对象,代表该任务的结果,您可以通过这个对象来获取任务的执行状态和结果
        Future<Integer> future = executor.submit(task);
        
        System.out.println("主线程执行其他任务...");
        
        Integer result = future.get(); // 等待任务完成并获取结果
        System.out.println("任务结果: " + result);
        
        // 用于关闭执行器,并停止接受新的任务。在调用 `shutdown()` 方法后,已经提交的任务会继续执行,但不会接受新的任务提交
        executor.shutdown();
    }
}

多线程状态

来自余胜军总结的多线程底层的七种状态。

image.png

Java线程有五种主要状态

  1. NEW(新建)

    • 当线程被创建但还未启动时,处于 NEW 状态。这时使用 new Thread() 创建了一个线程对象,但并没有调用 start() 方法。
    Thread thread = new Thread();
    // 此时线程处于 NEW 状态
    
  2. RUNNABLE(可运行)

    • 当线程调用 start() 方法后,它会进入 RUNNABLE 状态。线程在这个状态下可以运行,也可能由于线程调度的原因而被挂起。这个状态包括了实际上正在运行的线程和准备运行的线程。
    Thread thread = new Thread(() -> {
        // 这里代码运行时线程处于 RUNNABLE 状态
    });
    thread.start(); // 线程进入 RUNNABLE 状态
    
  3. BLOCKED(阻塞)

    • 如果一个线程试图获取一个已经被其他线程持有的锁时,它进入 BLOCKED 状态。线程将保持该状态直到获得锁。
    synchronized (object) {
        // 进入 BLOCKED 状态等待锁
    }
    
  4. WAITING(等待)

    • 当线程等待其他线程执行特定操作后返回时(例如,调用 Object.wait()Thread.join() 或 LockSupport.park()),它进入 WAITING 状态。处于这个状态的线程不会占用 CPU 资源。
    synchronized (object) {
        object.wait(); // 该线程进入 WAITING 状态
    }
    
  5. TIMED_WAITING(计时等待)

    • 当线程在等待某个时间段后返回时(例如,调用 Thread.sleep(millis)Object.wait(millis) 或 Thread.join(millis)),它进入 TIMED_WAITING 状态。在这个状态下,线程会在指定的时间内等待,过后返回 RUNNABLE 状态。
    Thread.sleep(1000); // 该线程进入 TIMED_WAITING 状态
    
  6. TERMINATED(终止)

    • 当线程的 run() 方法执行完成或因异常终止时,它进入 TERMINATED 状态。
    public void run() {
        // 线程执行完毕后进入 TERMINATED 状态
    }
    

状态转换示例

public class ThreadStateExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                // 线程进入 TIMED_WAITING 状态
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 线程处于 NEW 状态
        System.out.println("状态:" + thread.getState());

        thread.start(); // 线程进入 RUNNABLE 状态
        System.out.println("状态:" + thread.getState());

        try {
            // 等待线程结束
            thread.join(); // 主线程等待 thread 线程结束
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 线程处于 TERMINATED 状态
        System.out.println("状态:" + thread.getState());
    }
}

为什么会出现内存飙升?

内存使用飙升通常是由于以下原因之一引起的:

  1. 内存泄漏:对象不再使用,但由于引用仍然存在而未被垃圾回收。
  2. 过多的对象创建:程序在短时间内创建了大量对象,导致内存占用过高。
  3. 不当的资源管理:未能及时释放或关闭资源,例如数据库连接、文件句柄等。

使用 Thread.sleep() 的示例

虽然使用 Thread.sleep() 可以降低某些情况下的CPU占用,但它并不能解决内存占用高的问题。以下是一个简单示例,展示了如何在循环中使用 sleep()

public class SleepExample {
    public static void main(String[] args) {
        while (true) {
            System.out.println("正在执行任务...");
            try {
                // 暂停1秒,减轻CPU负担
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 重新设置中断状态
                e.printStackTrace();
            }
        }
    }
}

防止内存飙升的建议

  1. 检测内存泄漏:使用工具(如 Java VisualVM、Eclipse Memory Analyzer 等)监测内存使用情况,找出并修复内存泄漏。
  2. 正确使用集合:避免在循环中使用 growable collections(如 ArrayList)而不适时清理不必要的对象。
  3. 及时释放资源:确保在使用完数据库连接、文件流等资源后,及时关闭它们。
  4. 对象池:使用对象池来重用对象,例如数据库连接池,这可以减少对象的创建和销毁。
  5. 合理的GC设置:调整Java虚拟机的垃圾回收器选项,根据需求优化内存使用。

守护线程与用户线程

用户线程(User Thread)

  1. 定义:用户线程是指在程序中创建的所有线程,用户线程的生命周期与应用程序相同。只要还有用户线程在运行,Java虚拟机(JVM)就不会退出。

  2. 特性

    • 用户线程是默认线程类型,所有通过 new Thread() 创建的线程都是用户线程。
    • 当所有用户线程都结束时,JVM 会关闭,程序也会终止。
  3. 示例

public class UserThreadExample {
    public static void main(String[] args) {
        Thread userThread = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("用户线程完成");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        
        userThread.start();

        // 主线程等待用户线程完成
        try {
            userThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主线程结束");
    }
}

守护线程(Daemon Thread)

  1. 定义:守护线程是服务于用户线程的线程。守护线程的主要目的是为其他用户线程提供服务,例如进行后台处理。

  2. 特性

    • 守护线程在用户线程全部结束后会被JVM自动终止。这意味着如果只有守护线程在运行,JVM将退出。
    • 可以通过设置线程为守护线程,以便其可以执行后台任务。
  3. 设置守护线程:可以通过调用 setDaemon(true) 方法将一个线程设置为守护线程,该方法必须在调用 start() 之前调用。

  4. 示例

public class DaemonThreadExample {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("守护线程正在运行...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 设置为守护线程
        daemonThread.setDaemon(true);
        daemonThread.start();

        // 主线程等待一段时间后结束
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("主线程结束");
    }
}

区别总结

特性用户线程(User Thread)守护线程(Daemon Thread)
生命周期应用程序运行期间存在仅在用户线程存在时运行
JVM行为当所有用户线程结束时,JVM才结束当所有用户线程结束时,JVM立即结束
用途执行用户需要的计算任务执行后台服务和支持性任务
默认线程类型默认情况下是用户线程通过调用 setDaemon(true) 设置为守护线程

如何安全停止一个线程

安全地停止一个线程是Java多线程编程中非常重要的一个方面。直接停止线程(例如使用Thread.stop()方法)是不安全的,因为它可能导致数据不一致或资源泄漏。以下是一些常见的方法,能够安全地停止线程:

1. 使用标志位

使用一个 boolean 类型的标志位来控制线程的停止。在运行线程的循环中检查这个标志位,如果该标志位被设置为 false,则安全地退出循环。

示例:

class StoppableThread extends Thread {
    private volatile boolean running = true;  // 使用 volatile 确保可见性

    public void run() {
        while (running) {
            System.out.println("线程正在运行...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 重新设定中断状态
            }
        }
        System.out.println("线程已安全停止");
    }

    public void stopRunning() {
        running = false;  // 设置标志位为 false
    }
}

public class ThreadStopExample {
    public static void main(String[] args) throws InterruptedException {
        StoppableThread thread = new StoppableThread();
        thread.start();

        // 主线程等待一段时间
        Thread.sleep(2000);

        // 安全地停止线程
        thread.stopRunning();

        // 等待线程结束
        thread.join();
        System.out.println("主线程结束");
    }
}

2. 使用中断

使用线程的 interrupt() 方法可以请求线程中断。在线程内部需要定期检查自身的中断状态,适当地退出或处理当前工作。

示例:

class InterruptibleThread extends Thread {
    public void run() {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程正在工作...");
                Thread.sleep(500); // 可能会抛出 InterruptedException
            }
        } catch (InterruptedException e) {
            // 处理被中断的情况
            Thread.currentThread().interrupt(); // 恢复中断状态
        }
        System.out.println("线程已安全停止");
    }
}

public class ThreadInterruptExample {
    public static void main(String[] args) throws InterruptedException {
        InterruptibleThread thread = new InterruptibleThread();
        thread.start();

        // 主线程等待一段时间
        Thread.sleep(2000);

        // 请求中断
        thread.interrupt();

        // 等待线程结束
        thread.join();
        System.out.println("主线程结束");
    }
}

3. 使用 ExecutorService

如果使用 ExecutorService 来管理线程池,可以通过 shutdown() 或 shutdownNow() 方法来停止线程。

  • shutdown():请求线程池停止接受新任务,并等待已提交的任务执行完成。
  • shutdownNow():尝试停止所有正在执行的任务,并返回未执行的任务列表。

示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceExample {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        executor.submit(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("任务正在运行...");
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                // 处理被中断
                Thread.currentThread().interrupt();
            }
            System.out.println("任务已安全停止");
        });

        // 主线程等待一段时间
        Thread.sleep(2000);

        // 请求停止
        executor.shutdownNow(); // 立刻停止所有任务

        System.out.println("主线程结束");
    }
}

多线程的优先级

在Java中,多线程的优先级是通过 Thread 类中的 setPriority(int priority) 方法设置的。线程的优先级是一个整数值,范围从 Thread.MIN_PRIORITY(1)到 Thread.MAX_PRIORITY(10),默认值是 Thread.NORM_PRIORITY(5)。优先级的作用是在调度线程时给定的建议,而并非强制要求,线程调度的实际行为可能会因不同的Java虚拟机实现和操作系统而异。

优先级设置

  1. 设置优先级

    • 可以通过调用 Thread 类的 setPriority(int priority) 方法来设置线程的优先级,优先级值的范围是 1 到 10。
    Thread thread = new Thread(() -> {
        // 线程任务
    });
    thread.setPriority(Thread.MAX_PRIORITY); // 设置为最高优先级
    thread.start();
    
  2. 获取优先级

    • 可以使用 getPriority() 方法获取线程的优先级。
    int priority = thread.getPriority(); // 获取线程的优先级
    

优先级的影响

在多线程环境中,线程的优先级会影响线程调度的顺序,高优先级的线程可能会比低优先级的线程获得更多的CPU时间。虽然优先级可以影响调度,但并不是绝对的,具体的调度策略取决于操作系统和Java虚拟机。

Join wait sleep的区别

在Java中,joinwait 和 sleep 是三种用于线程控制的方法,它们有不同的用途和行为。

1. join() (先执行另外的一个线程在等待的过程中释放对象锁)

  • 功能join() 方法用于让当前线程等待另一个线程完成。当前线程调用某个线程的 join() 方法,当前线程将被挂起,直到被调用的线程终止。

  • 使用场景:在需要等待其他线程完成后再继续执行的情况下使用。

  • 示例

    class MyThread extends Thread {
        public void run() {
            System.out.println("子线程正在执行...");
        }
    }
    
    public class JoinExample {
        public static void main(String[] args) throws InterruptedException {
            MyThread thread = new MyThread();
            thread.start();
            thread.join(); // 主线程等待子线程执行完毕
            System.out.println("子线程已完成,主线程继续执行");
        }
    }
    

2. wait() (在等待的过程中释放对象锁)

  • 功能wait() 方法是 Object 类的方法,用于使当前线程在某个对象上等待,直到其他线程调用 notify() 或 notifyAll() 方法。调用 wait() 的线程必须持有对象的监视器(锁)。

  • 使用场景:在多线程中进行线程间通信时使用,通常在消费者-生产者模型中。

  • 示例

    class SharedResource {
        private boolean available = false;
    
        public synchronized void produce() throws InterruptedException {
            while (available) {
                wait(); // 等待资源可用
            }
            System.out.println("生产者生产了资源");
            available = true;
            notify(); // 通知消费者
        }
    
        public synchronized void consume() throws InterruptedException {
            while (!available) {
                wait(); // 等待资源可用
            }
            System.out.println("消费者消费了资源");
            available = false;
            notify(); // 通知生产者
        }
    }
    

3. sleep() (睡眠时不释放对象锁)

  • 功能sleep(long millis) 方法是 Thread 类的方法,用于使当前线程暂停执行指定的毫秒数。线程在调用 sleep() 时不会释放锁。

  • 使用场景:在需要让线程暂停一段时间的情况下使用,通常用于简单的延时或控制循环的频率。

  • 示例

    public class SleepExample {
        public static void main(String[] args) {
            System.out.println("线程即将进入休眠状态");
            try {
                Thread.sleep(2000); // 休眠2秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("线程休眠结束,继续执行");
        }
    }
    

总结对比

特征 join()  wait()  sleep() 
所属类 Thread  Object  Thread 
释放锁是(等待的线程释放锁)是(释放锁)否(保持锁)
用途等待其他线程完成线程间通信,等待条件暂停执行一段时间
抛出异常 InterruptedException  InterruptedException  InterruptedException