多线程


线程

1、什么是多线程?

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

2、创建方法一:集成Thread类

方法一:

  1. 子类集成Thread类
  2. 必须重写Thread类的run方法
  3. 在主线程中,创建MyThread线程类的对象代表一个线程
  4. 调用start( )方法,启动线程

注意事项:

  1. 启动线程必须调用start方法,而不是run方法。
  2. 不要把主线程任务放到启动子线程任务之前。
package com.dapixiu;

public class Main {
    /**
     * 程序的入口点
     * 创建并启动一个自定义线程,同时在主线程中输出计数
     * @param args 命令行参数,本程序未使用
     */
    public static void main(String[] args) {
//3. 在主线程中,创建MyThread线程类的对象代表一个线程    
        // 创建自定义线程实例
        MyThread t = new MyThread();
//4. 启动    
        // 启动自定义线程
        t.start();
    
        // 主线程输出循环
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
package com.dapixiu;
// 1. 子类继承Thread方法
public class MyThread extends Thread {
    /**
     * 重写run方法以定义线程的执行逻辑
     * 此方法没有输入参数和返回值,通过打印信息来执行其任务
     */
//2. 重写run方法    
    @Override
    public void run() {
        // 使用for循环打印5次信息,以展示线程的执行过程
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

2.1 方法一的优缺点

3、创建方法二:实现Runnable接口

方法二:

  1. 定义一个任务类,实现Runnable接口
  2. 重写Runnable的run方法
  3. 在主线程中new一个任务类对象
  4. 把任务类对象交给一个线程对象处理,并调用start( )方法
package com.dapixiu;

public class Main {

    /**
     * 程序的入口点
     * 该方法创建并启动一个新的线程来执行MyRunnable任务,同时在主线程中执行一个简单的循环输出
     * @param args 命令行参数,本程序未使用
     */
    public static void main(String[] args) {
        // 创建MyRunnable实例,准备在线程中执行
        MyRunnable target = new MyRunnable();
        // 创建一个新线程,将MyRunnable实例作为目标传递给它,然后启动线程
        new Thread(target).start();
    
        // 主线程执行的循环,输出5次,每次输出一个数字,从0到4
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程输出:" + i);
        }
    }
}
package com.dapixiu;

public class MyRunnable implements Runnable{
    /**
     * 重写run方法以定义线程的执行逻辑
     * 此方法中包含了线程一旦启动就需要执行的任务
     * 在这个例子中,线程的任务是循环五次,每次打印一个数字
     */
    @Override
    public void run() {
        // 循环五次,每次打印一个数字
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

3.2 方法二的优缺点

3.3 匿名内部类的写法

匿名内部类的写法

package com.dapixiu;

public class Main {

    /**
     * 程序的入口点
     * 创建并启动一个新的线程,同时主线程也执行自己的任务
     * @param args 命令行参数,本程序不使用此参数
     */
    public static void main(String[] args) {
        // 创建一个Runnable对象,定义新线程要执行的任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                // 新线程执行的任务:循环5次,每次打印一个信息
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程MyThread输出:" + i);
                }
            }
        };
        
        // 创建一个新的线程,并将之前创建的Runnable对象作为参数传递给它,然后启动线程
        new Thread(runnable).start();
    
        // 主线程执行的任务:循环5次,每次打印一个信息
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程Thread输出:" + i);
        }
    }
}

匿名内部类的简化写法:

package com.dapixiu;

public class Main {
    public static void main(String[] args) {
        // 创建一个新的线程,并将Runnable对象作为参数传递给它,然后启动线程
        new Thread(() -> {
            // 新线程执行的任务:循环5次,每次打印一个信息
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程MyThread输出:" + i);
            }
        }).start();

        // 主线程执行的任务:循环5次,每次打印一个信息
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程Thread输出:" + i);
        }
    }
}

4、创建方法三:实现Callable接口

背景

创建方法

package com.dapixiu;

import java.util.concurrent.Callable;

//1、让这个类实现Callable接口
public class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }
    
// 2.重写call方法以定义线程的任务
    @Override
    public String call() throws Exception {
        int sum = 0; // 初始化累加和变量为0
        // 使用循环来计算从1到n的累加和
        for (int i = 0; i <= n; i++) {
            sum += i; // 将当前数字i加到累加和sum上
        }
        // 返回计算结果的字符串表示
        return "线程求得1到" + n + "的和是:" + sum;
    }
}
package com.dapixiu;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
//3.创建一个Callable对象
        Callable<String> call = new MyCallable(100);
//4.把Callable对象封装成为一个FutureTask对象(任务对象)
        /*未来任务对象有什么用?
        * 1、是一个任务对象,实现了Runnable对象
        * 2、可以在线程执行完毕之后,用未来任务对象来调用get方法,获取线程执行完毕后的结果*/
        FutureTask<String> f1 = new FutureTask<String>(call);

        MyCallable call2 = new MyCallable(200);
        FutureTask<String> f2 = new FutureTask<>(call2);


//5.把任务对象交给一个Thread对象
        new Thread(f1).start();
        new Thread(f2).start();
//6.获取线程执行完毕后返回的结果.
        String s = f1.get();
        System.out.println(s);

        String s2 = f2.get();
        System.out.println(s2);

    }
}

4.1 方法三的优缺点

5、Thread的常用方法

6、线程安全(三种方法)

  • 多个线程,同时处理一个共享资源,可能会出现线程安全问题。
  • 加锁。每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

加锁的常见方式有三种:

  1. 同步代码块
  2. 同步方法
  3. Lock锁

6.1 同步代码块

同步代码块

锁对象的使用规范:

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

6.2 同步方法

同步方法

同步代码块与同步方法:

  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大。
  • 同步方法在可读性上会更好一些

6.3 Lock锁

  • 需要自己创建锁对象,手动加锁和解锁,来保证线程安全。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

方法

文档1

文档2

7、线程通信

线程通信

  • 生产者消费者模型:
  • 先唤醒,再等待。

8、创建并使用线程池

8.1 什么是线程池

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

8.2 不使用线程池的问题

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

第七个参数-拒绝策略

  • 线程工厂:是用来创建线程的
package com.dapixiu;

import java.util.concurrent.*;

public class Main {
    /* 
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
     * 该方法演示了如何配置和使用一个线程池
     * @param args 命令行参数,本例中未使用
     * @throws Exception 如果发生任何异常,将被直接抛出
     */
    public static void main(String[] args) throws Exception {
        // 配置线程池的参数
        int corePoolSize = 3; // 核心线程数
        int maximumPoolSize = 5; // 最大线程数
        long keepAliveTime = 8; // 空闲线程存活时间
        TimeUnit unit = TimeUnit.SECONDS; // 时间单位
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5); // 工作队列
        ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略
    
        // 创建线程池
        // 解释:这里创建了一个线程池,核心线程数为3,最大线程数为5,空闲线程存活时间为8秒
        // 使用ArrayBlockingQueue作为工作队列,队列大小为5,使用默认的线程工厂
        // 当任务无法提交到线程池时采用AbortPolicy策略,直接抛出异常
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue, threadFactory, handler);
    }
}

8.3 临时线程什么时候创建

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

8.4 什么时候开始会拒绝信任务

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

8.5 ThreadPoolExecutor创建Runnable

package com.dapixiu;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        int corePoolSize = 3; // 核心线程数
        int maximumPoolSize = 5; // 最大线程数
        long keepAliveTime = 8; // 空闲线程存活时间
        TimeUnit unit = TimeUnit.SECONDS; // 时间单位
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(5); // 工作队列
        ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue, threadFactory, handler);

        MyRunnable target = new MyRunnable();
        pool.execute(target);//线程池自动创建一个信线程,自动处理这个任务,自动执行。
        pool.execute(target);//线程池自动创建一个信线程,自动处理这个任务,自动执行。
        pool.execute(target);//线程池自动创建一个信线程,自动处理这个任务,自动执行。
        pool.execute(target);//复用前面的 核心线程。
        pool.execute(target);//复用前面的 核心线程。
    }
}
package com.dapixiu;

public class MyRunnable implements Runnable {
    /**
     * 重写run方法以定义线程的执行逻辑
     * 这个方法首先打印当前线程的名称和一个固定消息
     * 然后线程暂停1秒以模拟耗时操作或线程活动
     */
    @Override
    public void run() {
        // 打印当前线程的名称,用于标识线程
        System.out.println(Thread.currentThread().getName() + "====> 输出666");
        try {
            // 线程暂停1秒,模拟耗时操作
            Thread.sleep(1000);
        } catch (Exception e) {
            // 异常处理:如果线程被中断,打印异常信息
            e.printStackTrace();
        }
    }
}

8.6 ThreadPoolExecutor处理Callable

package com.dapixiu;

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws Exception {
        int corePoolSize = 3; // 核心线程数
        int maximumPoolSize = 5; // 最大线程数
        long keepAliveTime = 8; // 空闲线程存活时间
        TimeUnit unit = TimeUnit.SECONDS; // 时间单位
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(4); // 工作队列
        ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略

        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue, threadFactory, handler);


        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));
        Future<String> f5 = pool.submit(new MyCallable(500));
        Future<String> f6 = pool.submit(new MyCallable(600));
        Future<String> f7 = pool.submit(new MyCallable(600));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
        System.out.println(f6.get());
        System.out.println(f7.get());
    }
}
package com.dapixiu;

import java.util.concurrent.Callable;

//1、让这个类实现Callable接口
public class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    // 2.重写call方法以定义线程的任务
    @Override
    public String call() throws Exception {
        int sum = 0; // 初始化累加和变量为0
        // 使用循环来计算从1到n的累加和
        for (int i = 0; i <= n; i++) {
            sum += i; // 将当前数字i加到累加和sum上
        }
        // 返回计算结果的字符串表示
        return Thread.currentThread().getName() + "线程求得1到" + n + "的和是:" + sum;
    }
}

执行结果

9、Executors

是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

package com.dapixiu;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    /**
     * 程序的入口点
     * 创建一个固定大小的线程池,并提交四个任务
     * @param args 命令行参数
     * @throws Exception 如果执行过程中发生错误
     */
    public static void main(String[] args) throws Exception {
        // 创建一个固定大小的线程池,包含3个线程
        ExecutorService pool = Executors.newFixedThreadPool(3);
    
        // 提交四个任务到线程池
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));
        Future<String> f4 = pool.submit(new MyCallable(400));
    
        // 获取每个任务的执行结果
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

核心线程数量到底配置多少呢???

  • 计算雷集型的11核心线程数量=CPU的核数+1

  • IO密集型的任务:核心线程数量=CPU核数*2

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

alibaba规则

10、其他

并发

并行

多线程,即并发和并行同时进行的。

线程的生命周期

状态转换


文章作者: ZhangYao
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ZhangYao !
  目录