1 Star 0 Fork 0

fresherlei / Java笔记

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
javanote.md 109.65 KB
一键复制 编辑 原始数据 按行查看 历史
fresherlei 提交于 2022-01-11 22:14 . 并发

计算机网络

协议

ARP协议

ip-->mac

先判断两台计算机子网地址是否一样

半连接池

半连接池限制的是同一时间的请求数而非连接数

J2SE

JDK JRE JVM 有什么区别

  • JDK 程序员开发产品
  • JRE 运行JAVA环境的集合
  • JVM 运行JAVA字节码文件,隐藏操作系统底层API,跨平台的核心(一次编译跨平台运行,JVM与字节码文件为核心)

常用数字类型的区别

名称 取值范围 存储空间
字节(byte) -2^7~2^7-1 1字节
短整数(short) -2^15~2^15 -1 2字节
整数(int) -2^31~2^31 -1 4字节
长整数(long) -2^63~2^63 -1 8字节
单精度(float) 2^-149~2^128 -1 4字节
双精度(double) 2^-1074~2^1024 -1 8字节

面向对象的三大特征

封装

隐藏对象的属性和实现细节,进对外提供公共访问方式

好处: 将变化隔离,减少耦合,提高复用性和安全性

继承

从已有的类中派生出新的类,新的类吸收了已有类的行为和属性并拓展新的能力,但不能选择性的继承

Java中类不支持多继承,接口支持,一个接口可以继承多个其他接口

父类是子类的抽象,子类是父类的具体

通过继承可以提高代码复用性,继承是多态的前提

多态

多态是三大特性中最重要的操作

多态是同一个行为具有多个不同表现形式或形态的能力

多态是同一个接口,使用不同的实例而执行不同操作

两种形式可以实现多态:继承(多个子类对同一方法的重写),接口(实现接口并覆盖接口中同一方法)

编译时多态 运行时多态
编译期间决定目标方法 运行期间决定目标方法
overloading重载实现 同名同参
方法名相同,参数名不同 overriding和继承实现
JVM决定目标方法

接口与抽象类有那些异同

相同 不同
都是上层的抽象 抽象类可以包含方法的实现,接口只能包含接口的声明
不能被实例化 继承类只能继承一个抽象类,如果继承类为非抽象类,必须实现抽象类所有方法,实现类可以实现多个接口
都可以包含抽象方法,子类必须覆写这些抽象方法 抽象级别:接口>抽象类>实现类
作用不同:接口用于约束程序行为,继承则用于代码复用
字段声明:抽象类任意,接口默认static final
构造器:抽象类可以有,接口不能有
声明:接口-interface(隐式抽象) , 抽象类-abstract
接口1.8之前 只有静态常量 和抽象方法 1.8加入了默认方法 1.9加入了私有方法

linux

系统目录

“/”: 根目录

/bin: 应用程序可执行文件,如常用的二进制命令:ls、cp、mkdir

/boot:Linux内核与系统引导程序目录

/dev:设备文件的目录,比如声卡、磁盘、光驱

/etc: yum、rpm方式安装应用程序的默认配置文件路径

/home: 用户数据

/var:系统与软件服务运行日志目录

/lib:启动系统与运行命令所需的共享库文件与内核模块目录

/proc:系统运行时,进程信息与内核信息存放在此目录

/root:Linux超级用户目录,admin

/sbin:系统管理命令存放目录,是超级用户root可执行命令的存放地

/usr:用户安装的三方应用,包含两个重要子目录

​ /user/local: 编译方式安装程序的默认目录

​ /user/src: 程序源码目录

特点:

  • 不同目录下的数据可以分布在不同的磁盘,所有目录按规则组织与命名
  • 区分绝对路径与相对路径

远程登陆

SSH客户端

SSH是专为远程登陆和其他网络服务提供的安全协议

SSH分为两个不兼容版本1.x和2.x,默认通过SSH2.x连接

常见的工具Xshell与SecureCRT

Xshell

常用于Window远程访问

还支持Telent、RLogin、Serial等其他连接方式

Xftp是Xshell配套组件,用于向服务器上传/下载文件

Linux文件操作核心命令

cd 切换目录
pwd 查看当前目录
ls、ll 显示目录内容
mkdir -p -v 连续创建多级内容并打印信息
cp 复制文件与目录 -r 复制整个文件夹
mv 移动或重命名文件
rm -rf 强制删除文件或目录 多级
find 查找目录或文件
clear 控制台清屏
which 查看可执行文件位置

vim文本编辑器

vim重要快捷键

命令 用途
delete or x 删除
dd 删除整行
/str 全文查找str字符,n下一个,N前一个
:% s/old/new/g 替换文件内所有old字符为new
u 撤销最近一次操作
:wq or :wq! 退出并保存,只读文件需要加!
:q! 强制退出放弃保存
i 编辑模式
home/end 行首/行尾

linux文本工具

命令 用途
echo >> 屏幕打印与文本输出 >重写 >>追加
cat -nE(cat >>)(<<abc) (合并文件)或查看文件内容 显示行号 和空行 (以abc结束输入)
tail -n 2 看日志 查看文件内容尾部最后两行
grep abc ahsfi.txt 文本过滤工具
tail -f 动态监控文本尾部(产生变化)
ll | grep -E "log[0-9]{1,5}.txt" 正则表达式模糊查询 详细显示目录(通道)

打包与压缩

tar zcvf 压缩

tar zxvf 文件名 -C 解压缩

选项 用途
z 通过gzip压缩或者解压
c 创建新的tar.gz文件
v 显示执行过程
f 指定压缩文件名称
x 解压缩tar.gz文件
-C 指定解压缩目录

安装与卸载应用程序

rpm:Red Hat 软件包管理器,相当于应用程序安装文件的执行者

编译安装:下载程序源码进行编译安装

yum

yum通过引入软件仓库,联网下载rpm包及依赖,并依次自动安装

yum search 应用名 在仓库中查询是否存在指定应用
yum install -y 应用名 全自动下载安装应用及其依赖
yum info 应用名 查看应用详细信息
yum list installed 应用名 查看已安装的应用程序
rpm -ql 应用名 查看安装后输出的文件清单
yum remove -y 应用名 全自动卸载指定应用
yun localinstall -y 本地安装

yum 下载缓存路径:/var/cache/yum/

rpm

rpm需要用户自己解决依赖问题

linux 进阶应用

linux 系统管理命令

ifconfig 查看网卡ip
netstat 查看网络端口号
ps -ef 查看进程
kill -9 PID 杀掉进程

nestat 常用选项

选项 用途
t 显示tcp传输协议的连接状况
u 显示UDP传输协议的连接状况
l 显示处于监听状态的网络连接
p 显示应用pid和程序名称
n 显示ip地址
a 显示所有连接
o 显示计时器

centos应用服务化

systemctl

指令 用途
start 启动服务
stop 停止服务
restart 重启服务
enable 设置开机启动
disable 禁止开机启动
status 查看服务状态
daemon-reload 重载服务配置文件
list-unit-files 列出所有服务

参考博文

https://blog.csdn.net/csucsgoat/article/details/118612323?utm_term=redis%E5%BA%94%E7%94%A8%E6%9C%8D%E5%8A%A1%E5%8C%96&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduweb~default-0-118612323&spm=3001.4430

linux 用户与用户组管理

linux用户与权限

命令 用途
useradd 添加新用户
password 修改密码
usermod 修改用户信息/分配组(覆盖原组)
groupadd 创建新的用户组
chown 更改文件的属主或属组
chmod 更改文件的访问权限
newgrp 切换用户当前组
visudo 等同于vi /etc/sudoers 100gg(快速定位)
sudo

linux文件权限

d r W X r w x r w x
4 2 1 4 2 1 4 2 1
目录 属主读取 属主写入 属主运行 组读取 组写入 组运行 其他读取 其他写入 其他运行

chomd 777 abc.md

sudo获取超级管理员权限

root 目录下 visudo 修改文本 后 visudo -c 解析

CentOS7防火墙firewall

firewall-cmd state 查看防火墙状态

firewall-cmd --list-ports 当前防火墙放行端口

firewall-cmd --zone=public --permanent --add-port=8080/tcp 永久放行8080

firewall-cmd --reload 重载防火墙

firewall-cmd --zone=public --permanent --remove-port=8080/tcp 移除8080

firewall-cmd --zone=public --permanent --add-rich-rule="rule family="ipv4" source address=" " port portocol="tcp" port="3306" accept " 限制 3306端口 来自address "

对外开放TomCat

bash shell

.sh文件

linux shell 分类

种类 shell解释器
Bourne Shell /usr/bin/sh或/bin/sh
Bourne Again Shell /bin/bash(默认)
C shell /usr/bin/csh
K shell /usr/bin/ksh
Shell for Root /sbin/sh

常见shell https://blog.csdn.net/Dome_/article/details/87866346

mysql相关

flush privileges

jvm

概述

作用

jvm就是二进制字节码的运行环境,负责装在字节码到其内部,解释/编译为对应平台的机器指令执行。

特点

一次编译,到处运行

自动管理内存

自动垃圾回收机制

位置

(用户(字节码文件(jvm(操作系统(硬件)))))

整体结构

image-20210916223821024

虚拟机栈

局部变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内部的局部变量,这些数据包括各类基本数据类型、对象引用,以及returnAddress类型

  • 最基本的存储单元是slot(变量槽),32位以内只占用一个slot,byte、short、char、boolean在存储前被转换为int,long、double占两个slot

  • 线程私有数据,不存在线程安全问题

  • 局部变量表所需容量大小是在编译期确定下来的,在方法运行期间是不会改变局部变量表大小

  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁而销毁

  • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量完成方法的传递

  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或者间接引用的变量就不会被回收

slot

JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索即可成功访问到局部变量表中的局部变量值

当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量会按照顺序被复制到局部变量表的每一个slot上

如果需要访问局部变量表中一个64bit(占位两个slot) 的局部变量值时,只需要使用前一个索引即可(因为占两个位,使用起始索引)。

如果当前帧是由构造方法或者实例方法(静态方法)创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列

栈帧中的局部变量表中的slot是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很可能会复用过期局部变量的槽位,从而节省资源

操作数栈

又名表达式栈,数组实现

主要用于保存计算结果的中间过程,同时作为计算过程中变量临时存储空间

栈的最大深度在编译器确定 max_stack

操作数栈并非采用索引方式来进行数据访问,而是采用出栈入栈方式来完成一次数据操作

栈顶缓存技术

操作数存储在内存中,频繁的执行内存读/写操作必然会影响执行速度。

栈顶缓存技术,将栈顶元素全部缓存在 物理cpu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

动态链接(帧数据区)

指向运行时常量池的方法引用

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含着个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令

在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件 的常量池里。比如描述一个方法调用了其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

方法的调用

在jvm中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

静态链接

被调用的方法在编译器可知,且运行时保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程

动态链接

被调用的方法在编译器无法被确定下来。只能够在程序运行时将符号引用转换为直接引用。

早期绑定

绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,仅仅发生一次

被调用的方法编译期可知,且运行期间保持不变,即可将这个方法与所属类型进行绑定。由于明确了被调用的目标方法是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用

invokespecial

体现为构造器

晚期绑定

被调用的方法在编译器无法被确定下来。只能够在程序运行时根据实际的类型绑定相关的方法

invokeinterface

invokevirtal

虚方法

invokevirtal 调用所有虚方法(除去final)

invokeinterface

在编译器确定版本,运行时不可变的方法

静态方法、私有方法、final方法、实例构造器、父类方法

子类多态性使用前提

类的继承关系 声明是父类 创建的是子类对象

方法重写

非虚方法

invokestatic 调用静态方法 解析阶段确定唯一方法版本

invokespecial 调用方法、私有及父类方法,解析阶段确定唯一方法版本

方法重写的本质

找到操作数栈顶的第一个元素所执行的实际类型,记作A

如果在类型A中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IlleagalAccessError

否则,按照继承关系从下往上一次对A 的各个父类进行第二步的搜索和验证过程。

如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

java.lang.IlleagalAccessError

程序试图访问或者修改一个属性或调用一个方法,这个属性或者方法,你没有访问权限。一般会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变

方法返回地址 (帧数据区)

方法正常退出或者异常退出的定义

存放调用该方法的pc寄存器的值

方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的吓一条指令的地址。

异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息

无论那种方式退出,在方法退出后都返回到该方法被调用的位置。

本质上,方法的退出就是当前栈帧出栈的过程。此时需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置pc寄存器的值等,让方法调用继续下去

正常完成出口和异常完成出口的区别在于:通过异常完成出口的不会给他的上层调用者产生任何的返回值

附加信息 (帧数据区)

对程序调试提供支持的信息

本地方法栈

当某个线程调用一个本地方法时,就不受虚拟机限制

  • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
  • 甚至可以直接使用本地处理器中的寄存器
  • 直接从本地的堆中分配任意数量的内存

hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一

堆空间

-Xms 起始空间 默认 物理电脑内存大小 /64

-Xmx 最大空间 默认 物理电脑内存大小 /4

通常将二者设置相同值,目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分割计算堆区的大小,以提高性能

新生代与老年代

自适应机制

-XX:UseAdaptiveSizePolicy 关闭自适应

在HotSpot中,Eden空间和另外两个幸存者空间缺省所占的比例是8:1:1

可以通过选项“-XX:SurvivorRatio” 调整空间比例

分代式GC

年轻代 minor GC触发机制

  1. 当年轻代空间不足,就会触发Minor GC,这里的年轻代满指的是伊甸区满,幸存者区满不会引发GC(每次minor GC会清理年轻代的内存)
  2. 大多数Java对象具备朝生夕死的特性,所以minor gc特别频繁,回收速度比较快
  3. minor gc会引发STW,暂停其他用户的进程,等垃圾回收结束,用户线程恢复

老年代 major GC 触发机制

  1. 出现了major GC,经常会伴随至少一次的minor GC(非绝对,分虚拟机类别):也就是老年代空间不足,先尝试minor GC,如果空间还是不足,触发major GC
  2. major GC速度比minor GC慢10倍以上,STW时间更长
  3. major GC后,内存还不足,则报OOM

full GC触发机制

  1. 调用system.gc()时,系统建议执行full gc ,但是不必然执行
  2. 老年代空间不足
  3. 方法区空间不足
  4. 通过minor gc后进入老年代的平均大小大于老年代的可用内存
  5. 由伊甸区,幸存者from区向向幸存者to区复制时,对象大小大于to区可用内存,则把该对象转存到老年代,且老年代可用内存大小小于该对象大小
  6. full gc是开发或调优中尽量要避免的,这样STW时间会短一些

GC日志 PrintGCDetails

堆空间分代思想

不同对象的生命周期不同,10%-99%的对象是临时对象

分代唯一理由就是优化GC性能,对特定区域经行扫描,提高效率

内存分配策略(或者对象提升规则)

对象出生在伊甸区并经过第一次minor gc后仍然存活,并且能够被幸存区容纳的话,将被移动到幸存区中,将年龄置1。该对象每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认15,每个jvm,每个gc都有所不同)时,就会被晋升到老年代

对象晋升老年代的阈值,可以通过 选项 -XX:MaxTenuringThreshold来设置。

针对不同年龄段的对象分配原则如下:

  • 优先分配到伊甸区

  • 大对象直接分配到老年代

    ​ 尽量避免程序中出现过多的大对象

  • 长期存活的对象分配到老年代

  • 动态对象年龄判断

    如果幸存区中相同年龄的所有对象大小的总和大于幸存区空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需达到阈值

  • 空间分配担保

    • -XX:HandlePromotionFailure

对象分配过程:TLAB

为什么有TLAB(thread local allocation buffer)

  • 堆是共享区域,任何线程都可以访问到堆中的共享资源
  • 由于对象实例的创建在jvm中是十分频繁的,因此在并发环境下从堆区中划分内存空间是不安全的
  • 为避免多个线程操纵同一地址,需要使用加锁等机制,影响分配速度

什么是TLAB

  • 从内存模型来看:对伊甸区继续进行划分,jvm为每个线程分配了一个私有缓存区域,他包含在伊甸空间内
  • 多线程同时分配内存时,使用TLAB可以避免一系列非线程安全问题,同时还能提升内存分配的吞吐量,我们可以将这种内存分配方式称为快速分配策略

TLAB注意事项

  • jvm将TLAB作为对象实例分配内存的首选,但不是所有的对象实例都能在Tlab中成功分配内存
  • 通过选项-XX:UseTLAB 设置是否开启TLAB空间
  • 默认情况下,TLAB空间的内存十分小,仅占有整个伊甸区的1%,通过 -XX:TLABWasteTargetPercent 设置百分比
  • 一旦对象在TLAB 空间分配内存失败时,jvm就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在伊甸空间中分配内存

常用参数设置

  • -XX:+PrintFlagsInitial 查看所有参数默认初始值

  • -XX:+PrintFlagsFinal 查看所有参数的最终值(意味着存在修改)

    • 具体查看某个参数指令:jps 查看当前运行中的进程

      ​ jinfo -flag SurvivorRatio 进程id

  • -XX:-Xms 初始堆空间内存(默认物理内存的1/64)

  • -XX:-Xmx 最大堆空间内存(默认物理内存的1/4)

  • -XX:-Xmn 设置新生代的大小(初始值及最大值)

  • -XX:NewRatio 配置新生代与老年代在堆结构中的占比

  • -XX:SurvivorRatio 设置新生代伊甸区和s0/s1空间的比例

  • -XX:MaxTenuringThreshold 设置新生代垃圾的最大年龄

  • -XX:+PrintGCDetails 打印详细的GC日志

  • -XX:HandlePromotionFailure 是否设置空间分配担保

堆并不是分配对象存储的唯一选择

如果经过逃逸分析后发现,一个对象并没有逃出方法的话,那么就可能被优化成栈上分配。这样无需要在堆上分配内存,也不需要垃圾回收。这是最常见的堆外存储技术

基于OpenJDK深度定制的TaoBaoVM ,其中创新的GCIH技术实现off-heap,将生命周期较长的Java对象从heap中移至heap外,并且GC无法管理GCIH内部的JAVA对象,以此降低GC的回收频率和 提升GC效率

逃逸分析概述

  • 如何将堆上的对象分配到栈,需要使用逃逸分析的手段
  • 这是一种可以有效减少java程序中同步负载(同步省略)和堆内存分配压力的跨函数全局数据流分析算法
  • 通过逃逸分析,java hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象 分配到堆上
  • 逃逸分析的基本行为就是分析对象动态作用域
    • 当一个对象在方法中被定义后,对象只能在方法内部使用,则认为没有发生逃逸,分配到栈上,随着方法执行的结束,占空间被移除
    • 当一个对象在方法中被定义后,他被外部方法所引用,则认为发生逃逸,如作为调用参数传递到其他地方

逃逸分析:代码优化

栈上分配

将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是对分配

常见的栈上分配场景:成员变量赋值、方法返回值、实例引用传递

hotspot 中并未应用栈上分配

同步省略

如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以考虑不同步。

线程同步的代价是相当高的,同步的后果是降低并发性和性能

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有发布到其他线程。如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除

字节码中存在synchronized 运行时消除

分离对象或者标量替换

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在cpu寄存器中

标量是指一个无法分解成更小数据的数据。java中的原始数据类型就是标量。

相对的,还可以分解的数据叫聚合量,java的对象就是聚合量,可以分解成其他聚合量和标量

在JIT阶段,如果经过逃逸分析,发现一个对象不能被外界发现,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的成员变量来代替。这个过程就是标量替换。虚拟机默认打开

开发中能使用局部变量的,就不要使用在方法外定义

总结

逃逸分析并不成熟

无法保证逃逸分析的性能消耗一定高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂分析的,这其实也是一个相对耗时的过程。极端例子:分析之后发现没有对象逃逸!

所有的对象实例都是创建在堆上

总结

  • 年轻代是对象诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被GC
  • 老年代放置长生命周期的对象,通常都是从幸存者区筛选拷贝过来的对象。当然也有特殊情况。普通对象分配在TLAB上;对象较大,直接分配在伊甸区;如果对象太大,完全无法再新生代找到合适的连续空闲空间,则直接分配在老年代
  • 当GC只发生在年轻代中,回收年轻代对象的行为成为Minor GC 。当GC发生在老年代时则被称为MajorGC或者FULLGC。一般的,MInorGC发生的频率要比majorGC高很多,即老年代中垃圾回收发生的频率大大低于年轻代

方法区

概述

尽管所有的方法去在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。但对于HotspotJVM而言

,方法区还有一个别名叫做non-heap(非堆),目的就是要和堆分开。所以,方法区看作是一块独立于java堆的内存空间

  • 线程共享
  • 在Jvm启动时被创建,物理内存可以不连续
  • 方法区的大小,跟堆空间一样,可以拓展
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机抛出oom:Permen space(7之前) 或者 oom:Metaspace(8)
    • 大量加载Jar包
    • tomcat部署工程过多
    • 大量动态生成反射类
  • 关闭JVM即释放内存
  • 元空间不在虚拟机设置的内存中,而是使用本地内存
  • 方法区的大小不必固定,jvm可以根据应用的

参数

  • 元空间可以使用参数 -XX:MetaspaceSize 和-XX:MaxMetaspaceSize指定空间大小
  • windows 下 -XX:MetaspaceSize 默认值 21M , -XX:MaxMetaspaceSize 默认值-1 , 即没有限制
  • 与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出,虚拟机一样会抛出异常oomm
  • 21M为元空间水位线。一旦触及水位线, FUll GC将会触发并卸载没用的类(灭活类对应的加载器),然后高水位线被重置。新的高水位线的值取决于GC后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetasoaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。
  • 如果初始化的高水位线设置过低,调整的情况就会出现很多次。通过GC日志可以观察到FULL GC 多次调用。为了避免频繁地GC,建议将水位线设置为一个相对较高的值

存储

存储已被虚拟机加载的类型信息、变量、静态常量、即编译后的代码缓存等

类型信息

对每个加载的类型(Class、interface、enum、注解),jvm必须在方法区中存储一下类型信息:

  1. 该类型完整有效名称(包名.类名)
  2. 该类型型直接父类完整有效名(interface或者Object没有父类)
  3. 该类型的修饰符
  4. 该类型直接接口的一个有序列表
运行时常量池

->

静态变量

->

JIT代码缓存

->

域信息(成员变量)

域名称、类型、修饰符

方法信息

方法名称

方法返回类型(或者void)

方法参数的数量和类型(按照数量)

方法修饰符

方法的字节码、操作数栈、局部变量表以及其大小(abstract和native)

根对象

System Class

系统类 启动器加载器加载的核心类,运行期间会用到 java.lang.Class Object HashMap String

Native Stack

java虚拟机调用操作系统方法执行时所引用的java对象

Busy Monitor

同步锁机制 被加锁的对象

Thread

活动线程中所使用的对象 (栈帧中(引用)局部变量所(指向)引用的对象)

引用

image-20211026165114740

image-20211026165136450

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那么垃圾回收器绝不会回收它。例如:StringBuilder sb = new StringBuilder("test");变量str指向StringBuffer实例所在的堆空间,通过str可以操作该对象。如下:

image-20211026201514351

强引用特点:

  1. 强引用可以直接访问目标对象
  2. 只要有引用变量存在,垃圾回收器永远不会回收。JVM即使抛出OOM异常,也不会回收强引用所指向的对象。
  3. 强引用可能导致内存泄漏问

软引用

软引用是除了强引用外,最强的引用类型。可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆使用率临近阈值时,才会去回收软引用的对象。因此,软引用可以用于实现对内存敏感的高速缓存。SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对 这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。一旦垃圾线程回收该Java对象之后,get()方法将返回null。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();//有时候会返回null
//sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当这个对象被标记为需要回收的对象时,则返回null;

软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。使用软引用能防止内存泄露,增强程序的健壮性。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。当调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。在任何时候,都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。利用这个方法,可以检查哪个SoftReference所软引用的对象已经被回收,于是可以把这些失去所软引用的对象的SoftReference对象清除掉。

弱引用

弱引用是一种比软引用较弱的引用类型。在系统GC时,只要发现弱引用,不管系统堆空间是否足够,都会将对象进行回收。在java中,可以用java.lang.ref.WeakReference实例来保存对一个Java对象的弱引用。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();//有时候会返回null
wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

虚引用

虚引用是所有类型中最弱的一个。一个持有虚引用的对象和没有引用几乎是一样的,随时可能被垃圾回收器回收,当试图通过虚引用的get()方法取得强引用时,总是会失败。并且虚引用必须和引用队列一起使用,它的作用在于检测对象是否已经从内存中删除,跟踪垃圾回收过程。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,销毁这个对象,将这个虚引用加入引用队列。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj=null;
pf.get();//永远返回null
//pf.isEnQueued();//返回是否从内存中已经删除

垃圾收集器

G1

初始标记

在young GC时会进行 GC root的初始标记

并发标记

老年代占用堆空间达到阈值时,进行并发标记(不会STW)

-xx:InitiatingHeapOccupancyPercent = percent (默认45%)

最终标记

会STW

筛选回收
YOUNG Collection

新生代垃圾收集

YOUNG Collection+ concurrent mark

新生代垃圾收集同时进行并发标记

mixed collection

混合收集 (对新生代老年代幸存区都进行较大规模的收集)等内存收集完成 重新进入新生代垃圾收集过程

三阶段循环

java集合

iterator

//创建 iterator obj

Collection collection = new ArrayList();

Iterator iterator = collection.iterator() ;

while(iterator.hasnext){

Object obj =iterator.next();

}

//再次遍历

iterator = collection.iterator();

增强for

//增强for是迭代器的简化版
Collection collections = new ArrayList();
for(Object col:cllections){
    sout(col);
}

Collection

Collection extends iterable

List

特点

元素有序,即添加元素顺序和取出顺序一致,且可重复

每个元素都有其对应索引

遍历方法

迭代 foreach fori

ArrayList

特点

可以加入 null 且多个

数组实现存储

基本等同于vector 线程不安全(效率高)多线程下不建议使用

底层结构和源码分析
  1. ArrayList中维护了一个transient Object[] elementData;
  2. transient 修饰 不被序列化
  3. 当创建obj时,如果使用的是无参构造器,则初始 elementData容量为0,第一次添加,则扩容elementData为10,如需再次扩容,则elementDdata为1.5倍
  4. 如使用的是指定大小的构造器,则初始容量为指定大小,再次扩容为1.5倍
  5. 扩容使用的是Arrays.copyof()目的是为了保留扩容前数据
问题
讲讲ArrayList的扩容和什么是溢出感知代码

https://www.jianshu.com/p/58f36f37405a

grow(int minCapacity) 方法实现的功能用一句话概括就是,如果 minCapacity 小于等于原数组的 1.5 倍,则扩容至原数组的 1.5 倍,如果 minCapacity 大于原数组的 1.5 倍,则扩容至 minCapacity

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // newCapacity 是 oldCapacity 的 1.5 倍
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
 	transient Object[] elementData; // non-private to simplify nested class access
	
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

LinkedList

全面说明

linkedlis底层实现了双线链表和双端队列特点

可以添加任意元素(可重复),包括null

线程不安全,没有实现同步

底层操作机制

底层维护了一个双向链表

维护了两个属性first和last分别指向首尾节点

每个节点(node对象),里边又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向最后一个节点。最终实现双向链表

linkedlist的元素的添加和删除,不是通过数组完成的,相对来说效率较高

Vector

定义说明
	extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
特点

线程同步的,即线程安全。Vector的操作方法带有synchronized

底层也是一个 object数组

在开发中,需要考虑线程同步安全时,使Vector

底层源码与结构分析

无参,默认10,满后按两倍扩。

指定大小,则直接两倍扩

Set

特点

无序(插入和取出顺序不一致)没有索引

不允许重复元素,最多包含一个null

Set实现类有:

遍历

迭代器

增强for

不能使用fori

HashSet

全面说明

实现了set接口

实际上是HashMap

可以存放null值,但是只能有一个null

不保证元素是有序,取决于Hash()后,再确定索引的结果

哈希值不是哈希code 是哈希code再处理后得到return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

加载因子0.75

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
    	//防止数组越界  按位与 同1为1
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //p为数组索引
            Node<K,V> e; K k;
            //引用相同或者内容相同
            //且哈希相同(进入 同一数组索引下的链表)
            if (p.hash == hash && 
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //再判断 p 是不是红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //判断链表是否到达七个节点,达到则树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
底层分析

添加一个元素时,先得到hash值-会转成-索引值

找到存储数据表table,看到这个索引位置是否已经存放的有元素

如果有,调用equals比较,如果相同,就放弃添加,如果不同,则添加到最后

在java8中,如果一条链表的元素个数超过默认值8,并且table的大小>=64,就会进行树化

LinkedHashSet

全面说明

linkedhashset底层维护了一个hash表(数组)和双向链表

每一个节点有before和after属性 目的为形成双向链表

在添加一个元素时,先算hash值,在求索引,确定该元素在table的位置,然后将添加的元素加入到双向链表

这样保证插入顺序和遍历顺序一致

Map

HashMap

Hashtable

LinkedHashMap

TreeMap

Properties

Collections

boot项目

自动配置原理

  1. boot项目启动时加载主配置类@SpringBootApplication,开启自动配置功能

  2. @EnableAutoConfiguration 中 @Import({AutoConfigurationImportSelector.class}) 下 AutoConfigurationImportSelector类 中的getCandidateConfigurations方法里通过SpringFactoriesLoader.loadFactoryNames()扫描具有META-INF/spring.factories的jar包

    (为什么要有spring.factories?:因为我们整个项目里面的入口文件只会扫描整个项目里面下的@Compont @Configuration等注解 但是如果我们是引用了其他jar包,而其他jar包只有@Bean或者@Compont等注解,是不会扫描到的。 除非你引入的jar包没有Bean加载到容器当中 所以我们是通过写/META-INF/spring.factories文件去进行加载的。)

    此时获取了所有候选配置(经过去重)。

  3. 按需加载配置 , 加载生效的配置类(默认绑定配置文件)就会给容器中装配很多组件

  4. 修改默认配置(如果用户自己配置了 以用户的优先,没配置的话默认)

日志

日志级别:error,warn,info,debug,trace

为什么用AOP统一处理日志

对于系统健壮性的保证 fileter打印每一个请求提高开发和调试的效率

笔记

static 变量 @value 无法从配置文件注入

redis

NoSQL数据库简介

NoSQL数据库概述

NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指非关系型的数据库

NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。

不遵循SQL标准。

不支持ACID。

远超于SQL的性能。

NoSQL适用场景

对数据高并发的读写

海量数据的读写

对数据高可扩展性的

NoSQL不适用场景

需要事务支持

基于sql的结构化查询存储,处理复杂的关系,需要即席查询。

(用不着sql的和用了sql也不行的情况,请考虑用NoSql)

Redis简介

默认16个数据库,类似数组下标从0开始,初始默认使用0号库

使用命令 select 来切换数据库。如: select 8

统一密码管理,所有库同样密码。

dbsize查看当前数据库的key的数量

flushdb清空当前库

flushall通杀全部库

技术

Redis是单线程+多路IO复用技术

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)

(与Memcache三点不同: 支持多数据类型,支持持久化,单线程+多路IO复用)

常用五大数据类型

Redis 键(key)

keys *查看当前库所有key

exists key判断某个key是否存在

type key 查看你的key是什么类型

del key 删除指定的key数据

unlink key 根据value选择非阻塞删除

仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。

expire key 10 10秒钟:为给定的key设置过期时间

ttl key 查看还有多少秒过期,-1表示永不过期,-2表示已过期

select命令切换数据库

dbsize查看当前数据库的key的数量

flushdb清空当前库

flushall通杀全部库

字符串(string)

命令

二进制安全

一个string value最多可以是512m

incr 将key中存储的数字值增1 只能对数字值操作,空则新增值为1

decr 减一,空则新增值问-1

incrby/decrby <步长> 自定义步长增减

mset .....

同时设置一个或多个 key-value对

mget .....

同时获取一个或多个 value

msetnx .....

同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。

getrange <起始位置><结束位置> 获得值的范围

setrange <起始位置> 用 覆写所储存的字符串值,从<起始位置>开始(索引从0开始)。

setex <过期时间> 设置键值的同时,设置过期时间,单位秒。

getset 以新换旧,设置了新值同时获得旧值。

数据结构

String的数据结构为动态字符串,可修改。采用预分配冗余空间的方式来减少内存的频繁分配

当字符串长度小于1m,扩容是加倍现有的空间,如果超过1m,扩容时只会多扩1m的空间,不超过512m

列表(list)

简介

单键多值

底层双向链表,对两端操作性高,通过索引下标操作中间的节点性能很差

lpush/rpush .... 从左边/右边插入一个或多个值。

lpop/rpop 从左边/右边吐出一个值。

rpoplpush 从列表右边吐出一个值,插到列表左边。

lrange 按照索引下标获得元素(从左到右)

lrange mylist 0 -1 0左边第一个,-1右边第一个,(0-1表示获取所有)

lindex 按照索引下标获得元素(从左到右)

llen 获得列表长度

linsert before 在的前面插入插入值

lrem 从左边删除n个value(从左到右)

lset将列表key下标为index的值替换成value

数据结构

List的数据结构为快速链表quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。

因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

集合(set)

简介

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

常用命令

sadd .....

将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

smembers 取出该集合的所有值。

sismember 判断集合是否为含有该值,有1,没有0

scard返回该集合的元素个数。

srem .... 删除集合中的某个元素。

spop 随机从该集合中吐出一个值。

srandmember 随机从该集合中取出n个值。不会从集合中删除 。

smove value把集合中一个值从一个集合移动到另一个集合

sinter 返回两个集合的交集元素。

sunion 返回两个集合的并集元素。

sdiff 返回两个集合的差集元素(key1中的,不包含key2中的)

数据结构

Set数据结构是dict字典,字典是用哈希表实现的。

Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。

Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

哈希(hash)

简介

Redis hash 是一个键值对集合。

Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。

类似Java里面的Map<String,Object>

通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化(改变对象为可存储或者可传输的状态)和并发修改控制的问题

常用命令

hset 给集合中的 键赋值

hget 从集合取出 value

hmset ... 批量设置hash的值

hexists查看哈希表 key 中,给定域 field 是否存在。

hkeys 列出该hash集合的所有field

hvals 列出该hash集合的所有value

hincrby 为哈希表 key 中的域 field 的值加上增量 1 -1

hsetnx 将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在

数据结构

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。、

当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

有序集合(zset)

简介

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

常用命令

zadd …

将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

zrange [WITHSCORES]

返回有序集 key 中,下标在之间的元素

带WITHSCORES,可以让分数一起和值返回到结果集。

zrangebyscore key minmax [withscores] [limit offset count]

返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。

zrevrangebyscore key maxmin [withscores] [limit offset count]

同上,改为从大到小排列。

zincrby 为元素的score加上增量

zrem 删除该集合下,指定值的元素

zcount 统计该集合,分数区间内的元素个数

zrank 返回该值在集合中的排名,从0开始。

数据结构

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

Redis的发布与订阅

概念

Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息

Redis客户端可以订阅任意数量的频道

实现

  1. 打开一个客户端订阅channel1 SUBSCRIBE channel1
  2. 打开另外一个客户端,给channel1 发布消息hello publish channnel1 hello (会返回订阅者数量 :1)
  3. 打开第一个客户端可以看到发送的消息

注意:发布的消息没有持久化,订阅后才能收到

新数据类型

Bitmaps

简介

redis 使用 Bitmaps 实现位操作

  1. Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
  2. Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

命令

  • setbit设置Bitmaps中某个偏移量的值(0或1) offset:偏移量从0开始

  • getbit获取Bitmaps中某个偏移量的值 获取键的第offset位的值(从0开始算)

  • bitcount[start end] 统计字符串从start字节到end字节比特值为1的数量

    统计字符串被设置为1的bit数。一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。start 和 end 参数的设置,都可以使用负数值:比如 -1 表示最后一个位,而 -2 表示倒数第二个位,start、end 是指bit组的字节的下标数,二者皆包含。

  • 注意:redis的setbit设置或清除的是bit位置,而bitcount计算的是byte位置。

  • bitop and(or/not/xor) [key…]

    bitop是一个复合操作, 它可以做多个Bitmaps的and(交集) 、 or(并集) 、 not(非) 、 xor(异或) 操作并将结果保存在destkey中。

HyperLogLog

简介

在工作当中,我们经常会遇到与统计相关的功能需求,比如统计网站PV(PageView页面访问量),可以使用Redis的incr、incrby轻松实现。

但像UV(UniqueVisitor,独立访客)、独立IP数、搜索记录数等需要去重和计数的问题如何解决?这种求集合中不重复元素个数的问题称为基数问题。

解决基数问题有很多种方案:

(1)数据存储在MySQL表中,使用distinct count计算不重复个数

(2)使用Redis提供的hash、set、bitmaps等数据结构来处理

以上的方案结果精确,但随着数据不断增加,导致占用空间越来越大,对于非常大的数据集是不切实际的。

能否能够降低一定的精度来平衡存储空间?Redis推出了HyperLogLog

Redis HyperLogLog 是用来做基数统计(去除重复元素后剩余的数据)的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

命令

pfadd < element> [element ...] 添加指定元素到 HyperLogLog 中

将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。

pfcount [key ...] 计算HLL的近似基数,可以计算多个HLL

pfmerge [sourcekey ...] 将一个或多个HLL合并后的结果存储在另一个HLL中,比如每月活跃用户可以使用每天的活跃用户来合并计算可得

Geospatial

简介

GEO 地理信息的缩写,元素的二维坐标,在地图上就是经纬度

redis 基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作

命令

geoadd< longitude> [longitude latitude member...] 添加地理位置(经度,纬度,名称)

已经添加的数据,是无法再次往里面添加的。

geopos [member...] 获得指定地区的坐标值

geodist [m|km|ft|mi ] 获取两个位置之间的直线距离

georadius< longitude>radius m|km|ft|mi 以给定的经纬度为中心,找出某一半径内的元素

redis事务和锁机制

定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 Redis事务的主要作用就是串联多个命令防止别的命令插队。

Multi、Exec、Discard

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。 组队的过程中可以通过discard来放弃组队。

事务的错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

事务冲突问题

乐观锁

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

悲观锁

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

WATCH key [key ...]

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

unwatch

取消 WATCH 命令对所有 key 的监视。 如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

Redis事务的三特性

  • 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

Redis持久化

RDB

简介

在指定的时间间隔内将内存中的数据集快照写入磁盘,它恢复时是将快照文件直接读到内存里。

注:快照是数据存储的某一时刻的状态记录;备份则是数据存储的某一个时刻的副本。

Redis可以通过创建快照来获得存储在内存⾥⾯的数据在某个时间点上的副本。Redis创建快照之后,可 以对快照进⾏备份,可以将快照复制到其他服务器从⽽创建具有相同数据的服务器副本(Redis主从结 构,主要⽤来提⾼Redis性能),还可以将快照留在原地以便重启服务器的时候使⽤

备份执行

Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

Fork

  • ​ Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

触发RDB快照:保持策略

优劣

  • ​ 适合大规模的数据恢复 ​ 对数据完整性和一致性要求不高更适合使用 ​ 节省磁盘空间 ​ 恢复速度快
  • ​ Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑 ​ 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。 ​ 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

AOF

简介

以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。AOF默认不开启!当AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

执行流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内;
  2. AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

AOF启动/修复/恢复

AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

  • 正常恢复 修改默认的appendonly no,改为yes 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir) 恢复:重启redis然后重新加载

  • 异常恢复 修改默认的appendonly no,改为yes 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof--fix appendonly.aof进行恢复 备份被写坏的AOF文件 恢复:重启redis,然后重新加载

同步频率设置

appendfsync always 始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好 appendfsync everysec 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。 appendfsync no redis不主动进行同步,把同步时机交给操作系统。

Rewrite压缩

简介

AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

重写原理

AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。 no-appendfsync-on-rewrite: 如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能) 如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低) 触发机制,何时重写 Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发 重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。 auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发) auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。 例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB 系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size, 如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

重写流程
  1. bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。

  2. 主进程fork出子进程执行重写操作,保证主进程不会阻塞。

  3. 子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。

  4. 1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。

    2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。

  5. 使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

优劣
  • 备份机制更稳健,丢失数据概率更低。

    可读的日志文本,通过操作AOF稳健,可以处理误操作。

  • 比起RDB占用更多的磁盘空间。

    恢复备份速度要慢。

    每次读写都同步的话,有一定的性能压力。

    存在个别Bug,造成恢复不能。

Redis主从复制

是什么

主机数据更新后根据配置和策略, 自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主

能干啥

  • 读写分离,性能拓展
  • 容灾快速恢复

常用三招

  • 一主二仆
  • 薪火相传
  • 反客为主

哨兵模式

是什么

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

使用步骤

  1. 调整为一主二仆模式

  2. 自定义的redis目录下新建sentinel.conf文件,名字不能错

  3. 配置哨兵,填写内容

    sentinel monitor mymaster 127.0.0.1 6379 1

    其中mymaster为监控对象起的服务器名称, 1 为至少有多少个哨兵同意迁移的数量。

  4. 启动哨兵

  5. 当主机挂掉,从机中根据优先级别:slave-priority选举产生新的主机。原主机重启后会变为从机

  6. 复制延时:由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

故障恢复

新主登基:从下线的主服务的所有从服务里面挑选一个从服务,将其转成主服务。

选择的条件依次是:

  1. 选择优先级靠前的。优先级在redis.conf中默认:slave-priority 100,值越小优先级越高
  2. 选择偏移量最大的。偏移量是指获得原主机数据最全的
  3. 选择runid最小的从服务。每个redis实例启动后都会随机生成一个40位的runid

群仆俯首:挑选出新的主服务之后,sentinel(哨兵)向原主服务的从服务发送slaveof新主服务的命令,复制新master

旧主俯首:当已经下线的服务重新上线时,哨兵会向其发送slaveof命令,让其成为新主的从。

应用问题解决

缓存穿透

简介

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。

解决方案

  1. 对无效值进行缓存。如果一个查询返回的数据为空(不管数据存在与否),仍然把这个空结果(NULL)进行缓存。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟,最长不超过五分钟
  2. 布隆过滤器。把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端。存在的话再去查询是否存在于缓存中,存在则返回对应数据,不存在缓存中则查询数据库中是否存在对应数据。数据库中存在则更新缓存并返回,不存在则返回空。**但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**误判的原因是因为不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)。
  3. 设置白名单。使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
  4. 进行实时监控。

缓存击穿

简介

数据库访问压力瞬时增大;redis里面没有出现大量key过期;redis正常运行;redis某个key过期,恰好有大量请求访问使用这个key

解决方案

  • 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长
  • 实时调整:现场监控哪些数据热门,实时调整key的过期时长
  • 使用锁(效率低)
    • 在缓存失效的时候(判断拿出来的值为空),不立即去load db。
    • 先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key
    • 当操作返回成功时,再进行load db的操作,并回设缓存,最后删除mutex key;
    • 当操作返回失败,证明有线程在load db,当前线程睡眠一段时间再重试整个get缓存的方法。

缓存雪崩

简介

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,前者则是某一个key

解决方案

  • 构建多级缓存架构:nginx缓存+redis缓存+其他缓存(ehcache等)
  • 使用锁或者队列:用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况
  • 设置过期标志更新缓存:记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。
  • 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

分布式锁

java多线程

this锁与class锁

    public synchronized void test1(){

    }
    public void test2(){
        synchronized (this){

        }
    }
    public synchronized static void test3(){}
    public static  void test4(){
        synchronized (MethodSynchronized.class){

        }
    }

轻量级锁

生产者消费者

package DesignPattern;

import lombok.extern.slf4j.Slf4j;

import java.util.LinkedList;

import static java.lang.Thread.sleep;

@Slf4j
public class ProducePay {
    public static void main(String[] args) {

        QueueMessage queueMessage = new QueueMessage(2);


        for (int i = 0; i < 3; i++) {
            int id=i;
            new Thread(()->{
                queueMessage.put(new Message(id,"值"+id));
            },"生产者"+i).start();
        }

        new Thread(()->{
          while(true){
              try {
                  sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              Message message = queueMessage.take();
          }
        },"消费者").start();
    }
}
@Slf4j(topic = "c.QueueMessage")
class QueueMessage{

    private LinkedList<Message> list = new LinkedList<>();
    private int capcity;

    public QueueMessage(int capcity){
        this.capcity=capcity;
    }

    public Message take(){
       synchronized (list){
           while(list.isEmpty()){
               try {
                   log.debug("队列为空,消费者线程等待");
                   list.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           //从头部取

           Message message = list.removeFirst();
           log.debug("已消费消息{}",message);
           list.notifyAll();
           return message;

       }


    }

    public void put(Message message){
        synchronized (list){
            //判满
            while(list.size()==capcity){
                try {
                    log.debug("队列已满,生产者线程等待");
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.addLast(message);
            log.debug("已生产消息{}",message);
            list.notifyAll();
        }
    }
}
final class Message{
    private int id;
    private Object value;
    public Message(int id,Object value){
        this.id =id;
        this.value=value;
    }
    public int getId(){
        return id;
    }
    public Object getValue(){
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

ReentrantLock

公平锁

降低并发度

线程安全集合类

Blocking

大部分实现基于锁,并提供用来阻塞的方法

CopyOnWrite

修改开销相对于较重 读多写少

内部很多操作采用cas优化,一般可提供较高吞吐量

弱一致性

  • 遍历时弱一致性,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续遍历,这时候内容是旧的

  • 求大小弱一致性,size操作未必是100%准确

  • 读取弱一致性

    遍历时如果发生了修改,对于非安全容器来说,使用fail-fast机制让遍历立刻失败抛出ConcurrentModificationException

Concurrent

ConcurrentHashMap

JUC并发

基础知识

juc简介

java.util.concurrnet工具包,JDK1.5出现

进程与线程

  • 进程:进程是程序的实体,程序一旦运行就是进程;进程是资源分配的最小单位。
  • 线程:操作系统能够运算调度的最小单位。被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程并行执行不同的任务

线程的状态

线程状态枚举类

Java.Lang.Thread.State

线程状态。 线程可以处于以下状态之一:

  • NEW 尚未启动的线程处于此状态。
  • RUNNABLE 在Java虚拟机中执行的线程处于此状态。
  • BLOCKED 被阻塞等待监视器锁定的线程处于此状态。
  • WAITING 正在等待另一个线程执行特定动作的线程处于此状态。
  • TIMED_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态。
  • TERMINATED 已退出的线程处于此状态。 一个线程可以在给定时间点处于一个状态。 这些状态是不反映任何操作系统线程状态的虚拟机状态。

wait/sleep的区别

  1. sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
  2. sleep不释放锁,也不需要占用锁。wait会释放锁,但调用前提shi当前线程占有锁(代码在synchronized中)
  3. 都会被interrupted打断

并发与并行

串行

所有任务按照顺序进行,当前任务完成方可进行下一任务。

并行

单位时间内,多个任务同时执行(必须多核cpu才能做到)

并发

同一时间段多个任务同时执行(单核cpu就可以做到,并发微观串行,宏观并行)

管程

简介

管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。翻译成Java领域的语言,就是管理类的状态变量,让这个类是线程安全的。

管程对应的英文是Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。

作用

管程(monitor)是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同 一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行 JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程 (monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁 执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方 法在执行时候会持有管程,其他线程无法再获取同一个管程

用户线程和守护线程

  • 用户线程:平时用到的普通线程,自定义线程
  • 守护线程:运行在后台,是一种特殊的线程,比如垃圾回收
  • 当主线程结束后,用户线程还在运行,JVM 存活
  • 如果没有用户线程,都是守护线程,JVM 结束

线程的daemon属性为true表示守护,false表示用户线程,设置守护线程需要再start()之前

创建线程的方式

  1. 继承Thread类 很少用(单继承,继承珍贵)
  2. 实现Runnable接口 (作为Thread()的参数)
  3. 使用Callable接口
  4. 线程池

LOCK接口

ReentrantLock 可重入锁


ReentrantLock lock = new ReentrantLock();
//上锁
lock.lock();
try{
//代码
//解锁   
}finally{
    lock.unlock();
}

构造有参公平锁,无参非公平锁

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

ReadWriteLock

线程间通信

注意虚假唤醒问题 while循环代替for循环


集合线程安全

vector

syn关键字解决并发问题


Collections

List(String) list =Collections.synchronizedList(new ArrayList);
//synchronizedMap
//synchronizedSortdedMap
//synchronizedSortedSet

CopyOnWriteArrayList

public void add(E e){
    final Reentrantlock lock = this.lock;
    lock.lock();
    try{
        //总的概括就是 ”写时复制”
        Object elements = getArray();
        int len = elements.length;
        Object[] newElements =Arrays.copyOf(elements,len+1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    }finally{
        lock.unlock();
    }
}		

ConcurrentHashMap


多线程锁

synchronized实现同步的基础:Java中的每一个对象 都可以作为锁;

具体表现:

  • 对于普通同步方法,锁是当前实例对象;
  • 对于静态同步方法,锁是当前类Class对象;
  • 对于同步方法块,锁是synchronized括号里配置的对象

Callable接口

Callable接口创建线程有返回值,这是Runnable所没有的;

Callable 会抛出异常;

FutureTask构造可以传递Callable

class MyThread implements Runnable{
	@Override
	public void run() {
		}
}
新类 MyThread2 实现 callable 接口
class MyThread2 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
		return 200;
	}
}

Future接口

FutureTask<Integer> futuretask1 = new FutrueTask<>(new MyThread2());
//也可以这样写
FutureTask<Integer> futuretask2 = new FutrueTask<>(()->{ 
    return 200;
});
new Thread(futuretask2,"name").start();

辅助类

减少计数CountDownLatch

CountDownLatch允许一个或者多个线程去等待其他线程完成操作。

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行 减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法 之后的语句。

  • CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这 些线程会阻塞
  • 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程 不会阻塞)
  • 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
//定义一个数值为6的计数器
CountDownLatch count = new CountDownLatch(6);
//计数器减一,不会阻塞
count.countDown();
//数值减至零时唤醒主线程
count.await();

循环栅栏CycliBarrier

利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

//构造方法
//创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待它时,它将跳闸,并且当屏障跳闸时不执行预定义的动作。 
CyclicBarrier(int parties) ;
//创建一个新的 CyclicBarrier ,当给定数量的线程(线程)等待时,它将跳闸,当屏障跳闸时执行给定的屏障动作,由最后一个进入屏障的线程执行。  
CyclicBarrier(int parties, Runnable barrierAction) ;
//方法
//等待所有 parties已经在这个障碍上调用了 await 。  
int await() 
//等待所有 parties已经在此屏障上调用 await ,或指定的等待时间过去。  
int await(long timeout, TimeUnit unit) 
//返回目前正在等待障碍的各方的数量。  
int getNumberWaiting() 
//返回旅行这个障碍所需的聚会数量。  
int getParties() 
//查询这个障碍是否处于破碎状态。  
boolean isBroken() 
//将屏障重置为初始状态。 
void reset() 

信号灯Semaphore

//Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方法获得许可证,release 方法释放许可
//场景: 抢车位, 6 部汽车 3 个停车位
/**
* Semaphore 案列
*/
public class SemaphoreDemo {
 /**
 * 抢车位, 10 部汽车 1 个停车位
 * @param args
 */
 public static void main(String[] args) throws Exception{
 	//定义 3 个停车位(许可量)
	 Semaphore semaphore = new Semaphore(1);
 	//模拟 6 辆汽车停车
 	for (int i = 1; i <= 10; i++) {
 		Thread.sleep(100);
 	//停车
 	new Thread(() ->{
 		try {
            System.out.println(Thread.currentThread().getName() + "找车位 ing");
            //获取许可
 			semaphore.acquire();
 			System.out.println(Thread.currentThread().getName() + "汽车停车成功!");
 			Thread.sleep(10000);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println(Thread.currentThread().getName() + "溜了溜了");
            semaphore.release();
        }
    }, "汽车" + i).start();
    }
 }
}

读写锁

阻塞队列BlockingQueue

BlockingQueue提供了可阻塞的插入和移除的方法

ArrayBlockingQueue

底层采用数组实现的有界队列实现类

ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock ,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:

private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);

LinkedBlockingQueue

底层采用单向链表实现的阻塞队列,可以当作无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE

PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。

PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。

简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。

线程池

线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

线程池的七个参数

int corePoolSize 常驻线程数量

int maximumPoolSize 最大线程数量

long keepAliveTime , TimeUnit unit 线程存活时间

BlockingQueue<Runnable> workQueue 阻塞队列

ThreadFactory threadFactory 线程工厂

RejectedExecutionHandler handler 拒绝策略

拒绝策略

  • ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy :将任务交给调用者执行,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果应用程序可以承受此延迟并且要求任何一个任务请求都要被执行的话,可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy :不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。

微服务

服务远程调用

调用方式

  • 基于RestTemplete发起的http请求实现远程调用
  • http请求做远程调用是与语言无关的调用,只要知道对方的ip、端口、接口路径、请求参数即可

Eureka注册中心

EurekaServer:服务端,注册中心

  • 记录服务信息
  • 心跳监控

EurekaClient:客户端

Provider 服务提供者
  • 注册自己的信息到EurekaServer
  • 每隔三十秒向EurekaServer发送心跳
consumer 服务消费者
  • 根据服务名称从EurekaServer拉取服务列表
  • 基于服务列表做负载均衡,选中一个微服务后发起远程调用

搭建eureka-server

创建eureka-server服务
引入eureka依赖

引入SpringCloud为eureka提供的starter依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
编写启动类

给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}
编写yml

编写一个application.yml文件,内容如下:

server:
  port: 10086
spring:
  application:
    name: eureka-server
eureka:
  client:
    service-url: 
      defaultZone: http://127.0.0.1:10086/eureka
启动服务

启动微服务,然后在浏览器访问:http://127.0.0.1:10086

服务注册

引入eureka-client依赖

在yml中配置eureka地址

服务拉取

服务拉取是基于服务名称获取服务列表,然后再对服务列表做负载均衡

  • 用服务名代替IP、端口

  • 启动类中的RestTemplate添加负载均衡注释

    @loadbalanced

Ribbon负载均衡

饥饿加载

即时记录(以后整合)

为什么JAVA是半解释、半编译语言(JVM 执行引擎)

首先了解两个概念:编译和解释。

编译:将源代码一次性转换成目标代码的过程,类似于全文翻译

解释:将源代码逐条转换成目标代码逐条运行的过程,可理解为同声传译。可能没有整体优化效果好。

在Hotspot(Java虚拟机中的一种)中存在JIT即时编译器,能够捕获程序中的热点代码,编译成机器码缓存起来存入方法区中,当遇到相同的代码时,不必再去使用解释器翻译,直接去找对应的机器码执行。避免解释器重复多次的解释执行。

类加载过程

加载

  1. 通过一个类的全限定名获取定义此类的二进制字节流
  2. 将获取的字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.class对象,作为方法区的这个类的各种数据的访问入口

注意:数组类并不通过类加载器加载,而是由Java虚拟机直接创建,但数组类的元素还是要依靠类加载器进行加载。

其创建过程遵循以下规则:

  • 如果数组的组件类型(数组去掉一个维度的数据类型)是引用类型,就递归使用这些引用类型的类加载器进行加载。
  • 如果组件类型不是引用类型,例如 int[] 数组,Java 虚拟机会将数组标记为与引导类加载器管理
  • 数组的可见性与它的组件类型可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

Java规范中并没有规定 Class 对象的存放位置,对于Hot Spot 虚拟机来说,Class 对象虽然是对象,但却是存放在方法区中。

链接linking

验证

  • 文件格式验证 这阶段主要是检查字节流是否符合Class 文件的规范,版本是否能够处理
  • 元数据验证 对字节码描述的信息进行语义分析,验证类的继承关系
  • 字节码验证 字节码验证是验证过程中最复杂的一个阶段,它的目的是确定程序语义是合法的、符合逻辑的。在元数据验证完成之后,它会对方法体进行验证,确保程序不会做出危害虚拟机安全的行为。
  • 符号引用验证 最后一个阶段的验证放生在虚拟机将符号引用转化为直接引用的时候,这个阶段将会在解析阶段发生。符号引用验证可以看做是对类自身以外的信息进行匹配校验。

准备

  • 为类变量分配内存并且设置该类变量的默认初始值,即零值
  • 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段显式初始化
  • 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到java堆中

解析

  • 将常量池内的符号引用转换为直接引用的过程
  • 解析操作往往会伴随着JVM在执行完初始化之后再执行。
  • 符号引用就是一组符号来描述所引用的目标。在解析阶段,JVM根据字符串的内容找到内存区域中相应的地址,然后把符号替换成直接指向目标的指针、句柄、偏移量等,这些直接指向目标的指针、句柄、偏移量就被称为直接引用。
  • 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info

初始化

  • 初始化阶段就是执行类构造器方法()的过程
  • 此方法不需定义,是javac编译器自动收集类中所有变量的赋值动作和静态代码块中的语句合并而来
  • 不同于类的构造器
  • JVM会保证子类的 执行前,父类的已经执行完毕
  • JVM必须保证一个类d()在多线程下被同步加锁

SpringBoot

设计模式

创建型

  • 创建型模式关注点“怎样子创建出对象”
  • “将对象的创建与使用分离”
  • 降低系统的耦合度
  • 使用者无需关注对象的创建细节
    • 对象的创建由相关工厂来完成(各种工厂模式)
    • 由一个建造者来完成(建造者模式)
    • 由原来的对象克隆完成(原型模式)
    • 对象始终在系统中只有一个实例(单例模式)

单例

饿汉式(静态变量)

Class Singleton{
	//构造器私有 外部能new
	private Singleton{
	}
	//内部创建静态实例
	private final static Singleton instance = new Singleton();
	//提供公有静态方法 返回实例对象
	public Singleton getInstance(){
		return instance;
	}
}
  1. 优点:写法简单,在类装载时就完成了实例化,避免线程同步问题
  2. 缺点:在类装载时就完成实例化,没有达到lazy loading效果(延迟加载也叫动态函数加载,它是一种模式允许开发者指定程序的什么组件不应该在程序启动的时候默认读入内存。通常情况下,系统加载程序会同时自动加载初始程序和从属组件。在迟加载中这些组件只在调用的时候才加载。当程序有许多从属组件而且并不常用的时候,迟加载可以用于提高程序的性能。)
  3. 结论:可用,但可能会造成内存浪费

饿汉式(静态代码块)

Class Singleton{
	//构造器私有 外部能new
	private Singleton{
	}
	//内部创建静态实例
	private  static Singleton instance;
    
    static{
        instance = new Singleton();
    }
	//提供公有静态方法 返回实例对象
	public Singleton getInstance(){
		return instance;
	}
}

结论:和静态变量差不多

懒汉式(线程不安全)

Class Singleton{
	private Singleton{
	
	}
	private static Singleton singleton;
	
	public static Singleton getInstance(){
		if(instance == null){
			instance = new Singleton;
		}
		return instance;
	}
}
  1. 优点:实现了懒加载
  2. 缺点:只能在单线程下使用;多线程下因为if语句的存在可能会创建多个实例
  3. 结论:实际开发中别用;

懒汉式(线程安全,同步方法)

Class Singleton{
	private Singleton{
	
	}
	private static Singleton singleton;
	
	public static synchronized Singleton getInstance(){
		if(instance == null){
			instance = new Singleton;
		}
		return instance;
	}
}
  1. 缺点:效率低,每个线程要获得类实例时都要同步
  2. 结论:别用

双重检查

Class Singleton{
	private Singleton{
	
	}
	private static volatile Singleton singleton;
	public static Singleton getInstance(){
		if(singleton == null){       //第一个if为了提高效率,当为null时才进入syn代码块
			synchronized (Singleton.Class){
				if(singleton == null){ //第二个if为了防止出现多个实例的情况
					singleton = new Singleton();
					/*
						new实例过程
						1.分配内存给对象
						2.初始化对象
						3.设置singleton指向分配的内存空间(此时singleton才!=null)
						其中2,3会指令重排
					*/
				}
			}
		}
		return singleton;
	}
}

静态内部类

Class Singleton{
    private Singleton{}
    private static Class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    private static final Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}
  1. 优点:这种方式采用了类装载的机制来保证初始化实例只有一个线程

    ​ 静态内部类在Singleton类装载时不会立即实例化,而是在需要实例化时,调用getInstance()方法才会装载SingletonHolder类,从而完成Singelton的实例化

    ​ 类的静态属性只会在第一次加载类的时候初始化。所以JVM帮助了我们保证了线程的安全性,在别的类初始化时,别的线程是无法进入的

  2. 结论:推荐使用

枚举

enmu Singleton{
    INSTANCE;
    public void sayOk{
        System.out.println("ok");
    }
}
  1. 优点:即避免多线程同步问题,又可以防止反序列化创建新的对象

举例

java.lang.Runtime 饿汉式


注意事项

  1. 单例模式保证了系统内存中该类只存在一个实例化对象,节省了系统资源,对于一些需要频繁销毁创建的对象,使用单例模式可以提高系统性能
  2. 通过调用相应方法创建类实例,而不是通过new
  3. 使用场景:需要频繁的进行创建和销毁的对象、创建对象耗时过多或耗费资源过多(重量级对象)但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂、线程池、连接池、系统环境信息、上下文)

引申

  1. 系统环境信息?
  2. Spring中如何保持组件单例?
  3. ServletContext是什么?是单例吗?怎么保证?
  4. ApplicationContext是什么?是单例吗?怎么保证?
  5. 数据库连接池一般怎么创建出来的?怎么保证单实例?

工厂

工厂模式提供了一种创建对象的最佳方式。我们不关心对象的创建细节,只需要根据不同情况获取不同产品即可

简单工厂

原型

意图

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

主要解决

在运行期建立和删除原型。

何时使用

1、当一个系统应该独立于它的产品创建,构成和表示时。 2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3、为了避免创建一个与产品类层次平行的工厂类层次时。 4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。

如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。

关键代码

1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),

2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。

应用实例

1、细胞分裂。 2、JAVA 中的 Object clone() 方法。

优点: 1、性能提高。 2、逃避构造函数的约束。

缺点: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。 2、必须实现 Cloneable 接口。

使用场景

  1. 资源优化场景。
  2. 类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
  3. 性能和安全要求的场景。
  4. 通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
  5. 一个对象多个修改者的场景。
  6. 一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
  7. 在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。

注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。

建造者

建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。

意图

将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。

主要解决

主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。

何时使用

一些基本部件不会变,而其组合经常变化的时候。

应用实例

  1. 去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。
  2. JAVA 中的 StringBuilder。

优点

  1. 建造者独立,易扩展。
  2. 便于控制细节风险。

缺点

  1. 产品必须有共同点,范围有限制。
  2. 如内部变化复杂,会有很多的建造类。

使用场景

  1. 需要生成的对象具有复杂的内部结构。
  2. 生成的对象内部属性本身相互依赖。

注意事项

与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。

结构型

行为型

1
https://gitee.com/fresherlei/java-notes.git
git@gitee.com:fresherlei/java-notes.git
fresherlei
java-notes
Java笔记
master

搜索帮助