Java并发
面试题
基础
进程与线程
定义
对比
关系
- 线程是进程划分的更小的运行单元
- 同一个进程的多个线程极有可能互相影响
区别
- 进程相互之间是独立的
- 线程互相之间极有可能互相影响
优缺点
- 线程执行开销小、不利于资源的管理与保护
- 进程执行开销大、可以有效的管理和保护资源
程序计数器(为什么私有)
- 字节码解释器通过改变程序计数器来依次读取指令,实现代码的流程控制
- 在多线程情况下,程序计数器记录当前线程执行的位置,保证上下文切换的时候
当前线程还在之前执行的位置 - 作用:上下文切换之后,线程能恢复到正确的位置
虚拟机栈与本地方法栈(为什么私有)
- 虚拟机栈:Java方法在执行的时候,用来存储局部变量、操作数栈、常量池等
引用信息。入栈与出栈等同于方法的调用与执行结束
- 本地方法栈:虚拟机栈是为虚拟机执行Java方法(字节码)服务,本地方法为虚拟
机使用到的native服务
- 为了保证现线程中的局部变量不被别的线程访问,所以都是线程私有的
- 虚拟机栈:Java方法在执行的时候,用来存储局部变量、操作数栈、常量池等
堆与方法区
- 都是线程的共享资源
- 堆:内存中最大的区域,存储对象
- 方法区:存放已被加载的类信息、常量、静态变量、即时编译后的代码
并发与并行
并发
- 同一时间段,多个任务都在执行(单位时间内不一定执行)
并行
- 单位时间内,多个任务执行
多线程
多线程的优点
总体来说
- 从计算机底层:多核CPU时代,多个线程可以同时运行,减少线程上下文切换的
开销
- 多线程并发编程利于开发高并发系统,可以大大提高系统整体的并发能力以及性
能
- 从计算机底层:多核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已满,执行设定的饱和策略。
- corePoolSize已满,将任务放入BlockingQueue
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原子类总结
- 上面有详细介绍