阿里专家与你分享:你必须了解的Java多线程技术!

本次的分享主要围绕以下两个方面:

Lambda入门

多线程技术

一、Lambda入门

Lambda起源于数学中的λ演算中的一个匿名函数,从它的起源我们可以知道,Lambda本身就是一个匿名函数,是Java8才推出的亮点,体现了函数式编程的思想。现在主流的编程语言都包含了函数式编程的特性,Java8在进化过程中吸收了该特性,作为面向编程对象的补充。

Lambda基本语法如下图所示,Lambda语法较为简单,和普通函数相比,没有返回值以及函数名,它的参数和执行语句之间通过->连接,表示参数将传递到语句中执行。Lambda表达式还有两种简化表达式的方法,当表达式中只有一个执行语句时,可以省略语句的{};如果接口的抽象方法只有一个形参,()可以省略,只需要参数的名称即可。Lambda可以替代特定匿名内部类,Lambda表达式不能单独存在,在使用时必须继承函数式接口。

下图示例中的第一个Lambda表达式,形参列表的数据类型会自动推断,只需要参数名称。

代码示例:小编推荐一个学JAVA的学习裙【 一三三,九三零,六九三】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!

在上图展示的代码中,代码中的匿名内部类继承了Flyable接口,实现了接口中的fly()方法。代码准备了Lambda表达式重新实现了Flyable接口。根据代码中的输出命令,执行结果显示Lambda表达式起到了和匿名内部类相同的作用。代码中,并没有定义Lambda表达式的参数类型,但是我们也可以在Lambda表达式中定义符合要求的类型flyable=(int t)->System.out.println(“I can fly by Lambda”),如果参数类型与接口中方法参数类型不一致flyable=(String t)->System.out.println(“I can fly by Lambda”),编译器就会报错。

假如接口实现了两个方法,匿名内部类可以重写新的方法。但是,Lambda表达式没法做到这一点,编译后,将会提示发现有多个需要重写的抽象方法。因此,Lambda表达式在实现接口时,只允许接口中有一个抽象方法,我们将这样的接口称为函数式接口,Java8中提供了注解@FunctionalInterface检验接口是否为函数式接口,如果不是,注解将会报错。另外,代码尝试使用Lambda表达式替代抽象类的匿名内部类的写法,但会报错,提示必须继承函数式接口。因此,Lambda可以替代特定匿名内部类,简化代码,但是必须继承函数式接口。

二、多线程技术

1.进程与线程

进程是具有一定独立功能的程序,关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位。线程是进程的一个实体,是CPU分配调度的基本单位,代码的执行体。从概念上,我们可以知道进程是程序的一次运行活动,需要系统进行分配和调度的;线程是最终代码的执行体,是CPU分配调度的基本单位。同一个进程中可以包括多个线程,并且线程共享整个进程的资源,一个进程至少包括一个线程。如果在理解概念时很费解,想要充分理解这些概念,我们可以采用反抽象的方法,即联系,我们需要在实际生活中寻找符合概念描述的事物。举例说明:我们经常说安卓手机比较卡,手机上App跑的太多,导致内存不足,那么我们在手机上看到的这些App,就是一个个程序;在手机卡顿时,双击home键,看到有App在后台运行,这是我们看到的这些app就是进程。进程是需要系统分配资源的,资源相当于手机的内存。通过这个例子,我们可以加深对进程和程序概念上的理解。另外,我们也可以通过反抽象的方法理解进程与线程的概念。举例说明:公司运转与员工工作,这里的公司,我们可以对应到程序;进程是程序的运行活动,这里的进程,我们可以理解为公司的正常运转;同时,公司想要正常运转,离不开员工的工作,员工是公司运转不可分割的实体,只有员工才是真正做事的人,因此我们可以将线程类比员工。

小编推荐一个学JAVA的学习裙【 一三三,九三零,六九三】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!

2.线程的生命周期

下图为线程的状态图。所谓的生命周期,指的是线程从出生到死亡过程中,经历的一系列状态。线程通过创建Thread的一个实例new Thread()进入new新建状态;之后调用start()方法进入等待被分配时间片,进入runnable状态;之后,线程获得CPU资源执行任务,进入running状态;当线程执行完毕或被其它线程杀死,线程就进入dead死亡状态;如果由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入blocked堵塞状态,在多种条件下,blocked状态可以恢复成runnable状态,最终在线程重新拿到时间片后,就可以进入running状态重新运行。在running状态下,如果时间片用完了或者线程主动放弃CPU的使用,线程重新回到runnable状态。

时间片指的是CPU的时间片段,CPU将它的可执行时间分成很多片段,每个片段随机分配给处在runnable状态下的线程,这样可以达到并发的效果。假设我有一个单核的CPU,通过分割很多的时间片,每个程序都有机会运行,仍然可以跑很多的程序,宏观上看是并发的,但是由于只有一个CPU,实际上程序还是串行的。

我们可以通过阅读JDK的Thread类注释,创建并使用线程,如下图所示。

按照JDK的注释,下图代码中使用了两种创建线程的方法。由于Runnable是一个函数式接口,因此代码中使用Lambda表达式替代匿名内部类,再将runnable传递给Thread,使用start()启动线程。

上述代码结果如下图所示。在下图代码中,如果我们将t.start();替换成t.run(),打印结果将会变成:

Thread Thread run

Main runnable run.

Main

这说明run()方法并没有真正启动线程,run()方法只是在当前的线程中执行了run中的函数。

3. 线程协作

小编推荐一个学JAVA的学习裙【 一三三,九三零,六九三】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!

并行与协作:线程在并发的过程中更多的是协作关系,就像之前的概念中所提到的,进程是系统资源分配的单位,线程本身并没有多少分配资源,除了维护自己必须的内存开销之外,线程的所有资源都是在进程中。多线程在使用竞争中资源时,存在抢占或者说是共享的关系。

这时,多线程之间该如何协作,是需要我们去解决的。我们通过下面的代码,学会使用关键字synchronized,以及理解临界区,锁的概念。

上图代码模拟售票操作。一共有10张票,三个售票员sellerA,seller,sellerC一起去售票,sell( )方法模拟售票行为。代码启动线程之后,运行结果如下图所示。售票员sellerA在一个时间片内将sell方法中的代码全部跑完,票售空,但是sellerB与sellerC在线程并发时,也售出了第10张票,存在重复售票,这样的操作是不合理的。

为了解决重复售票的问题,我们可以使用Java中提供的同步关键字synchronized修饰sell( )方法,代码如下图所示。使用关键字synchronized修饰后,多线程在访问sell( )方法时,能保证只有一个线程执行这个方法,当前线程执行完sell( )方法后,其他线程才能执行sell( )方法。

执行上述代码后,输出结果如下图所示。从下面结果可以看到,代码解决了重复售票的不合理问题,但是仍然只有sellerA一个在售票。原因在于,通过关键字synchronized修饰sell( )方法后,sellerA在拿到sell( )方法的执行权时,把里面的代码一口气执行完了,也就是将票全部卖出,等sellerA执行完后,sellerB和sellerC再执行sell( )方法时,票数已经为0,自然会出现下图中没有卖出一张票的现象。我们将方法sell( )中的内容叫做临界区,当一个线程进入临界区后,其他线程必须等待该线程执行完临界区内容后,才能进入该临界区。

下图所示的代码改善了上述sellerA一口气卖完所有票的现象。代码在方法体内使用关键字synchronized,括号中的this表示一个对象或者一个类。代码相较于上面的解决方法,将临界区从整个方法缩小到两行代码。也就是说多线程在执行这两行代码时是同步的。

上图代码执行结果如下图所示。从图中我们可以发现,不再是只有sellerA在卖票。并且代码每次执行结果都是不一样的,因为CPU的时间片是随机给出的。上述代码中的try catch方法块使线程睡50ms,延长售票操作的时间,在这段时间内可以执行其他的操作(比如,将该票给某个顾客)。代码改善过后,保证资源不是被独占的,使资源分配均匀。

从上图我们发现,存在无效票,原因在于:假设当前票数为1,A进入临界区售票,而此时B已经进行判断,在临界区外等待了。当A卖完票后,票数为0,但是B还是会进入临界区进行售票操作,因此,出现无效票-1的情况。这说明代码需要进一步改善。改善后的代码如下图所示。代码在临界区内加入判断条件,只有票数大于0时,才会进行售票操作,这是常用的双重检验方法。经过双重检验后,运行代码就不会出现无效售票。

下面介绍另外一种单线程同步的方法。代码如下图所示。代码通过Lock接口定义了一个锁,使用ReentrantLock实现。锁和上面提到的关键字synchronized作用是一样的,都是定义出一个临界区,让线程进入临界区时实现线程同步。代码通过lock.lock( )定义临界区的初始点,使用在try语句块中定义临界区执行内容, finally语句块中采用unlock( )方法进行解锁。在unlock后线程才算真正走出临界区。使用try,finally的原因在于:如果try中抛出异常,如果没有finally中的解锁,线程不会调用unlock方法,永远占用这把锁,导致其他线程无法进入临界区执行代码。在finally中调用unlock( )方法保证无论什么情况下,锁终将被释放。避免死锁。

上图中的代码,如果线程遇到售卖同一张票,锁没有被释放,线程将会等待。改善这种情况的方法是,我们使用10把锁,使得每张票都有一把锁,当线程A售卖某张票时,其他线程可以跳过这张票,无需等待去卖其他未售出的票。或者,使用两把锁,五张票一把锁,这种分段锁的策略进一步提高了并发的效率。

4. 线程池

线程虽然不占用进程中的资源,但在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。并且,如果在一个JVM里创建太多的线程,可能会导致系统由于过度消耗内存导致系统资源不足,为了防止资源不足,应该尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量复用已有对象来进行服务,这就线程池技术产生的原因。如果想要实现线程的复用,我们需要继承线程,在run方法中通过循环不断从外部获取runnable的实现,以此达到线程复用的目的。有了复用后,可以提供线程池,管理线程,线程池可以控制线程的并发度,同时,通过对多个任务重用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。

下面介绍一下线程池的使用。下图代码中展示了ThreadPoolExecutor的构造方法,下面介绍一下方法中包含的参数。

corePoolSize:表示线程池的核心线程数,指线程池中常驻线程的数量,核心线程数会一直在线程池中存活,除非线程池停止使用被资源回收了。

maximumPoolSize:指线程池所能容纳的最大线程数量,当活动线程数到达这个数值后,后续的新任务将会被阻塞。

keepAliveTime:非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true时,keepAliveTime同样会作用于核心线程。

Unit:用于指定keepAliveTime参数的时间单位。

workQueue:表示线程池中的任务队列(阻塞队列),通过线程池的execute方法提交Runnable对象会存储在这个队列中。

threadFactory:表示线程工厂,为线程池提供创建新线程的功能。

RejectExecutionHandler:这个参数表示当ThreadPoolExecutor已经关闭或者已经饱和时(达到了最大线程池大小而且工作队列已经满),提供以下几个策略考虑是否拒绝到达的任务。DiscardPolicy:直接忽略提交的任务

AbortPolicy:忽略提交的任务,在拒绝的同时抛出异常,通知调用者拒绝执行

CallerRunsPolicy:让线程池的使用者所在的线程运行提交的任务调用者

DiscardOlderestPolicy:忽略最早放到队列中的任务

下图代码中自定义了一个线程池。通过线程池的submit( )方法提交runnable的实现,最终通过线程池的shutdown( )方法关闭线程池。

Java包中预置的线程池有以下几种:newSingleThreadExecutor;newFixedThreadPool:newCachedThreadPool: newScheduledThreadPool: 但在阿里巴巴的Java开发中是不建议甚至禁止使用Java预置线程池的。下图中的代码目的是寻找SingleThreadExecutor的bug。

小编推荐一个学JAVA的学习裙【 一三三,九三零,六九三】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!上述代码的运行结果如下图所示。代码利用循环,无限添加runnable的实现,但是由于单一线程的阻塞队列是没有边界的,会导致添加的对象过多,耗尽内存资源。因此阿里巴巴开发手册是明确禁止使用Java预置线程池的。

如果对JAVA微服务、分布式、高并发、高可用、大型互联网架构技术、面试经验交流。小编推荐一个学JAVA的学习裙【 一三三,九三零,六九三】,无论你是大牛还是小白,是想转行还是想入行都可以来了解一起进步一起学习!裙内有开发工具,很多干货和技术资料分享!,这些资料都是从各个技术网站搜集、整理出来的,如果你有好的学习资料可以私聊发我,我会注明出处之后分享给大家。欢迎分享,欢迎评论,欢迎转发!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,783评论 3 53
  •   一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺...
    OmaiMoon阅读 1,630评论 0 12
  • 好像很久没有走出去接触新的人,听新的故事,在得知有这么一个活动时还是买了一张票,去熏陶熏陶。会上有1位开场嘉宾和7...
    佳丽Lavinia阅读 2,026评论 0 0
  • 1. hello world 是编程的入门级。 2. 后台语言是Python相对语法简单些。 3. 引入相对应的模...
    不见长安丶灬阅读 510评论 0 0
  • 不知道为何而来, 不知要去往何处。 白白走了一遭, 无能又委屈, 平静又绝望。
    大橙子树阅读 154评论 0 0