Java基础之多线程

一、多线程的创建

1.1 方式一 继承Thread类

在这里插入图片描述
在这里插入图片描述

public class Create {
    //通过继承Thread类
    public static void main(String[] args) {
        //3.new一个新的线程对象
        Thread t1 = new MyThread();//多态
        //4.调用start方法启动线程(最终执行的还是run方法) 思考:为什么不直接调用run方法?
        t1.start();//主线程从进入main方法时就启动了 执行到这里 主线程把t1线程启动了

        //5.主线程(main)里执行的任务
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程执行中" + i);
        }

    }
}

//1.定义一个线程类(这只是一个类 并不是线程对象) 继承Thread类
class MyThread extends Thread {

    //2.重写Thread的run方法 定义这个线程以后要干啥
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程执行中" + i);
        }
    }
}

为什么不直接调用run方法?
在这里插入图片描述

  • 假如直接调用run方法 主线程执行到run的时候 就把它当成一个类的一个方法 不会开启一个新的线程 依然就是主线程往下执行 导致永远都是先跑完run方法 再继续执行主线程
  • 而start方法 就告诉了操作系统 执行到我这 请CPU把它当做一个单独的执行流程 以线程的方式来启动run方法

1.2 方式二 实现Runnable接口

  • 这种实现接口的方式 将来就可以继承其他类 优化了方式一单继承的确定

在这里插入图片描述
在这里插入图片描述

public class Create02 {
    public static void main(String[] args) {
        //3.实例化一个[任务对象]
        Runnable myRunnable = new MyRunnable();//多态

        //4.把一个[任务对象]交给一个[线程对象]来处理
        //调用Thread类的构造器 Thread(Runnable target)
        //new Thread(myRunnable).start();
        Thread t = new Thread(myRunnable);
        t.start();

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


//1.定义一个线程[任务类] 实现Runnable接口
class MyRunnable implements Runnable {

    //2.重写run方法 编写线程要完成的任务
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程执行中" + i);
        }
    }
}

在这里插入图片描述

1.3 方式二的化简(匿名内部类)

public class test01 {
    public static void main(String[] args) {
        //实例化一个[任务对象]
        Runnable myRunnable = new Runnable() {
            //直接new Runnable的匿名对象
            //右边这一坨 就相当于Runnable的一个[实现类对象]
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程执行中" + i);
                }
            }
        };

        Thread t = new Thread(myRunnable);
        t.start();

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

化简一下:

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

        Thread t = new Thread(new Runnable() {
            //new Thread的构造器 传进来一坨Runnable的匿名对象
            //然后这一整托就创建了一个线程对象
            //这个线程对象 可以返回给某个变量  也可以直接作为一个对象来调用方法
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程执行中" + i);
                }
            }
        });

        t.start();

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

进一步化简(匿名对象):

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

       new Thread(new Runnable() {
            //这个线程对象 也可以直接作为一个对象来调用方法
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("子线程执行中" + i);
                }
            }
        }).start();

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

Lambda表达式继续化简

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

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

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

1.4 实现Callable接口(JDK5新增)

  • 为了优化前两种方式 重写的run方法不能返回结果的问题(有些业务场景 可能需要返回线程执行结果)
  • 任务的执行结果 就是call方法执行完的结果
    在这里插入图片描述

主要理解:

  • Callable任务对象交给FutureTask对象 FutureTask对象(属于Runnable接口家族)就可以交给Thread线程对象了
  • FutureTask的特有方法get会等待子线程任务跑完 再去读取它的返回结果
  • 在这里插入图片描述
  • 在这里插入图片描述
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class test02 {
    public static void main(String[] args) {
        //Callable任务对象交给FutureTask对象 FutureTask对象(属于Runnable接口家族)就可以交给Thread线程对象了

        //3.创建Callable[任务对象]callable
        Callable<String> callable = new MyCall(200);

        //可以把callable这个任务 像之前一样直接给线程对象吗?-不可以 所以还需要一层封装-FutureTask
        //Thread t = new Thread(call);//err [Thread构造器只能接Runnable类型的]

        //4.把callable任务对象 交给FutureTask类的对象(FT的构造器能接Callable类型)
        // FutureTask类 [实现]了RunnableFuture接口 RunnableFuture接口又[继承]了Runnable接口
        // 所以FutureTask的对象 本质上是Runnable的一个对象 而Runnable就可以交给Thread了
        // FutureTask的对象通过它的get方法 得到线程的执行结果
        // get方法能追踪call方法 [只有等call方法跑完了(线程执行完毕了)] 才会去拿执行结果
        // 不能用callable对象直接调用call方法 因为这样去调call方法的时候 并不能保证call方法已经跑完了
        FutureTask<String> futureTask = new FutureTask<>(callable);
        //Runnable ft = new FutureTask<>(callable);//多态 但是get是FT的[特有方法] 所以不能这么写

        //5.把futureTask交给Thread线程处理
        Thread t1 = new Thread(futureTask);
        t1.start();

        try {
            //get方法拿到线程执行结果 最终找的就是call方法的返回结果
            //当主线程运行到这 就要去拿子线程的结果了 这个时候call方法可能都没跑完
            //但是别担心 [如果没执行完 这里的代码就会等待 直到线程跑完 才拿结果]
            System.out.println(futureTask.get());
            //所以这里不能直接调call() 它不会监测子线程任务有没有跑完
//            System.out.println(callable.call());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

//1.定义一个任务类 实现Callable[泛型]接口
// 因为Callable里面的任务要返回结果 泛型规定返回结果的类型
class MyCall implements Callable<String> {
    private int n;

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

    //2.重写call方法 线程要完成什么任务
    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return "子线程执行结果是" + sum;
    }
}

1.5 小节

在这里插入图片描述

二、Thread常用API

2.1 获取当前线程对象

在这里插入图片描述

2.2 获取/设置线程名称

在这里插入图片描述

在这里插入图片描述

2.3 Thread的构造器

在这里插入图片描述
在这里插入图片描述

2.4 Thread类的线程休眠方法

在这里插入图片描述
在这里插入图片描述

2.5 小节

  • 我们调start启动线程之后 run方法是线程自己去调用的
    在这里插入图片描述

三、线程安全问题

3.1 问题出现场景

为什么会出现问题?
在这里插入图片描述

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改资源的操作(读资源是不会出现线程安全问题的)
    在这里插入图片描述

3.2 面向对象模拟出线程安全问题

在这里插入图片描述

在这里插入图片描述


下面是不断执行程序 可能出现的结果
这说明小明和小红两个线程同时去取钱
最后结果到底是谁先去取钱 谁的取钱动作更快 有没有在下一个人来取钱之后把余额更新这些都是不确定的
很容易引发线程安全问题!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、线程同步

4.1 如何保证线程安全

在这里插入图片描述
上面的取钱之所以存在安全问题 就是因为多个线程去取钱发现钱都是够的
虽然ABCD很多人可以同时访问同一个账户去取钱 但是在取钱那一刻 开始排队 就没有问题了

4.2 线程同步的核心思想-加锁

怎么保证线程先后依次访问共享资源?(唯一的锁对象)
在这里插入图片描述
在这里插入图片描述

4.3 线程同步方式一:同步代码块

在这里插入图片描述

在这里插入图片描述

  • 小明小红来取钱 可能有先后 那么就让先来的人解锁进去取钱 然后更新余额
    等先来的人走了 就解锁 第二个人进来发现余额不足
  • 就算是他俩真的在时间维度上都是同一时刻来的 也存在锁竞争的算法 一定也只会让一个人进去取钱
    在这里插入图片描述

4.4 锁对象规范

思考:上面随意地用"唯一"这种字符串作为锁 合不合理?
虽然理论上是可以 但是会影响其他无关线程的执行
假如有另外一家人 小黑小白 来取钱
如果我用的是"唯一"这种锁
那么小黑小白在小明取钱的时候 也被锁在外面等
这显然不合理
应该一家人用一家人的锁才对

  • 建议使用共享资源作为锁对象
  • 对于实例方法 建议用this作为锁对象

在这里插入图片描述
在这里插入图片描述

  • 对于静态方法 建议用字节码对象(类名.class)作为锁对象
    在这里插入图片描述

4.5 线程同步方式二:同步方法

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

  • 同步代码块锁的范围更小 性能会好一些
    同步代码块相当于把厕所的坑锁起来了 多个线程可以都同时跑到厕所里面去

  • 同步方法锁的范围更大 性能相对较差(但是可读性好 所以用的也多)
    它相当于把整个厕所锁起来了 一起跑到厕所门口等 像之前的取钱案例 同步代码块还能一起先拿到name 同步方法只能在整个方法外面等着

4.7 线程同步方式三:Lock锁(JDK5)

在这里插入图片描述

小明 小红两个线程(用户) 对同一个账户来操作 拿的锁对象也是同一把
(一个账户对应一把锁)
在这里插入图片描述

在这里插入图片描述

五、线程池

5.1 线程池的作用

  • 线程池就是一个可复用线程的技术 可以提高性能
  • 工作线程也叫核心线程
  • ExecutorService接口代表了线程池
  • 任务队列里的任务 是通过任务接口Runnable和Callable创建的
  • 不使用线程池的话 每次用户提交任务都新建线程 对于内存和CPU的开销都很大(java中 线程就是对象 对象存在内存)
    在这里插入图片描述

5.2 ThreadPoolExecutor创建线程池

  • 通过ExecutorService接口的实现类ThreadPoolExecutor自创一个线程池对象
  • 使用Executors(线程池的工具类)调用API 返回不同特点的线程池对象

在这里插入图片描述
参数三指定的是临时线程的存活时间 也就是除去核心线程(长期工) 剩下的那些(临时工)

  • corePoolSize 核心线程数有新任务提交时会进行下面的判断
    1.如果当前线程数(正在工作的线程)小于corePoolSize,则新建(核心)线程处理任务,即使此时有空闲线程也不使用
    2.如果当前线程数(正在工作的线程)大于corePoolSize且小于maximumPoolSize(说明还能创建临时线程)如果workQueue未满,则加入workQueue,否则新建(临时)线程处理此任务
    3.如果corePoolSize 和 maximumPoolSize相同,当workQueue未满时放入workQueue,否则按照拒绝策略处理新任务
    4.如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。

  • workQueue
    假设10个最大线程数的线程都在忙 第11个任务提交的时候 如果任务队列没满 就进去这个任务队列缓存起来

  • keepAliveTime
    临时线程空闲时间等待超过这个keepAliveTime时间才会被回收

  • handler 拒绝策略,分为下面几种
    1.AbortPolicy,直接抛出异常,默认的拒绝策略;
    2.CallerRunsPolicy,使用调用者所在的线程执行任务(绕过线程池 主线程亲自来服务);
    3.DiscardOldestPolicy,丢弃队列中最前面的任务(等待最久的任务),并将当前任务加入到队列中;
    4.DiscardPolicy,直接丢弃任务

本段参数说明原文链接

5.3 临时线程什么时候创建

核心线程都在忙;临时线程还能创建;任务队列也满了
这个时候提交新任务 就会创建临时线程(看5.2的核心线程第二点判断)
为什么要这么做?
这样其实尽量少创建了线程
因为核心线程都在忙 任务队列也没排满
核心线程可能马上就忙完了 就可以处理其他任务了
只有等任务队列都满了 还有新任务提交 这种极端情况 才要临时线程登场帮忙
如果核心线程都在忙 新任务来了直接就创建临时线程 资源利用率就低了
在这里插入图片描述

5.4 什么时候执行任务拒绝策略

如果运行的线程数量大于等于maximumPoolSize
这时如果workQueue已经满了
则通过handler所指定的策略来处理任务
在这里插入图片描述

5.5 线程池处理流程

在这里插入图片描述

5.6 线程池处理Runnable任务

import java.util.concurrent.*;
public class test05 {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(
                //创建线程池对象
                //三个核心线程池 最大线程数5 任务队列5  也就是最多处理5+5个任务
                3,5,6,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(5),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        Runnable task = new MyThread001();
        //到这里 每次都让一个核心线程来工作
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);

        //当我加上sleep 上面三个还没跑完 到这里 这里的任务只会等待(如果任务不超过任务队列能接纳的5个)
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);
        pool.execute(task);

        //再来一个 超过五个了 就要派出临时线程了(并不一定只创建一个)
        //最对5-3=2个临时线程
        pool.execute(task);
        pool.execute(task);

        // 核心线程都在忙
        // 临时线程创建完了 也都在满
        // 任务队列也满了
        // 这个时候要执行拒绝策略了
        pool.execute(task);
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.7 线程池处理Callable任务

  • 使用ExecutorService接口提供的submit方法
  • 同样这里的get方法会等任务跑完再取出结果
    在这里插入图片描述

在这里插入图片描述

5.8 Executors工具类创建线程池

在这里插入图片描述

import java.util.concurrent.*;

public class test05 {
    public static void main(String[] args) throws Exception{

        //创建固定线程数量的线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(3);

        pool.execute(new MyThread001());
        pool.execute(new MyThread001());
        pool.execute(new MyThread001());

        // 这个任务会一直被阻塞 除非run里面没有sleep
        // 那么三个核心线程应该能对付>3个任务
        pool.execute(new MyThread001());
    }
}

在这里插入图片描述
在这里插入图片描述

5.9 Executors存在的问题

在这里插入图片描述
在这里插入图片描述

六、定时器

6.1 Timer实现

  • 定时器是一种控制任务延时调用 周期调用的技术
  • TimerTask实现了Runnable接口
  • 他最大的问题是单线程执行
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 在这里插入图片描述
import java.util.Timer;
import java.util.TimerTask;

public class Timer_ {
    public static void main(String[] args) {
        Timer timer = new Timer();//定时器本身就是一个单线程
        //TimerTask task, long delay, long period
        //这一坨代表一个匿名对象(千万不能看成[抽象类]TimerTask的实现类 但可以看成TimerTask的子类的对象)
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"执行了一个任务");
            }
        },3000,2000);//3s后开始执行 每过2s执行一次
    }
}

Timer存在的问题:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

6.2 ScheduleExecutorService实现

  • 在这里插入图片描述
  • 在这里插入图片描述
  • 在这里插入图片描述
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Timer_ {
    public static void main(String[] args) {
        //1.创建ScheduledExecutorService线程池 做为定时器
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);

        //2.开启定时任务1
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
//                System.out.println(10/0); //这个任务就算挂了 也不影响任务2
                try {
                    System.out.println(Thread.currentThread().getName() + "在执行任务" + new Date());
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2, TimeUnit.SECONDS);//不延迟 2s执行一次

        //3.开启定时任务2 不会收到任务1的影响
        pool.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "在执行任务" + new Date());
            }
        }, 0, 1, TimeUnit.SECONDS);//不延迟 2s执行一次
    }
}

七、并发并行、线程的生命周期

7.1 并发和并行

  • 正在运行的程序(软件 跑起来的java代码)就是一个独立的进程,线程是属于进程
  • 线程其实是CPU并发与并行同时执行
  • 并发就是CPU分时轮询执行线程 但是切换的速度贼快
  • 并行是站在时间维度上 任何一个时刻 CPU最多在处理8个线程(8个取决于CPU有几个逻辑处理器)

假设我CPU的逻辑处理器是8 那么每次最多同时处理(并行)8个线程
但是计算机肯定不止处理8个线程
这时候就会并发执行:CPU一会处理这几个线程 一会又处理那几个线程 轮询提供服务 但是切换的速度极快 所以看上去似乎好多线程在同时执行 这就是并发
也就是说我这个8个逻辑处理器的CPU 在任何一个时刻/瞬间 最多都只会处理8个线程(这就是并行)

7.2 线程的生命周期

在这里插入图片描述

  • 新建状态只有Java(对象)特征 没有线程特征
  • 调用sleep()休眠之后 不会释放锁 小红就算在里面睡一天 小明也得在外面等着
  • 调用wait()进入无限等待状态后 会立即释放锁
  • wait(5000) 如果5s内一直没人唤醒我 我自己就醒了
    在这里插入图片描述
  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值