day12 多线程

目录

1.概念相关

1.1什么是线程

1.2什么是多线程

2.创建线程

2.1方式一:继承Thread类

2.1.1实现步骤

2.1.2优缺点

2.1.3注意事项

2.2方式二:实现Runnable接口

2.2.1实现步骤

2.2.2优缺点

2.2.3匿名内部类写法

2.3方式三:实现callable接口

2.3.1callable接口解决了什么问题

2.3.2实现步骤

2.3.3FutureTask的API

2.3.4优缺点

3.线程的常用方法

4.线程安全

4.1什么是线程安全

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

[4.3模拟线程安全问题场景 :取钱](#4.3模拟线程安全问题场景 :取钱)

5.线程同步

5.1什么是线程同步

5.2线程同步的核心思想

5.3常见方案

5.4方式一:同步代码块

5.4.1作用:

5.4.2原理:

5.4.3注意事项:

5.4.4锁对象的使用规范

5.4.5如何实现线程安全的

5.5方式二:同步方法

5.5.1作用

5.5.2原理

5.5.3底层原理

5.5.4同步代码块和同步方法哪种好

5.6lock锁

5.6.1lock锁是什么

5.6.2lock锁的构造器、常用方法

5.6.3锁对象建议加上什么修饰

5.6.4释放锁的操作建议放到哪里

6.线程池

6.1认识线程池

6.2不使用线程池的后果

6.3创建线程池

6.4任务拒绝策略

6.5线程池的注意事项

6.6处理runnable任务

6.4.1ExecutorService的常用方法

6.7处理callable

6.8通过Executors创建线程池

6.8.1方法

6.8.2Executors使用可能存在的陷阱

7.并发、并行

7.1进程

7.2并发的含义

7.3并行的含义


1.概念相关

1.1什么是线程

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

1.2什么是多线程

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

2.创建线程

2.1方式一:继承Thread类

2.1.1实现步骤

【1】定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

【2】创建MyThread类的对象

【3】调用线程对象的start()方法启动线程(启动后还是执行run方法的)

2.1.2优缺点

【1】优点:编码简单

【2】缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

2.1.3注意事项

【1】启动线程必须是调用start方法,不是调用run方法。

【2】不要把主线程任务放在启动子线程之前。

java 复制代码
public class Test {
    public static void main(String[] args) {
        Thread t1=new MyThread();
        t1.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程"+i);
        }
    }

}

class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 10; i++){
            System.out.println("子线程" + i);
        }
    }
}

2.2方式二:实现Runnable接口

2.2.1实现步骤

【1】定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

【2】创建MyRunnable任务对象

【3】把MyRunnable任务对象交给Thread处理。

【4】调用线程对象的start()方法启动线程

2.2.2优缺点

【1】优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。

【2】缺点:需要多一个Runnable对象。

2.2.3匿名内部类写法

【1】可以创建Runnable的匿名内部类对象。

【2】再交给Thread线程对象。

【3】再调用线程对象的start()启动线程。

java 复制代码
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程"+i);
                }
            }
        }).start();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程2"+i);
            }
        }).start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程"+i);
        }
    }

2.3方式三:实现callable接口

2.3.1callable接口解决了什么问题

假如线程执行完毕后有一些数据需要返回,前两种重写的run方法均不能直接返回结果。使用Callable接口和FutureTask类来实现创建,可以返回线程执行完毕后的结果。

2.3.2实现步骤

【1】定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。

【2】把Callable类型的对象封装成FutureTask(线程任务对象)。

【3】把线程任务对象交给Thread对象。

【4】调用Thread对象的start方法启动线程。

【5】线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

2.3.3FutureTask的API

|--------------------------------------|-----------------------------|
| FutureTask提供的构造器 | 说明 |
| public FutureTask<>(Callable call) | 把Callable对象封装成FutureTask对象。 |

|---------------------------------|--------------------|
| FutureTask提供的方法 | 说明 |
| public V get() throws Exception | 获取线程执行call方法返回的结果。 |

2.3.4优缺点

【1】优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。

【2】缺点:编码复杂一点。

java 复制代码
public class ThreadDemo3 {
        public static void main(String[] args) {
        // 目标:掌握多线程的创建方式三:实现Callable接口,方式三的优势:可以获取线程执行完毕后的结果的。
        // 3、创建一个Callable接口的实现类对象。
        Callable<String> c1 = new MyCallable(100);
        // 4、把Callable对象封装成一个真正的线程任务对象FutureTask对象。
        /**
         * 未来任务对象的作用?
         *    a、本质是一个Runnable线程任务对象,可以交给Thread线程对象处理。
         *    b、可以获取线程执行完毕后的结果。
         */
        FutureTask<String> f1 = new FutureTask<>(c1); // public FutureTask(Callable<V> callable)
        // 5、把FutureTask对象作为参数传递给Thread线程对象。
        Thread t1 = new Thread(f1);
        // 6、启动线程。
        t1.start();

        Callable<String> c2 = new MyCallable(50);
        FutureTask<String> f2 = new FutureTask<>(c2); // public FutureTask(Callable<V> callable)
        Thread t2 = new Thread(f2);
        t2.start();

        // 获取线程执行完毕后返回的结果
        try {
            // 如果主线程发现第一个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
            System.out.println(f1.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            // 如果主线程发现第二个线程还没有执行完毕,会让出CPU,等第一个线程执行完毕后,才会往下执行!
            System.out.println(f2.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 1、定义一个实现类实现Callable接口
class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    // 2、实现call方法,定义线程执行体
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程计算1-" + n + "的和是:"  + sum;
    }
}

3.线程的常用方法

|--------------------------------------|----------------------------|
| Tread提供的常用方法 | 说明 |
| public void run() | 线程的任务方法 |
| public void start() | 启动线程 |
| public String getName() | 获取当前线程的名称,线程名称默认是Thread-索引 |
| public void setName(String name) | 为线程设置名称 |
| public static Thread currentThread() | 获取当前执行的线程对象 |
| public static void sleep(long time) | 让当前执行的线程休眠多少毫秒后,再继续执行 |
| public final void join()... | 让调用当前这个方法的线程先执行完! |

java 复制代码
public class ThreadApiDemo1 {
    public static void main(String[] args) {
        // 目标:搞清楚线程的常用方法。
        Thread t1 = new MyThread("1号线程");
        // t1.setName("1号线程");
        t1.start();
        System.out.println(t1.getName()); // 线程默认名称是:Thread-索引

        Thread t2 = new MyThread("2号线程");
        // t2.setName("2号线程");
        t2.start();
        System.out.println(t2.getName()); // 线程默认名称是:Thread-索引

        // 哪个线程调用这个代码,这个代码就拿到哪个线程
        Thread m = Thread.currentThread(); // 主线程
        m.setName("主线程");
        System.out.println(m.getName()); // main
    }
}

// 1、定义一个子类继承Thread类,成为一个线程类。
class MyThread extends Thread {
    public MyThread(String name) {
        super(name); // public Thread(String name)
    }

    // 2、重写Thread类的run方法
    @Override
    public void run() {
        // 3、在run方法中编写线程的任务代码(线程要干的活儿)
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);
        }
    }
}
java 复制代码
public static void main(String[] args) {
        // 目标:搞清楚Thread类的Sleep方法(线程休眠)
        for (int i = 1; i <= 10; i++) {
            System.out.println(i);
            try {
                // 让当前执行的线程进入休眠状态,直到时间到了,才会继续执行。
                // 项目经理让我加上这行代码,如果用户交钱了,我就注释掉。
                Thread.sleep(1000); // 1000ms = 1s
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
java 复制代码
public class ThreadApiDemo3 {
    public static void main(String[] args) {
        // 目标:搞清楚线程的join方法:线程插队:让调用这个方法线程先执行完毕。
        MyThread2 t1 = new MyThread2();
        t1.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() +"线程输出:" + i);
            if(i == 1){
                try {
                    t1.join(); // 插队 让t1线程先执行完毕,然后继续执行主线程
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() +"子线程输出:" + i);
        }
    }
}

4.线程安全

4.1什么是线程安全

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

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

【1】存在多个线程在同时执行

【2】同时访问一个共享资源

【3】存在修改该共享资源

4.3模拟线程安全问题场景 :取钱

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    private String cardId; // 卡号
    private double money; // 余额

    // 小明和小红都到这里来了取钱
    public synchronized 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 + "取钱失败,余额不足");
        }
    }
}
java 复制代码
// 取钱线程类
public class DrawThread extends Thread{
    private Account acc; // 记住线程对象要处理的账户对象。

    public DrawThread(String name, Account acc) {
        super(name);
        this.acc = acc;
    }

    @Override
    public void run() {
        // 小明 小红 取钱
        acc.drawMoney(100000);
    }
}
java 复制代码
    public static void main(String[] args) {
        // 目标:模拟线程安全问题。
        // 1、设计一个账户类:用于创建小明和小红的共同账户对象,存入10万。
        Account acc = new Account("ICBC-110", 100000);

        // 2、设计线程类:创建小明和小红两个线程,模拟小明和小红同时去同一个账户取款10万。
        new DrawThread("小明", acc).start();
        new DrawThread("小红", acc).start();
    }

5.线程同步

5.1什么是线程同步

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

5.2线程同步的核心思想

让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。

5.3常见方案

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

5.4方式一:同步代码块

5.4.1作用:

把访问共享资源的核心代码给上锁,以此保证线程安全。

5.4.2原理:

每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

5.4.3注意事项:

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

5.4.4锁对象的使用规范

【1】建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。

【2】对于静态方法建议使用字节码(类名.class)对象作为锁对象。

5.4.5如何实现线程安全的

【1】对出现问题的核心代码使用synchronized进行加锁

【2】每次只能一个线程占锁进入访问

java 复制代码
// 小明和小红都到这里来了取钱
    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 + "取钱失败,余额不足");
            }
        }
    }

5.5方式二:同步方法

5.5.1作用

把访问共享资源的核心方法给上锁,以此保证线程安全。

5.5.2原理

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

5.5.3底层原理

【1】同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。

【2】如果方法是实例方法:同步方法默认用this作为的锁对象。

【3】如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

5.5.4同步代码块和同步方法哪种好

同步代码块锁的范围更小,同步方法锁的范围更大。同步方法可读性更好

java 复制代码
    // 小明和小红都到这里来了取钱
    public synchronized 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 + "取钱失败,余额不足");
        }
    }

5.6lock锁

5.6.1lock锁是什么

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

5.6.2lock锁的构造器、常用方法

|-------------------------|---------------|
| 构造器 | 说明 |
| public ReentrantLock​() | 获得Lock锁的实现类对象 |

|---------------|-----|
| 方法名称 | 说明 |
| void lock() | 获得锁 |
| void unlock() | 释放锁 |

5.6.3锁对象建议加上什么修饰

建议使用final修饰,防止被别人篡改

5.6.4释放锁的操作建议放到哪里

建议将释放锁的操作放到finally代码块中,确保锁用完了一定会被释放

java 复制代码
    private final Lock lk = new ReentrantLock(); // 保护锁对象

    // 小明和小红都到这里来了取钱
    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();// 解锁
        }
    }

6.线程池

6.1认识线程池

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

6.2不使用线程池的后果

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

6.3创建线程池

通过ThreadPoolExecutor创建线程池。使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

【1】参数一:corePoolSize : 指定线程池的核心线程的数量。

【2】参数二:maximumPoolSize:指定线程池的最大线程数量。

【3】参数三:keepAliveTime :指定临时线程的存活时间。

【4】参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)

【5】参数五:workQueue:指定线程池的任务队列。

【6】参数六:threadFactory:指定线程池的线程工厂。

【7】参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

6.4任务拒绝策略

|-------------------------------------------|--------------------------------------------|
| 策略 | 说明 |
| ThreadPoolExecutor.AbortPolicy() | 丢弃任务并抛出RejectedExecutionException异常。是默认的策略 |
| ThreadPoolExecutor. DiscardPolicy() | 丢弃任务,但是不抛出异常,这是不推荐的做法 |
| ThreadPoolExecutor. DiscardOldestPolicy() | 抛弃队列中等待最久的任务 然后把当前任务加入队列中 |
| ThreadPoolExecutor. CallerRunsPolicy() | 由主线程负责调用任务的run()方法从而绕过线程池直接执行 |

6.5线程池的注意事项

java 复制代码
// 1、定义一个线程任务类实现Runnable接口
public class MyRunnable implements Runnable {
    // 2、重写run方法,设置线程任务
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出:" + i);
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (Exception e) {
               e.printStackTrace();
            }
        }
    }
}
java 复制代码
    public static void main(String[] args) {
        // 目标:创建线程池对象来使用。
        // 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象。
        ExecutorService pool = new ThreadPoolExecutor(3, 5,
                10, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3),
               Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());

        // 2、使用线程池处理任务!看会不会复用线程?
        Runnable target = new MyRunnable();
        pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务
        pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务
        pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target); // 到了临时线程的创建时机了
        pool.execute(target); // 到了临时线程的创建时机了
        pool.execute(target); // 到了任务拒绝策略了,忙不过来

        // 3、关闭线程池 :一般不关闭线程池。
        // pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!
//        pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!
    }

6.6处理runnable任务

6.4.1ExecutorService的常用方法

|----------------------------------------|-------------------------------------|
| 方法名称 | 说明 |
| void execute(Runnable command) | 执行 Runnable 任务 |
| Future<T> submit(Callable<T> task) | 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果 |
| void shutdown() | 等全部任务执行完毕后,再关闭线程池! |
| List<Runnable> shutdownNow() | 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |

java 复制代码
        // 2、使用线程池处理任务!看会不会复用线程?
        Runnable target = new MyRunnable();
        pool.execute(target); // 提交第1个任务 创建第1个线程 自动启动线程处理这个任务
        pool.execute(target); // 提交第2个任务 创建第2个线程 自动启动线程处理这个任务
        pool.execute(target); // 提交第2个任务 创建第3个线程 自动启动线程处理这个任务
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target); // 到了临时线程的创建时机了
        pool.execute(target); // 到了临时线程的创建时机了
        pool.execute(target); // 到了任务拒绝策略了,忙不过来

        // 3、关闭线程池 :一般不关闭线程池。
        pool.shutdown(); // 等所有任务执行完毕后再关闭线程池!
        pool.shutdownNow(); // 立即关闭,不管任务是否执行完毕!

6.7处理callable

线程池如何处理Callable任务,并得到任务执行完后返回的结果?

Future<T> submit(Callable<T> command)

6.8通过Executors创建线程池

6.8.1方法

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

|----------------------------------------------------------------------------------|------------------------------------------------|
| 方法名称 | 说明 |
| public static ExecutorService newFixedThreadPool​(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。 |
| public static ExecutorService newSingleThreadExecutor() | 创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。 |
| public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。 |
| public static ScheduledExecutorService newScheduledThreadPool​(int corePoolSize) | 创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。 |

6.8.2Executors使用可能存在的陷阱

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

【2】不适合做大型互联网场景的线程池方案

【3】建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则,规避资源耗尽的风险。

7.并发、并行

7.1进程

【1】正在运行的程序(软件)就是一个独立的进程。

【2】线程是属于进程的,一个进程中可以同时运行很多个线程。

【3】进程中的多个线程其实是并发和并行执行的。

7.2并发的含义

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

7.3并行的含义

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

相关推荐
程序猿麦小七3 分钟前
基于springboot的景区网页设计与实现
java·spring boot·后端·旅游·景区
敲敲敲-敲代码4 分钟前
游戏设计:推箱子【easyx图形界面/c语言】
c语言·开发语言·游戏
weisian15110 分钟前
认证鉴权框架SpringSecurity-2--重点组件和过滤器链篇
java·安全
蓝田~11 分钟前
SpringBoot-自定义注解,拦截器
java·spring boot·后端
ROC_bird..13 分钟前
STL - vector的使用和模拟实现
开发语言·c++
.生产的驴14 分钟前
SpringCloud Gateway网关路由配置 接口统一 登录验证 权限校验 路由属性
java·spring boot·后端·spring·spring cloud·gateway·rabbitmq
MavenTalk19 分钟前
Move开发语言在区块链的开发与应用
开发语言·python·rust·区块链·solidity·move
v'sir28 分钟前
POI word转pdf乱码问题处理
java·spring boot·后端·pdf·word
提高记忆力36 分钟前
SpringBoot整合FreeMarker生成word表格文件
java·spring
JDS_DIJ37 分钟前
RabbitMQ
java·rabbitmq·java-rabbitmq