java8函数式编程简介

函数作为一等公民

一个函数可以作为另一个函数的返回值.

//js中
function f1(){
    var n=1;
    function f2(){
        alert(n);
    }
    return f2;
}

var result=f1();
result();      //1

无副作用

副作用指的是函数在调用过程中,除了给出返回值,还修改函数外部的状态.
函数式编程认为,函数的副作用应该尽量被避免.
注意:
显示函数:与外界交换数据的唯一渠道是参数和返回值,不会读取或修改函数的外部状态
隐式函数:除参数和返回值外,还会读取/修改外部信息.

声明式

命令式: 使用可变对象和指令
声明式: 需要提供明确的指令操作
//命令式
public static void imperative(){
    int[] iArr={1,3,4,5,6,9,7,8,4,2};
    for(int i=0;i<iArr.length;i++){
        System.out.println(iArr[i]);
    }

}

//声明式
public static void declarative(){
    int[] iArr={1,3,4,5,6,9,7,8,4,2};
    Arrays.stream(iArr).forEach(System.out::println);
}

不变的对象

函数式编程中,几乎所有传递的对象都不会被轻易修改
int[] iArr={1,3,4,5,6,9,7,8,4,2};
Arrays.stream(arr).map((x)->x=x+1).forEach(System.out::println); //输出+1的数组
System.out.println();
Arrays.stream(arr).forEach(System.out::println);  //原数组不变

易于并行

不变模式不需要关心线程安全问题,多线程下性能更好.

更少的代码

简洁明了,代码更少

函数式编程基础

FunctionalInterface注释

函数式接口只能有一个抽象方法,任何被Object实现的方法,都不能被视为抽象方法
@FunctionalInterface
public static interface IntHandler{
    void handle(int i)
}

@FunctionalInterface
public static interface IntHandler{
    void handle(int i);
    boolean equals(Object obj);    //object中含有的方法,该接口也为函数式接口
}

接口默认方法

java8接口也可以包含多个实例方法,具体实现如下:

public interface IHorse{
    void eat();
    default void run(){        //接口默认方法
        System.out.println("hourserun");
    }
}

public interface IAnimal{
    default void breath(){        //接口默认方法
        System.out.println("breath");
    }
}

public class Mule implements IHorse,IAnimal{
    @Override
    public void eat(){
        System.out.println("Mule eat");
    }
    public static void main(String[] args){
        Mule m=new Mule();
        m.run();
        m.breath();
    }
}

lambda表达式

List<Integer> numbers=Arrays.asList(1,2,3,4,5,6);
numbers.forEach((Integer value)-> System.out.println(value));

lambda表达式也可以访问外部的局部变量,但局部变量不允许改变,即final修饰

final int num=2; //不写final也可以,但不允许改变此值
Function<Integer,Integer> stringConverter=(from)->from*num;
System.out.println(stringConverter.apply(3));

方法引用

  • 静态方法引用: ClassName::methodName
  • 实例方法引用: instanceReference::methodName
  • 超类上实例方法引用: super::methodName
  • 类型上的实例方法: ClassName::methodName
  • 构造方法引用: Class::new
  • 数组构造方法引用: TypeName[]::new
public class Test2 {
    public static void main(String[] args) {

        ArrayList<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add(new User(i, "billy" + i));
        }
        users.stream().map(User::getName).forEach(System.out::println);//调用User的实例方法
    }
}

User::getName,表示User类的实例方法,在执行时,自动识别流中的元素作为调用目标还是调用方法的参数.

一般来说,如果使用的是静态方法,或者调用目标明确,那么流内的元素会自动作为参数使用,如果函数引用表示实例方法,并且不存在调用目标,那么流内元素自动作为调用目标

一步一步走入函数式编程

public class Test2 {
    static int[] arr = {1, 3, 4, 5, 6, 7, 8, 9, 10};
    public static void main(String[] args) {
//        ArrayList<User> users = new ArrayList<>();
//        for (int i = 0; i < 10; i++) {
//            users.add(new User(i, "billy" + i));
//        }
//        users.stream().map(User::getName).forEach(x -> System.out.println(x));
        IntConsumer outprintln = System.out::println;    //约等于函数接收函数
        IntConsumer errprintln = System.out::println;
        Arrays.stream(arr).forEach(outprintln.andThen(errprintln));
    }
}

并行流与并行排序

并行流过滤数据

public class PrimeUtil {
    public static boolean isPrime(int number) {
        int tmp = number;
        if (tmp < 2) {
            return false;
        }
        for (int i = 2; Math.sqrt(tmp) >= i; i++) {
            if (tmp % i == 0) {
                return false;
            }
        }
        return true;
    }
}

IntStream.range(1,10000).filter(PrimeUtil::isPrime).count  //串行流处理
IntStream.range(1,10000).parallel().filter(PrimeUtil::isPrime).count  //并行流处理

并行排序

数组可以用Arrays.sort()进行串行排序,也可以用Arrays.parallelSort()来进行并行排序
    Random r=new Random();
    Arrays.setAll(arr,i->r.nextInt())
    Arrays.parallelSetAll(arr,i->r.nextInt())

增强future:CompletableFuture

完成了就通知

和Future一样,向Completable请求一个数据,如果数据还没有准备好,请求线程就会等待
public class CFutureMain1 {
    public static class AskThread implements Runnable {
        CompletableFuture<Integer> re = null;
        public AskThread(CompletableFuture<Integer> re) {
            this.re = re;
        }
        @Override
        public void run() {
            int myRe = 0;
            try {
                myRe = re.get() * re.get();     //CompletableFuture中没有数据时阻塞住
            } catch (Exception e) {
            }
            System.out.println(myRe);
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final CompletableFuture<Integer> future = new CompletableFuture<>();
        new Thread(new AskThread(future)).start();
        // 模拟长时间其他调用
        Thread.sleep(1000);
        // 告知完成结果
        future.complete(60);
    }
}

异步执行

public class CFutureMain2 {
    public static Integer calc(Integer para) {
        try {
            // 模拟一个长时间的执行
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        return para*para;
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        final CompletableFuture<Integer> future =
                CompletableFuture.supplyAsync(() -> calc(50));  //异步执行
        System.out.println(future.get());   //get()会阻塞,如果不阻塞住线程结束,无法获取值
    }
}

异步方法有如下几个:

clipboard.png

注意
supplyAsync()方法用于需要返回值的场景,
runAsync()用于没有返回值的场景
可以接受Executor参数则使用指定的线程池,否则使用默认使用ForkJoinPool.commonPool
这个线程池中均为Daemon线程,主线程若退出则所有线程均退出

流式调用

CompletableFuture实现了Future也实现了CompletionStage接口

public class CFutureMain3 {
    public static Integer calc(Integer para) {
        try {
            // 模拟一个长时间的执行
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        return para * para;
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<Void> fu = CompletableFuture.supplyAsync(() -> calc(50))    //异步计算
                .thenApply((i) -> Integer.toString(i))    //计算完成后调用
                .thenApply((str) -> "\"" + str + "\"")
                .thenAccept(System.out::println);
        fu.get();
    }
}

异常处理

public class CFutureMain4 {
    public static Integer calc(Integer para) {
        return para / 0;
    }
    public static void main(String[] args) throws InterruptedException,ExecutionException {
        CompletableFuture<Void> fu = CompletableFuture
                .supplyAsync(() -> calc(50))
                .exceptionally(ex -> {            //优雅简单
                    System.out.println(ex.toString());
                    return 0;
                })
                .thenApply((i) -> Integer.toString(i))
                .thenApply((str) -> "\"" + str + "\"")
                .thenAccept(System.out::println);
        fu.get();
    }
}

组合多个CompletableFuture

  • thenCompose
  • thenCombine
public class CFutureMain5 {
    public static Integer calc(Integer para) {
        return para/2;
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<Void> fu =
                CompletableFuture.supplyAsync(() -> calc(50))
                .thenCompose((i)->CompletableFuture.supplyAsync(() -> calc(i)))
                .thenApply((str)->"\"" + str + "\"").thenAccept(System.out::println);
        fu.get();
    }
}
public class CFutureMain6 {
    public static Integer calc(Integer para) {
        return para / 2;
    }
    public static void main(String[] args) throws InterruptedException,ExecutionException {
        CompletableFuture<Integer> intFuture = CompletableFuture.supplyAsync(() -> calc(50));
        CompletableFuture<Integer> intFuture2 = CompletableFuture.supplyAsync(() -> calc(25));
        CompletableFuture<Void> fu = intFuture.thenCombine(intFuture2, (i, j) -> (i + j))    //两个执行结果进行相加
                .thenApply((str) -> "\"" + str + "\"")
                .thenAccept(System.out::println);
        fu.get();
    }
}

支持timeout的CompletableFuture

在CompletableFuture中添加timeout的功能,超时则直接抛出异常.仅适用于jdk9以上版本

public class CFutureMain7 {
    public static Integer calc(Integer para) {
        return para / 2;
    }
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            return calc(50);

        }).orTimeout(1, TimeUnit.SECONDS).exceptionally(e -> {
            System.err.println(e);
            return 0;
        }).thenAccept(System.out::println);
        try
        {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
    }
}

读写锁改进:StampedLock

读写锁的一种改进版本,读写锁虽然分离了读写功能,但读写之间依旧冲突,大量的读线程可能会引起写线程的"饥饿".而Stampedock则提供了一种乐观的读策略,类似于无锁,使得乐观锁完全不会阻塞写线程.

使用示例

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();
    void move(double deltaX, double deltaY) { // 排他锁
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }
    double distanceFromOrigin() { // 只读方法
        long stamp = sl.tryOptimisticRead();    //返回类似时间戳的整数
        double currentX = x, currentY = y;
        if (!sl.validate(stamp)) {    //比较时间戳 若一致表示安全 以上可重复尝试
            stamp = sl.readLock();    //验证失败则使用悲观读锁
            try {
                currentX = x;
                currentY = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }
    void moveIfAtOrigin(double newX, double newY) { // upgrade
        // Could instead start with optimistic, not read mode
        long stamp = sl.readLock();
        try {
            while (x == 0.0 && y == 0.0) {
                long ws = sl.tryConvertToWriteLock(stamp);
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            sl.unlock(stamp);
        }
    }
}

StampedLock的小陷阱

StampedLock内部使用了类似CAS操作的死循环,挂起线程时,使用的是Unsafe.park()函数,park函数被中断时直接返回不抛出异常,而在StampedLock死循环中没有处理中断的逻辑,这会导致再次进入循环条件,疯狂占用cpu

public class StampedLockCPUDemo {
    static Thread[] holdCpuThreads = new Thread[3];
    static final StampedLock lock = new StampedLock();
    public static void main(String[] args) throws InterruptedException {
        new Thread() {
            public void run() {
                long readLong = lock.writeLock();
                //LockSupport.parkNanos(600000000000L);
                LockSupport.parkNanos(15L*1000*1000*1000);    //写锁占用很长时间
                lock.unlockWrite(readLong);
            }
        }.start();
        Thread.sleep(100);
        for (int i = 0; i < 3; ++i) {
            holdCpuThreads[i] = new Thread(new HoldCPUReadThread());//启动读线程
            holdCpuThreads[i].start();
        }
        Thread.sleep(10000);
        //线程中断后,会占用CPU
        for (int i = 0; i < 3; ++i) {
            holdCpuThreads[i].interrupt();    //10秒后打断读线程,cpu使用率会飙升
        }
    }
    private static class HoldCPUReadThread implements Runnable {
        public void run() {
            long lockr = lock.readLock();
            System.out.println(Thread.currentThread().getName()+ " 获得读锁");
            lock.unlockRead(lockr);
        }
    }
}

StampedLock的实现思想

StampedLock的内部实现是基于CLH锁.CLH锁是一种自旋锁,保证FIFO和无饥饿
CLH的基本思想:维护一个等待线程队列,所有申请锁但没成功的线程都记录在队列中.每一个节点保存一个标记位(locked),用于判断当前线程是否已经释放锁.
当一个线程试图获取锁时,获取当前等待队列的尾部节点作为其前序节点,并通过while(pred.locked){}来判断前序节点是否已经成功释放锁
如果前序节点没有释放锁,当前线程就不能执行,只会自旋等待.
释放锁时,自身节点locked位置标记位false,后续等待线程就能继续执行了.

clipboard.png

原子类的增强

更快的原子类:LongAdder

CAS循环的机制若在竞争激烈的情况下失败的概率会更大,从而原子操作就会进行多次循环尝试,性能就会受到影响.
在竞争激烈时一般会采用热点分离的方案来提高性能.
1.减少锁粒度
2.类似于concurrentHashMap,采用分段的形式将热点数据分离.

LongAdder原理: 将核心数据value分离成一个数组,每个线程访问时,通过hash算法映射到其中一个数字进行计数,value被分离成多个单元(cell),每个cell独自维护内部的值,当前对象的实际值由所有的cell累计合成

clipboard.png
实际操作中,LongAddr并不是一开始就动用数组进行处理,二是将所有数据都先记录在一个base变量中.一旦发生base变量的修改冲突,就会初始化cell数组,使用新策略,如果发现某一个cell上的更新依旧发生冲突,那么系统就会尝试创建新的cell或者将cell数量加倍,以减少冲突可能.

LongAdder的增强版:LongAccumulator

LongAdder只是每次对给定的整数执行一次加法,LongAccumulator可以实现任意函数操作.

//构造函数第一个参数是需要执行的二元函数(接收两个long型参数并返回long),第二个参数是初始值
public LongAccumulator(LongBinaryOperator accumulatorFunction,long identity)
public class LongAccumulatorDemo {
    public static void main(String[] args) throws Exception {
        LongAccumulator accumulator = new LongAccumulator(Long::max, Long.MIN_VALUE);
        Thread[] ts = new Thread[1000];
        for (int i = 0; i < 1000; i++) {
            ts[i] = new Thread(() -> {
                Random random = new Random();
                long value = random.nextLong();
                accumulator.accumulate(value);
                System.out.println(value+"::"+accumulator.longValue());
            });
            ts[i].start();
        }
        for (int i = 0; i < 1000; i++) {
            ts[i].join();
        }
        System.out.println(accumulator.longValue());
    }
}

ConcurrentHashMap的增强

foreach操作

clipboard.png

reduce操作

reduce操作对map的数据进行处理的同事会将其转为另外一种形式

clipboard.png

public class ReduceDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        for (int i = 1; i <= 100; i++) {
            map.put(Integer.toString(i), i);
        }
        int count = map.reduceValues(2, (i, j) -> i + j);
        System.out.println(count);
    }
}
上述方法是对concurrenthashmap中所有value求和
第一个参数表示并行度,一个并行任务可以处理的元素个数(估算值),如果设置为Long.MAX_VALUE,则表示完全禁用并行,设置为1表示使用最大可能并行.

条件插入

当元素不存在时需要创建并且将对象插入Map中,而当Map中已经存在该元素时,则直接获得当前在Map中的元素,从而避免多次创建.
concurrentHashMap.computeIfAbsent(key,k->new HeavyObject());

search操作

ConcurrentHashMap可以做并发搜索

clipboard.png

int found=map.search(2,(str,i)->{
    if(i%25==0){
        return i;
    }
    return null;
})

其他方法

mappingCount():返回map中的条目总数,返回long型,该方法不一定精准
newKeySet():返回一个高效并发的HashSet,该方法是一个静态工厂方法,返回一个线程安全的Set.

发布与订阅模式

JDK9中的内容,目前暂不讨论......
待续....


AshShawn
6 声望3 粉丝