JUC并发编程(二)

130 阅读5分钟

前言:

并发编程第二篇。主要为技术类原理讲解。

拓展:新时代程序员必会四大件:Lamda表达式链式编程Stream流式计算函数式接口

一、池化技术

线程池的好处:降低资源消耗(创建销毁耗时严重)、方便管理(线程复用,控制并发线程数等)、提高响应速度。

两种创建方式:Executors(不推荐)、ThreadPoolExcutor

  1. Executors 三大方法

三大方法是对ThreadPoolExecutor的封装,分别为单线程固定数目线程弹性线程池

  • newSingleThreadExecutor();
  • newFixedThreadPool(int);
  • newCachedThreadPool(,);
  1. 七大参数

说明:在实际工作中,我们不会使用Executors工具类创建线程池,因为他的弹性线程池最大参数太大(2^31-1),反而不安全(容易造成内存泄漏)。取而代之的是自己编写底层实现 ThreadPoolExecutor方法。下面是对该方法的剖析:

  • 源码分析

可看出Executors创建线程池的底层是 ThreadPoolExecutor image.png

七大参数: image.png

  • ThreadPoolExecutor 的工作原理

原理:类似银行排队问题。core即现开启线程资源数,max即最大数但max-core个尚未开启。当core占用完后,后来线程会先放在阻塞队列中而不会直接给max - core执行。阻塞队列满后,后来线程再交予其执行。倘若银行所有资源都占用了,银行有四种解决策略: 拒绝并报错拒绝不报错哪来回哪去(如让main线程执行)尝试与最早线程竞争(不报错)

max 的选择根据: 1.CPU密集型2.IO密集型。前者保证CPU效率最高(核数),后者一般为任务中耗 IO 线程数的 2 倍。

二、四大函数式接口

  1. Function()函数式接口
  2. Predicate()断定型接口 (返回布尔值)
  3. Supplier()供给型接口 (只返回不输入)
  4. Consumer()消费者接口 (只输入不返回)
String (String str)->{return str;} //Function接口的Lamda表达式表示,其他三个同理

三、Stream流式计算

思想:集合管存储,计算交给流(强烈依赖函数式接口)。使用场景例如下题:

题目:用一行代码实现对Users的筛选,条件如下:

1.ID为偶数 2.大于23岁 3.名字大写 4.倒序 5.只输出一个用户名

List<User> list = Arrays.asList(u1,u2,u3,u4,u5);
list.stream().filter(u->{return u.getId%2==0;})
             .filter(u->{return u.getAge>23;})
             .map(u->{u.getName().toUpperCase()});
             .sorted((uu1,uu2)->{return uu1.compareTo(uu2));
             .limit(1)
             .forEach(System.out::println); 
//最后一句系方法引用,可理解为lamda的语法糖。函数式接口的输入和println()方法接受的参数类型一致->简写

四、ForkJoin

ForkJoin是在大数据量时通过并发提高效率的技术,隐藏问题有工作窃取问题:当一个线程完成任务后会将其他线程未完成的任务拿来做来提高效率,但是两者可能出现任务争夺问题。

369等程序员对于0-10亿相加的处理方式:普通方式(14s)、ForkJoin方式(10s)、并发Stream流方式(150ms)

/**
 *parallel:并发Stream流 ;
 *rangeClosed:闭区间;
 *reduce:以什么方式返回结果
**/
LongStream.rangeClosed(0L,10_0000_0000L),parallel().reduce(0,Long::sum);

五、异步回调

Future 类,返回未来结果的建模。常用其子类CompletableFuture。参照JS hook函数理解。详细用法参见源码。

六、引出 volatile

JMM 即 java 内存模型,JMM有几条同步约定

  • 加锁前必须将主存中变量的最新值读入工作内存
  • 解锁前将共享变量值立刻刷入主存
  • 加锁解锁为同一把锁

其工作原理如下(线上对应八种操作):

image.png

实际工作中,试想,如果 B 刷新了 Flag 的值,而 A 线程不会立刻得到它而可能导致一些问题。此时就要使用一些措施来管控,告诉 A 线程 Flag 已经刷新,Volatile 是轻量级的 Synchro-nized,非常适合这种情况。下面探讨一下 volatile 的特性:

  • 保证可见性(及时告知)
  • 不保证原子性 (Synchronzied或者Lock锁会保证)

如何用解决? JUC 原子类(unsafe 在内存中修改值,CAS并发原语,高效率)

  • 禁止指令重排(频率很小但是逻辑上必有)

源代码的执行过程会经历各种重排:源代码->编译器的优化重排->指令并行的优化重排->内存系统重排->执行,例如源代码顺序为1234,而最终执行指令顺序为2314。编译器也有依赖策略,例如先定义后才能使用。

volatile 避免指令重排的方式:内存屏障。(Volatile写会将其上下加屏障,禁止上下指令顺序的交换)

七、单例模式 - volatile使用最多

对象只有一个,构造方法私有化。

  1. 饿汉式单例模式
  2. 懒汉式单例模式

边用边创建(并发下单例不唯一问题)-> 解决(DCL懒汉式)+ 防止指令重排(volatile,因为new 操作不是原子性操作)

  1. 反射破坏单例模式
  2. 枚举防止单例模式被反射破坏的原理

八、CAS

CAS(java): AtiomicInteger::compareAndSet(expect,update) ,值达到expect就更新否则返回false。(是CPU的并发原语,内存修改,效率很高)

AtinucUbteger atomicInteger = new atomicInteger(2020);//初始值
atomicInteger.compareAndSet(2020,2021);               //达到就更新

Unsafe类: Java操作C++底层的后门,很多native方法的集合

CAS缺点:

  • 自旋锁循环耗时
  • 一次性,只能保证一个共享变量的原子性
  • ABA问题(狸猫换太子)

ABA问题:A线程不知情的情况下,B线程将目标值修改后又改回去。解决方法:

  • Mysql: 乐观锁解决
  • Java:原子引用 (版本号控制,乐观锁原理)

九、各种锁的总结

  1. 公平、非公平锁 (能否插队)
  2. 可重入锁(递归锁,拿到外锁自动获得所有子锁)
//synchronized版、lock版
public synchronized void sms(){
    //lock.lock();
    System.out.println("屋");
    call();
    //lock.unlock();
}
public synchronized void call(){
    //lock.lock();
    System.out.println("里屋");
    //lock.lock();
}
  1. 乐观锁与悲观锁
  2. 自旋锁 (spinlock、原子引用实现)
  3. 死锁

死锁的排查方法:

  • 日志
  • 查看堆栈信息:jps -l (查看进程号)、jstack PID (查看堆栈信息)