并发编程总结

作者: 小丸子的呆地 | 来源:发表于2021-08-30 15:30 被阅读0次

    并发概念

    并发变成三大特性

    • 原子性
    • 可见性
    • 有序性

    区分数据库事务四大特性:原子性、一致性、隔离性、持久性

    线程的状态流转

    初始化(new) -> 就绪(start) -> run(CUP执行) -> terminate(终止)
    -> wait(wait、join、park)
    -> timeWait(sleep、park、wait带时间)
    -> blocked阻塞(竞争锁时的状态)

    守护线程

    new Thread().setDaemon(true),守护线程会在所有用户线程结束之后自动接入,典型的守护线程就是GC线程

    sleep、yied、join、wait、notify

    sleep使当前线程进入timeWait状态,并释放cpu资源。
    yied是当前线程进入就绪状态,并释放cpu资源;concurrentHashmap初始化的时候使用了这个yied
    join是当前线程进入wait状态,等待join的线程执行完毕。
    wait使当前线程进入wait状态,进入synconized的等待池,等待被notify
    notify唤醒wait状态的线程,被唤醒的线程进入锁池竞争队列;wait和notify只能在synconized代码块中使用,利用的是ObjectMonitor对象的原理。

    volatile

    内存可见 lock前缀
    规避指令重排序 JVM定义了4个内存屏障,ll ls sl ss,被volatile修饰的变量的操作前后会被插入这四个屏障,hospot的实现是lock缓存
    总线风暴,在多线程场景下,volatile变量总是发生变化的时候,由于由于总线嗅探,会占用总线大量资源

    指令重排序原则

    CPU和JVM对代码指令重排序要保证以下两个基本原则:
    as-if-serial 向单线程一样,执行结果不能改变
    hapens-before 定义了8种事件需要保证顺序

    lock前缀 之前是锁总线,后面优化为锁定缓存区域
    在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,
    使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,
    intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),
    并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。

    synconized

    • monitorenter monitorexit*2 异常exit 修饰方法时,对方法标记acc_synconized标记,内部隐式使用monitor
    • hospot实现是objectmonitor 当前线程、重入次数、竞争队列、阻塞队列、等待池
    • 升级过程 无锁->偏向锁->轻量级锁(自旋10次)->重量级锁(调用内核了) 无法降级
    • 偏向锁默认不开启 需要jvm参数 不开启偏向锁一旦发生竞争就是轻量级锁,偏向锁撤销需要等待safepoint

    CAS

    无锁 compareAndSwap 通过循环比较原值判断是否可以进行赋值,在并发中保证线程安全的一种方式
    Atomic相关类使用CAS原理+自旋锁,通过循环尝试,来达到目的,不应该在超高并发下、锁定时间较长的场景下使用,以前是用Unsafe类、JDK11之后使用VarHanlder

    MESI

    指的是缓存行的四种状态,mesi协议通过总线嗅探机制能发现发生在其他cup上对缓存的操作,并同步修改其他cup内的状态

    JMM

    工作内存-主内存 8种原子行为 lock、unlock、read、load、use、assign、store、write

    并发工具

    AQS

    AQS维护了一个state 和 一个队列,定义了一些排队和唤醒等的方法

    共享与独占,就是共享模式不会排斥共享的请求,共享请求不需要判断当前线程是否为资源拥有者;独占模式会排斥包含共享和独占的所有操作,必须位当前资源的持有线程再能操作

    CountDownLatch

    倒计时锁存器,用于等待多个线程执行结束。
    内部封装sync继承AQS,初始化的时候设置AQS.state。countDown操作利用共享模式的操作state--。await进入等待队列,等待state归0

    Exchanger

    交换器,用于线程间交换数据,或者让两个线程达到同一个位置,一个线程执行exchange操作,会阻塞到另一个线程也同样执行exchange操作;并且将数据互相交换。
    ThreadLocal + LockSupport

    线程通信几种方式:wait+notify、共享变量、exchanger、LockSupport等

    Semaphore

    信号量,可以控制一些资源的访问,控制线程执行并发度,令牌桶原理,获取令牌需要预先放入令牌,否则就阻塞。
    内部封装sync继承AQS,分别实现公平和非公平的sync,初始化的时候设置AQS.state,作为令牌桶初始容量。acquire操作通过共享模式操作尝试从令牌桶(state--)获取令牌,获取不到就进入队列阻塞;release操作,通过共享操作模式向令牌桶中(stat++)放入令牌,并唤醒阻塞中的等待获取令牌的线程

    公平和非公平,就是公平模式会是否检查有等待的线程,如果有就加入到队列尾部,非公平模式是一开始就尝试获取资源,如果没获取到进入队列尾部

    CyclicBarrier

    栅栏,控制多个线程达到同一位置,先达到的会阻塞,可以重复使用。
    初始化的时候设置栅栏数量count,await方法获取lock执行count--然后进入condition的await状态,当count为0的时候,breakBarrier()唤醒所有await状态的线程,并恢复栅栏。

    ReentrantLock

    可重入锁,用于资源的访问控制
    内部维护sync对象集成AQS,同样有公平和非公平模式;可重入实现是通过独占模式操作AQS的state的数量,获取一次state++,释放一次state--,当state为0时,放弃锁的持有并唤醒队列中的等待线程。

    可重入就是持有锁的线程可再次进入需要这把锁的代码块而不需要竞争。

    ReentrantReadWriteLock

    可重入读写锁,用于控制资源的读写并发。
    读写锁,其实就是共享锁和独占锁的一种实现。内部维护两个lock,一个readlock,一个writelock,但是其实是同一个sync对象,都是对同一个state的争抢,谁抢到就标记当前锁状态为读还是写,读状态先所有读操作可以共享的获取锁。

    Condition

    锁条件队列,有点类似synconized的等待池,每一个condition内部维护了一个等待队列,通过await()方法进入等待队列,主动放弃锁的持有,通过single()方法被唤醒。
    一把Lock可以创建多个condition。
    多用于阻塞队列中实现。

    LockSupport

    阻塞工具,利用UnSafe类,进行线程阻塞和唤醒。LockSupport.park()和LockSupport.unPark(ThreadId);

    并发集合

    ConcurrentHashMap

    并发的哈希表
    JDK1.7 segment分段锁 lock;并发度与segment数量有关,默认16
    JDK1.8 ACS+synconized+分段扩容(counter) 本质上还是一个HashMap,复用HashMap的一些机制

    ConcurrentSkipListMap

    并发的跳跃表
    有序的并发map,对应TreeMap,因为TreeMap在并发场景下过于复杂,所以提供此类

    Vector

    同步list,使用synconized修饰所有方法

    HashTable

    同步Map,使用synconized修饰所有方法

    Collections.synconizedMap

    将传入的Map,所有方法使用synconized修饰;同样的还有synconizedSet、synconizedList等

    CopyOnWriteArrayList CopyOnWriteArraySet

    写时复制list。
    读不加锁,写加锁,写时copy一个新的数组,操作完新数组,将新数组赋值给对象内部数组,内部数组使用volatile修饰。

    既然已经加锁,为什么不直接操作内部数组,内部数组有volatile修饰,volatile修饰的对象能保证线程间可见性,所以只有改变被volatile修饰的数组才能触发总线嗅探,使其他线程感知到数据变化了。

    ThreadLocal

    线程共享变量,ThreadLocalMap存储于Thread中,Key为ThreadLocal对象的弱引用,Value为对应的值。
    key使用弱引用是为了防止内存泄露,弱引用随时可能被回收,key为null的键值对会在get的时候进行回收

    强软弱虚引用,软引用在内存不足时会被回收,弱引用在GC时会被回收,虚引用用来管理对外内存

    队列

    有了list为什么还需要队列

    list更多的使用是存储一组数据,和方法间的传递
    队列描述的是一个元素进出的动作,队列提供了add-remove\offer-poll的操作;add-remove会抛出异常

    ConcurrentLinkedQueue

    并发链表队列
    内部使用链表,节点属性使用volatile修饰,cas操作保证线程安全

    PriorityQueue

    优先级队列,内部使用堆排序

    BlockingQueue阻塞队列

    阻塞队列提供put和take方法,阻塞式操作

    LinkedBlockingQueue

    链表形式的阻塞队列,无界 lock+condition

    ArrayBlockingQueue

    数组形式的阻塞队列,有界 lock+condition

    自己实现阻塞队列

    lock+condition(putCondition、takeCOndition);
    put的时候,判断队列长度,达到最大值putCondition.await,没达到就++,然后takeCondition.signal一个take线程
    take的时候,判断队列长度,为0就takeCondition.await,没达到就++,然后putCondition.signal一个put线程

    DelayQueue

    延时队列,无界的BlockingQueue,存储实现了Delayed接口的对象,内部使用PriorityQueue进行排序,
    Delayed继承Comparable,实现Delayed需要实现获取过期时间和比较接口。put的时候由于无界,所以不会阻塞;taked时候获取PriorityQueue头部元素,如果队列为空,进入condition的等待队列。
    定时任务线程池使用此队列。

    SynchronousQueue

    同步队列,提交任务会阻塞至有现成获取任务,同时只有一个

    TransferQueue

    交换阻塞队列,提交任务会阻塞至有线程获取任务,同时可以有多个

    线程池

    为什么要使用多线程

    最大化利用cpu,业务中难免会有一些IO等阻塞操作,此时cpu空闲,多线程可以在IO阻塞的时候干点别的。

    如何实现多线程

    1. 继承Thread,重写run方法,start方法启动
    2. 实现Runnable接口,实现run方法,new一个Thread传入,Thread.start方法启动
    3. 实现Callable接口,实现call方法,new一个FutureTask传入,FutureTask.run方法启动
    4. 创建线程池,提交实现了Runnable接口或者Callable接口的任务

    线程是不是越多越好

    不是,cpu切换线程需要切换上下文信息,这个过程也是需要消耗资源的,如果线程过多,cpu疲于切换线程,啥也干不了了

    什么是线程池、线程池的优势

    线程池用来管理一组线程,管理线程的生命周期;
    java线程池创建完线程之后,让线程进入while(true)获取任务状态,通过阻塞队列来管理任务,没有任务的时候线程处于阻塞阶段;
    生产者消费者模式。

    java的线程与操作系统的线程是1对1的,创建线程是也是一件比较消耗资源的事,我们要尽量规避线程的创建和销毁,线程创建之后不能轻易让其中止;
    1.缓存一组线程,线程声明周期管理,比如创建的线程的属性、拒绝策略等
    2.线程复用,规避频繁创建和销毁线程

    Executor、ExecutorService、ThreadPoolExecutor、ForkJoinPool

    Executor是线程池顶级父接口,定义了execute方法
    ExecutorService继承Executor,完善了线程池的声明周期管理方法
    ThreadPoolExecutor是线程池的具体实现,内部维护一个阻塞队列存储任务,实现了线程池的方法
    ForkJoinPool是线程池的具体实现,拆分任务,并行执行,workstealing

    ThreadPoolExecutor参数详解

    核心线程数 线程池最少cache的线程数量
    最大线程数 线程池最大cache的线程数量,大于核心线程的部分,由下面的超时配置控制何时销毁
    超时时间 非核心线程的销毁时间(空闲多久之后)
    时间类型 非核心线程的销毁时间(空闲多久之后)
    阻塞队列 存放任务的队列
    线程工厂 ThreadFactory定义一些线程的基本信息,比如命名
    拒绝策略 在队里塞满之后执行的方法,默认是抛出异常,还有丢弃、使用当前线程执行、或者自定义

    ThreadPoolExecutor状态

    RUNNING 运行中,接收新任务
    SHUTDOWN shutdown()不再接收新任务,等队列中任务处理完之后,停止线程池运行
    STOP shutdownNow()不接受新任务,不处理队列任务,打断现在执行任务的线程
    TIDYING 所有任务都停止,工作线程全部销毁,关闭之前的状态
    TERMINATED 线程池已关闭,关闭之前会调用terminated();

    线程Interrupt

    如果要interrupt线程,必须在线程阻塞状态下

    ThreadPoolExecutor的ctl

    ctl是一个32位AtomicInteger类型数字,高3位代表当前线程池状态,低29位代表当前线程数,所以一个线程池最大线程数是Integer.MAX_VALUE>>>3

    ThreadPoolExecutor工作流程

    创建ThreadPoolExecutor之后,默认没有创建核心线程,可以使用prestartAllCoreThreads添加空转的核心线程;
    execute或者submit任务之后,首先会判断现在是否已达到最大核心线程数量,如果没有就创建一个
    尝试offer进入队列,如果超过队列最大数量,尝试创建非核心线程,如果非核心线程没有名额,执行拒绝策略。

    为什么不先创建非核心线程

    最大程度复用核心线程,减少线程创建。队列中的任务认为是核心线程承受范围之内。
    比如工作量偶尔超过负荷,让员工加加班,总好过招一个新员工。

    Execuetors

    java提供的快速创建线程池的工具类,存在一些问题
    newFixedThreadPool 创建一个核心线程数=最大线程数的线程池,使用无界LinkedBlockingQueue,有内存溢出风险
    newSingleThreadExecutor 创建一个单线程的线程池,使用无界LinkedBlockingQueue,有内存溢出风险
    newCachedThreadPool 创建一个核心线程数为0最大线程数为Integer.MAX_VALUE的线程池,使用SynchronousQueue,可以用于一些短小快的任务,极端场景可能会创建大量线程,让cpu疲于上下文切换
    newSingleThreadScheduledExecutor 创建一个定时任务线程池,ScheduledThreadPoolExecutor内部使用DelayedWorkQueue存储任务,可以创建循环执行的任务
    newWorkStealingPool 创建一个ForkJoinPool线程池

    Tomcat中的线程池有什么改动

    tomcat中自定义线程池继承自ThreadPoolExecutor,创建之后默认初始化所有核心线程;
    提交任务超过核心线程数优先创建非核心线程,任务优先,自定义TaskQueue持有线程池对象,重写offer方法,在没达到最大线程数时,伪装成队列已经满了,让任务优先创建。

    相关文章

      网友评论

        本文标题:并发编程总结

        本文链接:https://www.haomeiwen.com/subject/hwhoiltx.html