Java并发编程之美
阅读开源框架的一点心得
为什么要看源码
-
由经验不足导致的问题
- 不知道如何去设计,就看当前系统类似需求的设计,然后去仿照
- 设计的时候,考虑不周全
工作经验的积累来自于年限与实践,看源码可以扩展思路
-
可以解决经验不足的办法
-
通过学习开源框架、开源项目来获取经验
- 对于比较大型的项目,可能迷失在源码中,需要找到自己想要的点
-
-
看源码的好处
如果有阅读源码的经验,研究新系统的代码逻辑时就不会那么费劲
当你使用框架或者工具做开发时,如果对它的实现有所了解,就能最大化的减少出故障的可能
解决比较难的问题
快速解决问题
-
开阔思维,提升架构设计能力
- 有些东西靠书本和自己思考是很难学到的,必须通过看源码,看别人如何设计,然后思考为何这样设计才能领悟到
打发时间
-
能力的提高
-
不在于写了多少代码,做了多少项目,而在于给一个业务场景时,能拿出几种靠谱的解决方案,并且说出各自的优缺点
- 靠经验和归纳总结
- 看源码可以快速增加经验
-
如何看源码
在看某个框架的源码前,先去Google查找这个开源框架的官方介绍,通过资料了解该框架有几个模块,各个模块是做什么的,之间有什么联系,每个模块都有哪些核心类,在阅读源码时可以着重看这些类。
-
对哪个模块感兴趣就写demo,了解这个模块的具体作用,然后再debug进入看具体实现
- 在debug的过程中,第一遍是走马观花,简略看一下调用逻辑,都用了哪些类;第二遍需有重点的debug,看看这些类担任了架构图例的哪些功能,使用了哪些设计模式。第三遍debug,最好把主要类的调用时序图以及类图结构画出来,等画好后,再对时序图分析调用流程,知道类之间的调用关系,通过类图可以知道类的功能以及它们之间的依赖关系
-
阅读开源框架里每个类或者方法上的注释
- JUC包里的一些并发组件的注释,就已经说明了它们的设计原理和使用场景
在阅读源码时,最好画出时序图和类图,因为人总是善忘的。避免每次从头开始debug
查框架使用说明最好去官网查
研究代码时,不一定非要debug三遍,这只是三种掌握程度,如果debug一遍就能掌握,那自然好
自己写简易
看源码相关的坑
项目基本停止维护,官网不更新
-
项目过于小众,包括国内使用比较少、项目比较新,网上资料很少
- 比如当时的Laravel
官方文档、博客与实际项目不符,版本不一致
开源项目质量良莠不齐
什么源码比较适合看
- 经典的开源项目,比如spring、netty、jdk、可供参考的开源项目
Java并发编程基础篇
并发编程线程基础
-
什么是线程
进程是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个新城共享进程的资源
CPU是比较特殊的资源,它是被分配到线程的,因为真正要占用CPU运行的是线程。线程是CPU分配的基本单位
在Java中,当启动main函数时,其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,称为主线程
-
进程和线程的关系
- 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域
- 程序计数器是一块内存区域,用来记录线程当前要执行的指令地址
- 之所以设置程序计数器为私有,是为了在CPU轮询时保存程序执行地址
- 如果执行的是native方法,那么pc计数记录的是undefined地址,只有执行的是Java代码时pc计数器记录的才是下一条指令的地址
- 线程自己的栈资源,用于存储该线程的局部变量,这些局部变量是该线程私有的,其他线程是访问不了的,栈还用来存放线程的调用栈帧
- 堆是一个进程中最大的一块内存,堆是被进程中的所有线程共享的,是进程创建时分配的,堆里面主要存放使用new操作创建的对象实例
- 方法区则用来存放JVM加载的类、常量及静态变量等信息,也是线程共享的
-
线程创建与运行
-
Java中有三种线程创建方式
-
实现Runnable接口的run方法
- 实现Runnable接口可以继承其他类,缺点是没有返回值
-
继承Thread类并重写run的方法
- 好处是在run()方法内获取当前线程直接使用this就可以了,无需使用Thread.currentThread()方法;缺点是Java不支持多继承,如果继承了Thread类,那么就不能继承其他类;任务域代码没有分离,当多个线程执行一样的任务时需要多分任务代码,而Runnable则没有这个限制,没有返回值
使用FutureTask方式
这里需要一个表格来比较各个创建方式的优缺点
线程的状态转换图是啥?条件是啥?怎么查看线程的状态?终止了还能再运行吗?
-
当创建完thread对象后该线程并没有被启动执行,直到调用了start方法后才真正启动了线程
调用start方法后吸纳还曾并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会处于运行状态。一旦run方法执行完毕,该线程就处于终止状态
-
-
线程通知与等待
-
Java中的Object类是所有类的父类,Java把所有类都需要的方法放到了Object类里面,包括通知与等待系列函数
- Object类有哪些方法?
- 为什么把等待、通知放到Object类中?
-
等待与通知系列函数
-
wait()函数
-
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生几件事才返回
- 其他线程调用了该共享对象的notify()或者notifyAll()方法
- 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回
如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常
-
一个线程获取一个共享变量的监视器锁的方式
-
执行synchronized同步代码块时,使用该共享变量作为参数
- synchronized(共享变量){}
-
调用该共享变量的方法,并且该方法使用了synchronized修饰
- synchronized void add(int a, int b){}
-
-
什么是共享变量?
- 如果要给变量再多个线程中都使用到了,那么这个变量就是这几个线程的共享变量
-
一个线程可以从挂起状态变为可以运行状态(唤醒),即使该线程没有被其他线程notify()、notifyAll()方法进行通知,或者被中断,或者等待超时,这就是所谓的虚假唤醒
-
虽然虚假唤醒在应用实践中很少发生,但要防患于未然,做发表就是不停区测试该线程被唤醒的条件是否满足,不满足则继续等待,就是说在一个循环中调用wait()方法进行防范。退出循环的条件是满足唤醒该线程的条件
-
生产者-消费者演示
- 生产者
- 消费者
自己默写还是写不出来
-
-
执行wait()会释放持有的锁
-
-
wait(long)
- 如果要给线程调用共享对象的该方法挂起后,没有在指定的timeout ms实践内被其他线程调用该共享变量的notify()或者notifyAll()唤醒后,那么该函数还是会因为超时而返回。如果将timeout设置为0则和wait方法效果一样。传递一个负的,会抛出异常
wait(long, int)
-
notify()函数
- 一个线程调用共享对象的notify()方法后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的
- 只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的noitfy方法,否则抛出异常
-
notifyAll()函数
- 唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程
-
-
-
等待线程执行终止的join方法
- 主线程调用threadOne.join()方法后被阻塞,等待threaOne执行完毕后返回
- 线程A调用线程B的join方法后会被阻塞,当其他线程调用了线程A的interrupt()方法中断线程A时,线程A会抛出InterrutedException异常而返回
- CountDownLatch是不错的选择
-
让线程睡眠的sleep方法
- Thread类中有一个静态的sleep方法,当一个执行中的线程调用了Thread的sleep方法后,调用线程会暂时让出指定时间的执行权,即不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁,还是持有不让出的
- 指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与CPU的调度,获取到CPU资源后就可以继续运行了
- 如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回
-
让出CPU执行权的yield方法
- Thread类中有一个静态的yield方法,当一个线程调用yield方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU使用,但是线程调度器可以无条件忽略这个暗示
- 当一个线程调用yield方法时,当前线程会让出CPU使用权,然后处于就绪状态
- 一般很少使用这个方法,在调式或者测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,在设计并发控制时或许有用途
- sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定时间,在这期间线程调度器不会区调度该线程。而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态
-
线程中断
Java中线程中断,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理
-
几个方法
-
void interrupt()
- 中断线程。当线程A运行时,线程B可以调用线程A的interrupt()方法来设置线程A的中断标志为true并立即返回。设置标志仅仅是设置标志,线程A实际并没有被中断,它会继续往下执行。
-
boolean isInterrupted()
- 检查当前线程是否被中断,如果是返回true,否则返回false
-
boolean interrupted()
- 检查当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志,并且该方法是static方法,可以通过Thread类直接调用
-
如何响应中断
-
理解线程上下文切换
什么是上下文切换
-
上下文切换时机
- 当前线程的CPU时间片使用完处于就绪状态时
- 当前线程被其他线程中断时
-
线程死锁
-
什么是线程死锁
死锁是指两个或两个以上线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去
死锁的分类
-
死锁产生必备的四个条件
-
互斥条件
- 指线程堆已获取到的资源进行排他性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源
-
请求并持有条件
-
指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并部释放自己已经获取的资源
- 就是至少争夺两个资源
-
-
不可剥夺条件
- 指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源
-
环路等待条件
- 指在发生死锁时,必然存在一个线程一资源的环形链
-
死锁演示代码
一般发生死锁的场景
-
如何避免线程死锁
只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有和环路等待条件是可以被破坏的
-
造成死锁的原因和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁
- 相同的申请顺序
-
守护线程与用户线程
Java中线程分为两类,分别为daemon线程和user线程
-
区别
- 当没有非守护线程时,JVM会正常退出,而不管当前是否有守护线程,即守护线程是否结束并不影响JVM的退出
-
-
ThreadLocal
-
作用
- 提供线程本地变量,就是如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题
使用示例
-
实现原理
- 其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。ThreadLocal就是一个工具壳,他通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用
不支持继承性
-
InheritableThreadLocal类
- 让子线程可以访问再父线程中设置的本地变量
-
并发编程的其他基础知识
什么是多线程并发编程
为什么要进行多线程并发编程
-
Java中的线程安全问题
- 共享资源就是被多个线程访问的资源
- 线程安全问题是指多个线程同时读写一个共享资源,并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果问题
-
Java中共享变量的内存可见性问题
-
Java内存模型
- 子主题 1
- Java内存模型归档,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作内存,线程读写变量时操作的是自己工作内存中的变量
- 实际实现中的工作内存
- 内存可见性问题
-
-
Java中的synchronized关键字
-
介绍
- synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以作为要给同步锁来使用。
- 线程的执行代码再进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时被阻塞挂起
- 拿到内部锁的线程会再正常退出同步代码块或者抛出异常后或者再同步块内调用了该内置锁资源的额wait系列方法时释放该内置锁
- 内置锁是排他锁
- synchronized的使用会导致上下文切换。因为阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作
- 关键词:内置锁、阻塞、获取释放锁 、排他锁、上下文切换
-
内存语义
- 进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取
- 退出synchronized块的内存语义是把在synchronized块内堆共享变量的修改刷新到主内存
-
其实也是加锁和释放锁的语义
- 当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存
-
用途
- 解决共享变量内存可见性问题
- 原子性操作
- 有序性
-
注意
- synchronized关键字因为阻塞会引起线程上下文切换,并带来线程调度开销
-
-
Java中的volatile关键字
引入的背景是在解决共享内存可见性使用锁太笨重,因为它会带来线程上下文的切换开销
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主存。当其他线程读取该共享变量时,会从主存重新获取最新值,而不是使用当前线程的工作内存中的值
-
内存语义
- 当线程写了volatile变量值时等价于线程退出synchronized同步代码块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步代码块(先清空本地内存变量值,再从主内存获取最新值)
-
作用
- 提供可见性
-
使用场景
- 写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取-计算-写入散步操作,这三部操作不是原子性的,而volatile不保证原子性
- 读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为volatie
-
Java中的原子性操作
原子性操作是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况
通过javap -c查看汇编代码,来知道某操作是不是原子性的
-
保证原子性的方法
-
synchronized关键字
- 是独占锁
- 对于读操作,是为了保证多线程的可见性
原子性操作类AtomicLong之类的
-
-
Java中的CAS操作
使用锁不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销
非阻塞volatile关键字解决共享变量的可见性问题,一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读-写-写等原子性问题
-
Compare and Swap
- 它是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新操作的原子性。
- JDK里面的Unsafe类提供了一系列的compareAndSwap*方法
-
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)
- 四个参数,分别时对象内存位置、对象中变量的便宜量、变量预期值和新的值
- 含义是,如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性指令
-
ABA问题
- 因为变量的状态值产生了唤醒转换,就是变量的值从A到B,然后再从B到A
- AtomicStamepdReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生
-
Unsafe类
-
重要方法
JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库
-
几个主要的方法
- long objectFieldOffset(Field filed):返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用
- int arrayBaseOffset(Class arrayClass):获取数组中第一个元素的地址
- int arrayIndexScale(Class arrayClass):获取数组中一个元素占用的字节
- boolean compareAndSwapLong(Object obj, long offset, long expect, long update):比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false
- public native long getLongvolatile(Object obj, long offset):获取对象obj中偏移量为offset的变量对应volatile语义的值
- void putLongvolatile(Object obj, long offset, long value):设置obj对象中offset偏移类型为long的field的值为value,支持volatile语义
- void putOrderedLong(Object obj, long offset, long value):设置obj对象中offset便宜地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改堆其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法
- void park(boolean isAbsolute, long time):阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是相对值,是个增量值,就是相当于当前时间类加time后当前线程就会被唤醒。如果isAbsolute等价于true,并且time大于0,则表示阻塞的线程到指定的时间点会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。
- 等等
如何使用Unsafe类
-
-
Java指令重排序
Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题
重排序是针对的单一线程内执行的指令重排序,多线程之间不存在
-
volatile修饰的变量具有避免重排序问题
- 写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序volatile读之前
-
伪共享
- 什么是为共享
- 为何出现为共享
- 如何避免伪共享
- 小结
-
锁的概述
-
乐观锁与悲观锁
- 乐观锁和悲观锁是在数据库中引入的名词,但在并发包锁里面也引入了类似思想
- 悲观锁指对数据被外界修改保持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,即在数据库中,在对数据记录擦做前给记录加排它锁。如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常。如果获取锁成功,则对记录进行操作,然后提交事务后释放排它锁
- 乐观锁认为数据在一般情况下不会造成冲突,所以在访问记录前不会加排它锁,而是在进行数据提交更新时,才会正式对数据冲突与否进行检测。
- 乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁知道提交时才锁定,所以不会产生任何死锁
- 乐观锁悲观锁分别有哪些?
-
公平锁与非公平锁
根据线程获取锁的抢占机制划分
公平锁表示线程按照先来先到的方式获取锁,而非公平锁则是随机分配锁
-
ReentrantLock提供了公平和非公平锁的实现
公平锁:ReentrantLock pairLock = new ReentrantLock(true)
非公平锁:ReentrantLock noPairLock = new ReentrantLock(false)。如果构造函数不传递参数,则默认是非公平锁。 在没有公平需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销
-
独占锁与共享锁
- 根据锁只能被单个线程持有还是能被多个线程共同持有划分
- 独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。共享锁则可以同时由多个线程持有,例如ReadWriteLock读写锁,它允许一个资源可以被多个线程同时进行读操作
- 独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取
- 共享锁则是一种乐观锁,它放宽了加锁条件,允许多个线程同时进行读操作
-
什么是可重入锁
-
当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时,如果不被阻塞,那么该锁就是可重入的,也就是只要该线程获取了该锁,可以有限次的进入被该锁锁住的代码
- 子主题 1
synchronized内部锁是可重入锁。
原理:在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起。但是当获取了该锁的线程再次获取锁时发现拥有者是自己,就会把计数值+1,当释放锁后计数器值-1.当计数器值为0时,所里面的线程表示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁
-
-
自旋锁
- 当一个线程在获取锁失败后,会被切换到内核状态而被挂起。当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。
- 自选锁是,当前线程在获取锁时,如果发现已经被其他线程占有,他不马上阻塞自己,在部放弃CPU使用权的情况下,多次尝试获取(默认10次,可以使用-XX:PreBlockSpinsh参数设置),很有可能在后面几次尝试中其他线程已经释放了锁。如果尝试制定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。自旋锁是使用CPU时间换取线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费了。
数据库中的悲观锁和乐观锁及其实现
-
总结
Java并发编程高级篇
Java并发包中ThreadLocalRandom类原理剖析
-
Random类及其局限性
- 多个线程使用同一个原子性种子变量,使用CAS,从而导致对原子变量更新的竞争
- 自选重试导致的降低并发性能
-
ThreadLocalRandom
- 每个线程都维护一个种子变量,每个线程生成随机数时都根据自己老的种子计算新的种子,并使用新种子更新老的种子,再根绝新种子计算随机数,就不会存在竞争问题了,这大大提供并发性能
- 并发性能:ThreadLocal > CAS
- 并发性能更好
- 重写了nextInt()及相关方法,在current()生成种子时,使用了System.currentTimeMills()和System.nanoTime()
- 计算新种子时是根据自己线程内维护的种子变啊零进行更新,从而避免了竞争
-
源码分析
-
类图结构
- 子主题 1
-
主要代码的实现逻辑
- Unsafe机制
- ThreadLocalRandom current()方法
- int nextInt(int bound)
-
总结
-
问题
使用方式有问题,如果只实例化一个实例,然后在多个线程中访问,不就不需要使用处理并发问题了?
为什么要AtomicLong修饰,不是局部变量么?又不是共享变量?
-
两个seed有啥不同?
- 赋值对象的化,是相同的引用
Atomic类型变量的原子性在这里有啥用?
ThreadLocalRandom对相同实例,不会造成生成相同的伪随机数吗?
Java并发包中原子操作类原理剖析
JUC提供了一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大提高
原子操作对可见性、有序性、原子性的保证?
-
原子变量操作类
- JUC并发包中包含有AtomicInteger、AtomicLong和AtomicBoolean等原子性操作类,它们的原理类似
- 内部使用Unsafe来实现
虽然Atomic系原子操作类使用CAS非阻塞算法,但是在高并发情况下AtomicLong还会存在性能问题。因此提供了高并发下性能更好的LongAdder类
-
JDK 8新增的原子操作类LongAdder
-
LongAdder简单介绍
- 为了克服AtomicLong由于多线程同时去竞争一个变量的更新而产生的,那么把一个变量分解为多个变量,让同样多的线程去竞争多个资源,就解决了性能问题
- 使用AtomicLong时,多个线程同时竞争同一个原子变量
- 使用LongAdder时,则是在内部维护多个Cell变量,每个Cell里面有一个初始值为0的long型变量,这样,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相减少了争夺共享资源的并发量。另外,多个线程在争夺同一个Cell原子变量时如果失败了,它并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试CAS成功的可能性。最后,在获取LongAdder当前值时,是把所有Cell变量的value值累加后再加上base返回的
-
LongAdder代码分析
-
问题
- LongAdder的结构是怎么样的?
- 当前线程应该访问Cell数组里面的哪一个Cell元素?
- 如何初始化Cell数组?
- Cell数组如何扩容?
- 线程访问分配的Cell元素有冲突后如何处理?
- 如何保证线程操作被分配的Cell元素的原子性
-
类图
-
小结
-
-
LongAccumulator类原理探究
LongAdder类是LongAccumulator的一个特里,LongAccumulator比LongAdder的功能更强大。
-
构造函数
- 子主题 1
传的参数是个函数是接口类型,可以自定义计算类型,LongAdder是制定了累加运算,
关键是函数式接口类型参数
总结
Java并发包中List源码剖析
-
介绍
- 并发包中的List只有CopyOnWriteArrayList
- CopyOnWriteArrayList是一个线程安全底ArrayList,对其进行的修改操作都是再底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略
-
类图
- 子主题 1
- ReentrantLock独占锁对象用来保证同时只有一个线程对aray进行修改
-
问题
-
如果让我们自己做一个写时复制的线程安全的list我们会怎么做,有哪些点需要考虑?
-
何时初始化list,初始化的list元素个数为多少,list是有限大小吗?
- 无界list
如何保证线程安全,比如多个线程进行读写时如何保证是线程安全的?
如何保证使用迭代器遍历list时的数据一致性?
如何保证性能?
-
-
-
主要方法源码剖析
初始化
-
添加元素
- 数组赋值时不是传的引用么?
获取指定位置元素
修改指定元素
-
删除元素
- 出现弱一致性问题
-
弱一致性的迭代器
- 弱一致性是指返回迭代器后,其他线程对list的增删改对迭代器是不可见的,因为迭代和增删改操作的是两个不同的数组
- COWIterator对象的snapshot变量保存了当前list的内容,cursor是遍历list时数据的下表
- 老数组被snapshot引用
总结
-
相似实现
- CopyOnWriteArraySet
Java并发包中锁原理剖析
-
LockSupport工具类
-
介绍
- JDK中rt.jar报里面的LockSupport是要个工具类,它的主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础
- LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是使用Unsafe类实现的
-
基本方法
-
void park()
- 如果调用park()的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被阻塞挂起
- 返回方式是使用unpark(Thread thread)或者调用阻塞线程的interrupt()
-
void unpark(Thread thread)
- 当一个线程调用unpark时,如果参数thread线程没有持有thread与LockSupport类关联许可证,则让thread线程持有。如果thread之前因调用park()而被挂起,则调用unpark()后,该线程会被唤醒。如果thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回
- 简而言之,就是给参数线程一个LockSupport类关联的许可证
- 如果调用两次unpark()会出现什么情况呢?
-
void parkNanos(long nanos)
- 和park方法类似。区别是,如果没有拿到许可证,则调用线程会被挂起nanos时间后修改为自动返回
-
void park(Object blocker)
- 当线程没有持有许可证的情况下,调用park方法而被阻塞时,这个blocker对象会被记录到该线程内部
- 使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以JDK推荐我们使用带有blocker参数的park方法,并且blocker被这是为this,这样挡在打印线程堆栈排查问题时就能知道是哪个类被阻塞了
- 使用带有blocker参数的park方法,线程堆栈可以提供更多有关阻塞对象的信息
-
void parkNanos(Object blocker, long nanos)
- 相比park(Object blocker)多了个超时时间
-
void parkUntil(Object blocker, long deadline)
- 制定毫秒的时间点
-
获取当前线程Thread.currentThread()
-
-
抽象同步队列AQS概述
AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。虽然一般不会直接使用AQS,但是直到其原理对于架构设计有帮助
-
锁的底层支持
-
类图
- 子主题 1
-
-
条件变量的支持
-
说明
-
notify和wait是配合synchronized内置锁实现线程间同步的基础设施
- 在调用共享变量的notify和wait方法前必须首先获取该共享变量的内置锁。在调用条件变量的signal和await方法前必须限获取条件变量对应的锁
条件变量signal和await方法是配合使用AQS实现的锁来实现线程间同步的基础设施
-
-
基于AQS实现自定义同步器
-
独占锁ReentrantLock的原理
- 类图结构
- 获取锁
- 释放锁
- 案例介绍
- 小结
-
读写锁ReentrantReadWriteLock的原理
- 类图结构
- 写锁的获取与释放
- 读锁的获取与释放
- 案例介绍
- 小结
-
JDK 8中新增的StampedLock锁探究
- 概述
- 案例介绍
- 小结
-
Java并发包中并发队列原理剖析
-
ConcurrentLinkedQueue原理探究
- 类图结构
- ConcurrentLinkedQueue原理介绍
- 小结
-
LinkedBlockingQueue原理探究
- 类图结构
- LinkedBlockingQueue原理介绍
- 小结
-
ArrayBlockingQueue原理探究
- 类图结构
- ArrayBlockingQueue原理介绍
- 小结
-
PriorityBlockingQueue
- 介绍
- 类图结构
- 原理介绍
- 案例介绍
- 小结
-
DelayQueue原理探究
- 类图结构
- 主要函数原理讲解
- 案例介绍
- 小结
-
-
Java并发包中线程池ThreadPoolExecutor原理探究
介绍
类图介绍
-
源码分析
- public void execute(Runnable command)
- 工作线程Worker的执行
- shutdown操作
- shutdownNow操作
- awaitTermination操作
总结
-
Java并发包中ScheduledThreadPoolExecutor原理探究
介绍
类图介绍
-
原理剖析
- schedule(Runnable command, long delay, TimeUnit unit)
- scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
- scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
总结
-
Java并发包中线程同步原理剖析
-
CountDownLatch原理剖析
- 案例介绍
- 实现原理探究
- 小结
-
回环屏障CyclicBarrier原理探究
- 案例介绍
- 实现原理探究
- 小结
-
信号量Seaphore原理探究
- 案例介绍
- 实现原理探究
- 小结
总结
-
Java并发编程实践篇
并发编程实践
-
ArrayBlockingQueue的使用
- 异步日志打印模型概述
- 异步日志与具体实现
- 小结
-
Tomcat的NioEndPoint中ConcurrentLinkedQueue的使用
- 生产者线程——Acceptor线程
- 消费者——Poller线程
- 小结
并发组件ConcurrentHashMap使用注意事项
-
SimpleDateFormat是线程不安全的
- 问题复现
- 问题分析
- 小结
-
使用Timer时需要注意的事情
- 问题的产生
- Timer实现原理分析
- 小结
-
对需要复用但是会被下游修改的参数进行深复制
- 问题的产生
- 问题分析
- 小结
-
创建线程和线程池时要指定与业务相关的名称
- 创建线程需要有线程名
- 创建线程池时也需要指定线程池的名称
- 小结
-
使用线程池的情况下当程序结束时记得调用shutdown关闭线程池
- 问题复现
- 问题分析
- 小结
-
线程池使用FutureTask时需要注意的事情
- 问题复现
- 问题分析
- 小结
-
使用ThreadLocal不当可能导致内存泄漏
- 为何会出现内存泄漏
- 在线程中使用ThreadLocal导致的内存泄漏
- 在Tomcat的Servlet中使用ThreadLocal导致内存泄漏
- 小结
总结
XMind: ZEN - Trial Version