Java多线程高级特性(JDK8)

[TOC]

一、Java多线程

1.Java多线程基础知识

Java 给多线程编程提供了内置的支持。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

进程:一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

2.Java线程的生命周期

  • 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  • 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  • 阻塞(BLOCKED):表示线程阻塞于锁。
    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的Sleep()或Join发出I/O请求,线程就会进入到阻塞状态,当Sleep()状态超时,join()等待线程终止或超时,或者I/O处理完毕,线程就会进入就绪状态。
  • 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  • 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  • 终止(TERMINATED):表示该线程已经执行完毕。


    线程生命周期

3.线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

4.创建线程

Java创建线程提供了以下三种方法:

  • 通过实现Runnable接口
  • 通过继承Thread类本身
  • 通过Callable和Future创建线程
4.1 通过实现Runnable接口来创建线程

通过Runnable接口需要重写run() 方法

public void run()

在创建一个实现 Runnable 接口的类之后,你可以在类中实例化一个线程对象。
Thread 定义了几个构造方法,下面的这个是我们经常使用的:

Thread(Runnable threadOb,String threadName);

这里,threadOb 是一个实现 Runnable 接口的类的实例,并且 threadName 指定新线程的名字。

新线程创建之后,你调用它的 start() 方法它才会运行。

void start();
4.2 通过继承Thread来创建线程

创建一个线程的第二种方法是创建一个新的类,该类继承Thread类,然后创建一个该类的实例。
继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。

Thread方法:

序号 方法描述
1 public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2 public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3 public final void setName(String name) 改变线程名称,使之与参数 name 相同。
4 public final void setPriority(int priority) 更改线程的优先级。
5 public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
6 public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
7 public void interrupt() 中断线程。
8 public final boolean isAlive()

Thread静态类方法

序号 方法描述
1 public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
2 public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3 public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4 public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
5 public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。
4.3 通过Callable和Future创建线程
  • 创建 Callable 接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建 Callable 实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该 Callable 对象的 call() 方法的返回值。
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
    For Example:
import java.util.concurrent.*;
public class Test {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        Task task = new Task();
        FutureTask futureTask = new FutureTask(task);
        executorService.submit(futureTask);
        executorService.shutdown();
        
        System.out.println("主线程在执行任务...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
         
        try {
            System.out.println("task运行结果:"+futureTask.get());
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } catch (ExecutionException ex) {
            ex.printStackTrace();
        }
         
        System.out.println("所有任务执行完毕");
    }
}
class Task implements Callable{
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在执行任务...");
        //模拟任务耗时
        Thread.sleep(5000);
        return 1000;
    }
}

5.线程的取消和中断

  • 不安全的取消
    Stop()/suspend()/resume()是过期的API,容易导致死锁或者数据不一致
  • 安全的终止线程
    • interrupt() 中断线程,本质是将线程的中断标志位设为true,其他线程向需要中断的线程打个招呼。是否真正进行中断由线程自己决定
    • isInterrupted() 线程检查自己的中断标志位
    • 静态方法Thread.interrupted() 将中断标志位复位为false

6.常用的方法理解

  • run()和start()

    run就是一个普通的方法,跟其他类的实例方法没有任何区别。
  • Sleep

    不会释放锁,所以我们在用sleep时,要把sleep放在同步代码块的外面。
  • yield()

    当前线程出让cpu占有权,当前线程变成了可运行状态,下一时刻仍然可能被cpu选中,不会释放锁。
  • wait()和 notify()/notiyfAll()

    调用以前,当前线程必须要持有锁,调用了wait() notify()/notiyfAll()会释放锁。

通知机制:

线程 A调用了对象O的wait方法进入等待状态,线程 B调用了对象O的notify方法进行唤醒,唤醒的是在对象O上wait的线程(比如线程A)
notify() 唤醒一个线程,唤醒哪一个完全看cpu的心情(谨慎使用)

notiyfAll() 所有在对象O上wait的线程全部唤醒(应该用notiyfAll())

6.线程关键字

6.1 volatile

多个线程同时访问一个共享的变量的时候,每个线程的工作内存有这个变量的一个拷贝,变量本身还是保存在共享内存中。
Violate修饰字段,对这个变量的访问必须要从共享内存刷新一次。最新的修改写回共享内存。可以保证字段的可见性。绝对不是线程安全的,没有操作的原子性。
适用场景:

1、一个线程写,多个线程读;

2、volatile变量的变化很固定

public class VolatileThread implements Runnable {

    private volatile  int a= 0;

    @Override
    public void run() {
//        synchronized (this){
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            a=a+1;
            System.out.println(Thread.currentThread().getName()+"----"+a);

//        }
    }
}
public class VolatileTest {
    public static void main(String[] args) {
        VolatileThread volatileThread = new VolatileThread();

        Thread t1 = new Thread(volatileThread);
        Thread t2 = new Thread(volatileThread);
        Thread t3 = new Thread(volatileThread);
        Thread t4 = new Thread(volatileThread);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
6.2 synchronized

关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
Synchronized的类锁和对象锁,本质上是两把锁,类锁实际锁的是每一个类的class对象。对象锁锁的是当前对象实例。

public class InstanceAndClass {

    //测试类锁
    private static class TestClassSyn extends Thread{
        @Override
        public void run() {
            System.out.println("TestClass is going...");
            synClass();
        }
    }

    //测试对象锁
    private static class TestInstanceSyn extends Thread{
        private InstanceAndClass instanceAndClass;

        public TestInstanceSyn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }

        @Override
        public void run() {
            System.out.println("TestInstance is going..."+instanceAndClass);
            instanceAndClass.synInstance();
        }

    }

    //测试对象锁
    private static class TestInstance2Syn implements Runnable{
        private InstanceAndClass instanceAndClass;

        public TestInstance2Syn(InstanceAndClass instanceAndClass) {
            this.instanceAndClass = instanceAndClass;
        }
        @Override
        public void run() {
            System.out.println("TestInstance2 is going..."+instanceAndClass);
            instanceAndClass.synInstance2();
        }
    }

    //锁对象的方法
    private synchronized void synInstance(){
        SleepUtils.second(3);
        System.out.println("synInstance is going...");
        SleepUtils.second(3);
        System.out.println("synInstance ended");
    }

    //锁对象的方法
    private synchronized void synInstance2(){
        SleepUtils.second(3);
        System.out.println("synInstance2 going...");
        SleepUtils.second(3);
        System.out.println("synInstance2 ended");
    }

    //锁类的方法
    private static synchronized void synClass(){
        SleepUtils.second(1);
        System.out.println("synClass going...");
        SleepUtils.second(1);
    }

    public static void main(String[] args) {
        InstanceAndClass instanceAndClass = new InstanceAndClass();
        Thread t1 = new TestClassSyn();
        Thread t2 = new Thread(new TestInstanceSyn(instanceAndClass));
        Thread t3 = new Thread(new TestInstance2Syn(instanceAndClass));
        t2.start();
        t3.start();
        SleepUtils.second(1);
        t1.start();
    }

}
6.3等待和通知机制
  • 等待方原则:

1、获取对象锁

2、如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足

3、条件满足以后,才能执行相关的业务逻辑

Synchronized(对象){
    While(条件不满足){
    对象.wait()
}
业务逻辑处理
}

  • 通知方原则

1、获得对象的锁;

2、 改变条件;

3、 通知所有等待在对象的线程

Synchronized(对象){
    业务逻辑处理,改变条件
    对象.notify/notifyAll
}

6.4管道输入输出流

文件输入输出,网络输入输出,管道输入输出流用于线程中间的数据传递,传输媒介的内存
pipedOutputStream/input 面向的字节

pipedReader/Writer 面向的是字符

只适合线程间一对一的通信,适用范围较狭窄。

public class PipeTransfer {

    private static class Print implements Runnable{
        private PipedReader in;

        public Print(PipedReader in) {
            this.in = in;
        }

        @Override
        public void run() {
           int receive =0;
            try {
                while((receive=in.read())!=-1){
                    System.out.println((char) receive);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        PipedWriter out = new PipedWriter();
        PipedReader in = new PipedReader();
        //必须进行连接
        out.connect(in);

        Thread t1 = new Thread(new Print(in),"PrintThread");
        t1.start();
        int receive =0;
        try {
            while((receive=System.in.read())!=-1){
                out.write(receive);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            out.close();
        }
    }

}

6.5 join方法

线程A,执行了thread.join(),线程A等待thread线程终止了以后,A在join后面的语句才会继续执行

6.6 一些有用的方法

很多时候我们希望在元素不存在时插入元素,我们一般会像下面那样写代码

synchronized(map){
  if (map.get(key) == null){
      return map.put(key, value);
  } else{
      return map.get(key);
  }
}

putIfAbsent(key,value)方法原子性的实现了同样的功能

V putIfAbsent(K key, V value)

如果key对应的value不存在,则put进去,返回null。否则不put,返回已存在的value。
boolean remove(Object key, Object value)
如果key对应的值是value,则移除K-V,返回true。否则不移除,返回false。
boolean replace(K key, V oldValue, V newValue)
如果key对应的当前值是oldValue,则替换为newValue,返回true。否则不替换,返回false。

二、并发工具和并发容器

1.Hash

散列,任意长度的输入,通过一种算法,变换成固定长度的输出。属于压缩的映射。Md5,Sha,取余都是散列算法,ConcurrentHashMap中是wang/jenkins算法

2.ConcurrentHashMap

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

3.ConcurrentHashMap在JDK1.7中的实现

分段锁的设计思路

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment实际是一种可重入锁(ReentrantLock),HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构。一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得与它对应的Segment锁。

ConcurrentHashMap

ConcurrentHashMap初始化方法是通过initialCapacity、loadFactor和concurrencyLevel(参数concurrencyLevel是用户估计的并发级别,就是说你觉得最多有多少线程共同修改这个map,根据这个来确定Segment数组的大小concurrencyLevel默认是DEFAULT_CONCURRENCY_LEVEL = 16;)。

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,可以看到其中的对象属性要么是final的,要么是volatile的。

4.ConcurrentHashMap在1.8下的实现

改进一:取消segments字段,直接采用transient volatile HashEntry[]
table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。


Table数组+单向链表+红黑树

5.ConcurrentSkipListMap和ConcurrentSkipListSet

ConcurrentSkipListMap TreeMap的并发实现
有序Map
ConcurrentSkipListSet TreeSet的并发实现
有序Set

6.SkipList

二分查找和AVL树查找
二分查找要求元素可以随机访问,所以决定了需要把元素存储在连续内存。这样查找确实很快,但是插入和删除元素的时候,为了保证元素的有序性,就需要大量的移动元素了。

如果需要的是一个能够进行二分查找,又能快速添加和删除元素的数据结构,首先就是二叉查找树,二叉查找树在最坏情况下可能变成一个链表。

于是,就出现了平衡二叉树,根据平衡算法的不同有AVL树,B-Tree,B+Tree,红黑树等,但是AVL树实现起来比较复杂,平衡操作较难理解,这时候就可以用SkipList跳跃表结构。

传统意义的单链表是一个线性结构,向有序的链表中插入一个节点需要O(n)的时间,查找操作需要O(n)的时间。

跳表

如果我们使用上图所示的跳跃表,就可以减少查找所需时间为O(n/2),因为我们可以先通过每个节点的最上面的指针先进行查找,这样子就能跳过一半的节点。

比如我们想查找19,首先和6比较,大于6之后,在和9进行比较,然后在和12进行比较......最后比较到21的时候,发现21大于19,说明查找的点在17和21之间,从这个过程中,我们可以看出,查找的时候跳过了3、7、12等点,因此查找的复杂度为O(n/2)。

跳跃表其实也是一种通过“空间来换取时间”的一个算法,通过在每个节点中增加了向前的指针,从而提升查找的效率。
跳跃表又被称为概率,或者说是随机化的数据结构,目前开源软件 Redis 和 lucence都有用到它。

7.ConcurrentLinkedQueue 无界非阻塞队列

常用方法:

Add,offer:添加元素

Peek:get头元素并不把元素拿走

poll():get头元素把元素拿走

8.CopyOnWriteArrayList和CopyOnWriteArraySet

写的时候进行复制,可以进行并发的读。

适用读多写少的场景:比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

弱点:内存占用高,数据一致性弱

Tips 多用isEmpty()少用 Size()

9.Lock

Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;

Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

1.接口类

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}
  • lock 获取锁
Lock lock =new ReentrantLock();  # ReetrantLock 可重入锁
lock.lock();
try{
    //处理任务
}catch(Exception ex){
     
}finally{
    lock.unlock();   //释放锁
}
  • tryLock() 尝试获取锁 返回类型: boolean
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
  • lockInterruptibly() 线程中断

三、阻塞对列

队列满额,线程数据无法插入,就会被阻塞等待数据插入,直到队列可以插入为止,当队列为空时,会被阻塞,直到数据插入后才可以唤醒线程移除。

1.常用的方法

方法 抛出异常 返回值 一直阻塞 超时退出
插入 Add offer put offer
移除 remove poll take poll
检查 element peek 没有 没有

2.常用阻塞队列

  • ArrayBlockingQueue

数组结构组成有界阻塞队列,先进先出原则,初始化必须传大小,take和put时候用的同一把锁。

  • LinkedBlockingQueue

链表结构组成的有界阻塞队列,先进先出原则,初始化可以不传大小,put,take锁分离

  • PriorityBlockingQueue

支持优先级排序的无界阻塞队列,自然顺序升序排列,更改顺序:类自己实现compareTo()方法,初始化PriorityBlockingQueue指定一个比较器Comparator

  • DelayQueue

使用了优先级队列的无界阻塞队列,支持延时获取,队列里的元素要实现Delay接口。DelayQueue非常有用,可以将DelayQueue运用在以下应用场景:

缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。还有订单到期,限时支付等.

  • SynchronousQueue

不存储元素的阻塞队列,每个put操作必须要等take操作

  • LinkedTransferQueue

链表结构组成的无界阻塞队列,Transfer,tryTransfer,生产者put时,当前有消费者take,生产者直接把元素传给消费者

  • LinkedBlockingDeque

链表结构组成的双向阻塞队列,可以在队列的两端插入和移除,xxxFirst头部操作,xxxLast尾部操作。工作窃取模式。

3.阻塞队的实现原理

Java 阻塞队列使用 Condititon、Lock,在元素进入使用等待wait(),出队使用唤醒。

4.For/Join框架

kyijQs.png

把大任务拆分成很多的小任务,汇总每个小任务的结果得到大任务的结果。
Fork/Join使用两个类来完成以上两件事情。

①ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务
中执行fork()和join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了以下两个子类。
·RecursiveAction:用于没有返回结果的任务。
·RecursiveTask:用于有返回结果的任务。

②ForkJoinPool:ForkJoinTask需要通过ForkJoinPool来执行。
Fork/Join有同步和异步两种方式。

5.CountDownLatch

允许一个或多个线程等待其他线程完成操作。CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。当我们调用CountDownLatch的countDown方法时,N就会减1,CountDownLatch的await方法会阻塞当前线程,直到N变成零。由于countDown方法可以用在任何地方,所以这里说的N个点,可以是N个线程,也可以是1个线程里的N个执行步骤。用在多个线程时,只需要把这个CountDownLatch的引用传递到线程里即可。

6.CyclicBarrier

CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。

CyclicBarrier还提供一个更高级的构造函数CyclicBarrier(int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行barrierAction,方便处理更复杂的业务场景。
CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。

CyclicBarrier和CountDownLatch的区别

CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重
置,CountDownLatch.await一般阻塞主线程,所有的工作线程执行countDown,而CyclicBarrierton通过工作线程调用await从而阻塞工作线程,直到所有工作线程达到屏障。

四 、Java 8高级特性

1、Lambda 表达式

Lambda 是一个匿名函数,我们可以把 Lambda 表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。

  • Style 1 无参,无返回值,Lambda只需要一条语句
Runnable runnable = () -> System.out.println("hello Lambda");
  • Style 2 一个参数
Consumer con = (t) -> System.out.println(t);
  • Style 3 参数省略括号
Consumer con = t -> System.out.println(t);
  • Style 4 2个参数,具有返回值
Comparator comparator = (x,y) -> {
        System.out.println("相加结果是:"+(x+y));
        return Integer.compare(x,y);
    };
  • Style 5 省略return和大括号
Comparator comparator = (x,y) -> Integer.compare(x,y);
  • Style 6 类型推断写法
Comparator comparator = (x,y) -> Integer.compare(x,y);

2.Stream API

Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。

  • 创建Stream

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

 default Stream stream() :   #返回一个顺序流 
 default Stream parallelStream() : # 返回一个并行流

3.Stream 中间操作

原始集合:

List appleList = new ArrayList<>();//存放apple对象集合

Apple apple1 =  new Apple(1,"苹果1",new BigDecimal("3.25"),10);
Apple apple12 = new Apple(1,"苹果2",new BigDecimal("1.35"),20);
Apple apple2 =  new Apple(2,"香蕉",new BigDecimal("2.89"),30);
Apple apple3 =  new Apple(3,"荔枝",new BigDecimal("9.99"),40);

appleList.add(apple1);
appleList.add(apple12);
appleList.add(apple2);
appleList.add(apple3);
  • List转Map
/**
 * List -> Map
 * 需要注意的是:
 * toMap 如果集合对象有重复的key,会报错Duplicate key ....
 *  apple1,apple12的id都为1。
 *  可以用 (k1,k2)->k1 来设置,如果有重复的key,则保留key1,舍弃key2
 */
Map appleMap = appleList.stream().collect(Collectors.toMap(Apple::getId, a -> a,(k1,k2)->k1));

{1=Apple{id=1, name='苹果1', money=3.25, num=10}, 2=Apple{id=2, name='香蕉', money=2.89, num=30}, 3=Apple{id=3, name='荔枝', money=9.99, num=40}}
  • 分组
//List 以ID分组 Map>
Map> groupBy = appleList.stream().collect(Collectors.groupingBy(Apple::getId));

System.err.println("groupBy:"+groupBy);

{1=[Apple{id=1, name='苹果1', money=3.25, num=10}, Apple{id=1, name='苹果2', money=1.35, num=20}], 2=[Apple{id=2, name='香蕉', money=2.89, num=30}], 3=[Apple{id=3, name='荔枝', money=9.99, num=40}]}
  • 过滤 filter
//过滤出符合条件的数据
List filterList = appleList.stream().filter(a -> a.getName().equals("香蕉")).collect(Collectors.toList());

System.err.println("filterList:"+filterList);

[Apple{id=2, name='香蕉', money=2.89, num=30}]
  • 求和
# BigDecimal:
BigDecimal totalMoney = appleList.stream().map(Apple::getMoney).reduce(BigDecimal.ZERO, BigDecimal::add);
System.err.println("totalMoney:"+totalMoney);  //totalMoney:17.48

# Integer
int sum = appleList.stream().mapToInt(Apple::getNum).sum();
System.err.println("sum:"+sum);  //sum:100

你可能感兴趣的:(Java多线程高级特性(JDK8))