• 多线程学习总结


    基础概念

    什么是进程 , 什么是线程 ?

    进程是一个服务 也就是一个程序的动态表现 线程是进程中的最小执行单元

    创建线程的方式

    • 从Thread类继承

    • 实现 Runnable接口

    线程的sleep/yeild/join

    sleep : Thread#sleep() 不会释放锁 但是 Object#wait()会释放锁 睡眠结束完后 也会回到就绪队列

    yeild : 本线程进入等待队列 , 让出一下CPU

    join : 让本线程 跑到被调用者线程中去运行 且要在调用线程执行完后 运行

    线程的interrupt

    线程中断获取响应

    synchronized

    同步关键字

    hospot 实现 : 锁的应该是对象头 一个对象头有32/64位具体看JVM的位数 其中两位 用于标志锁

    锁升级 : 无锁 - > 偏向锁 - > 轻量级锁(自旋锁 默认旋10次 如果还得不到锁 就升级) -> 重量级锁(操作系统锁 进入等待队列)

    什么时候用自旋锁 ? 什么时候用重量级锁 ?

    线程数量少 并且锁定的业务代码运行时间短 就用自旋锁

    线程数多 并且等待时间长的 就用重量级锁

    可重入

    程序执行出现异常时 默认情况下 锁是会被释放的

    同步方法和非同步方法 是可以同时执行的

    当做锁的对象一定要加final 并且不能用String作为锁对象

    volatile

    一致性 , 可见性 ( 保证线程可见性 )

    底层使用MESI(CPU缓存一致性协议)

    保证Jvm指令不会被重排序( 禁止指令重排序 )

    底层使用cpu读写屏障来完成的 loadFence storeFence (CPU原语)

    volatile修饰引用对象其实是没有什么意义的 引用对象内的数据改变了 其他线程是没法看见的

    CPU缓存一致性协议(MESI)

    现代CPU的缓存一致性是通过缓存锁 + 总线锁实现
    CPU缓存一共有三层
    1. L1 速度非常快 存的数据也很少 本人电脑中 只有256k

    2. L2 速度慢于L1 本人电脑中 只有1M L1 和L2 是CPU独享的 在CPU内部

    3. L3 在主板上 CPU共享 在本人电脑中 只有6M

    缓存行

    为了提高效率 读取数据的时候 会一次性读取缓存行大小的数据 一般为 64字节

    伪共享

    同一缓存行的两个不同数据, 被两个不同的CPU锁定 产生互相影响的伪共享问题

    解决方法 : 缓存行对齐

    CAS(自旋)

    各种Atomic类就是使用了CAS保证线程安全的

    ABA问题 如果是基础数据类型无所谓 但如果是引用类型 就可能有问题了

    解决 : 加一个版本号 一起判断 就OK

    所有的CAS 都是使用Unsafe类

    JUC同步工具

    各种类型锁

    ReentrantLock 可重入锁

    底层使用的是CAS

      加锁时 Lock.lock() 最后需要手动解锁
    synchronizedReentrantLock
    重入 可重入 可重入
    解锁 可自动解锁 需要手动解锁
    维护队列 只有一个队列 可有多个队列 (Condition)
        可尝试获取锁 tryLock() 不管有没有获取都立即返回
    公平锁 只有非公平锁 有公平与非公平的切换
      ReentrantLock.unlock() -> 调用 AQS.release(int args)

    AQS.release() -> AQS.tryRelease(int arg)调用具体的实现类 -> ReentrantLock.Syn.tryRelease(int arg) 该方法中主要做了以下几件事
    1 : int c = getState() - releases 获取state 并且减 arg
    2 : 判断c == 0 ? setExclusiveOwnerThread为空 即无线程获取锁
    3 : setState(c); 将state值更新

    CountDownLatch 门闩

    await() 线程阻塞

    countDown() 减数 当门闩减到0的时候线程会继续运行

    CyclicBarrier 循环栅栏

    初始化时 可传入两个参数 ( 数量, 当满了时调用的Runnable)

    Phaser 阶段

    ReadWriteLock 读写锁

    其实也就是共享锁和排他锁

    Semaphore 信号灯

    可用于限流

    Exchanger 交换器

    用于两线程之间数据交换 调用exchange() 阻塞 等待第二方调用exchange()时 都会放开阻塞

    LockSupport

    底层使用的是UnSafe实现的

    wait/notify/notifyAll

    wait()会释放锁

    看Object源码得知

    fail-fast or fail-safe

    fail-fast(快速失败)

    在使用迭代器对集合对象进行遍历的时候,如果A线程对集合进行遍历,正好B线程对集合进行修改(增加、删除、修改)则A线程会抛出ConcurrentModificationException异常。

    java.util 下的集合都是快速失败的 不支持并发修改

    fail-safe(安全失败)

    采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception,例如CopyOnWriteArrayList。

    java.util.concurrent 下的集合都是安全失败的 支持并发修改

    同步容器

    ConcurrentHashMap

    ConcurrentSkipListMap

    底层实现使用了跳表

    CopyOnWriteArrayList

    写时复制 当需要添加元素的时候 复制一份原数组 然后操作复制出来的 最后将修改后的数据 set回去

    其主要用在读比较多 写比较少的情况下

    写操作是线程安全的 读的时候没有加锁

    CopyOnWriteArraySet

    Queue

    Queue  
    insert offer 如果插入没有成功 就会返回false add 如果插入没有成功 就会抛异常
    remove poll 如果队列没有元素了 就会返回null remove 如果队列没有元素了就会抛异常
    Examine element 只是检索队列 不改变队列 为空返回异常 peek 只检索队列 不改变队列 为空返回null

    BlockingQueue 阻塞队列

    BlokingQueue 
    insert put 如果队列满了 会阻塞 最大值为Integer.MAX_VALUE
    remove take 如果队列空了 会阻塞
    priorityQueue 优先队列

    内部实现了一个排序 内部是一颗树 二叉小顶堆实现

    最小堆

    是一颗完全二叉树 其中 父节点一定小于叶子节点

    leftNodeNo = parentNo * 2 +1

    rightNodeNo = parentNo * 2 + 2

    parentNodeNo = (nodeNo - 1) /2

    插入节点的时候 算出节点下标 然后逐层比较 直到>= 父节点时

    链接 : https://www.cnblogs.com/CarpenterLee/p/5488070.html

    DelayQueue

    按时间去排序的Queue 按理说 这已经打破了queue的规范了 队列存储的顺序 并不是插入顺序 而是比较后的顺序

    一般用来 按时间进行任务调度

    SynchronousQueue

    这个queue 容量是0的

    用来两个线程间传递数据的

    TransferQueue

    transfer(E e) 该方法 只有被其他线程消费了 才会继续往下走 否则会一直阻塞在那

    Queue 和 List的区别 :

    Queue添加了很多对线程友好的api 比如 offer poll peek

    BlokingQueue 在Queue的基础上 又添加了一些api 比如 put take

    put take 实现了阻塞 天然的实现了生产者消费者模型

     

    边边角角

    PriorityQueue 优先队列

    扩容机制 : 如果oldCapacity < 64 ? 扩容两倍 + 2: 扩容50%

    初始容量 : 11

    ThreadLocal

    每个Thread中都维护了一个ThreadLocal->ThreadLocalMap

    ThreadLocal 有用在Spring的声明式事务 比如 将数据库连接放入ThreadLocal中

    只要是一个事务中的 都是同一个数据库连接

    对象四种引用 强/软/弱/虚

    强引用

    只要引用指向对象 对象就不会被回收

    软引用 SoftReference

    当一个对象只有一个软引用指向的时候 当系统内存不够用时 才会被GC回收

    弱引用 WeakReference

    当一个对象只有一个虚引用指向时 只要发生了GC 就会被GC回收

    一般用在容器里

    WeakHashMap

    使用了弱引用

    WeakHashMap中的Entry<K,V> K 是使用了弱引用

    如果发生gc WeakHashMap中的key会被干掉

    每次put的时候 都会清理一遍WeakHashMap

    WeakHashMap 的默认初始容量 16 负载因子 0.75f 每次扩容为原本的两倍

    ThreadLocal

    Thread中维护着一个ThreadLocalMap

    ThreadLocalMap中有一个Entry<K,V>

    Entry继承WeakReference

      Entry(ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
    }
    内部 : ThreadLocal 被一个弱引用指向着
    外面 : 如果是直接new出来 被一个强引用指向 如果外面的强引用断开了
    那么每当发生gc时 ThreadLocal就会被干掉

    如果内部不用弱引用指向 那么会造成内存泄漏 ThreadLocal 永远不会被回收
    但是 那也只是回收了Key value指向的对象 依旧存在
    所以 使用完ThreadLocal后 必须要调用remove()方法
    虚引用 PhantomReference

    用于管理堆外内存的

    new PhantomReference(引用 , 引用队列);

    给写JVM的人用的

    当与引用指向的对象被回收后 会在Queue中存放进一个对象

    线程池

    Callable

    相当于有返回值的Runnable

    Future

    存储执行的将来才会产生的结果

    get方法是阻塞的

    FutureTask

    Futrue + Runnable

    Executor

    主要做线程的执行接口

    把线程的定义与线程的执行分开

    ExecutorService

    线程池的父接口

    里面定义了一些生命周期方法和提交任务方法

    submit方法是异步调用的

    CompletableFuture

    管理多个Future的结果

    ThreadPoolExecutor

    线程池的七个参数
      corePoolSize 
    核心线程数
    maximumPoolSize
    最大线程数
    keepAliveTime
    当线程的数量大于内核时,这是多余的空闲线程在终止之前等待新任务的最大时间。
    unit
    时间单位
    workQueue
    任务队列
    threadFactory
    线程工厂
    handler
    拒绝策略 jdk一共提供了四种拒绝策略
    1 : AbortPolicy 抛异常
    2 : CallerRunsPolicy 在调用者的线程中执行任务
    3 : DiscardOldestPolicy 丢弃队列头部的任务(其实也是等待最久的任务) 并且将当前任务尝试处理
    4 : DiscardPolicy 不处理 抛弃掉
    当执行被阻塞时使用的处理策略,因为达到了线程边界和队列容量
    执行流程
      加入一个任务 先看核心线程 如果和心线程没满 起核心线程 
    如果核心线程满了 查看任务队列 如果任务队列没满 加入任务队列
    如果任务队列满了 起一个新线程去处理
    如果达到了最大线程数 并且 任务队列也满了 那么启动拒绝策略
    最大处理任务数

    最大线程数 + 任务队列长度

    ForkJoinPool

    分叉出子任务执行 然后最后做汇总

    分解汇合任务

    用很少的线程可以执行很多的任务(子任务) ThreadPoolExecutor做不到先执行子任务

    CPU密集型

  • 相关阅读:
    erlang中的图片下载
    erlang进程监控:link和monitor
    如何在linux centos下安装git(转)
    mnesia的脏写和事物写的测试
    关于proplists:get_value/2 与lists:keyfind/3 的效率比较
    mnesia的脏读和事物读的测试
    扒一扒P2P风控的底牌(转)
    ejabberd源码流程梳理
    Erlang的系统限制
    mark
  • 原文地址:https://www.cnblogs.com/self-crossing/p/12658087.html
Copyright © 2020-2023  润新知