java并发

Java并发

面试题

基础

进程与线程

  1. 定义

    • 进程
      • 程序的一次执行过程(进程从创建到消亡的过程)
      • 系统运行程序的最小单元
    • 线程
      • 共享进程的堆与方法区
      • 线程自有 程序计数器、虚拟机栈、本地方法栈
  2. 对比

    • 关系

      • 线程是进程划分的更小的运行单元
      • 同一个进程的多个线程极有可能互相影响
    • 区别

      • 进程相互之间是独立的
      • 线程互相之间极有可能互相影响
    • 优缺点

      • 线程执行开销小、不利于资源的管理与保护
      • 进程执行开销大、可以有效的管理和保护资源
  3. 程序计数器(为什么私有)

    • 字节码解释器通过改变程序计数器来依次读取指令,实现代码的流程控制
    • 在多线程情况下,程序计数器记录当前线程执行的位置,保证上下文切换的时候
      当前线程还在之前执行的位置
    • 作用:上下文切换之后,线程能恢复到正确的位置
  4. 虚拟机栈与本地方法栈(为什么私有)

    • 虚拟机栈:Java方法在执行的时候,用来存储局部变量、操作数栈、常量池等
      引用信息。入栈与出栈等同于方法的调用与执行结束
    • 本地方法栈:虚拟机栈是为虚拟机执行Java方法(字节码)服务,本地方法为虚拟
      机使用到的native服务
    • 为了保证现线程中的局部变量不被别的线程访问,所以都是线程私有的
  5. 堆与方法区

    • 都是线程的共享资源
    • 堆:内存中最大的区域,存储对象
    • 方法区:存放已被加载的类信息、常量、静态变量、即时编译后的代码

并发与并行

  • 并发

    • 同一时间段,多个任务都在执行(单位时间内不一定执行)
  • 并行

    • 单位时间内,多个任务执行

多线程

  • 多线程的优点

    • 总体来说

      • 从计算机底层:多核CPU时代,多个线程可以同时运行,减少线程上下文切换的
        开销
      • 多线程并发编程利于开发高并发系统,可以大大提高系统整体的并发能力以及性
    • 计算机底层

      • 单核时代:多线程为了提高CPU 和O设备的综合利用率
      • 多核时代:提高CPU利用率,多线程可以利用多个CPU核心
  • 多线程的缺点

    • 内存泄漏
    • 上下文切换
    • 死锁
    • 硬件与软件资源闲置的问题

线程的生命周期

  • 6种状态

    • NEW 初始状态,新建的线程
    • RUNNABLE 运行状态,操作系统中的就绪与运行两种状态统称为“运行中”
    • BLOCKING 阻塞状态,线程阻塞于锁
    • WAITE 等待状态,当前线程需要等待其他线程做出一些特定状态(通知或者中断)
    • TIME_WAITE 超时等待状态,在指定时间内自行返回
    • TERMINATED。终止状态 当前线程已经执行完毕
  • 图示:

上下文切换

  • 简介:CPU采取的策略是为每个线程分配时间片并轮询的方式。一个线程的时间片用完的时候,重新就绪让其他线程使用,这就是一次上下文切换
  • 终结:当前任务执行完的时间片切换到另一个任务之前会保存自己的状态,以便下次切换回来能仔加载这个任务的状态
  • Linux Linux系统上下文切换和模式切换的时间消耗非常少

线程死锁

  • 死锁定义

    • 多个线程同时被阻塞,或者一个或者多个线程在等待某个资源释放,线程会被无限制的阻塞,程序无法终止
  • 产生死锁的四个条件

    • 互斥条件:某个资源只能被一个线程使用
    • 请求和保存条件:一个线程因为请求某个资源阻塞时,对自己已获得的资源不释放
    • 不剥夺条件:线程已获得的资源在未使用完成时,不能被其他的线程剥夺。只能自己使用完成后释放
    • 循环等待条件:多个线程形成收尾相接的循环等待资源的情况
  • 避免死锁的方法

    • 破坏互斥条件:没得办法
    • 破坏请求和保存条件:一次性申请所有的资源
    • 破坏不剥夺条件:已获得部分资源的线程,在申请其他资源受阻的时候,主动释放以获取的资源
    • 按序申请资源来预防,破坏循环等待的条件

sleep()与wait()

  • 区别

    • sleep()方法不释放锁,wait()方法释放锁
    • sleep()方法用于线程等待,wait()方法主要用于线程间通信与交互
    • sleep()方法,方法执行完成之后,会主动唤醒;wait()方法方法执行完成,需要别的线程调用对象的notify()或者notifyAll()方法才能唤醒线程
  • 共同点

    • 都是使线程等待

进阶

  • synchronized关键字

    • 简介

      • 解决多个线程访问资源的同步性
      • JDK1.6版本进行优化:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
    • 使用方式

      • 修饰实例方法:用于实例对象的加锁,进入同步代码前需要获取实例对象的锁

      • 修饰静态方法

        • 用于当前类加锁,作用于该类的所有实例对象。
        • 访问静态synchronized方法是获取类的锁,访问非synchronized方法或获取当前实例对象的锁,所以可以共存
      • 修饰语句块

        • 对给定对象加锁,进入同步代码代码块之前要获取给定对象的锁
    • 底层原理

      • 同步代码块

        • moniterenter指令是同步代码块开始的位置
        • moniterexit指令是同步代码块结束的位置
      • 修饰方法

        • ACC_SYNCHRONIZED标识,指明该方法是一个同步方法
        • JVM通过ACC_SYNCHRONIZED判断某个方法是否是同步方法
    • 优化

      • JDK1.6之后对锁的优化,加入了如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

      • 锁的四种状态

        • 无锁状态
        • 偏向锁状态
        • 轻量级锁状态
        • 重量级锁状态
      • 锁可以升级不能降级,目的是为了获取锁与释放锁的效率

    • 与ReentrantLock区别

      • 相同点:都是可重入锁

        • 可重入锁:自己可以重新获取自己的锁
        • 这样能够减少死锁
        • 举例:当一个线程获取某个对象的锁,此时该对象的锁还没有释放,当前线程还是可以再次获取该对象的锁。
        • 线程每次获取锁,对象锁的计数器都会加1,只有对象锁的计数器为0,才能释放锁
      • 不同点

        • 子主题 1
  • volatile关键字

    • 内存模型

      • 后续画图
    • 与synchronized区别

      • volatile关键字是线程同步的轻量级实现

      • 性能

        • volatile性能较好
        • synchronized关键字性能较低
      • 修饰对象

        • volatile关键字修饰变量
        • synchronized关键字修饰实例方法、静态方法、代码块
      • 阻塞与否

        • volatile关键字不阻塞
        • synchronized关键字可能阻塞
      • 原子性

        • volatile关键字可以保证数据的可见性,但是不能保证原子性
        • synchronized关键字 即可保证数据的可见性,也可以保证数据的原子性
      • 目的

        • volatile关键字 主要是解决变量在多个线程之间的可见性
        • synchronized关键字 主要解决多线程在访问资源时候的同步性
  • ThreadLocal

    • 简介

      • ThreadLocal类 主要是解决让每个线程绑定自己的值,用来存储每个线程自己的私有数据
      • 每个线程在访问ThreadLocal变量的时候会创建一个本地副本,使用get()和set()方法获取与改变值
    • 代码示例

      • 另图
    • 原理分析

      • 底层是一个HashMap,ThreadLocalMap类型的变量。Thread.currentThread().getMap(Thread)访问ThreadLocalMap对象
      • ThreadLocalMap的数据结构:key为Thread对象,value为Object对象
    • 内存泄漏问题

      • ThreadLocalMap 对key的引用是弱引用,而对value的引用是强引用,垃圾回收的时候会把key回收,但是value没有回收;会出现个key为null的value,会造成内存溢出

      • 解决方法:ThreadLocalMap通过调用remove()方法清楚key为null的值

      • 弱引用

        • 简介
        • 与软引用的区别
  • 线程池

    • 线程池的优点

      • 降低资源消耗: 重复利用已建的线程,降低线程创建于销毁的消耗
      • 提高反应速度
      • 提高线程的可管理性: 使用线程池可以对线程进行统一的分配、调优、监控
    • Executor框架

      • 简介

        • 易于对线程进行管理,线程工厂、队列、饱和策略等。启动线程比Thread.start()更好,解决this逃避的问题
        • this逃避:构造函数返回之前,其他线程就拥有该对象的引用,调用未构造完成的对象,会出现错误
      • 框架结构

        • 任务(Runnable/Callable)

          • 需要实现Runnable接口或者Callable接口
          • 这样可以被ThreadPoolExecutor或者ScheduledThreadPoolExecutor执行
        • 任务的执行(Executor)

          • 核心接口Executor接口 子接口ExecutorService接口,ThreadPoolExecutor和ScheduledThreadPoolExecutor实现上述接口

          • Executor接口

            • ExecutorService接口

              • AbstractExecutorService抽象类

                • ThreadPoolExecutor实现类
              • ScheduledExecutorService接口

                • ScheduledThreadPoolExecutor实现类,继承extends ThreadPoolExecutor类
        • 异步计算的结果(Future)

          • Future接口以及实现类FutureTask都可以代表异步计算的结构
          • 实现Runnable或者Callable接口的类提交给ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。
      • 使用示意图以及释义

        • 主线程首先创建实现Runnable/Callable接口的任务对象

        • 把创建完成的对象交给ExecutorService执行

          • ExecutorService.executor(Runnable command)
          • ExecutorService.submit(Runnable task)
          • ExecutorService.subnit(Callable task)
        • 执行结束,返回一个Future接口的对象

        • 最后,主线程执行FutureTask.get()方法等待任务执行完成。或者执行FutureTask.cancel(boolean myInterruptIfRunning)取消任务

    • 几种比较

      • Runnable接口与Callable接口区别

        • 返回和异常处理

          • Runnable不会返回值也不会抛出异常
          • Callable 返回一个Future对象、也会抛出异常
        • 工具类Executors可以实现 Runnable接口与Callable接口的转换

      • Excute()与submit()区别

        • executor()方法用于没有返回值的任务,无法判断线程池是否执行完成
        • submit()方法 会返回一个Future类型对象(判断任务是否执行完成),Future对象的get()方法会阻塞线程,直到任务执行完成
      • shutdown与shutdownNow

        • shutdown 关闭线程,线程池的状态为SHUTDOWN,线程池不再接受新的任务,但是队列中的任务会继续执行完成
        • shutdownNow 关闭线程池,线程池的状态为STOP线程池会终止当前正在进行的任务,并停止处理队列的任务并返回正在等待执行任务的List
      • isTerminated与isShutdown

        • isShutdown 当调用shutdown()方法后返回为true
        • isTerminated 当调用shutdown()方法后,并且将所有提交的任务完成后返回true
    • 创建方式

      • 通过构造方法创建: ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue blockingQueue, ThreadFactory factory)

      • 通过Executor的工具类Executors创建

        • FixedThreadPool

          • 简介:创建一个可重用固定线程数量的线程池

          • 执行任务的过程介绍

            • 如果当前运行的线程数小于coolPoolSize,如果有新任务,直接创建新线程执行
            • 如果当前执行的任务数等于coolPoolSize,如果有新任务,将任务放入LinkedBlockingQueue队列中等待
            • 线程池中的线程执行完手头的任务后,会循环反复从LinkedBlockingQueue中获取任务来执行
          • 缺点

            • 当线程池中的线程数达到corePoolSize后,新任务加入无界队列中等待,则maximum就是一个无效参数,在任务比较多的时候会出现OOM(OutOfMemory) 内存溢出
        • SingleThreadPool

          • 简介:只有一个线程的线程池

          • 执行任务的过程

            • 如果当前没有运行的线程,则会新建线程来执行任务
            • 如果线程池中有一个线程在运行,将新任务加入到LinkedBlockingQueue
            • 线程执行完当前任务,会循环反复从BlockingQueue中获取新任务执行
          • 缺点

            • 无界队列的LinkedBlockingQueue 作为线程池的工作队列,会不断扩充队列,可能导致OOM 内存溢出
        • CacheThreadPool

          • 简介

            • 可根据实际情况调整线程数量的线程池
            • corePoolSize的值为0,maximum的值为Integer.MAX_VALUE
          • 执行任务的过程

            • 首先,执行SynchronousQueue.offer(Runnable task)提交任务到任务队列
            • 如果当前maximum中有闲置线程正在执行SynchronousQueue.pool(keepAliveTime, TimeUnit.NANSECONDS),则主线程执行offer与空置线程执行poll匹配。主线程将任务交给闲置线程执行
            • 如果maximum是空的,或者maximum没有空的线程,此时CachedThreadPool会创建新的线程执行任务
          • 缺点

            • 允许创建的最大线程数为Integer.MAX_VALUE可能会创建大量线程,导致OOM
    • ThreadPoolExecutor 类分析

      • 构造方法中的参数

        • corePoolSize:核心线程数 同时可运行的最小线程数
        • maximumPoolSize:最大线程数,当任务队列满的时候,可以运行线程的最大值
        • keepAliveTime:任务队列满的时候,等待的队列可存活的时间
        • TimeUnit:时间单位,用来设置存活的时间单位
        • BlockingQueue:存放任务的阻塞队列,当前任务打到corePoolSize的时候,任务存在在该队列
        • ThreadFactory: 创建线程的工厂
        • Handle:饱和策略,当corePoolSize、BlockingQueue、maximum都满的时候执行怎么执行任务的策略
      • 饱和策略

        • 定义:同时运行的线程数达到线程池的最大线程数(maximumPoolSize),任务队列也满(BlockingQueue),执行的策略

        • 策略

          • ThreadPoolExecutor.AbortPolicy:拒绝策略,直接抛出RejectExecutionException来拒绝任务的执行
          • ThreadPoolExecutor.CallerRunsPolicy:用执行自己的线程来执行任务
          • ThreadPoolExecutor.DiscardPolicy:不执行任务,直接抛弃掉
          • ThreadPoolExecutor.DiscardOldestPolicy:将最早的任务抛弃掉
    • 代码示例:后续

    • 原理分析

      • corePoolSize已满,将任务放入BlockingQueue
      • BlockingQueue已满,maximum是否已满
      • 如果,maximum已满,执行设定的饱和策略。
    • ScheduledThreadPoolExecutor详解

      • 简介

        • ScheduledThreadPoolExecutor使用DelayQueue 封装的是一个PriorityQueue
        • PriorityQueue会对队列中的任务进行排序,执行所需时间短的会被先执行。ScheduledFutureTask的time变量小的会先执行
        • 如果所需时间相同,则先提交的任务会被先执行(ScheduledFutureTask的squenceNumber变量小的会先执行)
      • ScheduledThreadPoolExecutor与Timer的比较

        • 系统时钟

          • Timer对系统时钟的变化敏感
          • ScheduledThreadPoolExecutor不是
        • 线程执行

          • Timer只是一个执行线程,长时间的运行任务会延迟其他的任务
          • ScheduledThreadPoolExecuator可以配置任意数量的线程,通过ThreadFactory可以控制创建的线程
        • 异常情况

          • TimerTask 中抛出的异常会杀死一个线程
          • ScheduledThreadPoolExecutor会抛出运行时异常,还可以去处理这个异常(重写afterExecute()方法ThreadPoolExecutor)。 抛出异常的任务将被取消,其他线程继续运行
      • 运行机制

        • ScheduledThreadPoolExecutor执行

          • 调用scheduleAtFixedRate()方法或者schduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现RunnableScheduledFuture接口的ScheduledFutureTask
          • 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务
        • 对ThreadPoolExecutor做的修改

          • 使用DelayQueue作为任务队列
          • 获取任务的方式不同
          • 执行周期结束后,增加了额外的处理
      • 执行周期任务的步骤

        • 线程一从DelayQueue中获取已到期的SchduledFutureTask(DelayQueue.take()).到期是指ScheduledFutureTask的time大于等于当前系统的时间
        • 线程一执行这个ScheduledFutureTask
        • 线程一修改ScheduledFutureTask的time变量为下次将要被执行的时间
        • 线程一把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())
  • Atomic原子类

    • 简介:具有原子性或者原子类型的类

    • JUC 中的原子类

      • 基本数据类型

        • AtomicInteger:整型原子类
        • AtomicLong:长整型原子类
        • AtomicBoolean:布尔类型的原子类
      • 数组类型

        • AtomicIntegerArray:整型数组原子类
        • AtomicLongArray:长整型数组原子类
        • AtomicReferenceArray:引用类型数组原子类
      • 引用类型

        • AtomicReference:引用类型原子类

          • 例子
        • AtomicStampedReference:原子更新引用类型里的字段的原子类

          • 例子
        • AtomicMarkableReference:原子更新有标位的原子类

          • 例子
      • 对象的属性修改类型

        • AtomicIntegerFieldUpdater:原子更新整型字段的更新器

          • 例子
        • AtomicLongFieldUpdater:原子更新长整型字段的更新器

          • 例子
        • AtomicStampedFieldUpdater:更新带有版本号的引用类型,可以解决原子的更新数据和数据的版本号,结合使用CAS进行原子更新时可能出现的ABA问题

          • 例子
    • AtomicInteger的使用

      • public final int get() 获取当前值
      • public final int getAndSet(int newValue) 获取当前的值并替换成新值
      • public final int getAndIncrement() 获取当前的值并自增
      • public final int getAndDecrement()获取当前值并自减
      • public final int getAndAdd(int delta)获取当前值,并添加预期的值
      • Boolean compareAndSet(int expect, int update) 如果输入的值等于预期的值,则以原子的方式将该值设置成输入值(upfate)
      • public final void lazySet(int newValue) 最终设置成newValue,使用lazySet设置之后,会使线程在一段时间内还是会读到旧的直播
    • AtomicInteger原理分析

      • 主要是利用CAS(compare and swap)+volatile关键字和native方法保证原子性。避免了synchronized的高开销
      • CAS原理:拿期望的值与原本的值作比较,如果相同则更新成最新的值
    • AutomcticInteger优势

      • 1.多线程情况下不使用原子类,加锁保证线程安全
      • 2.多线程情况下使用原子类,保证线程安全
  • AQS

    • 简介

      • 全称:AbstractQueuedSynchronizer
      • 用来构建锁和同步器的框架
    • 原理

      • 概述

        • 核心思想:如果被请求的共享资源空闲,则将当前的请求资源的线程设置为有效的工作线程,并且将该共享资源设置成锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是利用CLH队列锁实现的,即将暂时获取不到锁的线程放入到队列中
        • CLH队列:全称 Craig,Landin,and Hegersten 是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH队列的一个结点(Node)来实现锁的分配
        • 图示:后附
      • 对资源的共享方式

        • Exclusive(独占):只有一个线程可以执行。如ReentrantLock

          • 公平锁:根据线程的在队列中的先后顺序,先到者先拿到锁
          • 非公平锁:当线程要获取锁的时候,无视先后顺序,直接去抢锁,谁抢到是谁的
        • share(共享):多个线程可以同时执行。

      • AQS 自定义同步器的步骤

        • 继承AbstractQueuedSynchronizer,并重写方法(主要是对共享资源的state的获取与释放)

        • 将AQS组合在自定义的同步组件的实现中,并调用其模板

        • 需要重写的方法

          • isHeldExclusively()该线程是否在独占资源,只有用到Condition才需要去实现它
          • tryAcquire(int) 独占方式,尝试获取资源
          • tryRelease(int) 独占方式,尝试释放资源
          • tryAcquireShared(int) 共享方式,尝试获取资源
          • tryReleaseShared(int) 共享方式,尝试释放资源
        • 自定义同步器的例子

          • 独占方式:以ReentrantLock为例

            • 共享资源的state初始状态为0,表示未锁定状态
            • A线程lock()时,会调用tryAcquire()独占该锁,则state+1
            • 其他线程再tryAcquire()将会失败,直到A线程unlock()到state为0(释放锁),其他线程才有机会获取该锁
            • 在释放锁之前,A线程可以重复获取此锁(state会累加),这就是可重入的概念。获取多少次就要释放多少次,直到state为0
          • 共享凡是:以CountDownLatch为例

            • 任务分成N个子线程去执行,state初始值为N(与子线程数量一致),这N个线程是并行执行的
            • 每个子线程执行完成都会countDown()一次,state会CAS减1,。
            • 等到所有子线程执行完成(也就是state=0),会unpark()主线程,然后主线程就会从await()函数返回,继续后续动作
        • 占用资源的方式

          • 独占方式
          • 共享方式
          • 可以同时实现以上两种:ReentrantReadWriteLock
    • 组件

      • Semaphore(信号量):允许多个线程同时访问,可以指定多个线程同时访问某个资源

      • CountDownLatch(倒计时器)

        • 简介:是一个同步工具类,用来协调多个线程之间的同步,控制线程等待,知道倒计时结束,才继续执行

        • 三种典型用法

          • 1.某个线程在开始运行之前,等待n个线程执行完毕。

            • 将CountDownLatch初始化为n,new CountDownLatch(n),
            • 每当一个任务执行完毕,CountDownLatch减1,countDownLatch.countDown()
            • 当计数器为0,CountDownLatch上的await()的线程被唤醒。
            • 示例:主线程需要等待其他组件加载完成,才执行
          • 2.实现多个线程执行任务的最大并行量。(某一时刻同时运行)

            • 初始化一个共享的CountDownLatch对象,将计数器初始化为1.new CountDownLatch(1)
            • 多个线程开始执行任务之前,countDownLatch.await()
            • 主线程调用countDown时,计数器为0,多个线程被唤醒
          • 死锁检测

            • 使用N个线程访问共享资源,在每次测试阶段的线程数目不同,尝试产生死锁
        • 缺点

          • CountDownLatch是一次性的,计数器的值只能在构造方法中初始化一次。
          • CountDownLatch使用完毕后,不能再次使用
        • 常见面试题

          • 解释一个CountDownLatch概念
          • CountDownLatch与CyclicBarrier的不同之处
          • 给出一些CountDownLatch的使用例子
          • CountDownLatch的主要方法
      • CyclicBarrier(循环栅栏):让一组线程到达一个屏障(同步点)时阻塞,直到最后一个线程到达屏障(同步点),屏障才会开门,所有被屏障拦截的线程才会后续操作。默认构造方法:CyclicBarrier(int parties),parties:代表屏障拦截的线程数,每个线程调用await()方法告诉CyclicBarrier自己已到达屏障

        • 应用场景:用于多线程计算数据,最后合并计算结果的场景

        • 使用示例

        • 源码分析

        • CyclicBarrier与CountDownLatch的区别

          • 使用次数

            • CyclicBarrier提供rest功能,可以重复使用
            • CountDownLatch只能使用一次
          • 重点

            • CountDownLatch:一个或者多个线程等待,其他的线程完成执行后,可以终止或者等待
            • CyclicBarrier:多个线程中,任意一个线程执行完成之后,所有的线程等待
          • 功能

            • CountDownLatch:是一个计数器,线程完成一个纪录一个
            • CyclicBarrier:是一个阀门。所以线程达到阈值,阀门才打开,然后继续执行

必备技能

并发容器

  • ConcurrentHashMap:线程安全的HashMap

    • 读操作时候,基本不需要锁

    • 写操作的时候,通过分段技术对操作的段加锁,不影响其他段的访问

    • 具体实现方式(底层如何实现)

      • JDK1.7

        • 分段实现segment,将HashMap分成很多段,按着段进行加锁
        • 底层数据结构:是segment+数组+链表
      • JDK1.8

        • 取消segment,使用synchronized和CAS来并发控制
        • 底层数据结构:数组+链表+红黑树
  • CopyOnWriteArrayList:线程安全的List,读多写少的场合性能非常好,远远高于Vector

    • 简介:读取的数据的时候不加锁,写操作不会阻塞读操作,只是写入与写入之间阻塞

    • 实现原理

      • CopyOnWriteArrayList 在对数据进行修改时(可变操作 set 、add等),先将要修改的数据复制一份,将要修改的数据写入副本,写完之后再替换原来的数据,此时,写操作不影响读操作
      • CopyOnWrite:在计算机中,对内存进行修改,不在原有内存块中进行写操作,而是拷贝一份,在新内存中进行写操作,写完之后将原有内存指针指向新内存块,原来的内存会被回收
    • 源码分析

      • 读操作:将数组封装到volatile Object[] array中,然后从volatile Object[] array获取数据
      • 写操作:对add操作加锁(ReentrantLocak),拷贝新数组,将先数组写入到原数组
  • ConcurrentLinkedQueue:高效的并发队列,使用链表实现。线程安全的LinkedList,非阻塞队列

    • 阻塞队列通过加锁实现,非阻塞队列通过CAS操作实现
    • 通过链表作为数据结构,高并发下的性能最好的队列,通过CAS非阻塞算法实现线程安全
  • BlockingQueue:这是一个接口,JDK内部通过链表和数组实现这个接口。阻塞队列,适用于作为数据共享的通道

    • 简介:应用于 生产者-消费者问题,当队列已满,生产者阻塞,直到队列有空位;当队列为空,消费者阻塞,直到队列中有数据(非空)

    • ArrayBlockingQueue

      • BlockingQueue有界队列的实现类,底层是数组
      • 容量不可变,并发控制采用可重入锁,无法保证线程访问队列的公平性
    • LinkedBlockingQueue

      • 底层用单向链表实现的阻塞队列
      • 可作为无界队列也可作为有界队列,无界时最大值为Integer.MAX_VALUE
    • PriorityBlockingQueue

      • 支持优先级的无界队列,默认自然顺序排列,可用Comparator来指定排序规则
      • 并发控制使用ReentrantLock
      • PriorityQueue的线程安全版本,不能有null值,插入队列的对象必须是可以比较大小的
  • ConcurrentSkipListMap:调表的实现,这是一个Map,使用跳表的数据结构进行快速查找

    • 跳表:针对单向链表,即将链表分段,高并发下,分段操作,提高性能
    • 维护多个链表,链表是分层的
    • 跳表中的所有元素是有序的,利用空间换时间的算法

java线程池学习总结

  • 上面已经写全

乐观锁与悲观锁

  • 定义

    • 悲观锁

      • 每次去拿数据的时候都认为别人会修改,每次拿数据都会上锁。
      • 传统的关系型数据库就是这种锁机制,行锁、表锁、读锁、写锁等,java中的synchronized和ReentrantLock等独占资源的都是这种
    • 乐观锁

      • 每次去拿数据的时候都认为别人不会去修改数据,所以不会上锁,在更新的时候,判断写一次在此期间别人有没有去更新数据,可以使用CAS和版本号去实现
      • 适用于多读的应用类型,可以调高吞吐量。数据库中的 write_conditon机制。java中java.util.concurrent.atomic包下面的原子变量类就是使用CAS实现的
  • 使用场景

    • 乐观锁:适用于写比较少的情况下(多读场景)
    • 悲观锁:适用于多写的场景
  • 乐观锁的实现方式

    • 版本号机制

      • 数据表中添加version字段,表示修改的次数

        • 修改时候,version会+1
        • 更新的时候,读取数据的同时会读取版本号version值,提交更新的时候,读取的version值与数据库中的version一致才更新
      • 例子:账户信息表中有个version字段,当前值为1,当前用户的余额字段(balace)为$100

        • 操作员A此时读取(version=1),并从账号余额中扣除$50($100-$50)
        • 在操作员A操作的过程中,操作员B也读取此用户信息(verison=1),并从账户扣除$20($100-20)
        • 操作员A完成修改工作,将数据版本号+1(version=2),同时提交扣除账户后账户余额(balance=$50)到数据库。
        • 操作员B完成修改工作,将版本号+1(version=2),同时提交余额(balance=$80)给数据库,比对数据库中的version值。此时数据库的version=2,操作员的提交被驳回。(乐观锁策略:提交的版本号要大于数据库中的版本号才能提交)
    • CAS算法

      • 简介

        • compare and swap(比较和交换),无锁算法,非阻塞同步
        • 不用锁的情况下,实现多线程之间的变量同步
      • CAS算法核心

        • 三个操作数

          • 需要读取的内存值V
          • 进行比较的值A
          • 拟写入的值B
        • 算法

          • 当且仅当V的值等于A时,CAS通过原子方式用新值B替换V
          • 否则不会执行任何操作,比较和替换是一个原子操作
          • 一般情况下是一个自旋操作,会不断的重试
  • 乐观锁的缺点

    • ABA问题

      • 简介:变量V初次读取的时候等于A,准备赋值仍然是A。但是在初次读取数据与准备赋值的中间V的值可能被改成其他值,然后再改成A。但是CAS仍然认为他没有被修改过
      • 解决办法:AtomicStampedReference类中的compareAndSet方法。首先要检查当前引用是否是预期引用,同时当前标志是否等于预期标志,全部相等才会用原子的方式更新值
    • 循环时间长开销大

      • 自旋CAS 如果不成功,一直再重试,这样会使得CPU的开销加大
      • JVM能支持处理器提供的pause指令
    • 只能保证一个共享变量的原子操作

      • 只会对当个共享变量有效
      • 解决办法:利用AtomicReference类保证引用对象之间的原子性;可以将多个变量放在一个对象中进行CAS操作。
  • CAS与sychronized的使用场景

    • CAS使用场景

      • 写比较少的场景,多读场景,冲突比较少
      • 多于资源竞争少的情况,synchronized同步锁需要线程阻塞、唤醒、切换等操作,消耗CPU资源,而CAS基于硬件,能够获得更高的性能
    • synchronized使用场景

      • 写比较多的场景 冲突比较多
      • 对于资源竞争严重的,CAS自旋的几率大。浪费CPU资源
    • synchronized底层实现

      • 依靠Lock-Free的队列
      • 自旋后阻塞,竞争切换后继续竞争锁
      • 稍微牺牲公平性,提高吞吐量

JUC中的Atomic原子类总结

  • 上面有详细介绍

AQS原理以及AQS同步组件

吴超 wechat
subscribe to my blog by scanning my public wechat account