Java SE基础知识详解第[17]期—多线程

发布于:2023-01-01 ⋅ 阅读:(254) ⋅ 点赞:(0)

写在前面:

        每一个不曾起舞的日子,都是对生命的辜负。

        希望看到这里的每一个人都能努力学习,不负韶华,成就更好的自己。


        以下仅是个人学习过程中的一些想法与感悟,Java知识博大精深,作为初学者,个人能力有限,哪里写的不够清楚、明白,还请各位不吝指正,欢迎交流与讨论。如果有朋友因此了解了一些知识或对Java有了更深层次的理解,从而进行更进一步的学习,那么这篇文章的意义也就达到了。

目录

1.多线程的创建

1.1方式一:继承Thread类

1.2方式二:实现Runnable接口

1.3方式三:JDK 5.0新增:实现Callable接口

2.Thread的常用方法

3.线程安全

4.线程同步

4.1线程同步思想概述

4.2方式一:同步代码块

4.3方式二:同步方法

4.4方式三:Lock锁

5.线程通信[了解]

6.线程池

6.1线程池概述

6.2线程池实现的API、参数说明

6.3线程池处理Runnable、Callable任务

6.4Executors工具类实现线程池

7.补充知识:定时器

8.补充知识:并发、并行

9.补充知识:线程的生命周期


1.多线程的创建

Thread类

        Java是通过java.lang.Thread类来代表线程的。

        按照面向对象的思想,Thread类提供了实现多线程的方式。

1.1方式一:继承Thread类

多线程的实现方案一:继承Thread类

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

        ② 创建MyThread类的对象。

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

方式一优缺点:

        优点:编码简单。

        缺点:线程类已经继承Thread,无法继承其他类,不利于扩展。

        示例代码如下:

public class ThreadDemo1 {
    public static void main(String[] args) {
        // 3.实例化线程对象
        Thread t = new MyThread();
        // 4.调用start方法启动子线程(执行run方法)
        t.start();

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

/**
 * 1.自定义一个线程类继承Thread类
 */
class MyThread extends Thread {
    /**
     * 2.重写run方法,其中是此线程要做的工作
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行输出:" + i);
        }
    }
}

        注:main方法中不是直接调用run方法,而是调用start方法启动线程中的run方法。

        直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。

        只有调用start方法才是启动一个新的线程执行。

        注意将主线程任务放在子线程任务之后,否则主线程执行完毕后再执行子线程,相当于是一个单线程的效果。

1.2方式二:实现Runnable接口

多线程的实现方案二:实现Runnable接口

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

        ② 创建MyRunnable任务对象。

        ③ 把MyRunnable任务对象交给Thread处理。

        ④ 调用线程对象的start()方法启动线程。

Thread类构造器

方法名

说明

public Thread(String name)

可以为当前线程指定名称

public Thread(Runnable target)

封装Runnable对象成为线程对象

public Thread(Runnable target ,String name )

封装Runnable对象成为线程对象,并指定线程名称

方式二优缺点:

        优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。

        缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的。

        示例代码如下:

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

        // 4.把任务对象交给Thread处理
        Thread t = new Thread(target);

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

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行:" + i);
        }
    }
}

/**
 * 定义一个线程任务类,实现Runnable接口
 */
class MyRunnable implements Runnable {
    /**
     * 重写run方法,定义线程的执行任务
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程执行:" + i);
        }
    }
}

多线程的实现方案二:实现Runnable接口(匿名内部类形式)

        ① 可以创建Runnable的匿名内部类对象。

        ② 交给Thread处理。

        ③ 调用线程对象的start()启动线程。

        示例代码如下:

public class ThreadDemo2Other {
    public static void main(String[] args) {

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("子线程执行:" + i);
            }
        }).start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行:" + i);
        }
    }
}

1.3方式三:JDK 5.0新增:实现Callable接口

        前2种线程创建方式都存在一个问题:他们重写的run方法均不能直接返回结果,不适合需要返回线程执行结果的业务场景。

        怎么解决这个问题呢?JDK 5.0提供了Callable和FutureTask来实现,这种方式的优点是可以得到线程执行的结果

多线程的实现方案三:利用Callable、FutureTask接口实现

        ①得到任务对象

        1)定义类实现Callable接口,重写call方法,封装要做的事情。

        2)用FutureTask把Callable对象封装成线程任务对象。

        ②把线程任务对象交给Thread处理。

        ③调用Thread的start方法启动线程,执行任务

        ④线程执行完毕后、通过FutureTask的get方法去获取任务执行的结果。

FutureTask的常用API

方法名

说明

public FutureTask<>(Callable call)

把Callable对象封装成FutureTask对象

(需要声明任务对象返回值的泛型)

public V get() throws Exception

获取线程执行call方法返回的结果

        示例代码如下:

public class ThreadDemo3 {
    public static void main(String[] args) {
        // 3.创建Callable任务对象
        Callable<String> call = new MyCallable(10);
        // 4.将Callable任务对象交给FutureTask对象,封装为真正的任务对象
        /*
            FutureTask对象的作用:实现了Runnable接口,可以交给Thread对象处理
                                可以在线程执行完毕之后通过调用其get方法获取线程执行完毕的返回值
         */
        FutureTask<String> f = new FutureTask(call);
        // 5.将任务对象交给Thread处理
        Thread t = new Thread(f);
        // 6.启动线程
        t.start();

        Callable<String> call2 = new MyCallable(20);
        FutureTask<String> f2 = new FutureTask(call2);
        Thread t2 = new Thread(f2);
        t2.start();

        try {
            // 若任务没有执行完毕,则代码会在这里等待,直至线程执行完毕,再执行get方法,获取返回值
            String rs = f.get();
            System.out.println("第1个结果:" + rs);
        } catch (Exception e) {
            e.printStackTrace();
        }

        try {
            String rs2 = f2.get();
            System.out.println("第2个结果:" + rs2);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

/**
 * 1.定义一个任务类,实现Callable接口,需要声明泛型,表明返回值类型
 */
class MyCallable implements Callable<String> {
    private int n;

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

    /**
     * 2.重写call方法(任务方法)   此处为求和方法
     *
     * @return
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程执行结果" + sum;
    }
}

        注:若任务没有执行完毕,则代码会在获取返回值的get方法处等待,直至线程执行完毕,再执行get方法,获取返回值。

2.Thread的常用方法

Thread类常用方法

方法名

说明

String getName ()

获取当前线程的名称,默认线程名称是Thread-索引

void setName (String name)

将此线程的名称更改为指定的名称,

通过构造器也可以设置线程名称

public static Thread currentThread()

返回对当前正在执行的线程对象的引用

public static void sleep(long time)

让当前线程休眠指定的时间后再继续执行,单位为毫秒

public void run()

线程任务方法

public void start()

线程启动方法

        示例代码如下:

        自定义MyThread类

public class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        // 为当前线程对象设置名称,交给父类有参构造器初始化
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出:" + i);
        }
    }
}

        测试类

public class ThreadDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread("1号线程");
        t.start();

        Thread t2 = new MyThread("2号线程");
        t2.start();

        // 哪个线程执行它,它就得到哪个线程对象(当前线程对象)
        Thread m = Thread.currentThread();
        System.out.println(m.getName()); // 主线程名称为"main"

        for (int i = 0; i < 5; i++) {
            if (i == 2) {
                // 让线程休眠3s
                Thread.sleep(3000);
            }
            System.out.println("main线程输出:" + i);
        }
    }
}

        程序运行结果如下:

main

main线程输出:0

main线程输出:1

1号线程输出:0

1号线程输出:1

1号线程输出:2

1号线程输出:3

1号线程输出:4

2号线程输出:0

2号线程输出:1

2号线程输出:2

2号线程输出:3

2号线程输出:4

main线程输出:2

main线程输出:3

main线程输出:4

3.线程安全

线程安全问题

        多个线程同时操作同一个共享资源且存在修改该资源的时候可能会出现业务安全问题,称为线程安全问题。

线程安全问题出现的原因

        ①存在多线程并发。

        ②同时访问共享资源。

        ③存在修改共享资源。

线程安全问题案例模拟-取钱业务

        需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:

        ①:需要提供一个账户类,创建一个账户对象代表2个人的共享账户。

        ②:需要定义一个线程类,线程类可以处理账户对象。

        ③:创建2个线程对象,传入同一个账户对象。

        ④:启动2个线程,去同一个账户对象中取钱10万。

        示例代码如下:

        Account账户类

public class Account {
    private String cardId;
    double money; // 账户余额

    public Account() {
    }

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

    public void drawMoney(double drawMoney) throws InterruptedException {
        // 获取取钱人姓名
        String userName = Thread.currentThread().getName();
        // 判断账户余额是否足够
        if (this.money >= drawMoney) { // 余额足够
            System.out.println(userName + "取款余额" + this.money + "余额足够,开始取款");
            // 取钱
            Thread.sleep(1000); // 取钱需要1s
            System.out.println(userName + "成功取出" + drawMoney + "元");
            // 更新余额
            this.money -= drawMoney;
            System.out.println(userName + "取钱后,账户余额剩余" + this.money + "元");
        } else { // 余额不足
            System.out.println(userName + "取款余额不足!");
        }
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

        取钱线程类

public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;

    public DrawThread() {

    }

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

    @Override
    public void run() {
        // 调用取钱方法
        try {
            acc.drawMoney(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

        测试类

public class ThreadDemo {
    public static void main(String[] args) {
        // 1.定义线程类,创建一个共享的账户对象
        Account acc = new Account("icbc-001", 100000);

        // 2.创建2个线程对象,代表小明和小红同时登陆账户取钱
        new DrawThread(acc, "小明").start();
        new DrawThread(acc, "小红").start();
    }
}

        程序运行结果如下:

小红取款余额100000.0余额足够,开始取款

小明取款余额100000.0余额足够,开始取款

小明成功取出100000.0元

小明取钱后,账户余额剩余0.0元

小红成功取出100000.0元

小红取钱后,账户余额剩余-100000.0元

        分析:小红取款线程访问账户资源,判断余额,余额足够,开始取钱(线程休眠代表取钱过程),在此期间,小明取款线程访问账户资源,判断余额,余额足够,开始取钱,后两者均取钱成功,更新账户余额。

        小红线程取钱过程中,在更新账户余额之前,小明线程执行判断余额任务,在小红线程取款完毕后,小明线程已经执行完判断余额任务,不再重复判断,同样开始取款,出现线程安全问题。

4.线程同步

4.1线程同步思想概述

取钱案例出现问题的原因?

        多个线程同时执行,发现账户都是够钱的。

如何才能保证线程安全呢?

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

线程同步的核心思想

        加锁,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。

4.2方式一:同步代码块

同步代码块

        作用:把出现线程安全问题的核心代码给上锁。

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

        同步代码块代码格式:

synchronized(同步锁对象) {

操作共享资源的代码(核心代码)

}

锁对象要求

        理论上:锁对象只要对于当前同时执行的线程来说是同一个对象即可。

        例如,对于某一确定字符串,在堆内存中具有唯一地址,同步锁对象若是某一字符串,则针对所有的线程来讲,锁对象均是唯一的,则表明所有的线程对象均需要遵守唯一进入,任务执行结束后解锁的原则。同步锁对象若是某一对象,因不同对象的地址不同,因此当且仅当对于不同的线程来说,同步锁对象是同一对象时,才会上锁,若对于不同的线程对象,同步锁对象是不同的(堆内存中地址不同),那么这些线程对象不受同步锁的影响

        因此锁对象用任意唯一的对象并不好,会影响其他无关线程的执行。

锁对象的规范要求

        规范上:建议使用共享资源作为锁对象

        对于实例方法建议使用this作为锁对象

        对于静态方法建议使用字节码(类名.class)对象作为锁对象(与用字符串效果一致,类名.class更规范)。

        示例代码如下:

更改第3节Account账户类中的drawMoney方法代码。

    public void drawMoney(double drawMoney) throws InterruptedException {
        // 获取取钱人姓名
        String userName = Thread.currentThread().getName();
        // 同步代码块
        synchronized (this) {
            // 判断账户余额是否足够
            if (this.money >= drawMoney) { // 余额足够
                System.out.println(userName + "取款余额" + this.money + "余额足够,开始取款");
                // 取钱
                Thread.sleep(1000); // 取钱需要1s
                System.out.println(userName + "成功取出" + drawMoney + "元");
                // 更新余额
                this.money -= drawMoney;
                System.out.println(userName + "取钱后,账户余额剩余" + this.money + "元");
            } else { // 余额不足
                System.out.println(userName + "取款余额不足!");
            }
        }
    }

        程序运行结果如下:

小明取款余额100000.0余额足够,开始取款

小明成功取出100000.0元

小明取钱后,账户余额剩余0.0元

小红取款余额不足!

4.3方式二:同步方法

同步方法

        作用:把出现线程安全问题的核心方法给上锁。

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

        同步方法格式:

修饰符 synchronized 返回值类型 方法名称(形参列表) {

操作共享资源的代码

}

同步方法底层原理

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

        如果方法是实例方法,同步方法默认用this作为的锁对象,但是代码要求高度面向对象。

        如果方法是静态方法,同步方法默认用类名.class作为的锁对象

        示例代码如下:

更改第3节Account账户类中的drawMoney方法代码。

    public synchronized void drawMoney(double drawMoney) throws InterruptedException { // synchronized代表同步方法
        // 获取取钱人姓名
        String userName = Thread.currentThread().getName();
        // 判断账户余额是否足够
        if (this.money >= drawMoney) { // 余额足够
            System.out.println(userName + "取款余额" + this.money + "余额足够,开始取款");
            // 取钱
            Thread.sleep(1000); // 取钱需要1s
            System.out.println(userName + "成功取出" + drawMoney + "元");
            // 更新余额
            this.money -= drawMoney;
            System.out.println(userName + "取钱后,账户余额剩余" + this.money + "元");
        } else { // 余额不足
            System.out.println(userName + "取款余额不足!");
        }
    }

4.4方式三:Lock锁

Lock锁

        为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便。

        Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作。

        Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来构建Lock锁对象

ReentrantLock构造器

方法名

说明

public ReentrantLock()

获得Lock锁的实现类对象

Lock类常用API

方法名

说明

void lock()

获得锁

void unlock()

释放锁

        Lock使用格式:

public void m() {

lock.lock();

try {

// ...method body

} finally {

lock.unlock();

}

}

        示例代码如下:

        更改第3节Account账户类的代码。

public class Account {
    private String cardId;
    double money; // 账户余额
    // final修饰,表示锁对象唯一且不可替换
    private final Lock lock = new ReentrantLock();

    public Account() {
    }

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

    public void drawMoney(double drawMoney) throws InterruptedException { // synchronized代表同步方法
        // 获取取钱人姓名
        String userName = Thread.currentThread().getName();
        lock.lock(); // 上锁
        try {
            // 判断账户余额是否足够
            if (this.money >= drawMoney) { // 余额足够
                System.out.println(userName + "取款余额" + this.money + "余额足够,开始取款");
                // 取钱
                Thread.sleep(1000); // 取钱需要1s
                System.out.println(userName + "成功取出" + drawMoney + "元");
                // 更新余额
                this.money -= drawMoney;
                System.out.println(userName + "取钱后,账户余额剩余" + this.money + "元");
            } else { // 余额不足
                System.out.println(userName + "取款余额不足!");
            }
        } finally { // 将解锁方法添加到finally语句中,保证即使try语句出现异常,也可以将上锁的资源解锁,等待下一线程访问
            lock.unlock(); // 解锁
        }
    }

    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
}

5.线程通信[了解]

什么是线程通信、如何实现?

        所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信。

线程通信常见形式

        通过共享一个数据的方式实现。

        根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

线程通信实际应用场景

        生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者产生的数据。

        要求:生产者线程生产完数据后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己。

线程通信案例模拟

        假如有这样一个场景,小明和小红有三个爸爸,爸爸们负责存钱,小明和小红负责取钱,必须一存、一取。

        线程通信的前提:线程通信通常是在多个线程操作同一个共享资源的时候需要进行通信,且要保证线程安全。

Object类的等待和唤醒方法

方法名

说明

void wait ()

让当前线程等待并释放所占锁,

直到另一个线程调用notify()方法或 notifyAll()方法

void notify ()

唤醒正在等待的单个线程

void notifyAll ()

唤醒正在等待的所有线程

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

        锁是可以跨方法的!!!对于不同的线程来说,只要锁对象是相同的,就把被synchronized修饰的所有代码(代码块、方法)放在同一个箱子里,只允许单线程进入并操作。

        结论:两个线程同时执行被synchronized修饰的相同对象的不同(相同)方法,锁生效,因为两个线程使用的是相同的对象锁。

        类锁和对象锁是相互独立的,互不相斥。

        示例代码如下:

账户类

public class Account {
    private String cardId; // 账户ID
    double money; // 账户余额

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

    /**
     * 亲爹、干爹、岳父存钱
     * @param money
     */
    public synchronized void depositMoney(double money) {
        try {
            String name = Thread.currentThread().getName();
            if (this.money == 0) {
                // 余额为0,允许存款
                this.money += money;
                System.out.println(name + "存款成功,金额为" + money + "元,当前余额为" + this.money + "元");
                // 此时钱已存入,有钱了(模拟一存一取)
                this.notifyAll();
                this.wait();
            } else {
                // 余额不为0,拒绝存款
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 小红、小明取钱
     * @param money
     */
    public synchronized void drawMoney(double money) {
        try {
            String name = Thread.currentThread().getName();
            if (this.money >= money) {
                // 余额足够,允许取款
                this.money -= money;
                System.out.println(name + "取款成功,金额为" + money + "元,当前余额为" + this.money + "元");
                // 此时钱已取出,没钱了(模拟一存一取 )
                this.notifyAll();
                this.wait();
            } else {
                // 余额不足,拒绝取款
                // 一旦发现无法消费,此时需要将自己线程挂起,让出CPU并唤醒生产者,等生产完毕后继续执行
                this.notifyAll(); // 唤醒所有线程
                this.wait(); // 锁对象,让当前线程进入等待(哪个线程占用该锁对象哪个线程就等待)
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public String getCardId() {
        return cardId;
    }

    public void setCardId(String cardId) {
        this.cardId = cardId;
    }

    public double getMoney() {
        return money;
    }

    public void setMoney(double money) {
        this.money = money;
    }
    
}

存款类:

public class DepositThread extends Thread {
    // 接收处理的账户对象
    private Account acc;


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

    @Override
    public void run() {
        // 亲爹、干爹、岳父存钱
        while (true) {
            acc.depositMoney(100000);
            try {
                Thread.sleep(2000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

取款类

public class DrawThread extends Thread {
    // 接收处理的账户对象
    private Account acc;


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

    @Override
    public void run() {
        // 小明、小红取钱
        while (true) {
            acc.drawMoney(100000);
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

测试类

public class ThreadDemo {
    public static void main(String[] args) {
        // 目标:了解线程通信流程
        // 使用3个爸爸(生产者)存钱,2个孩子(消费者)取钱,模拟线程通信思想(一存10W、一取10W)
        // 1.创建账户对象,代表5个人共同操作的账户
        Account acc = new Account("ICBC-001", 0);
        // 2.创建2个取钱线程,代表小红和小明
        new DrawThread(acc, "小明").start();
        new DrawThread(acc, "小红").start();
        // 3.创建3个存钱线程,代表亲爹、干爹和岳父
        new DepositThread(acc, "亲爹").start();
        new DepositThread(acc, "干爹").start();
        new DepositThread(acc, "岳父").start();
    }
}

        程序运行结果如下:

亲爹存款成功,金额为100000.0元,当前余额为100000.0元

小红取款成功,金额为100000.0元,当前余额为0.0元

干爹存款成功,金额为100000.0元,当前余额为100000.0元

小红取款成功,金额为100000.0元,当前余额为0.0元

干爹存款成功,金额为100000.0元,当前余额为100000.0元

小明取款成功,金额为100000.0元,当前余额为0.0元

亲爹存款成功,金额为100000.0元,当前余额为100000.0元

小红取款成功,金额为100000.0元,当前余额为0.0元

岳父存款成功,金额为100000.0元,当前余额为100000.0元

...

6.线程池

6.1线程池概述

什么是线程池?

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

不使用线程池的问题

        如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能。

6.2线程池实现的API、参数说明

谁代表线程池?

        JDK 5.0起提供了代表线程池的接口:ExecutorService

如何得到线程池对象

        方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

        方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

ThreadPoolExecutor构造器的参数说明

public ThreadPoolExecutor(int corePoolSize,

 int maximumPoolSize,

   long keepAliveTime,

   TimeUnit unit,

   BlockingQueue<Runnable> workQueue,

   ThreadFactory threadFactory,

   RejectedExecutionHandler handler)

参数一:指定线程池的线程数量(核心线程)corePoolSize:不能小于0

参数二:指定线程池可支持的最大线程数: maximumPoolSize 最大数量≥核心线程数量

参数三:指定临时线程的最大存活时间: keepAliveTime 不能小于0

参数四:指定存活时间的单位(秒、分、时、天): unit 时间单位

参数五:指定任务队列: workQueue 不能为null

参数六:指定用哪个线程工厂创建线程: threadFactory 不能为null

参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler 不能为null

线程池常见面试题

临时线程什么时候创建?

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

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

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

6.3线程池处理Runnable、Callable任务

ThreadPoolExecutor创建线程池对象示例:

ExecutorService pools = new ThreadPoolExecutor(3, 5 , 8 , TimeUnit.SECONDS, new ArrayBlockingQueue<>(6), Executors.defaultThreadFactory() , new ThreadPoolExecutor.AbortPolicy());

ExecutorService的常用API

方法名

说明

void execute(Runnable command)

执行任务/命令,没有返回值,

一般用来执行 Runnable 任务

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

执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务

void shutdown()

等任务执行完毕后关闭线程池

List<Runnable> shutdownNow()

立刻关闭,停止正在执行的任务,

并返回队列中未执行的任务

新任务拒绝策略

方法名

说明

ThreadPoolExecutor.AbortPolicy

丢弃任务并抛出RejectedExecutionException异常,是默认的策略

ThreadPoolExecutor.DiscardPolicy

丢弃任务,但是不抛出异常 这是不推荐的做法

ThreadPoolExecutor.DiscardOldestPolicy

抛弃队列中等待最久的任务,

然后把当前任务加入队列中

ThreadPoolExecutor.CallerRunsPolicy

由主线程负责调用任务的run()方法

从而绕过线程池直接执行

        示例代码如下:

自定义MyRunnable线程任务类

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出Hello World ==>" + i);
        }
        try {
            System.out.println(Thread.currentThread().getName() + "线程休眠了");
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

线程池执行Runnable任务测试类

public class ThreadDemo1 {
    public static void main(String[] args) {
        // 1.创建线程池对象
        /*
            public ThreadPoolExecutor(int corePoolSize,
                                     int maximumPoolSize,
                                      long keepAliveTime,
                                     TimeUnit unit,
                                     BlockingQueue<Runnable> workQueue,
                                     ThreadFactory threadFactory,
                                     RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        // 2.将任务交由线程池处理
        Runnable target = new MyRunnable();
        // 此时创建3个核心线程
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 此时5个任务进入临时队列
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 此时创建2个临时线程
        pool.execute(target);
        pool.execute(target);
        // 此时核心线程都在忙,临时队列满了,且临时线程也均在工作,此时抛出异常
//        pool.execute(target);

        // 关闭线程池(开发中一般不会使用)
//        pool.shutdownNow(); // 立刻关闭线程池,即使任务还没有完成,此时会丢失任务
        pool.shutdown(); // 等待正在执行的线程任务与队列中的任务执行完再关闭线程池
    }

}

自定义MyCallable线程任务类

/**
 * 1.定义一个任务类,实现Callable接口,需要声明泛型,表明返回值类型
 */
class MyCallable implements Callable<String> {
    private int n;

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

    /**
     * 2.重写call方法(任务方法)   此处为求和方法
     *
     * @return
     * @throws Exception
     */
    @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;
    }

}

线程池执行MyCallable任务测试类

public class ThreadDemo2 {
    public static void main(String[] args) throws Exception {
        // 1.创建线程池对象
        /*
            public ThreadPoolExecutor(int corePoolSize,
                                     int maximumPoolSize,
                                      long keepAliveTime,
                                     TimeUnit unit,
                                     BlockingQueue<Runnable> workQueue,
                                     ThreadFactory threadFactory,
                                     RejectedExecutionHandler handler)
         */
        ExecutorService pool = new ThreadPoolExecutor(3, 5,
                6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        // 2.将任务交由线程池处理
        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(700));
        Future<String> f8 = pool.submit(new MyCallable(800));

        Future<String> f9 = pool.submit(new MyCallable(900));
        Future<String> f10 = pool.submit(new MyCallable(1000));

//        String rs1 = f1.get();
//        System.out.println(rs1);

        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());
        System.out.println(f8.get());
        System.out.println(f9.get());
        System.out.println(f10.get());
    }
    
}

6.4Executors工具类实现线程池

Executors:线程池的工具类通过调用方法返回不同类型的线程池对象。

Executors得到线程池对象的常用API

方法名

说明

public static ExecutorService newCachedThreadPool()

线程数量随着任务增加而增加,

如果线程任务执行完毕且空闲了一段时间

则会被回收掉。

public static ExecutorService newFixedThreadPool (int nThreads)

创建固定线程数量的线程池,

如果某个线程因为执行异常而结束,

那么线程池会补充一个新线程替代它。

public static ExecutorService newSingleThreadExecutor ()

创建只有一个线程的线程池对象,

如果该线程出现异常而结束,

那么线程池会补充一个新线程。

public static ScheduledExecutorService newScheduledThreadPool (int corePoolSize)

创建一个线程池,

可以实现在给定的延迟后运行任务,

或者定期执行任务。

注:Executors的底层其实也是基于线程池的实现类ThreadPoolExecutor创建线程池对象的。

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

Executors使用可能存在的陷阱

方法名

存在问题

public static ExecutorService newFixedThreadPool (int nThreads)

允许请求的任务队列长度是Integer.MAX_VALUE,可能出现 OOM错误( java.lang.OutOfMemoryError )

public static ExecutorService newSingleThreadExecutor()

public static ExecutorService newCachedThreadPool()

创建的线程数量最大上限是Integer.MAX_VALUE, 线程数可能会随着任务1:1增长,也可能出现OOM错误 ( java.lang.OutOfMemoryError )

public static ScheduledExecutorService newScheduledThreadPool (int corePoolSize)

        示例代码如下:

public class ThreadDemo3 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);

        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable()); // 不执行,因没有多余线程了
    }
    
}

注:

Executors工具类底层是基于什么方式实现的线程池对象?

        线程池ExecutorService的实现类:ThreadPoolExecutor。

Executors是否适合做大型互联网场景的线程池方案?

        不合适。

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

7.补充知识:定时器

定时器是什么?

        定时器是一种控制任务延时调用,或者周期调用的技术。

        作用:闹钟、定时邮件发送。

定时器的实现方式

        方式一:Timer

        方式二: ScheduledExecutorService

Timer定时器

Timer定时器构造器

方法名

说明

public Timer()

创建Timer定时器对象

Timer定时器常用API

方法名

说明

public void schedule

(TimerTask task, long delay, long period)

开启一个定时器,按照计划处理TimerTask任务

        示例代码如下:

public class TimerDemo1 {
    public static void main(String[] args) {
        // 1.创建Timer定时器
        Timer timer = new Timer();
        // 2.调用方法,处理定时任务
        // TimerTask继承了Runnable,属于线程任务类
        timer.schedule(new TimerTask() { // 定时器本身就是一个单线程
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行了一次");
            }
        }, 10000, 2000);
    }

}

Timer定时器的特点和存在的问题

        1、Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入。

        2、可能因为其中的某个任务的异常使Timer线程死掉,从而影响后续任务执行。

ScheduledExecutorService定时器

        ScheduledExecutorService是 jdk1.5中引入了并发包,目的是为了弥补Timer的缺陷,ScheduledExecutorService内部为线程池。

Executors的方法

方法名

说明

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

得到线程池对象

ScheduledExecutorService的常用API

方法名

说明

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)

周期调度方法

        示例代码如下:

public class TimerDemo2 {
    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        // 1.创建ScheduleExecutorService线程池做定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        // 2.开启定时任务
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(String.format(Thread.currentThread().getName() + "AAA执行了" + sdf.format(new Date())));
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(String.format(Thread.currentThread().getName() + "BBB执行了" + sdf.format(new Date())));
//                System.out.println(10 / 0); 此时此线程死亡,其他线程正常运行
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(String.format(Thread.currentThread().getName() + "CCC执行了" + sdf.format(new Date())));
            }
        }, 0, 2, TimeUnit.SECONDS);
    }

}

ScheduledExecutorService的优点

        基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

8.补充知识:并发、并行

并发与并行

        正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的。

并发的理解:

        CPU同时处理线程的数量有限。

        CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

结论:

并发和并行的含义。

        并发:CPU分时轮询的执行线程,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。

        并行:同一个时刻同时在执行。

9.补充知识:线程的生命周期

线程的状态

        线程的状态:也就是线程从生到死的过程,以及中间经历的各种状态及状态转换。

        理解线程的状态有利于提升并发编程的理解能力。

Java线程的状态

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

public class Thread{

...

public enum State {

NEW,

RUNNABLE,

BLOCKED,

WAITING,

TIMED_WAITING,

TERMINATED;

}

...

}

注:sleep()方法与wait()方法休眠的不同

        线程执行sleep()方法时,不会让出锁对象,而是继续占有,在休眠结束后继续向下执行。

        线程执行wait()方法时,会让出锁对象,在等待结束后重新获取锁对象(不一定能够获取到,有可能被其他线程获取到)。

线程的6种状态

线程状态

描述

NEW(新建)

线程刚被创建,但是并未启动

Runnable(可运行)

线程已经调用了start()等待CPU调度

Blocked(锁阻塞)

线程在执行的时候未竞争到锁对象,

则该线程进入Blocked状态

Waiting(无限等待)

一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒

Timed Waiting(计时等待)

同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。带有超时参数的常用方法有Thread.sleep、Object.wait

Terminated(被终止)

因为run方法正常退出而死亡,

或者因为没有捕获的异常终止了run方法而死亡


写在最后:

        感谢读完!

        纵然缓慢,驰而不息!加油!

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到