2020-06-18 Java并发编程-----基础-----JUC

threadpool - 线程池

理解为计划经济,资源总量被控制,减少不熟练劳动力带来的过渡开销问题

应用场景
    服务器接收大量的请求的时候
    实际开发中需要创建5个以上的线程,就可以使用线程池来进行管理 
创建和停止
    添加线程规则
        1.如果线程小于corePoolSize,即使其他工作线程处于空闲状态,也会创建一个新的线程来运行新任务
        2.如果线程数等于或者大于corePoolSize但少于maximumPoolSize,则将任务放入队列
        3.如果队列已经满,并且线程数小于maxPoolSize,则创建一个显得线程来运行任务
        4.如果队列满了,并且线程数大于或等于maxPoolSize,则拒绝该任务
    增减线程的特点
        1.通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池
        2.线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它
        3.通过设置maximumPoolSize.MAX_VALUE,可以允许线程池容纳任意数量的并发任务
        4.是只有在队列填满时次啊创建多于corePoolSize的线程,所以如果你使用的是无界队列(LinkedBlockingQueue),那么线程数就不会超过corePoolSize
    构造函数的参数 6个 
        - corePoolSize (添加线程规则,增减线程的特点)
        - maxPoolSize (添加线程规则,增减线程的特点)
        - keepAliveTime (如果线程池当前的线程数多于corePoolSize,那么如果多余的线程空闲时间超过KeepAliveTime,他们就会被终止)
        - workQueue (添加线程规则,增减线程的特点)
        - ThreadFactory (新的线程是由ThreadFactory创建的,默认使用的Executors.defaultThreadFactory(),已经够用了,创建出来的线程都在同一个线程组,优先级为5,非守护线程[可以设置:线程名,线程组,优先级,是否为守护线程])
        - handler 拒绝策略
    workQueue : 
        直接交换 SynchronousQueue (任务不多,只是通过队列进行简单的中转,存不下任务的,maxPoolSize要设置的大一点,没有队列缓冲,防止频繁创建新的线程)
        无界队列 LinkedBlockingQueue (处理的速度一定要大于任务提交的速度,否则会内存浪费或者OOM异常)
        有界队列 ArrayBlockingQueue (maxPoolSize就会更有意义,当对列满了之后,就会常见新的线程)
    JDK自带的线程池
        newFixedThreadPool corePoolSize=maxPoolSize keepAliveTime=0L(因为不会有线程被回收)时间单位为ms LinkedBlockingQueue - 会产生请求堆积(大多数请求的执行时间过长或者请求暴增),造成占用大量的内存,可能会造成OOM
        newSimgleThreadPool corePoolSize=maxPoolSize=1 keepAliveTime=0L(因为不会有线程被回收)时间单位为ms LinkedBlockingQueue    
        newCacheThreadPool corePoolSize=0, maxPoolSize=Integer.MAX_VALUE,keepAliveTime=60L, TimeUnit.SECONDS, new SynchronousQueue(),可回收,无缓存队列,因为maxPoolSize=Integer.MAX_VALUE,会创建非常多的线程,造成OOM
        newScheduledThreadPool corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,new DelayedWorkQueue()
        1.8加入的 workStealingPool 常用与递归执行的场景,子任务的线程池,窃取(子任务队列独立,子任务队列里面的任务可能会被他的其他空闲线程去执行),不保证执行顺序
    手动创建还是自动创建
        手动
    线程池里的线程数量设定为多少比较合适 
        CPU密集型(加密,计算hash等):最佳线程数为CPU核心数的1-2倍左右
        耗时IO型:(读写数据库,文件,网络读写等):最佳线程数一般会大于CPU核心线程数很多倍,以JVM线程监控显示繁忙情况为依据,保证线程空闲时间能接的上,参考Brain Goetz推荐的计算方法
            线程数=CPU核心数*(1+平均等待时间/平均工作时间)
        压测进行估计
    停止线程池的正确方法
        shutdown : 会把存量的任务(正在执行和队列里的)执行完,新的任务都会被拒绝 RejectedExecutionException
        isShutdown : 是不是调用了shutdown,用来后提交新任务前的判断
        isTerminated : 返回是不是线程池已经完全终止了
        awaitTermination : 测试一段时间内线程是否会完全停止的方法
        shutdownNow : 立刻把线程池关闭到,返回待执行的任务列表(这个参数用运用好)
    任务太多,怎么拒绝
         拒绝时机
            1 当Executor关闭时,提交新任务会被拒绝
            2 以及当Executor对最大线程和工作队列容量使用有限边界并且已经饱和时
         4种拒绝策略
            AbortPolicy 直接抛出异常RejectedExecutionException
            DiscardPolicy 直接把新提交的任务进行丢弃
            DiscardOldestPolicy 丢弃最老的任务
            CallerRunsPolicy 把新的任务给你提交的线程来跑
    钩子方法
        每个任务执行前后
        日志,统计
    实现原理
        线程池管理器 管理线程池的
        工作线程
        任务队列
        任务借口(Task)
    家族关系
        Executor(顶层接口) <———— ExecutorService(扩展管理接口) <---- AbstractExecutoeService <————  ThreadPoolExecutor     
        Executors(创建线程池的工具类)             
    重要概念
        线程池实现复用任务的原理 - 相同线程执行不同任务(runwork()-> 调用run)
        线程池状态
            RUNNING: 接收新任务并处理排队任务
            SHUTDOWN: 不接受新任务,但处理排队任务
            STOP: 不接受新任务,也不处理排队任务,并中断正在进行的任务
            TIDYING: 中文是简洁,所有任务都已经终止了,workerCount为0时,线程会跳转到TIDYING状态,并将运行terminate()钩子方法
            TERNINATED: terminate()运行完成
    注意点
        避免任务堆积
        避免线程过度增加
        排除线程泄漏
    ps:
        当解析文件的时候是会CPU飙升,采取的办法是每个线程休眠几ms,防止CPU异常飙升(多线程池时候应该如何考虑,不让CPU/IO全部跑满,也就是CPU/IO资源的合理分配)
        手写一个线程池
        概念的总结是基于源码的,要能在源码中进行复现

threadlocal

使用场景
    1. 每一个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormate和Random)
        每个Thread内有自己的实例副本,不共享
    2. 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
        解决同一个对象作为参数层层传递,代码冗余不易维护
        每个线程内要保存全局变量,可以让不同的方法直接使用,避免直接参数传递带来的麻烦
        用ThreadLocal保存一些业务内容(用户权限信息,从用户系统获取到的用户名,user ID等)
        这些信息在同一个线程内是相同的,但是在不同的线程使用的业务内容是不相同的
        当前用户信息需要被线程内所有方法共享
            使用UserMap,需要保证多线程问题,可以使用synchronized,也可以使用ConcurrentHashMap,但是无论用什么,都会对性能有影响
            强调的是同一个请求内(同一个线程内)不同方法的共享
            不需要重写initalValue()方法,但是必须手动调用set方法
            
作用
    1. 让某个需要用到的对象在线程间隔离(每个线程都拥有自己的独立对象)
    2. 在任何方法中都可以轻松获取到该对象
    选用时机
        根据共享对象的生成时机不同,选择initialValue或set来保存对象
            initialValue
                在ThreadLocal第一次get的时候就把对象给初始化出来了,对象的初始化时机由我们控制
            set
                需要保存到ThreadLocal里的对象生成设时机不由我们随意控制,例如拦截去生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用
好处
    达到线程安全
    不需要加锁,提高执行效率
    更高效的利用内存,节省开销,相比于每个任务都新建一个SimpleDataFormat,显然用ThreadLocal可以节省内存和开销
    免去传参麻烦,降低代码耦合度,更优雅
原理
    Thread -(1对1)-> ThreadLocalMap -(1对多)-> ThreadLocal
主要方法
    initialValue()
        1.该方法会返回当前线程对应的初始值,这是一个延迟加载的犯法,只要在调用get的时候,才会触发
        2.当线程第一次调用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
        3.每个线程最多调用一次此方法,但如果已经调用remove()方法后,再调用get(),则可以再次调用此方法
        4.如果不重写本方法,这个方法会返回null.一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象
    set()
        为这个线程设置一个新值
    get()
        得到这个线程对应的value,如果是首次调用get(),则会调用 initialize来得到这个值 
        先取出当前线程ThreadLocalMap,然后调用map.getEntry方法.把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value值
        这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中  
    remove()
        删除这个线程对应的值,单个threadLocal
核心组成
    ThreadLocalMap
        ThreadLocalMap类是每个线程Thread类里面的变量,开麦你最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对
            键: 这个ThreadLocal 值: 实际需要的成员变量,比如user或者simpleDateFormat对象
    冲突: HashMap
        ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个位置空位置,而不是用链表拉链,就找下一个空位置进行填补        
注意点
    内存泄漏
        对象没用,但是占用的内存却不能被回收
        ThreadLocalMap 的 kv 
            k 是使用弱引用(如果这个对象只是被弱引用关联,没有任何强关联,那么这个对象就可以被垃圾回收器进行回收的)的方式进行赋值的
            value 是强引用(定义的),会导致存在内存泄漏的可能性
        出现场景
            OOM:
            正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任务强占用了
            但是线程始终不终止,那么value就不能被回收,因为有以下的调用链: Thread -> ThreadLocalMap -> Entry(key为null) -> Value
            JDK会扫描key为null的Entry,并把对应的Value设置为null,这样value对象就可以被回收了 // HELP THE GC
            但是如果一个ThreadLocal不被使用了,set,remove,rehash方法不会被调用,线程又不会停止,就会导致value的内存泄漏
            规约:
                最后要调用remove方法,防止内存泄漏问题发生
            NPE:
                在get前要先进行set吗,为什么会有空指针异常
                控指针异常是在拆箱装箱时候会产生的,和ThreadLocal无关
            共享对象:
                如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()
                取得的还是这个共享对象本身,还是有并发访问安全问题    
            不要强行使用:
                如果对象很少的情况下就没必要用ThreadLocal来解决问题
            框架中已经有了就不需要自己去创造
                Spring中,如果可以使用RequestContextHolder,那么就不用自己维护ThreadLocal,因为自己会忘记调用remove(),造成内存泄漏
Spring中的实例分析
    DateTimeContextHolder类,看到里面用了ThreadLocal
    RequestContextHolder类,请求信息
    每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型场景
ps:
    有时候好的设计即可把复杂的问题解决
    threadlocal的设计就是 set赋值和初始值进行赋值的写法
    两个场景殊途同归,入口不同,但是结果都是相同的
    文件被占用考虑是什么情形,为什么360可以对文件进行检测占用和强制删除                   

lock - 锁

简介
    锁是一种工具,用于对共享资源的访问
    Lock并不是用来替代synchronized的,而是当使用synchronized不适合或者不足以满足要求的时候,来提供高级功能的
    Lock接口的实现类最常见的就是ReentrantLock
    通常情况下Lock只允许一个线程来访问这个共享资源的,不过有时候,一些特殊的实现也允许并发访问,比如ReadWriteLock里面的ReadLock
synchronized不太够用原因
    1. 效率低:锁的释放情况少,试图获取锁时不能设定超时时间,不能中断一个正在试图获得锁的线程
    2. 不够灵活,(读写锁更加灵活),加锁和释放锁的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
    3. 无法知道是否成功获取到了锁
Lock主要方法介绍
    lock()
        就是最普通的获取锁,如果锁已被其他线程获取,则进行等待
        Lock不会像synchronized一样在异常时候自动释放锁
        因此最佳实践是,在finally中释放锁,以保证发生异常时锁一定会被释放  
        lock方法不能被中断,这会带来很大的隐患,一旦陷入死锁,lock就会陷入永久等待  
    tryLock()
        用来尝试获取锁,如果当前所没有被其他线程占用,则获取成功,则返回true,否则返回false,代表获取锁失败
        相比于lock,这样的方法显然功能更加强大了,我们可以根据是否能获取锁来决定后续程序的行为
        该方法会立即返回,即便在拿不到锁时不会一直在那等    
    tryLock(long time,TimeUnit unit)
        超时就放弃
    lockInterruptibly()
        相当于tryLock(long time,TimeUnit unit),把超时时间设置为无限,在等待锁的过程中,线程可以被中断
    unlock()
        一定要配合try-finally使用,先写
可见性保证
    happens-before
    Lock加解锁和synchronized有同样的内存语义,也就是说,下一个线程加锁后可以看到所有前一个线程解锁发生的所有操作
    Lock,synchronized都是有这个能力的
锁的分类
    从不同的角度来看待锁的分类
    -- 乐观锁(非互斥同步锁)和悲观锁(互斥同步锁)
    互斥同步锁的劣势
        阻塞和唤醒带来的性能劣势
        永久阻塞:如果持有锁的线程被永久阻塞,比如遇到了无限循环,死锁等活跃性问题,那么等待该线程释放锁的那几个悲催的线程的线程,将永远得不到执行
        优先级反转
    -
    乐观锁的实现一般都是用CAS算法实现的
    乐观锁的核心就是执行完之后进行比较共享资源是否被修改了(ABA问题)
    -
    Java中悲观锁的实现是synchronized和Lock相关的类
    乐观锁的典型例子就是原子类和并发容器,git
    - 开销对比
    悲观锁的原始开销高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越差,也不会对互斥锁的开销造成影响
    相反,虽然乐观锁一开始开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
    - 使用场景
    悲观锁: 适合并发写入多的情况,适用于临界区持续时间比较长的情形,悲观锁可以避免大量的无用自旋等消耗,典型情况:
        1.临界区有IO操作
        2.临界区代码复杂或者循环量大
        3.临界区竞争非常激烈
    乐观锁:
        适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅度提高
    -- 可重入锁和非可重入锁,ReentrantLock
        字符串打印
        电影院
    - 可重入特性
        可重入锁又叫做递归锁,同一个线程同一把锁不需要重新获取
    - 好处
        避免死锁
        提升封装性
    - 源码对比
        可重入锁ReentrantLock及非可重入ThreadPoolExecutor的Worker类(具体的在AQS里面进行补充)
    - 其他方法  
        isHeldByCurrentThread 可以看出是否被当前线程持有
        getQueueLength可以返回当前正在等待这把锁的队列有多长
        一般这两个方法是开发和调试时候使用,上线后用的不多
    -- 公平锁和非公平锁
        公平指的是按照线程请求的顺序,来分配锁,非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队
        (非公平锁特不提倡进行插队,这里的非公平指的是在合适的时机进行插队,而不是盲目的进行插队)
        非公平锁是为了提高效率,避免唤醒线程带来的空档期
        ReentrantLock公平的时候,就需要把创建时的参数设置为true
    -特例
        针对tryLock()方法,它是很猛的,不遵守设定的公平的规则
        例如,当有线程执行tryLock()的时候,一旦有线程释放了锁,那么这个正在tryLock的线程就能获取到锁,即使它之前已经有其他在等待队列里的                  
    - 对比
        公平锁,每个线程都可以执行,劣势比较慢,吞吐量小
        非公平锁,更快,吞吐量大,有可能会产生线程饥饿,也就是某些线程在长时间内,始终不能执行
    -- 共享锁和排它锁
        排他锁,又称独占锁,独享锁
        共享锁,又称读锁,获取共享锁后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享锁,也可以查看但无法修改和删除数据
        共享锁和排它锁的典型是读写锁ReentrantReadWriteLock,其中读锁是共享锁,写锁是独享锁
    - 作用
        读没必要加锁,写才是需要加锁的,为了提高程序执行的效率
    - 规则
        要么是一个或多个线程同时读,要么是一个线程有写锁,但是两者不会同时出现(要么多读,要么一写)
    - 理解
        可以理解为一把锁的两种方式进行锁定,也可以理解为两把锁
    - 读写锁的交互方式
        选择规则,
        读线程插队,提高效率
        升降级
    - 读锁插队策略
        公平不允许插队     
        策略1:    
            读可以插队,效率高
            容易让想要获取写锁的线程造成饥饿        
        策略2:
            把其插入到队列里,等待写锁执行完
         非公平写锁可以随时插队,读仅在等待队列头不是想获取写锁线程的时候可以插队
    - 升降级
        写降级和读升级 - 支持锁的降级不支持升级,为什么不支持锁的升级:死锁(可以自定义保证同一时间只有一个升级就可以实现锁的升级)
    -- 自旋锁和阻塞锁
        让当前线程稍微等一下,当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必要阻塞而是直接获取同步资源,避免线程切换带来的开销
        阻塞锁在没拿到锁的情况下,就会直接把线程阻塞,直到被唤醒
    - 缺点
        如果锁占用的时间比较长,那么自旋锁的线程就会白白浪费处理器资源
    - 实现
        java1.5的concurrent的atmoic包下的类基本就是自旋锁进行实现的
        自旋锁的实现是CAS
        AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中其他线程竞争导致没修改成功,就在while里死循环,直至修改成功
    - 使用场景
        自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
        自旋锁适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久才会释放),那也不合适的                         
    -- 可中断锁
        synchronized是不可中断锁,Lock是可中断锁
    --- 锁优化
        JVM对锁的优化
            自旋锁和自适应,在一定时间内就转为阻塞锁,不会一直进行自旋
            锁消除,没必要加锁的,私有方法内的就会帮你去掉锁
            锁粗化,前后相邻的代码块使用的是同一个锁对象,把这几个合成一个较大的,就不需要反复的申请和释放锁了
        代码级别的优化
            缩小同步代码块
            尽量不要锁住方法 (锁代码块来代替方法)
            减少请求锁的次数 - 比如写数据库,利用中间件,然后从中间件中获取数据统一写入到数据库中
            避免人为制造热点 - 比如hashMap中的size(),自己维护一个计数,复杂度降为O(1)
            锁中不用包含锁
            选择合适的锁类型或合适的工具类
ps:
    核心就是算法,所以理解算法,可以给问题一个解决思路,也是开发的核心    

atomic - 原子类

不可分割
一个操作是不可中断的,即便是多线程的情况下也可以保证
java.util.concurrent.atomic
- 作用
原子类和锁类似,不过原子类相比于锁,有一定优势:
    粒度更细,变量级别的锁
    使用原子类的效率比使用锁的效率高,除了高度竞争的情况
- Atomic 基本类型
    AtomicInteger
    AtomicLong
    AtomicBoolean
    - API
        get() 获取当前的值
        getAndSet(int newValue) 获取当前的值,并设置新的值
        getAndIncrement() 获取当前的值,并且自增
        getAndDecrement() 获取当前的值,并且减
        getAndAdd(int data) 获取当前的值,并且加上预期的值
        compareAndSet(int expect, int update) 如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
- Atomic 数组类型
    AtomicIntegerArray 
    AtomicLongArray
    AtomicReferenceArray
    当有多个变量要批量操作的时候,用一个数组来存
- Atomic 引用类型
    AtomicReference
- 普通变量升级为Atomic功能的变量
    AtomicIntegerFieldUpdater 
    - 使用场景
        不是你进行声明的,可以进行升级
        偶尔需要一个原子的get-set操作
    - 注意点
        可见范围(不能被private修饰)
        不能被static修饰
- 累加器
    Adder
    Accumulator
    - Adder产生特性
        Java8引入的,相对比较新的
        高并发下LongAdder比AtomicLong效率高,不过本质是空间换时间
        竞争激烈的时候,LongAdder把不同线程对应下到不同的Cell上进行修改,降低了冲突的概率,是多段锁的概念,提高了并发性
        (因为AtomicLong每一次加法,都要flush和refresh,导致很耗费资源)
        (LongAdder,每一个线程有一个自己的计数器,仅用来在自己的线程内计数,这样一来不会和其他线程的计数器干扰)
        - 原理
            LongAdder引入了分段累加的概念,内部有一个base变量和一个Cell[]数组共同参与计数
            base变量,竞争不激烈的情况下,直接累加到该变量上
            Cell[]数组,竞争激烈,各个线程分散累加到自己的槽Cell[i]中(空间)
            sum()源码,先加base,再加Cell
        - 使用场景
            在低竞争下,AtomicLong和LongAdder这两个类具有相似的特征,但是在竞争激烈的情况下,LongAdder的预期吞吐量要高得多,但是消耗更多的空间
            LongAdder适合的场景是统计求和计数的场景,而且LongAdder基本只提供了add方法,而AtomicLong还具有cas方法
    - Accumulator
        - 使用场景
            多线程并行计算,比for循环的优势大
            不能有计算顺序,也不能有业务执行顺序     

CAS(cas) - 各种锁的底层原理

并发,CPU的特殊指令,不会出现线程安全问题
CAS有三个操作数,内存值V,预期值A,要修改的值B,并且仅当预期值A和内存值V相同时,才将内存值改成B,否则什么都不做,最后返回现在的V值
- 应用场景
    乐观锁
    并发容器
    原子类
        AtomicInteger加载Unsafe工具,用来直接操作内存数据
        用Unsafe来是实现底层操作
            Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地native方法来访问,不过尽管如此,JVM还是开了一个后门,JDK有一个类Unsafe,它提供了硬件级别的原子操作
            valueOffSet表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存便宜地址获取数据的原值的,这样我们就能通过unsafe来实现CAS了
        用valatile修饰value字段,保证可见性
        native - Unsafe-compareAndSwapInt
            Atomic::cmpxchg(x,addr,e) 实现了底层原子性的比较替换,x是即将更新的值,参数e是原内存的值,至此,最终完成了CAS的全过程
- 缺点
    ABA问题(重结果忽略过程)
        解决ABA问题,可以借鉴版本号来解决
    自旋时间过长
        用到了死循环,可能会有较长时间的CPU占用 

final - 不变应万变

不变性
    如果对象在被创建之后,状态就不能被修改,那么它就是不可变的
    不可变对象
        就是内部元素都不可以被外部访问修改到,具有不可变性的对象一定是线程安全的,不需要对他进行额外的措施,也能保证线程安全
- final 的作用
    final在早期是实现内嵌调用的,就是被一个类的方法拷到该方法下
        (其实现在是为了进行清晰的标识,JVM已经可以自动的进行final化的处理了)
    现在,类防止被继承,方法防止被重写,变量防止被修改
    天生是线程安全的,而不需要额外的同步开销        
- final 的3种用法
    修饰变量
        被final修饰的变量,意味着值不能被修改,如果变量是对象,那么对象的引用不能变,但是对象自身的内容依旧可以改变
        - 类中的final属性
            赋值时机(必须选择一种进行赋值)
            - 声明变量的等号右边直接赋值
            - 在构造函数中赋值 - 类似于现在的Spring的Bean构造器方式注入
            - 在类的初始化代码块中赋值(不常用) { }
        - 类中的static final属性
            赋值时机(必须选择一种进行赋值)
            - 声明变量的等号右边直接赋值
            - 还可以用static修饰的代码块进行赋值    
        - 方法中的final变量
            - 不规定赋值时机,但是要求在使用前必须及进行赋值,这和方法中的非final变量的要求也是一样的
    修饰方法
        - 修饰构造方法
            构造方法不允许我们用final修饰
            不可被重写,这个和static方法是一样的
                但是static可以在子类中有相同的静态方法定义,原因是static与类绑定,不是动态绑定的
    修饰类
        不可被继承,String
    总结
        final修饰对象时,只是对象的引用不可变,而对象本身的属性是可以变化的
        final使用规则,良好的编码习惯
不变性和final的关系
    不变性不意味着,简单的用final修饰就是不可变了
        对基本数据类型,确实被final修饰就具有不可变性了
        但是对于对象,需要该对象保证在自身创建后,状态永远不可变才可以
    如何利用final实现对象不可变
        把所有的属性都声明为final?
            内部有final修饰的引用,引用还存在有不是被final修饰的属性,就不满足了
        一个属性是对象类型的不可变对象
            private final Set xxx = new HashSet<>();
        满足以下条件才是不可变的
            对象创建后,其状态不可被修改
            所有属性都是final修饰的
            对象创建过程中没有发生溢出
        栈封闭
            把变量写在线程内部
        例子
            面试题 宏替换 编译器优化               
ps:
    赋值时机的设置合理性,如果你初始化不赋值,后续进行赋值,就从null变成了你的更改,就违反了final不变原则了

collections - 常见的并发容器

ConcurrentHashMap,CopyOnWriteArrayList,阻塞队列
- 过时的同步容器
    Vector和Hashtable
        Vector使用类似于List  所有方法都是synchronized  修饰的
        Hashtable使用类似于Map 所有方法都是synchronized  修饰的
   缺点:
        并发性能差
- 常见的容器变同步容器
    ArrayList和HashMap
        两个类不是线程安全的
            Collections.synchronizedList(new ArrayList())
            Collections.synchronizedMap(new HashMap())
            可以转成线程安全的
            使用同步代码块的方式来实现的
    缺点:
        性能还是较差
- 常见的并发容器    
    ConcurrentHashMap 和 CopyOnWriteArrayList
    - Map 
        实现
            (map 是根据key来进行设计的,所以对key的操作,都要灵活使用它的API)
            HashMap -> LinkedHashMap(顺序是插入的顺序)
            Hashtable(已经不使用了)
            SortedMap -> NavigableMap -> TreeMap(可以排序,默认是升序)
        为什么HashMap是线程不安全的?
            同时put碰撞会导致数据丢失
            同时put会导致数据丢失
            死循环造成的CPU100% ?
                - 主要存在于JAVA7中
                https://coolshell.cn/articles/9606.html
                在多线程扩容的时候可能会导致循环链表,产生CPU100%,这个问题的本质是HashMap没有用对地方,HashMap不支持多线程环境
        ConcurrentHashMap1.7实现
            Java7中的ConcurrentHashMap最外层是多个 segment,每个 segment 的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
            每一个segment独立上 ReentrantLock 锁,每个segment之间互不影响,提供了并发效率
            ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写,(操作分别分布在不同的Segment上),这个默认值在初始化的时候射为止其他值,但是一旦初始化,是不可以扩容的
        ConcurrentHashMap1.8实现(对1.7版本的完全重写 代码增加5000行)
            putVal流程
                判断key value不为空
                计算hash值
                根据对应位置节点的类型,来赋值,或者helptransfer,或者增长链表,或者给红黑树增加节点
                检查满足阈值就"红黑树化"
                返回oldVal
            get流程
                计算hash值
                找到对应的位置,根据情况进行:
                    直接取值
                    红黑树里找值
                    遍历链表取值
                    返回找到的结果
            核心:借鉴了1.8Map的实现,使用node来代替segment,扩容使用CAS和链表转红黑树或转移节点(降低了算法的复杂度)
        为什么要把1.7的结构改成1.8的结构?
            数据结构(提升并发度)
            Hash碰撞(对1.7的拉链法进行升级)
            保证并发安全(1.7是分段锁,1.8是CAS+synchronized)
            查询复杂度(On Ologn)
            为什么超过8转成红黑树?
                默认是链表的形式,所占用的内存更少
                基本不会遇到链表转成红黑树的情况,冲突为8次是千万分之一的概率(泊松分布)               
        错误使用ConcurrentHashMap也会造成线程不安全的情况?
            组合操作并不保证线程安全的
            用replace方法和while(true)进行优化,安全性保证
            putIfAbsent()相当于检查key是否存在再进行加操作
        实际案例
            司机答题打乱题目,造成题目乱序的问题
    - CopyOnWriteArrayList 
        - 介绍  
            代替Vector和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因一样
            Vector和SynchronizedList锁的粒度太大了,并发效率相对比较低,并且迭代时无法编辑
            Copy-On-Write并发容器还包括CopyOnWriteArraySet,用来替代同步Set
        - 适用场景
            读操作可以尽可能得快,而写即使慢一点也没有太大关系
                读多写少:黑名单,每日更新;监听器,迭代操作远多于修改操作
        - 读写规则
            回想读写锁,除了读读其他都是互斥的
            读写锁规则的升级:读取是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待
            解决传统ArrayList在迭代时候不能修改的场景,改和迭代不冲突,迭代的还是原来的值                         
        - 实现原理
            创建新副本,读写分离
            "不可变"原理,旧的不可变
            迭代的时候,数据可能已经过期,迭代的时候,取决于迭代对象的创建时间,不取决于开始迭代的时间    
        - 缺点
            数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上就能读到,马上能读到,请不要使用CopyOnWrite容器。
            内存占用问题:因为CopyOnWrite的是写的复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。
        - 源码分析
            用ReentrantLock加锁
            复制一份出来                                                  
- 并发队列 Queue: 阻塞队列(重点)
    - 简介
        在线程中传递数据:生产者消费者模式
        考虑锁等线程安全问题的重任从“你”转移到了"队列"上
    - 重要并发队列关系图(todo 这个需要再进行探究下)
        Queue
            SynchronousQueue(BlockingQueue)
            ConcurrentLinkedQueue
            BlockingQueue    
                ArrayListBlockingQueue
                PriorityBlockingQueue
                LinkedBlockingQueue 
    - 阻塞队列 BlockingQueue
        简介
            阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是阻塞功能
            通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的
        核心方法     
            take()方法:获取并移除队列的头结点,如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据
            put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间
        是否有界(容量有多大)
            这是一个非常重要的属性,无界队列意味着里面可能可以容纳非常多,(Integer.MAX_VALUE,约为2的31次方,是一个非常大的数字,可以近似认为是无限容量)
            阻塞队列和线程池的关系(阻塞队列是线程池的重要组成部分)        
        重要方法
            put,take
            add,remove,element
            offer,poll,peek
        重要实现
            - 阻塞并发队列
            ArrayBlockingQueue
                有界
                指定容量
                公平:可以指定是否需要保证公平,如果想保证公平的话,那么等待了最长时间的线程会优先被处理,不过这会同事带来一定的性能损耗
                生产者消费者的设计方式
            LinkedBlockingQueue
                无界
                容量 Integer.MAX_VALUE
                内部结构 Node 两把锁 分析put方法
            PriorityBlokingQueue
                支持优先级
                自然排序(而不是先进先出)
                无界队列(可以进行扩容)
                PriorityQueue的线程安全的版本
            SynchronousQueue
                容量为0
                因为SynchronousQueue不需要去持有元素,他所做的就是直接传递(direct handoff)
                效率很高
                没有peek等函数,因为不进行存储,也没iterate方法
                是一个极好的用来直接传递的并发数据结构
                Executors.newCachedThreadPool()使用的阻塞队列 
            DelayQueue
                延迟队列,根据延迟时间进行排序    
                元素需要实现Delayed接口,规定排序规则
            - 非阻塞并发队列
            ConcurrentLinkedQueue
                使用链表作为数据结构
                使用CAS非阻塞算法实现线程安全
                    offer方法的CAS思想,内有p.casNext方法,用了UNSAFE。compareAndSwapObject
                适合用在对性能要求比较高的并发场景,用的相对比较少一点
        如何选择适合自己的队列
            边界
                是否有边界
                    容量特别大,近似为无边界
                    容量不够,会进行自动扩容
            空间
                    ArrayBlockingQueue比LinkedBlockingQueue内存更加整齐
            吞吐量
                    LinkedBlockingQueue优于ArrayBlockingQueue,因为锁的粒度更加细致
            其他特殊场景
                    SynchronousQueue用于不存储,直接交换的场景
                    DelayQueue延迟排序场景
                    PriorityBlokingQueue自定义优先级的队列
    - 并发容器总结
        java.util.concurrent包提供的容器,分为3类:Concurrent*,CopyOnWrite*,Blocking*
            Concurrent*的特点是大部分通过CAS实现并发,而CopyOnWrite*则是通过复制一份原数据来实现,Blocking通过AQS实现                            
ps: 调试技巧
    修改JDK版本
    多线程配合 Thread - make default

process - 并发流程控制

-   大致了解
    -   Semaphore
        作用:信号量,可以通过控制"许可证"的数量,来保证线程之间的配合
        说明:线程只有在拿到"许可证"后才能继续运行,相比于其他的同步器,更灵活    
    -   CyclicBarrier
        作用:线程会等待,直到足够多的线程达到了事先规定的数目,达到触发条件,就可以进行下一步的动作
        说明:适用于线程之间相互等待处理结果就绪的场景
    -   Phaser
        作用:和CyclicBarrier类似,但是计数器可变
        说明:Java7加入的
    -   CountDownLatch
        作用:和CyclicBarrier类似,数量减到0时,触发动作
        说明:不可以重复使用
    -   Exchanger
        作用:让两个线程在合适时交换对象
        场景:当两个线程工作在一个类的不同实例上时,用于交换数据
    -   Condition
        作用:可以控制线程的"等待"和"唤醒"
        说明:是Object.wait()的升级版
-   CountDownLatch
    倒数门闩
    流程
        等待,倒数为0,才进行执行
    主要方法
        CountDownLatch(int count):仅有一个构造函数,参数count为需要倒数的数值
        await():调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
        countDown():讲count值减1,直到值为0,等待的线程会被唤醒
    用法
        用法一:一等多,一个线程等其他线程都执行完再开始执行
        用法二:多等一,多个线程等待某一个线程的信号,同时开始执行
        用法三:组合两个用法,进行全过程的描述
        扩展用法:多等多,多个线程等多个线程完成执行后,再同时执行
    例子
        拼团,人满发车
        同一时间起跑
        压测
    注意:
        CountDownLatch是不能够重用的,如果需要重新计数,可以考虑使用CyclicBarrier或者创建新的CountDownLatch实例
-   Semaphore
    概念
        信号量        
        可以用来限制或管理数量有限资源的使用情况
        许可证的概念
    使用流程
        1.初始化Semaphore并制定许可证的数量
        2.在需要被现在的代码前加acquire()或者acquireUninterruptibly()方法
        3.在任务结束后,调用release()来释放许可证
    主要方法
        new Semaphore(int permits,boolean fair):这里可以设置是否要使用公平策略,如果传入true,那么Semaphore会把之前等待的线程放到FIFO队列里,以便于当有了新的许可证,可以分发给之前等了最长时间的线程
        acquire()响应可以中断
        acquireUninterruptibly()不可以响应中断
        tryAcquire():看看现在有没有空闲的许可证,如果有的话就获取,如果没有的话也没关系,我不必陷入阻塞,我可以去做别的事,过一会再来查看许可证的空闲情况
        tryAcquire(timeout):和tryAcquire()一样,但是多了一个超时时间,比如"在3秒内获取不到许可证,我就去做别的事"
        release()        
    用法
        保护线程池中同时执行的线程数
            acquire()可以指定一个方法拿到的许可证数量,从而进行权重,acquire几个,release就要对应的释放几个   
        信号量特殊用法
            1.一个方法拿走权重的数量来进行控制方法是否会执行,从而规定方法的执行先后顺序和执行顺序权重
            2.除了控制临界区最多同时有N个线程访问外,另一个作用是可以实现"条件等待",例如线程1需要在线程2完成准备工作后才能开始工作,那么就线程1acquire(),而线程2完成任务后release(),这样的话,相当于是轻量级的CountDownLatch
    注意点
        获取和释放的许可证数量必须一致, 如果不一致,会造成程序卡死,写程序的规范
        注意在初始化Semaphore的时候设置公平性,一般设置为true会更合理,避免线程饿死
        获取和释放许可证对线程并无要求,也许是A获取了,然后由B释放,只要合理即可
-   Condition接口(又称为条件对象)        
    作用:
        当线程1需要等待某个条件的时候,他就去执行condition.await()方法,一旦执行了await()方法,线程就会进入阻塞状态
        通常会有另外一个线程,假设是线程2,去执行对应的条件,直到这个条件达成的时候,线程2就会去执行condition.signal()方法,这时JVM就会从被阻塞的线程里找,找到那些等待该condition的线程,当线程1就会收到可执行信号的时候,他的线程状态就会变成Runnable可执行状态
    signalAll()和signal()区别
        signalAll()会唤醒所有正在等待的线程
        signal()是公平的,只会唤起那个等待时间最长的线程
    使用
        普通示例
            用ReentrantLock创建Condition                        
            注释先用一个线程进行唤醒,然后再用另外一个进行阻塞,就是和正常的逻辑相反
        实现生产者和消费者模式
    注意点
        实际上,如果说Lock用来代替synchronized,那么Conidtion就是用来代替响应的Object.wait/notify的,所以在用法和性质上,几乎一样
        await方法会自动释放持有的Lock锁,和Object.wait一样,不需要自己手动先释放锁
        调用await的时候,必须持有锁,否则会抛出异常,和Object.wait一样
-   CyclicBarrier循环栅栏
    概念
        CyclicBarrier循环栅栏和CountDownLatch很类似,都能阻塞一组线程
        CyclicBarrier可以构造一个集合点,当某一个线程执行完毕,它就会到集结点等待,直到所有线程都到了集合点,那么该栅栏就会被撤销,所欲线程再统一出发,继续执行剩下的任务
    使用 
        可重用       
    CyclicBarrier和CountDownLatch区别
        作用不同:
            CyclicBarrier要等到固定数量的线程都到达了栅栏位置才能继续执行,而CountDownLatch只需要等待数字到了0,也可以说,CountDownLatch是基于事件,但是CyclicBarrier是用于线程的
        可重用性不同:
            CountDownLatch在倒数到0并触发门闩打开后,就不能再次使用了,除非创建新的实例,而CyclicBarrier可以重复使用

AQS - 并发灵魂人物

学习思路
    AQS的目的是想理解原理,提高技术,以及面对面试
    先从应用层面理解为什么需要他如何使用他,然后再看看我们的设计者如何使用它的,了解他的使用场景
    之后再去分析它的结构,这样我们就学习的更加轻松了
为什么需要AQS
    锁和写作类的共同点:闸门
    ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock都有类似的协作功能,他们底层都用到一个共同的基类,这就是AQS
    因为上面的那些协作类,他们有很多工作是类似的,所以如果能提取一个工具了类,那么就直接可以用了,对于ReentrantLock和Semaphore而言就可以屏蔽很多的细节,只关注他们的“业务逻辑”就可以了
Semaphore和AQS关系
    Semaphore内部有一个Sync类,Sync类继承了AQS(CountDownLatch,ReentrantLock也是类似的)
AQS帮我们做的(通用需求)
    同步状态的原子性管理
    线程的阻塞与解除阻塞
    队列的管理
作用
    AQS是一个用于构建锁,同步器,协作工具类的工具类(框架),有了AQS以后,更多的协作工具类都可以很方便得被写出来(标准和设计性能)
    有了AQS,线程的协作类构件就容易多了
重要性和地位
    (AbastractQueuedSynchronizer)是Doug Lea写的,从JDK1.5加入的一个基于FIFO等待队列实现的一个用于实现同步器的基础框架,相关的实现类有
AQS内部原理的解析
    核心(三个部分)
        state状态
            根据类的实现不同而有所区别
                在Semaphore里,表示"剩余的许可证数量",在CountDownLatch中"还需要倒数的数量",ReentrantLock(可重入计数)
                是volatile修饰的,会被并发的修改,所以所有修改state的方法都需要保证线程安全,比如getState,setState以及compareAndSetState    
        控制线程抢锁和配合的FIFO队列
            存放等待的线程的,AQS就是"排队管理器",当多个线程争用同一把锁的时候,必须有排队机制将那些没能拿到锁的线程串在一起,当锁释放的时候,锁管理器会挑选一个合适的线程来占用这个刚刚释放的锁
            AQS会维护一个等待的线程队列,把线程都放在这个队列里(双向链表)
        期望协作工具类去实现的获取/释放等重要方法
            写作类自己实现,含义各不相同
            -                   
            获取操作会依赖state变量,经常会阻塞(比如获取不到锁的情况)
            Semaphore - acquire - 获取一个许可证                      
            CountDownLatch - await - 等待,直到倒数结束
            -
            释放操作不会阻塞
            Semaphore - release - 作用是释放一个许可证
            CountDownLatch中- countDown- 作用是"倒数一个数"
AQS源码解析
    AQS用法
        1. 写一个类,想好协作方式,实现获取/释放方法
        2. 内部写一个Sync类继承 AbstractQueuedSynchronized            
        3. 根据是否独占tryAcquire/tryRelease或tryAcquireShared(int acquires)和tryReleaseShared(int release)等方法,在之前写的获取/释放方法中调用AQS的acquire/release或者Shared方法
    使用AQS实现一个自己的门闩(线程协作器)
        实现一个获取,释放的方法
        用state实现状态的标识

Future和Callable - 线程治理的第二大法宝

Runnable的缺陷
    不能返回一个返回值
    也不能抛出 checked Exception
    - 为什么有这样的缺陷?
        run方法
Callable接口
    有返回值
    能抛出异常
Future和Callable关系
    Future是一个存储器,它存储了call()这个任务的结果,而这个任务的执行时间是无法提前确定的,因为这完全取决于call()方法执行的情况
Future的主要方法
    get方法,取得运行的结果,行为主要取决于Callable任务的状态,只有以下5种情况:
        1.任务正常完成:get方法会立刻返回结果
        2.任务尚未完成(任务还没开始或进行中):get将阻塞并直到任务完成
        3.任务在执行过程中抛出Exception,不论抛出什么异常,最后抛出的是ExecutionException,异常抛出时机是get方法调用的时候才进行抛出
        4.任务被取消,get会抛出CancellationException
        5.任务超时,get方法会有一个重载方法,是传入一个延迟时间,如果时间到了还没有获得结果,get方法就会抛出TimeoutException
    cancel方法
        超时不获取,任务需要取消
        1.Future.cancel(true)适用于+
            任务有能力处理中断的
        2.Future.cancel(false)
            仅用于避免启动尚未启动的任务
            1.未能处理interrupt的任务
            2.不清楚任务是否支持取消
            3.需要等待已经开始的任务执行完成    
    isDown
        只是说任务是否执行完成了
    isCancelled
        是否被取消
主要用法
    用法1:线程池的submit方法返回Future对象
        我们给线程池提交任务,提交线程会立刻返回给我们一个空的Future容器。当线程的任务一旦执行完毕,也就是我们可以获取结果的时候,线程池就会把结果填入到我们刚才的那个Future容器中,
        而不是创建一个新的Future,我们此时就可以从Future中获得任务执行的结果。
    用法2:用FutureTask来创建Future
        是一种包装器,可以把Callable转换成Future和Runnable,它同时实现二者的接口
        既可以作为Rannable被线程执行,又可以作为Future得到Callable的返回值
        放到线程或者线程池中执行,都会get获取到结果
注意点
    1. 当for循环批量获取future结果的时候,容易发生一部分线程很慢的情况,get方法调用时候使用timeout进行限制舍弃慢的结果,或者使用CompletableFuture先出先获取
    2. Future的生命周期不能后退

实战

自己写一个高性能缓存

=========================================

你可能感兴趣的:(2020-06-18 Java并发编程-----基础-----JUC)