一、多线程基础知识
1.进程和线程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程:进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。
进程和线程的区别:
- 进程有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。线程堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
- 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。
- 程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行,线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点
- 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间
注意:
- 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。
- Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
- 创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。
2.并行和并发
并行:指两个或多个时间在同一时刻发生(同时发生)
并发:指两个或多个事件在一个时间段内发生。
注意:单核处理器的计算机肯定不能并行的处理多个任务,只能是多个任务交替的在单个 CPU 上运行。
3.线程状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的成为“运行”。
- 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得cpu 时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIME_WAITING):该状态不同于WAITING,它可以在指定的时间内自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
4.守护线程和非守护线程
守护线程:和主线程一起结束的线程,叫守护线程,例如典型的守护线程---(GC)垃圾回收线程。
非守护线程:主线程的结束不影响线程的执行的线程,也叫用户线程。例如用户线程。
注意:
- 用户可以将非守护线程设置为守护线程,利用Thread.setDaemon()即可。thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程
- 在守护线程中产生的新线程也是守护线程
- 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。
5.多线程和单线程的选择
对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,因为解压文件需要一直占用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。但是对于比如交互类型的任务,肯定是需要使用多线程的、而对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。虽然多线程能够提升程序性能,但是相对于单线程来说,它的编程要复杂地多,要考虑线程安全问题。因此,在实际编程过程中,要根据实际情况具体选择。
6.上下文切换
对于单核CPU来说,同一个时刻,CPU只能运行一个线程,当运行一个线程过程中发生线程切换,去运行另外一个线程。这个过程即线程的上下文切换,进程的上下文切换类似。注意:对于线程的上下文切换实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。多线程可以提高程序的运行效率,但是线程的创建和销毁,以及线程的上下文切换非常的消耗资源,所以有时候多线程未必能带给我们想要的效果。
7.同步与异步
同步:单任务模式,排队执行,在一个操作结束前,另外一个操作需要等待。
异步:多任务模式,多个任务切换占用CPU时间片,切换执行,多线程是异步的。线程被调用的时机是随机的。
二、多线程--java线程的创建
java中创建线程的话,一般有两种方式:1)继承Thread类;2)实现Runnable接口。
1.继承Thread类
1 /**
2 * 继承Thread类,需要重写 Thread 类的 run()方法
3 */
4 public class ThreadDemo extends Thread{
5
6 @Override
7 public void run() {
8 System.out.println("子线程执行完毕");
9 }
10 public static void main(String[] args) {
11 ThreadDemo t1 = new ThreadDemo();
12 t1.start();//启动线程,调用run
13 System.out.println("主线程执行完毕");
14 }
15
16 }
结果输出:
主线程执行完毕
子线程执行完毕
或者
子线程执行完毕
主线程执行完毕
通过继承Thread类创建线程类后,通过start()方法启动线程,不是调用run()方法启动线程,run方法中只是定义需要执行的任务,如果调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。
2.实现Runnable接口
1 public class RunableDemo implements Runnable{
2
3 @Override
4 public void run() {
5 System.out.println("子线程执行完毕1");
6 }
7
8 public static void main(String[] args) {
9 new Thread(new RunableDemo()).start();
10 System.out.println("主线程执行完毕1");
11 }
12
13 }
结果输出:
子线程执行完毕1
主线程执行完毕1
或
主线程执行完毕1
子线程执行完毕1
通过实现Runnable接口,我们定义了一个子任务,然后将子任务交由Thread去执行。注意,这种方式必须将Runnable作为Thread类的参数,然后通过Thread的start方法来创建一个新线程来执行该子任务。如果调用Runnable的run方法的话,是不会创建新线程的,与普通的方法调用没有任何区别。
3.优先选择通过实现接口的方式来实现多线程的原因
- 可以避免由于Java的单继承特性而带来的局限;
- 增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;
- 适合多个相同程序代码的线程区处理同一资源的情况。