导图社区 JAVA复习重点
这是一篇关于复习重点的思维导图,包含了ACticiti工作流、多线程等。干货满满,有需要的朋友赶紧收藏吧!
编辑于2023-12-21 17:34:50复习重点
JVM
逃逸分析
并不是所有的new对象都会在堆中分配内存,如果一个新new的对象被虚拟机分析,除了方法范围以内,不会有其他地方引用,那么这个对象就会在栈上分配内存
栈上分配对象有大小限制,如果是大对象,也会分配到堆中
在Java代码运行时,通过JVM参数可指定是否开启逃逸分析
-XX:+DoEscapeAnalysis : 表示开启逃逸分析
-XX:-DoEscapeAnalysis : 表示关闭逃逸分析 从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis
双亲委派模型
想要实现一个自定义的类加载器,首先要继承JDK中的ClassLoader类,如果我们要打破双亲委派模型,就去重写他的loadClass方法;如果我们想遵循双亲委派模型,只是实现自定义的加载路径,我们只需要重写findClass
重写findClass时有一个核心方法defineClass, 他可以将一个字节数组转为Class对象,这个字节数组就是.class文件读取后最终的字节数组,也就是说我们只需要通过文件流字节流FileInputStream把.class文件中的字节流读进来,然后通过defineClass反序列化成一个Class对象即可
商业源码Class文件加密
生成的Class字节码文件加密,读取字节流的时候解码
垃圾回收算法
引用计数法
标记清除法
什么是GC Roots
GCRoots即引用链的起点,引出他们所指向的下一个节点,通常为生命周期于JVM一致的对象
那些对象可以作为GC Roots 4种
虚拟机栈 -----栈帧中的局部变量表中引用的对象
本地方法栈 -----即一般说的 Native方法引用的对象
方法区中 类静态属性引用的对象
方法区中 常量引用的对象
垃圾回收器
窜行
Serial
垃圾回收如果时间过长,STW的时间也越长,影响吞吐量
并发
Parallel Scavenge (新生代的 复制回收) / Parallel Old (老年代的 标记清除)
可以指定垃圾回收的时间,如果过长,将停止垃圾回收,执行用户线程 但相应的,垃圾回收次数也将增多,从而导致STW时间过长,影响吞吐量
并行
Parallel NEW(新生代的 复制回收) / CMS (老年代的 标记清除)
CMS 垃圾回收器 将垃圾回收过程分为5步
初始化标记(CMS-initial-mark) :标记root直接关联的对象,会导致stw,但是这个没多少对象,时间短
并发标记(CMS-concurrent-mark):沿着上一步的root,往下追踪,这步耗时最长,但是与用户线程同时运行
三色标记 黑,白,灰
黑色:代表该对象以及该对象下的属性全部被标记过了。(程序需要用到的对象,不应该被回收) 灰色:对象被标记了,但是该对象下的属性未被完全标记。(需要在该对象中寻找垃圾) 白色:对象未被标记(需要被清除的垃圾
重新标记(CMS-remark) :因为上一步是并发进行的,所以再增量过一遍有变化的,会导致stw,但比上一步少很多
并发清除(CMS-concurrent-sweep):标记完的干掉,因为是标记-清除算法,不需要移动存活对象,所以这一步与用户线程同时运行
重置线程:重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行
CMS垃圾回收器缺点
JMM Java内存模型
主内存 与 工作内存
JMM 规定了所有的变量都存储在主内存(Main Memory)中
每条线程还有自己的工作内存(Working Memory),工作内存中保留了该线程使用到的变量的主内存的副本
线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
i+=1
此命令包含了三个步骤
调用load 将变量从 主内存加载到工作内存
变量 = 变量+1 进行赋值操作
调用 store ,将变量的值 由工作内存写会主内存
非原子性操作
JMM解决什么问题?
工作内存数据一致性:可见性问题
约束指令重排序优化:有序性问题
JMM内存交互
JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作
lock (锁定) - 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 unlock (解锁) - 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 read (读取) - 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。 load (载入) - 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。 use (使用) - 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。 assign (赋值) - 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 store (存储) - 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。 write (写入) - 作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。 如果要把一个变量从主内存中复制到工作内存,就需要按序执行 read 和 load 操作;如果把变量从工作内存中同步回主内存中,就需要按序执行 store 和 write 操作。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。 JMM 还规定了上述 8 种基本操作,需要满足以下规则: read 和 load 必须成对出现;store 和 write 必须成对出现。即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况出现。 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须把变化同步到主内存中。 不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。换句话说,就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对出现。 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)
Happens-Before
1. 程序的顺序性规则
这条规则是指在一个线程中,按照程序顺序(可能是重排序后的顺序),前面的操作 Happens-Before 于后续的任意操作,程序前面对某个变量的修改一定是对后续操作可见的
xxxxxxxxxx ClassReordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } }
2. volatile 变量规则
这条规则是指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作
xxxxxxxxxx class VolatileExample { int x = 0; volatile boolean v = false; // 线程A 先 public void writer() { x = 42; v = true; } // 线程B 后 public void reader() { if (v == true) { // 这里x会是多少呢? 42 } } }
3. 管程中锁的规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁
xxxxxxxxxx int x = 10; public void syn() { synchronized (this) { //此处自动加锁 if (this.x < 12) { this.x = 12; } } //此处自动解锁 }
管程(Monitors,也称为监视器),是一种通用的同步原语,能够实现对共享资源的互斥访问,Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现
4. 线程启动规则
它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
xxxxxxxxxx static int var = 66; // 主线程A public static void t1() { Thread B = new Thread(()->{ // 主线程调用B.start()之前 // 所有对共享变量的修改,此处皆可见 // 此例中,var==77 }); // 此处对共享变量var修改 var = 77; // 主线程启动子线程 B.start(); }
5. 线程join规则
它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作结果可见。
xxxxxxxxxx static int var = 55; //主线程A public static void t1() { Thread B = new Thread(()->{ // 此处对共享变量var修改 var = 66; }); // 主线程启动子线程 B.start(); //主线程等待子线程B结束 B.join() // 子线程所有对共享变量的修改 // 在主线程调用B.join()之后皆可见 // 此例中,var==66 }
6. 线程中断规则
对线程interrupt()方法的调用 Happens-Before 被中断线程的代码检测到中断事件的发生,比如我们可以通过Thread.interrupted()/isInterrupted方法检测到是否有中断发生。
Thread.interrupted() 会清除中断标记位,而isInterrupted不会
7. 对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。
对象逃逸问题,不要构造函数当中将对象赋值给外部变量。有可能对象的属性还未初始化
jvm锁升级过程
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁默认不是立即开启的,jvm启动4秒后才会开启偏向锁 或者可以通过命令立即启动-XX:BiasedLockingStartupDelay=0
锁粗化,锁消除
JDK源码
1.Object类
任何类都继承自Object类:在编译时期继承Object类
通过javap 反编译字节码文件,jdk6中可以看到在编译时期显示到声明了继承自 Object,而在jdk8以后,则能看的初始化Object的方法
native关键字
jni,表示没有通过java代码实现,而是通过c/c++ 实现的,native只是一个指针,指向了.dll动态库中的文件
toString()
equals()
重写equals方法的五个原则
1.自反性: x.equals(x) = true
2.对称性 :x.equals(y) = true,那么 y.equals(x) = true
3.传递性 :x.equals(y) = true,y.equals(z)=true,那么z.equals(x)也一定等于true
4.一致性 : x.equals(y)=true,那么无论调用多少次依然是true
5.对于任何非空引用x,x.equals(null)一定返回false
hashcode()
重写equals方法,为什么一定要重写hashcode方法?
维护hashcode的一般性约定: equals相等,那么hashcode一定相等 hashcode相等,equals不一定相等
提高集合中元素比较当效率,当出现哈希碰撞时,才需要调用equals方法
hashcode方法主要是为了提高查找和定位的快捷性,一次hash计算,准确定位下标位置
对象的组成
对象头(8个字节) : GC分代年龄,锁标志位,hashcode值
分代年龄只占4个bit位,而4个bit位能表示的最大值位16,因此,新生代中 的对象经过16次 Minor GC 还存活的话,就会进入老年代
对象头类型指针(4个字节)
实列数据
字节填充 : 对象所占字节必须为8的倍数
生成hashcode
一个新new出来的对象,对象头中是不会记录hashcode的值
当调用hashcode方法后,对象头中才会记录hashcode当值
如果当前是偏向锁状态,由于对象头中空间有限,如果想要记录hashcode当值,需要先撤销偏向锁,因为当前mark work中存当是当前偏向锁持有当线程id,腾出空间,然后计算hashcode当值并且返回,此时将会将锁由偏向锁改为重量级锁
一个对象在调用原生hashCode方法后(来自Object的,未被重写过的),该对象将无法进入偏向锁状态,起步就会是轻量级锁。若hashCode方法的调用是在对象已经处于偏向锁状态时调用,它的偏向状态会被立即撤销,并且锁会升级为重量级锁。
偏向锁需要记录线程ID,生产 hashcode是没有空间记录偏向锁线程ID的 因此会升级
如果是轻量级锁或重量级锁的情况,hashcode值会放到当前mark work中存到对象指针所指向的监视器对象中找
无锁对象头,偏向先撤销,轻量找helper,重量看moniter
getCLass()
clone()
浅克隆和深克隆
浅克隆 : 引用类型克隆同一份地址值
finalize()
当GC确定不再有改对象当引用时,将调用finalize方法来清楚回收 JVM会确保finalize()方法只被调用一次,程序不能直接调用finalize()方法
2.数据结构
ArrayList 类
数据结构 : 数组(连续的存储空间)
特点: 查找快,增删慢
初始化
JDK7 之前 初始化时,长度为 10的数组
JDK8 初始化时,底层为一个空的数组
Arraylist扩容为原数组长度+原数组长度右移一位(除以2)
add 方法
无参构造时
初始化容量为 10
有参构造
当初始化容量少于10时,每次加一
大于10时,每次扩容右移一位
remove 方法
调用System.copy方法 自己拷贝自己 (浅拷贝) 并将最后的一位赋值为null
LinkedList
数据结构 : 双向链表
特点 : 增删快,查询慢
HashMap
数据结构 : 哈希表
key-value 的形式存储数据
初始化容量为 16 肯定是2的幂次方 加载因子是 0.75
容量为什么是2的幂次方? 因为当key进行哈希运算后,需要保证再次计算后的值在容量范围内,与运算比模运算效率高,源码底层会将容量-1进行与运算,如果不是2的幂次方,将导致一半的空间被浪费
当元素个数 = 容量 * 加载因子 将触发扩容 0.75的原因是 空间和时间取一个平衡 即不会频繁扩容,也不会频繁产生hash碰撞导致某个链表长度过长
JDK 1.7版本中头插法的实现是通过创建一个新的Entry,将Entry的next指针指向原来的 [ ]Entry集合
JDK1.7中,计算key时,将会扰动7次,JDK1.8只会扰动两次。 扰动只为了让高位参与运算,提升离散性
Entry对象的四个属性 : hash,key,value,next
JDK1.7中,将会先添加再扩容,而JDK1.8将先扩容后添加,添加元素后判断节点是否超过8个,列表节点超过8个并且元素个数超过64,将会转化为红黑树,当列表节点个数少于等于6时,将会退树成链表
1.8中HashMap采用尾插法,1.7使用头插法,避免了并发下出现当环(死循环) 但仍然存在线程安全问题。HashMap本身就不是一个线程安全的类
hashtable
每个方法都用sync修饰
锁粒度太大,不支持多线程
concurrenthashmap
分段锁
由 segment 组成,每一个segment类似一个hashMap segment一旦初始化,就不能再增加了,而每一个segment底层的 emtry[ ]数组是可以扩容的
concurrentHashMap的segment的数量决定concurrentHashMap支持的最大并发数
讲concurrentHashMap就的与HashTable做比较 HashTable 锁住的是整个 数组, 而concurrentHashMap 将数组分成一个个segment,每次锁的时候锁的是一个segment 每个segment包含一个entry[ ]数组,每个entry[ ] 数组就等于 一个hashmap
concurrentHashMap计算下标位置需要计算两次,首先确定元素应该 在哪一个 segment上,然后再次hash计算,确定应该放在那个下标上 再去判断需要不需要扩容,需不需要树化
如何做到线程安全
通过Unsafe类实现CAS创建segment对象,然后再创建node节点(1.7是数组)
1.7
初始化segment数组(必须是2的n次方),然后通过cas实列化segment[ 0 ] 上的 segment对象,用于存储以后实例化其他segment的信息,比如 entry[]数组的长度 锁加载entry数组上,通过trylock()方法
1.8
相比于1.7,在jdk1.8中,采用了链表+红黑树,锁加载node链表上,锁对象 是 node链表的头节点,锁的粒度比1.7更细
HashTable与ConcurrentHashmap不允许key为null的原因
避免二义性:在多线程环境下,不知道存进去的是null 还是没有get到值为null
因为ConcurrentHashMap是线程安全的,一般使用在并发环境下,你一开始get方法获取到null之后,再去调用containsKey方法,没法确保get方法和containsKey方法之间,没有别的线程来捣乱,刚好把你要查询的对象设置了进去或者删除掉了
sync
特性
原子性
一致性
可见性
非公平锁,关键字
用法
方法声明上-同步方法
同步代码块
底层基于monitor监视器实现的 对象头mark word中记录了 monitor对象的内存地址,而monitor对象有两个重要属性 count,own count记录锁的重入次数,own记录线程id 当执行同步方法时,会首先own记录当线程id是不是当前线程 再判断count是不是等于0,如果等于0 就将自己当线程id赋值给own 如果不是,阻塞等待
monitor对象
每个对象都会有一个对应当monitor对象与之对应
monitor对象组成
count 记录锁当重入次数
owner 记录当前持有锁当线程id
waitset 处于等待锁wait状态的线程,将会加入到此队列中
entrylist 处于阻塞等待锁block状态的线程,将会加入到此队列中
wait 和 sleep的区别 : 1.sleep 是线程类Thread的方法,而wait是Object的方法 2.wait会释放锁,而sleep不会释放锁 3.打断线程的sleep状态可以通过调用interrupt 方法打断 而线程的wait状态可以通过notify或notifyAll 唤醒
高性能分布式缓存
Redis (支持大量的QPS)
Redis是单线程吗?
6.0之前的版本
Redis在6.0版本之前,网络IO和数据读写都是单线程的 但 数据持久化,异步删除,集群间数据同步是多线程的,数据持久化会从主线程上fork一条子线程出来,进行数据持久化
6.0之后的版本
Redis在6.0之后的版本,网络IO变成了多线程,但默认也是关闭的,需要手动开启
Redis 快的 原因 :
单线程,避免维护多线程共享变量的消耗
高效的索引数据结构: Hash跳表
基于内存操作
多路IO复用
Mysql和Redis怎么保存数据一致性?
延时双删(还是会存在问题)
先删除Redis中的数据,然后更新Mysql中的数据,间隔3~5秒 再删除Redis中的数据
监听bingLog日志+消息队列(保证消息百分比投递)
五种常用数据类型
String
数据结构:简单动态字符串(simple dynamic String)SDS
用途:
数据缓存
分布式锁
接口限流
Hash
数据结构: 数据量小的情况下是 zipList 数量量大 则是 Hash 表
用途:缓存对象
优缺点:
优点:
相比较String,更加节省空间
比String更加减少内存消耗
方便同类数据的存储
缺点:
设置过期只能作用于 key 上,不能作用于某个具体的字段上
Redis集群的时候,不适合大规模使用 (大量请求访问同一个节点)
List
数据结构:双向链表
用途
栈(先进后出):lpush -> lpop
队列( 先进先出 ) : lpush -> rpop
阻塞队列:lpush -> brpop
Set
数据结构 : 无序不重复集合
用途
数据去重
取交集,并集,差集
Zset
数据结构 : 有序集合(相比set增加权重参数)
用途
用户排名,热点新闻
bitmap 位图
数据结构:连续的二进制数组
Lua脚本
Lua脚本的好处
Redis的命令执行是单线程的,所以不用担心其他命令的插入,保证了原子性
Lua脚本将存储再Redis中,提高了复用性
Lua脚本命令
生成Lua脚本
Eval
数据持久化
RDB快照
触发方式
手动触发
save 命令
会造成当前主线程阻塞
bgsave 命令
fork一条子进程,由子进程进行数据持久化 因此只会在fork进程的时候造成阻塞
redis.conf配置触发
持久化流程: 首先触发 fork 子进程,读取主进程数据,生成临时文件,当读取完毕时 将临时文件改名,替换原有的 .rdb文件 持久化过程中,如果需要执行set命令,将采用写时复制技术,复制一个副本让子进程进行读取
AOF日记
触发方式 : 所有的命令将追加到AOF缓冲区中,再有AOF缓冲区写到.aof文件中
AOF重写 : 由于aof持久化是以日志的方法持久化,必然将导致文件越来越大, 因此,需要对aof文件进行重写。
混合持久化
Redis 4.0之后支持
AOF重写将以RDB的形式进行存储,后续命令以AOF日志的形式进行记录
数据恢复时,.aof 备份文件优先
线上经验:
主从模式下,一般从机开启持久化
自己制定数据持久化策略,定期检查,可以手动触发备份,持久化
尽量开启混合持久化
内存淘汰算法
LRU
LRU 算法 对象只记录 上次调用的时间戳
LFU
LFU 算法 对象即记录 上次调用的时间戳 以及 调用的次数 如果次数相同,在比较时间戳
高可用
主从模式
一主多从
多主多从(集群)
从机 与 主机建立连接后首先将进行一次全量同步, 由 主机 生成.RDB文件 发送给从机 。在此期间,主机接收到的 新命令将由buffer缓存文件的形式发送给从机。从机完整接收到 .RDB文件与缓存文件后,将首先刷新清空自己的数据,在加载从 主机接收到的文件
分布式锁
互斥性:不仅在同一个jvm进程下的不同线程间互斥,在不同的jvm进程下不同线程也要互斥
锁超时: 支持锁的自动释放,防止死锁
正确的解锁:那个线程加上的锁,就应该由哪个线程取解锁
可重入 : 当一个线程获取锁后,再去获取锁,无需去抢锁
阻塞/非阻塞 : 获取不到直接返回是非阻塞。如果获取不到锁会一直等待锁,即为阻塞的
公平/非公平 : 获取锁是有序的 最先失败的最先抢锁
Redis带来的问题
缓存穿透
接口增加参数校验
缓存空值,同时设置较短的过期时间
引入布隆过滤器
缓存击穿
缓存雪崩
使用CLuster集群,将热点数据分布在不同的节点中
每个key设置过期时间时加上随机值
数据库扩容
数据库平滑扩容
双主模式,两台mysql服务器,各自即为主机也为从机,互为主从
垂直扩容 : 将导致大量数据需要迁移,如果不迁移的话,取模获取不到数据
扩容方式
停机方案
停写方案
不能写,只能读
日记记录
异步记录日志(增删改)
数据同步工具
日志增量同步工具
平滑2N方案
为每个主库设置一个从库,保证从库信息的完整度。 然后将从库升级为主库,并将取模不应该放在此库中的记录删除(冗余数据处理)
ACticiti工作流
初始化25张表
1.一般数据通用表
2.流程历史表
3.流程定义表
4.运行实列表
五个Service
流程符号
事件
活动
网关
流向
多线程
线程状态
初始化
就绪
阻塞
等待
超时等待
死亡
线程池
Executors封装的四种线程池
FixedThreadPool 定长线程池
只有核心线程
ScheduledThreadPool 定时线程池
等待队列为DelayedWorkQueue() 无界的队列,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走
CachedThreadPool 缓存线程池
没有核心线程,只有非核心线程
SingleThreadExecutor单线程化线程池
只有一条核心线程
submit和execute的区别
submit可以执行实现了Runable接口或Callable接口的线程任务
而executor只能执行实现了Runable接口的线程任务
面试题
线程池是如何保证线程不被销毁的呢?
如果队列中没有任务时,核心线程会一直阻塞在获取任务的方法,直到返回任务。而任务执行完后,又会进入下一轮 work.runWork()中循环。如果是非核心线程,将调用队列的非阻塞poll()方法,在设置的时间内尝试获取,如未获取到将超时状态设置为true
那么线程池中的线程会处于什么状态?
RUNNABLE,WAITING
Fork/Join
概念
Fork
Fork(分岔),先把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。
Join
join,分割后的子任务被多个线程执行后,再合并结果,得到最终的完整输出。
原子操作
CAS
比较并替换,是一种实现并发常用到的技术子主题
主要参数
当前值
预期值
新值
存在ABA问题
采用版本号的方式解决
AQS 抽象队列同步器(模版方法模式)
实现公平锁与非公平锁
公平锁
直接去抢锁,也不看现在等待队列中有没有线程在排队,如果有线程在排队,那岂不是变成了插队,导致不公平。
非公平锁
先看一下等待队列中是否有在排队的,如果有那就乖乖去排队,不插队,如果没有则可以直接去获取锁。
AQS用一个volatile int state;属性表示锁状态,1表示锁被持有,0表示未被持有,具体的维护由子类去维护,但是提供了修改该属性的三个方法:getState(),setState(int newState),compareAndSetState(int expect, int update),其中CAS方法是核心。
框架内部维护了一个FIFO的等待队列,是用双向链表实现的,我们称之为CLH队列,
框架也内部也实现了条件变量Condition,用它来实现等待唤醒机制,并且支持多个条件变量
AQS支持两种资源共享的模式:独占模式(Exclusive)和共享模式(Share),所谓独占模式就是任意时刻只允许一个线程访问共享资源,譬如ReentrantLock;而共享模式指的就是允许多个线程同时访问共享资源,譬如Semaphore/CountDownLatch
使用者只需继承AbstractQueuedSynchronizer并重写指定的方法,在方法内完成对共享资源state的获取和释放,至于具体线程等待队列的维护,AQS已经在顶层实现好了,在那些final的模板方法里
Thread类
Sleep()
暂停一下,只是让出CPU的执行权,并不释放锁。
yield()
不释放锁,运行中转为就绪,让出cpu给同优先级去竞争。当然有可能自己又抢了回来
join()
父线程等待子线程执行完成后再执行,将异步转为同步。
线程三大特性
原子性
可见性
有序性
需要解决的问题
CPU缓存导致可见性问题 Volatile
对于共享变量i,首先要将其从内存中读到CPU中,然后对其进行相关操作,如果线程A对其进行了修改操作,线程B能够立马看到线程A操作的结果,我们将其称之为线程之间的可见性。 在单核CPU架构下, 所有的线程都是在一颗 CPU 上执行,因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。 但是在多核CPU时代,每颗 CPU 都有自己的缓存,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存
Volatile 关键字可以解决缓存可见性问题,被它修饰的变量在工作内存中的缓存行将会失效,当前线程将会去主内存中获取最新值
线程切换导致原子性问题 sync关键字 与 原子类
性能优化导致有序性问题 Volatile
双检锁判断对象是否为空,new 不是原子性的 对象的内存地址已经有了,但属性还为初始化(执行init方法)
Object target = null; if (target == null) { synchronized (Object.class) { if (target == null) { target = new Object(); } } }
Jenkins持续继承
概念:
CI : 持续集成
CD : 持续交付
分布式事务
消息队列