Java多线程

79 阅读16分钟

多线程

1. 多线程概述

1.1 什么是进程?什么是线程?

进程: 正在运行的程序(软件)就是一个独立的进程。(可以看做是现实生活当中的公司)

线程: 是一个程序内部的一条执行流程。(可以看做是公司当中的某个员工)

注意: 线程是属于进程的,一个进程中可以同时运行多个线程

1.2 多线程是什么?

多线程: 在单个程序中同时运行多个线程完成不同的工作。

1.3 并发、并行

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

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

1.4 生命周期

Java线程的状态

  • Java总共定义了6种状态
  • 6种状态都定义在Thread类的内部枚举类中。
NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。

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

image-20240227144730983.png

2. 多线程的创建★

2.1 线程创建方式一 :继承Thread类

实现步骤:

1.定义一个子类 继承Thread类 ,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(线程启动后,会自动执行run方法中的代码)
	注意:直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。

代码演示如下:

MyThread类:

public class MyThread extends Thread{
    private String name;//线程名
    public MyThread(String name) {
        this.name = name;
    }
    
    @Override
    public void run() { //线程任务
       //线程任务,循环10次 
        for (int i = 1; i <= 10; i++) {
            //Thread.currentThread().getName():获取当前线程的名字
            //System.out.println("在"+Thread.currentThread().getName()+" 线程中执行"+i);
            
            System.out.println("线程"+name+"运行了"+(i+1)+"次");
        }
    }
}


Test类:
public class Test {
    public static void main(String[] args) {
         // 当我们点击 运行 的时候 就开启了一个线程 去执行当前main方法里面写的代码 都是由上至下
        System.out.println("主线程"+Thread.currentThread().getName());

        // 在main线程中开启一个新的线程
        MyThread myThread = new MyThread("A");
        myThread.start();
        
        //主线程
        for (int i = 1; i <= 10; i++) {
            System.out.println("在"+Thread.currentThread().getName()+"中正在执行:"+i);
        }
        
    }

}

运行结果截图:

image-20240227095001903.png

2.2 线程创建方式二:实现Runnable接口

实现步骤:

1.创建一个实现类 实现 线程任务接口 Runnable接口 重写run方法
2.创建Runnable接口实现类对象 
3.将线程任务对象教给线程对象 创建Thread类对象的同时 传递线程任务对象
	注意:线程创建方式
			1.普通对象创建
			2.匿名内部类创建
			3.lambda表达式创建
4.调用线程对象的start()方法启动线程

代码实现如下:

MyRunnable类:(线程任务类)

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName()+"执行了"+i+"次");
        }
    }
}

Test类:
public class Test {
    public static void main(String[] args) {
        //创建 Runable接口实现类对象  MyRunnable对象。
        MyRunnable myRunnable = new MyRunnable();
        
        // 将线程任务对象交给线程对象  创建Thread类对象的同时 传递线程任务对象
        Thread thread1 = new Thread(myRunnable);
        //开启线程
        thread1.start();

        
        //内部类方式创建线程
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName() + "执行了" + (i + 1) + "次");
                }
            }
        });
        thread2.start();

        //lambda表达式创建
        Thread thread3 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "执行了" + (i + 1) + "次");
            }
        });
        thread3.start();
    }
}

运行结果截图:(让每个线程都睡眠了100毫秒)

image-20240227102037475.png

2.3 线程创建方式三:实现Callable接口

实现步骤:

1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Thread的start()方法启动线程(启动后会自动执行call方法)
   等call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中
6.调用FutrueTask对的get()方法获取返回结果

代码演示如下:

MyCallable类:(Callable接口的实现类)
    
public class MyCallable implements Callable<Integer> {

    private Integer number;//定义了成员变量

    public MyCallable(Integer number){//怎么把接收到number传递到 下面call方法中
        this.number = number;//传过来的值 给了 成员变量
    }

    // 求一个数的绝对值
    @Override
    public Integer call() throws Exception {

        System.out.println("当前在:"+Thread.currentThread().getName()+"完成绝对值的获取");

        return Math.abs(number); //使用到了成员变量
    }
}


public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
       System.out.println(Thread.currentThread().getName()+"中执行1");

         // 在main线程中开启一个新的线程。
          // 创建 Callable实现类对象
        MyCallable mc = new MyCallable(-10);
        //创建 处理用于接收线程任务返回值类对象 FutureTask  传递线程任务
        FutureTask<Integer> task = new FutureTask<>(mc);
        // 创建线程对象 传入 处理返回值的线程任务
        new Thread(task).start();//线程对象

        // 返回值怎么处理 ?  tack处理返回值
        System.out.println("新线程的返回值是:"+task.get());

        System.out.println("在"+Thread.currentThread().getName()+"中执行2");

    }
}

执行结果截图:

image-20240227103610504.png

2.4三种创建方式的优缺点

优点缺点
继承Thread类方式编码简单存在单继承的局限性,线程类继承Thread后,不能继承其他类,不便于扩展
Runnable接口方式任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。需要多一个Runnable对象。
Callable接口方式线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果编码复杂一点。

3. Thread的常用方法★

image-20240227104942507.png

image-20240227105006858.png

4.线程安全

4.1什么是线程安全问题?

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

4.2线程安全出现的原因?

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

4.3用程序模拟线程安全问题

image-20240227113851342.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(1000);
    }
}

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

运行结果截图:

image-20240227114558113.png

5.线程同步★

5.1认识线程同步

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

5.2 线程同步思想

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题。
  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来

5.3 线程同步的解决方案

5.3.1同步代码块

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

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

写法:
    synchronized(锁对象){
        //...访问共享数据的代码...
    }
    
同步锁的注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

锁对象的使用规范 
	1.建议使用共享资源作为锁对象 
	2.对于实例方法建议使用this作为锁对象 
	3.对于静态方法建议使用字节码(类名.class)对象作为锁对象

代码演示如下:

// 小明 小红线程同时过来的
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 + "来取钱:余额不足~");
        }
    }
}

5.3.2 同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
写法:
    修饰符 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 + "来取钱:余额不足~");
    }
}

5.3.3 Lock锁

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

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

代码演示如下:

// 创建了一个锁对象
private final Lock lk = new ReentrantLock();

public void drawMoney(double money) {
        // 先搞清楚是谁来取钱?
        String name = Thread.currentThread().getName();
        try {
            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(); // 解锁
        }
    }
}

6.线程通信

6.1 什么是线程通信?

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

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

生产者线程负责生产数据

消费者线程负责消费生产者生产的数据

注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

案例: 有3个厨师(生产者线程),两个顾客(消费者线程)。

image-20240227143321067.png 案例分析:

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

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

代码如下:

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(1000);
                //唤醒吃货线程
                this.notifyAll();
                //线程等待
                this.wait();
            }else {

                //线程等待
                this.wait();
            }
        } catch (InterruptedException 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(1500);
                //唤醒厨师
                this.notifyAll();
                //吃货休息
                this.wait();
            }else {
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


public class Test {
    public static void main(String[] args) {
        //开始准备吃包子
        System.out.println("进入庆丰包子铺 坐下来 点餐");

        //创建共享资源
        Desk desk = new Desk();

        new Thread(()->{
            while (true){
                desk.put();
            }
        },"张三大厨").start();

        new Thread(()->{
            while (true){
                desk.put();
            }
        },"李四大厨").start();

        new Thread(()->{
            while (true){
                desk.put();
            }
        },"王五大厨").start();


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


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

    }
}

代码运行结果:

image-20240227143705354.png

7.线程池

7.1什么是线程池?

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

7.2 如何创建线程池?

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

image-20240227113020859.png 代码如下:

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

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

7.2.3线程池的注意事项

  1. 临时线程什么时候创建?
    • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  2. 什么时候会开始拒绝新任务?
    • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

7.3线程池处理Runnable任务

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

image-20240227141456874.png 代码如下:

先准备一个线程任务类
public class CuoZao implements Runnable{

    //搓澡任务
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name+"师傅正在给客人搓澡=====盐搓---奶搓---醋搓--");

        //模拟搓澡时间
        try {
            //5秒搓一个
            Thread.sleep(5000);
        } catch (InterruptedException e) {
           e.printStackTrace();
        }
    }
}

下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。
 public static void main(String[] args) throws InterruptedException {
        //先构建一个,线程池对象
        ExecutorService pool = new ThreadPoolExecutor(
                3,//核心线程数量
                5,//最大线程数量 = 核心线程数量 + 临时线程数量
                8,//临时存活时间
                TimeUnit.SECONDS,//超过核心现场后 如果有线程超过8秒中没有被使用 就销毁掉
                new ArrayBlockingQueue<>(4),//指定任务队列 任务阻塞队列  阻塞长度是4
                Executors.defaultThreadFactory(),//用户创建线程对象的工程对象 固定代码
                new ThreadPoolExecutor.CallerRunsPolicy());//任务拒绝策略 四个 我选取最后 忙不过来 找外援

        //线程池 草堂
        //核心线程数量 老板招聘的 三个搓澡师傅
        //执行搓澡任务
        CuoZao cz = new CuoZao();
        //来一个顾客 搓一个顾客
        //接客
        pool.execute(cz);//核心
        pool.execute(cz);//核心
        pool.execute(cz);//核心
        //3个客人

        pool.execute(cz);//第四个客人 先等待了  核心线程为他服务
        pool.execute(cz);//第五个客人
        pool.execute(cz);//第六个客人
        pool.execute(cz);//第七个客人

        pool.execute(cz);//第八个客人 触发了 招聘临时工  阻塞队列4 一旦超出阻塞队列 就增派人手

        pool.execute(cz);//第九个客人                  阻塞队列4  超出阻塞队列两个 增派两个人手
        // 第九个客人 已经有五个搓澡师傅 已经达到 最大线程数量

        pool.execute(cz);//第十个客人   阻塞队列满了 超出阻塞队列的 用两个人手  但是还少一个
        // 这个时候拒绝策略 -- 增派人手  main来处理。。。

        Thread.sleep(20000);//时间过了十一秒 没有新的任务 肯定有线程没有处理任务的 这种任务就会销毁
        System.out.println("至少空闲了12秒 已经有被销毁的线程了...销毁之后 ");
        pool.execute(cz);
        pool.execute(cz);
        pool.execute(cz);

        pool.execute(cz);
        pool.execute(cz);

        pool.shutdown();//都搓完了 把 澡堂关闭
//        pool.shutdownNow();//里面关闭 没搓完的任务回到队列中
        
    }

代码运行结果截图:

image-20240227141840615.png

7.3.1使用ExecutorService线程池对象的常用方法

void execute(Runnable command)

7.3.2新任务拒绝策略

image-20240227113501263.png

7.4线程池处理Callable任务

callable任务相对于Runnable任务来说,就是多了一个返回值。

执行Callable任务需要用到下面的submit方法

image-20240227142116523.png

先准备一个Callable线程任务  模拟迅雷下载任务

import java.util.concurrent.Callable;

public class Download implements Callable<String> {


    @Override
    public String call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"正在下载.....");
        Thread.sleep(1000);
        return "资源下载完毕";
    }
}

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

public class XunLei {

   public static void main(String[] args) throws Exception{
        //创建一个线程池 表示 迅雷

        ExecutorService pool = new ThreadPoolExecutor(
                2,
                5,
                8,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        // 执行提交 callable任务
        Future<String> f1 = pool.submit(new Download());
        Future<String> f2 = pool.submit(new Download());
        Future<String> f3 = pool.submit(new Download());
        Future<String> f4 = pool.submit(new Download());

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
    }
}

7.5 Executors工具类实现线程池

Java为开发者提供了一个创建线程池的工具类,叫做Executors,它提供了方法可以创建各种不能特点的线程池。如下图所示

image-20240227142508894.png 代码演示:

public class ThreadPoolTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        //创建一个线程池  传递一个参数
        ExecutorService pool = Executors.newFixedThreadPool(3);

            // 执行提交 callable任务
        Future<String> f1 = pool.submit(new Download());
        Future<String> f2 = pool.submit(new Download());
        Future<String> f3 = pool.submit(new Download());
        Future<String> f4 = pool.submit(new Download());

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());

        //可以关闭线程池
//        pool.shutdown();// 在关闭
//       pool.shutdownNow();//不搓了
    }
}