本次分享主要围绕以下两个方面:Lambda多线程技术介绍1.Lambda介绍Lambda起源于数学中λ演算中的一个匿名函数。从它的起源我们可以知道Lambda本身就是一个匿名函数。函数是Java8的重头戏,体现了函数式编程的思想。现在主流的编程语言都包含了函数式编程的特性,而Java8在演进过程中吸收了这个特性,作为面向对象编程的补充。Lambda的基本语法如下图所示。Lambda语法相对简单。与普通函数相比,没有返回值和函数名。它的参数和执行语句之间用->连接,表示参数会传递给语句执行。Lambda表达式还有两种方法可以简化表达式。当表达式中只有一条执行语句时,语句的{}可以省略;如果接口的抽象方法只有一个形参,()可以省略,只需要参数的名字即可。Lambda可以替换特定的匿名内部类。Lambda表达式不能单独存在,使用时必须继承函数式接口。对于下例中的第一个Lambda表达式,形式参数列表的数据类型是自动推断的,只需要参数名称。代码示例:上图代码中,代码中的匿名内部类继承了Flyable接口,实现了接口中的fly()方法。该代码准备Lambda表达式以重新实现Flyable接口。根据代码中的输出命令,执行结果表明,Lambda表达式起到了与匿名内部类相同的作用。代码中没有定义Lambda表达式的参数类型,但是我们也可以在Lambda表达式中定义需要的类型flyable=(intt)->System.out.println("IcanflybyLambda"),如果参数类型与接口flyable=(Stringt)->System.out.println("IcanflybyLambda")中的方法参数类型不一致,编译器会报错。如果接口实现了两个方法,匿名内部类可以覆盖新方法。但是,Lambda表达式无法做到这一点。编译后会提示有多个抽象方法需要重写。因此,当一个Lambda表达式实现一个接口时,接口中只允许有一个抽象方法。我们称这样的接口为函数式接口。Java8提供注解@FunctionalInterface来判断接口是否为函数式接口。如果没有,注解将会报错。另外,代码尝试使用Lambda表达式替换抽象类的匿名内部类,但是会报错,提示必须继承函数式接口。因此,Lambda可以替换特定的匿名内部类来简化代码,但必须继承函数式接口。二、多线程技术1、进程与线程进程是具有一定独立功能的程序。对于某个数据集上的一个运行活动,它是系统分配和调度资源的一个独立单元。线程是进程的实体,是CPU分配和调度的基本单位,是代码的执行体。从概念上我们可以知道进程是一个程序的运行活动,需要系统进行分配和调度;线程是最终代码的执行主体,是CPU分配和调度的基本单位。同一个进程中可以包含多个线程,线程共享整个进程的资源,一个进程至少包含一个线程。如果很难理解概念,如果我们想完全理解这些概念,我们可以使用一种反抽象的方法,即连接。我们需要在现实生活中找到符合概念描述的东西。比如:我们常说安卓手机比较慢,手机运行的app太多,导致内存不足。那么我们在手机上看到的app就是一个一个的程序;手机卡顿的时候,双击home键可以看到后台有运行的app,我们看到的这些app都是进程。该过程需要系统分配资源,资源相当于手机的内存。通过这个例子,可以加深我们对流程和程序的概念性理解。另外,我们还可以通过反抽象的方法来理解进程和线程的概念。比如:公司的运作和员工的工作,公司在这里,我们可以对应程序;进程就是程序的运行,这里的进程可以理解为公司的正常运行;同时,公司也离不开员工的工作,员工是公司运作中不可分割的一个整体,只有员工才是真正做事的人,所以我们可以把线程比作员工。2.线程的生命周期下图是线程的状态图。所谓生命周期,是指一个线程从出生到死亡所经历的一系列状态。线程通过创建Thread(newThread())的实例进入新的new状态;然后调用start()方法等待时间片分配完毕,进入runnable状态;之后线程获得CPU资源执行任务,进入运行状态;当线程执行完成或被其他线程杀死时,线程进入死亡状态;如果正在运行的线程由于某种原因放弃了CPU并暂停了自己的执行,则进入阻塞状态。在各种条件下,阻塞状态可以恢复到可运行状态,最后线程重新获得时间片后,可以进入运行状态重新运行。在运行状态下,如果时间片用完或者线程主动放弃CPU的使用,则线程回到可运行状态。时间片是指CPU的时间片。CPU将自己的可执行时间分成许多片段,每个片段随机分配给处于可运行状态的线程,从而达到并发的效果。假设我有一个单核CPU。通过划分很多时间片,每个程序都有运行的机会,很多程序仍然可以运行。从宏观上看,它们是并发的,但由于只有一个CPU,所以程序实际上是串行的。的。我们可以通过阅读JDK的Thread类注解来创建和使用线程,如下图所示。根据JDK的注释,下面的代码中使用了两种创建线程的方法。由于Runnable是函数式接口,代码中使用Lambda表达式代替匿名内部类,将runnable传递给Thread,使用start()启动线程。上面代码的结果如下图所示。在下面的代码中,如果我们替换t.start();使用t.run(),打印结果会变成:ThreadThreadrunMainrunnablerun.Main这说明run()方法并没有真正启动线程,run()方法只是在当前线程中执行run中的函数。3.线程协作并行与协作:线程在并发过程中更多的是一种协作关系。前面的概念提到过,进程是系统资源分配的单位,线程本身分配的资源并不多,除了维护。除了自身必要的内存开销外,线程的所有资源都在进程中。多线程在竞争使用资源时,存在抢占或共享的关系。这时候就需要解决多线程之间如何协作。通过下面的代码,我们学习使用关键字synchronized,理解临界区和锁的概念。以上代码模拟售票操作。一共有10张门票。三个售票员,卖家A,卖家,卖家C,一起去卖票。sell()方法模拟卖票的行为。代码启动线程后,运行结果如下图所示。售票员sellerA在一个时间片内运行sell方法中的所有代码,售完票,而sellerB和sellerC在线程并发时也卖掉第10张票,重复售票。这样的操作是不合理的。为了解决重复售票的问题,我们可以使用Java提供的同步关键字synchronized来修改sell()方法,代码如下图所示。使用关键字synchronized修饰后,当多个线程访问sell()方法时,可以保证只有一个线程执行该方法。当前线程执行完sell()方法后,其他线程就可以执行sell()方法了。执行上述代码后,输出结果如下图所示。从下面的结果可以看出,代码解决了重复卖票不合理的问题,但是只有卖家A还在卖票。原因是用synchronized关键字修改sell()方法后,当sellerA拿到sell()方法的执行权时,一口气执行里面的代码,即卖掉所有的票。sellerA执行完后,sellerB在sellerC和sellerC再次执行sell()方法时,票数已经为0,自然会出现下图中没有售票的现象。我们称方法sell()中的内容为临界区。当一个线程进入临界区时,其他线程必须等待该线程执行完临界区的内容后才能进入临界区。下图代码改善了卖家A一口气卖掉所有门票的现象。代码在方法体中使用了关键字synchronized,括号中的this代表一个对象或类。代码与上面的解决方案相比,关键部分从整个方法减少到两行代码。也就是说,在执行这两行代码的时候,多个线程是同步的。执行上述代码的结果如下图所示。从图中可以看出,卖票的不再是卖家A了。而且每次执行代码的结果都不一样,因为CPU的时间片是随机给定的。上面代码中的trycatch方法块让线程休眠50ms,延长了售票操作的时间,期间可以进行其他操作(比如把票给某位顾客)。代码改进后,保证资源不被垄断,使资源分配均匀。从上图中,我们发现有无效票。原因是:假设当前票数为1,A进入临界区卖票,此时B已经做出判断,在临界区外等待。当A售完票时,票数为0,但B仍会进入售票临界区,因此会出现无效票-1。这表明代码需要进一步改进。改进后的代码如下图所示。代码在临界区加入判断条件,只有票数大于0时,才会进行售票操作。这是一种常用的双重检查方法。经仔细检查,运行代码时不会出现无效售票。下面介绍另一种单线程同步的方法。代码如下图所示。代码通过Lock接口定义了一把锁,使用ReentrantLock实现。锁和上面说的关键字synchronized的作用是一样的。它们都定义了一个临界区,并允许线程在进入临界区时进行同步。代码通过lock.lock()定义了临界区的起始点,在try语句块中定义了临界区的执行内容,并在finally语句块中使用unlock()方法解锁。解锁后,线程才真正脱离临界区。使用try和finally的原因是:如果try中抛出异常,如果finally中没有unlock,则线程不会调用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:让线程池的用户所在线程运行提交的任务队列中的任务下面的代码中自定义了一个线程池。通过线程池的submit()方法提交runnable的实现,最后通过线程池的shutdown()方法关闭线程池。Java包中预设的线程池如下:newSingleThreadExecutor;newFixedThreadPool:newCachedThreadPool:newScheduledThreadPool:但是在阿里巴巴的Java开发中不推荐甚至禁止使用Java预设的线程池。下图代码的目的是找出SingleThreadExecutor的bug。运行上述代码的结果如下图所示。代码使用循环最终添加runnable的实现,但是由于单线程的阻塞队列没有边界,会导致添加过多的对象,耗尽内存资源。所以阿里巴巴开发手册明确禁止使用Java预置线程池。针对JAVA微服务、分布式、高并发、高可用、大规模互联网架构技术,面试经验交流。有兴趣的可以关注我的头条号。我会不定期在微头条发布免费资讯链接。这些资料是从各个技术网站上收集整理出来的。如果大家有好的学习资料可以私信发给我,我注明出处后分享给大家。欢迎分享,欢迎评论,欢迎转发!
