Java高并发(六)-Java8/9/10

1 Java8的函数式编程

2 函数式编程基础

2.1 函数式编程特点

  1. 函数可作为返回值
  2. 限制副作用:限制修改函数外部的状态的途径
     显式函数:函数与外界交换数据的唯一渠道是参数返回值
     隐式函数:除参数返回值外,还会读取或改变外部信息
  3. 声明式:操作被封装到程序库中,不需要指定明确的执行语句,只需声明需求
    	public void imperative() {
    		int[] iArr= {1,3,4,5,6,2};
    		for(int i=0;i<iArr.length;i++) {
    			System.out.println(iArr[i]);
    		}
    	}
    	public void declarative() {
    		int[] iArr= {1,3,4,5,6,2};
    		Arrays.stream(iArr).forEach(System.out::println);
    	}
    
  4. 不变对象:传递的对象不会轻易修改
  5. 易于并行:对象处于不变状态,不用考虑一致性问题
  6. 代码简洁

2.2 FunctionalInterface注释

FunctionalInterface用于注释函数式接口(只有单一抽象方法的接口)

@FunctionalInterface
public static interface IntHandler{
	void handle(int i);
}

FunctionalInterface的作用
强制检查一个接口是否符合函数式接口的规范,否则产生编译错误(l类似@override)

函数式接口的判定

  1. 函数式接口的两个关键:单一抽象(java8允许接口中存在实例方法)
  2. 接口中任何被Object实现的方法不能视为抽象方法
    //不是函数式接口
    interface NonFunc {
    	boolean equals(Object obj);
    }
    

2.3 接口默认方法

  • Java8之前,接口只能包含抽象方法
  • Java8开始,接口可以包含实例方法

接口默认方法实现
使用default关键字在接口中定义实例方法

public interface IHorse{
	void eat();
	default void run(){
		System.out.printIn("hourse run");
	}
}

默认接口方法实现类多继承功能
在这里插入图片描述
类似多继承,Mule同时具有IHorse和IAnimal的实例方法

默认接口方法解决多继承的问题
在这里插入图片描述
当类实现的接口中有多个相同的default方法,实现类必须重写该方法.

public class Mule implements IHorsefIDonkey,lAnimal{
	@Override
	public void run(){
		IHorse.super.run();//沿用IHorse的run()
	}
	@Override
	public void eat() {
		System.out.println('Mule eat');
	}
}

Comparator接口新增方法

  1. 再次比较方法
    default Comparator<T> thenComparing(Comparator<? super T> other) {
        Objects.requireNonNull(other);
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }
    
  2. 字符串长度排序,继而字符顺序排序的比较器
    Comparator<String> cmp = Comparator.comparinglnt(String;:length).thenComparing(String.CASE_INSENSITIVE_ORDER);
    

3 Lambda表达式与方法引用

https://mp.youkuaiyun.com/mdeditor/103108043#

4 stream(流)

Stream就像一个高级版本的迭代器,用户对其包含的元素制定需要执行的操作,Stream会在内部隐式进行遍历,做出相应的数据转换,再执行操作。
参考文章
非常感谢https://www.jianshu.com/p/eb331282f2f3等一系列文章
非常感谢https://blog.youkuaiyun.com/qq_28410283/article/details/81052881等一系列文章
在这里插入图片描述

4.1流的基本知识

流的分类

  1. stream顺序流
    流中元素顺序与数组元素相同
  2. parallelStream并行流
    将原来的流分为多束流返回,用于并行操作
  3. unorderedStream无序流
    将原来的流中元素打乱后返回新的流

流的操作

  1. 终端操作

    Iterator<T> iterator();//返回迭代器
    Spliterator<T> spliterator();//返回spliterator
    

    消耗流,并产生一个结果

  2. 中间操作

    S sequential();//返回顺序流
    S parallel();//返回并行流
    S unordered();//返回无序流
    

    消耗流,并产生新的流

中间操作

  1. 无状态操作
    处理流中的元素时,会对当前的元素进行单独处理
  2. 有状态操作
    元素的处理可能依赖于其他元素,如最值(并行操作需要多次处理)

4.2 流操作

4.2.1 流的常用操作

流元素操作

Stream<T> filter(Predicate<? super T> predicate);//筛选复合条件的流元素
boolean test(T t);

<R> Stream<R> map(Function<? super T, ? extends R> mapper);//对流元素进行映射
R apply(T t);

Stream<T> distinct();//流元素唯一

Stream<T> sorted(Comparator<? super T> comparator);//流元素排序
int compare(T o1, T o2);

Stream<T> limit(long maxSize);//截断流

void forEach(Consumer<? super T> action);//消费流元素
void accept(T t);

类型转换

1. list->ref_arr
Integer[] irs=l.stream().toArray(Integer[]::new);
Integer[] irs= l.toArray(new Integer[0]);
2. list->arr
int[] is=l.stream().mapToInt(Integer::valueOf).toArray(int[]::new);
3. arr->ref_arr
int[] is = Arrays.stream(irs).mapToInt(Integer::valueOf).toArray();
4. arr->list
List<Integer> list1 = Arrays.stream(arr).boxed().collect(Collectors.toList());
5. ref_arr->list
List<Integer> l= Arrays.asList(irs);
6. ref_arr->arr
Integer[] irs = Arrays.stream(is).boxed().toArray(Integer[]::new);

4.2.2 流的缩减操作

https://www.jianshu.com/p/7c76d1940ea9
将流缩减为一个值,是终端操作(min()/count()等)

通用的缩减操作:reduce

public interface Stream<T> extends BaseStream<T, Stream<T>> {
...
T reduce(T identity, BinaryOperator<T> accumulator);
Optional<T> reduce(BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

代码分析

  1. reduce(base,(a,b)->fun(a,b))
    最初,base作为a,流中第一个元素作为b
    fun的返回值作为a,流中下一个元素作为b,直到结束
    int[] arr={1,2,3,4,5,6};
    System.out.println(Arrays.stream(arr).reduce(0,(a,b)->a+b);//求和结果21
    System.out.println(Arrays.stream(arr).reduce(1,(a,b)->a*b);//求积结果720
    
  2. reduce((a,b)->fun(a,b))
    最初,流中第一个元素作为a,第二个元素作为b
    fun的返回值作为a,流中下一个元素作为b,直到结束

缩减操作的约束
3. 无状态:元素的处理不依赖其他元素
4. 不干预:操作不会影响原始数据
5. 关联性(结合性):最终结果与数据操作的顺序无关(便于并行)

4.2.3 流的收集操作

原文链接https://blog.youkuaiyun.com/qq_28410283/article/details/81052881

<R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);//常用

stream中的collect操作可以使用Collectors静态工厂类中的方法
在这里插入图片描述

4.3 并行流

并行流的获取

  1. Collection接口提供的parallelStream()
  2. Stream接口提供的parallel()

并行流中的reduce

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
List<Integer> lists = new ArrayList<>();
lists.add(1);
lists.add(2);
lists.add(3);
Integer product = lists.parallelStream().reduce(1, (a, b) -> a * (b * 2),
                                                   (a, b) -> a * b * 2 );
    累加器部分(水平向右)
        accumulator
-----------------------------›
thread-1:   1 * 1 * 2   =   2          |   合并器方向(竖直向下)
thread-2:   1 * 2 * 2   =   4  *  2    |         combiner
thread-3:   1 * 3 * 2   =   6  *  2    |   因此最终的答案是2  *4  *  2*6  *  2=   192

代码解析

  1. 并行reduce只有Stream类才有
  2. identity只作用在accumulator上,不作用在combiner上

并行排序

Arrays.parallelSort(T[],Comparator<T>);
原始类型数组在并行排序时,不能指定比较器

并行赋值

public static void setAll(int[]! array, IntUnaryOperator generator)

Random r=new Random();
Arrays, parallelSetAll (arr, (i)->r.nextlnt()));//给元素赋随机值

5 增强的Future:CompletableFuture

参考文章
感谢https://www.cnblogs.com/happyliu/archive/2018/08/12/9462703.html

5.1 CompletableFuture介绍

Future接口的局限性
只能处理单任务,不能组合多个任务

  1. 将多个异步计算合并为一个
  2. 等待 Future 集合中的所有任务都完成。
  3. 仅等待 Future 集合中最快结束的任务完成,并返回它的结果。

CompletableFuture类图
在这里插入图片描述

  • CompletionStage中所有方法都有普通模式和异步模式(方法后+Asyn)
  • CompletionStage可以在方法中指定执行的Executor,默认为系统公共的ForkJoinPool.common线程池
    ForkJoinPool,commonPool()获取公共ForkJoin线程池,所有线程为Daemon线程

5.2 CompletableFuture使用

流式调用

public static Integer calc(Integer para) {
	try {
		// 模拟一个长时间的执行
		Thread.sleep(para*10);
	} catch (InterruptedException e) {
	}
    return para*para+1/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();
}
输出:"2500"

组合调用

  1. or

    public static void main(String[] args) throws InterruptedException, ExecutionException {       
    	CompletableFuture<Void> fu=CompletableFuture.supplyAsync(() -> calc(50))
          .applyToEither(CompletableFuture.supplyAsync(() -> calc(10)),(x)->x)
          .thenAccept(System.out::println);
        fu.get();
    }
    输出:100
    
  2. and

    public static void main(String[] args) throws InterruptedException, ExecutionException {       
        CompletableFuture<Void> fu=CompletableFuture.supplyAsync(() -> calc(50))
              .thenAcceptBoth(CompletableFuture.supplyAsync(() -> calc(10)),(x,y)->System.out.println(x+","+y));
        fu.get();
    }
    输出:2500,100
    

异常处理

public static void main(String[] args) throws InterruptedException,ExecutionException {
	CompletableFuture<Void> fu = CompletableFuture
		.supplyAsync(() -> calc())
		.exceptionally(ex -> {
			System.out.println(ex.toString());
			return 0;
		})
	fu.get();
}
输出: java.lang.ArithmeticException: / by zero

timeout(JDK9)

public static void main(String[] args) {
	CompletableFuture<Integer> fu=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);
    fu.get();
}
输出:超时异常

6 读写锁改进:StampedLock

参考文章https://segmentfault.com/a/1190000015808032?utm_source=tag-newest

6.1 StampedLock介绍

ReentrantReadWriteLock的不足

  • 读写锁虽然分离了读和写的功能,读读之间可以并发,但是读写之间冲突
  • 读锁会完全阻塞写锁,且使用的是悲观策略
  • 在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”

StampedLock官方示例

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);
        }
    }

    /**
     * 使用乐观读锁访问共享资源
     * 注意:乐观读锁在保证数据一致性上需要拷贝一份要操作的变量到方法栈,并且在操作数据时候可能其他写线程已经修改了数据,
     * 而我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
     *
     * @return
     */
    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访问模式

  1. Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
  2. Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
  3. Optimisticreading(乐观读模式):这是一种优化的读模式。

Optimistic Reading模式
“Optimistic reading”乐观读锁,使得读写可以并发执行,但是“Optimistic reading”的使用必须遵循以下模式:

double distanceFromOrigin() {
	long stamp = lock.tryOptimisticRead();  // 非阻塞获取版本信息
	copyVaraibale2ThreadMemory();           // 拷贝变量到线程本地堆栈
	if(!lock.validate(stamp)){              // 校验
   		long stamp = lock.readLock();       // 获取读锁
    	try {
        	copyVaraibale2ThreadMemory();   // 拷贝变量到线程本地堆栈
     	} finally {
       		lock.unlock(stamp);              // 释放悲观锁
    	}
	}
	useThreadMemoryVarables();              // 使用线程本地堆栈里面的数据进行操作
}

StampedLock特点

  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
  3. StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
  4. StampedLock支持读锁和写锁的相互转换
    RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
  5. StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
  6. StampedLock锁不支持Conditon等待

6.2 StampedLock原理

StampedLock 的内部实现是基于 CLH 锁的。 CLH 锁是一种自旋锁,保证没有饥饿FIFO 的服务顺序。

CLH基本思想

  1. 锁维护一个等待线程队列, 所有申请锁但是没有成功的线程都记录在这个队列中。

  2. 每一个节点( 一个节点代表一个线程) 保存一个标记位 (locked) ,用于判断当前线程是否己经释放锁
    获取锁时,检查前节点locked==false?
    释放锁时,修改自身locked=false
    在这里插入图片描述

  3. 一个线程试图获得锁时, 取得当前等待队列的尾部节点作为其前序节点, 并判断前序节点是否己经成功释放锁.若节点长时间不能获取锁,则挂起该线程.

    while (pred.locked) {}
    

StampedLock结构

  1. 等待队列结构
    /** Wait nodes */
    static final class WNode {
        volatile WNode prev;
        volatile WNode next;
        volatile WNode cowait;    // list of linked readers
        volatile Thread thread;   // non-null while possibly parked
        volatile int status;      // 0, WAITING, or CANCELLED
        final int mode;           // RMODE or WMODE
        WNode(int m, WNode p) { mode = m; prev = p; }
    }
    /** Head of CLH queue */
    private transient volatile WNode whead;
    /** Tail (last) of CLH queue */
    private transient volatile WNode wtail;
    
  2. state字段
    state倒数第8位 为1,表示写锁占用;
           为0,后7位表示当前读取线程数量;
    private transient volatile long state;
    
  3. 乐观读
    	public long tryOptimisticRead() (
    		long s;
    		return (((s = state) & WBIT) OL) ? (s & SBITS) : OL;
    	}
    
  4. 乐观读后,有线程申请写锁
    public long writeLock() {
    	long s, next;  // bypass acquireWrite in fully unlocked case only
    	return ((((s = state) & ABITS) == 0L &&
        	    U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?//写锁位设置为1
                next : acquireWrite(false, 0L));
        }
    
    writeLock()会将写锁位设置为1,乐观锁validate时,会检查这个改动
    public boolean validate(long stamp) {
        U.loadFence();
        return (stamp & SBITS) == (state & SBITS);
    }
    
    乐观锁失败后,提升为悲观读锁
    public long readLock() {
        long s = state, next;  // bypass acquireRead on common uncontended case
        return ((whead == wtail && (s & ABITS) < RFULL &&
                 U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?
                next : acquireRead(false, 0L));
    }
    
    若readLock成功,会将state+1,否则调用acquireRead()
    acquireRead()方法的工作流程(acquireWrite()类似)
    自旋失败
    自旋成功
    自旋失败
    自旋成功
    线程CAS获取锁
    加入CLH队尾自旋
    激活读线程
    park挂起线程
    激活读线程

StampedLock陷阱

  • StampedLock 内部实现时, 使用类似于 CAS 操作的死循环反复尝试的策略。
  • 在挂起线程时, 使用的是 Unsafe.park()函数, 而 park()函数在遇到线程中断时, 会直接返回(不会抛出异常
  • 线程被中断后,再次进入循环,导致大量CPU占用(尤其是多个阻塞线程同时被中断时)

7 原子类增强

7.1 更快的原子类:LongAdder(Java8)

Atomiclnteger 的缺陷

  • 在竞争不激烈时,修改成功概率高,远超于锁的性能
  • 竞争激烈时,大量修改失败导致多次循环尝试,性能受到影响

LongAdder核心思想
在这里插入图片描述

  • 当多个线程对value进行更新时,会对cells数组中对应的Cell的value进行更新.
  • 如果两个线程对同一个Cell操作,则会引以cells数组扩容
  • 在获取value时,需要对base和各个Cell进行sum求和

LongAdder主要方法

  1. add
    public void add(long x) {
        Cell[] as; long b, v; int[] hc; Cell a; int n;
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            if ( as == null || (n = as.length) < 1 ||
                (a = as[getProbe() &m]) == null ||
                !(uncontended = a.cas(v = a.value, v + x)))
                longAccumulate(x, hc, uncontended);
        }
    }
    
    • 若cells为空,说明没有冲突(竞争不激烈),直接对base进行CAS修改
    • 竞争激烈时,对cells数组中的Cell修改
n
成功
失败
y
y
n
y
n
存在
成功
不成功
不存在
cells不为空?
cas修改base
return
cells为空?
longAccumulate
cells容量为0?
计算cells数组索引
CAS修改索引对应Cell
  1. longAccumulate
    在这里插入图片描述
  2. sum
    public long sum() {
        long sum = base;
        Cell[] as = cells;
        if (as != null) {
            int n = as.length;
            for (int i = 0; i < n; ++i) {
                Cell a = as[i];
                if (a != null)
                    sum += a.value;
            }
        }
        return sum;
    }
    
    sum求值没有对base和cells进行同步,可能存在一致性问题

Cell结构

@sun.misc.Contended
static final class Cell {
    volatile long p0, p1, p2, p3, p4, p5, p6;
    volatile long value;
    volatile long q0, q1, q2, q3, q4, q5, q6;
    Cell(long x) { value = x; }
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

声明了多个变量,解决伪共享问题

7.2 LongAdder功能增强:LongAccumulator

LongAccumulator是LongAdder的增强版.

  • LongAdder只支持加减运算
  • LongAccumulator支持任意函数操作

构造函数

public LongAccumulator(LongBinaryOperator accumulatorFunction,long identity)
  • accumulatorFunction是二元函数
  • identity是初始值

示例

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);
        });
        ts[i].start();
    }
    for (int i = 0; i < 1000; i++) {
        ts[i].join();
    }
    System.out.println(accumulator.longValue());
}
输出随机数中最大值
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值