day11-多线程

news2024/11/15 0:14:03

一、线程安全问题

线程安全问题出现的原因?
    存在多个线程在同时执行
    同时访问一个共享资源
    存在修改该共享资源
线程安全:
    多个线程同时修改同一个资源

取钱案例 小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元 如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

实现步骤
    1. 创建1个账户类,在类中定义账户金额和取钱方法
    2. 创建1个取钱线程类,在类中调用取钱方法
    3. 在主线程中创建1个账户对象代表二人的共享账户
    4. 在主线程中创建2个取钱线程,分别代表小明和小红,并将共享账户对象传递给2个线程处理
    5. 启动2个线程
public class Demo {
    public static void main(String[] args) {
        //在主线程中创建1个账户对象代表二人的共享账户
        Account account = new Account();
        //创建2个取钱线程
        Person xiaoming = new Person(account);
        xiaoming.setName("小明");//线程名称
        Person xiaohong = new Person(account);
        xiaohong.setName("小红");//线程名称
        //启动2个线程,开始取钱
        xiaoming.start();
        xiaohong.start();
​
    }
​
}
//账户类
class Account{
    //定义账户金额和取钱方法
    private Integer money = 100000;
​
    public void drawMoney(Integer drawMoney){
        //判断余额是否充足
        if(drawMoney > money){
            throw new RuntimeException(Thread.currentThread().getName() + "余额不足");
        }
        //模拟取钱
        System.out.println(Thread.currentThread().getName() +"ATM吐出" + drawMoney);
        //更新余额
        money -= drawMoney;
        System.out.println("余额是" + money);
    }
}
​
//取钱线程类
class Person extends Thread{
    //线程共享的账户对象,不能创建Account对象,要传入Account对象
    private Account account;
    //构造器
    public Person(Account account){
        this.account = account;
    }
​
    //调用取钱方法
    @Override
    public void run() {
        account.drawMoney(10000);
    }
}
​
运行结果:
    小明100000
    小红100000
    小红余额为-100000
    小明余额为0

二、线程同步方案

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

2.1 同步代码块

同步代码块
    把访问共享资源的核心代码给上锁,以此保证线程安全
​
格式
    synchronized(同步锁){
        访问共享资源的核心代码
    }
​
原理
    每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
​
注意
    1、对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug注意
    2、同步锁不建议随便使用一个唯一的对象,也能锁住,但可能影响无关线程, 建议使用共享资源作为锁对象
        对于实例方法建议使用this作为锁对象规范
        对于静态方法建议使用字码(类名.class) 对象作为锁对象
public class Account {
​
    //账户余额
    private Integer balance = 100000;
​
    //取钱
    public void drawMoney(Integer money) {
        //0.获取线程名称
        String threadName = Thread.currentThread().getName();
​
        synchronized (this){//排他互斥锁
      //this: 当前对象, 当前对象就是锁对象,这里是共享资源,即账户余额
            //1. 判断余额是否充足
            if (money > balance) {
                System.out.println(threadName + "取钱失败,余额不足");
                return;//方法结束
            }
​
            //2. 如果够,出钱
            System.out.println(Thread.currentThread().getName() + "取钱成功");
​
            //3. 更新余额
            balance -= money;
            System.out.println(Thread.currentThread().getName() + "取钱之后余额为:" + balance);
        }
​
    }
}
​
//取钱人
public class Person extends Thread {
​
    //账户
    private Account account;
    public Person(Account account) {
        this.account = account;
    }
​
    @Override
    public void run() {
        //调用取钱的方法
        account.drawMoney(100000);
    }
}
​
/*
测试类
*/
public class Demo {
    public static void main(String[] args) {
        //1. 创建一个账户对象
        Account account = new Account();
​
        //2. 创建两个取钱的人,并把账户交给它
        Person person1 = new Person(account);
        person1.setName("小明");
        Person person2 = new Person(account);
        person2.setName("小红");
​
        //3. 启动2个线程
        person1.start();
        person2.start();
    }
}

2.2 同步方法

同步方法
    把访问共享资源的核心方法给上锁,以此保证线程安全。
​
格式
    修饰符 synchronized 返回值类型 方法名称(形参列表) {
       操作共享资源的代码
    }
​
原理
    每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
    同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
    如果方法是实例方法:同步方法默认用this作为的锁对象。
    如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
public class Account {
​
    //账户余额
    private Integer balance = 100000;
​
    public Integer getBalance() {
        return balance;
    }
    //充值
    public synchronized void setBalance(Integer balance) {
        this.balance = this.balance + balance;
        System.out.println(Thread.currentThread().getName() + "充值之后余额为:" + balance);
    }
    //取钱
    public synchronized void drawMoney(Integer money) {
        //0.获取线程名称
        String threadName = Thread.currentThread().getName();
​
        //1. 判断余额是否充足
        if (money > balance) {
            System.out.println(threadName + "当前余额为:" + balance);
            System.out.println(threadName + "取钱失败,余额不足");
            return;//方法结束
        }
​
        //2. 如果够,出钱
        System.out.println(Thread.currentThread().getName() + "取钱成功");
​
        //3. 更新余额
        balance -= money;
        System.out.println(Thread.currentThread().getName() + "取钱之后余额为:" + balance);
    }
}
​
//取钱人
public class Person extends Thread {
​
    //账户
    private Account account;
    public Person(Account account) {
        this.account = account;
    }
​
    @Override
    public void run() {
        //调用取钱的方法
        account.drawMoney(100000);
        //调用取钱的方法
        account.setBalance(2000);
    }
}
​
/*
测试类
*/
public class Demo {
​
    public static void main(String[] args) {
        //1. 创建一个账户对象
        Account account = new Account();
​
        //2. 创建两个取钱的人,并把账户交给它
        Person person1 = new Person(account);
        Person person2 = new Person(account);
​
        //3. 启动2个线程
        person1.start();
        person2.start();
​
    }
}

是同步代码块好还是同步方法好一点?

范围上:同步代码块锁的范围更小,同步方法锁的范围更大。

可读性:同步方法更好。

2.3 Lock锁

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

方法
    public ReentrantLock() 创建锁对象
    public void lock()    上锁
    public void unlock() 释放锁

Lock锁使用规范
    规范1、锁对象创建在成员位置,使用final修饰
    规范2、释放锁的代码写在finally块中
构造器说明
public ReentrantLock()获得Lock锁的实现类对象
Lock常用方法名称说明
void lock()获得锁
void unlock()释放锁
public class Account {

    //账户余额
    private Integer balance = 100000;

    //创建锁对象
   private Lock lock =  new ReentrantLock();
    //取钱
    public void drawMoney(Integer money) {

        //0.获取线程名称
        String threadName = Thread.currentThread().getName();

        //1.上锁
        lock.lock();
        try {
            //2.判断余额是否充足
            //2.1 判断余额是否充足
            if (money > balance) {
                System.out.println(threadName + "取钱失败,余额不足");
                return;//方法结束
            }

            //2.2 如果够,出钱
            System.out.println(Thread.currentThread().getName() + "取钱成功");

            //2.3 更新余额
            balance -= money;
            System.out.println(Thread.currentThread().getName() + "取钱之后余额为:" + balance);
        } catch (Exception e){
            e.printStackTrace();
        }finally {
            //3.释放锁
            lock.unlock();
        }

    }
}

//取钱人
public class Person extends Thread {

    //账户
    private Account account;
    public Person(Account account) {
        this.account = account;
    }

    @Override
    public void run() {
        //调用取钱的方法
        account.drawMoney(100000);
    }
}

/*
测试类
*/
public class Demo {

    public static void main(String[] args) {
        //1. 创建一个账户对象
        Account account = new Account();

        //2. 创建两个取钱的人,并把账户交给它
        Person person1 = new Person(account);
        Person person2 = new Person(account);
            // 设置线程的名称
        person1.setName("张三");
        person2.setName("李四");

        //3. 启动2个线程
        person1.start();
        person2.start();

    }
}

三种线程同步方式的对比

同步代码块同步方法lock
语法synchronized (this){ }synchronized 方法(){ }lock.lock(); lock.unlock();
加锁方式自动加锁、释放锁自动加锁、释放锁手动加锁、释放锁
锁粒度代码行方法代码行

三、线程池

3.1 认识线程池

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

它就像一个大的池子一样,里面可以放置一些线程,当需要的时候,就从里面取出来用,用完了就还回去

如此一来,就不必频繁的创建和销毁线程了,大大的提高了线程的利用率,提供系统的性能

3.2 线程池的执行流程

线程池创建后,内部没有线程,当第一个任务提交后,线程工程就创建线程,

1.判断核心线程是否已满,如果未满,则创建一个新的核心线程来执行任务

2.如果核心线程满了,则判断工作队列是否已满,如果没满,则将任务存储到这个工作队列 3.如果工作队列满了,则判断最大线程数是否已满,如果没满,则创建临时线程执行任务

4.如果最大线程已满,则执行拒绝策略 

3.3 创建线程池

JDK5.0起提供了代表线程池的接口:ExecutorService
	ExecutorService接口---ThreadPoolExecutor实现类

 

任务缓冲队列

队列详解
ArrayBlockingQueue基于数组的有界缓存等待队列,可以指定缓存队列的大小
LinkedBlockingQueue基于链表的无界阻塞队列,此时最大线程数无效

任务拒绝策略

策略详解
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run()方法从而绕过线程池直接执行

3.4 线程池处理Runnable任务

线程池如何处理Runnable任务:
	使用ExecutorService的方法:
		void  execute(Runnable target)

public class Demo2 {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                3,// 核心线程数量
                5,// 最大线程数量
                10,// 临时线程的存活时间
                TimeUnit.SECONDS,// 存活时间单位
                new ArrayBlockingQueue(5),// 等待队列
                Executors.defaultThreadFactory(),// 线程工厂
                new ThreadPoolExecutor.AbortPolicy()
        );
        System.out.println(threadPoolExecutor);
        //提交多次任务
        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    System.out.println("执行任务");
                }
            });
        }
        System.out.println(threadPoolExecutor);

    }
}

3.5 线程池处理Callable任务

线程池如何处理Callable任务,并得到任务执行完后返回的结果?
		使用ExecutorService的方法:
			Future<T> submit(Callable<T> command)

public class Demo3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                3,// 核心线程数量
                5,// 最大线程数量
                10,// 临时线程的存活时间
                TimeUnit.SECONDS,// 存活时间单位
                new ArrayBlockingQueue(5),// 等待队列
                Executors.defaultThreadFactory(),// 线程工厂
                new ThreadPoolExecutor.AbortPolicy()
        );
        System.out.println(poolExecutor);
        //执行自己的任务
        SumTask sumTask1 = new SumTask(5);
        Future<Integer> submit1 = poolExecutor.submit(sumTask1);// 返回未来任务对象,用于获取线程返回的结果
        Integer sum1 = submit1.get();// 获取线程返回的结果
        System.out.println(sum1);

        SumTask sumTask2 = new SumTask(10);
        Future<Integer> submit2 = poolExecutor.submit(sumTask2);
        Integer sum2 = submit2.get();
        System.out.println(sum2);

        //关闭线程池
        List<Runnable> runnables = poolExecutor.shutdownNow();// 立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务
        System.out.println(runnables);// 返回未执行的任务
    }
}

//需求: 编写一个任务类, 可以通过构造器接收n, 计算并返回1~n的和
class SumTask implements Callable<Integer> {
    private int n;
    public SumTask(int n) {// 有参构造
        this.n = n;
    }

    /**
     * 计算1-n的和
     * @return
     * @throws Exception
     */
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}

3.6 Executors工具类实现线程池

 

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

线程池ExecutorService的实现类:ThreadPoolExecutor

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

不合适。Executors指定了线程的参数,不能自己设置,而且设置的上限很大,可能会导致OOM。

建议使用ThreadPoolExecutor来指定线程池参数,这样可以明确线程池的运行规则

四、线程通信(了解)

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

Object类的等待和唤醒方法(这些方法应该使用当前同步锁对象进行调用)

方法名称说明
void wait()让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或 notifyAll()方法
void notify()唤醒正在等待的单个线程
void notifyAll()唤醒正在等待的所有线程

4.1 进程与线程

进程:正在运行的程序(软件)就是一个独立的进程
线程:线程是属于进程的,一个进程中可以同时运行很多个线程
关系:进程=火车     线程=车厢

4.2 并发与并行

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

4.3 线程的生命周期和状态

public class Thread{
     ...     
     public enum State {    	
     	NEW, 线程刚被创建,但是并未启动
		RUNNABLE, 线程已经调用了start(),等待CPU调度    	
		BLOCKED, 线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态   				WAITING, 一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法		才能够唤醒
		TIMED_WAITING, 同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态
        TERMINATED; 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡
     }
     ...
}

 

新建一个线程,正常进入就绪态,获取CPU时间片就会被执行,执行完就会结束, 而在执行期间若获取锁失败就会进入阻塞态,重新获取锁成功进入就绪态, 执行期间如调用wait进入等待状态,被notify唤醒进入就绪态, 执行器若调用sleep进入计时等待,时间到进入等待状态。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2129551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

速看!6款可以写论文的ai写作网站,这才是真正的论文神器!(含教程)

在当今信息爆炸的时代&#xff0c;AI写作工具的出现极大地提高了写作效率和质量。特别是对于需要撰写论文的学生和研究人员来说&#xff0c;这些工具提供了极大的便利。本文将重点介绍一款备受推荐的AI写作平台——千笔-AIPassPaper&#xff0c;并结合相关教程帮助用户更好地使…

【北京迅为】《STM32MP157开发板使用手册》- 第二十四章 STM32CubeIDE的初步使用

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

校园水电费管理微信小程序的设计与实现+ssm(lw+演示+源码+运行)

校园水电费管理小程序 摘 要 随着社会的发展&#xff0c;社会的方方面面都在利用信息化时代的优势。互联网的优势和普及使得各种系统的开发成为必需。 本文以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;它主要是采用java语言技术和mysql数据库来…

基于SSM的学生信息管理系统(选课管理系统)的设计与实现 (含源码+sql+视频导入教程)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSM的学生信息管理系统&#xff08;选课管理系统&#xff09;13拥有三种角色 管理员&#xff1a;学生管理、教师管理、专业管理、课程管理、审批管理、课程表管理、开课管理、教室管…

高德地图JS API加载行政区边界AMap.Polygon

&#x1f916; 作者简介&#xff1a;水煮白菜王 &#xff0c;一位资深前端劝退师 &#x1f47b; &#x1f440; 文章专栏&#xff1a; 高德AMap专栏 &#xff0c;记录一下平时在博客写作中&#xff0c;总结出的一些开发技巧✍。 感谢支持&#x1f495;&#x1f495;&#x1f49…

大模型LLM之SpringAI:Web+AI(二)

2.2.2、ChatModel API(聊天模型API) 聊天模型太多了,这里只写OpenAI和Ollama ChatModel和ChatClient区别:ChatClient针对的是所有模型,共用一个客户端。而ChatModel是针对各个模型实现的。 (1)OpenAI 自动配置 <dependency><groupId>org.springframework…

vue3 内置组件 <Suspense>

官方文档&#xff1a; <Suspense> 指南-Suspense 官方提示&#xff1a; <Suspense> 是一项实验性功能。它不一定会最终成为稳定功能&#xff0c;并且在稳定之前相关 API 也可能会发生变化。 <Suspense>是一个内置组件&#xff0c;用来在组件树中协调对异步依…

git删除本地分支报错:error: the branch ‘xxx‘ is not fully merged

git删除本地分支报错&#xff1a;error: the branch xxx is not fully merged error: the branch xxx is not fully merged 直接&#xff1a; git branch -D xxx 就可以。 如果删除远程分支&#xff1a; git push origin --delete origin/xxx git强制删除本地分支 git branc…

如何将Git本地代码推送到Gitee云端仓库

如何将Git本地代码推送到Gitee云端仓库 在使用Git进行版本控制时&#xff0c;将本地代码推送到远程仓库是一个基本且重要的操作。本文将详细介绍如何将你的Git本地代码推送到Gitee&#xff08;码云&#xff09;云端仓库。Gitee是一个国内非常流行的代码托管平台&#xff0c;类…

NX—UI界面生成的文件在VS上的设置

UI界面保存生成的三个文件 打开VS创建项目&#xff0c;删除自动生成的cpp文件&#xff0c;将生成的hpp和cpp文件拷贝到项目的目录下&#xff0c;并且在VS项目中添加现有项目。 修改VS的输出路径&#xff0c;项目右键选择属性&#xff0c;链接器中的常规&#xff0c;文件路径D:…

线性代数 第七讲 二次型_标准型_规范型_坐标变换_合同_正定二次型详细讲解_重难点题型总结

文章目录 1.二次型1.1 二次型、标准型、规范型、正负惯性指数、二次型的秩1.2 坐标变换1.3 合同1.4 正交变换化为标准型 2.二次型的主要定理3.正定二次型与正定矩阵4.重难点题型总结4.1 配方法将二次型化为标准型4.2 正交变换法将二次型化为标准型4.3 规范型确定取值范围问题4.…

《中国制药设备行业市场现状分析与发展前景预测研究报告》

报告导读&#xff1a;本报告从国际制药设备发展、国内制药设备政策环境及发展、研发动态、供需情况、重点生产企业、存在的问题及对策等多方面多角度阐述了制药设备市场的发展&#xff0c;并在此基础上对制药设备的发展前景做出了科学的预测&#xff0c;最后对制药设备投资潜力…

​​操作系统 ---- 进程调度的时机、切换与过程

目录 一、进程调度的时机 1.1 什么时候需要进行进程调度与切换&#xff1f; 1.2 什么情况下不能进行进程调度与切换&#xff1f; 二、进程调度的方式 2.1 非抢占方式(Nonpreemptive Mode) 2.2 抢占方式(Preemptive Mode) 三、总结 一、进程调度的时机 进程调度&am…

FreeRTOS内部机制学习04(任务通知和软件定时器)

文章目录 何为任务通知&#xff1f;任务通知使用例子任务通知的优势以及劣势优势劣势 深入源码看看API函数内部干了什么函数的种类函数都做了啥&#xff1f; 软件定时器软件定时器的作用软件定时器内部到底做了什么实现了“闹钟”功能引入守护任务&#xff0c;守护任务做了啥&a…

SprinBoot+Vue网上购物商城的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质…

我们怎么把自动化测试落地到一个项目上呢?

现在的软件测试行业已经不是原先的点点点的功能测试&#xff0c;要想在软件测试这一行中扎根稳住&#xff0c;就需要你会的很多&#xff0c;不局限于功能测试&#xff0c;还要会自动化测试、接口测试、性能测试等。 今天就来说一下自动化测试&#xff0c;首先什么是自动化测试…

简单分享-获取.txt文件内数据 文件内数据逗号分隔 分隔符 C语言

简单分享-获取.txt文件内数据 文件内数据逗号分隔 分隔符 C语言 数据存储到文件中&#xff0c;把文件数据读取到数组&#xff0c;方便数据处理。 # include <stdio.h> # include <stdlib.h> # include <string.h>#define DATANUM 307200 //数组个数 int ma…

Linux之MySQL定时备份

#!/bin/bash #author: zking #MySQL定义备份并发送邮件 #定义变量 DATE$(date %F"_"%H:%M:%S) HOST127.0.0.1 DBdb1 USERNAMEroot PASSWORDun1xR00t MAILdonkeevip.qq.com BACKUP_DIR/data/db_backup SQL_FILE${DB}_sql_$DATE.sql#判断备份目录是否存在 if [ ! -d $B…

Visual Studio提示:无法安装CPpython.Exe.x64

如果你需要在Visual Studio中使用python环境&#xff0c;而且你本身已经有一个python环境&#xff0c;则只需要将你自己的python环境配置到Visual Studio中即可&#xff0c;可以无视如题报错&#xff0c;将不会产生实质性的问题或影响。 解决办法&#xff1a; 工具->获取工…

zabbix“专家坐诊”第255期问答

问题一 Q&#xff1a;大家好&#xff0c;问一下&#xff0c;zabbix做聚合图的时候&#xff0c;可以做到两根线在一个图里- 吗&#xff1f;还是说只能&#xff0c;单边计算聚合&#xff0c;然后再最后作图的时候&#xff0c;添加两条线上去 A&#xff1a;两个数据吗 Q&#xff…