「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
一、多线程理论
1.1、操作系统的发展
在计算机发明之前,人们处理大量的计算是通过人工处理的,耗费人力,成本很大而且错误较多。为了处理大量的数学计算问题,人们发明了计算机。
最初的计算机只能接受一些特定的指令,用户输入一个指令,计算机就做出一个操作。当用户在思考或者输入时,计算机就在等待。显然这样效率低下,在很多时候,计算机都处在等待状态。
1.1.1、批处理操作系统
既然传统计算机那么慢,那么能不能把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机,计算机通过不断得读取指令进行相应的操作。
就这样,批处理操作系统诞生了。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。
1.1.2、如何提高CPU利用率
虽然批处理操作系统的诞生提高了任务处理的便捷性(省略了用户输入的时间),但是仍然存在一个很大的问题:
假如有两个任务A和B,需要读取大量的数据输入(I/O操作),而其实CPU只能处在等待状态,等任务A读取完数据再能继续进行,这样就白白浪费了CPU资源。于是人们就想,能否在任务A读取数据的过程中,让任务B去执行,当任务A读取完数据之后,暂停任务B,让任务A继续执行?
这时候又出现了几个问题:内存中始终都只有一个程序在运行,而想要解决上述问题,必然要在内存中装入多个程序,如何处理呢?多个程序使用的数据如何辨别?当一个程序暂停后,随后怎么恢复到它之前执行的状态呢?
1.1.3、进程来了
这时候,人们就发明了进程,用一个进程对应一个程序,每个进程都对应一定的内存地址和内存空间,并且只能自己使用自己的内存空间,多个进程之间的内存互不共享,且进程之间彼此不打扰。
进程同时也保存了程序每时每刻的运行状态,为进程切换提供了如可能。
当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的2状态进行恢复,接着继续执行。
1.2、并发和并行
1.2.1、并发
并发是能够让操作系统从宏观上看起来同一时间段执行多个任务。 换句话说,进程让操作体统的并发成为了可能,至此出现多任务操作系统。
虽然并发从宏观上看是有多个任务在执行,但是实际上对于单核CPU来说,任意具体时刻都只有一个任务在占用CPU资源,操作系统一般通过CPU时间片轮转来实现并发。
总的来说,并发就是在一段时间内多个进程轮流使用同一个 CPU,多个进程形成并发。
1.2.2、并行
在同一时刻多个进程使用各自的 CPU,多个进程形成并行。并行需要多个 CPU 支持。
1.3、线程
1.3.1、线程出现的原因
出现了进程之后,操作系统的性能(CPU利用率)得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们不满足,逐渐对实时性有了要求。因为一个进程在一个时间段内只能做一个事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
举一个例子:对于监控系统这个进程来说,不仅要与服务器端进行通信获取图像数据并将图像信息显示在画面上,还要处理与用户的交互操作。如果在一个时刻该系统正在与服务器通信获取图像数据,而用户在监控系统上点击了一个按钮,那么系统只能等获取完图像后才能与用户进行交互操作。如果获取图像需要10s,用户就得等待10s。显然这样的系统,无法满足人们的需求。
1.3.2、线程
为了让子任务可以分开执行,即上个例子说的,在与服务器通信获取图形数据的同时相应用户,为了处理这种情况,人们发明了线程,一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。在用户点击按钮的时候,可以暂停获取图像数据的线程,让出CPU资源,让UI线程获取CPU资源,响应用户的操作,响应完后再切换回来,获取图像数据的线程重新获取CPU资源。让用户感觉系统在同时做很多事,满足用户对实时性的要求。线程的出现是为了解决实时性的问题。
总的来说,线程是进程的细分,通常,在实时性操作系统中,进程会被划分为多个可以独立运行的子任务,这些子任务被称为线程,多个线程配合完成一个进程的任务。
注意
一个进程包含多个线程,但是这些线程共享进程占有的内存地址空间和资源。进程是操作系统进行资源分配的基本单位(进程之间互不干扰),而线程是操作系统进行CPU调度的基本单位(线程间互相切换)。
1.3.3、线程工作的原理
假设 P 进程抢占 CPU 后开始执行,此时如果 P 进行正在进行获取网络资源的操作时,用户进行UI 操作,此时 P 进程不会响应 UI 操作。可以把 P 进程可以分为 Ta、Tb 两个线程。Ta 用于获取网络资源,Tb 用于响应 UI 操作。此时如果 Ta 正在执行获取网络资源时、用户进行 UI 操作,为了做到实时性,Ta 线程暂时挂起,Tb 抢占 CPU 资源,执行 UI 操作,UI 操作执行完成后让出CPU,Ta 抢占 CPU 资源继续执行请求网络资源。
总结
1.线程再一次提高了CPU的利用率
- 线程是包含在进程中,是对进程任务的细分,线程共享进程资源(内存资源等)
- 线程细分后称为 CPU 调度的基本单位。进程称为操作系统资源分配的基本单位。
1.4、线程和进程的区别
- 根本区别:进程是操作系统资源分配的基本单位,而线程是CPU调度和执行的基本单位
- 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
- 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
- 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分。
1.5、线程调度
1.5.1、分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
1.5.2、抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
二、实现线程的方式
在 Java 中实现线程的方式有 2 种,一种是继承 Thread,一种是实现 Runnable 接口。
如果一个进程没有任何线程,我们成为单线程应用程序;如果一个进程有多个线程存在,我们成为多线程应用程序。进程执行时一定会有一个主线程(main 线程)存在,主线程有能力创建其他线程。多个线程抢占 CPU,导致程序的运行轨迹不确定。多线程的运行结果也不确定。
2.1、继承Thread类
线程开启我们需要用到了java.lang.Thread
类,API中该类中定义了有关线程的一些方法,具体如下:
构造方法
public Thread()
:分配一个新的线程对象。public Thread(String name)
:分配一个指定名字的新的线程对象。public Thread(Runnable target)
:分配一个带有指定目标新的线程对象。public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字。
常用方法
public String getName()
:获取当前线程名称。public void start()
:导致此线程开始执行; Java虚拟机调用此线程的run方法。public void run()
:此线程要执行的任务在此处定义代码。public static void sleep(long millis)
:使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。public static Thread currentThread()
:返回对当前正在执行的线程对象的引用。
继承 Thread 实现多线程,必须重写 run 方法,启动的时候调用的也是调用线程对象的start()方法来启动该线程,如果直接调用run()方法的话,相当于普通类的执行,此时相当于只有主线程在执行。
package day16_thread.classing.thread; /** * @author Xiao_Lin * @date 2020/12/20 11:40 */ public class MyThread extends Thread{ @Override public void run() { for (int i =1;i<501;i++){ System.out.println("A Thread"+i); } } }
package day16_thread.classing.thread; /** * @author Xiao_Lin * @date 2020/12/20 11:41 */ public class TestThread { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); for (int i=1;i<501;i++){ System.out.println("MainThread"+i); } } }
从结果我们可以看出,每一次抢占CPU资源的线程是不同的,多个线程轮流使用 CPU,谁先抢占到谁使用 CPU 并执行线程。所以执行结果不确定。
2.1.1、继承Thread类的优点
编码简单
2.1.2、继承Thread类的缺点
线程类已经继承了Thread类了就无法再继承其他类了,功能不能通过其他类继承拓展,功能没有那么强大。
2.2、实现 Runnable 接口
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run方法即可。
步骤如下:
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动线程。
package day16_thread.classing.thread; /** * @author Xiao_Lin * @date 2020/12/20 13:49 */ public class MyRun implements Runnable { @Override public void run() { for (int i =1;i<501;i++){ System.out.println("A Thread"+i); } } }
package day16_thread.classing.thread; /** * @author Xiao_Lin * @date 2020/12/20 13:49 */ public class TestMyRun { public static void main(String[] args) { Thread thread = new Thread(new MyThread()); thread.start(); for (int i=1;i<501;i++){ System.out.println("MainThread"+i); } } }
2.2.1、实现Runnable的接口的优点
- 线程任务类只是实现了Runnable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现Runable或Callable线程任务对象。
- 其实Thread类本身也是实现了Runnable接口的。
- 唯一的遗憾是不能直接得到线程执行的结果!
2.3、实现Callable接口(拓展)
实现多线程还有另一种方式,那就是实现Callable
接口,前面的两种方式都没办法拿到线程执行返回的结果,因为run()方法都是void修饰的。但是这种方式是可以拿到线程执行返回的结果。
步骤
- 定义一个线程任务类实现Callable接口 , 申明线程执行的结果类型。
- 重写线程任务类的call方法,这个方法可以直接返回执行的结果。
- 创建一个Callable的线程任务对象。
- 把Callable的线程任务对象包装成一个未来任务对象。
- 把未来任务对象包装成线程对象。
- 调用线程的start()方法启动线程。
package day16_thread.classing.thread; /** * @author Xiao_Lin * @date 2020/12/20 13:49 */ // 1.创建一个线程任务类实现Callable接口,申明线程返回的结果类型 class MyCallable implements Callable<String>{ // 2.重写线程任务类的call方法! @Override public String call() throws Exception { // 需求:计算1-10的和返回 int sum = 0 ; for(int i = 1 ; i <= 10 ; i++ ){ System.out.println(Thread.currentThread().getName()+" => " + i); sum+=i; } return Thread.currentThread().getName()+"执行的结果是:"+sum; } } public class ThreadDemo { public static void main(String[] args) { // 3.创建一个Callable的线程任务对象 Callable call = new MyCallable(); // 4.把Callable任务对象包装成一个未来任务对象 // -- public FutureTask(Callable<V> callable) // 未来任务对象是啥,有啥用? // -- 未来任务对象其实就是一个Runnable对象:这样就可以被包装成线程对象! // -- 未来任务对象可以在线程执行完毕之后去得到线程执行的结果。 FutureTask<String> task = new FutureTask<>(call); // 5.把未来任务对象包装成线程对象 Thread t = new Thread(task); // 6.启动线程对象 t.start(); for(int i = 1 ; i <= 10 ; i++ ){ System.out.println(Thread.currentThread().getName()+" => " + i); } // 在最后去获取线程执行的结果,如果线程没有结果,让出CPU等线程执行完再来取结果 try { String rs = task.get(); // 获取call方法返回的结果(正常/异常结果) System.out.println(rs); } catch (Exception e) { e.printStackTrace(); } } }
2.3.1、实现Callable接口优点
- 线程任务类只是实现了Callable接口,可以继续继承其他类,而且可以继续实现其他接口(避免了单继承的局限性)
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个多个线程去共享同一个资源
- 实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立。
- 线程池可以放入实现Runable或Callable线程任务对象。
- 能直接得到线程执行的结果!
- 唯一的遗憾就是编码比较复杂,写的代码会比较多。
2.4、两种实现方式的区别
需求:模拟售票窗口买票的过程,共有五张票
2.4.1、Thread实现
package day16_thread.classing.thicks; /** * @author Xiao_Lin * @date 2020/12/20 13:53 */ public class MyThread extends Thread{ private static int count = 5; public MyThread() { } public MyThread(String name) { super(name); } @Override public void run() { for (int i=0;i<5;i++){ if (count>0){ count--; System.out.println(super.getName()+"卖了一张票。还剩下"+count+"张票"); } } } }
package day16_thread.classing.thicks; /** * @author Xiao_Lin * @date 2020/12/20 13:55 */ public class TestThread { public static void main(String[] args) { MyThread t1 = new MyThread("窗口A"); MyThread t2 = new MyThread("窗口B"); MyThread t3 = new MyThread("窗口C"); MyThread t4 = new MyThread("窗口D"); t1.start(); t2.start(); t3.start(); t4.start(); } }
2.4.2、Runable实现
package day16_thread.classing.thicks; /** * @author Xiao_Lin * @date 2020/12/20 14:15 */ public class MyRun implements Runnable { private int count = 5; @Override public void run() { for (int i=0;i<5;i++){ if (count>0){ count--; System.out.println(Thread.currentThread().getName()+"卖了一张票。还剩下"+count+"张票"); } } } }
package day16_thread.classing.thicks; /** * @author Xiao_Lin * @date 2020/12/20 14:17 */ public class TestRun { public static void main(String[] args) { MyRun myRun = new MyRun(); Thread t1 = new Thread(myRun,"窗口A"); Thread t2 = new Thread(myRun,"窗口B"); Thread t3 = new Thread(myRun,"窗口C"); Thread t4 = new Thread(myRun,"窗口D"); t1.start(); t2.start(); t3.start(); t4.start(); } }
2.4.3、两者实现的区别
- 继承Thread类后,不能再继承其他类,而实现了Runnable接口后还可以继承其他类。
- 实现Runnable接口更方便共享资源,同一份资源,多个线程并发访问,如果多个线程需要访问共享资源,优先考虑Runnable方式,如果线程不访问共享资源,可以考虑继承Thread。
- Thread类本身也是实现类Runnable接口的。
实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免Java中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池可以放入实现Runable或Callable类线程。
2.5、存在的问题
多线程访问共享资源的同时,存在一个十分严重的问题,那就是会导致共享资源数据错乱。
2.6、多线程执行轨迹分析
假设我们拿一种执行情况来分析
2.7、总结
- 线程通过抢占CPU的方式工作,在执行过程中,随时可能CPU时间片的时间到了,然后被挂起,在程序的任何地方都有可能被切换出去
- 由于随时被挂起或者切换出CPU,导致访问共享资源会出现数据错乱,解决方法为加锁
三、线程常用的方法
3.1、设置线程优先级
我们可以设置线程的优先级调用,优先级越高 ,被 CPU 调动的可能性越大,但不一定是优先级越高就一定先执行。,有可能设置了最高的优先级但是确实最后调用。
//系统的默认三种优先级 System.out.println(Thread.MAX_PRIORITY);//数字是10 System.out.println(Thread.MIN_PRIORITY);//数字是1 System.out.println(Thread.NORM_PRIORITY);//数字是5
package day16_thread.classing.PriorityTest; /** * @author Xiao_Lin * @date 2020/12/20 19:30 */ public class TestPriority { public static void main(String[] args) { PriorityThrea p1 = new PriorityThrea("线程1"); PriorityThrea p2 = new PriorityThrea("线程2"); p1.setPriority(PriorityThrea.MAX_PRIORITY); p2.setPriority(PriorityThrea.MIN_PRIORITY); p1.start(); p2.start(); } }
3.2、线程的强制执行
强制执行(join方法)会导致其他线程阻塞,当线程执行完以后,其他线程阻塞原因消除,进入就绪状态。
package day16_thread.classing.join; /** * @author Xiao_Lin * @date 2020/12/20 15:45 */ public class TestMyJoinThread { public static void main(String[] args) { MyJoinThread myJoinThread = new MyJoinThread(); myJoinThread.start(); for (int i =0;i<5;i++){ System.out.println("main -> " + i); if (i==2){ try { myJoinThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } } 复制代码
3.3、线程休眠
线程调用(sleep方法)方法,传入一个毫秒值,会导致当前线程进入阻塞状态,阻塞时间到了以后线程进入就绪状态,sleep方法会抛出一个编译时异常InterruptedException
3.3.1、正常执行
package day16_thread.classing.sleep; /** * @author Xiao_Lin * @date 2020/12/20 19:47 */ public class SleepThread extends Thread{ public SleepThread() { } public SleepThread(String name) { super(name); } @Override public void run() { System.out.println("线程A开始执行"); try { sleep(2000); System.out.println("休眠结束"); } catch (InterruptedException e) { System.out.println("外界有程序中断线程 A"); } System.out.println("线程A即将结束"); } }
package day16_thread.classing.sleep; /** * @author Xiao_Lin * @date 2020/12/20 19:50 */ public class TestSleepThread { public static void main(String[] args) { SleepThread s = new SleepThread(); s.start(); } }
3.3.2、异常情况
package day16_thread.classing.sleep; /** * @author Xiao_Lin * @date 2020/12/20 19:47 */ public class SleepThread extends Thread{ public SleepThread() { } public SleepThread(String name) { super(name); } @Override public void run() { System.out.println("线程A开始执行"); try { sleep(20000); System.out.println("休眠结束"); } catch (InterruptedException e) { System.out.println("外界有程序中断线程 A"); } System.out.println("线程A即将结束"); } }
package day16_thread.classing.sleep; /** * @author Xiao_Lin * @date 2020/12/20 19:50 */ public class TestSleepThread { public static void main(String[] args) { SleepThread s = new SleepThread(); s.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } //线程中断 s.interrupt(); System.out.println("主线程结束"); } }
3.3.3、总结
- 线程休眠导致当前线程进入阻塞状态,休眠时间结束后,线程进入就绪状态,抢占CPU,抢到后继续运行
- 线程休眠过程中可以被中断,所以存在一个编译时异常:
InterruptedException
,外界程序中断该线程时,休眠时间提前结束,进入就绪状态,等待CPU调度执行。
3.4、线程的礼让
package day16_thread.classing.yield; /** * @author Xiao_Lin * @date 2020/12/20 20:00 */ public class YieldThread extends Thread{ public YieldThread() { } public YieldThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(super.getName() + "=>" + i); } } }
package day16_thread.classing.yield; /** * @author Xiao_Lin * @date 2020/12/20 20:01 */ public class TestThreadYield { public static void main(String[] args) { System.out.println("主线程开始执行"); YieldThread y1 = new YieldThread(); y1.start(); for (int i = 0;i<1000;i++){ System.out.println(Thread.currentThread().getName()+"->"+i); if (i%2 == 0){ Thread.yield(); } } } }
当前线程礼让后,线程进入就绪状态。
3.5、线程结束
stop表示强制停止一个线程,停止一个线程的风险较大,不建议使用,通过interrupt
发送中断信号中断线程,线程就会在在那个时间点结束
interrupt
中止正在运行的线程,该线程不会立即结束,而是继续执行,在适当的时机选择结合异常处理机制结束,异常处理机制可以保证线程继续执行,通过异常处理机制让一个线程正常结束。