一、多线程技术概述
1、线程与进程
进程
- 是志一个内存运行的应用程序,每个进程都有一个独立的内存空间
线程
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,一个进程最少有一个线程
- 线程实际上是进在进程给基础上的进一步划分,一个进程启动后,里边的若干执行路径又可以划分成若干个线程
进程与线程一样分为五个阶段
- 创建
- 就绪
- 执行
- 阻塞
- 终止
线程的6种状态
- 初始 (new):新创建一个线程对象,但还没有调用start()方法。
- 运行(runnable):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行“。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程获取CPU的时间片后变为运行中状态(running)。
- 阻塞(blocked):表示线程阻塞于锁。
- 等待(waiting):进入该状态的线程需要等待其他线程做出一些特定的动作(通知和中断)。
- 超时等待(timed_waiting):该状态不同于waiting,它可以再指定的时间后自行返回。
- 终止(terminated):表示该线程已经执行完毕。
2、线程调度
分时调度
- 所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间。
抢占式调度
- 优先让优先级搞得线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),java使用的是抢占式调度。
- CPU使用抢占式调度模式在多个线程间进行着告诉的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
3、同步异步
同步:排队执行,效率低但是安全
异步:同时执行,效率高但是数据不安全
4、并发与并行
并发:指两个或多个事件在同一个时间段发生
并行:指两个或多个事件在同一时刻发生
二、线程的创建方式
1、继承 Thread 类
package com.basics.bather.kaikeba_basics.hexinlei.duoxianchen.summary; /** * @description: 多线的创建方式一:继承Thread类 * @autor: WJY * @create: 2021-08-23 16:59 * @since: 1.8 */ public class ThreadSummary extends Thread{ private String name; public ThreadSummary(String name) { this.name = name; } /** * 重写Thread中的run()方法 */ @Override public void run() { /** * 可以通过调用Thread类中的start()方法启动线程执行run() */ for (int i = 0; i < 3; i++) { System.out.println(name + "线程执行了" + (i+1) + "次"); } } public static void main(String[] args) { // 创建两个对象,分别生成两个线程 ThreadSummary a = new ThreadSummary("A"); ThreadSummary b = new ThreadSummary("B"); // 将线程启动 a.start();; b.start(); } } |
输出: A线程执行了1次 B线程执行了1次 A线程执行了2次 B线程执行了2次 A线程执行了3次 B线程执行了3次 |
说明:
程序启动执行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法。另外两个线程也启动了,这样,整个应用就在多线程下执行。
2、实现Runable接口
package com.basics.bather.kaikeba_basics.hexinlei.duoxianchen.summary; /** * @description: 多线程创建方式二:实现Runable接口 * @autor: WJY * @create: 2021-08-23 17:24 * @since: 1.8 */ public class RunableSummary implements Runnable { private String name; public RunableSummary(String name) { this.name = name; } @Override public void run() { /** * 可以通过调用创建Thread线程调用start()方法启动线程执行run() */ for (int i = 0; i < 3; i++) { System.out.println(name + "线程执行了" + (i+1) + "次"); } } public static void main(String[] args) { // 创建线程对象然后传入运行的线程内容,通过调用Thread中的start()方法启动线程 new Thread(new RunableSummary("A")).start(); new Thread(new RunableSummary("B")).start(); } } |
运行结果: A线程执行了1次 B线程执行了1次 B线程执行了2次 B线程执行了3次 A线程执行了2次 A线程执行了3次 |
说明:
Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。全部的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候。须要先通过Thread类的构造方法Thread(Runnable target) 构造出对象。然后调用Thread对象的start()方法来执行多线程代码。
实际上全部的多线程代码都是通过执行Thread的start()方法来执行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,终于还是通过Thread的对象的API来控制线程的。熟悉Thread类的API是进行多线程编程的基础。
3、实现Callable<返回值类型> 接口
package com.basics.bather.kaikeba_basics.hexinlei.duoxianchen.summary; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * @description: 线程创建的第三种方式:实现Callable<返回至类型>接口 -> 可以获取线程的运行结果 * @autor: WJY * @create: 2021-08-23 17:43 * @since: 1.8 */ public class CallableSummary implements Callable<Integer> { private String name; public CallableSummary(String name) { this.name = name; } @Override public Integer call() throws Exception { // 实例化线程的运行结果 Integer num = 0; // 线程的执行内容 for (int i = 0; i < 3; i++) { System.out.println(name + "线程执行了" + (i+1) + "次"); num ++; } return num; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建FutureTask对象,并且将需要执行的内容实例化入对象 FutureTask<Integer> futureTask1 = new FutureTask<>(new CallableSummary("A")); // 创建FutureTask对象,并且将需要执行的内容实例化入对象 FutureTask<Integer> futureTask2 = new FutureTask<>(new CallableSummary("B")); // 通过创建一个线程将futureTask对象实例化入线程对象中,调用start()方法启动线程 new Thread(futureTask2).start(); // 通过创建一个线程将futureTask对象实例化入线程对象中,调用start()方法启动线程 new Thread(futureTask1).start(); // 获取响应的结果 get()方法会等待对应的线程执行结束,在调用这个方法时,主线程从get()方法往下的代码不会执行 System.out.println(futureTask1.get()); System.out.println(futureTask2.get()); } } |
运行结果: A线程执行了1次 B线程执行了1次 A线程执行了2次 B线程执行了2次 A线程执行了3次 B线程执行了3次 3 3 |
三、Thread、Runable、Callable<T>之间的区别
1、Thread和Runable之间的差别(Runable的优势)
- 是个多个同样的程序代码的线程去处理同一个资源
- 能够避免java中单继承的限制
- 添加的程序的健壮性,代码能够被多个线程共享,代码和数据独立
- 线程池仅仅能放入实现Runable或Callable线程,不能直接放入Thread的类
2、Runable和Callable<T>之间的差别
相同点:
- 都是接口
- 都可以编写多线程程序
- 都是采用Thread.start()启动线程(new Thread(传入线程对象).start)
不同点:
- Runable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runable的run()不能抛出异常
注意:
Callable接口支持返回执行结果,需要调用FutureTask.get()得到,但是这个方法会阻主线程继续往下执行,如果不调用则不会阻塞。
四、线程的状态切换
1、线程的6中状态&图
- 初始 (new):新创建一个线程对象,但还没有调用start()方法。
- 运行(runnable):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行“。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程获取CPU的时间片后变为运行中状态(running)。
- 阻塞(blocked):表示线程阻塞于锁。
- 等待(waiting):进入该状态的线程需要等待其他线程做出一些特定的动作(通知和中断)。
- 超时等待(timed_waiting):该状态不同于waiting,它可以再指定的时间后自行返回。
- 终止(terminated):表示该线程已经执行完毕。
2、线程6种状态详解
初始状态(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
就绪状态(RUNNABLE之READY)
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
调用线程的start()方法,此线程进入就绪状态。
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
锁池里的线程拿到对象锁后,进入就绪状态。
运行中状态(RUNNABLE之RUNNING)
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
阻塞状态(BLOCKED)
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
终止状态(TERMINATED)
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。