java——多线程基础

线程的概述

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

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

如何在程序中创建出多条线程?
Java是通过java.lang.Thread 类的对象来代表线程的。

多线程的创建

方式一:继承Thread类

  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

示例代码如下:
主线程类:

public class ThreadTest1 {
    // main方法是由一条默认的主线程负责执行。
    public static void main(String[] args) {
 
        // 3、创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        // 4、启动线程(自动执行run方法的)
        t.start();  // main线程 t线程
 
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

子线程类:

/**
 * 1、让子类继承Thread线程类。
 */
public class MyThread extends Thread{
    // 2、必须重写Thread类的run方法
    @Override
    public void run() {
        // 描述线程的执行任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

方式一优缺点:
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

多线程的注意事项

1、启动线程必须是调用start方法,不是调用run方法。
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。
2、不要把主线程任务放在启动子线程之前。
这样主线程一直是先跑完的,相当于是一个单线程的效果了。

方式二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程
    20240224-021236-Ye.png

示例代码如下:
定义一个任务类

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

主线程类

/**
 * 多线程的创建方式二:实现Runnable接口。
 */
public class ThreadTest2 {
    public static void main(String[] args) {
        // 3、创建任务对象。
        Runnable target = new MyRunnable();
        // 4、把任务对象交给一个线程对象处理。
        //  public Thread(Runnable target)
        new Thread(target).start();
 
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出 ===》" + i);
        }
    }
}

方式二的优缺点
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
缺点:需要多一个Runnable对象。

线程创建方式二的匿名内部类写法

  1. 可以创建Runnable的匿名内部类对象。
  2. 再交给Thread线程对象。
  3. 再调用线程对象的start()启动线程。

代码示例:

/**
 * 多线程创建方式二的匿名内部类写法。
 */
public class ThreadTest2_2 {
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();
 
        // 简化形式1:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();
 
        // 简化形式2:
        new Thread(() -> {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();
 
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

方式三:利用Callable接口、FutureTask类来实现。

前两种线程创建方式都存在的一个问题:
假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果。

怎么解决这个问题?
JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
这种方式最大的优点:可以返回线程执行完毕后的结果

方式三 实现步骤:

  1. 创建任务对象
    定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    把Callable类型的对象封装成FutureTask(线程任务对象)。
  2. 把线程任务对象交给Thread对象。
  3. 调用Thread对象的start方法启动线程。
  4. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

示例代码如下:
创建任务对象类

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 {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "线程求出了1-" + n + "的和是:" + sum;
    }
}

主线程类

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
 
/**
 * 线程的创建方式三:实现Callable接口。
 */
public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        // 3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1  = new FutureTask<>(call);
        // 5、把任务对象交给一个Thread对象
        new Thread(f1).start();
 
 
        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2  = new FutureTask<>(call2);
        new Thread(f2).start();
 
 
        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        String rs = f1.get();
        System.out.println(rs);
 
        String rs2 = f2.get();
        System.out.println(rs2);
    }
}

FutureTask的API
20240224-023728-f1.png

方式三的优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果:
缺点:编码复杂一点。线程创建方式三的优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。

Thread常用的方法

Thread提供了很多与线程操作相关的方法
20240224-024029-q8.png

示例代码:
子线程

public class MyThread extends Thread{
    public MyThread(String name){
        super(name); // 为当前线程设置名字了
    }
    @Override
    public void run() {
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread t = Thread.currentThread();
        for (int i = 1; i <= 3; i++) {
            System.out.println(t.getName() + "输出:" + i);
        }
    }
}

主方法

/**
 * Thread的常用方法。
 */
public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1 = new MyThread("1号线程");
        // t1.setName("1号线程");
        t1.start();
        System.out.println(t1.getName()); // Thread-0
 
        Thread t2 = new MyThread("2号线程");
        // t2.setName("2号线程");
        t2.start();
        System.out.println(t2.getName()); // Thread-1
 
        // 主线程对象的名字
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        m.setName("最牛的线程");
        System.out.println(m.getName()); // main
 
        for (int i = 1; i <= 5; i++) {
            System.out.println(m.getName() + "线程输出:" + i);
        }
    }
}

掌握sleep方法,join方法的作用

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
 * sleep方法,join方法的作用。
 */
public class ThreadTest2 {
    public static void main(String[] args) throws Exception {
        System.out.println(Runtime.getRuntime().availableProcessors());
        for (int i = 1; i <= 5; i++) {
            System.out.println(i);
            // 休眠5s
            if(i == 3){
                // 会让当前执行的线程暂停5秒,再继续执行
                // 项目经理让我加上这行代码,如果用户交钱了,我就注释掉!
                 Thread.sleep(5000);
            }
        }
 
        // join方法作用:让当前调用这个方法的线程先执行完。
        Thread t1 = new MyThread("1号线程");
        t1.start();
        t1.join();
 
        Thread t2 = new MyThread("2号线程");
        t2.start();
        t2.join();
 
        Thread t3 = new MyThread("3号线程");
        t3.start();
        t3.join();
    }
}

线程安全问题

线程安全问题概述

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

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

  1. 存在多个线程在同时执行
  2. 同时访问一个共享资源
  3. 存在修改该共享资源

线程安全问题案例

取钱案例描述

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

1. 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户。
2. 需要定义一个线程类(用于创建两个线程,分别代表小明和小红),
3. 创建2个线程,传入同一个账户对象给2个线程处理。
4. 启动2个线程,同时去同一个账户对象中取钱10万。

出现线程安全问题的步骤:
20240224-141227-FK.png

模拟代码如下:

先定义一个共享的账户类

public class Account {
    private String cardId; // 卡号
    private double money; // 余额。

    public Account() {
    }

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

    // 小明 小红同时过来的
    public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        // 1、判断余额是否足够
        if(this.money >= money){
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "来取钱后,余额剩余:" + this.money);
        }else {
            System.out.println(name + "来取钱:余额不足~");
        }
    }

    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(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        // 取钱(小明,小红)
        acc.drawMoney(100000);
    }
}

最后,再写一个测试类,在测试类中创建两个线程对象

public class ThreadTest {
    public static void main(String[] args) {
         // 1、创建一个账户对象,代表两个人的共享账户。
        Account acc = new Account("ICBC-110", 100000);
        // 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。
        new DrawThread(acc, "小明").start(); // 小明
        new DrawThread(acc, "小红").start(); // 小红
    }
}

执行结果

某个执行结果:

小明来取钱100000.0成功!
小红来取钱100000.0成功!
小红来取钱后,余额剩余:-100000.0
小明来取钱后,余额剩余:0.0

线程同步

概述

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

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

线程同步的常见方案

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

Java提供了三种方案:

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

1. 同步代码块

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

synchronized(同步锁){
    访问共享资源的核心代码
}

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步锁的注意事项
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

代码示例
在共享账户类里使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改Account类中的代码即可。

// 小明 小红线程同时过来的
public void drawMoney(double money) {
    // 先搞清楚是谁来取钱?
    String name = Thread.currentThread().getName();
    // 1、判断余额是否足够
    // this表示该账户对象,正好代表共享资源!
    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 + "来取钱:余额不足~");
        }
    }
}

执行结果:

小明来取钱100000.0成功!
小明来取钱后,余额剩余:0.0
小红来取钱:余额不足~

锁对象如何选择的问题

1. 建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象
我们把锁改为"锁" 这样一个字符串也行 因为这个资源在内存中永远只有一份
所以各个线程需要去竞争 但是这样好不好? 明显不行 万一有另外俩个人再创了一个账户 那就变成了四个人竞争一把锁了
2. 对于实例方法,建议使用this作为锁对象
3. 对于静态方法,建议把类的字节码(类名.class)当做锁对象这里是Account.class

2. 同步方法

同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行,同样是修改Account类中的代码即可。

修饰符 synchronized 返回值类型 方法名称(形参列表){
    操作共享资源的代码
}

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

同步方法底层原理

  1. 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  2. 如果方法是实例方法:同步方法默认用this作为的锁对象
  3. 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

示例代码如下:

// 同步方法
public synchronized void drawMoney(double money) {
    // 先搞清楚是谁来取钱?
    String name = Thread.currentThread().getName();
    // 1、判断余额是否足够
    if(this.money >= money){
        System.out.println(name + "来取钱" + money + "成功!");
        this.money -= money;
        System.out.println(name + "来取钱后,余额剩余:" + this.money);
    }else {
        System.out.println(name + "来取钱:余额不足~");
    }
}

同步代码块和同步方法区别

1.不存在哪个好与不好,只是一个锁住的范围大,一个范围小
其中锁的范围小一点 性能稍微好一点 可以提前加载那些公共区域的代码 但是提升的性能对于现在的计算机来说可以忽略不计
反而同步方法的可读性要好一些
2.同步方法是将方法中所有的代码锁住
3.同步代码块是将方法中的部分代码锁住

3. Lock锁

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

Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。格式如下

1.首先在成员变量位置,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)
	private final Lock lk = new ReentrantLock();
2.在需要上锁的地方加入下面的代码
	 lk.lock(); // 加锁
	 //...中间是被锁住的代码...
	 lk.unlock(); // 解锁

使用Lock锁改写前面DrawThread中取钱的方法,代码如下

// 创建了一个锁对象
//因为俩个线程公用一个账户 所以建立一个实例变量作为锁是可以的
//用final修饰更专业 防止二次赋值
private final Lock lk = new ReentrantLock();

public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        try {//用try cath finally写更专业 因为你不能保证被锁的代码没有bug 有bug也要及时解锁
            lk.lock(); // 加锁
            // 1、判断余额是否足够
            if(this.money >= money){
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "来取钱后,余额剩余:" + this.money);
            }else {
                System.out.println(name + "来取钱:余额不足~");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lk.unlock(); // 解锁
        }
    }
}

运行程序结果,观察线程安全问题已解决。

注意事项:

  1. lock锁需要使用final修饰更专业 防止二次赋值
    private final Lock lk = new ReentrantLock();

  2. 加锁和解锁时用try cath finally写更专业 因为你不能保证被锁的代码没有bug 有bug也要及时解锁

线程通信

概述

什么是线程通信?
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  1. 生产者线程负责生产数据
  2. 消费者线程负责消费生产者生产的数据。
  3. 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

线程通信案例

比如下面案例中,有3个厨师(生产者线程),两个顾客(消费者线程)。
20240224-150124-SZ.png

案例的思路:

1.先确定在这个案例中,什么是共享数据?
	答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。

2.再确定有那几条线程?哪个是生产者,哪个是消费者?
	答:厨师是生产者线程,3条生产者线程; 
	   顾客是消费者线程,2条消费者线程
	   
3.什么时候将哪一个线程设置为什么状态
	生产者线程(厨师)放包子:
		 1)先判断是否有包子
		 2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待
		 3)有包子时,不做包子了,直接唤醒别人、然后让自己等待
		 	
	消费者线程(顾客)吃包子:
		 1)先判断是否有包子
		 2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待
		 3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待

20240224-151402-IS.png

注意:上述方法应该使用当前同步锁对象进行调用。
释放当前锁对象时,必须先唤醒其他线程,再释放自己所占锁

案例代码实现

按照上面分析的思路和java Object提供的api写代码。先写桌子类,代码如下

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

    // 放1个包子的方法
    // 厨师1 厨师2 厨师3
    //实例方法默认用this作为锁 所以可以保证锁住5个线程 它们公用一个桌子对象
    //锁也是可以跨方法的
    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();//必须用当前同步锁对象进行调用 否则会出bug
                this.wait();//因为只有锁对象知道当前谁占据着它 谁需要等待
            }else {
                // 有包子了,不做了。
                // 唤醒别人, 等待自己
                this.notifyAll();//注意!!!notifyAll()和wait()位置不能调换
                this.wait();//你如果先wait了 你让自己等待了 那你还怎么唤醒别人
            }
        } catch (Exception e) {//拦截sleep异常
            e.printStackTrace();
        }
    }

    // 吃货1 吃货2
    public synchronized void get() {
        try {
            String name = Thread.currentThread().getName();
            if(list.size() == 1){
                // 有包子,吃了
                System.out.println(name  + "吃了:" + list.get(0));
                list.clear();
                Thread.sleep(1000);
                this.notifyAll();
                this.wait();
            }else {
                // 没有包子
                this.notifyAll();
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程

public class ThreadTest {
    public static void main(String[] args) {
        //   需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
        //      2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。
        Desk desk  = new Desk();

        // 创建3个生产者线程(3个厨师)
        new Thread(() -> {//匿名内部类写法
            while (true) {
                desk.put();
            }
        }, "厨师1").start();

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

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

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

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

执行结果如下:

厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货1吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
//不终止则一直运行下去 可以发现没有出现线程安全问题 

线程池

线程池概述

  1. 什么是线程池?
    线程池就是一个可以复用线程的技术。

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

  3. 线程池解决的问题:
    使用线程池,就可以解决上面的问题。线程池内部会有一个容器,存储几个核心线程,假设有3个核心线程,这3个核心线程可以处理3个任务。
    但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。
    所以,线程池就是一个线程复用技术,它可以提高线程的利用率。

线程池创建

在JDK5版本中提供了代表线程池的接口ExecutorService,而这个接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。下面是它的构造器,参数比较多

20240224-154547-S2.png

用这7个参数的构造器来创建线程池的对象。代码如下

ExecutorService pool = new ThreadPoolExecutor(
    3,	//核心线程数有3个
    5,  //最大线程数有5个。   临时线程数=最大线程数-核心线程数=5-3=2
    8,	//临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。
    TimeUnit.SECONDS,//时间单位(秒)
    new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在任务队列中等待
    Executors.defaultThreadFactory(), //用于创建线程的工厂对象
    new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

关于线程池,需要注意下面的两个问题

  1. 临时线程什么时候创建?
    注意!新任务提交时,发现核心线程都在忙、并且任务队列满了、并且还可以创建临时线程,此时会创建临时线程。
    注意是任务队列满了之后才会创建临时线程 而不是临时线程满了才加入任务队列

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

线程池执行Runnable任务

创建好线程池之后,接下来我们就可以使用线程池执行任务了。
线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。
下面的execute方法可以用来执行Runnable任务。

20240224-155432-D2.png
20240224-160537-Gc.png

代码案例

先准备一个线程任务类

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        // 任务是干啥的?
        System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");
        //为了模拟线程一直在执行,这里睡久一点
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程池处理任务类
执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。

public class ThreadPoolTest1 {
    public static void main(String[] args) {
        // 1、通过ThreadPoolExecutor创建一个线程池对象。
        ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy());

        Runnable target = new MyRunnable();
        pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        pool.execute(target);
        // 到了临时线程的创建时机了
        pool.execute(target);
        pool.execute(target);
        // 到了新任务的拒绝时机了!
        pool.execute(target);

        // pool.shutdown(); // 等着线程池的任务全部执行完毕后,再关闭线程池
        // pool.shutdownNow(); // 立即关闭线程池!不管任务是否执行完毕!
    }
}

执行结果:

pool-1-thread-5 ==> 输出666~~
main ==> 输出666~~
pool-1-thread-1 ==> 输出666~~
pool-1-thread-3 ==> 输出666~~
pool-1-thread-4 ==> 输出666~~
pool-1-thread-2 ==> 输出666~~
//其中123是核心线程执行的 45是临时线程执行的
//注意程序还是一直运行的 线程池不会自动关闭 设计出来就是一直服务的
//main输出是因为使用了CallerRunsPolicy()拒绝策略 新来的要拒绝的任务由主线程main执行了

线程池执行Callable任务

Callable任务相对于Runnable任务来说,就是多了一个返回值。
执行Callable任务需要用到上面ExecutorService的submit方法

代码案例

先准备一个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 {
        // 描述线程的任务,返回线程执行返回后的结果。
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;
    }
}

再准备一个测试类,在测试类中创建线程池,并执行callable任务。

public class ThreadPoolTest2 {
    public static void main(String[] args) throws Exception {
        // 1、通过ThreadPoolExecutor创建一个线程池对象。
        ExecutorService pool = new ThreadPoolExecutor(
            3,
            5,
            8,
            TimeUnit.SECONDS, 
            new ArrayBlockingQueue<>(4),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());

        // 2、使用线程处理Callable任务。
        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));

        // 3、执行完Callable任务后,需要获取返回结果。
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

某次执行后,结果如下所示

pool-1-thread-1求出了1-100的和是:5050
pool-1-thread-2求出了1-200的和是:20100
pool-1-thread-3求出了1-300的和是:45150
pool-1-thread-3求出了1-400的和是:80200

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

根据经验法则,大致参考以下原则:

  1. 如果是计算密集型的任务:核心线程数量 = CPU的核数 + 1
  2. 如果是IO密集型的任务:核心线程数量 = CPU核数 * 2

CPU核数查看,这个cpu是16核。
20240224-162721-sw.png

线程池工具类(Executors)

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

20240224-162047-Pf.png

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

测试代码:

public class ThreadPoolTest3 {
    public static void main(String[] args) throws Exception {
        // 1、通过Executors创建一个线程池对象。
        ExecutorService pool = Executors.newFixedThreadPool(17);

        // 2、使用线程处理Callable任务。
        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());
    }
}

Executors创建线程池这么好用,为什么不推荐同学们使用呢?原因在这里:看下图,这是《阿里巴巴Java开发手册》提供的强制规范要求,在大型并发系统环境中容易出bug。
20240224-162619-Io.png

并发、并行和生命周期

并发和并行

1. 什么是进程、线程?

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

可以打开系统的任务管理器看看(快捷键:Ctrl+Shfit+Esc),自己的电脑上目前有哪些进程。
20240224-163444-Vo.png

2. 什么是并发?

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

简单记:并发就是多条线程交替执行

3. 什么是并行?

并行指的是,多个线程同时被CPU调度执行。如下图所示,多个CPU核心在执行多条线程

20240224-163545-35.png

4. 多线程到底是并发还是并行呢?

其实多个线程在我们的电脑上执行,并发和并行是同时存在的。

线程的生命周期

在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示
20240224-164432-Ts.png

NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。

这几种状态之间切换关系如下图所示

20240224-164913-Fh.png

线程的六种状态的总结
20240224-164948-tM.png

乐观锁与悲观锁

悲观锁:一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后再解锁。是线程安全的,但是性能较差!

乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出线程安全问题的时候才开始控制。是线程安全的,且性能较好。

下面举例说明,先写一个没有锁的多线程场景:

public static void main(String[] args) throws Exception {
        //需求:1个静态变量,100个线程,每个线程对其加100次 最终值为10000
        Runnable target = new MyRunnable();

        for (int i = 1; i <= 100; i++) {
            new Thread(target).start();
        }
    }

public class MyRunnable implements Runnable{
    private int count;//用实例变量代替静态变量 反正线程都是公用一个任务对象的 所以是可以的
    @Override
    public void run() {
       //100次
        for (int i = 0; i < 100; i++) {
            System.out.println("count--------->"+(++count));
        }
    }
}

某次运行结果:

...
count--------->9989
count--------->9990
count--------->9991
count--------->9992
count--------->9993
count--------->9994
count--------->9995
count--------->9996
count--------->9997
count--------->9998
count--------->9999
//虽然概率比较小 还是出现了一次线程安全问题
//有一次增值计算重叠了 没有加到10000

乐观锁解决:

首先解释原理,安全问题来自于,比如当count是10时,俩个线程几乎同时进入,将其值修改成11,于是便发生了安全问题,少加了一次。乐观锁采用CAS算法(可以自己进入count.incrementAndGet()源码看看),在加之前就记录了count的原来的值,比如当线程进入时记录count是10,然后将其加到11准备写入时,发现count已经变成11了,于是会将这次修改写入作废,重复上述过程,重新加一次。

代码如下:

public class MyRunnable implements Runnable{
    //整数修改的乐观锁:用java的原子类实现的
    private AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
       //100次
        for (int i = 0; i < 100; i++) {
        	System.out.println("count--------->"+(count.incrementAndGet()));
        }
    }
}

执行结果可以发现没有线程安全问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不愿意做鱼的小鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值