微信号:yunqiinsight

介绍:云栖社区是由阿里云负责运营、阿里巴巴技术协会和阿里巴巴集团各技术团队提供内容支持的开放式技术社区.

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

2018-06-02 22:38 云栖社区

云栖君导读:本文介绍了Lambda表达式的起源以及基本语法,并提供代码实例帮助大家理解Lambda表达式的使用。另外,本文介绍了Java开发中常用的多线程技术,详细介绍多线程涉及到的概念以及使用方法。


演讲嘉宾简介:


吕德庆(花名:嵛山),阿里巴巴高级开发工程师,武汉大学地信硕士,有丰富的系统开发经验,使用过Java,C++、Go、Python、Javascript、.Net等多种语言,目前主要精力在Java,就职于阿里巴巴代码中心团队,负责后端开发。


本次直播视频精彩回顾地址:

http://click.aliyun.com/m/50640/


PPT地址:

http://click.aliyun.com/m/50641/ 

或点击左下角【阅读原文】进入下载!


以下内容根据演讲嘉宾视频分享以及PPT整理而成。


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


一、Lambda入门

二、多线程技术


一、Lambda入门


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


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


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



代码示例:



在上图展示的代码中,代码中的匿名内部类继承了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就是进程。进程是需要系统分配资源的,资源相当于手机的内存。通过这个例子,我们可以加深对进程和程序概念上的理解。另外,我们也可以通过反抽象的方法理解进程与线程的概念。举例说明:公司运转与员工工作,这里的公司,我们可以对应到程序;进程是程序的运行活动,这里的进程,我们可以理解为公司的正常运转;同时,公司想要正常运转,离不开员工的工作,员工是公司运转不可分割的实体,只有员工才是真正做事的人,因此我们可以将线程类比员工。


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. 线程协作


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


这时,多线程之间该如何协作,是需要我们去解决的。我们通过下面的代码,学会使用关键字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。



上述代码的运行结果如下图所示。代码利用循环,无限添加runnable的实现,但是由于单一线程的阻塞队列是没有边界的,会导致添加的对象过多,耗尽内存资源。因此阿里巴巴开发手册是明确禁止使用Java预置线程池的。



end

点此下载演讲PPT!

 
云栖社区 更多文章 宝宝眼中的程序员,最后一个的回答太萌了! 阿里七层流量入口 Tengine硬件加速探索之路 张雪峰:创业团队极速发展过程中的分分合合 阿里云技术专家金九:Tengine HTTPS原理解析、实践与调试 阿里HBase的数据管道设施实践与演进
猜您喜欢 java定时器的几种用法 0x08 七级浮屠-02,数据马步 《Java编程思想》作者Bruce Eckel演讲《Failing at Failing》 Swift包管理器:在Linux上创建和使用X11包 iOS UI显示-View篇