并发编程

208 阅读21分钟

并发编程

线程模型

分类

  • 内核线程(直接由操作系统内核支持的线程,这种线程由内核来完成线程切换)
  • 用户线程(完全建立在用户空间的线程库上,系统内核不感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助)

操作系统线程

  • linux或者unix系统(使用pthread_create命令创建线程)
  • windows(使用_beginthreadex函数创建线程)

java线程

  • java线程和操作系统线程是1对1的。1个java线程对应一个操作系统内核线程,java中的Thread对象的start方法,底层是调用jdk的start0方法,start0是用c语言写的native方法
  • java的Thread对象构造函数会调用名jdk中名为registerNatives的native方法。这个方法是用来注册native方法,将相关的native方法与hotspot jvm注册在一起,其中就注册了start0方法映射到虚拟机中的JVM_StartThread方法,JVM_StartThread方法使用c++写的
  • JVM_StartThread方法在虚拟机中的jvm.cpp代码中定义,可以将jvm.cpp理解为java与jvm之间的桥梁。JVM_StartThread方法内部创建了JavaThread对象,并将java Thread对象中的run方法作为入参传进去
  • JavaThread类的构造函数定义在thread.cpp中,构造函数内部调用create_thread函数
  • create_thread函数每个平台的虚拟机都有自己的实现,因为底层是调用操作系统的指令或者函数。如果是linux jvm,会使用pthread_create命令创建线程;如果是windows jvm,会使用_beginthreadex函数来创建线程
  • java的Thread对象提供的run方法最终作为pthread_create命令或者_beginthreadex函数的回调方法

CPU内存模型

图解

结构

  • CPU(寄存器,速度非常快)
  • 多级缓存(一般好的处理器都是三级缓存,读取速度在CPU和内存之间。多个cpu的时候,每个cpu拥有自己独立的多级缓存,不同的cpu之间相互独立)
  • 主内存(所有CPU共享)

问题

  • 缓存不一致的问题(CPU遵循了MESI协议,来确保缓存数据一致性的问题,或者使用bus总线锁来解决缓存不一致的问题)

特性

  • CPU指令重排(CPU会将对指定进行排序优化等)

JMM(java内存模型)

为什么要有JMM?

  • 因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果

图解

结构

  • 主内存(线程共享)
  • 工作内存(线程私有,主内存的副本拷贝,所有线程都是直接访问工作内存。工作内存和主内存通过一套机制来保证数据的一致性)

操作

  • Lock(锁定主内存中的变量,变成线程独占)
  • Read(把主内存中的变量值传输到工作内存线程中,便于后面的Load指令使用)
  • Load(把Read操作得到的变量值,加载到工作内存中)
  • Use(把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作)
  • Assign(把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作)
  • Store(把工作内存中的一个变量的值传送到主内存中,便于后面的Write操作使用)
  • Write(把 Store 操作传送的值,写入到主内存中)
  • UnLock(把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定)

特性

  • 可见性(当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改)
  • 原子性(原子性是指一个操作是不可中断的. 即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。像i++这种本身有多个指令组成,所以它不具备原子性)
  • 有序性(在单线程环境中,程序是按序依次执行的;而在多线程环境中,程序的执行可能因为指令重排而出现乱序)

实现

  • Volatile关键字(保证可见性、禁止CPU指令重排,但无法保证原子性的问题)

happens before规则(禁止编译优化)

  • 程序的顺序性规则(一个线程中,按照程序的顺序,前面的操作happens-before后续的任何操作)
  • volatile规则(对一个volatile变量的写操作,happens-before后续对这个变量的读操作)
  • 传递性规则(如果A happens-before B,B happens-before C,那么A happens-before C)
  • 管程中的锁规则(对一个锁的解锁操作,happens-before后续对这个锁的加锁操作)
  • 线程start()规则(主线程A启动线程B,线程B中可以看到主线程启动B之前的操作。也就是start() happens before 线程B中的操作)
  • 线程join()规则(主线程A等待子线程B完成,当子线程B执行完毕后,主线程A可以看到线程B的所有操作。也就是说,子线程B中的任意操作,happens-before join()的返回)

高质量博客

内存屏障(Memory Barrier )

c/c++的volatile关键字:可以禁止编译器优化(不加volatile的变量可能会被优化成直接从cpu的寄存器或者高速缓存中读取数据,导致可见性问题)

cpu的写缓冲区:有的cpu架构底层有StoreBuffer(写缓冲区),写入是异步写入,可能也会导致可见性问题

JMM屏障

  • LoadLoad

    • 使用c/c++的volatile关键字禁止编译器优化
    • 执行movq 0(%%rsp), %0命令
  • LoadStore(和LoadLoad实现完全一样)

  • StoreStore

    • 使用c/c++的volatile关键字禁止编译器优化
  • StoreLoad

    • 使用lock; addl指令实现读写屏障

      • lock会使用cpu中总线锁或者缓存锁(如果跨多个缓存行,就会使用总线锁),写完刷新到主内存。实现写屏障效果
      • MESI协议会保证在lock指令内修改缓存行后,其它cpu中相关的缓存行失效。实现读屏障效果

CPU屏障

  • lfence(读屏障)
  • sfence(写屏障)
  • mfence(读写屏障)

Volatile原理

volatile关键字修饰的字段会加上ACC_VOLATILE标记

jvm底层提供了is_volatile方法,判断是否是volatile字段

volatile字段的读,会调用JMM的LoadLoad屏障

volatile字段的写,会调用JMM的StoreLoad屏障

Blocking Queue阻塞队列

ArrayBlockingQueue(数组)

  • 内部使用Object数组存储元素
  • 使用ReentrantLock锁,然后创建两个Condition对象,一个名称为notEmpty的对象(用于关联需要获取元素的线程集合),一个名称为notFull的对象(用于关联需要插入元素的线程集合)
  • put的时候会使用调用lock方法,然后判断当前元素数量是否小于队列总大小;如果小于,则添加元素,如果大于等于,则调用notFull的await方法,释放锁并进行等待,最后调用notEmpty的signal方法唤醒notEmpty的等待线程
  • take的时候同样也会调用lock方法,然后判断当前元素的数量是否大于0;如果大于0,则获取数组的第0个元素,并从数组中移除,如果小于等于0,则调用notEmpty的await方法,释放锁并进行等待。最后调用notFull的signal方法唤醒notFull的等待线程

LinkedBlockingQueue(链表)

  • 内部使用两个ReentrantLock对象,分别叫做putLock和takeLock,一个用来读数据,一个用来取数据。同时创建名称为notEmpty的Condition对象,和名称为notFull的Condition对象,实现了读写分离
  • 我这里是有个疑问的,当链表中只有1个元素的时候,因为读写是两把锁,这个时候读写都会对这一个节点进行操作,怎么去保证线程安全的??有时间仔细看看

SynchronousQueue(没有容量)

PriorityBlockingQueue(优先级)

DelayQueue(延迟)

线程Thread和线程池ThreadPoolExecutor

线程(Thread)

  • 实现

    • 继承Thread类,重写run方法
    • 实现Runnable接口,实现run方法,作为Thread对象的参数传入
    • 实现Callable接口,并使用FutureTask来包装,最后将FutureTask对象作为Thread对象的参数传入
  • 弊端

    • 频繁的创建和销毁线程,会带来更多的资源开销
    • 因为线程之间是通过cpu的时间分片来进行调度运行
    • 所以线程池的技术应运而生,就像数据库连接池一样

线程池(ThreadPoolExecutor)

  • 核心参数

    • corePoolSize(核心线程数量)

    • maximumPoolSize(最大线程数量,最大线程数量 - 核心线程数量 = 非核心线程数量)

    • keepAliveTime(非核心线程在空闲状态下的存活时间)

    • unit(存活时间单位)

    • workQueue(队列)

    • threadFactory(线程工厂,用于创建Thread线程对象)

    • handler(拒绝策略,当corePoolSize、workQueue和maximumPoolSize都满了,会进行拒绝)

      • AbortPolicy(默认,抛出异常)
      • DiscardPolicy(丢弃)
      • DiscardOldestPolicy(丢弃队列中最老的任务)
      • CallerRunsPolicy(由调用者运行)
  • 分类

    • singleThreadPool(核心和最大线程数量为1,无界队列,单线程的执行一个个线程)
    • cachedThreadPool(缓存线程池,核心线程数量为0,同步队列没有长度,最大线程数量为int的最大值,非核心线程数量空闲1分钟就会被回收)
    • fixedThreadPool(固定大小的线程池,超过核心线程数数量,则放入队列中;超过最大线程数,则返回异常)
    • scheduledThreadPool(定时的线程池)
  • 原理(以fixThreadPool举例)

    • 首先创建一个ThreadPoolExcutor对象(指定核心线程数,默认最大线程数如果不指定,则与核心线程数一样,创建LinkedBlockingQueue类型队列)
    • submit方法会将传入的Runnable实现对象通过FutureTask包装,然后去调用execute方法执行该Runnable对象
    • 判断当前正在运行的线程数是否小于核心线程数。如果小于,则执行addWorker方法,添加一个Worker工作者
    • 否则,则去向BlockingQueue类型的workQueue队列中放入当前需要执行的Runnable对象。如果因为队列满了或者其它情况无法放入,则直接调用addWorker方法
    • addWorker方法中,内部会判断是否要创建核心的worker,如果是,判断当前工作的线程是否大于等于核心,是则return false,添加worker失败。如果创建的是非核心的worker,判断当前工作的线程是否大于等于最大线程池大小,是则return false
    • 说明现在是满足了Worker创建的条件,当前会创建Worker对象,Worker构造函数中会创建一个名称为thread的Thread对象(会把worker对象自己传给线程),并把传入的Runnable对象赋值给firstTask变量。
    • Worker创建成功后,会添加到HashSet类型的workers集合中(因为workers集合并不是线程安全的,这里使用了ReentrantLock锁)
    • 最后调用Worker对象里面的thread线程对象的start方法,也就是会执行Worker对象的run方法。run方法,又调用了runWork方法,runWork方法里面调用了firstTash的run方法(归根结底还是执行我们submit过来的Runnable对象中的run方法)
  • 原理(scheduledThreadPool)

    • 创建ScheduledThreadPoolExecutor对象,指定核心线程数,且内部使用DelayedWorkQueue类型的队列
    • 使用ScheduledFutureTask这样的FutureTask来包装Runnable对象
    • 调用delayedExecute方法,内部调用workQueue指向的队列的add方法,将任务放进队列中,add方法内部会调用siftUp方法,会将延迟队列中的任务按照时间排序
    • 再调用ensurePrestart方法,内部调用熟悉的addWorker方法,上面有分析过
    • 最后调用ScheduledThreadPoolExecutor对象的run方法,run方法内部根据执行周期等信息来判断,当前是否立即执行任务,还是需要运行完任务并设置下一次执行时间等

ThreadLocal原理

每个线程Thread对象里面有一个类型为ThreadLocalMap的变量

ThreadLocalMap里面的key为ThreadLocal对象,value为设置的值

分类

  • 公平锁/非公平锁(ReentrantLock里面默认是非公平锁,可以设置为公平锁)
  • 可重入锁(ReentrantLock和synchronized)
  • 独享锁/共享锁(ReentrantLock和ReentrantReadWriteLock)
  • 互斥锁/读写锁(ReentrantLock和ReentrantReadWriteLock)
  • 乐观锁/悲观锁(CAS和synchronized)
  • 分段锁(一种锁的概念,ConcurrentHashMap在jdk1.7的时候,使用Segment作为分段锁)
  • 偏向锁/轻量级锁/重量级锁(synchronized底层实现)
  • 自旋锁和适应性自旋锁(线程一直循环等待)

实现

  • synchronized(jvm级别的锁)

    • 类型

      • 对象锁
      • 类锁
    • 原理

      • 反汇编得到的jvm指令是monitorenter、monitorexit或者方法的flag标识为ACC_SYNCHRONIZED
      • 每个对象都会有对应的objectMonitor(对象监测器)对象,对象头中MarkWord中的重量级锁标志保存的就是objectMonitor对象的指针。底层调用C++中objectMonitor对象的enter、exit方法
      • synchronized锁膨胀过程
  • ReentrantLock(可重入锁,java级别的)

    • 使用

      • lock方法,锁住
      • unlock方法,解锁
    • 原理

      • 创建ReentrantLock对象的时候,会创建NonfairSync类型的公平同步对象赋值给sync属性

      • lock方法

        • 调用lock方法,内部是调用sync的lock方法,也就是NonfairSync对象的lock方法
        • 方法内部会使用CAS技术将state状态从0改成1。如果成功,则设置exclusiveOwnerThread独占的拥有者线程为当前线程
        • 如果失败,说明拿锁失败,此时调用aqs里面的acquire方法,并将1作为参数传递过去(就是想要变成的目标状态)
        • aqs调用了tryAcquire方法。如果当前状态为0的时候,再次使用CAS技术将state状态从0改为1,如果成功,则设置exclusiveOwnerThread独占的拥有者线程为当前线程,返回成功;如果状态不是0,且当前线程就是拥有锁的线程,则累加state状态,并返回成功(这里说明了ReentrantLock是可重入锁,就是可以从重复进入的)
        • 如果tryAcquire方法返回false,说明没有拿到锁。这时候会调用addWaiter方法,创建一个节点,如果链表中的尾节点存在的话,将使用CAS技术当前新创建的节点加到链表尾部。插入成功,则返回插入的尾节点,如果不成功,则调用enq方法
        • enq方法内部会自旋,判断链表中节点是否为空,为空则插入头节点,不为空则插入尾节点,都是使用CAS来完成,直到节点插入成功为止,方法才会返回当前创建的节点
        • 创建完Node节点之后,会调用acquireQueued方法。该方法内部会自旋,判断当前节点的前一个节点是否为head节点,如果不是,则继续循环判断,直到为是才会去执行tryAcquire方法来获取锁。如果获取成功,则将当前节点设置为头节点,将之前的头结点断开,返回成功获取。如果未成功,则使用LockSupport.park方法挂起节点线程。
      • unlock方法

        • 调用unlock方法,内部是调用sync的release方法,也就是AQS里面的release方法
        • release方法内部首先会调用ReentrantLock里面的tryRelease方法,判断释放完状态是否为0,如果是则将独占拥有者线程setExclusiveOwnerThread设为null,返回释放成功,否则返回失败。如果释放成功,则调用unparkSuccessor方法来唤醒当前节点的next节点
        • unparkSuccessor方法内部会将当前已释放节点的等待状态waitStatus设置为0
        • 调用当前节点的next节点的LockSupport.unpark(唤醒节点对应的线程)
  • ReentrantReadWriteLock(可重入读写锁,粒度更细)

  • CountDownLatch(倒计时器)

  • Semaphore(信号量,可用于限流)

  • CyclicBarrier(循环栅栏,屏障)

synchronzied锁原理

无锁

  • 对象头mark word(64位)

    • unused(25位)
    • hashcode(31位)
    • unused(1位)
    • GC分代年龄(4位)
    • 偏向锁标识(1位。1表示偏向锁,0表示非偏向锁。此时为0)
    • 锁标识(00标识轻量级锁,01表示无锁或者偏向锁,10表示重量级锁。此时为01)
  • 当jvm关闭偏向锁或者偏向锁的延迟时间还未过,此时则为无锁。或者偏向锁被撤销,也会被撤销为无锁

  • 可以使用JOL工具类打印对象头,观察不同的锁对象头的变化

偏向锁

  • 对象头mark word(64位)

    • thread线程信息(54位)
    • epoch(2位,偏向时间戳)
    • unused(1位)
    • gc分代年龄(4位)
    • biased_lock偏向锁标识(偏向锁为1)
    • lock锁标识(偏向锁为01)
  • 偏向锁的对象头中是没有hashcode信息的,这也是为什么锁对象计算过hashcode,就不再是偏向锁的原因

  • 偏向锁默认是开启的,并有有4秒的延迟时间(在程序启动时,延迟4秒启用偏向锁,避免刚开始时jvm各种线程并发,导致锁升级)

  • 如果过了偏向锁的延迟时间,并开启了偏向锁,对象默认是匿名偏向。锁标识是偏向锁,但是线程ID为空,暂时还没有偏向任何一个线程

  • 如果这时候,偏向锁被其它线程获取,jvm会发现当前线程ID和偏向锁mark work中存储的线程ID不一样。此时不会发生重偏向,而是则会升级为轻量级锁

  • 批量重偏向(阀值默认为20)

  • 批量撤销(阀值默认为40)

轻量级锁

  • 对象头mark word

    • ptr_to_lock_record(62位,lock_record对象指针)
    • lock锁标识(2位,轻量级锁为00)
  • 轻量级锁是,当不同的线程一前一后的持有锁,没有竞争的情况;一旦,多个线程同时去竞争同一把锁,此时锁会升级为重量级锁

  • lock_record对象是存储在当前线程对应的栈帧上面,通过CAS操作去持有轻量级锁

重量级锁

  • 对象头mark word

    • ptr_to_heavyweight_monitor(62位,objectMonitor对象指针)
    • lock锁标识(2位,重量级锁为10)
  • 如果在linux系统上,重量级锁底层就是调用的pthread_mutex_lock函数上锁,pthread_mutex_unlock解锁。涉及到用户态与内核态之间的切换,非常耗时

  • mark word存储的是objectMonitor对象的指针。objectMonitor是jvm虚拟机中定义的c++类,该类内部有owner(指向当前持有objectMonitor对象的线程)、EntryList(排队线程列表)、WaitSet(等待线程列表,调用wait方法进行等待的线程,notify也是从该集合中唤醒线程)

  • 因为wait方法需要将线程放到objectMonitor对象中的WaitSet中进行等待。所以当调用了锁对象的wait或者notify方法,锁都会膨胀为重量级锁

图解

  • 子主题 1

AQS(AbstractQueuedSynchronizer)原理

获取锁(acquire方法)

  • 首先调用tryAcquire方法,尝试获取锁(tryAcquire不同的锁可能有不同的实现,现以ReentrantLock的非公平锁来分析)

    • 首先判断state状态是否为0,为0表示锁属于空闲状态,如果为0,则通过CAS将state从0改为1
    • 如果CAS更改成功,则将当前线程赋值给AQS中的exclusiveOwnerThread字段,表示当前持有锁的线程
    • 如果不为0,则判断当前线程是否是当前持有锁的线程,如果是则,累加state状态(从这里可以看出ReentrantLock是可重入锁)
  • 如果获取失败,则调用addWaiter方法,返回Node对象

    • 判断AQS队列中队尾是否有节点,也就是判断AQS队列中是否有节点。如果有,则新创建一个节点并通过CAS来放到队尾中,然后将新创建的节点返回
    • 如果队列中没有任何节点,则调用enq方法,创建一个空节点(没有thread信息)放在队列的头部,然后再创建一个新的节点放在队列的尾部
  • 再调用acquireQueued方法,将Node对象放入队列中

    • 首先执行一个死循环(for (;;))
    • 判断当前节点的前置节点是否为队头(也就是空节点,没有线程信息),如果是表示它可以竞争锁。则调用tryAcquire方法,如果获取锁成功,将AQS队列头部设置为当前节点,并将当前节点设置为一个空节点,跳出循环
    • 如果不是队列中的第二个节点或者获取锁失败,则调用shouldParkAfterFailedAcquire方法判断,是否需要park当前线程。该方法第一次循环为false,且会使用CAS技术将前一个节点的waitStatus设置为-1,第二次循环执行该方法的时候,就会返回true
    • 为true时,则会调用parkAndCheckInterrupt方法,该方法内部会使用LockSupport.park方法暂停当前线程,等待使用unpark对其进行唤醒

释放锁(release方法)

  • 执行tryRelease方法,尝试释放锁

    • 对state状态进行修改,且将AQS当前持有锁的线程信息设置为null
    • 如果当前执行tryRelease的线程和当前AQS持有锁的线程不一致,则抛出异常
  • 判断头节点是否不为空且waitStatus状态不等于0,则调用unparkSuccessor方法对头节点的next节点,进行LockSupport.unpark操作

    • 将头节点的waitStatus状态从-1设置为0
    • 对头节点的next节点关联的线程,执行LockSupport.unpark方法进行线程唤醒

ReentrantLock锁原理(AQS)

非公平锁NonfairSync(默认)

  • lock方法首先去拿锁,通过CAS将state状态从0改为1。如果拿到了,则修改AQS当前持有锁的线程信
  • 否则调用AQS的acquire方法

公平锁FairSync

  • 调用AQS的acquire方法

ReentrantReadWriteLock锁原理(AQS)

ReadLock读锁

  • 使用int类型state变量的前16位保存
  • 读锁是共享锁

WriteLock写锁

  • 使用int类型state变量的后16位保存
  • 写锁是排它锁

CAS原理和Atomic原子类

CAS(Compare and Swap,比较和交换)

  • 概念

    • CAS是一种无锁算法。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当内存值V和预期值A相同时,将内存值V修改为B;否则会自旋等待,并获取最新的预期值,进行相应的操作,然后再次尝试修改。
    • CAS是一种原子性的操作,它是CPU指令级别的一种操作,且速度特别快
  • 缺点

    • 只能保证对一个变量的原子性操作。当设计到多个共享变量的修改就无法保证原子性了
    • 长时间自旋会给CPU带来压力
    • CAS ABA问题(如果值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性)

Atomic原子类(底层都是通过CAS技术实现)

  • 实现

    • 基本类型

      • AtomicInteger
      • AtomicBoolean
      • AtomicLong
    • 数组

      • AtomicIntegerArray
      • AtomicLongArray
      • AtomicRefrenceArray
    • AtomicStampedReference(解决CAS ABA问题)

  • 原理(拿AtomicInteger举例)

    • 内部的value都是使用volatile修饰的,保证value值在多个线程中都是可见的,保证了value值的可见性
    • 方法内部是调用本地native方法,调用native compareAndSwapInt方法,来实现CAS原子性操作

Fork/Join框架

使用

  • RecursiveAction,重写computer方法,无返回值;RecursiveTask,重新computer方法,有返回值。fork方法用于拆分,join方法用于合并结果
  • 实例化ForkJoinPool对象,然后调用invoke方法,执行指定的RecursiveAction或者RecursiveTask任务

原理

Disruptor框架

解决了CPU伪共享的问题,伪共享是并发编程的隐形杀手。缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

因为JDK提供的大多数的BlockingQueue的实现都是使用锁来实现的,通常是使用ReentrantLock来实现。锁在并发高的情况下,性能相对来说是比较低下的,Disruptor框架没有用到锁,使用CAS技术解决并发问题