从零开始学Java之线程中有哪些核心API方法?

181 阅读16分钟

作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在上一篇文章中,壹哥给大家介绍了进程、线程的概念,以及创建线程的几种方式。现在我们对线程已经有了基本的了解,但是有些同学看了昨天的文章就来问壹哥,说线程的方法只有start()和run()吗?怎么没有看到别的线程方法?所以,今天壹哥要给大家介绍一些别的核心API方法,这些方法与我们的多线程开发都息息相关哦。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【5100】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear…

Gitee: gitee.com/sunyiyi/Lea…

一. 线程相关的核心API方法

为了让大家有个清晰的认知,壹哥会给大家分类绘制表格,来展示与线程相关的常用核心API方法及其用法。

1. Thread中的方法

Thread是Java中最基本的线程类,它提供了许多用于创建、管理和控制线程的方法,如下表所示:

方法名返回类型描述
start()void启动线程,JVM会调用线程的run()方法
run()void执行线程的核心代码块
sleep(long millis)static void指定线程休眠的毫秒数
interrupt()void中断线程
isInterrupted()boolean判断线程是否被中断
join()void把指定的线程加入到当前线程,将两个交替执行的线程合并为顺序执行的线程,并可以让线程陷入等待
getName()String获取某个线程的名称
setName(String name)void设置线程的名称
yield()static void暂停当前线程,让出CPU执行时间
isAlive()boolean判断线程是否存活
setPriority(int priority)void设置线程的优先级
getPriority()int获取线程的优先级
wait()final void线程等待,释放锁
notify()final void唤醒等待中的线程
notifyAll()final void唤醒等待中的所有线程

注意:以上这些方法都是在Thread类中定义的,因此我们需要先创建出Thread类的实例才能使用它们。

2. Object中的方法

虽然Object本身不是线程类,但它是Java中所有类的基类,它也给我们提供了几个用于线程操作的方法,如下所示:

方法名称方法描述
wait()使当前线程等待,直到另一个线程调用该对象的notify()或notifyAll()方法,或者指定的时间到期。
notify()唤醒正在等待该对象的一个线程。
notifyAll()唤醒正在等待该对象的所有线程。

3. Runnable中的方法

Runnable接口是Java中另一个重要的线程接口,它用于定义线程的任务,常用方法如下:

方法名称方法描述
run()定义线程的任务。

4. Callable中的方法

Callable接口是Java 5中引入的一个新接口,它与Runnable接口类似,但它可以返回一个值。常用方法如下:

方法名称方法描述
call()定义线程的任务,并返回一个值。

5. Executor中的方法

Executor接口是Java中线程池的核心接口,它定义了一组执行任务的方法。常用方法如下:

方法名称方法描述
execute(Runnable command)将一个任务提交到线程池中执行。
submit(Callable task)将一个带有返回值的任务提交到线程池中执行。

6. Executors中的方法

Executors类是Java中用于创建线程池的工厂类,它提供了许多静态方法来创建不同类型的线程池。常用方法如下:

方法名称方法描述
newFixedThreadPool(int nThreads)创建一个固定大小的线程池。
newCachedThreadPool()创建一个有缓存的线程池。
newSingleThreadExecutor()创建一个单线程的线程池。
newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性执行任务的线程池。

接下来壹哥就给大家简单演示一下,以上方法中几个重点方法的用法。

二. 核心方法的使用

1. sleep()方法

sleep()方法是Thread类中的一个静态方法,可以使线程暂停指定的时间,该方法接受一个long类型的参数,表示线程需要暂停的毫秒数。调用sleep()方法会让当前线程进入阻塞状态,暂停执行,等到指定的时间过去后会被唤醒继续执行当前任务。示例代码如下:

/**
 * @author 一一哥Sun
 */
public class Demo05 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println("当前线程:" + Thread.currentThread().getName() + ",i = " + i);
            Thread.sleep(1000); // 暂停一秒
        }
    }
}

该上面的案例中,主线程会输出数字0-9,每个数字之间暂停一秒,实现了每秒输出一次的效果。

2. join()方法

Thread.join()方法可以使一个线程等待另一个线程执行完毕后再继续执行,该方法会阻塞当前线程,直到调用该方法的线程执行完毕。比如我们在主线程中调用了另一个线程的join()方法,则主线程就会被阻塞,直到另一个线程执行完成,主线程才能继续执行。示例代码如下:

/**
 * @author 一一哥Sun
 */
public class Demo06 {
	public static void main(String[] args) throws InterruptedException {
		// 采用Lambda表达式创建线程
		Thread thread1 = new Thread(() -> {
			for (int i = 0; i < 5; i++) {
				System.out.println("子线程1执行,i = " + i + ",当前线程:" + Thread.currentThread());
			}
		});

		Thread thread2 = new Thread(() -> {
			for (int i = 0; i < 5; i++) {
				System.out.println("子线程2执行,i = " + i + ",当前线程:" + Thread.currentThread());
			}
		});

		thread1.start();
		thread2.start();

		//主线程会等待两个子线程执行完毕后再继续执行
		thread1.join();
		thread2.join();

		System.out.println("主线程执行完毕,当前线程:" + Thread.currentThread());
	}
}

在上面的案例中,壹哥在主线程中启动了两个子线程,子线程分别输出数字0-4。在启动子线程后,主线程调用了thread1.join()和thread2.join()方法,使主线程等待两个子线程执行完毕后再继续执行。

3. yield()方法

Thread.yield()方法可以使当前线程放弃CPU资源,让其他线程优先执行。这就好比两个人在高速上开车,一个开奔奔的主动把路权让给了开库里南的,等库里南通过后自己才走,这就是让出了“CPU资源”。yield方法没有参数,调用该方法后,当前线程会进入就绪状态,等待重新获得CPU资源后又会继续执行。

但大家要注意,yield()方法只是告诉调度器当前线程愿意放弃CPU资源,以便其他线程能够运行。但该方法并不会阻塞当前线程,它只是让当前线程从运行状态转为就绪状态,让CPU重新调度安排。这就好比壹哥前面给大家举的开车的例子,奔奔愿意把路权让给库里南,但并不是说奔奔直接就把路权让给了库里南,而是奔奔告诉交警说:“我把路权让出来,您安排给别人使用吧”。此时交警看奔奔发扬了风格让出了路权,于是就重新调度安排了路权,让库里南先走。

/**
 * @author 一一哥Sun
 */
public class Demo07 {
	public static void main(String[] args) throws InterruptedException {
		// 采用Lambda表达式创建线程
		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				System.out.println("子线程1执行,i = " + i);
				// 当前子线程放弃CPU资源
				Thread.yield();
			}
		}).start();

		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				System.out.println("子线程2执行,i = " + i);
				// 当前子线程放弃CPU资源
				Thread.yield();
			}
		}).start();

		// 主线程
		for (int i = 0; i < 10; i++) {
			System.out.println("主线程执行,i = " + i);
			// 当前线程放弃CPU资源
			Thread.yield();
		}
	}
}

在这个案例中,我们有3个线程,1个主线程,2个子线程,我们分别在这3个线程中循环调用yield()方法,这就相当于3个人都分别发扬风格让出了自己的CPU资源,大家互相谦让。所以在多次执行时就可能会出现不同的结果,一会A线程在前,一会B线程在前,一会又可能是C线程在前。

4. interrupt()方法

当某个线程调用了interrupt()方法时,该线程就会收到一个中断信号,中断状态会被设置为true,所以interrupt()方法可以用于中断一个线程。当前线程可以在中断信号的提示下进行一些清理工作,然后结束线程的执行。如果调用了interrupt()方法的线程正在被阻塞(如调用了sleep()、wait()、join()等方法),则该线程会抛出一个 InterruptedException异常,从而提前结束阻塞状态。

/**
 * @author 一一哥Sun
 */
public class Demo08 {
	public static void main(String[] args) throws InterruptedException {
		Thread t = new Thread(() -> {
			//判断当前线程是否中断了
            while (!Thread.currentThread().isInterrupted()) {
				System.out.println("Thread 正在运行...");
                try {
                	//休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                	// 发生异常时,中断线程
                    Thread.currentThread().interrupt(); 
                    System.out.println("Thread 中断了");
                }
            }
        });

		//开始执行
        t.start();

        // 主线程等待5秒
        Thread.sleep(5000);
        //中断t线程,如果发现线程t正在被阻塞,则t线程会抛出InterruptedException异常
        t.interrupt();
	}
}

在上面的案例中,我们开启了一个线程t,在该线程内部判断当前线程是否处于中断状态,如果没有中断则每隔1秒钟打印输出一句话。在主线程等待5秒后,线程t调用interrupt()方法进行中断,因为线程t每隔1秒会sleep()一下,所以线程t收到中断信号后就立即抛出了InterruptedException异常。

5. 优先级方法

setPriority()方法用于设置线程的优先级,getPriority()方法用于获取当前线程的优先级。优先级越高的线程在竞争CPU资源时获得的机会越大,所以就可以优先执行。Java中线程的优先级范围是1~10,其中1是最低优先级,10是最高优先级。

/**
 * @author 一一哥Sun
 */
public class Demo09 {
	public static void main(String[] args) throws InterruptedException {
		// 采用Lambda表达式创建线程
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				System.out.println("子线程1执行,i = " + i+",优先级:"+Thread.currentThread().getPriority());
			}
		});

		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				System.out.println("子线程2执行,i = " + i+",优先级:"+Thread.currentThread().getPriority());
			}
		});

		// 设置线程优先级,我们可以直接使用Thread中自带的常量值
		t1.setPriority(Thread.NORM_PRIORITY);
		t2.setPriority(Thread.MAX_PRIORITY);

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

在Thread类,有3个内置的优先级的常量:

  • Thread.*MIN_PRIORITY: *对应的值是1;
  • Thread.*NORM_PRIORITY: *对应的值是5;
  • Thread.*MAX_PRIORITY: *对应的值是10。

6. isAlive()方法

isAlive()方法用于判断当前线程是否处于活动状态,即线程是否还在执行中。该方法会返回一个布尔值,用于表示该线程是否正在运行中,如果线程已经终止或还未启动,则返回false。

当一个线程启动后,它会执行其中的run()方法。只要run()方法没有执行完毕,该线程就处于活动状态,即处于运行中。一旦run()方法执行完毕,线程就会停止运行,即不再处于活动状态。所以当线程已经启动并处于活动状态时,isAlive()方法就会返回true。如果一个线程还没有启动,或者已经执行完毕停止运行,调用isAlive()方法就会返回false

在这种情况下,如果我们需要确定一个线程是否已经停止运行,就可以使用isAlive()方法来进行判断。如果该方法返回true,说明线程还在运行;如果返回false,说明线程已经停止运行。

/**
 * @author 一一哥Sun
 */
public class Demo10 {
	public static void main(String[] args) throws InterruptedException {
        MyThread2 thread = new MyThread2();
        System.out.println("Thread is alive: " + thread.isAlive()); // 输出 false
        thread.start();
        
        System.out.println("Thread is alive: " + thread.isAlive()); // 输出 true
        // 等待2秒,等待线程执行完成
        Thread.sleep(2000);
        System.out.println("Thread is alive: " + thread.isAlive()); // 输出 false
	}
}

class MyThread2 extends Thread {
    public void run() {
        System.out.println("Thread is running...");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread is finished.");
    }
}

在上面的示例中,壹哥创建了一个继承自Thread类的自定义线程MyThread2。在main方法中,壹哥首先创建了一个MyThread2对象,并打印出该线程是否处于活动状态(即是否调用了start方法)。由于线程还没有开始运行,所以isAlive()方法返回false。接着,我们调用了start()方法启动了该线程,并再次调用isAlive()方法,此时返回true,因为线程已经开始运行。最后,我们又通过Thread.sleep()方法等待2秒,等待线程执行完成,再次调用isAlive()方法,此时返回false,因为线程已经执行完毕。

7. wait()和notify()方法

wait()方法是Java中Object类提供的方法,可以使当前线程进入到等待状态,等其他线程发出唤醒通知后,该线程会继续执行。当一个线程调用了wait()方法后,它会释放当前持有的锁,进入到等待状态,直到其他线程调用了相应对象的notify()或 notifyAll()方法来唤醒它。接下来壹哥会设计一个wait()方法的案例,实现两个线程之间的通信。

在接下来的案例中,壹哥在Demo11这个类中定义了一个变量number和两个方法increment()和decrement()。increment()方法会增加变量number的值,而decrement()方法会减少它的值。在主方法中,壹哥会创建两个线程ThreadA和ThreadB,它们分别调用increment()和decrement()方法,并使用wait()和notify()方法实现线程之间的协作。具体就是,当变量number的值为0时,ThreadA线程调用increment()方法增加它的值,并使用notify()方法通知ThreadB线程开始执行decrement()方法。当变量number的值为1时,ThreadB线程会调用decrement()方法来减少它的值,并使用wait()方法等待ThreadA线程发来的通知,直到变量number的值再次变为0。这样,两个线程之间就可以协作地增加和减少变量number的值。

/**
 * @author 一一哥Sun
 */
public class Demo11 {
	//定义一个数字
	private int number = 0;

	//数字加1
	//添加synchronized同步锁,以后壹哥再细讲synchronized的作用
	public synchronized void increment() {
		number++;
		// 完成操作后,通知其他线程可以执行了
		notify();
	}

	//数字减1
	public synchronized void decrement() throws InterruptedException {
		while (number == 0) {
			// 进入等待状态,等待其他线程的通知
			wait();
		}
		//数字减1
		number--;
	}

	public static void main(String[] args) {
		Demo11 demo = new Demo11();

		//创建A线程
		Thread threadA = new Thread(() -> {
			for (int i = 0; i < 5; i++) {
				//数字增加
				demo.increment();
				System.out.println("Thread A: " + demo.number);
			}
		});

		//创建B线程
		Thread threadB = new Thread(() -> {
			for (int i = 0; i < 5; i++) {
				try {
					//数字减少
					demo.decrement();
					System.out.println("Thread B: " + demo.number);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});

		//启动线程
		threadB.start();
		threadA.start();
	}
}

在上面的案例中,壹哥使用了synchronized关键字来保证两个方法的原子性操作,避免了线程的安全问题。同时,我们在decrement()方法中使用while循环来避免虚假的唤醒问题。当变量number为0时,ThreadB线程调用wait()方法等待ThreadA线程的通知,且只有变量number再次变为0时,才会从wait()方法中返回,继续执行decrement()方法。在这个过程中,两个线程之间通过notify()和wait()方法协作,避免了死锁和数据竞争的问题。

8. 其他方法

除了以上方法之外,还有一些其他与线程操作相关的方法,壹哥就不再一一展示对应的代码了,大家可以自行尝试一下。

比如setDaemon()方法用于设置线程是否为守护线程。守护线程是一种特殊的线程,当一个Java应用程序中只剩下守护线程时,JVM就会自动退出。因此通常情况下,我们不应该在守护线程中执行一些需要必须完成的任务,如数据存储等。

getState()方法用于获取线程的状态。线程的状态包括:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)和TERMINATED(终止) 。关于线程状态,壹哥后面再详细介绍。

getName()和setName()方法则分别用于获取和设置线程的名称。

------------------------------正片已结束,来根事后烟----------------------------

三. 结语

今天的这篇文章,壹哥重点给大家介绍了线程中核心方法的使用,请大家多敲代码多熟悉这些方法的使用,并仔细体会这些方法的具体含义,以后这些方法我们都会经常用到。尤其是sleep()、join()、yield()、wait()、interrupt()这几个控制线程执行的方法,我们在面试时经常会被面试官问到这几个方法的区别

  • sleep()方法:使当前线程进入休眠阻塞状态,让出CPU的执行权,不会释放对象的锁,指定时间之后线程会重新进入可运行的就绪状态。常用于模拟耗时操作。
  • join()方法:使得当前线程等待指定的线程执行结束后再继续执行,常用于等待子线程执行完毕后再执行主线程。
  • yield()方法:让出当前线程占用的CPU时间,让其他线程有机会运行。与sleep()不同的是,yield()方法不会进入休眠阻塞状态,而是进入可运行的就绪状态,CPU执行权有可能被立即重新分配给该线程。常用于调试或测试多线程程序。
  • wait()方法:使当前线程开始等待,直到其他线程调用notify()或notifyAll()方法唤醒它,或者等到指定的时间之后自动唤醒。wait()方法必须在同步块或同步方法中调用,调用wait()方法会释放对象的锁。
  • interrupt()方法:用于中断线程的执行。当线程调用interrupt()方法时,该线程会收到一个中断信号,线程可以在中断信号的提示下进行一些清理工作,然后结束线程的执行。

简而言之,sleep()和yield()主要用于控制线程的运行时间和调度,join()方法主要用于等待其他线程的执行结果,wait()主要用于线程间的通信和协作。现在你明白了吗?

如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。