Java 高级应用-多线程-(一)实现 Runnable 接口与继承 Thread 类

1.1 程序、进程与线程
• 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段
静态的代码,静态对象。
• 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行
中的 QQ,运行中的网易音乐播放器。
– 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创
建、运行到消亡的过程。(生命周期)
– 程序是静态的,进程是动态的
– 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基
本单位),系统在运行时会为每个进程分配不同的内存区域。
– 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:
现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,
dos 窗口等软件。
• 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。
一 个进程中至少有一个线程。
– 一个进程同一时间若并行执行多个线程,就是支持多线程的。

– 线程作为 CPU 调度和执行的最小单位。
– 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对
象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但
多个线程操作共享的系统资源可能就会带来安全的隐患。
– 下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。

注意:
不同的进程之间是不共享内存的。
进程之间的数据交换和通信的成本很高。

1.5.2 并行与并发
• 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。
指在同一时刻,有多条指令在多个 CPU 上同时执行。比如:多个人同时做不同的事。

**• 并发(concurrency):指两个或多个事件在同一个时间段内发生。**即在一段时间内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。

2.创建和启动线程
2.1 概述
• Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所
有的线程对象都必须是 Thread 类或其子类的实例。
• Thread 类的特性
– 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此
把 run()方法体称为线程执行体。
– 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()
– 要想实现多线程,必须在主线程中创建新的线程对象。

2.2 方式 1:继承 Thread 类

Java 通过继承 Thread 类来创建并启动多线程的步骤如下:

  1. 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程
    需要完成的任务
  2. 创建 Thread 子类的实例,即创建了线程对象
  3. 调用线程对象的 start()方法来启动该线程
    代码如下:
package com.atguigu.thread;
//自定义线程类
public class MyThread extends Thread {
 //定义指定线程名称的构造方法
 public MyThread(String name) {
 //调用父类的 String 参数的构造方法,指定线程的名称
 super(name);
 }
 /**
 * 重写 run 方法,完成该线程执行的逻辑
 */
 @Override
 public void run() {
 for (int i = 0; i < 10; i++) {
 System.out.println(getName()+":正在执行!"+i);
 }
 }
}

测试类:


package com.atguigu.thread;
public class TestMyThread {
 public static void main(String[] args) {
 //创建自定义线程对象 1
 MyThread mt1 = new MyThread("子线程 1");
 //开启子线程 1
 mt1.start();
 
 //创建自定义线程对象 2
 MyThread mt2 = new MyThread("子线程 2");
 //开启子线程 2
 mt2.start();
 
 //在主方法中执行 for 循环
 for (int i = 0; i < 10; i++) {
 System.out.println("main 线程!"+i);
 }
 }
}


注意:

1.如果自己手动调用 run()方法,那么就只是普通方法,没有启动多线程模 式。
2.run()方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU 调度决定。
3.想要启动多线程,必须调用 start 方法。
4.一个线程对象只能调用一次 start()方法启动,如果重复调用了,则将抛出 以上的异常“IllegalThreadStateException”。

2.3 方式 2:实现 Runnable 接口

Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心
类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法,
然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法

步骤如下:
2. 定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样是该线程的线程执行体。
3. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建Thread 对象,该 Thread 对象才是真正 的线程对象。
4. 调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的run 方法。
代码如下:

package com.atguigu.thread;
public class MyRunnable implements Runnable {
 @Override
 public void run() {
 for (int i = 0; i < 20; i++) {
 System.out.println(Thread.currentThread().getName() + " "
+ i);
 }
 }
}

测试类:

package com.atguigu.thread;
public class TestMyRunnable {
 public static void main(String[] args) {
 //创建自定义类对象 线程任务对象
 MyRunnable mr = new MyRunnable();
 //创建线程对象
 Thread t = new Thread(mr, "长江");
 t.start();
 for (int i = 0; i < 20; i++) {
 System.out.println("黄河 " + i);
 }
 }
}

通过实现 Runnable 接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在 run 方法里面。
在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象,然后调用 Thread 对象的 start()方法来运行多线程代码。
实际上,所有的多线程代码都是通过运行 Thread 的 start()方法来运行的。因此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。

说明:Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含的 run()方法仅作为线程执行体。
而实际的线程对象依然是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run()方法。

2.5 对比两种方式

联系

Thread 类实际上也是实现了 Runnable 接口的类。即: public class Thread extends Object implements Runnable

区别

• 继承 Thread:线程代码存放 Thread 子类 run 方法中。
• 实现 Runnable:线程代码存在接口的子类的 run 方法。

实现 Runnable 接口比继承 Thread 类所具有的优势

• 避免了单继承的局限性
• 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资 源。
•增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

3. Thread 类的常用结构
3.1 构造器

• public Thread() :分配一个新的线程对象。
• public Thread(String name) :分配一个指定名字的新的线程对象。
• public Thread(Runnable target) :指定创建线程的目标对象,它实现了 

Runnable 接口中的 run 方法

• public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。

3.2 常用方法系列 1

• public void run() :此线程要执行的任务在此处定义代码。
• public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
• public String getName() :获取当前线程名称。
• public void setName(String name):设置该线程名称。
• public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在
Thread 子类中就是 this,通常用于主线程和 Runnable 实现类
• public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时
停止执行)。
• public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新
调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这
个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调
度器又将其调度出来重新执行。

4.1 JDK1.5 之前:5 种状态
线程的生命周期有五种状态:新建(New)、就绪(Runnable)、运行
(Running)、阻塞(Blocked)、死亡(Dead)。
CPU 需要在多条线程之间切
换,于是线程状态会多次在运行、阻塞、就绪之间切换


1.新建
当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状
态。此时它和其他 Java 对象一样,仅仅由 JVM 为其分配了内存,并初始化了
实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行
它的线程体 run()。
2.就绪
但是当线程对象调用了 start()方法之后,就不一样了,线程就从新建状态转为
就绪状态。JVM 会为其创建方法调用栈和程序计数器,当然,处于这个状态中
的线程并没有开始运行,只是表示已具备了运行的条件,随时可以被调度。至
于什么时候被调度,取决于 JVM 里线程调度器的调度。
注意:
程序只能对新建状态的线程调用 start(),并且只能调用一次,如果对
非新建状态的线程,如已启动的线程或已死亡的线程调用 start()都会
报错 IllegalThreadStateException 异常。
3.运行
如果处于就绪状态的线程获得了 CPU 资源时,开始执行 run()方法的线程体代
码,则该线程处于运行状态。如果计算机只有一个 CPU 核心,在任何时刻只有
一个线程处于运行状态,如果计算机有多个核心,将会有多个线程并行
(Parallel)执行。
当然,美好的时光总是短暂的,而且 CPU 讲究雨露均沾。对于抢占式策略的系
统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用
完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。
此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程
的优先级。
4.阻塞
当在运行过程中的线程遇到如下情况时,会让出 CPU 并临时中止自己的执
行,进入阻塞状态:
• 线程调用了 sleep()方法,主动放弃所占用的 CPU 资源;
• 线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;
• 线程执行过程中,同步监视器调用了 wait(),让它等待某个通知(notify);
• 线程执行过程中,同步监视器调用了 wait(time)
• 线程执行过程中,遇到了其他线程对象的加塞(join);
• 线程被调用 suspend 方法被挂起(已过时,因为容易发生死锁);
当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况,当
发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再
次调度它:
• 线程的 sleep()时间到;
• 线程成功获得了同步监视器;
• 线程等到了通知(notify);
• 线程 wait 的时间到了
• 加塞的线程结束了;
• 被挂起的线程又被调用了 resume 恢复方法(已过时,因为容易发生死锁);
5.死亡
线程会以以下三种方式之一结束,结束后的线程就处于死亡状态:
• run()方法执行完成,线程正常结束
• 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)
• 直接调用该线程的 stop()来结束该线程(已过时)

4.2 JDK1.5 及之后:6 种状态
在 java.lang.Thread.State 的枚举类中这样定义:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
• NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法。
• RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只
能标记为可运行,至于什么时候运行,不是 JVM 来控制的了,是 OS 来进行调度的,
而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。
• Teminated(被终止):表明此线程已经结束生命周期,终止运行。
• 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:BLOCKED、WAITING、
TIMED_WAITING。
– BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视
器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行
机会。
• 比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到
锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked
锁阻塞状态。
– TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待
另一个线程执行一个(唤醒)动作的线程处于这一状态。
• 当前线程执行过程中遇到 Thread 类的 sleep 或 join,Object 类
的 wait,LockSupport 类的 park 方法,并且在调用这些方法时,
设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间
到,或被中断。
– WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线
程执行一个特别的(唤醒)动作的线程处于这一状态。
• 当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的
join,LockSupport 类的 park 方法,并且在调用这些方法时,没
有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒。
– 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的
notify/notifyAll 唤醒;
– 通过 Condition 的 await 进入 WAITING 状态的要有
Condition 的 signal 方法唤醒;
– 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有
LockSupport 类的 unpark 方法唤醒
– 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join
方法的线程对象结束才能让当前线程恢复;
说明:当从 WAITING 或 TIMED_WAITING 恢复到 Runnable 状态时,如果发现
当前线程没有得到监视器锁,那么会立刻转入 BLOCKED 状态。


OR

版权声明:本文为博主作者:super.aladdin原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/weixin_45817985/article/details/130942023

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
扎眼的阳光的头像扎眼的阳光普通用户
上一篇 2024年1月11日
下一篇 2024年1月11日

相关推荐