JAVA线程基础——线程的创建和常用方法

一、线程创建的三种方法

    1、继承Thread类并重写run的法

// 继承Thread类  
public class MyThread extends Thread {  
  
    // 重写run方法  
    @Override  
    public void run() {  
        // 在这里编写线程的执行体  
        System.out.println("线程运行中...");  
        // 假设这里有一些耗时的操作  
        try {  
            Thread.sleep(1000); // 让线程休眠1秒  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println("线程执行完毕!");  
    }  
  
    public static void main(String[] args) {  
        // 创建MyThread类的实例  
        MyThread myThread = new MyThread();  
          
        // 启动线程  
        myThread.start();  
          
        // main线程继续执行,不会等待myThread线程完成  
        System.out.println("main线程继续执行...");  
    }  
}

        如上代码中的 MyThread 类继承了 Thread 类,并重写了run()方法。在 main 函数里面创建了一个 MyThread 的实例,然后调用该实例的start 方法启动了线程。需要注意的是,当创建完 thread 对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程。
        其实调用 start 方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除 CPU 资源外的其他资源,等待获取 CPU 资源后才会真正处于运行状态。一旦 mun 方法执行完毕,该线程就处于终止状态。
        使用继承方式的好处是,在run0)方法内获取当前线程直接使用this就可以了,无须使用 Thread.currentThread0)方法;不好的地方是Java不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码,而Runable则没有这个限制。

2、实现Runnable接口的run方法方式

// 定义一个实现了Runnable接口的类  
public class SharedTask implements Runnable {  
    // 这里没有成员变量,因为任务被设计为无状态的  
  
    // 实现Runnable接口的run方法  
    @Override  
    public void run() {  
        // 执行任务,这里只是简单地打印一条消息  
        System.out.println(Thread.currentThread().getName() + " 正在执行任务");  
    }  
  
    public static void main(String[] args) {  
        // 创建一个SharedTask实例,这个实例将被两个线程共享  
        Runnable sharedTask = new SharedTask();  
  
        // 通过这个实例创建两个Thread对象  
        Thread thread1 = new Thread(sharedTask, "线程1");  
        Thread thread2 = new Thread(sharedTask, "线程2");  
  
        // 启动这两个线程  
        thread1.start();  
        thread2.start();  
  
        // 注意:由于线程调度是由JVM的线程调度器控制的,  
        // 因此我们不能确切地知道哪个线程会先执行。  
        // 但在这个例子中,两个线程将执行相同的代码(打印一条消息)。  
    }  
}

         如上面代码所示,两个线程共 task代码逻辑,如果需要,可以给RunableTask 添加参数进行任务区分。另外 RunableTask 可以继承其他类。但是上面介绍的两种方式都有缺点,就是任务都没有返回值。

3、使用FutureTask的方式

import java.util.concurrent.*;

// 实现Callable接口,允许有返回值
class MyCallable implements Callable<Integer> {
    // 模拟耗时任务
    @Override
    public Integer call() throws Exception {
        Thread.sleep(1000); // 假设任务需要1秒
        return 123; // 模拟的返回值
    }
}

public class FutureTaskExample {
    public static void main(String[] args) {
        // 创建Callable实例
        Callable<Integer> task = new MyCallable();

        // 使用Callable实例创建FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<>(task);

        // 创建一个线程来执行FutureTask
        Thread thread = new Thread(futureTask);

        // 启动线程
        thread.start();

        try {
            // 等待任务执行完成,并获取结果
            // 注意:这里会阻塞当前线程,直到FutureTask中的任务执行完成
            Integer result = futureTask.get(); // get方法会阻塞,直到任务完成
            System.out.println("任务执行结果: " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }



        // 如果需要,可以检查任务是否已经完成
        System.out.println("任务是否完成: " + futureTask.isDone());
        System.out.println("222");
        // 注意:FutureTask只能被启动(即执行其run方法)一次
        // 如果尝试再次启动同一个FutureTask实例,将会抛出IllegalStateException异常

    }
}

        如上代码中的CallerTask类实现了Callable接口的 call()方法。在 main 函数内首先创建了一个FutrueTask对象(构造函数为CallerTask的实例),然后使用创建的 FutrueTask对象作为任务创建了一个线程并且启动它,最后通过futureTask.get()等待任务执行完毕并返回结果。

二、线程常用方法

1、线程的通知与等待——wait()方法  与  notify()方法 

        当一个线程调用一个共享变量的 wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回:

        1)其他线程调用了该共享对象的notify()或者 notifyAII() 方法;

        2)其他线程调用了该线程的imnterrupt()方法,该线程抛出InterruptedException 异常返回。

        另外需要注意的是,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用 wait()方法时调用线程会抛出 IlegalMonitorStateException 异常。

  1. wait()
     

      synchronized (obj) {
          while (!condition) {
              obj.wait();
          }
    
      }

            如上代码是经典的调用共享变量 wait()方法的实例,首先通过同步块获取obj 上面的监视器锁,然后在while 循环内调用 obj的 wait() 方法。

  2. wait(long timeout )函数
           该方法相比 wait()方法多了一个超时参数,它的不同之处在于,如果一个线程调用共享对象的该方法挂起后,没有在指定的timeoutms时间内被其他线程调用该共享变量的notify() 或者notifyAll()方法唤醒,那么该函数还是会因为超时而返回。如果将 timeout 设置为0则和 wait 方法效果一样,因为在 wait 方法内部就是调用了 wait(0)。需要注意的是,如果在调用该函数时,传递了一个负的timeout则会抛出HlegalArgumentException 异常。

  3. wait(long timeout, int nanos )函数
            在其内部调用的是 wait(long timeout)函数,如下代码只有在nanos>0 时才使参数timeout 递增1。

    public final void wait(long timeout, int nanos) throws InterruptedException {
            if (timeout < 0) {
                throw new IllegalArgumentException("timeout value is negative");
            }
    
            if (nanos < 0 || nanos > 999999) {
                throw new IllegalArgumentException(
                                    "nanosecond timeout value out of range");
            }
    
            if (nanos > 0) {
                timeout++;
            }
    
            wait(timeout);
        }
  4. notify() 函数
           一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
           此外,被唤醒的线程不能马上从wait方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回,也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
           类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify()方法,否则会抛出IlegalMonitorStateException 异常。

  5. notifyAll() 函数
            不同于在共享变量上调用notifyO函数会唤醒被阻塞到该共享变量上的一个线程notifyAIl() 方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。

2、让线程睡眠的sleep方法

        Thread 类中有一个静态的sleep方法,当一个执行中的线程调用了Thread 的 sleep方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用 sleep 方法的地方抛出 InterruptedException 异常而返回。
 

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

3、让出 CPU 执行权的 yield 方法

        Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有CPU的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了Thread 类的静态方法yield时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。
        当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出CPU的那个线程来获取CPU执行权。下面举一个例子来加深对 yield方法的理解。

public class YieldExample {  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(new YieldRunner("线程1"));  
        Thread t2 = new Thread(new YieldRunner("线程2"));  
  
        t1.start();  
        t2.start();  
    }  
  
    static class YieldRunner implements Runnable {  
        private String name;  
  
        public YieldRunner(String name) {  
            this.name = name;  
        }  
  
        @Override  
        public void run() {  
            for (int i = 0; i < 5; i++) {  
                System.out.println(name + " 执行中: " + i);  
                  
                // 在循环的每次迭代后让出CPU执行权  
                Thread.yield();  
  
                try {  
                    // 稍作暂停,以模拟耗时操作  
                    Thread.sleep(100);  
                } catch (InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
            }  
        }  
    }  
}

        一般很少使用这个方法,在调试或者测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,其在设计并发控制时或许会有用途,后面在讲解java.util.concurrent.1ocks 包里面的锁时会看到该方法的使用。
        总结:sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

三、线程中断

        Java 中的线程中断是 种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行 而是被中断的线程根据中断状态自行处理。

      1、void interrupt() 方法

Thread thread = ...; // 假设这是你想要中断的线程  
thread.interrupt(); // 请求中断该线程

        中断线程通常是通过调用该线程的interrupt()方法来实现的。这个方法是Thread类的一个实例方法,任何线程都可以调用自身的interrupt()方法来请求自己中断,或者调用其他线程的interrupt()方法来请求那个线程中断。

2、boolean isinterrupted() 方法

        检测当前线程是否被中断,如果是返回 true 否则返回false,这个方法会返回当前线程的中断状态,并且不会清除这个状态。

if (Thread.currentThread().isInterrupted()) {  
    // 当前线程被中断了  
}  
  
// 或者在线程内部  
if (this.isInterrupted()) {  
    // 当前线程被中断了  
}

3、boolean interrupted() 方法

if (Thread.interrupted()) {  
    // 当前线程被中断了,但中断状态已被清除  
}

下面是一个简单的示例,展示了如何检查和处理线程中断:

public class InterruptHandlingExample {  
    public static void main(String[] args) throws InterruptedException {  
        Thread worker = new Thread(() -> {  
            while (!Thread.currentThread().isInterrupted()) {  
                try {  
                    // 模拟耗时任务  
                    System.out.println("Worker is running...");  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    // 捕获InterruptedException,并重新设置中断状态  
                    System.out.println("Worker was interrupted. Exiting gracefully.");  
                    // 注意:在这里可以选择不重新设置中断状态,取决于你的需求  
                    // Thread.currentThread().interrupt(); // 如果需要,可以取消注释  
                    return; // 退出循环,线程结束  
                }  
            }  
        });  
  
        worker.start();  
  
        // 主线程等待一段时间后中断worker线程  
        Thread.sleep(3000);  
        worker.interrupt();  
    }  
}

        在这个示例中,worker线程在循环中检查自己是否被中断,并在捕获到InterruptedException时优雅地退出。注意,在catch块中,我们并没有重新设置中断状态,因为在这个简单的示例中,我们不需要在捕获异常后继续传递中断信号。然而,在更复杂的应用程序中,根据具体需求,可能需要在捕获异常后重新设置中断状态。
PS:许多Java标准库中的阻塞方法(如Thread.sleep()、Object.wait()、以及大部分I/O操作)在被中断时会抛出InterruptedException。在这些情况下,线程应该捕获这个异常,并作出适当的响应,比如重新设置中断状态(如果打算继续传递中断信号)或执行清理操作。

四、理解线程上下文切换

        在多线程编程中,线程个数一般都大于CPU个数,而每个CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
        线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。

五、线程死锁

六、守护线程与用户线程

        Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM 启动时会调用main 函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM 就不会退出。

守护线程的特点:

  1. 不阻止JVM退出:这是守护线程最显著的特点。如果JVM中只剩下守护线程,则JVM会自动退出。相反,如果有任何非守护线程(用户线程)仍在运行,JVM将不会退出,直到所有非守护线程都结束执行。

  2. 优先级:守护线程与非守护线程在优先级上没有区别,都是可以设置和修改的。但是,守护线程的存在与否,并不影响JVM的终止行为。

  3. 用途:守护线程通常用于执行一些“后台”任务,比如垃圾收集、线程调度等。这些任务对于程序的正常运行是必要的,但它们不应该阻止程序的正常退出。

  4. 创建守护线程:

    在Java中,你可以通过调用线程对象的setDaemon(true)方法将线程设置为守护线程。但需要注意的是,这个调用必须在线程启动(即调用start()方法)之前进行。一旦线程启动,就不能再将其设置为守护线程了。

    public class DaemonThreadExample {  
        public static void main(String[] args) {  
            Thread daemonThread = new Thread(() -> {  
                while (true) {  
                    try {  
                        System.out.println("Daemon Thread is running");  
                        Thread.sleep(1000); // 休眠1秒  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            });  
      
            // 将线程设置为守护线程  
            daemonThread.setDaemon(true);  
      
            // 启动线程  
            daemonThread.start();  
      
            // 假设main线程(非守护线程)执行完以下代码就结束  
            try {  
                Thread.sleep(5000); // 主线程休眠5秒  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
      
            // 5秒后,main线程结束,由于只剩下守护线程在运行,JVM将退出  
            // 注意:这里JVM的退出并不会因为守护线程的继续执行而延迟  
        }  
    }


    在这个例子中,我们创建了一个守护线程,并在main方法中启动了它。然后,main线程休眠了5秒钟后结束。由于此时JVM中只剩下守护线程在运行,所以JVM会在main线程结束后立即退出,而守护线程将被JVM终止,不会有机会完成其后续的执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值