多线程
程序:为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或者是正在运行的程序。是一个动态的过程,有它自身的产生、存在、和消亡的过程。——生命周期。
- 如,运行中的QQ,运行中的MP3播放器
- 程序是静态的,进程是动态的
- 进程作为资源分配的单位,系统在运行时会为每个内存分配不同的内存区域
线程:进程可进一步细化为线程,是程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的
- 线程作为调度和执行的单位,每个线程都拥有独立的运行栈和程序计数器(PC),线程切换的开销小
- 一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便,高效。但多个线程操作共享的系统资源可能会带来安全的隐患。
单核CPU和多核CPU的理解:
- 单核CPU,其实是一种假的多线程。单核CPU上运行的多线程程序,同一时间只能一个线程在跑,系统给每个线程分配时间片来执行,每个时间片大概10ms左右,看起来像是同时在跑多个线程,但实际是每个线程跑一点点就换到其他线程继续跑。举个例子,一个厨房里只有一个厨师在干活,这个时候来了三个客人,分别下了一个汤,一个小炒,一个煮螺蛳粉(电磁炉煮)。但是由于只有一个厨师,他在一个时间内只能做一件事(同一时间只能一个线程在跑),要么煮螺蛳粉,要么炒菜,要么煲汤,不可能说一个人同时做三件事。但是这个厨师能力很强(单核CPU主频很高),他先花1分钟把螺蛳粉都泡好,然后停下来再花1分钟把煲汤需要弄的弄好,然后再花1分钟把炒菜需要的都放到锅里,然后又回去弄螺蛳粉,因为这个厨师能力非常强,他能合理的分配时间来做这三件事,而且做的特别快,一下子就把这三道菜做好了,让你感觉厨房里好像有三个厨师分别在做这三道菜一样,但实际是他一个人挣了三个人的工资。
- 多核CPU,是真正意义上的多线程,同一时间多个CPU同时跑其他的程序,效率很高。
- 一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然,如果发生异常,会影响主线程。
并行和并发:
- 并发:一个应用程序(进程)如果可以开启多个线程,让多个线程同时存在,但是交替执行(参考上面的单核CPU概念),则称之为并发执行。
- 并行:一个应用程序能并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器上核上,因此可以并行执行。
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,增强用户的体验
- 提高计算机系统CPU的利用率
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
多线程的创建:
-
方式一:通过继承于Thread类
// 1.创建一个继承于Thread类的子类 class MyThread extends Thread { // 2.重写Thread类的run()方法-->将此线程的的操作声明在run()方法中 // 遍历100以内的偶数 @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(i); } } } } public class ThreadTest { public static void main(String[] args) { // 3.在测试类中创建Thread类的子类的对象 MyThread mt = new MyThread(); // 4.通过此对象调用Thread类中的start()方法 mt.start(); // 问题一:不能通过直接调用run()的方式启动线程 // mt.run(); 这是错的 // 问题二:不可以还让已经start()的线程去执行,否则会报IllegalTheradStateException线程非法的异常。 // 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。 // mt.start(); // 需要重新创建一个线程的对象才可以start() MyThread mt2 = new MyThread(); mt2.start(); // 还可以通过创建匿名子类的方式 new Thread(){ @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(i); } } } }.start(); } }
-
Thread中常用的方法:
- start():启动当前线程,调用当前线程的run()
- run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread:静态方法,返回对当前正在执行的线程对象的引用
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():暂停当前正在执行的线程对象,并执行其他线程
- join():在线程a中调用线程b的join()方法,此时线程a就会进入阻塞状态,直到线程b完全执行完以后,线程a才会结束阻塞状态
- sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
- isAlive():判断当前线程是否还存活
-
线程的调度
-
调度策略
- 以 “时间片” 的形式进行调度
- 抢占式:高优先级的线程抢占CPU
-
Java的调度方法
- 同优先级的线程组成先进先出队列(先到先得服务),使用时间片的策略
- 对高优先级使用抢占式策略
-
线程的优先级等级
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 --> 默认优先级
-
涉及的方法
- getPriority():返回线程的优先级等级
- setPriority(int p):设置线程的优先级的等级
-
说明:
- 线程创建时继承父线程的优先级等级
- 低优先级只是获得CPU调度的概率低,并非是在高优先级线程之后才被调用
-
调度策略
-
方式二:实现Runnable接口
// 1.创建一个实现了Runnable接口的类 class MyThread1 implements Runnable { // 2.实现类去实现Runnable接口的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(i); } } } } public class ThreadTest1 { public static void main(String[] args) { // 3.创建实现类的对象 MyThread1 myThread1 = new MyThread1(); // 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象 Thread t1 = new Thread(myThread1); // 5.通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()方法 t1.start(); } }
-
比较创建多线程的两种方式:
- 开发中,优先选择实现Runnable接口的方式(面向接口编程)
- 原因:
- 实现接口的方式没有类的单继承的局限性
- 实现接口的方式更适合用来处理多线程共享数据的情况
线程的生命周期:
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建:当一个Thread类及其子类的对象被声明创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()之后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU的资源时,便进入运行状态,run()方法中国定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或者线程被提前强制性地中止或线程出现异常且没处理导致结束