Java基础——多线程

时间:2024-11-17 07:19:37

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. 线程的生命周期

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