JDK8新特性详解Lambda、StreamAPI、Optional等(四)

简介: JDK8新特性详解Lambda、StreamAPI、Optional等(四)

7. Fork/Join框架

parralelStream使用的是Fork/Join框架,Fork/Join框架自JDK7引入。Fork/Join框架可以将一个大任务拆分成为很多的小任务来异步执行

Fork/Join框架主要包含三个模块

  1. 线程池:ForkJoinPool
  2. 任务对象:ForkJoinTask
  3. 执行任务的线程:ForkJoinWorkerThread

7.1 Fork/Join原理-分治法

ForkJoinPoll主要使用分治法来解决问题。典型的应用比如快速排序算法。ForkJoinPool需要使用相对较少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理。到最后会设置一个阈值来规定当数据规模达到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割。转而使用插入排序对他们进行排序,那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,他才能够被执行。

7.2 Fork/Join工作窃取法

Fork/Join最核心的地方就是利用了现代硬件设备的多核,在一个操作会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提升性能的关键。而这里我们要提到的工作窃取算法就是整个Fork/Join框架的核心理念。Fork/Join工作窃取算法是指讴歌线程从其他队列里窃取任务来执行。

那么为什么需要使用工作窃取算法呢?加入我们需要做一个比较大的任务,我们可以把这个任务分割成若干互不依赖的子任务,为了减少线程的竞争。于是把这些子任务分别放到不同的队列中。并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。比如A线程负责处理队列里的任务,但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等待着,不如去帮助其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行,而在这时他们会访问同一个队列,所以为了减少窃取任务线程和被窃取线程之间的 竞争,通常会使用双端队列。被窃取线程永远会从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

工作窃取算法的优点在于充分利用线程进行并行计算,并减少了线程之间的竞争,其缺点是在某些情况下还是会存在竞争,比如双端队列只有一个任务时,并且消耗了更多的系统资源。比如新建了多个线程和多个双端队列。

上文中已经提到了在java8中引入了自动并行化的概念,它能够让一部分java代码自动地以并行的方式执行,也就是我们使用了ForkJoinPool的ParallelStream。

对于ForkJoin通用线程池的线程数量,通常使用默认值就可以了,即运行时计算机的处理器数量,可以通过设置系统属性:java.util.concurrent.ForkJoinPool.common.parallelism-N(N为线程数量),来调整ForkJoinPool的线程数量,可以尝试调成不同的参数来观察每次的输出结果。

7.3 Fork/Join案例

需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000的时候就拆分任务。数量小于3000的时候就计算

public class Test05ForkJoin {
    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        SumRecursiveTask sumRecursiveTask = new SumRecursiveTask(0, 10000);
        Long invoke = pool.invoke(sumRecursiveTask);
        System.out.println("result>>>"+invoke);
    }
}
class SumRecursiveTask extends RecursiveTask<Long>{
    //定义一个拆分的临界值
    private static final long THREADHOLD = 3000l;
    private final long start;
    private final long end;
    public SumRecursiveTask(long start, long end) {
        this.start = start;
        this.end = end;
    }
    @Override
    protected Long compute() {
        long length = end -start;
        if(length <= THREADHOLD){
            //任务不用拆分 计算求和
            System.out.println("求和计算");
            long sum = 0;
            for(long i=start ;i<=end ;i++){
                sum +=i;
            }
            System.out.println("sum="+sum);
            long reduce = LongStream.range(start, end+1).reduce(0, Long::sum);
            System.out.println("reduce计算结果为:"+reduce+"开始:"+start+",结束:"+end);
            System.out.println("sum计算结果为:"+sum+"开始:"+start+",结束:"+end);
            return reduce;
        }else{
            System.out.println("任务拆分");
            long half = (end + start)/2;
            System.out.println("任务一:开始:"+start+",结束:"+half);
            System.out.println("任务二:开始:"+(half)+",结束:"+end);
            SumRecursiveTask left = new SumRecursiveTask(start, half);
            SumRecursiveTask right = new SumRecursiveTask(half + 1, end);
            right.compute();
            return  left.compute() + right.compute();
        }
    }
}

注意reduce结束位置

输出结果:

任务拆分
任务一:开始:0,结束:5000
任务二:开始:5000,结束:10000
任务拆分
任务一:开始:5001,结束:7500
任务二:开始:7500,结束:10000
求和计算
sum=21876250
reduce计算结果为:21876250开始:7501,结束:10000
sum计算结果为:21876250开始:7501,结束:10000
求和计算
sum=15626250
reduce计算结果为:15626250开始:5001,结束:7500
sum计算结果为:15626250开始:5001,结束:7500
求和计算
sum=21876250
reduce计算结果为:21876250开始:7501,结束:10000
sum计算结果为:21876250开始:7501,结束:10000
任务拆分
任务一:开始:0,结束:2500
任务二:开始:2500,结束:5000
求和计算
sum=9376250
reduce计算结果为:9376250开始:2501,结束:5000
sum计算结果为:9376250开始:2501,结束:5000
求和计算
sum=3126250
reduce计算结果为:3126250开始:0,结束:2500
sum计算结果为:3126250开始:0,结束:2500
求和计算
sum=9376250
reduce计算结果为:9376250开始:2501,结束:5000
sum计算结果为:9376250开始:2501,结束:5000
任务拆分
任务一:开始:5001,结束:7500
任务二:开始:7500,结束:10000
求和计算
sum=21876250
reduce计算结果为:21876250开始:7501,结束:10000
sum计算结果为:21876250开始:7501,结束:10000
求和计算
sum=15626250
reduce计算结果为:15626250开始:5001,结束:7500
sum计算结果为:15626250开始:5001,结束:7500
求和计算
sum=21876250
reduce计算结果为:21876250开始:7501,结束:10000
sum计算结果为:21876250开始:7501,结束:10000
result>>>50005000

七、Optional

Optional主要用来解决空指针异常

1. 以前对于null的处理

@Test
    public void test01(){
//        String name = "张三";
        String name = null;
        if(name != null){
            System.out.println(name);
        }else {
            System.out.println("是空值");
        }
    }

2. Optional类

Optional类是一个没有子类的工具类,Optional是一个可以为null的容器对象,他的主要作用就是为了避免Null检查,防止NullpointerException

3. Optional的基本使用

Optional的创建方式

//Optional的创建方式
    @Test
    public void test02(){
        //第一种方法 通过of方法。of方法不支持null
        Optional.of("张三");
        //Optional.of(null);
        //第二种方法 通过ofNullable 支持null
        Optional.ofNullable("张三");
        Optional.ofNullable(null);
        //第三种方法 empty 直接创建一个空的Optional对象
        Optional.empty();
    }

4. Optional的常用方法

@Test
    public void test03(){
        Optional<String> op1 = Optional.of("张三");
        Optional<String> op2 = Optional.empty();
        System.out.println(op1.get());
//        System.out.println(op2.get());
        if(op1.isPresent()){
            System.out.println(op1.get());
        }
        if(op2.isPresent()){
            System.out.println(op2.get());
        }else{
            System.out.println("是空值");
        }
        String s3 = op1.orElse("李四");
        System.out.println("s3="+s3);
        String s4 = op2.orElse("空值");
        System.out.println("s4="+s4);
        String s5 = op2.orElseGet(() -> {
            return "空数据";
        });
        System.out.println("s5="+s5);
        //如果存在值就做什么操作
        op1.ifPresent(s-> {
            System.out.println("有数据存在");
        });
    }

例子

/**
     * 自定义一个方法,将Person对象中的name转换为大写,并返回
     */
    @Test
    public void test05(){
        Person p = new Person();
        String op1 = getNameOptinal(Optional.of(p));
        String oldp1 = getName(p);
        System.out.println("Oldop1->result="+oldp1);
        System.out.println("op1->result="+op1);
        p.setName("zhangsan");
        String oldp2 = getNameOptinal(Optional.of(p));
        String op2 = getNameOptinal(Optional.of(p));
        System.out.println("Oldop2->result="+oldp2);
        System.out.println("op2->result="+op2);
    }
    public String getNameOptinal(Optional<Person> person){
        if(person.isPresent()){
            String msg = person.map(Person::getName)
                    .map(String::toUpperCase)
                    .orElse("是空值");
            return msg;
        }else{
            return null;
        }
    }
    public String getName(Person person){
        if(person!=null){
            String name = person.getName();
            if(name !=null){
                return name.toUpperCase(Locale.ROOT);
            }else{
                return null;
            }
        }else{
            return null;
        }
    }

输出

Oldop1->result=null
op1->result=是空值
Oldop2->result=ZHANGSAN
op2->result=ZHANGSAN

八、新时间日期API

1. 旧版日期时间的问题

在旧版本JDK对于日期和时间的这块是非常差的

public void test01(){
        //1. 设计不合理
        Date now = new Date();
        System.out.println(now);
        Date data2 = new Date(2022,07,22);
        System.out.println(data2);
        //2. 时间格式化和解析操作是线程不安全的
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        System.out.println("sdf.format(now) = " + sdf.format(now));
        for(int i=0;i<50;i++){
            new Thread(()->{
//                System.out.println(sdf.format(now));
                try {
                    System.out.println(sdf.parse("2022-03-03"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
  1. 设计不合理,在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间,而java.sql.Date只包含日期。此外用于格式化和解析的类在java.text包下
  2. 非线程安全问题,java.util.Date是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一。
  3. 时区处理麻烦,日期类并不提供国际化,没有时区支持。

2. 新日期时间API介绍

JDK8增加了一套全新的时间日期API,这套API设计合理,是线程安全的。新的日期时间API位于java.time包中,下面是一些关键类:

  • LocalDate:表示日期,包含年于日,格式为2022-07-22
  • LocalTime:表示时间,包含时分秒,格式为17:38:54.132333444
  • LocalDateTime:表示日期时间,包含年月日时分秒,格式为2022-07-22 17:38:54.132333444
  • DateTimeFormatter:日期时间格式化类。
  • Instant:时间戳,表示一个特定的时间瞬间。
  • Duration:用于计算两个时间(LocalTime,时分秒)的距离
  • Period:用于计算两个日期(LocalDate,年月日)的距离
  • ZonedDateTime:包含时区的时间

java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公里。平年有365天,闰年366天。此外java8还提供了4套其他历法,分别是:

  • ThaiBuddhistDate:泰国佛教历
  • MinguoDate:中华民国历
  • JapaneseDate:日本历
  • HijrahDate:伊斯兰历

2.1 日期时间的常见操作

LocalDate、LocalTime、LocalDateTime的操作

/**
     * JDK8 时间日期操作
     */
    @Test
    public void test02(){
        LocalDate nowDate = LocalDate.now();
        System.out.println("nowDate = " + nowDate);
        LocalDate localDate = LocalDate.of(2022, 05, 05);
        System.out.println("localDate = " + localDate);
        LocalTime nowTime = LocalTime.now();
        System.out.println("nowTime = " + nowTime);
        LocalTime localTime = LocalTime.of(5, 5, 5);
        System.out.println("localTime = " + localTime);
        LocalDateTime now = LocalDateTime.now();
        System.out.println("now = " + now);
        LocalDateTime localDateTime = LocalDateTime.of(2022, 2, 2, 2, 2, 2);
        System.out.println("localDateTime = " + localDateTime);
        System.out.println("now.getYear() = " + now.getYear());
        System.out.println("now.getMonth() = " +now.getMonth());
        System.out.println("now.getDayOfMonth() = " + now.getDayOfMonth());
        System.out.println("now.getHour() = " + now.getHour());
        System.out.println("now.getMinute() = " + now.getMinute());
        System.out.println("now.getSecond() = " + now.getSecond());
    }

2.2 时间日期的修改和比较

/**
     * 日期时间的修改
     */
    @Test
    public void test03(){
        LocalDateTime now = LocalDateTime.now();
        System.out.println("now = " + now);
        //修改日期时间 对日期时间的修改,对已经存在的LocalDate对象创建了他的模板,并不会修改原来的信息
        LocalDateTime localDateTime = now.withYear(2010);
        System.out.println("修改年份 = " + localDateTime);
        System.out.println("修改小时 = " + now.withHour(1));
        System.out.println("修改月份 = " + now.withMonth(1));
        System.out.println("修改日期 = " + now.withDayOfMonth(1));
        //日期加上指定时间
        System.out.println("now.plusYears(2) = " + now.plusYears(2));
        System.out.println("now.plusMonths(2) = " + now.plusMonths(2));
        System.out.println("now.plusDays(2) = " + now.plusDays(2));
        //日期减去指定时间
        System.out.println("now.minusYears(2) = " + now.minusYears(2));
        System.out.println("now.minusMonths(2) = " + now.minusMonths(2));
        System.out.println("now.minusDays(2) = " + now.minusDays(2));
    }
  //时间日期比较
    @Test
    public void test04(){
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime old = LocalDateTime.of(2010, 1, 1, 1, 1, 1);
        System.out.println("now.isBefore(old) = " + now.isBefore(old));
        System.out.println("now.isAfter(old) = " + now.isAfter(old));
        System.out.println("now.isEqual(old) = " + now.isEqual(old));
    }

2.3 格式化和解析操作

在JDK8中我们可以通过java.time.format.DateTimeFormatter类可以进行日期的解析和格式化操作

/**
     * 日期时间格式化
     */
    @Test
    public void test05(){
        LocalDateTime now = LocalDateTime.now();
        //系统默认的格式 2022-07-22T18:10:31.852
        DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME;
        //将时间日期转换为字符串
        String formatstr = isoDateTime.format(now);
        System.out.println("isoDateTime.format(now) = " + formatstr);
        //通过ofPattern方法指定特定的格式
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println("dateTimeFormatter.format(now) = " + dateTimeFormatter.format(now));
        //将字符串解析为一个时间日期类型
        LocalDateTime parse = LocalDateTime.parse("2010-10-10 10:10:10", dateTimeFormatter);
        System.out.println(parse);
    }

2.4 Instant类

在JDK8中给我们新增了一个Instant类(时间戳/时间线)内部保存了从1970年1月1日00:00:00以来的秒和纳秒

/**
     * instant时间戳
     */
    @Test
    public void test06() throws InterruptedException {
        Instant now = Instant.now();
        System.out.println("Instant.now() = " + now);
        now.getNano();
        Thread.sleep(5);
        Instant now1 = Instant.now();
        System.out.println("Instant.now1() = " + now1);
        System.out.println("系统耗时"+ (now1.getNano()-now.getNano()));
    }

2.5 计算日期时间差

JDK8中提供了两个工具类Duration/Period:计算时间日期差

  1. Duration: 用来计算两个时间的差(LocalTime)
  2. Period: 用来计算两个日期的差(LocalDate)
/**
     * 计算时间日期的差
     */
    @Test
    public void test07(){
        LocalTime now = LocalTime.now();
        LocalTime time = LocalTime.of(2, 2, 2);
        System.out.println("now = " + now);
        System.out.println("time = " + time);
        Duration between = Duration.between(time, now);
        System.out.println(between.toDays());
        System.out.println(between.toHours());
        System.out.println(between.toMillis());
        System.out.println(between.toNanos());
        LocalDate now1 = LocalDate.now();
        LocalDate date = LocalDate.of(2020, 1, 1);
        Period betweenDate = Period.between(date, now1);
        System.out.println("betweenDate.getYears() = " + betweenDate.getYears());
        System.out.println("betweenDate.getMonths() = " + betweenDate.getMonths());
        System.out.println("betweenDate.getDays() = " + betweenDate.getDays());
    }

2.6 时间校正器

有时候我们可能需要做出如下调整:将日期调整到下个月的第一天等操作。这时我们通过时间校正器效果可能会更好

  • TemporalAdjuster:时间校正器
  • TemporalAdjusters:通过该类的静态方法提供了大量的常用TemporalAdjuster的实现
@Test
    public void test08(){
        LocalDateTime now = LocalDateTime.now();
        TemporalAdjuster adjuster = (temporal)->{
            LocalDateTime localDateTime = (LocalDateTime) temporal;
            System.out.println(localDateTime);
            LocalDateTime nextMonthDay = localDateTime.plusMonths(1).withDayOfMonth(1);
            System.out.println(nextMonthDay);
            return nextMonthDay;
        };
        adjuster.adjustInto(now);
        // 我们可以通过TemporalAdjusters 来实现 // LocalDateTime nextMonth = now.with(adJuster);
        LocalDateTime next = now.with(TemporalAdjusters.firstDayOfNextMonth());
        System.out.println("next = " + next);
    }

2.7 时间日期的时区

Java8中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。

其中每个时区都对应着ID,ID的格式为“区域/城市”。例如Asia/Shanghai等。

ZonedId:该类中包含了所有的时区信息

/**
     * 时区操作
     */
    @Test
    public void test09(){
//        ZoneId.getAvailableZoneIds().stream().forEach(System.out::println);
        //获取当前时间 中国使用的是东八区的时区,比标准时间早八个小时
        LocalDateTime now = LocalDateTime.now();
        System.out.println("now = " + now);
        //获取标准时间
        ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
        System.out.println("bz = " + bz);
        //使用计算机默认的时区,创建日期时间
        ZonedDateTime now1 = ZonedDateTime.now();
        System.out.println("now1 = " + now1);
        //使用指定时区创建日期时间
        ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
        System.out.println("now2 = " + now2);
    }

JDK新的日期时间API的优势:

  1. 新版时间日期API中,日期和时间对象是不可变的,操作日期不会影响原来的值,而是生成一个新的实例
  2. 提供不同的两种方式,有效的区分了任何机器的操作
  3. TemporalAdjuster可以更精确的操作日期,还可以自定义日期调整器
  4. 线程安全
    LocalDate date = LocalDate.of(2020, 1, 1);
    Period betweenDate = Period.between(date, now1);
    System.out.println("betweenDate.getYears() = " + betweenDate.getYears());
    System.out.println("betweenDate.getMonths() = " + betweenDate.getMonths());
    System.out.println("betweenDate.getDays() = " + betweenDate.getDays());
    }
#### 2.6 时间校正器
有时候我们可能需要做出如下调整:将日期调整到下个月的第一天等操作。这时我们通过时间校正器效果可能会更好
- TemporalAdjuster:时间校正器
- TemporalAdjusters:通过该类的静态方法提供了大量的常用TemporalAdjuster的实现
```java
    @Test
    public void test08(){
        LocalDateTime now = LocalDateTime.now();
        TemporalAdjuster adjuster = (temporal)->{
            LocalDateTime localDateTime = (LocalDateTime) temporal;
            System.out.println(localDateTime);
            LocalDateTime nextMonthDay = localDateTime.plusMonths(1).withDayOfMonth(1);
            System.out.println(nextMonthDay);
            return nextMonthDay;
        };
        adjuster.adjustInto(now);
        // 我们可以通过TemporalAdjusters 来实现 // LocalDateTime nextMonth = now.with(adJuster);
        LocalDateTime next = now.with(TemporalAdjusters.firstDayOfNextMonth());
        System.out.println("next = " + next);
    }

2.7 时间日期的时区

Java8中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。

其中每个时区都对应着ID,ID的格式为“区域/城市”。例如Asia/Shanghai等。

ZonedId:该类中包含了所有的时区信息

/**
     * 时区操作
     */
    @Test
    public void test09(){
//        ZoneId.getAvailableZoneIds().stream().forEach(System.out::println);
        //获取当前时间 中国使用的是东八区的时区,比标准时间早八个小时
        LocalDateTime now = LocalDateTime.now();
        System.out.println("now = " + now);
        //获取标准时间
        ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC());
        System.out.println("bz = " + bz);
        //使用计算机默认的时区,创建日期时间
        ZonedDateTime now1 = ZonedDateTime.now();
        System.out.println("now1 = " + now1);
        //使用指定时区创建日期时间
        ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Marigot"));
        System.out.println("now2 = " + now2);
    }

JDK新的日期时间API的优势:

  1. 新版时间日期API中,日期和时间对象是不可变的,操作日期不会影响原来的值,而是生成一个新的实例
  2. 提供不同的两种方式,有效的区分了任何机器的操作
  3. TemporalAdjuster可以更精确的操作日期,还可以自定义日期调整器
  4. 线程安全
目录
相关文章
|
5天前
|
安全 JavaScript 前端开发
JDK1.8的重要的新特性与功能
Java Development Kit (JDK) 1.8,也称为Java 8,是Java平台的一个重大更新,于2014年3月发布。它引入了多项新特性、新的API和性能提升
103 3
|
5天前
|
Java
【JAVA进阶篇教学】第二篇:JDK8中Lambda表达式
【JAVA进阶篇教学】第二篇:JDK8中Lambda表达式
|
5天前
|
算法 Java 编译器
升级指南之JDK 11+ 新特性和AJDK
本文详细介绍了JDK个版本之间的特性、区别以及JDK版本更迭时优化了哪些地方,对JDK的版本选择给出了建议,以及升级教程。
|
5天前
|
存储 安全 Java
JDK22发布了!来看看有哪些新特性
以上是介绍 JDK22新特性的全部内容了,突然V哥想要感慨一下,技术之路,学无止境,选择 IT 技术,作个纯粹的人,享受研究技术的过程,这种带来的快感,也许只有真正热爱编程的人才能有体会。
|
5天前
|
Java
Java jdk1.8 lambda 遍历集合的时候到底需不需判空
Java jdk1.8 lambda 遍历集合的时候到底需不需判空
|
5天前
|
编解码 Java API
集合在JDK9中的新特性
集合在JDK9中的新特性
|
5天前
|
Java
JDK8新特性--lambda表达式
面向对象思想强调:必须通过对象的形式来做事情
JDK8新特性--lambda表达式
|
5天前
|
SQL Java API
浅析jdk8所包含的主要特性
浅析jdk8所包含的主要特性
|
5天前
|
IDE Java Shell
02|手把手教你安装JDK与配置主流IDE
02|手把手教你安装JDK与配置主流IDE
27 0
|
5天前
|
Java Shell 开发者
都2024年了!你还不知道在Docker中安装jdk?
都2024年了!你还不知道在Docker中安装jdk?