多线程详解(1)——线程基本概念

6,256 阅读12分钟

0. 简介

这个系列开始来讲解 Java 多线程的知识,这节就先讲解多线程的基本知识。

1. 进程与线程

1.1 什么是进程?

进程就是在运行过程中的程序,就好像手机运行中的微信,QQ,这些就叫做进程。

1.2 什么是线程?

线程就是进程的执行单元,就好像一个音乐软件可以听音乐,下载音乐,这些任务都是由线程来完成的。

1.3 进程与线程的关系

  • 一个进程可以拥有多个线程,一个线程必须要有一个父进程
  • 线程之间共享父进程的共享资源,相互之间协同完成进程所要完成的任务
  • 一个线程可以创建和撤销另一个线程,同一个进程的多个线程之间可以并发执行

2. 如何创建线程

Java 中创建线程的方法有三种,以下来逐一详细讲解。

2.1 继承 Thread 类创建线程

使用继承 Thread 类创建线程的步骤如下:

  1. 新建一个类继承 Thread 类,并重写 Thread 类的 run() 方法。
  2. 创建 Thread 子类的实例。
  3. 调用该子类实例的 start() 方法启动该线程。

代码举例如下:

public class ThreadDemo extends Thread {
	
	// 1. 新建一个类继承 Thread 类,并重写 Thread 类的 run() 方法。
	@Override
	public void run() {
		System.out.println("Hello Thread");
	}
	
	public static void main(String[] args) {
		
		// 2. 创建 Thread 子类的实例。
		ThreadDemo threadDemo = new ThreadDemo();
		// 3. 调用该子类实例的 start() 方法启动该线程。
		threadDemo.start();
		
	}

}

打印结果如下:

Hello Thread

2.2 实现 Runnable 接口创建线程

使用实现 Runnable 接口创建线程步骤是:

  1. 创建一个类实现 Runnable 接口,并重写该接口的 run() 方法。
  2. 创建该实现类的实例。
  3. 将该实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
  4. 调用该 Thread 线程对象的 start() 方法。

代码举例如下:


public class RunnableDemo implements Runnable {

	// 1. 创建一个类实现 Runnable 接口,并重写该接口的 run() 方法。
	@Override
	public void run() {
		System.out.println("Hello Runnable");
	}

	
	public static void main(String[] args) {
		
		// 2. 创建该实现类的实例。
		RunnableDemo runnableDemo = new RunnableDemo();
		
		// 3. 将该实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
		Thread thread = new Thread(runnableDemo);
		
		// 4. 调用该 Thread 线程对象的 start() 方法。
		thread.start();
		
	}
	

}

打印结果如下:

Hello Runnable

2.3 使用 Callable 和 FutureTask 创建线程

使用这种方法创建的线程可以获取一个返回值,使用实现 Callable 和 FutureTask 创建线程步骤是:

  1. 创建一个类实现 Callable 接口,并重写 call() 方法。
  2. 创建该 Callable 接口实现类的实例。
  3. 将 Callable 的实现类实例传入 FutureTask(Callable callable) 构造方法中创建 FutureTask 实例。
  4. 将 FutureTask 实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
  5. 调用该 Thread 线程对象的 start() 方法。
  6. 调用 FutureTask 实例对象的 get() 方法获取返回值。

代码举例如下:

public class CallableDemo implements Callable<String> {

	// 1. 创建一个类实现 Callable 接口,并重写 call() 方法。
	@Override
	public String call() throws Exception {
		System.out.println("CallableDemo is Running");
		return "Hello Callable";
	}
	
	public static void main(String[] args) {
		
		// 2. 创建该 Callable 接口实现类的实例。
		CallableDemo callableDemo = new CallableDemo();
		
		// 3. 将 Callable 的实现类实例传入 FutureTask(Callable<V> callable) 构造方法中创建 FutureTask 实例。
		FutureTask<String> futureTask = new FutureTask<>(callableDemo);
		
		// 4. 将 FutureTask 实例传入 Thread(Runnable r) 构造方法中创建 Thread 实例。
		Thread thread = new Thread(futureTask);
		
		// 5. 调用该 Thread 线程对象的 start() 方法。
		thread.start();
		
		// 6. 调用 FutureTask 实例对象的 get() 方法获取返回值。
		try {
			System.out.println(futureTask.get());
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}

}

打印结果如下:

CallableDemo is Running
Hello Callable

3. 线程的生命周期

当一个线程开启之后,它会遵循一定的生命周期,它要经过新建,就绪,运行,阻塞和死亡这五种状态,理解线程的生命周期有助于理解后面的相关的线程知识。

3.1 新建状态

这个状态的意思就是线程刚刚被创建出来,这时候的线程并没有任何线程的动态特征。

3.2 就绪状态

当线程对象调用 start() 方法后,该线程就处于就绪状态。处于这个状态中的线程并没有开始运行,只是表示这个线程可以运行了。

3.3 运行状态

处于就绪状态的线程获得了 CPU 后,开始执行 run() 方法,这个线程就处于运行状态。

3.4 阻塞状态

当线程被暂停后,这个线程就处于阻塞状态。

3.5 死亡状态

当线程被停止后,这个线程就处于死亡状态。

其实掌握多线程最主要的就是要熟悉控制线程的状态,让各个线程能更好的为我们的服务,下面就来讲解控制线程的方法。

4. 控制线程

4.1 sleep()

4.1.1 线程生命周期的变化

sleep()

4.1.2 方法预览

public static native void sleep(long millis)
public static void sleep(long millis, int nanos)

该方法的意思就是让正在运行状态的线程到阻塞状态,而这个时间就是线程处于阻塞状态的时间。millis 是毫秒的意思,nanos 是毫微秒。

4.1.3 代码举例

public class SleepDemo {
	
	public static void main(String[] args) throws Exception {
		
		for(int i = 0; i < 10; i++) {
			System.out.println("Hello Thread Sleep");
			Thread.sleep(1000);
		}
		
	}

}

以上代码运行后每隔一秒就输出 Hello Thread Sleep。

4.2 线程优先级

4.2.1 方法预览

public final void setPriority(int newPriority)
public final int getPriority()

从方法名就可以知道,以上两个方法分别就是设置和获得优先级的。值得注意的是优先级是在 1~10 范围内,也可以使用以下三个静态变量设置:

  • MAX_PRIORITY:优先级为 10
  • NORM_PRIORITY:优先级为 5
  • MIN_PRIORITY:优先级为 1

4.3 yield()

4.3.1 线程生命周期的变化

yield()

4.3.2 方法预览

public static native void yield();

这个方法的意思就是让正在运行的线程回到就绪状态,并不会阻塞线程。可能会发生一种情况就是,该线程调用了 yield() 方法后,线程调度器又会继续调用该线程。 这个方法要注意的是它只会让步给比它优先级高的或者和它优先级相同并处在就绪状态的线程。

4.3.3 代码举例

public class YieldDemo extends Thread {

	@Override
	public void run() {

		for (int i = 0; i < 50; i++) {
			System.out.println(getName() + " " + i);

			if (i == 20) {
				Thread.yield();
			}
		}

	}

	public static void main(String[] args) {

		YieldDemo yieldDemo1 = new YieldDemo();
		YieldDemo yieldDemo2 = new YieldDemo();

		yieldDemo1.start();
		yieldDemo2.start();

	}

}

代码输出结果:

Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-1 6
Thread-1 7
Thread-1 8
Thread-1 9
Thread-1 10
Thread-1 11
Thread-1 12
Thread-1 13
Thread-1 14
Thread-0 0
Thread-0 1
Thread-0 2
Thread-1 15
Thread-0 3
Thread-1 16
Thread-1 17
Thread-1 18
Thread-1 19
Thread-1 20
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-1 21
Thread-0 8
Thread-1 22
Thread-0 9
Thread-1 23
Thread-0 10
Thread-0 11
Thread-0 12
Thread-1 24
Thread-1 25
Thread-0 13
Thread-1 26
Thread-1 27
Thread-0 14
Thread-0 15
Thread-1 28
Thread-0 16
Thread-0 17
Thread-0 18
Thread-1 29
Thread-0 19
Thread-0 20
Thread-1 30
Thread-1 31
Thread-0 21
Thread-1 32
Thread-1 33
Thread-1 34
Thread-1 35
Thread-1 36
Thread-1 37
Thread-1 38
Thread-1 39
Thread-1 40
Thread-1 41
Thread-1 42
Thread-1 43
Thread-1 44
Thread-1 45
Thread-1 46
Thread-1 47
Thread-1 48
Thread-1 49
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
Thread-0 30
Thread-0 31
Thread-0 32
Thread-0 33
Thread-0 34
Thread-0 35
Thread-0 36
Thread-0 37
Thread-0 38
Thread-0 39
Thread-0 40
Thread-0 41
Thread-0 42
Thread-0 43
Thread-0 44
Thread-0 45
Thread-0 46
Thread-0 47
Thread-0 48
Thread-0 49

从打印结果就可以看到打印 Thread-1 20的时候,下一个执行的就是 Thread-0 4。打印 Thread-20 的时候,下一个执行的就是 Thread-1 30。 但是要说明的是,不是每次的打印结果都是一样的,因为前面说过线程调用 yield() 方法后,线程调度器有可能会继续启动该线程。

4.4 join()

4.4.1 线程生命周期的变化

join()

4.4.2 方法预览

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

这个方法其实要有两个线程,也就是一个线程的线程执行体中有另一个线程在调用 join() 方法。举个例子,Thread1 的 run() 方法执行体中有 Thread2 在调用 join(),这时候 Thread1 就会被阻塞,必须要等到 Thread2 的线程执行完成或者 join() 方法的时间到后才会继续执行。

4.4.3 join() 代码举例


public class JoinDemo extends Thread {
	
	@Override
	public void run() {
		for(int i = 0; i < 50; i++) {
			
			System.out.println(getName() + " " + i);
		}
	}
	
	public static void main(String[] args) throws Exception {
		
		JoinDemo joinDemo = new JoinDemo();
		
		for(int i = 0; i < 50; i++) {
			
			if(i == 20) {
				
				joinDemo.start();
				joinDemo.join();
				
			}
			
			System.out.println(Thread.currentThread().getName() + " " + i);
			
		}
		
		
	}

}

代码输出的结果:

main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
Thread-0 11
Thread-0 12
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
Thread-0 20
Thread-0 21
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
Thread-0 30
Thread-0 31
Thread-0 32
Thread-0 33
Thread-0 34
Thread-0 35
Thread-0 36
Thread-0 37
Thread-0 38
Thread-0 39
Thread-0 40
Thread-0 41
Thread-0 42
Thread-0 43
Thread-0 44
Thread-0 45
Thread-0 46
Thread-0 47
Thread-0 48
Thread-0 49
main 21
main 22
main 23
main 24
main 25
main 26
main 27
main 28
main 29
main 30
main 31
main 32
main 33
main 34
main 35
main 36
main 37
main 38
main 39
main 40
main 41
main 42
main 43
main 44
main 45
main 46
main 47
main 48
main 49

以上的代码其实一个两个线程,一个是 Thread-0,另一个就是 main,main 就是主线程的意思。从打印结果可以看到,主线程执行到 main 20 的时候,就开始执行 Thread-0 0,直到 Thread-0 执行完毕,main 才继续执行。

4.4.4 join(long millis) 代码举例

public class JoinDemo extends Thread {
	
	@Override
	public void run() {
		for(int i = 0; i < 50; i++) {
			
			System.out.println(getName() + " " + i);
		}
	}
	
	public static void main(String[] args) throws Exception {
		
		JoinDemo joinDemo = new JoinDemo();
		
		for(int i = 0; i < 50; i++) {
			
			System.out.println(Thread.currentThread().getName() + " " + i);
			
			if(i == 20) {
				
				joinDemo.start();
				joinDemo.join(1);
				
			}
			
		}
		
		
	}

}

打印结果:

main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
Thread-0 11
Thread-0 12
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
Thread-0 20
Thread-0 21
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
Thread-0 30
Thread-0 31
Thread-0 32
Thread-0 33
main 21
main 22
Thread-0 34
main 23
Thread-0 35
Thread-0 36
main 24
main 25
main 26
Thread-0 37
main 27
Thread-0 38
main 28
Thread-0 39
main 29
Thread-0 40
main 30
Thread-0 41
main 31
Thread-0 42
main 32
main 33
main 34
main 35
Thread-0 43
Thread-0 44
Thread-0 45
main 36
Thread-0 46
main 37
main 38
main 39
main 40
main 41
main 42
main 43
main 44
main 45
main 46
main 47
main 48
main 49
Thread-0 47
Thread-0 48
Thread-0 49

其实这个的代码和 4.4.3 节的代码基本一样,就是将 join() 改成 join(1) ,可以看到 main 并没有等到 Thread-0 执行完就开始重新执行了。

4.5 后台线程

4.5.1 方法预览

public final void setDaemon(boolean on)
public final boolean isDaemon()

这个方法就是将线程设置为后台线程,后台线程的特点就是当前台线程全部执行结束后,后台线程就会随之结束。此方法设置为 true 时,就是将线程设置为后台线程。 而 isDaemon() 就是返回此线程是否为后台线程。

4.5.2 代码举例


public class DaemonDemo extends Thread {

	@Override
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.println(getName() + " "+ isDaemon() + " " + i);
		}
	}
	
	public static void main(String[] args) {
		
		DaemonDemo daemonDemo = new DaemonDemo();
		
		daemonDemo.setDaemon(true);
		
		daemonDemo.start();
		
		for(int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
		}
		
	}
	
}

打印结果:

main 0
main 1
main 2
main 3
main 4
Thread-0 true 0
main 5
main 6
main 7
main 8
main 9
Thread-0 true 1
Thread-0 true 2
Thread-0 true 3
Thread-0 true 4
Thread-0 true 5
Thread-0 true 6
Thread-0 true 7
Thread-0 true 8
Thread-0 true 9
Thread-0 true 10
Thread-0 true 11

从打印结果可以看到 main 执行完后,Thread-0 没有执行完毕就结束了。

5. 一些注意点

5.1 sleep() 和 yield() 区别

作用处 sleep() yield()
给其他线程执行机会 会给其他线程执行机会,不会理会其他线程的优先级 只会给优先级相同,或者优先级更高的线程执行机会
影响当前线程的状态 从阻塞到就绪状态 直接进入就绪状态
异常 需要抛出 InterruptedException 不需要抛出任何异常

多线程系列文章:

多线程详解(2)——不得不知的几个概念