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

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

数十款阿里云产品限时折扣中,赶紧点击这里,领劵开始云上实践吧!

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

PPT地址:https://yq.aliyun.com/download/2657
以下内容根据演讲嘉宾视频分享以及PPT整理而成。

本次的分享主要围绕以下两个方面:
一、Lambda入门
二、多线程技术

一、Lambda入门
Lambda起源于数学中的λ演算中的一个匿名函数,从它的起源我们可以知道,Lambda本身就是一个匿名函数,是Java8才推出的亮点,体现了函数式编程的思想。现在主流的编程语言都包含了函数式编程的特性,Java8在进化过程中吸收了该特性,作为面向编程对象的补充。
Lambda基本语法如下图所示,Lambda语法较为简单,和普通函数相比,没有返回值以及函数名,它的参数和执行语句之间通过->连接,表示参数将传递到语句中执行。Lambda表达式还有两种简化表达式的方法,当表达式中只有一个执行语句时,可以省略语句的{};如果接口的抽象方法只有一个形参,()可以省略,只需要参数的名称即可。Lambda可以替代特定匿名内部类,Lambda表达式不能单独存在,在使用时必须继承函数式接口。
下图示例中的第一个Lambda表达式,形参列表的数据类型会自动推断,只需要参数名称。
  e25e8b6c335288e0e8f02486063991a4df56e763

代码示例:
package lambda;

public class Lambda {

    public static void main(String[] args) {

        Flyable flyable = new Flyable() {
            @Override
            public void fly(int a) {
                System.out.println("I can fly by anonymous class");
            }

            //@Override
            //public void landing() {
            //    System.out.println("I can landing by anonymous class");
            //}
        };
        flyable.fly(1);

        flyable = (t) -> System.out.println("I can fly by lambda");
        flyable.fly(1);


        Bird bird = new Bird() {
            @Override
            void fly() {
                System.out.println("I can fly by bird");
            }
        };

        bird = () -> System.out.println("I can fly by lambda");

    }

    @FunctionalInterface
    interface  Flyable {
        void fly(int a);
        //void landing();
    }

    abstract static class Bird {
        abstract void fly();
    }
}
在上图展示的代码中,代码中的匿名内部类继承了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,实际上程序还是串行的。
b6af11b5b2e4270a4f6d429eefa16e149a6ae56a 
我们可以通过阅读JDK的Thread类注释,创建并使用线程,如下图所示。
3d7f805acb23a47fa7e42132afeb02119bbd9317 
按照JDK的注释,下述代码中使用了两种创建线程的方法。由于Runnable是一个函数式接口,因此代码中使用Lambda表达式替代匿名内部类,再将runnable传递给Thread,使用start()启动线程。
public class ThreadTest {
    public static void main(String[] args) {
        PrimeThread thread = new PrimeThread();
        thread.setName("Thread ");
        thread.start();

        Runnable runnable = () -> System.out.println(Thread.currentThread().getName() + " runnable run.");
        Thread t = new Thread(runnable);
        t.run();


        System.out.println(Thread.currentThread().getName());

    }

    static class PrimeThread extends Thread {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + " Thread run");
        }
    }

}
上述代码结果如下图所示。在下图代码中,如果我们将t.start();替换成t.run(),打印结果将会变成: 
Thread Thread run
Main runnable run.
Main
这说明run()方法并没有真正启动线程,run()方法只是在当前的线程中执行了run中的函数。
d1e9ab0938926008ee1318e80ace96ccc4ae7d01 
3. 线程协作
并行与协作:线程在并发的过程中更多的是协作关系,就像之前的概念中所提到的,进程是系统资源分配的单位,线程本身并没有多少分配资源,除了维护自己必须的内存开销之外,线程的所有资源都是在进程中。多线程在使用竞争中资源时,存在抢占或者说是共享的关系。
这时,多线程之间该如何协作,是需要我们去解决的。我们通过下面的代码,学会使用关键字synchronized,以及理解临界区,锁的概念。
public class Tickets {

    int tickets = 10;

    /**
     * 重复卖票
     */
    void sell() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " sell ticket:" + tickets);
            tickets--;
        }
        System.out.println(Thread.currentThread().getName() + " sell out.");
    }
    public static void main(String[] args) {
        Tickets tickets = new Tickets();

        Thread sellerA = new Thread(tickets::sell);
        sellerA.setName("sellerA");

        Thread sellerB = new Thread(tickets::sell);
        sellerB.setName("sellerB");

        Thread sellerC = new Thread(tickets::sell);
        sellerC.setName("sellerC");

        sellerA.start();
        sellerB.start();
        sellerC.start();
    }

}
上述代码模拟售票操作。一共有10张票,三个售票员sellerA,seller,sellerC一起去售票,sell( )方法模拟售票行为。代码启动线程之后,运行结果如下图所示。售票员sellerA在一个时间片内将sell方法中的代码全部跑完,票售空,但是sellerB与sellerC在线程并发时,也售出了第10张票,存在重复售票,这样的操作是不合理的。
b3cb295110fd3d96b5770dcc84830892d68dadba 
为了解决重复售票的问题,我们可以使用Java中提供的同步关键字synchronized修饰sell( )方法,代码如下述所示。使用关键字synchronized修饰后,多线程在访问sell( )方法时,能保证只有一个线程执行这个方法,当前线程执行完sell( )方法后,其他线程才能执行sell( )方法。
/**
     * sync之后,导致独占资源
     */
    synchronized void sell() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " sell ticket:" + tickets);
            tickets--;
        }
        System.out.println(Thread.currentThread().getName() + " sell out.");
    }
执行上述代码后,输出结果如下图所示。从下面结果可以看到,代码解决了重复售票的不合理问题,但是仍然只有sellerA一个在售票。原因在于,通过关键字synchronized修饰sell( )方法后,sellerA在拿到sell( )方法的执行权时,把里面的代码一口气执行完了,也就是将票全部卖出,等sellerA执行完后,sellerB和sellerC再执行sell( )方法时,票数已经为0,自然会出现下图中没有卖出一张票的现象。我们将方法sell( )中的内容叫做临界区,当一个线程进入临界区后,其他线程必须等待该线程执行完临界区内容后,才能进入该临界区。
  6112c390e14c4b2a14142dc7ab6768c5777ce9a1
下述代码改善了上述sellerA一口气卖完所有票的现象。代码在方法体内使用关键字synchronized,括号中的this表示一个对象或者一个类。代码相较于上面的解决方法,将临界区从整个方法缩小到两行代码。也就是说多线程在执行这两行代码时是同步的。
/**
     * 改善后,资源没有独占
     */
    void sell() {
        while (tickets > 0) {
            synchronized (Tickets.class) {
                System.out.println(Thread.currentThread().getName() + " sell ticket:" + tickets);
                tickets--;
            }
            //do something
            try {
                TimeUnit.MILLISECONDS.sleep(50L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        System.out.println(Thread.currentThread().getName() + " sell out");
    }
上述代码执行结果如下图所示。从图中我们可以发现,不再是只有sellerA在卖票。并且代码每次执行结果都是不一样的,因为CPU的时间片是随机给出的。上述代码中的try catch方法块使线程睡50ms,延长售票操作的时间,在这段时间内可以执行其他的操作(比如,将该票给某个顾客)。代码改善过后,保证资源不是被独占的,使资源分配均匀。
  934a4a73b312ef8a1077101b94e4b3856550e49b
从运行结果来看,存在无效票,原因在于:假设当前票数为1,A进入临界区售票,而此时B已经进行判断,在临界区外等待了。当A卖完票后,票数为0,但是B还是会进入临界区进行售票操作,因此,出现无效票-1的情况。这说明代码需要进一步改善。改善后的代码如下所示。代码在临界区内加入判断条件,只有票数大于0时,才会进行售票操作,这是常用的双重检验方法。经过双重检验后,运行代码就不会出现无效售票。 
 /**
     * 改善后,资源没有独占, 修复卖出无效票的问题
     */
    void sell() {
        while (tickets > 0) {
            synchronized (this) {
                if (tickets > 0) {
                    System.out.println(Thread.currentThread().getName() + " sell ticket:" + tickets);
                    tickets--;
                }

            }
            //do something
            try {
                TimeUnit.MILLISECONDS.sleep(50L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

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

            //do something
            try {
                TimeUnit.MILLISECONDS.sleep(50L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        System.out.println(Thread.currentThread().getName() + " sell out");
    }
在上述展示的代码中,如果线程遇到售卖同一张票,锁没有被释放,线程将会等待。改善这种情况的方法是,我们使用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:忽略最早放到队列中的任务
e0b8771792106407a8363f2d12e3c5abe414c1f5
下面代码自定义了一个线程池。通过线程池的submit( )方法提交runnable的实现,最终通过线程池的shutdown( )方法关闭线程池。
 ThreadPoolExecutor threadPoolExecutor =
            new ThreadPoolExecutor(16, 30, 30L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(10), new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {

                    Thread t = new Thread(r);

                    t.setDaemon(false);
                    t.setUncaughtExceptionHandler((thread, e) -> System.out.println(e.getMessage()));
                    return t;
                }
            }, new DiscardOldestPolicy());



        threadPoolExecutor.submit(() -> System.out.println(Thread.currentThread().getName()));

        threadPoolExecutor.shutdown();

        //ExecutorService executorService = Executors.newFixedThreadPool();

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

       for (;;) {
           executorService.submit(() -> {
               try {
                   TimeUnit.SECONDS.sleep(30);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           });
       }
    }
上述代码的运行结果如下图所示。代码利用循环,无限添加runnable的实现,但是由于单一线程的阻塞队列是没有边界的,会导致添加的对象过多,耗尽内存资源。因此阿里巴巴开发手册是明确禁止使用Java预置线程池的。
f3de659279c0b004709cd0c2db9333c648e8a3a3 
本文由云栖志愿小组沈金凤整理,编辑百见
相关文章
|
4天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
4天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
5天前
|
Java 关系型数据库 MySQL
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
UWB (ULTRA WIDE BAND, UWB) 技术是一种无线载波通讯技术,它不采用正弦载波,而是利用纳秒级的非正弦波窄脉冲传输数据,因此其所占的频谱范围很宽。一套UWB精确定位系统,最高定位精度可达10cm,具有高精度,高动态,高容量,低功耗的应用。
一套java+ spring boot与vue+ mysql技术开发的UWB高精度工厂人员定位全套系统源码有应用案例
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
5天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
6天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
存储 数据可视化 安全
Java全套智慧校园系统源码springboot+elmentui +Quartz可视化校园管理平台系统源码 建设智慧校园的5大关键技术
智慧校园指的是以物联网为基础的智慧化的校园工作、学习和生活一体化环境,这个一体化环境以各种应用服务系统为载体,将教学、科研、管理和校园生活进行充分融合。无处不在的网络学习、融合创新的网络科研、透明高效的校务治理、丰富多彩的校园文化、方便周到的校园生活。简而言之,“要做一个安全、稳定、环保、节能的校园。
25 6
|
6天前
|
存储 安全 Java
Java中的容器,线程安全和线程不安全
Java中的容器,线程安全和线程不安全
13 1
|
6天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
6 0
|
6天前
|
监控 前端开发 算法
Java技术体系
Java技术体系(韩顺平老师整理)
8 0

热门文章

最新文章