Java基础——多线程

1. 线程

  1. 是一个程序内部的一条执行流程
  2. 程序中如果只有一条执行流程,那这个程序就是单线程的程序

2. 多线程

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

2.1. 如何创建多条线程

  1. Java通过java.lang.Thread类的对象来代表线程
2.1.1. 方式一:继承Thread类
复制代码
// 1. 继承Thread类
public class MyThread extends Thread{
    // 2. 重写run方法
    @Override
    public void run() {
        System.out.println("子线程");
    }
}

public static void main(String[] args) {
    // 3. 创建子线程对象
    Thread t = new MyThread();

    // 4. 启动子线程
    t.start();

    System.out.println("主线程");
}
  1. 优点:编码简单
  2. 缺点:已经继承Thread,无法继承其他类,不利于功能的扩展
  3. 启动线程必须调用start方法,而不能调用run方法,否则会将线程对象当作一个普通的对象。
  4. 不要把主线程任务放在启动子线程之前。
2.1.2. 方式二:实现Runnable接口
复制代码
// 1. 定义任务类, 实现Runnable接口
public class MyRunnable implements Runnable {
    // 2. 重写run方法
    @Override
    public void run() {
        System.out.println("Runnable子线程");
    }
}

public static void main(String[] args) {
    // 3. 创建一个任务对象
    Runnable runnable = new MyRunnable();

    // 4. 把任务对象交给一个线程对象, 启动线程
    new Thread(runnable).start();

    System.out.println("主线程");
}
  1. 任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强
2.1.3. 方式三:利用Callable接口、FutureTask类实现
  1. 以上两种方式的问题:如果线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。
  2. 使用Callable接口,可以返回线程执行完毕后的结果
  1. 未来任务对象的作用,是一个任务对象,实现了Runnable接口;可以在线程执行完毕之后,用未来任务对象调用get方法获取执行完的返回结果

    // 1. 实现Callable接口
    public class MyCallable implements Callable<String> {
    // 2. 重写call方法
    @Override
    public String call() throws Exception {
    // 描述线程任务, 返回线程执行后的结果
    int sum = 0;
    for(int i = 1; i <= 100; i++){
    sum += i;
    }
    return String.valueOf(sum);
    }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 3. 创建一个Callable对象
    Callable<String> callable = new MyCallable();

    复制代码
     // 4. 将Callable对象封装成FutureTask任务对象
     FutureTask<String> futureTask = new FutureTask<>(callable);
    
     // 5. 使用任务对象创建一个线程对象, 并启动
     new Thread(futureTask).start();
    
     // 6. 获取线程执行完毕返回的结果
     String s = futureTask.get();
     System.out.println(s);

    }

2.2. Thread的常用方法

3. 线程安全

  1. 多个线程,同时访问同一共享资源,且存在修改该资源的时候,可能会出现业务安全问题

4. 线程同步

  1. 是用于解决线程安全问题的方案
  2. 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

4.1. 线程同步的方案

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

4.2. 加锁的方式

4.2.1. 方式一:同步代码块
  1. 作用:把访问共享资源的核心代码块给上锁,以此保证线程安全
  2. 写法
  1. 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
  2. 注意事项:对于当前同时执行的线程 来说,同步锁必须是同一把(同一个对象)
  3. Ctrl + ALT + T选第九个,快速生成同步代码块
  4. 建议使用共享资源作为锁对象, 对于实例方法建议使用this作为锁对象;静态方法建议使用**字节码(类名.class)**对象作为锁对象
4.2.2. 方式二:同步方法
  1. 作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
  1. 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
4.2.3. 方式三:Lock锁
  1. 可以通过它创建锁对象,进行加锁和解锁
  2. Lock是接口,不能直接实例化,可以采用他的实现类ReentrantLock来创建锁对象。

5. 线程通信

  1. 当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,一相互协调,并避免无效的资源争夺
  2. 生产者消费者问题

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

复制代码
public class Test5 {
    public static void main(String[] args) {
        // 有三个生产者线程负责生产包子, 每次只能有一个包子放在桌子上
        // 两个消费者线程负责吃包子, 每次只有一个线程能将包子吃掉
        // 1. 创建一个桌子对象
        Desk desk = new Desk();

        // 2. 创建三个厨师线程
        new Thread(() ->{
            while (true) {
                desk.put(); // 抢桌子
            }
        }, "厨师1").start();

        new Thread(() ->{
            while (true) {
                desk.put(); // 抢桌子
            }
        }, "厨师2").start();

        new Thread(() ->{
            while (true) {
                desk.put(); // 抢桌子
            }
        }, "厨师3").start();

        // 3. 创建两个吃货线程
        new Thread(() -> {
            while (true){
                desk.get();
            }
        },"吃货1").start();

        new Thread(() -> {
            while (true){
                desk.get();
            }
        },"吃货2").start();
    }
}

public class Desk {
    private List<String> list = new ArrayList<>();

    public synchronized void put() {
        try {
            String name = Thread.currentThread().getName();
            if(list.size() == 0){
                list.add(name + "包的包子");
                System.out.println(name + "包了一个包子");
                Thread.sleep(2000);

                // 唤醒别人, 等待自己
                this.notifyAll();
                this.wait();
            }else {  // 有包子了
                // 唤醒别人, 等待自己
                this.notifyAll();
                this.wait();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public synchronized void get() {

        try {
            if(list.size() == 1){ // 有包子
                String name = Thread.currentThread().getName();
                System.out.println(name + "吃了" + list.get(0));
                list.clear(); // 清空
                Thread.sleep(1000);

                // 唤醒别人, 等待自己
                this.notifyAll();
                this.wait();
            } else {
                // 唤醒别人, 等待自己
                this.notifyAll();
                this.wait();
            }
        }
        catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

6. 线程池

  1. 线程池就是一个可以复用线程的技术
  2. 代表线程池的接口,ExecutorService

6.1. 创建线程池对象

6.1.1. 使用ExecutorService的实现类ThreadPoolExecutor创建线程池对象
复制代码
// 通过ThreadPoolExecutor创建一个线程池对象
/*
public ThreadPoolExecutor(int corePoolSize,
                      int maximumPoolSize,
                      long keepAliveTime,
                      TimeUnit unit,
                      BlockingQueue<Runnable> workQueue,
                      ThreadFactory threadFactory,
                      RejectedExecutionHandler handler)
 */
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
        TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());

注意事项:

  1. 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
  2. 核心线程和临时线程都在忙,任务队列也满了 ,新的任务过来的时候才会开始拒绝任务
  3. 如果是计算密集型任务,核心线程数 = CPU核数 + 1
  4. 如果是IO密集型任务,核心线程数 = CPU核数 * 2
6.1.2. 使用Executors(线程池工具类)调用静态方法返回不同特点的线程池对象
  1. 这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象
  2. 大型并发系统环境中使用Executors如果不注意可能会出现系统风险

6.2. 线程池处理Runnable任务、Callable任务

7. 并发、并行、线程的生命周期

7.1. 进程

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

7.2. 并发

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

7.3. 并行

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

7.4. 线程的生命周期

线程从创建到销毁的过程中,经历的各种状态及状态的转换

相关推荐
龚思凯4 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响6 分钟前
枚举在实际开发中的使用小Tips
后端
on the way 1239 分钟前
行为型设计模式之Mediator(中介者)
java·设计模式·中介者模式
保持学习ing11 分钟前
Spring注解开发
java·深度学习·spring·框架
wuhunyu12 分钟前
基于 langchain4j 的简易 RAG
后端
techzhi12 分钟前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
异常君37 分钟前
Spring 中的 FactoryBean 与 BeanFactory:核心概念深度解析
java·spring·面试
weixin_461259411 小时前
[C]C语言日志系统宏技巧解析
java·服务器·c语言
cacyiol_Z1 小时前
在SpringBoot中使用AWS SDK实现邮箱验证码服务
java·spring boot·spring