27Java基础之多线程

多线程

多线程的创建方式

什么是线程?

  • 线程(Thread)是一个程序内部的一条执行流程。
    image
  • 程序中如果只有一条执行流程,那这个流程就是单线程的程序。

多线程是什么?

  • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

多线程在哪里,有什么好处
image

  • 在例如:消息通信、淘宝、京东系统都离不开多线程技术。

如何在程序中创建出多条线程?

  • Java是通过Java.lang.Thread类的对象来代表线程的。

1. 多线程的创建方式:继承Thread类

  • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  • 创建MyThread类的对象
  • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

案例

线程类:
//定义一个类,继承Thread类,重写run方法。
public class MyThread extends Thread{
    // 重写run方法,声明线程要干的事情
    @Override
    public void run(){
        for(int i = 0; i < 4; i++){
            System.out.println("子线程输出:" + i);
        }
    }
}

测试类:
//目标:掌握线程的创建方式一:继承Thread类。
public class ThreadDemo01 {
    //注意:main方法本身就是由一条主线程负责执行的。
    public static void main(String[] args) {
        //创建一个线程对象
        Thread t = new MyThread();

        //启动线程:会自动执行线程的run方法
        //如果直接调用run,CPU不会注册新线程换行,此时相当于还是单线程。
        t.start();

        //主线程的任务
        for(int i = 0; i < 4; i++){
            System.out.println("主线程输出:" + i);
        }
    }
}

方式一优缺点

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的拓展。

多线程的注意事项

  1. 启动线程必须是调用start方法,不是调用run方法。
  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
  • 只有调用start方法才是启动一个新的线程执行。
  1. 不要把主线程任务放在启动子线程之前。
  • 这样主线程一直是先跑完的,相当于是一个单线程的效果。

线程创建方式二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法。
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
    image
  4. 调用线程对象的start()方法启动线程。

案例

MyRunnable类:
// 1. 定义一个类实现Runnable接口
public class MyRunnable implements Runnable{
    // 2. 重写run方法,设置线程任务
    @Override
    public void run(){
        for (int i = 0; i < 4; i++) {
            System.out.println("子线程输出:" + i);
        }
    }
}

测试类:
//目标:掌握多线程的创建方式二:实现Runnable接口
public class ThreadDemo02 {
    public static void main(String[] args) {
        //3. 创建任务类的一个对象
        Runnable mr = new MyRunnable();

        //4. 把任务对象交给线程对象
        Thread t = new Thread(mr);

        //5. 启动线程
        t.start();

        //主线程输出
        for (int i = 0; i < 4; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}

方式二的优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,拓展性强。
  • 缺点:需要多一个Runnable对象。

线程创建方式二的匿名内部类写法

  1. 可以创建Runnable的匿名内部类对象。
  2. 再交给Thread线程对象。
  3. 在调用线程对象的start()启动线程。

案例

//目标:掌握多线程的创建方式二:匿名内部类写法
public class ThreadDemo02_1 {
    public static void main(String[] args) {
        //3. 创建任务类的一个对象
        Runnable mr = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    System.out.println("子线程输出1:" + i);
                }
            }
        };

        Thread t = new Thread(mr);
        t.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 4; i++) {
                    System.out.println("子线程输出2:" + i);
                }
            }
        }).start();

        new Thread(()-> {
            for (int i = 0; i < 4; i++) {
                System.out.println("子线程输出3:" + i);
            }
        }).start();

        //主线程输出
        for (int i = 0; i < 4; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}

创建线程的第三种方式:实现Callable接口

前两种线程创建方式都存在的一个问题

  • 加入线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。

怎么解决这个问题?

  • JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
  • 这种方式最大的优点:可以返回线程执行完毕后的结果。

多线程的第三种创建方式:利用Callable接口,FutureTask类实现。

  1. 创建任务对象
  • 定义一个类实现Callable接口,重写call方法,封装要做的事情和返回的数据。
  • 把Callable类型的对象封装成FutureTask(线程任务对象)。
  1. 把线程任务对象交给Thread对象。
  2. 调用Thread对象的start方法启动线程。
  3. 线程执行完毕后,通过FutureTask对象的get方法去获取线程任务的结果。

FutureTask的API
image

线程创建方式的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,拓展性强;可以在线程接行完毕后去取线程执行的结果。
  • 缺点:编码有些复杂。

三种线程创建方式的对比
image

线程API

Thread提供了很多与线程操作相关的方法
image

案例

MyThread类:
public class MyThread extends Thread {

    MyThread(String name){
        super(name);
    }
    @Override
    public void run(){
        for(int i = 0; i < 4; i++){
            // 当前线程对象的名字
            System.out.println(Thread.currentThread().getName() + "子线程输出:" + i);
        }
    }
}

测试类1:
// 目标:掌握线程休眠,线程join。
public class ThreadDemo02 {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 4; i++) {
            System.out.println("输出:" + i);
            // 作用:让当前线程执行的慢一点。
            // 项目经理让我写上这行代码,用户交钱了,我就注释了!
            Thread.sleep(1000); //休眠1s后再执行
        }
    }
}

测试类2:
//目标:join线程
public class ThreadDemo03 {
    public static void main(String[] args) throws Exception {
        Thread t = new MyThread("子线程");
        t.start();

        for (int i = 0; i < 10; i++) {
            System.out.println("主线程输出:" + i);

            if(i == 2){
                t.join();
            }
        }
    }
}

线程安全

什么是线程安全问题?

  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

取钱的线程安全问题

  • 场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人都在取10万元,可能会出现什么问题呢?
    image

线程安全问题出现的原因?

  • 存在多个线程在同时执行。
  • 同时访问一个共享资源
  • 存在修改该共享资源

案例

复现上述场景中的问题。
Account账户类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {
    private String cardId;
    private double money;

    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        if(this.money >= money){
            System.out.println(name + "取钱成功,吐出钞票:" + money);
            // 更新余额
            this.money -= money;
            System.out.println(name + "取钱后,余额为:" + this.money);
        }
        else{
            System.out.println(name + "来取钱,余额不足");
        }
    }
}

线程类:
public class DrawThread extends Thread{
    private Account account;
    public DrawThread(String name, Account acc) {
        super(name);
        this.account = acc;
    }
    @Override
    public void run() {
       account.drawMoney(10000);
    }
}

测试类:
//目标:模拟线程安全问题
public class ThreadSafeDemo01 {
    public static void main(String[] args) {
        //1. 创建一个账户对象,两个人共享
        Account acc = new Account("ICBC-110", 10000);

        //2. 创建两个线程,模拟两个窗口同时取钱
        new DrawThread("小红", acc).start();
        new DrawThread("小明", acc).start();
    }
}

输出结果:
10000.0
小明取钱成功,吐出钞票:10000.0
小红取钱成功,吐出钞票:10000.0
小明取钱后,余额为:0.0
小红取钱后,余额为:-10000.0

线程同步

  • 线程同步是解决线程安全问题的方案。

线程同步的思想

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

线程同步的常见方案

  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
    image

同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
synchronized(同步锁){
访问共享资源的核心代码
}
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步锁的注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

案例
代码复用上边,只修改Account类

    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        synchronized (this) {
            if(this.money >= money){
                System.out.println(name + "取钱成功,吐出钞票:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱后,余额为:" + this.money);
            }
            else{
                System.out.println(name + "来取钱,余额不足");
            }
        }
    }
}

锁对象随便选择一个唯一的对象好不好?

  • 不好,会影响其他无关线程的执行。

锁对象的使用规范

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表){
  操作共享资源的代码
}

- 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行。

**同步方法底层原理**
- 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
- 如果方法是实例方法:同步方法默认是**this**作为锁对象。
- 如果方法是静态方法:同步方法默认用**类.class**作为锁的对象。

**案例**
``` java
account账户类:
public class Account {
    private String cardId;
    private double money;

    public synchronized void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        synchronized (this) {
            if(this.money >= money){
                System.out.println(name + "取钱成功,吐出钞票:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱后,余额为:" + this.money);
            }
            else{
                System.out.println(name + "来取钱,余额不足");
            }
        }
    }
}

Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
    image
    Lock的常见方法
    image

案例

  • 沿用上边的案例,只修改Account类的代码
@Data
@NoArgsConstructor
public class Account {
    private String cardId;
    private double money;
    private Lock lk = new ReentrantLock();

    public Account(String cardId, double money) {
        this.cardId = cardId;
        this.money = money;
    }

    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        // 加锁
        lk.lock();
        try {
            if (this.money >= money) {
                System.out.println(name + "取钱成功,吐出钞票:" + money);
                // 更新余额
                this.money -= money;
                System.out.println(name + "取钱后,余额为:" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
        } finally {
            lk.unlock();
        }
    }
}

线程通信

什么是线程通信?

  • 当多个线程共同操作共享的资源时,线程间通过某种方式相互告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者消费者模型)

  • 生产者线程负责生产数据

  • 消费者线程负责消费生产者产生的数据。

  • 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产。
    image
    Object类的等待和唤醒方法
    image

注意:上述方法应该使用当前同步锁对象进行调用。

案例

生产者MakeThread类:
// 生产者线程
public class MakeThread extends Thread{
    private Desk Desk;

    public MakeThread(Desk desk, String name) {
        super(name);
        this.Desk = desk;
    }

    @Override
    public void run() {
        //厨师一直做包子
        while(true){
            try {
                Thread.sleep(1000);
                Desk.put();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
---------------------------------------------------------
消费者ConsumerThread类:
// 消费者线程
public class ConsumerThread extends Thread{
    private Desk desk;

    public ConsumerThread(Desk desk, String name) {
        super(name);
        this.desk = desk;

    }

    @Override
    public void run() {
        //顾客一直吃包子
        while (true){
            try {
                Thread.sleep(1000);
                desk.get();
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
--------------------------------------------
桌子Desk类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Desk {
    private String data; // 存放包子的数据

    // 顾客
    public synchronized void get() throws Exception {
        String name = Thread.currentThread().getName();
        if(data == null){
            // 没有包子,暂停自己,唤醒别人。
            this.notifyAll();
            this.wait();
        }
        else{
            //有包子,开始吃包子
            System.out.println(name + "正在吃" + data);
            data = null;
            this.notifyAll();
            this.wait();
        }
    }
    // 厨师
    public synchronized  void put() throws Exception {
        String name = Thread.currentThread().getName();

        if(data == null){
            //没有包子,开始做包子
            data = name + "做的美味包子";
            System.out.println(name + "正在做美味的包子!");
            this.notifyAll();
            this.wait();
        }
        else{
            //有包子,暂停自己,唤醒别人。
            this.notifyAll();
            this.wait();
        }
    }
}
-----------------------------------
测试:
//目标:实现多线程的生产者消费者模型
public class Test {
    public static void main(String[] args) {
        //1. 创建一个共享桌子
        Desk desk = new Desk();

        //2. 创建两个消费者线程
    new ConsumerThread(desk, "顾客1号").start();
    new ConsumerThread(desk, "顾客2号").start();
        //3. 创建三个生产者线程
        new MakeThread(desk, "厨师1号").start();
        new MakeThread(desk, "厨师2号").start();
        new MakeThread(desk, "厨师3号").start();
    }
}

输出结果:
厨师2号正在做美味的包子!
顾客2号正在吃厨师2号做的美味包子
厨师1号正在做美味的包子!
顾客1号正在吃厨师1号做的美味包子
厨师3号正在做美味的包子!
顾客2号正在吃厨师3号做的美味包子
厨师1号正在做美味的包子!
顾客1号正在吃厨师1号做的美味包子

线程池

什么是线程池?

  • 线程池就是一个可以复用线程的技术。

不使用线程池的问题

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

谁代表线程池?

  • JDK5.0起提供了代表线程池的接口:ExecutorService。

如何得到线程池对象?

  • 方式一:使用ExecutorService实现类ThreadPoolExecutor自创建一个线程池对象。
    image
  • 方式二:使用Excutors(线程池的工具类)调用方法返回不同特点的线程池对象。

ThreadPoolExecutor构造器
image

  • 参数一:corePoolSize:指定线程池的核心线程的数据量。
  • 参数二:maxmumPoolSize:指定线程池的最大线程数量。
  • 参数三:keepAliveTime:指定临时线程的存活时间。
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
  • 参数五:workQueue:指定线程池的任务队列。
  • 参数六:ThreadFactory:指定线程池的线程工厂。
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

线程池的注意事项
1. 临时线程什么时候创建?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

2. 什么时候会开始拒绝新任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

线程池处理Runnable任务

ExecutorService的常用方法
image

新任务拒绝策略
image

案例

MyRunnable类:
public class MyRunnable implements Runnable{
    @Override
    public void run(){
        for (int i = 0; i < 4; i++) {
            System.out.println(Thread.currentThread().getName() + "输出:" + i);
            System.out.println(Thread.currentThread().getName() + "线程进入休眠!~");
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

测试类:
//目标:创建线程池对象,处理Runnable任务
public class ThreadPoolExecutorDemo01 {
    public static void main(String[] args) {
        //1. 创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3, 5,  1, TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //2. 处理Runnable任务
        Runnable target = new MyRunnable();
        pool.execute(target); //自动创建线程,并处理此任务。
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);// 复用线程
        pool.execute(target);// 复用线程
        pool.execute(target);// 复用线程
        pool.execute(target);// 复用线程
        pool.execute(target);// 复用线程
        pool.execute(target);// 复用线程

        //3. 关闭线程池
//        pool.shutdownNow(); // 立即关闭,不管任务是否完成。返回没有执行完的任务。
//        pool.shutdown(); // 等待任务完成后,关闭线程池。
        // 注意:线程池一旦关闭,就不能再次提交任务。
    }
}

线程池处理Callable任务

案例

MyCallable类:
public class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }

    // 重写call方法,声明任务和返回的结果
    @Override
    public String call() throws Exception {
        int sum = 0;

        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "从1到"+ n+"的和为:"+sum;
    }
}

测试类:
//目标:创建线程池对象,处理Callable任务
public class ThreadPoolExecutorDemo02 {
    public static void main(String[] args) {
        //1. 创建线程池
        ExecutorService pool = new ThreadPoolExecutor(3, 5,  1, TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());

        //2. 处理Runnable任务
       Future<String> f1 = pool.submit(new MyCallable(100));
       Future<String> f2 = pool.submit(new MyCallable(100));
       Future<String> f3 = pool.submit(new MyCallable(100));

        try{
            String s1 = f1.get();
            System.out.println(s1);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        try{
            String s2 = f2.get();
            System.out.println(s2);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        try{
            String s3 = f3.get();
            System.out.println(s3);
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }
}

Executors工具类实现线程池

  • Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
    image
    注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

案例

public class ExecutorsDemo03 {
    public static void main(String[] args) {
        // 1.创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(3);

        //2. 处理Runnable任务
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(100));
        Future<String> f3 = pool.submit(new MyCallable(100));

        try{
            String s1 = f1.get();
            System.out.println(s1);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        try{
            String s2 = f2.get();
            System.out.println(s2);
        }
        catch(Exception e){
            e.printStackTrace();
        }
        try{
            String s3 = f3.get();
            System.out.println(s3);
        }
        catch(Exception e){
            e.printStackTrace();
        }
    }
}

Executors使用可能存在的陷阱

  • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
    image

并发、并行

进程

  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程是属于进程的,一个进程中可以同时运行很多个线程。
  • 进程中多个线程其实是并发和并行执行的。

并发的含义

  • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

并行的理解

  • 在同一个时刻上,同时有多个线程在北CPU调度执行。

线程的生命周期

  • 也就是线程从生到死的过程中,经历的各种状态及状态转换。
  • 理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态

  • Java总共定义了6种状态
  • 6种状态都定义在Thread类的内部枚举类中。

线程的6中状态互相转换
image
image

posted @ 2025-10-27 15:25  狂风将军  阅读(0)  评论(0)    收藏  举报