Java基础快速入门: 多线程核心概念与入门实践

本文纲要

一、多线程基础概念
1.1 初步认识多线程
1.2 并发与并行
1.3 进程与线程

二、多线程的实现方式
2.1 项目结构概览
2.2 方式一:继承 Thread
2.3 run()start() 的区别
2.4 方式二:实现 Runnable 接口
2.5 方式三:实现 Callable 接口与 FutureTask
2.6 三种实现方式对比

三、Thread 类常用方法
3.1 获取和设置线程名称
3.2 获取当前线程对象 currentThread()
3.3 线程休眠 sleep()
3.4 线程优先级
3.5 守护线程

四、总结

多线程基础概念

1 ) 初步认识多线程

多线程是指从软件或硬件上实现多个线程并发执行的技术,具有多线程能力的计算机必须有硬件支持,从而在同一时间执行多个线程,提升性能。

以一个生活场景为例:你一边玩电脑,一边抽烟、喝可乐。你的右手一会儿点鼠标,一会儿拿烟,一会儿拿可乐。右手动作非常快,在外人看来你似乎在同时做三件事,但在某一瞬间,你其实只做了其中一件事(比如拿可乐喝)。CPU 就像这只右手,三个软件就像鼠标、烟、可乐。CPU 在多个软件之间高速切换,让我们感觉它们是同时执行的。

所以,对多线程的初步理解可以总结为:

  • 多线程技术就是让多个应用程序“同时”执行
  • 它需要硬件的支持(CPU 高速切换)

2 )并发与并行

并行:在同一时刻,有多个指令在多个 CPU 上同时执行。
并发:在同一时刻,有多个指令在单个 CPU 上交替执行。

概念执行方式比喻
并行多个 CPU 同时执行多个任务三个厨师同时炒三个菜(真·同时)
并发单 CPU 交替执行多个任务一个厨师快速切换炒三个菜(宏观同时,微观交替)

并发

厨师

交替炒三道菜

并行

厨师1

西红柿炒番茄

厨师2

青椒肉丝

厨师3

海参炒饭

重点:Java 多线程主要研究的是并发场景,多个线程在单个 CPU 上交替执行。

3 ) 进程与线程

进程:正在运行的软件。打开任务管理器可以看到许多正在运行的进程(如 PPT、IDEA)。

进程的特点:

  • 独立性:进程是能独立运行的基本单位,也是系统分配资源和调度的独立单位,进程之间不能直接传递数据。
  • 动态性:进程的实质是程序的一次执行过程,动态产生、动态消亡。
  • 并发性:任何进程都可以同其他进程一起并发执行。

线程:进程中的单个顺序控制流,是一条执行路径。
简单理解,线程属于进程,是进程中真正干活的部分。例如,360安全卫士同时进行“电脑体检”、“木马查杀”、“电脑清理”、“系统修复”、“优化加速”,这五项任务就是五个线程,它们都属于360这个进程。

  • 一个进程中如果只有一条执行路径,则称为单线程程序(如之前写的顺序执行代码)。
  • 一个进程中如果有多条执行路径,则称为多线程程序

进程: 360安全卫士

线程: 电脑体检

线程: 木马查杀

线程: 电脑清理

线程: 系统修复

线程: 优化加速

多线程的实现方式

Java 中多线程的实现主要有三种方式:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 FutureTask

1 ) 项目结构概览

以下示例代码的组织结构(基于文本与代码整理):

threadmodule/src/com/wb/
├─ threaddemo1    // 继承 Thread 类 
│  ├─ MyThread.java 
│  └─ Demo.java 
├─ threaddemo2    // 实现 Runnable 接口 
│  ├─ MyRunnable.java 
│  └─ Demo.java 
├─ threaddemo3    // 实现 Callable 接口 
│  ├─ MyCallable.java 
│  └─ Demo.java 
├─ threaddemo4    // 设置/获取线程名称 
│  ├─ MyThread.java 
│  └─ Demo.java 
├─ threaddemo5    // currentThread()
│  └─ Demo.java 
├─ threaddemo6    // sleep()
│  ├─ MyRunnable.java 
│  └─ Demo.java 
├─ threaddemo7    // 线程优先级 
│  ├─ MyCallable.java 
│  └─ Demo.java 
└─ threaddemo8    // 守护线程 
   ├─ MyThread1.java 
   ├─ MyThread2.java 
   └─ Demo.java 

2 ) 方式一:继承 Thread 类

步骤:

  1. 定义一个类继承 Thread
  2. 在这个类中重写 run() 方法
  3. 创建该类的对象
  4. 调用 start() 方法启动线程

代码示例(threaddemo1):

// MyThread.java 
public class MyThread extends Thread {
    @Override 
    public void run() {
        // 线程开启后执行的代码 
        for (int i = 0; i < 100; i++) {
            System.out.println("线程开启了" + i);
        }
    }
}
 
// Demo.java 
public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
 
        t1.start();  // 开启第一条线程 
        t2.start();  // 开启第二条线程 
    }
}

运行效果:两个线程交替打印,每次运行顺序可能不同,体现了多线程的随机性

3 ) run()start() 的区别

问:为什么要重写 run() 方法?
答:多线程开启后,执行的就是 run() 方法里的代码,因此必须重写它来封装任务。

问:run()start() 有什么区别?

如果直接调用 run() 方法:

t1.run();  // 只是普通的方法调用,没有开启新线程,main 线程顺序执行 
t2.run();

输出会严格按照顺序:t1 的循环全部执行完,再执行 t2 的循环,没有多线程效果

而调用 start() 方法:

t1.start(); // 与操作系统交互,真正开启一条新线程,由新线程执行 run()
t2.start();

此时两个线程交替执行,具有多线程效果。

小结

run():封装线程执行的代码,直接调用仅相当于普通方法调用,没有开启新线程。
start():开启新线程,JVM 会在新线程中调用 run() 方法。

4 ) 方式二:实现 Runnable 接口

步骤:

  1. 定义一个类实现 Runnable 接口
  2. 重写 run() 方法
  3. 创建该实现类的对象(作为参数对象)
  4. 创建 Thread 对象,将 Runnable 对象传入
  5. 调用 Thread 对象的 start() 方法启动线程

代码示例(threaddemo2):

// MyRunnable.java 
public class MyRunnable implements Runnable {
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            // 使用 currentThread() 获取当前线程对象,再获取名称 
            System.out.println(Thread.currentThread().getName() + " 第二种方式实现多线程" + i);
        }
    }
}
 
// Demo.java 
public class Demo {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();   // 参数对象 
        Thread t1 = new Thread(mr);         // 创建线程对象并绑定任务 
        t1.start();                         // 开启线程 
 
        MyRunnable mr2 = new MyRunnable();
        Thread t2 = new Thread(mr2);
        t2.start();
    }
}

注意:Runnable 接口没有 start() 方法,必须借助 Thread 对象启动。在线程启动后,执行的是参数对象中的 run() 方法。

5 ) 方式三:实现 Callable 接口与 FutureTask

特点:与前两种方式不同,Callable 允许线程执行完成后返回结果,返回值的类型可通过泛型指定。

步骤:

  1. 定义一个类实现 Callable<V> 接口(V 为返回值类型)
  2. 重写 call() 方法(相当于 run(),但有返回值)
  3. 在测试类中创建 Callable 实现类对象
  4. 创建 FutureTask<V> 对象,将 Callable 对象传入
  5. 创建 Thread 对象,将 FutureTask 对象传入(FutureTask 实现了 Runnable,因此可以传递)
  6. 启动线程
  7. start() 之后,通过 FutureTaskget() 方法获取结果

代码示例(threaddemo3):

// MyCallable.java 
import java.util.concurrent.Callable;
 
public class MyCallable implements Callable<String> {
    @Override 
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("跟女孩表白" + i);
        }
        // 返回值表示线程运行完毕之后的结果 
        return "答应";
    }
}
 
// Demo.java 
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
 
public class Demo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable mc = new MyCallable();          // 创建 Callable 对象 
        FutureTask<String> ft = new FutureTask<>(mc); // 包装成 FutureTask 
        Thread t1 = new Thread(ft);                // 传入 Thread 
        t1.start();                                // 启动线程 
 
        String result = ft.get();                  // 获取结果(会阻塞直到线程完成)
        System.out.println(result);                // 输出:答应 
    }
}

特别注意:get() 方法必须放在 start() 之后调用。如果放在 start() 之前,主线程会因等待一个尚未开始执行的结果而无限阻塞

子线程 (Callable)FutureTask主线程子线程 (Callable)FutureTask主线程创建 FutureTask创建 Thread 并 start()执行 call(),设置结果ft.get() 阻塞等待返回结果

6 ) 三种实现方式对比

对比维度继承 Thread 类实现 Runnable 接口实现 Callable 接口
是否有返回值有(泛型指定)
能否继承其他类不能(单继承)可以(实现接口同时还能继承)可以
编程复杂度较简单稍复杂(不能直接使用 Thread 方法)较复杂(需要 FutureTask
能否直接使用 Thread 中的方法不能(需 Thread.currentThread()不能
适用场景简单任务、无需共享资源推荐使用,可继承其他类、可共享资源需要线程执行结果时

Thread 类常用方法

1 ) 获取和设置线程名称

获取名称:getName()
设置名称:
方式一:setName(String name)
方式二:通过构造方法为线程传递名称(需在子类中定义对应构造方法调用 super(name)

即使不手动命名,线程也有默认名称,格式为 Thread-编号(编号从 0 开始自增)。

代码示例(threaddemo4):

// MyThread.java 
public class MyThread extends Thread {
 
    public MyThread() {}
 
    public MyThread(String name) {
        super(name);   // 将名称传递给 Thread 类 
    }
 
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "@@@" + i);
        }
    }
}
 
// Demo.java 
public class Demo {
    public static void main(String[] args) {
        // 方式一:构造方法命名 
        MyThread t1 = new MyThread("小蔡");
        MyThread t2 = new MyThread("小强");
 
        // 方式二:setName() 命名(注释演示)
        // t1.setName("小蔡");
        // t2.setName("小强");
 
        t1.start();
        t2.start();
    }
}

2 ) 获取当前线程对象 currentThread()

Thread.currentThread() 是一个静态方法,返回当前正在执行的线程对象
当需要在 Runnable 实现类中获取线程名称时非常有用(因为 Runnable 没有继承 Thread)。

代码示例(threaddemo5):

public class Demo {
    public static void main(String[] args) {
        // 当前线程为 main 线程 
        String name = Thread.currentThread().getName();
        System.out.println(name);  // 输出:main 
    }
}

Runnablerun() 方法中同样可以使用:

public void run() {
    System.out.println(Thread.currentThread().getName() + " 正在执行");
}

3 ) 线程休眠 sleep()

Thread.sleep(long millis) 是一个静态方法,让当前执行的线程休眠指定的毫秒数(1秒 = 1000毫秒)。
休眠期间该线程不会释放锁,休眠结束后将继续运行。

注意:如果 sleep() 被放在没有声明异常的方法中(如 run()),必须使用 try-catch 捕获 InterruptedException,不能抛出。

代码示例(threaddemo6):

// MyRunnable.java 
public class MyRunnable implements Runnable {
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(100);  // 休眠 100 毫秒 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
    }
}
 
// Demo.java 
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        // 主线程休眠演示 
        // System.out.println("睡觉前");
        // Thread.sleep(3000);
        // System.out.println("睡醒了");
 
        MyRunnable mr = new MyRunnable();
        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);
        t1.start();
        t2.start();
    }
}

4 ) 线程优先级

Java 采用抢占式调度模型:优先级高的线程获取 CPU 时间片的概率相对更高,但不保证一定优先执行

Thread 类中定义了优先级常量:

常量数值
MIN_PRIORITY1
NORM_PRIORITY5 (默认)
MAX_PRIORITY10

相关方法:

  • setPriority(int priority):设置优先级(1~10,范围外会抛出 IllegalArgumentException
  • getPriority():获取优先级

代码示例(threaddemo7):

// MyCallable.java 
public class MyCallable implements Callable<String> {
    @Override 
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + i);
        }
        return "线程执行完毕了";
    }
}
 
// Demo.java 
public class Demo {
    public static void main(String[] args) {
        MyCallable mc = new MyCallable();
        FutureTask<String> ft = new FutureTask<>(mc);
        Thread t1 = new Thread(ft);
        t1.setName("飞机");
        t1.setPriority(10);           // 最高优先级 
        // System.out.println(t1.getPriority()); // 默认 5 
        t1.start();
 
        MyCallable mc2 = new MyCallable();
        FutureTask<String> ft2 = new FutureTask<>(mc2);
        Thread t2 = new Thread(ft2);
        t2.setName("坦克");
        t2.setPriority(1);            // 最低优先级 
        t2.start();
    }
}

多次运行会发现,“飞机”虽然优先级高,但并不总是先执行完,只是抢占到 CPU 的概率更大。

5 ) 守护线程

守护线程(Daemon Thread)也叫后台线程,主要服务于普通线程。当所有普通线程结束时,守护线程会自动终止,不会继续执行完毕。

通过 setDaemon(true) 可将一个线程设置为守护线程(必须在 start() 之前设置)。
典型应用场景:垃圾回收线程、自动保存线程等。

代码示例(threaddemo8):

// MyThread1.java(女神:普通线程,执行 10 次)
public class MyThread1 extends Thread {
    @Override 
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
 
// MyThread2.java(备胎:守护线程,执行 100 次)
public class MyThread2 extends Thread {
    @Override 
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}
 
// Demo.java 
public class Demo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();
 
        t1.setName("女神");
        t2.setName("备胎");
 
        t2.setDaemon(true);  // 设置为守护线程 
 
        t1.start();
        t2.start();
    }
}

运行现象:“女神”线程(普通线程)打印 0~9 结束后,“备胎”线程(守护线程)挣扎打印一段时间后终止,不会打印完 100 次。这说明守护线程在失去最后的普通线程后将结束运行。

总结

本文从多线程的基本概念出发,介绍了:

  • 并发与并行的区别
  • 进程与线程的关系
  • 多线程的三种实现方式(继承 Thread、实现 Runnable、使用 Callable+FutureTask)及其对比
  • Thread 类的常用方法:名称设置获取当前线程sleep优先级守护线程

掌握这些知识就为 Java 多线程编程打下了坚实的基础。在后续学习中,还会涉及线程同步、线程池、并发工具等高级内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值