1.多线程的创建

1.1、方式一:继承Thread类

  • Thread类
    • Java是通过java.lang.Thread 类来代表线程
    • 按照面向对象的思想,Thread类应该提供了实现多线程的方式
  • 多线程实现步骤:继承Thread类
    • 定义子类MyThread(自命名)继承线程类Thread类,重写run()方法
    • 创建MyThread类对象
    • 调用线程对象的start()方法启动线程(启动后还是运行了run方法)
public class Main {
    public static void main(String[] args) {
        //3.创建线程对象
        MyThread mt = new MyThread();
        //4.启动线程,调用start()方法(运行的是run方法)
        mt.start();
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:"+i);
        }
    }
}
//1.定义一个线程类
class MyThread extends Thread{
    //2.重写run方法,里面定义线程启动后做什么
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:"+i);
        }
    }
}
  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于扩展

  • 为什么不直接调用run方法,而是调用start启动线程

    • 直接调用run方法会当成普通方法运行,此时相当于还是单线程执行
    • 只有调用start方法才是启动一个新的线程执行
  • 主线程放在子线程之前
    • 主线程会先跑完,还是相当于是单线程的效果

1.2、方式二:实现Runnable接口

  • 实现步骤:实现Runnable接口
    • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
    • 创建MyRunnable任务对象
    • 将MyRunnable任务对象交给Thread处理
    • 调用线程start()方法启动线程
public class Main {
    public static void main(String[] args) {
        //3.创建任务类对象
        //MyAsign ma = new MyAsign();
        Runnable rn = new MyAsign();
        //4.吧任务交给Thread,通过线程类,启动该任务
        new Thread(rn).start();

        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:"+i);
        }
    }
}
//1.定义一个线程任务类,实现Runnable接口
class MyAsign implements Runnable{
    //2.重写run方法,定义线程的执行内容
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:"+i);
        }
    }
}
  • Thread类的构造器

image-20230810162558547.png

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
  • 缺点:编程多一层对象包装,如果线程有执行结果是不可以直接返回的

  • 匿名内部类形式

    • 创建Runnable匿名内部类对象
    • 交给Thread处理
    • 调用线程启动start()
public class Demo {
    public static void main(String[] args) {
        //1.使用匿名内部类的方式,重写Runnable中的方法
//        Runnable rn = new Runnable() {
//            @Override
//            public void run() {
//                for (int i = 0; i < 5; i++) {
//                    System.out.println("子线程:"+i);
//                }
//            }
//        };
        //2.创建Thread类,并吧该匿名内部类重写后交给线程类来处理
        //new Thread(rn).start();
        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接口

  • 前两种线程方式存在问题

    • 重写的run方法不能直接返回结果
    • 不适合需要返回线程执行结果的业务场景
  • 实现步骤:利用Callable、FutureTask接口实现

    • 得到任务对象
      • 定义类实现Callable接口,重写call方法
      • 使用FutureTask吧Callable对象封装为线程对象
    • 交给Thread处理
    • 调用Thread线程方法start()启动线程
    • 线程执行完毕后,通过FutureTask的get方法来获取执行结果
public class Demo {
    public static void main(String[] args) throws Exception{
        //3.创建任务类对象,
        MyCallable mc = new MyCallable(100);
        //4.将Callable任务对象交给FutureTask
        /*
            FutureTask作用1:是Runnable的对象(实现了Runnable接口),可以将它交给Thread线程类
            FutureTask作用2:可以在线程执行完成后,使用get方法获取线程执行的结果
         */
        FutureTask<String> ft = new FutureTask<>(mc);
        //5.交给线程对象处理
        new Thread(ft).start();


        MyCallable mc1 = new MyCallable(200);
        FutureTask<String> ft1 = new FutureTask<>(mc1);
        new Thread(ft1).start();

        System.out.println(ft.get());
        System.out.println(ft1.get());
    }
}
//1.定义任务类,来实现Callable接口
class MyCallable implements Callable<String> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    //2.重写call方法
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum+=i;
        }
        return "子线程的执行结果是:"+sum;
    }
}
  • FutureTask API

image-20230810162710005.png

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强,并且可以的线程执行完毕后去得到执行结果
  • 缺点:编码复杂一些

1.4、方式比较

image-20230810162717629.png

2.Thread的常用方法

  • Thread API

image-20230810162724136.png

  • Thread类获取当前线程对象的API

image-20230810162730005.png

【MyThread】

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

【Main】

public class Main {
    public static void main(String[] args) {
        MyTread t1 = new MyTread();
        t1.setName("No.1");
        t1.start();
        System.out.println(t1.getName());

        MyTread t2 = new MyTread();
        t2.setName("No.2");
        t2.start();
        System.out.println(t2.getName());
        //谁执行它,它就得到哪个线程的名字
        System.out.println(Thread.currentThread().getName());

        for (int i = 0; i < 5; i++) {
            System.out.println("main线程输出:"+i);
        }
    }
}
  • 【注意点】
    • 此方法是Thread的静态方法,可以直接使用类调用
    • 这个方法在哪个线程执行中调用的,就会得到哪个线程对象

【Thread类提供的:yield、join、interrupt、不推荐的方法 stop、守护线程等等,在开发中很少使用。】

  • Thread类的线程休眠方法

image-20230810162738725.png

public class Demo {
    public static void main(String[] args) throws Exception{
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:"+i);
            if(i == 3){
                Thread.sleep(3000);//毫秒
            }
        }
    }
}

3.线程安全

3.1、线程安全问题是什么,发生的原因

  • 线程安全问题
    • 多线程同时操作同一个共享资源的适合可能会出现业务安全问题,称为线程安全问题
  • 取钱案例演示
    • 需求:小明和小红是一对夫妻,持有的共同账户,余额为10元
    • 如果两人同时来取钱,取10万元,会发生什么
  • 步骤
    • 提供一个账户类,创建一个账户对象代表2个人的共享账户
    • 需要定义一个线程类,线程类可以处理账户对象
    • 创建2个线程对象,传入同一个账户对象
    • 启动2个线程,去同时取钱

【Account】

public class Account {
    private String cardID;
    private double money;

    //取钱功能
    public void takeMoney(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+"来取钱,余额不足!");
        }
    }

    public String getCardID() {
        return cardID;
    }

    public void setCardID(String cardID) {
        this.cardID = cardID;
    }

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

    public Account() {
    }
}

【MyThread】

public class MyThread extends Thread{
    private Account acc;
    public MyThread(Account acc,String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        //取钱
        acc.takeMoney(100000);
    }
}

【Demo】

public class Demo {
    public static void main(String[] args) throws Exception{
        Account account = new Account("IQBQ",100000);

        new MyThread(account,"小红").start();
        new MyThread(account,"小明").start();
    }
}

【碰到的结果:两人都取了10万,银行亏了10万】

image-20230810162751520.png

4.线程同步

4.1、同步思想概述

  • 为了解决线程安全问题
    • 取钱案例:多个线程同时执行,都取到了钱
  • 解决

    • 让多个线程实现先后依次访问共享资源
  • 核心思想

    • 加锁:吧共享资源进行上锁,每次只能一个进程进入访问,完毕以后解锁,让其他线程进来执行

4.2、方式一:同步代码块

  • 同步代码块

    • 作用:吧出现线程安全问题的核心代码上锁
    • 原理:每次只能一个线程进入,执行完毕后自动解锁,其他线程才可以进来执行

image-20230810162914692.png

  • 锁对象要求

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

【Demo与MyThread同上】

【Account修改取钱功能】

public void takeMoney(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+"来取钱,余额不足!");
            }
        }
    }
  • 【问】锁对象用任意唯一的对象好用么
    • 不好,会影响其他无关线程的执行
  • 锁对象的规范要求
    • 规范上:建议使用共享资源作为锁对象
    • 对于实力方法建议使用this作为锁对象而不是随便的任意唯一对象
    • 对于静态方法建议使用字节码(类名.class)作为锁对象

4.3、方式二:同步方法

  • 同步方法
    • 作用:吧出现线程安全问题的核心方法上锁
    • 原理:每次只能进入一个线程,执行完毕后解锁,进入其他线程
  • 格式

image-20230810162923303.png

【修改Account取钱方法】

//取钱功能
    public synchronized void takeMoney(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+"来取钱,余额不足!");
        }
    }
  • 同步方法底层原理
    • 同步方法其实底层也有隐式锁对象的,只是锁的范围是整个方法代码
    • 如果方法是实力方法:同步方法默认用this作为锁对象,但是代码要高度面向对象
    • 如果方法是静态方法:同步方法默认会用类名.class作为锁对象

4.4、方式三:Lock锁

  • Lock锁
    • 为了更清晰的表达如何加锁和解锁,JDK5以后提供了一个新的锁对象Lock,更加灵活、方便
    • Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定使用
    • Lock是接口不能直接实例化,这里采用它的是实现类ReentrantLock来构建Lock锁对象

image-20230810162931295.png

  • Lock API

image-20230810162937473.png

【修改Account取钱方法】

//取钱功能
    public void takeMoney(double money){
        //获取谁来取钱
        String name = Thread.currentThread().getName();
        //判断是否足够金额
        //上锁
        rl.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 {
            //解锁
            rl.unlock();
        }
    }

【需要try finally原因:一旦上锁后,没有解锁之前遇到了异常,则锁会一直处于上锁状态,其他线程无法再进入】

5.线程通信【了解】

  • 概论
    • 所谓线程通信就是线程间相互发送数据,线程间共享一个资源即可实现线程通信
  • 线程通信常见形式
    • 通过共享一个数据的方式实现
    • 根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做
  • 线程通信实际应用场景

    • 生产者与消费者模型:生产者线程负责生产数据,消费者线程负责消费生产者生产的数据
    • 要求:生产者线程生产完后唤醒消费者,然后等待自己,消费者消费完该数据后唤醒生产者,然后等待自己
  • 线程通信的前提

    • 线程通信通常是在多个线程操作同一个共享资源的适合需要进行通信,且要保证线程安全
  • Object类和唤醒方法

image-20230810162945611.png

【注意:wati()是解锁并等待自己,而sleep()是不解锁单纯的等待自己】

【Account】

public class Account {
    private String cardID;
    private double money;
    public Account(String cardID, double money) {
        this.cardID = cardID;
        this.money = money;
    }

    public Account() {
    }

    //取钱
    public synchronized void takeMoney(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 {
                //没钱的情况下,叫醒其他线程,自己等待
                this.notifyAll();
                this.wait();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    //存钱
    public synchronized void saveMoney(double money){
        try{
            String name = Thread.currentThread().getName();
            if(this.money == 0){
                this.money += money;
                System.out.println(name+"来存钱,存钱后余额为:"+this.money);
                //存钱后唤醒其他线程,等待自己
                this.notifyAll();
                this.wait();
            }else {
                //已经有钱了,唤醒其他线程,等待自己
                this.notifyAll();
                this.wait();
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public String getCardID() {
        return cardID;
    }

    public void setCardID(String cardID) {
        this.cardID = cardID;
    }
}

【MyThread】-【TakeThread】

public class MyThread extends Thread{
    private Account acc;
    public MyThread(Account acc, String name){
        super(name);
        this.acc = acc;
    }
    @Override
    public void run() {
        while(true){
            acc.takeMoney(100000);
            try {
                Thread.sleep(3000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

【SaveThread】

public class SaveThread extends Thread{
    private Account account;
    public SaveThread(Account account,String name){
        super(name);
        this. account = account;
    }
    @Override
    public void run() {
        //存钱
        while(true){
            account.saveMoney(100000);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

【Demo】

public class Demo {
    public static void main(String[] args) {
        Account children = new Account("QQ",0);
        //创建两个线程,为取钱
        new MyThread(children,"小红").start();
        new MyThread(children,"小明").start();
        //三个线程,为存钱
        new SaveThread(children,"父亲").start();
        new SaveThread(children,"干爹").start();
        new SaveThread(children,"岳父").start();
    }
}

6.线程池【重点】

6.1、线程池概述

  • 什么是线程池
    • 线程池就是一个可以复用线程的技术
  • 不适用线程池的问题
    • 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,会严重影响系统的性能

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

  • 线程池接口

    • ExecutorService
  • 如何得到线程池对象

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

image-20230810162959131.png

  • 使用Executors(线程池工具类)调用方法返回不同特点的线程池对象
  • ThreadPoolExecutor构造器的参数说明

image-20230810163013953.png

  • 线程池常见面试题
    • 临时线程什么时候创建?
      • 新任务提交时发现核心线程都在忙,任务队列也慢了,且还可以创建临时线程,此时会创建临时线程
    • 什么时候开始拒绝任务
      • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

6.3、线程池处理Runnable任务

  • ExecutorService的常用方法

image-20230810163021435.png

  • 新任务拒绝策略

image-20230810163027698.png

【MyRunnable】

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"正在输出,输出:Hello ====>"+i);
        }
        try {
            System.out.println(Thread.currentThread().getName()+"进入休眠状态!!!!");
            Thread.sleep(10000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

【ThreadPool】

public class ThreaPool {
    public static void main(String[] args) {
        //创建线程池
        ExecutorService es = new ThreadPoolExecutor(3,5,6,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //执行线程
        MyRunnable target = new MyRunnable();
        //执行了三个核心线程(核心线程是一直存活的)
        es.execute(target);
        es.execute(target);
        es.execute(target);
        //截至,五个队列任务已经满
        es.execute(target);
        es.execute(target);
        es.execute(target);
        es.execute(target);
        es.execute(target);
        //开始创建临时线程
        es.execute(target);
        es.execute(target);
        //拒绝任务,拒绝策略机制触发
        es.execute(target);

        //es.shutdownNow();//立即关闭线程池,不管任务是否完成(通常不会用到)
        //es.shutdown();//关闭线程池,等待任务完成以后关闭(也不会用到)
    }
}

6.4、线程池处理Callable任务

【MyCallable】

public class MyCallable implements Callable<String> {
    private int n;

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

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"执行:1 - "+n+"和的结果:"+sum;
    }
}

【Demo】

public class Demo {
    public static void main(String[] args) throws Exception{
        //创建线程池
        ExecutorService es = new ThreadPoolExecutor(3,5,6,
                TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
        //执行线程
        Future<String> future1 = es.submit(new MyCallable(100));
        Future<String> future2 = es.submit(new MyCallable(200));
        Future<String> future3 = es.submit(new MyCallable(300));
        Future<String> future4 = es.submit(new MyCallable(400));
        System.out.println(future1.get());
        System.out.println(future2.get());
        System.out.println(future3.get());
        System.out.println(future4.get());
    }
}

6.5、Executors工具类实现线程池【了解】

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

23_6_5.png

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

  • Executors使用可能存在的陷阱
    • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险

image-20230810163047864.png

7.补充知识:定时器

  • 定时器

    • 定时器是一种控制任务延时调用,或者周期调用的技术
    • 作用:闹钟、定时邮件发送
  • 实现方式一:Timer

image-20230810163057574.png

  • Timer定时器的特点和存在的问题
    • Timer是单线程,处理多个任务按照顺序执行,存在延时与设置定时器的时间有出入
    • 可能因为其中的某个任务的异常使Timer线程死掉,会影响后续线程任务的执行
  • 实现方式二:ScheduleExecutorService

    • ScheduleExecutorService 是jdk1,5引入,目的是为了弥补Timer的缺陷,内部为线程池

image-20230810163112661.png

  • 优点:基于线程池,某个任务的执行情况不会影响其他定时任务的执行
public class Demo {
    public static void main(String[] args) {
        ScheduledExecutorService pool = new ScheduledThreadPoolExecutor(3);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行了语句一次:AAA"+new Date());
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },0,2, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行了语句一次:BBB"+new Date());
                System.out.println(10/0);
            }
        },0,2, TimeUnit.SECONDS);

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

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

  • 并发与并行
    • 正在运行的程序(软件)就是一个独立的进程,线程是属于进程的,多个线程其实是并发与并行同时进行的
  • 并发的理解
    • CPU同时处理线程的数量有限
    • CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,着就是并发

image-20230810163121782.png

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

  • 线程的状态
    • 也就是线程从生到死的过程,以及中间经历的各种状态及状态转换
    • 理解线程的状态有利于提升并发编程的理解能力
  • Java线程的状态

image-20230810163129290.png

image-20230810163135465.png

results matching ""

    No results matching ""