高考考试网
当前位置: 首页 高考资讯

java多线程编程的用处(Java中的多线程如何理解)

时间:2023-08-09 作者: 小编 阅读量: 3 栏目名: 高考资讯

线程同步为了能够解决刚才出现的问题,我们可以考虑使用线程同步,让多个线程实现先后依次访问共享资源。synchronized{//喝水是对于两个线程的不变对象if{System.out.println(thread.getName()"已经成功获得了这瓶水!

目录

线程池处理Runnable任务

线程池处理Callable任务

Executors的工具类构建线程池对象

引言

通过前面的学习,我们已经学会了线程是如何创建的以及线程的常用方法,接下来呢,我们将要深入性了解线程中的知识,主要是线程安全,线程同步,线程池三个知识点。我相信大家通过这节课的简单地学习,就可以大概地掌握了线程吧!好了,废话不多说,我们开始今天的学习吧!

线程安全

首先我们应该了解的是什么是“ 线程安全 ”问题呢?通俗易懂的讲的话,那就是“假设在某地有一瓶水,石原里美和工藤静香都很渴,想要去喝这瓶水,然而当这两个线程同时启动的时候,二人都会去拿这瓶水,并且同时判断这瓶水是否还在?在这瓶水未被取走之前,二人的判断都是true,因此二人都能够取到这瓶水,可是明明只有一瓶水,却可以让两个人都取到水,这很明显是与现实生活中的情况是不符合的。”这就是我们需要处理的“线程安全”问题。

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

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源
实战模拟

问题描述:

仍然是前面的例子,现有两人需要喝水,分别是石原里美和工藤静香,二者共享同一瓶水,若对方喝掉这瓶水,则另一方则没有水可以喝。

具体操作:

1、提供一个Account类并创建它,作为二人的共享水资源账户;

public class Account {private int num;//代表水的数量public Account() {}public Account(int num) {this.num = num;}public int getNum() {return num;}public void setNum(int num) {this.num = num;}}

2、定义一个线程类,并且该线程可以处理Account对象;

public class ThreadAccount extends Thread{private Account account;public ThreadAccount() {}public ThreadAccount(Account account) {this.account = account;}public Account getAccount() {return account;}public void setAccount(Account account) {this.account = account;}@Overridepublic void run() {Thread thread = Thread.currentThread();if(account.getNum()>0){System.out.println(thread.getName() "已经成功获得了这瓶水!");account.setNum(account.getNum()-1);System.out.println("此时还剩下" account.getNum() "瓶水");}else {System.out.println("水资源不够,已经无法取出");}}}

3、创建两个线程,并传入同一个Account对象;

public static void main(String[] args) throws Exception {Account account = new Account(1);ThreadAccount threadAccount1 = new ThreadAccount(account);ThreadAccount threadAccount2 = new ThreadAccount(account);Thread thread1 = new Thread(threadAccount1,"石原里美");Thread thread2 = new Thread(threadAccount1,"工藤静香");thread1.start();thread2.start();}

4、启动两个线程,同时获取同一瓶水。

通过运行结果,我们可以清晰的看出来存在很大的问题,当第一个人石原里美获得这瓶水的时候,水资源账户中已经没有水了,所以工藤静香是不能够取到水资源的,然而工藤静香仍然获得了水,且剩下了-1瓶水,这很明显是不符合现实情况的,那么我们该如何解决呢?

线程同步

为了能够解决刚才出现的问题,我们可以考虑使用线程同步,让多个线程实现先后依次访问共享资源。其核心思想是: 加锁 ,把共享资源进行上锁,每次只能一个线程进入访问完毕以后解锁,然后其他线程才能进来。通俗易懂的讲就是:“假如两个线程同时开始访问共享资源,在访问之前,谁先拿到钥匙打开这把锁,谁才能访问该共享资源。而另一个线程则只能在共享资源外等待钥匙空下来。”接下来介绍几种方式来解决该问题。

方式一:同步代码块

作用: 把出现线程安全问题的核心代码给上锁

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

synchronized (同步锁对象){操作共享资源的代码(核心代码)}

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

synchronized ("喝水") {//喝水是对于两个线程的不变对象if(account.getNum()>0){System.out.println(thread.getName() "已经成功获得了这瓶水!");account.setNum(account.getNum()-1);System.out.println("此时还剩下" account.getNum() "瓶水");}else {System.out.println("水资源不够,已经无法取出");}}

具体的操作就是将之前的run()方法中的核心代码加锁,即可实现多个线程依次访问共享资源

其中需要注意的一点是,同步锁对象等同于打开共享资源的一把钥匙,所以应该对多个线程是同一个对象,并且要规范命名,若同步锁对象命名相同,则会影响另一组线程对共享资源的访问。对于实例方法建议使用this作为锁对象;对于静态方法建议使用字节码(类名.class)对象作为锁对象。

方式二:同步方法

作用: 把出现线程问题的核心方法给上锁。

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

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

与方式一的不同之处在于,前者是封装共享资源的核心代码,后者则是封装整个方法

public synchronized void LockWater(){if(account.getNum()>0){System.out.println(thread.getName() "已经成功获得了这瓶水!");account.setNum(account.getNum()-1);System.out.println("此时还剩下" account.getNum() "瓶水");}else {System.out.println("水资源不够,已经无法取出");}}

如果方法是实例方法:同步方法默认用this作为锁对象,但是代码要高度面向对象;

如果方法是静态方法:同步方法默认用类名.class作为锁对象。

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

方法名称

说明

public ReentrantLock( )

获得Lock锁的实现类对象

void lock( )

获得锁

void unlock( )

释放锁

//在使用之前先定义一个锁对象private Lock lock = new ReentrantLock();//定义过锁对象之后,即可以调用其APIlock.lock();//共享资源代码lock.unlock();

线程池

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

线程池的实现原理:

在创建线程池时,设定该线程池固定存在N个 核心线程 用于处理任务,另外会有一个 任务队列 提供给任务排队等待,在任务队列中的前N个任务则是交给核心线程去处理,在没有空余线程的时候,其余任务则在任务队列中等待。

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象;
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程对象。

ThreadPoolExecutor构造器的参数说明

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)

参数一:指定线程池的线程数量(核心线程): corePoolSize (不能小于0)

参数二:指定线程池可支持的最大线程数: maximumPoolSize (最大数量>=核心线程)

参数三:指定临时线程的最大存活时间: keepAliveTime (不能小于0)

参数四:指定存活时间的单位(秒、分、时、天): unit (时间单位)

参数五:指定任务队列: workQueue (不能为null)

参数六:指定用哪个线程工厂创建线程: threadFactory (不能为null)

参数七:指定线程忙,任务满的时候,新任务来了怎么办: handler (不能为null)

为了更好的去理解多线程:我们可以假设 核心线程 数量为3个, 最大线程 数为5个,那么该线程池可以创建的 临时线程 数为5-3=2个线程,临时线程的最大存活时间是指其被创建之后不处理任务之后的存活时间,时间单位结合实际即可, 任务队列 设置为5个,参数六字面意思理解即可,参数七则是规定线程池不能再接收任务的时候如何处理。接下来通过这两个问题加深理解。

临时线程什么时候创建?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • (此时3个核心线程在处理任务,并且任务队列中已经有5个任务在等待了,然而仍然有任务过来,此时即可开始创建临时线程处理任务)

什么时候会开始拒绝任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝。
  • (当3个核心线程和2个临时线程都在处理任务,且任务队列满的情况下,将会开始拒绝接收任务,由参数七决定该如何处理,由此我们也可以得出该线程池任务处理的最大数量为:3 2 5=10,即核心线程 临时线程 任务队列)
线程池处理Runnable任务

ThreadPoolExecutor创建线程池对象:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());

ExecutorService常用方法

常用方法

说明

void executor(Runnable command)

执行任务/命令,没有返回值。一般用来执行Runnable任务

Future<T>submit(Callable<T> task)

执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务

void shutdown( )

等任务执行完毕后关闭线程池

List<Runnable shutdownNow>

立刻关闭,停止正在执行的任务,并返回队列中未执行的任务

参数七新任务拒绝策略

策略

说明

ThreadPoolExecutor.AbortPolicy

丢弃任务,并抛出RejectExecutionException异常,默认的策略

ThreadPoolExecutor.DiscardPolicy

丢弃任务,但是不抛出异常,不推荐

ThreadPoolExecutor.DiscardOldPolicy

抛弃队列中等待最久的任务,然后把当前任务加到队列中

ThreadPoolExecutor.CallerRunsPolicy

由主线程负责调用任务的run()方法从而绕过线程池直接执行

//main方法public static void main(String[] args) {ThreadPoolExecutor pool = new ThreadPoolExecutor(2,4,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());Runnable target = new MyThreadRunnable();//核心线程 任务队列=5,所以5个以内不需要创建临时线程pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target);//当任务数量达到5个的时候,接收任务的时候就需要创建临时线程pool.execute(target);pool.execute(target);//当超过7个线程时,再次接收任务时,则会拒绝pool.execute(target);}//实现Runnable接口public class MyThreadRunnable implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() "正在处理任务");Thread.sleep(10000000);//避免线程快速处理任务,无法达到实验效果}}//输出结果://pool-1-thread-2正在处理任务//pool-1-thread-4正在处理任务//pool-1-thread-3正在处理任务//pool-1-thread-1正在处理任务

线程池处理Callable任务

Future<T>submit(Callable<T> task)

执行任务,返回未来任务对象获取线程结果,一般拿来执行Callable任务

//main方法public class MoreThread {public static void main(String[] args) throws Exception{ThreadPoolExecutor pool = new ThreadPoolExecutor(2,4,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());Future<String> f1 = pool.submit(new MyThreadCallable());Future<String> f2 = pool.submit(new MyThreadCallable());Future<String> f3 = pool.submit(new MyThreadCallable());Future<String> f4 = pool.submit(new MyThreadCallable());System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}}//线程实现Callable接口public class MyThreadCallable implements Callable<String> {@Overridepublic String call() throws Exception {return Thread.currentThread().getName() "正在工作中~";}}//输出结果:仅由两个核心线程完成即可//pool-1-thread-1正在工作中~//pool-1-thread-2正在工作中~//pool-1-thread-2正在工作中~//pool-1-thread-2正在工作中~

Executors的工具类构建线程池对象

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

方法名称

说明

public static ExecutorsService newCachedThreadPool( )

线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。

public static ExecutorsService newFixedThreadPool(int nThread)

创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。

public static ExecutorsService newSingleThreadExecutor( )

创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。

public static ScheduledExecutorsService newScheduledThreadPool(int corePoolSize)

创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

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

ExecutorService pool1 = Executors.newFixedThreadPool(5);pool.execute(new MyThreadRunnable());pool.execute(new MyThreadRunnable());pool.execute(new MyThreadRunnable());

虽然Executors使用起来会很方便,但是仍然是存在风险的,因此还是推荐前面使用的线程池创建方式。

(1)FixedThreadPool和SingleThreadExecutor:

允许的请求队列长度为Integer.Max_VALUE,可能会堆积大量的请求,从而导致oom。

(2)CachedThreadPool和ScheduledThreadPool:

允许的创建线程数量为Integer.Max.VALUE,可能会创建大量的线程,从而导致oom。

创作不易,给个三连

    推荐阅读
  • 一线家居建材品牌排行榜(家居建材品牌哪家好)

    欧派欧派橱柜,连续11年蝉联中国橱柜行业会长单位,橱柜产业制造基地规模雄霸全球。拥有4200多家门店,产品出口6大洲100多个国家,连续15年销量遥遥领先,品牌第一提及率、品牌首选购买率均居行业第一,是无可争议的中国高端橱柜行业第一品牌。目前,多乐士品牌涵盖乳胶漆,木器漆,专业防腐漆和地坪漆等众多产品,充分满足不同层次的需求。并且东陶“水与电子相结合”为基础的产品系列在同业界被公认为第一产家。

  • 二年级语文期中考试复习要点(最适合家长的复习材料)

    虽然双减下不允许学习公开考试,但是作为家长,还是需要帮助孩子总结下半学期以来的学习情况,其中复习篇栗子共用五篇进行总结。“得意”也是满意的意思,表现了玲玲对自己画的画感到满足的高兴的心情。“伤心”表现了玲玲很痛苦,她很懊恼自己不小心把画弄脏。“满意”表现了玲玲的意愿得到了满足,非常开心的状态。(五)、考点提示1.《曹冲称象》掌握曹冲称象的步骤。

  • 奥地利总理个人资料(奥地利不寻求加入北约)

    奥地利总理个人资料来源:央视新闻客户端奥地利总理内哈默18日在维也纳举行的新闻发布会上表示,奥地利的历史与瑞典和芬兰不同,不会像瑞典和芬兰那样寻求加入北约,奥地利会保持中立地位。芬兰和瑞典一改两国多年来在军事上不结盟的政策,于5月18日正式递交加入北约的申请,北约30个成员国本月5日签署芬兰和瑞典加入北约议定书,向北约正式“北扩”继续迈进。按照程序,北约成员国将各自按程序对议定书予以审批。

  • 夜间车灯的正确使用解析(关于车灯的9个冷知识)

    因为日间行车灯亮度很大,导致一些车主在天黑后忘记打开大灯,从而引发危险,因此法规要求只要配备日间行车灯就必须配备自动大灯,而且只有把灯光选择在AUTO档位后日间行车灯才会正常点亮。

  • 道路上的交通标志是什么呢(新的道路交通标志来了)

    清远日报签约摄影师郑寿松摄1.清远疾控提醒:近期从新疆、西藏、贵州来(返)清人员,请做好居家健康监测!新华社的报道有答案3.清远两条拟新建高速正争取列入国家高速公路网规划4.关乎你我!2023年度清远市十件民生实事开始征集→5.这就是清远!

  • 为什么梦到没去过的地方和人(为何会做乱七八糟的梦)

    为什么会有“预知梦”?有一些人会做“预知梦”,比如梦中梦到自己去了一片沙漠,醒来之后发现自己很口渴,梦到自己去了一片寒冷地带,醒来之后发现自己被子掉了,这其实是某些生理状态的反应,不知道你生活中有没有遇到过这样一种现象。

  • 帝国时代2 重弩战象(帝国时代2毁灭战士)

    “毁灭时代”最早于今年的9月1日发行,不过最近发布的是成熟版本。“毁灭时代”将原始的《毁灭战士》射击体验融入帝国时代2的战役模式。玩家将控制主角Doomguy进入地狱版的世界同怪物厮杀,而非传统帝国的建造城市和发展文明。游戏为等距视角,非毁灭战士的第一人称视角。游戏从毁灭战士1代和3代汲取灵感,MOD一共包含四章,带领玩家穿越火星和它的两颗卫星。

  • 生物链和食物链各是什么意思(生物链和食物链分别解释)

    生物链和食物链各是什么意思?食物链是指消费者和生产者之间吃和被吃的关系,在该生态系统中蛇占有两条食物链是:绿色植物→蝉→螳螂→黄雀→蛇;绿色植物→蝉→黄雀→蛇,其中最长的食物链是:绿色植物→蝉→螳螂→黄雀→蛇。

  • 小芹菜怎么做没有(制作炒芹菜的方法)

    跟着小编一起来看一看吧!小芹菜怎么做没有主料:芹菜300克、五花肉80克。辅料:玉米油半勺、花椒粒适量、生抽半勺、料酒半勺、鸡精适量、盐1克、葱1段、姜1块、五香粉适量。准备五花肉,芹菜。芹菜去叶,清洗干净。葱切段,姜切丝。放入生抽,料酒小火烧至出味。倒入芹菜中火翻炒几下。调入五香粉拌匀。芹菜在变软之前快速出锅即可。

  • 一名合格韭菜需要经历的几个阶段(韭菜管理你不知道的几个秘诀)

    韭菜管理你不知道的几个秘诀,跟着这几个俗语走,一定不会差。但等到第二刀以后,温度就逐渐回升了,这时割韭菜就要稍微浅一点,不要贴着地面割,要离开地面割,以免伤及根茎,如果割得太深,不利于根系发育。以上是流行多年的农村俗语,都有一定的科学道理,所以,在平时管理韭菜的时候,还是要多加注意。