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

0. 简介

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

1. 进程与线程

1.1 什么是进程?

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

1.2 什么是线程?

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

1.3 进程与线程的关系

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

2. 如何建立线程

Java 中建立线程的方法有三种,如下来逐一详细讲解。并发

2.1 继承 Thread 类建立线程

使用继承 Thread 类建立线程的步骤以下:ide

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

代码举例以下:post

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();
		
	}

}

复制代码

打印结果以下:ui

Hello Thread
复制代码

2.2 实现 Runnable 接口建立线程

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

  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();
		
	}
	

}

复制代码

打印结果以下:code

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)——不得不知的几个概念

相关文章
相关标签/搜索