Skip to content

Java多线程与Lambda表达式完全指南

目录


一、多线程基础概念

1.1 进程与线程

进程:进入到内存运行的应用程序

线程:进程中的一个执行单元,负责当前进程中程序的运行

关键点

  • 一个进程中至少有一个线程
  • 一个进程可以有多个线程,这样的应用程序称为多线程程序
  • 简单理解:进程中的一个功能就需要一条线程去执行
进程示意图线程示意图

1.2 并发与并行

概念定义比喻
并行同一时刻,多个指令在多个CPU上同时执行多个厨师在炒多个菜
并发同一时刻,多个指令在单个CPU上交替执行一个厨师在炒多个菜
并行示意图并发示意图

重要细节

  1. 之前CPU是单核,执行多个程序时好像同时执行,原因是CPU在多个线程之间做高速切换
  2. 现在CPU都是多核多线程(如2核4线程),可以同时运行4个线程,超出部分仍需切换
  3. 现代CPU执行程序时,并发和并行都存在

1.3 CPU调度

1. 分时调度

  • 让所有线程轮流获取CPU使用权
  • 平均分配每个线程占用CPU的时间片

2. 抢占式调度(Java采用)

  • 多个线程抢占CPU使用权
  • 优先级越高的线程,先抢到CPU使用权的几率越大
  • 但不是每次都是优先级高的线程先抢到,只是几率更大

1.4 主线程

定义:专门为main方法服务的线程

主线程介绍

1.5 多线程使用场景

  • 软件中的耗时操作:拷贝大文件、加载大量资源
  • 所有的聊天软件
  • 所有的后台服务器
  • 优势:多线程程序同时干多件事,提高了CPU使用率

二、创建线程的方式

创建多线程有四种方式:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 线程池

2.1 方式一:继承Thread类

实现步骤

  1. 创建自定义类继承Thread
  2. 重写Thread类中的run方法,设置线程任务
  3. 创建自定义线程类的对象
  4. 调用Thread类中的start方法开启线程

代码示例

java
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("MyThread正在执行..." + i);
        }
    }
}
java
public class Demo01Thread {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println("Main正在执行......" + i);
        }
    }
}

注意事项

  • 如果直接调用run方法,并不代表将线程开启,仅仅是简单的调用方法
  • 只有调用start方法,线程才会真正开启
  • 同一个线程对象只能调用一次start,想开新线程需要重新new对象

2.2 多线程内存运行原理

多线程内存运行原理

2.3 Thread类常用方法

方法说明
void run()设置线程任务,这个线程能干啥
void start()使该线程开始执行;JVM调用该线程的run方法
void setName(String name)给线程设置名字
String getName()获取线程名字
static Thread currentThread()获取当前正在执行的线程对象
static void sleep(long millis)线程睡眠,设置毫秒值,超时后自动醒来继续执行

代码示例

java
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(2000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":MyThread正在执行..." + i);
        }
    }
}
java
public class Demo01Thread {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.setName("广坤");
        t1.start();
        
        for (int i = 0; i < 5; i++) {
            Thread.sleep(2000L);
            System.out.println(Thread.currentThread().getName() + ":Main正在执行......" + i);
        }
    }
}

问题:为什么run方法中有编译时期异常只能try,不能throws?

答案:Thread类中的run方法没有抛异常,重写后就不能抛,只能try

2.4 方式二:实现Runnable接口

实现步骤

  1. 创建自定义线程类,实现Runnable接口
  2. 重写run方法,设置线程任务
  3. 利用Thread类中的构造方法:
    • Thread(Runnable r)
    • Thread(Runnable r, String name) 可以设置线程名字
  4. 调用Thread类中的start方法开启线程

代码示例

java
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":正在执行..." + i);
        }
    }
}
java
public class Demo01Runnable {
    public static void main(String[] args) throws InterruptedException {
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":Main正在执行......" + i);
        }
    }
}

2.5 两种方式对比

方式优点缺点
继承Thread类代码简单Java单继承,有局限性
实现Runnable接口接口可多实现,还可继承其他类,局限性小代码稍复杂

推荐使用:实现Runnable接口方式

2.6 匿名内部类创建多线程

java
public class Demo02Runnable {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + ":正在执行......" + i);
                }
            }
        }, "广坤").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(Thread.currentThread().getName() + ":正在执行......" + i);
                }
            }
        }, "刘能").start();
    }
}
匿名内部类创建多线程

三、线程安全与同步机制

3.1 线程安全问题场景

发生场景:多个线程共享同一个资源的时候

经典案例:卖票问题

java
public class MyTicket implements Runnable {
    private int ticket = 100;
    
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}
java
public class Demo01Ticket {
    public static void main(String[] args) {
        MyTicket myTicket = new MyTicket();
        Thread t1 = new Thread(myTicket, "广坤");
        Thread t2 = new Thread(myTicket, "刘能");
        Thread t3 = new Thread(myTicket, "赵四");
        t1.start();
        t2.start();
        t3.start();
    }
}

问题:可能出现重票、错票等线程安全问题

3.2 解决方案一:同步代码块

格式

java
synchronized (任意对象) {
    线程不安全的代码
}

说明

  • 任意对象就是锁对象
  • 线程先抢到锁才能进入同步代码块执行,其他线程等待
  • 线程出了同步代码块自动释放锁,其他线程才能抢锁

代码示例

java
public class MyTicket implements Runnable {
    private int ticket = 100;
    Object obj = new Object();
    
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (obj) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}

3.3 解决方案二:同步方法

3.3.1 普通同步方法(非静态)

格式

java
修饰符 synchronized 返回值类型 方法名(形参) {
    方法体
    return 结果
}

默认锁:this

代码示例

java
public class MyTicket implements Runnable {
    private int ticket = 100;
    
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            method01();
        }
    }

    public synchronized void method01() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

等价于

java
public void method01() {
    synchronized (this) {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

3.3.2 静态同步方法

格式

java
修饰符 static synchronized 返回值类型 方法名(形参) {
    方法体
    return 结果
}

默认锁:当前类.class

代码示例

java
public class MyTicket implements Runnable {
    private static int ticket = 100;
    
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(100L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            method01();
        }
    }

    public static synchronized void method01() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

等价于

java
public static void method01() {
    synchronized (MyTicket.class) {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

四、单例模式

4.1 概念

  • :一个
  • :实例 → 对象
  • 目的:让一个类只产生一个对象供外界使用

4.2 饿汉式

特点:我好饿呀,我很急需一个对象,所以赶紧让对象new出来

实现要点

  1. 构造方法私有化
  2. 创建静态私有对象
  3. 提供公共静态获取方法

代码示例

java
public class Singleton {
    private Singleton() {
    }

    private static Singleton singleton = new Singleton();

    public static Singleton getSingleton() {
        return singleton;
    }
}

测试

java
public class Test01 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Singleton singleton = Singleton.getSingleton();
            System.out.println(singleton);
        }
    }
}

4.3 懒汉式

特点:不需要提前new对象,等到用的时候再new,同时保证只产生一个对象

实现要点

  1. 构造方法私有化
  2. 声明静态私有对象(不初始化)
  3. 提供公共静态获取方法(双重检查锁定)

代码示例

java
public class Singleton {
    private Singleton() {
    }

    private static Singleton singleton = null;

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

测试

java
public class Test01 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Singleton singleton = Singleton.getSingleton();
                System.out.println(singleton);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                Singleton singleton = Singleton.getSingleton();
                System.out.println(singleton);
            }
        }).start();
    }
}

4.4 饿汉式 vs 懒汉式

特性饿汉式懒汉式
对象创建时机类加载时立即创建首次使用时创建
线程安全天然安全需要同步处理
效率高(无同步开销)较低(双重检查锁定)
内存占用可能造成内存浪费按需创建,节省内存

五、Lambda表达式

5.1 函数式编程思想

面向对象编程思想

  • 强调先new对象,然后调用方法
  • 过多强调new对象这个过程

函数式编程思想

  • 强调目的(调用对象的方法),不强调过程(new对象)

5.2 Lambda表达式格式

格式() -> {}

解释说明

  • ():重写方法的参数位置
  • ->:将重写方法的参数传递给重写方法的方法体
  • {}:重写方法的方法体

代码示例

java
public class Demo01Lambda {
    public static void main(String[] args) {
        // 匿名内部类方式
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("正在执行...");
            }
        }).start();

        System.out.println("==========================");
        
        // Lambda表达式方式
        new Thread(() -> System.out.println("正在执行...")).start();
    }
}

5.3 Lambda表达式使用前提

前提条件

  1. 必须是函数式接口做方法参数传递或者返回值返回
  2. 函数式接口:必须有且只能有一个抽象方法的接口
  3. 使用@FunctionalInterface注解标识函数式接口

代码示例

java
@FunctionalInterface
public interface USB {
    void open();
}
java
public class Demo02Lambda {
    public static void main(String[] args) {
        // 匿名内部类方式
        method(new USB() {
            @Override
            public void open() {
                System.out.println("usb打开了");
            }
        });

        System.out.println("========================");
        
        // Lambda表达式方式
        method(() -> System.out.println("lambda打开"));
    }

    public static void method(USB usb) {
        usb.open();
    }
}

5.4 Lambda表达式省略规则

涛哥秘籍

  1. 先观察,是否是函数式接口做方法参数传递或者返回值返回
  2. 如果是,调用方法,以匿名内部类传参或者返回
  3. 从new接口开始到重写方法的方法名结束,选中,删除,然后别忘记多删除一个右半个大括号
  4. 在重写方法的参数和重写方法的方法体之间加 ->

省略规则

  1. 重写方法的参数类型可以省略
  2. 重写方法的参数如果只有一个,数据类型和所在的小括号可以省略
  3. 如果重写方法的方法体只有一句,所在的大括号和分号可以省略
  4. 如果重写方法的方法体只有一句,并且带return,那么所在的大括号、分号以及return都可以省略

代码示例

java
@FunctionalInterface
public interface USB {
    String open(String name);
}
java
public class Test01 {
    public static void main(String[] args) {
        // 匿名内部类方式
        method(new USB() {
            @Override
            public String open(String name) {
                return name + "打开了";
            }
        });
        
        System.out.println("========================");
        
        // Lambda表达式方式(省略后)
        method(name -> name + "打开了");

        System.out.println("===================");
        
        // Lambda作为返回值
        String result = method02().open("键盘");
        System.out.println(result);
    }

    public static void method(USB usb) {
        String result = usb.open("鼠标");
        System.out.println(result);
    }

    public static USB method02() {
        return name -> name + "打开了";
    }
}

六、常见面试题

6.1 基础概念题 ⭐

Q1:进程和线程的区别是什么?

答案

  • 进程是正在运行的程序,是系统进行资源分配和调度的独立单位
  • 线程是进程中的一个执行单元,是CPU调度和分派的基本单位
  • 一个进程至少有一个线程,也可以有多个线程
  • 进程之间相互独立,线程之间共享进程资源

Q2:并行和并发的区别是什么?

答案

  • 并行:同一时刻,多个指令在多个CPU上同时执行
  • 并发:同一时刻,多个指令在单个CPU上交替执行
  • 现代多核CPU中,并行和并发都存在

Q3:Java采用什么调度方式?

答案

  • Java采用抢占式调度
  • 多个线程抢占CPU使用权
  • 优先级高的线程抢到CPU使用权的几率更大,但不是绝对的

6.2 线程创建题 ⭐⭐

Q4:创建线程有哪几种方式?

答案

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口
  4. 线程池

Q5:继承Thread类和实现Runnable接口哪种方式更好?为什么?

答案

  • 实现Runnable接口方式更好
  • 原因:
    1. Java单继承,继承Thread类有局限性
    2. 接口可以多实现,还可以继承其他类
    3. 适合多线程共享同一资源的情况

Q6:start()和run()的区别?

答案

  • start():启动线程,JVM会自动调用run方法
  • run():只是普通方法调用,不会启动新线程
  • 直接调用run方法不会创建新线程,只是在当前线程中执行

6.3 线程安全题 ⭐⭐⭐

Q7:什么是线程安全问题?如何解决?

答案

  • 线程安全问题:多个线程共享同一资源时,可能出现数据不一致等问题
  • 解决方案:
    1. 同步代码块:synchronized (锁对象) { 代码 }
    2. 同步方法:在方法声明上加synchronized
    3. Lock锁(后续学习)

Q8:同步代码块和同步方法的锁对象分别是什么?

答案

  • 同步代码块:任意对象,需要手动指定
  • 普通同步方法:this(当前对象)
  • 静态同步方法:当前类.class(类对象)

Q9:同步代码块的执行流程是什么?

答案

  1. 线程先抢到锁才能进入同步代码块执行
  2. 其他线程等待
  3. 线程执行完同步代码块后自动释放锁
  4. 其他线程抢锁,抢到执行,抢不到继续等待

6.4 单例模式题 ⭐⭐⭐

Q10:什么是单例模式?有哪些实现方式?

答案

  • 单例模式:让一个类只产生一个对象供外界使用
  • 实现方式:
    1. 饿汉式:类加载时就创建对象
    2. 懒汉式:首次使用时创建对象

Q11:饿汉式和懒汉式的区别?

答案

特性饿汉式懒汉式
对象创建时机类加载时首次使用时
线程安全天然安全需要同步处理
效率较低
内存占用可能浪费按需创建

Q12:懒汉式如何保证线程安全?

答案

  • 使用双重检查锁定(Double-Check Locking)
  • 两次判断singleton == null
  • 中间加synchronized同步代码块
  • 锁对象为当前类.class

6.5 Lambda表达式题 ⭐⭐

Q13:什么是函数式接口?

答案

  • 有且只有一个抽象方法的接口
  • 可以使用@FunctionalInterface注解标识
  • 是Lambda表达式使用的前提

Q14:Lambda表达式的格式是什么?各部分代表什么?

答案

  • 格式:() -> {}
  • ():重写方法的参数列表
  • ->:箭头,将参数传递给方法体
  • {}:重写方法的方法体

Q15:Lambda表达式可以省略哪些内容?

答案

  1. 参数类型可以省略
  2. 只有一个参数时,小括号可以省略
  3. 方法体只有一句时,大括号和分号可以省略
  4. 方法体只有一句且有return时,return也可以省略

七、经典练习题

7.1 基础练习

练习1:创建三个线程,分别打印"线程A"、"线程B"、"线程C"

要求:使用继承Thread类的方式

参考答案

java
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "正在执行");
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("线程A");
        MyThread t2 = new MyThread("线程B");
        MyThread t3 = new MyThread("线程C");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

练习2:使用Runnable接口实现三个窗口卖票

要求:共100张票,三个窗口同时卖票

参考答案

java
public class Ticket implements Runnable {
    private int ticket = 100;
    
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        
        t1.start();
        t2.start();
        t3.start();
    }
}

7.2 进阶练习

练习3:使用Lambda表达式简化代码

要求:将以下匿名内部类改写为Lambda表达式

java
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程执行");
    }
}).start();

参考答案

java
new Thread(() -> System.out.println("线程执行")).start();

练习4:实现一个线程安全的单例模式

要求:使用懒汉式,保证线程安全

参考答案

java
public class Singleton {
    private Singleton() {
    }

    private static Singleton singleton = null;

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

练习5:使用同步方法实现卖票系统

要求:使用同步方法替代同步代码块

参考答案

java
public class Ticket implements Runnable {
    private int ticket = 100;
    
    @Override
    public void run() {
        while (true) {
            if (!sellTicket()) {
                break;
            }
        }
    }
    
    public synchronized boolean sellTicket() {
        if (ticket > 0) {
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
            ticket--;
            return true;
        }
        return false;
    }
}

八、学习建议

8.1 学习重点

  1. 线程创建方式:重点掌握继承Thread类和实现Runnable接口两种方式
  2. 线程安全:理解线程安全问题的产生原因,掌握同步代码块和同步方法
  3. 单例模式:理解饿汉式和懒汉式的区别,掌握懒汉式的线程安全实现
  4. Lambda表达式:理解函数式编程思想,掌握Lambda表达式的格式和省略规则

8.2 学习方法

  1. 多写代码:线程相关代码需要多动手实践,理解并发执行的效果
  2. 画图理解:使用图示理解线程执行流程、内存原理等
  3. 对比学习:对比两种线程创建方式、两种单例模式的区别
  4. 面试准备:重点理解面试题中的核心概念和实现原理

8.3 常见误区

  1. 误区一:直接调用run方法就能启动线程

    • 正确:必须调用start方法才能启动线程
  2. 误区二:同步代码块的锁对象必须是this

    • 正确:可以是任意对象,但要保证多个线程使用同一把锁
  3. 误区三:Lambda表达式可以用于任何接口

    • 正确:只能用于函数式接口(只有一个抽象方法)

8.4 扩展学习

下一章预告

  • 线程生命周期
  • 线程通信(wait/notify)
  • 线程池
  • Lock锁

附录:关键概念速查表

概念定义
进程正在运行的程序
线程进程中的执行单元
并行多个CPU同时执行多个指令
并发单个CPU交替执行多个指令
同步控制多个线程按顺序访问共享资源
函数式接口只有一个抽象方法的接口
Lambda表达式简化匿名内部类写法的语法糖