Java线程控制解密:深入理解sleep与wait、start与run的关键差异

发布于:2024-05-21 ⋅ 阅读:(158) ⋅ 点赞:(0)

1.sleep与wait区别

在多线程编程中,正确理解和使用线程控制方法如sleep和wait是至关重要的。它们的功能似乎有点相似,因为两者都能暂停线程的执行,但实际上它们的作用和应用场景大相径庭。

1.1. 方法的定义所属类别

sleep方法定义在Thread类中,它是一个静态方法,其作用是使当前正在执行的线程停止执行指定的时间段,毫无疑问,这是等待当前线程。

Thread.sleep(long millis);

相比之下,wait是Object类的方法,它的使用必须在同步代码块或同步方法中,通过调用某个对象的wait方法来使得当前线程等待,并释放该对象的锁。

object.wait();
object.wait(long timeout);

1.2. 控制的线程状态

sleep方法使线程从Running状态转到Timed Waiting状态,并在指定的时间后自动回到Runnable状态。
而wait则是将线程从Running状态转到Waiting状态,或者在指定时间后进入Timed Waiting状态。与sleep不同的是,wait控制的线程需要通过notify或notifyAll方法来唤醒。

1.3. 持有锁的影响

执行sleep方法的线程不会释放任何锁。
而执行wait的线程会释放其持有的对象锁,这是为了让其他线程有机会进入同步代码块,对对象进行修改,然后通过notify方法告知原线程对象已经被修改。

1.4. 唤醒条件

sleep无需外界干预,在设定时间结束后会自动唤醒。
wait则在以下三种情况会被唤醒:
其他线程调用了同一个对象的notify()方法,并且当前线程被选为被唤醒的线程。
其他线程调用了同一个对象的notifyAll()方法。
wait设置的等待时间已到。

1.5. 应用场景对比

sleep通常用于暂停执行一段时间,它不涉及对象锁,也不依赖于其他线程的通知来唤醒。
相对于sleep,wait被用于线程间的协作。当一个线程需要等待另一个线程的通知时,或者说一个线程在继续执行前必须要等待某个条件的满足,那它就应该使用wait。

1.6. 实践案例

让我们通过一段代码来演示sleep和wait的实际应用。

public class SleepWaitExample {
    private static final Object LOCK = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread sleepThread = new Thread(() -> {
            System.out.println("Sleep Thread sleeping for 2s.");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Sleep Thread woke up and finished.");
        });
        Thread waitThread = new Thread(() -> {
            synchronized (LOCK) {
                try {
                    System.out.println("Wait Thread waiting on LOCK.");
                    LOCK.wait();
                    System.out.println("Wait Thread woke up and finished.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        sleepThread.start();
        waitThread.start();
        // 主线程故意延迟启动,确保上面的线程执行相关的sleep和wait操作
        Thread.sleep(500); 
        synchronized (LOCK) {
            System.out.println("Main Thread calling notify on LOCK.");
            LOCK.notify(); // 注意,如果没有这行代码,waitThread将永远处于等待状态
        }
    }
}

在上面的代码中,我们建立了两个线程。sleepThread通过执行sleep方法暂停两秒钟。与此同时,waitThread通过调用LOCK.wait()暂停并释放了LOCK对象上的锁。主线程随后通过LOCK.notify()唤醒正在等待的waitThread。
这段代码说明了sleep和wait的区别:sleepThread在睡眠期间仍然持有任何已经拥有的锁,而waitThread在等待期间释放了LOCK上的锁。这允许其他持有同一对象锁想进入同步块的线程有机会执行。

2.start与run区别

在Java中,Thread类提供了start()和run()两种启动线程的方法,它们在功能上有本质的区别。

2.1. 方法的调用源与功能

start()方法是Thread类的一个实例方法,调用这个方法会启动一个新线程,并执行该线程的run()方法。

Thread thread = new Thread(() -> {
    // 线程执行的代码
});
thread.start(); // 正确启动一个线程的方式

run()方法也是Thread类的一个实例方法,但它只是普通的方法调用,并不会启动新线程。
thread.run(); // 这只会在当前线程中执行run方法的内容,并不会启动新的线程

2.2. 线程启动机制的内部原理

当调用线程对象的start()方法时,会为该线程分配必要的系统资源,并调用操作系统的线程执行功能,最终达到并发执行的效果。
而调用run()方法,则相当于在当前线程中直接执行一个方法调用,它并不会创建新线程,也就不会产生并行的效果。

2.3. 直接调用run方法的结果

直接调用run()方法,线程对象所封装的任务会在调用run()方法的当前线程中直接执行,而不会有线程调度操作。

2.4. 使用start方法的正确姿势

正确使用start()方法,可以实现对线程生命周期的管理,并且允许系统进行调度,如下所示:

Thread thread = new Thread(() -> {
    // 线程执行的代码
});
thread.start(); // 这会创建一个新线程,并在这个新线程中执行run方法的代码

2.5. 实战演示
让我们通过一个简单的例子来对比start()和run()的不同效果:

public class ThreadStartRunExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Inside the thread: " + Thread.currentThread().getName());
        });
        System.out.println("Before start(): " + Thread.currentThread().getName());
        // 使用start()方法启动线程
        thread.start();
        System.out.println("After start(): " + Thread.currentThread().getName());
        // 增加一些间隔,确保线程启动后执行
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Before run(): " + Thread.currentThread().getName());
        // 直接调用run()方法
        thread.run();
        System.out.println("After run(): " + Thread.currentThread().getName());
    }
}

以上代码执行结果将展示调用start()和直接调用run()的不同。当使用start()时,输出将在一个名为Thread-0的新线程中生成。而直接调用run()时,输出将在主线程中生成。

3.JAVA后台线程(Daemon Thread)

Java中的后台线程(也称守护线程)是程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分。因此,当所有的用户线程(非后台线程)终止时,程序也就终止了,同时会杀死所有剩余的守护线程。

3.1. 后台线程的特性

后台线程最大的特点是它不阻止JVM的终止。无论其执行代码是否运行完毕,一旦所有用户线程都结束,后台线程会自动终止。

3.2. 创建后台线程的方法

要创建一个守护线程,可以通过调用Thread对象的setDaemon(true)方法来设置线程为守护线程。

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

请注意,一旦启动了线程(即调用了start()方法),再尝试设置它为守护线程会抛出IllegalThreadStateException。

3.3. 主线程与后台线程的生命周期

守护线程的生命周期取决于创建它的用户线程。当最后一个非守护线程结束时,守护线程由JVM自动终止。

3.4. 前台线程与后台线程的对比

用户线程可以认为是程序的工作线程,而守护线程通常是在执行如日志记录、性能监控等服务性任务。用户线程终止其运行时不会考虑守护线程是否完成,直接退出JVM。

3.5. 后台线程的用例

以下示例展示了如何创建守护线程并观察其行为:

public class DaemonThreadExample {
    public static void main(String[] args) throws InterruptedException {
        Thread daemonThread = new Thread(() -> {
            while (true) {
                try {
                    System.out.println("Daemon Thread is running.");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        System.out.println("Main thread is sleeping for 3s.");
        Thread.sleep(3000);
        System.out.println("Main thread has finished.");
        // 当主线程结束后,守护线程也随之结束,即使它的循环并未完成
    }
}

在上述代码中,我们创建了一个无限循环的守护线程,并且主线程在休眠3秒后结束。即便守护线程的while循环并没有结束,但它会随着主线程的结束而立即终止。