本博文借鉴了《计算机操作系统(第3版)汤子瀛》(西电出版社)、《数据结构严蔚敏》(清华大学出版社)、《java并发编程的艺术》(机械工业出版社)、《java学习笔记林信良》(清华大学出版社)、《深入理解java虚拟机周志明》(机械工业出版社)、《JUC并发编程全套教程》。鉴于本人水平受限,论述错误之处请读者之处,欢迎大家找错,共同进步。内容还未全部完成。
Object类的主要方法要掌握
public boolean equals()方法
public native int hashCode()方法是一个native,集合类的方法中这两个方法都重写了
public final void wait()
public final native void notify()
public final native void notifyAll() 这三个都和并发直接相关
public String toString()方法用来重写的,输出自定义的对象信息
public final native Class> getClass();用来获取class类对象,不可被重写,是一个本地方法
java的基本数据类型有4类八种:
整数型:byte,short,int,long,有时还有long long(java可能不支持这种数据类型)
浮点型:float,double
字符型:char
布尔型:boolean
因此java又称为半面向对象语言,基本数据类型并不是类类型。需要特别注意,在java中如果在为一个数字变量赋字面常数值时,整型默认是int,浮点型默认是double类型。另外,由于浮点数在机器中存储是IEEE 754标准,所以不能精确的存储每一个数学上的小数,在浮点数运算时判断相等时需要格外注意,最后加入一些极小值作为浮动值。
在进行运算时,不同数据类型在一块运算,结果以最高的数据类型为准,小的数据类型会扩展为大的数据类型,而结果变量的数据类型大于等于该数据类型不会出现问题,如果是小于则会出现截断的现象,损失精度。
int类型是有符号数,那么java中如何表示无符号数呢?
每一个基本数据类型都有包装类
Byte、Short、Integer、Long
Float、Double
Character
Boolean
与基本数据类型之间会自动装箱、拆箱。
自动装箱存在语法糖,Integer i = 100;编译期在编译后会把该句变为Integer i = Integer.valueOf(100);,在实际执行时又会检查Integer的缓存池,如果范围在-128~127之间会直接从缓存池中取出对象引用,不需要生成新的对象;如果不再该范围需要生成新的对象,因此Integer i1 = 100; Integer i2 = 100;使用==判断两者是否相等时,返回结果为真。
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
new Integer(123) 每次都会新建一个对象;
Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。在 Java 8 中,Integer 缓存池的大小默认为 -128~127
String,StringBuilder,StringBuffer的区别
String是一个不可变类,底层由不可变的char[]或者byte[]实现,每次对String操作时都会在内存中产生一个新的对象,虽然可以保证线程的安全性,但是却产生了大量的String对象;StringBuilder底层也是char[]实现,只不过对象的引用却不会改变,内存空间节省上比String要好,但是线程不安全;StringBuffer是线程安全的,在方法上大量的使用synchronized加锁,性能上有损失,不过经过JVM进行逃逸分析之后,确定该变量只在局部内使用则可以进行锁消除,或者锁粗化。
使用场景:String用于字符串不太改变且线程安全的情况中,StringBuffer用在线程安全和字符串经常改变的场景中,至于StringBuilder经常是被用于在单线程的情况下对StringBuffer的替代。
StringBuilder类内部也是由char[]数组完成实现的,因此可以负责可变字符串的任务,但是不是线程安全的。StringBuffer是一个线程安全的可变字符串类型,里面的方法大都有synchronized修饰,Jdk1.8之前String的底层对象是final char[],并且String类本身也不能被继承,String这两个特性很好的应用在多线程并发安全方面、不可变的键方面、常量池实现方面。
String a1 = new String("hx");
a1.intern();
String b = "hx";
System.out.println((a1 == b));
如果结果是false,那么说明a1与b引用的对象不同,说明String a = new String(“111”);会在字符串常量池中产生字面常量;如果结果是true,那么说明a1与b引用的对象相同,String a = new String(“111”);不会在字符串常量池中产生字面常量
例子:
String a = new String("hx");
System.out.println("1."+(a == a.intern()));结果会是false
String a1 = new String("h") + new String("x");
a1.intern();
String b = "hx";
System.out.println("2."+(a1 == a1.intern()));结果是true
System.out.println("3."+(b == a1));结果是true
锁的消除和粗化:
当频繁的对StringBuffer进行加锁、解锁,会造成性能上的损失。如果虚拟机探测到有一连串操作都是对同一StringBuffer对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,也就是在第一个和最后一个append操作之后。
锁消除就是虚拟机根据一个对象是否真正存在同步情况,若不存在同步情况,则对该对象的访问无需经过加锁解锁的操作。(比如说程序员使用了StringBuffer的append方法,因为append方法需要判断对象是否被占用,而如果代码不存在锁的竞争,那么这部分的性能消耗是无意义的。于是虚拟机在即时编译的时候就会将上面代码进行优化,也就是锁消除。
值传递与引用传递
在java中参数传递都是才用的是值传递,对于基本数据类型而言,采用值传递(实参与形参变量之间互不影响,形参的改变不会同步到实参中),引用类型也是值传递,变量存储的是该引用对象在堆中的地址,如果在调用函数内部对该变量的引用不做修改,引用没有发生变化,实参会感受到修改,否则实参与形参指向了两个不同的对象。
==与equals()方法
对于基本类型, ==判断两个值是否相等,基本类型没有 equals() 方法。对于引用类型,==判断两个变量是否引用同一个对象,而 equals() 需要根据类的实际定义来决定比较的策略。默认的equals()方法
public boolean equals(Object obj) {
return (this == obj);
}
如果需要根据对象的内容来判断对象是否是同一个,那么就需要重写equals()方法和hashCode()方法
【引用变量存储的可能是自定义计算的hashCode,原则上不同的对象可能会有相同的哈希码,即所谓的哈希碰撞,应该尽可能的使hash码分散开】
重写equals()方法的主要流程
public boolean equals(Object o){
if(this == o)
return true;
if(!(o instanceof ClassName))
return false;
ClassName t = (ClassName) o;
然后根据t取出每一项内容与this进行比较,如果全部相同则说明内容是一样 的,否则内容不一样
}
instanceof 是一个二元运算符,用于测试左侧的对象是否是右侧类的类型,返回值是一个布尔值。
浅拷贝只拷贝引用对象,不会深入的去拷贝引用内部的对象,深拷贝可以拷贝引用内部的对象。
抽象类与接口的区别
使用abstract声明的方法是抽象方法,拥有抽象方法的类称为抽象类,不能被实例化,只能被继承,接口在1.8之前可以看成是抽象类的扩展,没有具体的方法,从1.8开始,接口有默认函数,(如果想对接口进行功能扩展,定义一个新函数,实现类中必须都要有具体的实现,即使实现为空,修改接口的成本很高),接口的字段和方法都默认为public,字段默认是static和final的,在1.9之后方法可以声明为private的
比较:
泛型:
Throwable:
分为Error和Exception,Exception又分为RuntimeException(非受检异常)和编译期异常(受检异常,主要提醒程序员对显式可见的异常进行处理)
异常的产生过程:
安全迭代与快速迭代
反射:是框架的灵魂,大部分的框架内部是由反射机制来支撑的
何为反射?反射就是把类的各个组成部分封装成可被调用的对象类型
Field对象的获取和使用
getFields() 只能获取到public声明的所有成员变量,返回值是一个Field[]
getField(String name) 可以按名字获取单个成员变量,返回值是一个Field
getDeclaredFields() 可以获取到声明的所有成员变量,返回值是一个Field[],就连private类型的变量也可以获取到
getDeclaredField(String name) 可以按名字获取单个成员变量,返回值是一个Field
Field对象有两个方法
get(Object object),传入的是一个对象实例,返回对象中该Field对应的值value
set(Object object, Object value),第一个参数是对象实例,第二个参数是该Field域的新值
在默认情况下系统会对权限检查,如果不符合设置的权限,会抛出异常
此时可以使用setAccess(true);暴力反射,此时就不会检查权限问题,
Constructor对象的获取和使用
getConstructor() public类型的默认构造函数
getConstructor(参数class的列表) public类型的有参数的构造函数
getConstructors() 得到所有的public类型的构造函数
getDeclaredConstructor() 默认构造函数
getDeclaredConstructor(参数class的列表) 有参数的构造函数
getDeclaredConstructors() 得到所有的构造函数
Constructor对象包含的方法
newInstance()
newInstance(实参参数列表) 可以产生一个对象的实例,并为其成员变量赋值
setAccess(true),这里我们就可以看到即使把默认构造函数私有化,仍然可以使用暴力反射来创建对象
Method对象
getMethod(String name) 无参函数
getMethod(String name, Class列表) 有参函数
getMethods() 子类和父类中public的方法对象
getDeclaredMethod(String name)
getDeclaredMethod(String name, Class列表)
getDeclaredMethods() 子类的所有的方法,此时不再不包括父类的方法
Method对象的方法
Object invoke(Object o, 实参参数列表),返回值是Object
反射+配置文件的好处:原有开发模式是开发人员直接在程序中使用new关键字产生所需对象,这种方式耦合性太高,源码需要根据功能经常性的改动,一旦源码被修改,整个项目就会重新编译部署,通过反射+配置文件的方式,在源码中直接生成任意类型的对象并执行任意方法,有很好的动态性,并且把修改源码转变为修改配置文件。
序列化:实现Serializable接口,可以把对象的状态持久化到磁盘上,并且可以从磁盘上再反序列化回内存,
BIO在accept和read处阻塞,accept等待客户端发起socket连接,read等待客户端发起数据传输
BIO的server端的伪代码:
ServerSocket ss = new ServerSocket(端口号); 在服务器端该端口用来监听客户端发来的请求,默认绑定的IP地址是0.0.0.0【0.0.0.0地址的作用见网络部分】
Socket socket = ss.accept(); 服务器在此时阻塞,一旦有客户端发来的请求,就会立马获得关于该连接的socket对象
while(socket.read()){读取操作} 通过该socket对象读取客户端发来的数据,此时也是阻塞式的读取
BIO客户端的伪代码
Socket socket = new Socket(IP地址,端口号); 在客户端建立socket连接,指明服务器的IP地址和端口号
while(socket.write()){不断地写入数据} 反复的向服务器传递数据
上面的代码只是服务器单线程的情况,只能处理一个socket的连接,如果要是多线程的处理
ServerSocket ss = new ServerSocket(端口号); 在服务器端该端口用来监听客户端发来的请求
while(true){
Socket socket = ss.accept(); 服务器在此时阻塞,一旦有客户端发来的请求,就会立马获得关于该连接的socket对象
new MyThread(socket).start(); 此时需要一个新线程来处理新socket,并在run方法内部写明读取操作
}
上面的情况就是在服务器端多线程的版本,每来一个socket都需要一个线程来负责
NIO模式的伪代码
NIO模式允许单线程或者单进程来管理多个socket连接
SocketServer ss = new SocketServer(端口号);
List<Socket> ls = new ArrayList<>(); 记录下保活socket连接
ss.setnoblock(); 设置非阻塞模式
while(true){ 一直不停的轮询
Socket socket = ss.accept(); 在新的一轮中查看一下有没有新来的连接,这是非阻塞的,如果没有就继续执行
if(socket == null){ 没有新的连接
for(Socket s:list){ 查看一下旧连接中是否有数据到来
int flag = s.read();
if(flag){
读取数据
}
}
}else{ 有新的连接
list.add(socket); 新的连接就加入到队列中
for(Socket s:list){
int flag = s.read();
if(flag){
读取数据
}
}
}
}
上面的代码就很好地实现了单线程管理多路IO的场景,虽然使用了轮询机制把阻塞消除掉了,但还是存在问题的,每一轮都会检测所有保活的socket,当大部分保活的socket连接在轮询过程中是没有数据传输的,系统的性能就浪费在无意义的检测上,如果这样的轮询由java程序完成,也会频繁的发生用户态和内核态的切换操作。
把轮询的工作交给操作系统完成,只要操作系统返回给我们可用的socket即可,select和poll都是按照上面的情况进行运转的,没有本质的区别,只是在监督socket数量上有区别,select只能监督固定数量的socket,而poll可以监督动态数量的socket
epoll
不需要通过轮询的方式来获得完成事件的描述符,epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。通过回调函数内核会将 I/O 准备好的描述符放在一起管理,进程调用 epoll_wait() 便可以得到完成事件的描述符。
具体的使用场景
四类权限修饰符public、protected、default、private
public访问权限最大,可以在其他包,本包,本类内内任意访问
protected受保护的,只能在本包内,其他包内的子类,本类内中被访问
default默认情况,只是在本包内,本类内被访问
private私有,只能在本类中被访问
start()和run()方法:
启动一个线程并不是通过实例调用run方法产生的,而是通过start方法,启动线程,run方法内部定义了该线程执行的逻辑。java中产生的线程最终需要由os来支持的,因此,在start内部有一个native类型的start0方法
sleep()和yeild()方法:
都是使当前的线程停止执行,sleep是让当前的线程睡眠一段时间,而yeild是使任务调度器暂停一下,重新进行调度一次
①sleep()方法给其他线程运行机会时不考虑线程的优先级,任何线程都有平等的机会被调度;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后由Running转入阻塞(Timed Waiting)状态,而执行yield()方法后转入就绪(Runnable)状态;
③ 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException,而yield()方法没有声明任何异常;
④ sleep睡眠结束的线程并不一定立即会执行,建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性,并且sleep相比于yeild具有更好的表达性,不建议由yeild来进行并发的控制
currentThread()
getState()
getName()
join():
join函数表示等待某个线程执行完毕,可以有参数,表示需要等待多长的时间,以毫秒millisecond计算。在同步时可以使用,等待某个线程的执行完毕,thread.join();在调用线程内部等待thread线程执行完。
stop、suspend、resume:弃用
stop方法执行,立即释放线程所持有的全部的锁,并且没有给与线程释放资源,回滚操作的机会,造成数据不一致。suspend方法用于暂停线程,resume用于唤醒线程,在suspend时不会释放对锁的持有,因此在resume唤醒线程时,会因为无法获得锁而阻塞,此时就陷入了死锁状态
在操作系统层面,进程有4种基本状态(就绪、运行、终止、阻塞)
在java中线程有六种状态
New:线程对象刚被创建出来,还未调用start方法
Runnable:此时包含os中的三种状态:
就绪状态:线程在等待队列中排队,
运行状态:cpu正在调用执行该线程
阻塞状态:BIO模式,线程因为读写IO事件而阻塞掉
Blocked:
同步阻塞,synchronzied加锁,当线程获得不了synchronized锁时就会把线程阻塞到EntryList中
Waiting:调用wait()/sleep()/join()/pack()方法,
Timed-Waiting:
调用wait(long timeout)\sleep(long timeout)方法,有超时参数的方法
Terminated:线程终结
synchronized主要是利用操作系统中管程的思想设计的。
在jvm层面主要是由monitor enter和monitor exit两条指令实现的。
每个 Java 对象都可以关联一个 Monitor 对象(由os支持),如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
sleep(long n) 和 wait(long n) 的区别
锁的优化:偏向锁、轻量级锁、重量级锁
锁消除、锁粗化、逃逸分析
局部变量是否存在线程安全问题?
局部的基本数据类型是不存在线程安全问题的,局部引用类型变量如果同时被引用多次就可能会出现线程安全问题。
保证可见性:缓存一致性协议
把缓存中新的内容写回到主存中
通过处理器和总线,把该变量在缓存中的地址状态设置为无效
禁止指令重排序:内存屏障
但是仅保证了在本线程内部指令有序性
不可变,如果对一个变量设置为final类型,一旦赋值就不可改变,直接保证了多线程的安全
使用CAS和synchronized的场景:
使用CAS比较划算的场景:
线程等待锁释放的时间比较短,临界区的代码比较短,执行时间短,临界区竞争不激烈,cpu资源不紧张,多核处理器
使用synchronzied比较划算的场景:
线程在临界区中执行时间比较长,比如包含IO操作,等待锁释放的时间就比较长,临界区的竞争比较激烈,单核处理器
是一个接口,提供了对Lock锁对象的基本操作,lock(),unlock(),
模板为
lock.lock();
try {
临界区代码
} finally {
lock.unlock();
}
全称是 AbstractQueuedSynchronizer,是同步器工具的框架,定义了同步操作,可以实现自定义同步组件。
有一个volatile类型的int变量state,state为0表示可以去竞争锁,为1表示锁被别人获得。
有3个方法对同步状态进行操作
getState():
setState(int newState):
compareAndSetState(int expect, int update):
还有一个FIFO同步队列,Node head和tail,也是volatile类型,当线程获得不到锁时进入同步队列中等待;并且内部有一个ConditionObject的类,实现了Condition的接口,可以定义多个条件变量,每一个条件变量都是一个等待队列
public class ConditionObject implements Condition, Serializable{
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
……其他逻辑
}
Condition是一个接口,提供了对条件队列的操作方法,await()、signal(),具体实现由实现类去完成
自定义实现不可重入锁时,同步器可被重写的方法
tryAcquire(int arg):
protected boolean tryAcquire(int acquires) {
if (acquires == 1) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
tryRelease(int arg):
protected boolean tryRelease(int acquires) {
if (acquires == 1) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
return false;
}
isHeldExclusively():
protected boolean isHeldExclusively() {
return getState() == 1;
}
自定义模板:当我们使用AQS时,一般的会结合Lock接口使用,
class MySync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int acquires) {}
@Override
protected boolean tryRelease(int acquires) {}
protected Condition newCondition() {
return new ConditionObject();
}
@Override
protected boolean isHeldExclusively() {}
}
class MyLock implements Lock {
MySync sync = new MySync();
@Override
// 尝试,不成功,进入等待队列
public void lock() {
sync.acquire(1);
}
@Override
// 尝试,不成功,进入等待队列,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
// 尝试一次,不成功返回,不进入队列
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
// 尝试,不成功,进入等待队列,有时限
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
// 释放锁
public void unlock() {
sync.release(1);
}
@Override
// 生成条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
原理:
加锁的过程:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
外部调用lock()方法加锁,内部会调用acquire(1)方法获取锁,tryAcquire(1)【可重写】尝试获取锁一次,如果获得锁返回true,则!运算后为false,短路与,不再执行后面的语句,if结束;如果没有获得锁返回false,说明锁已经被别人占用,先执行addWaiter()方法把当前的线程放在同步队列的末尾,同步队列需要互斥的访问,由CAS保证安全性,调用acquireQueued()方法自旋竞争锁,里面包含一个死循环,每一个节点都会先自旋几次,只有头结点的后继节点才有权去尝试获得锁,循环几次之后获得不到锁就会阻塞掉自己,获得锁之后就把自己设置成head头结点。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取当前节点的前驱
if (p == head && tryAcquire(arg)) { // 是否是第二个节点,并且尝试获得锁
setHead(node); //设置自己为头结点
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 判断在获取锁失败之后是否应该park掉,如果要,则用park阻塞,被唤醒之后,继续循环竞争锁
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire()方法,把当前节点(一般是尾结点)的前驱结点的waitStatus设置为SIGNAL(值为-1表示需要唤醒后继节点,新节点加入到同步队列中时waitStatus是0),表明这个节点需要唤醒,shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1或者前驱不是head,失败,再次进入
shouldParkAfterFailedAcquire,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true,进入 parkAndCheckInterrupt阻塞当前线程
释放锁的过程:
在外部调用unlock()方法,里面调用release(),
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
把状态清空,state由1变为0,exclusiveOwnerThread设置成null,
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程,找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,此时就回到该线程的acquireQueued中,此时该节点的前驱是head并且尝试获得锁成功,会设置
exclusiveOwnerThread 为自己,state = 1,head 指向刚刚Node,该 Node 清空 Thread,原本的 head 因为从链表断开,而可被垃圾回收
【此处涉及一个公平性问题:如果恰好释放锁,此时又来一个新线程,新线程与head后继节点竞争锁,有可能新线程抢到锁,head后继节点仍然要等待】
内置了几个内部类
abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync
非公平锁,默认机制
可重入的原理:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 当前锁未被别人获取
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //判断当前线程与获取锁的线程是否一致,如果一致就重入
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); //此时状态变量累加
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //谎报军情
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//重入次数为0,释放锁,否则还有重入未清除
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
公平锁
实现原理:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
线程竞争公平锁的时候先判断同步队列中当前节点是否有前驱节点,如果返回true,表示有线程比当前线程更早的请求获取锁
synchronzied和ReentrantLock的区别和联系,使用场景
在竞争不激烈的情况下使用synchronzied,在竞争比较激烈的情况下使用ReentrantLock
AQS中给出了等待队列的实现方式,与同步队列的实现类似,复用了Node的定义,包含了两个指针,firstWaiter和lastWaiter,只不过这是一个单向的链表,并且链表没有头结点直接firstWaiter是首元节点。
await()过程:线程执行await()方法就会去等待队列中等待,释放掉所拥有的全部状态数(如果发生重入,状态数>1),调用park(this)方法阻塞掉自己
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 利用当前线程生成一个等待节点,并增加到等待队列的尾部
Node node = addConditionWaiter();
// 把锁的状态都释放掉,会唤醒等待队列中的头节点的后继节点,savedState还是重入的状态值,因为唤醒后获得锁需要该状态
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断是否在同步队列中,第一次是false,第二次是true
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 节点从等待队列中唤醒后重新进入同步队列,
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
COW,顾名思义写时复制,当线程对该对象进行写入操作时,系统会拷贝一份该对象,写线程会在拷贝副本中进行写入,写入完成之后引用交换,读线程还继续在原对象处进行操作,这种现象分离了读写操作,可以在一定程度上对共享变量进行并发操作,但是这种系统也存在缺点。
jdk中基于CopyOnWrite思想的类有CopyOnWriteArrayList、CopyOnWriteArraySet
内部维护了一个ReentrantLock的锁变量,对集合进行更新时,就去竞争这把锁
倒计时,当计数器为0时,才会执行其他的操作
1.7
类中包含两个静态内部类 HashEntry 和 Segment。Segment 类继承于 ReentrantLock 类,从而使得 Segment 对象能充当锁的角色,一个 Segment 对象包含若干个HashEntry。HashEntry 用来封装实际存储的键 / 值对;HashEntry之间链式组织。一个ConcurrentHashMap 实例中包含多个 Segment 对象。在HashEntry 类中,key,hash 和 next 域都被声明为 final 型,value域被声明为 volatile 型。Segment里面包含一个HashEntry的数组
static final class HashEntry<K,V> {
final K key; // 声明 key 为 final 型
final int hash; // 声明 hash 值为 final 型
volatile V value; // 声明 value 为 volatile 型
final HashEntry<K,V> next; // 声明 next 为 final 型
HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
this.key = key;
this.hash = hash;
this.next = next;
this.value = value;
}
}
HashEntry发生碰撞时,采用链表解决冲突,采用头插法,所以链表中节点的顺序和插入的顺序相反。
1.8
放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全,在结构上更偏向于传统的HashMap。get()方法没有加锁,在table的副本上进行查找。put()方法不允许有null的key或者value,当桶数组未初始化时进行懒惰初始化,当位桶头结点是空时直接把该值放入其中,这两步操作使用CAS方式保证并发安全,当出现冲突时,只把头结点当作锁,使用synchronized保证并发效率。
关于size的计算方式
Concurrenthashmap线程安全的,在jdk1.7中采用Segment + HashEntry的方式进行实现的,lock加在Segment上面。size计算是先采用不加锁的方式,计算元素的个数,最多计算3次:1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全,1.8中使用一个volatile类型的变量baseCount记录元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount,通过累加baseCount和CounterCell数组中的数量,即可得到元素的总个数;
线程变量,把变量和线程进行绑定,使得对变量的访问在线程之外无法获得,主要是数据隔离,并且在同一个线程内部,不同的组件之间可以传递变量。多线程对共享变量互斥的访问操作,需要加锁,加锁和解锁的开销是比较大的。
ThreadLocalMap是一个map结构,但是没有实现map的接口,与常见的map还是有区别的,内部包含一个Entry的静态内部类,继承自WeakReference,目的是把key做成弱引用,key是ThreadLocal类型,而value是Object类型,key是弱类型容易造成value的内存泄漏,(key是强引用导致key的内存泄漏),不过为了避免内存泄漏,最好的办法是显式的调用remove方法,把key和value都显式的置null;如果忘记了调用remove,系统还会在调用set和get方法时,顺便清除掉一部分key为null的Entry。
应用场景
生产者和消费者在同一个时刻都是不能有两个对缓存区操作的,因此需要一把大锁,可以使用synchronzied来实现,或者使用ReentrantLock和Condition条件队列实现
堆,虚拟机栈,方法区,本地方法栈,程序计数器
堆,方法区是线程共享的
栈,本地方法栈,程序计数器是线程私有的,
堆:是JVM中存储对象数据用的,
java堆内存分布
Jdk1.7中
年轻代:new出来的对象一般先放在这
老年代:在年轻代中经过了垃圾回收机制之后仍然存活的对象移入该区域中
永久区:存放class对象、常量池、类的静态变量等信息
Jdk1.8中
年轻代:Eden + 2Survivor (Survivor from + Survivor to)8:1:1的比例
老年代:1:2的比例
metaspace:等价于永久代,不过该空间不在虚拟机JVM空间中,占用本地的内存空间,称为非堆内存,存放class对象,常量池、类的静态变量存储在堆中
虚拟机栈:是JVM中存放java程序中线程运行控制信息的,JVM为每一个线程分配一个线程栈帧,线程栈帧由该线程执行过程中方法栈帧组成,按照调用的顺序依次入栈弹栈,该栈帧中包含如下信息
局部变量表:运行该方法时传入的参数和该方法过程中用得到的参数变量
操作数栈:在执行方法过程中出现的临时变量都储存在该处
返回地址:
锁记录:
动态链接:class文件编译成字节码文件过程中,会把方法和字面常量值、返回值变成常量信息,在程序中用到的时候再引用上他们就可,把符号引用转化成直接引用。动态链接字段就是指向常量池中符号引用,目的就是为了支持动态链接。
为什么需要常量池?或者说动态链接是怎么回事?(以后再说)
java中的绑定机制,何为静态绑定,何为动态绑定?
编写高级程序时只关注于程序结构的逻辑关系,按照书写的顺序执行程序,当然也可以使用标号的形式跳转,不过这并不提倡,总之,我们在编写源文件时并不关注程序在内存中的具体分布情况,每一行代码具体的物理地址是多少,即使最终会有一个物理地址。在翻译为字节码文件的时候我们看到了文件有一大堆的常量声明,这其实是关于字面值、方法的标号,这是一种符号引用,不过在CPU看来这些标号最终会变成一个偏移地址,指引cpu取指令或者取数执行,在链接期间的解析工作就是把符号引用转化为直接引用,这其实是一个直接地址,根据该直接地址就可以找到方法的入口,进而执行方法的内部逻辑。这个解析的过程可能在加载的时候就可以确定下来,这是静态绑定,如果这个过程需要在运行期间才能最终确定是哪一个直接地址,这是动态绑定。
解绑的过程:
(1) 所有私有方法、静态方法、构造器及初始化方法都是采用静态绑定机制。在编译器阶段就已经指明了调用方法在常量池中的符号引用,JVM运行的时候只需要进行一次常量池解析即可。
(2) 类对象方法的调用必须在运行过程中采用动态绑定机制。
首先,根据对象的声明类型(对象引用的类型)找到“合适”的方法。具体步骤如下:
① 如果能在声明类型中匹配到方法签名完全一样(参数类型一致)的方法,那么这个方法是最合适的。
② 在第①条不能满足的情况下,寻找可以“凑合”的方法。标准就是通过将参数类型进行自动转型之后再进行匹配。如果匹配到多个自动转型后的方法签名f(A)和f(B),则用下面的标准来确定合适的方法:传递给f(A)方法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。
③ 如果仍然在声明类型中找不到“合适”的方法,则编译阶段就无法通过。
(3)根据在堆中创建对象的实际类型找到对应的方法表,从中确定具体的方法在内存中的位置。
虚拟机栈可能出现的异常:
1.StackOverFlowError:栈帧异常,申请的栈帧超过了jvm规定的最大栈帧长度,加大内存空间也只是减缓时间,并不能本质的解决问题
2.OutofMemoryError:内存满异常,整个的虚拟机栈空za间太大,内存不够用
其他的一些异常:下标越界、IO异常、除0异常、ClassNotFoundException
java虚拟机栈管理着java方法的运行情况,而本地方法栈管理着本地方法的执行情况,都是由native关键字修饰
原理:为每一个对象设置一个引用计数器,如果对某一个对象有引用,就加1,当引用计数器为0时就说明不再引用该对象,该对象就可被回收
优势:
实时性比较高,无需等待内存不够用才开始回收,只要计数器为0就可回收
回收时应用不需要挂起,如果某一个对象申请内存,满了,直接报内存溢出错误
回收是定向的,只对该对象进行操作,不会扫描到全对象,
劣势:
每次引用都会更新,会有额外的耗时
即使内存够用,仍然需要对计数器进行实时统计
无法避免循环引用的问题,最大的缺点
例子
Class A{
public Class B b;
}
Class B{
public class A a;
}
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null; b = null;
原理:垃圾回收分为标记和清除阶段,从GC Root对象开始把引用关系展开成一棵树状结构,标记时从GC Root开始向下标记,凡是经过的对象都被标记,在清除时把未标记的对象都回收。这种从GC Root可达性的算法解决了引用计数不能回收循环引用对象的弊端
优势:可以解决循环引用
劣势:在清除和标记时都需要扫描全部的对象,并且要停止程序的运行,对交互性要求比较高的应用来说不友好
通过标记清除法回收得到的内存空间往往是不连续的,零碎的空间,造成内存碎片
GC Root的候选者:
虚拟机栈中局部变量表中引用的对象
本地方法栈中 JNI 中引用的对象
方法区中类静态属性引用的对象
方法区中的常量引用的对象
原理:标记阶段与上相同,只是在清除阶段,对内存进行整理压缩成连续的
原理:把内存一分为二,一个称为from区,一个为to区,对象先放入from区,等到快满时进行垃圾回收,把需要回收的对象回收,剩余存活的对象都被拷贝到to区,to区的存活对象连续排列,然后把from和to的标签更换一下,进行后续处理
优势:解决了内存碎片的问题,但是如果存活对象比较多,复制移动的次数和代价就多,因此适合回收对象较多的情况,
劣势:在垃圾对象较少的情况下不太合适,另外内存的利用率只有一半
把全堆内存看作是一块一块的分区,每一个分区都可以是Eden、Survivor From、Survivor To、Olden,进行垃圾回收时可以回收价值比较高的分区,改变了原有回收一个大新生代分区的高延时,从而实现可控制的低延时策略。
新生代中的对象存活时间比较短,大部分对象只使用一次就不再被使用,回收比较频繁,使用复制算法效率比较高;而年老代长得对象存活时间比较长,大部分对象都不会被回收,回收频率不高,因此使用复制算法效率不高,一般使用标记类算法,标记-清除或者标记-整理\压缩。
新生代为什么要分为Eden区和Survivor区?
新生成的对象都先进入新生代区,对象的生命周期短,回收频繁,存活率低,适合用复制算法进行垃圾回收,而老年代的对象都是存活时间比较长的,回收并不频繁,适合使用标记类算法进行回收。复制算法要求内存一分为二,留出空间进行拷贝,如果新生代不分代,那么势必要一分为二,此时用于空余交换的区域所占内存的比重为1/31/2=1/6,而现在的分配方式为Eden:Survivor=8:1:1,用于空余交换的区域所占内存的比重为1/31/10=1/30,内存利用率得到提高
内存泄漏:
定义:当对象不再使用的时候, GC并不能回收它们
例子:
1.当访问数据库资源、socket网络资源、IO资源时没有显式的调用close方法,此时这些资源并不能被回收
2.单例模式下,单例对象持有对外部对象的引用时,由于单例对象的生命周期长,导致该外部对象也不能被回收
3.在ThreadLocal使用过程中,有可能会造成泄露
内存泄露太多,最终后果将会导致内存溢出
四种引用:
引用:在堆中开辟一块内存存储对象,在栈中通过变量进行访问,栈中的变量存储的值就是该对象的地址,
一般情况下只有引用和未引用两种语义,为了扩展引用的语义,也为了增强程序员在编程上和JVM垃圾回收时的灵活性,增加了几种引用
强引用:
Object o = new Object();直接使用new关键产生的对象,当内存不够用时也不会回收这类对象,
JavaAPI中提供了Reference抽象类,在程序中是无法单独使用这些引用的,
软引用SoftReference:内存不足就回收
Object obj = new Object();
SoftReference sf = new SoftReference(obj);
obj = null; // 使对象只被软引用关联
弱引用WeakReference:发现就回收
Object obj = new Object();
WeakReference wf = new WeakReference(obj);
obj = null;
【ThreadLocal使用的就是弱引用】
虚引用PhantomReference:一个对象是否有虚引用的存在,不会对其生存时间造成影响,也无法通过虚引用得到一个对象。为一个对象设置虚引用的唯一目的是能在这个对象被回收时收到一个系统通知。
以上的这三种扩展引用都需要强引用的支持
连线表示垃圾收集器可以配合使用。(可以想一想为什么会在其中的两个有连线,而不是任意的连线)
单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
串行收集器,年轻代,复制算法
并行收集器,年轻代,复制算法
并行收集器,更关注于高吞吐量=单位时间内cpu执行程序所占用的时间(单位时间=执行用户程序时+垃圾回收时间),年轻代,复制算法
标记-整理,老年代,串行收集器
标记-整理,老年代,并行收集器
第一款关注于低延迟的垃圾回收器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记-清除算法。
分为以下四个流程:
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
并发标记:从直接关联对象开始逐层向下寻找对象,它在整个回收过程中耗时最长,不需要停顿。
重新标记:并发标记期间因用户程序继续运作而导致标记产生变动,此时需要修正变动对象的标记记录,需要停顿。
并发清除:不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
为什么使用标记-清除而不是标记-整理?
标记整理算法需要进行对象的移动操作,在垃圾回收时不能与用户线程并发
3.3.7 G1回收器
第一款可控的低延迟垃圾回收器
G1 把堆划分成多个大小相等的独立区域(Region)。通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
具备如下特点:
空间整合:整体来看是基于“标记 - 整理”算法实现的收集器(筛选回收未并发可用标记-整理),从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
Minor GC和Full GC的时机
启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在
扩展类加载器(Extension ClassLoader)这个类加载器是由
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将
应用程序类加载器(Application ClassLoader)这个类加载器是由
AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的
getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
索引建立的条件:
不适合建立的条件:
索引优化的原则:
全值匹配我最爱,最左匹配记心间
【全值匹配:where条件中的字段个数及其顺序、order by字段与索引的个数与顺序一致】
【最佳左前缀匹配原则:一般是用在联合索引的情况下,索引依次的从左侧开始匹配,不能跳过索引的列】
带头大哥不能死,中间兄弟不能断
索引列上不计算,like%最右边
范围之后全失效,字符串要加’’
多用覆盖少用,不等null or全失效
范围查询在索引上的表现:
在单个索引上,可以利用索引来完成快速的检索操作,但是如果该索引的列值并不是很分散,而是比较集中,此时并不是绝对的会使用索引,当使用索引产生的结果比较多的时候,查询器会选择到底是根据索引进行查询,还是直接全表扫描。原则上根据索引直接定位到一个叶子结点。
在联合索引上,范围查询之后的字段索引失效
一条select语句在执行引擎中执行的过程:
(1) from:对左表left-table和右表right-table执行笛卡尔积(ab),形成虚拟表VT1;
(2) on: 对虚拟表VT1进行on条件进行筛选,只有符合条件的记录才会插入到虚拟表VT2中;
(3) join: 指定out join会将未匹配行添加到VT2产生VT3,若有多张表,则会重复(1)~(3);
(4) where: 对VT3进行条件过滤,形成VT4, where条件是从左向右执行的;
(5) group by: 对VT4进行分组操作得到VT5;
(6) cube | rollup: 对VT5进行cube | rollup操作得到VT6;
(7) having: 对VT6进行过滤得到VT7;
(8) select: 执行选择操作得到VT8,本人看来VT7和VT8应该是一样的;
(9) distinct: 对VT8进行去重,得到VT9;
(10) order by: 对VT9进行排序,得到VT10;
(11) limit: 对记录进行截取,得到VT11返回给用户。
一条SQL语句经历的过程:
(1)客户端发来一条sql语句,服务器端先查询缓存,如果缓存中有该sql语句的查询结果,直接返回给客户端即可,
(2)服务器端经过解析、处理阶段,在经过查询优化器得到该语句的执行计划
(3)查询执行引擎会根据该执行计划调用相应的存储引擎执行该sql语句,把结果返回给用户,并且自己也缓存下结果
事务有四个属性ACID
事务不隔离带来的缺点:
脏读数据
丢失修改【系统默认加锁完成】
不可重复读
MVCC多版本并发控制
有三个要素:隐式字段、undo日志、ReadView读视图
①隐式字段:数据库中每一行记录都有3个隐式字段,一个是主键DB_Row_id,一个是更新该数据行的事务ID即DB_Tx_id,一个是指向前一个时刻数据库中该行记录DB_Roll_ptr
②undo日志:把事务中对数据库的更新操作记录在该日志中,在回滚事务时作为参照,拥有相同主键的数据行按照时间顺序串成一个链表,尾部是最新的数据行,头部是最旧的数据行
③ReadView读视图:在每一个事务执行begin语句时,系统都会为该事务分配一个唯一的事务id,还会形成当前数据库的一个读快照,包含三个属性,tx_list、up_limit_id、low_limit_id,
tx_list表示当前还活跃的事务id列表,up_limit_id表示当前已经提交事务id的最大值,一般是tx_list列表中最小元素-1,low_limit_id表示当前最大事务的下一个事务,一般是tx_list列表中最大元素+1,需要根据当前事务id和读视图从undo日志中选择出恰当的数据行记录,
DB_Tx_id
DB_Tx_id>low_limit_id,
意味着id为DB_Tx_id的事务对当前事务来说,在当前事务开始的时候还未发生,数据不可见,即使DB_Tx_id事务有可能已经提交
up_limit_id
总之,不带锁的select语句触发快照读,可重复读;带锁的select语句和update、delete、insert触发当前读,不可重复读
LBCC基于锁的并发控制
锁的分类
锁在索引上的表现
主要的作用是确定传输媒体在传输比特流时的特性约束,强调通用的协议规范,而不是关注于传输媒体本身。
IP地址分类
常见的分类方式
CIDR地址块
公网IP和私网IP
路由器不转发私有地址,私有地址的产生更丰富了IP层,如果两个专用网络(局域网)通过公用互联网进行通信,可以使用隧道技术,
如果一个专用网络需要和互联网上某个主机进行通信需要NAT转换器,把专用网络IP转换成一个可用的公网IP。
Localhost、0.0.0.0和127.0.0.1有什么区别?
0.0.0.0地址的用途
在主机中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过这些IP地址都能够访问该服务。
在路由中,0.0.0.0表示的是默认路由,即当路由表中没有找到完全匹配的路由的时候所对应的路由,一般都是在路由表的最后一项。
127.0.0.1地址的用途
而所有网络号为127的地址都被称之为回环地址,127.0.0.1-127.255.255.255,以回环地址为目的IP地址的报文都不会转发到网络上,而是直接由本主机进行接收,而且其他主机也不能通过回环地址访问该主机。其实,回环地址并不能代表本主机,而是0.0.0.0才能代表本主机。
Localhost的用途:
Localhost更多的是一种域名上的表示,只不过大多数的情况下localhost被绑定到于127.0.0.1上
比如我有一台服务器,一个外网地址A,一个内网地址B,如果我绑定的端口指定了0.0.0.0,那么通过内网地址或外网地址都可以访问我的应用。但是如果我只绑定了内网地址,那么通过外网地址就不能访问。所以如果绑定0.0.0.0,也有一定安全隐患,对于只需要内网访问的服务,可以只绑定内网地址。
无连接的,数据通信之前不需要建立连接,数据通信完成也就没有必要释放连接。面向报文的,UDP一次传输就交付整个的报文,不会在运输层对报文进行分割。尽最大努力交付,发送方只是把数据传输给接收方,不保证报文段是否重复、丢失、缺少,不是可靠性的传输,不需要维持复杂的连接状态(可靠性的保证)
因此就UDP本身来说是不可靠的,如果要使UDP可靠,那么需要由应用层协议来保证,或者根据TCP的可靠传输原理进行改造【主要就是确定重传机制和确认机制】
面向连接,数据传输之前两方先建立一条虚连接,数据传输完成之后会释放虚连接
面向字节流,TCP把应用层的报文看成是一个一个的字节,传输的基本单位是字节,但是交付的时候是报文段,报文段包含多少字节由可靠性原理的参数保证
全双工,任何时刻双方都可以在通道上进行数据传输,因此在释放连接时要相互的发送释放连接的请求
保证可靠,强制性的保证数据有序到达,并有序交付上层应用程序
为什么说TCP是可靠的?
TCP的连接保证了可靠性,连接是双向的信息交互,一方驱动着另一方改变状态,连接记录两个端口间的通信状态,如果没有连接则无法知道丢失了哪个数据包,重复收到了哪个数据包,也无法确保数据包之间的到达顺序,还有很多增加可靠性的功能都无法应用。所以如果UDP也能交互起来,则也可以是可靠的。
首部格式:
需要特别关注的字段是:
序号:发送的字节流都会按照顺序进行编号,序号字段用于标识在该报文段中第一个字节的序号
确认号:确认已经收到N-1号之前的字节数据,接下来想要接收对方的N号字节
窗口:通报给发送方接收方的接收缓存窗口的大小,可以允许发送方接下来发送的最大字节数,
可靠性传输协议:
停等协议:
发送方发送一个报文段,必须等待接收方发回确认后才会传输下一个报文段,如果接收方因报文段发生错误(被人篡改)没有正确接收报文段,一直不发回确认,发送方对该报文段的计时器超时,重传该报文段,这是最暴力保证可靠性的办法,不过信道的利用率低,
信道利用率=
发送报文段时间/(发送报文段时间+RTT传输时间+接收方发回确认时间)
后退N帧协议
发送方可以一次发送多个报文段,但是接收方只能同时接收一个报文段,对按序接收的报文段确认,采用累积确认方式。可以提高信道利用率,但是不能反映接收端接收报文段的真实情况
选择重传协议
接收方可以缓存下不按序到达的报文段,确认的时候而是选择性的确认,避免了无意义的重复传输
简单来说就是滑动窗口协议,发送方会等待接收方的确认从而移动窗口向前滑动,发送方的发送窗口=min{接收窗口,拥塞窗口}
TCP通信状态和控制码都是非常重要的
三次报文握手,seq非常重要
三次报文握手的原因是防止过期失效的连接报文到达服务器端,从而造成虚假连接,浪费TCP资源;如果第三次握手报文没有在规定时间内到达服务器端,那么服务器端和客户端会做什么?
服务器端:此时服务器端处于SYN_RECV状态,在超时时间内没收到第三次握手报文,则会重试发送第二次握手报文,默认是5次,给与客户端重试的机会,如果在重试期间仍然没有收到,则会释放此次连接
客户端:此时发送出去第三次报文之后就变成了ESTABLISHED,可以传送数据,此时服务器端会发送RST报文,客户端收到后就会感知到服务器端的错误
四次报文挥手
四次握手的原因:
TCP是全双工,双方都需要进行关闭TCP资源,当被动方收到第四次的挥手报文之后就可直接关闭TCP连接,而主动方还需要等待2MSL(max segement live Time)时长才能关闭TCP连接
1.主动方确认被动方连接释放。避免第四次挥手失败,服务器端一直等待释放连接。等待FIN重传的时长+ACK确认的时长,就可以保证主动方知道被动方是否已经正确接收到了第四次挥手报文
2.确保旧报文已经在网络中传输完毕。避免建立新连接时,新旧链接的端口号一致,网络中留存的旧报文对新报文产生影响
拥塞控制
TCP的流量控制是要求发送方的发送速率不要太快,可以让接收方有时间接收并及时交付字节,这就要求发送方的传输窗口不要大于接收方的接收窗口,而拥塞控制更关注于在报文传输的过程中出现网路拥塞,路由器的接收缓存较小,处理器的效率不高,就会出现超时的情况,甚至丢包。
TCP的拥塞控制手段有四种,慢开始,拥塞避免,快重传和快恢复、
①在一开的时候,不确定网络的拥塞情况,如果贸然的传输多个数据段可能出现拥塞,此时,采用慢开始的算法,发送方按照每一个轮次1,2,4,8,,,,这样的数量发送报文段,达到初始设置的阈值之后开始拥塞避免
②拥塞避免阶段在每一传输轮次拥塞窗口加1,而不是像慢启动阶段成倍的增加,如果在传输过程中发送方某一个报文段的确认帧超时,此时发送方会认为出现了网络拥塞,开始慢启动,把当前窗口值一半设置为慢开始的门限,此时出现超时有两种情况,一是网络中真的是很拥塞,一种是该报文段不小心丢失,第二情况下采用慢开始,效率低,因此应该让发送端尽早的获知哪一个报文未按序接收到,进入快重传和快恢复阶段
③接收方连续的发送3个为按序到达的报文的确认帧,提醒发送方快恢复,此时,不执行慢开始算法,把当前的窗口值一半设置为下一个传输轮次的拥塞窗口,执行拥塞避免算法
53
有两种方式解析域名
迭代式
递归式
控制连接 21,服务器与客户端之间的控制信息传输通道
数据连接 20,服务器与客户端之间的数据信息传输通道,建立收据连接的方式有两种,第一种主动连接,由服务器端发起连接建立的请求,一般端口号是20,第二种被动连接,由客户端发起连接,服务器端的端口号不是20,50000-50009,
SFTP简单文件传输协议
源端口68,目的端口67
80
请求报文的格式:
请求行
请求头
空行
请求体
响应报文的格式:
响应行
响应头
空行
响应体
状态码
1XX:状态信息
2XX:成功信息,表示服务器成功的执行了用户的请求
3XX:重定向类信息
4XX:错误类信息,一般是服务器端无法执行客户端的请求
5XX:服务器信息
http1.0、1.1和1.2有什么区别?
http1.0当时只用于一些简单的网页请求,而http1.1是现在使用最广泛地http协议版本,
1.0与1.1的区别
①支持长连接和请求流水线:在1.0时代,每一次http请求都要建立tcp连接并释放连接,网络资源极大的浪费在连接上,1.1可以支持keep-alive的长连接,并且在一个连接中可以多次请求不用等待回复,但是服务器端是按照请求到达的顺序来决定响应的
②支持虚拟主机:在1.0时代默认一台服务器绑定一个唯一的IP地址,没有在请求头部中设置主机名,随着虚拟技术的产生,一台物理主机可能包含着多个虚拟主机,共享着一个ip地址,此时就需要区别这些主机,引入host字段
③缓存:1.1缓存策略更灵活、更丰富
④节约带宽:
⑤错误处理:增加了
https与http有什么区别
http的安全性问题:
使用明文进行通信,内容可能会被窃听;
不验证通信方的身份,通信方的身份有可能遭遇伪装;
无法证明报文的完整性,报文有可能遭篡改。
https是由http和ssl组装产生的,使用加密方式来避免窃听,使用认证来避免伪装,使用完整性检查避免篡改
加密的方式
对称密钥加密
优点:运算速度快
缺点:会话的密钥无法安全的传输
非对称密钥加密
优点:很好的解决对称密钥在传输过程中的不安全性
缺点:运算速度慢
A想与B通信,A只需要知道B的公钥即可,B的公钥可以在网络中任意传播,只要别人不清楚B的私钥就可
非对称加密的另一项工作就是数字签名,不可抵赖
A对某一个消息使用私钥加密,消息发送到B端使用A的公钥解密,此时只要能正确解密就说明这是A发来的,不可能是由其他人发来的
https采用混合加密的方式,采用非对称加密方式加密会话密钥,使用对称密钥(会话密钥)加密通信信息
认证过程
Client认证Server的身份,当然Server也可以认证Client的身份,只不过不太常用,现在只介绍client认证Server的过程
Server向CA(Certificate Authority)机构提出申请,CA会为其生成一个公钥,并且把Server的公钥和身份信息做一个hash得到一个摘要,对摘要使用CA的私钥加密,密文就作为Server的的数字签名,当client请求server时,server会先把证书发给client,client使用CA的公钥解密签名得到摘要,并且把Server公钥和身份信息hash,两者如果相等则说明证书有效,否则无效,server是一个虚假的身份
完整性检查
对传输的数据信息做一个摘要,
443
MySQL 3306
Redis 6379
Tomcat 8080
请简述客户端发起一个url请求到服务器端都经历了哪些过程?
在网络中通信最重要的是IP地址,和MAC地址,端口号,端口号一般都是由协议决定的,是公开的,但是IP和mac要在通信的过程才能确定,
1.DHCP协议
假设主机并不知道IP地址,子网掩码,DNS域名服务器IP地址,默认网关地址
应用层:封装DHCP discover协议报文
运输层:调用UDP协议封装,源端口68,目的端口67
网络层:使用IP协议封装报文,源IP0.0.0.0,目的255.255.255.255,在IP层上广播这个报文
数据链路层:源MAC是本机MAC,目的MAC是FF:FF:FF:FF:FF:FF,在数据链路层上广播该报文
物理层:传输二进制流
只有DHCP服务器才会对该报文响应,发回主机的IP地址、子网掩码、默认网关、DNS域名服务器IP地址
2.DNS
使用DNS域名解析协议解析域名获得目的IP地址
应用层:封装DNS请求报文
运输层:使用UDP协议封装报文,目的端口53
网络层:使用IP协议封装报文,源IP为刚才获得的本机IP,目的IP为刚才获得的DNS域名服务器IP
数据链路层:源MAC是本机的MAC,目的MAC不清楚,使用FF:FF:FF:FF:FF:FF,在数据链路层上广播该帧
物理层:传输二进制流
DNS域名服务器收到DNS的解析请求之后会把解析的IP地址返回给主机
3.ARP
主机知道目的端的IP地址,但是不知道目的端的MAC地址,ARP地址解析协议一般只在本局域网内进行解析
主机生成一个ARP的查询报文,源IP为本机IP,源MAC为本机MAC,目的IP为DNS服务器的IP,目的MAC为FF:FF:FF:FF:FF:FF,
该报文会广播全网,只有和目的IP匹配的DNS服务器才会对该报文响应,其余主机都会丢弃给报文,然后DNS服务器会发送一个单播的报文回应主机,主机就可获得该DNS服务器的MAC地址了。如果两者不在同一个子网内,主机也就没有必要知道DNS域名服务器的地址,此时只需要知道默认网关的MAC就可,再按照上述的步骤进行一遍即可。其实,在进行解析之前,要先检查两者是否在同一个子网内,根据本机IP和子网掩码获得本机所在的网络号,目的IP与子网掩码&运算之后也得到一个网络号,如果两者相等则说明在同一个子网中,否则就是两个子网。
4.HTTP
应用层:封装HTTP请求
运输层:使用TCP封装请求报文,目的端口是80,需要三次报文握手
网络层:使用IP协议封装报文,源IP是本机IP,目的IP是服务器IP
数据链路层:源MAC是本机MAC,目的MAC一般是默认网关的MAC
物理层:传输二进制流
浏览器收到该响应报文之后,抽取出response内容进行渲染展示给用户
处理机调度
进程和线程的区别
1、资源分配:进程是资源的分配单位,而线程不拥有资源,线程可以访问进程中的资源
2、调度:线程是调度的单位,同一个进程中线程切换不需要切换进程,当隶属于不同的进程中的线程切换时,需要进程的切换,
3、系统花销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
4、通信方式:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC(Inter-Process Communication)。【信号量+共享内存、管道、消息队列、socket】
进程切换都涉及哪些方面?为什么进程调度要比线程调度代价高?
进程间的切换需要改变的资源有:数据寄存器、程序计数器PC、进程栈帧信息、页表、IO设备信息、文件打开表的信息、获得的资源信息,进程间的虚拟内存是不同的,进程间切换的时候也会切换虚拟内存,不过最终是在物理内存上进行执行的,但是新的进程在第一次访问内存空间的时候TLB和页表都是空的,发生缺页中断,需要由磁盘调入页进入内存,这是比较耗时的。在线程切换时,线程会共享进程的地址空间,因此不会产生缺页的情况,运行效率高。
fork的父进程和子进程
父进程fork之后创建子进程,该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。子进程复制父进程的所有资源,子进程的代码段、数据段、堆栈都是指向父进程的物理空间
处于效率考虑,linux中引入了“写时复制技术-Copy-On-Write” ,若两个进程一直只是读数据,则子进程一直不会复制,直到任一进程进行写操作
父进程和子进程执行顺序没有规定,可以乱序执行,如果需要同步可以使用锁
在程序的语境下获得的变量的地址都是虚拟地址。
孤儿进程:父进程先执行完毕,子进程还在运行,此时由系统的init 进程(pid 为 1 )所 “收养”,由它做善后工作(把子进程的进程描述符从进程表中删除掉)。
僵尸进程:正常的父子进程间的关系应该是,子进程先执行结束,父进程等待子进程执行结束,把子进程描述符删除掉,但有时子进程已经结束了,父进程没有获取到结束的信号,此时父进程退出程序时,子进程描述符仍然滞留在进程表中,不过僵尸进程不会对系统造成伤害。
饿汉式
只要一加载该类时就创建出该类的唯一实例
// 问题1:为什么加 final
不允许继承,防止父类中方法被重写破坏单例性
// 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例
声明一个private Object readResolve()方法,返回单例对象
// 问题3:为什么设置为私有? 是否能防止反射创建新的实例?
在类外不能使用new关键字创建对象,但是不能阻止反射机制创建对象实例
// 问题4:这样初始化是否能保证单例对象创建时的线程安全?
static变量在类加载的时候就要执行的,类对象在内存中只有一份,是由jvm保证单线程执行的,因此该语句可以保证线程安全
// 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由
方法比变量有更好的封装性,可以返回单例的同时,提供其他的逻辑操作;可以懒汉初始化
public final class Singleton implements Serializable {
private Singleton() {}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
private Object readResolve() throws ObjectStreamException{
return INSTANCE;
}
}
懒汉式
等到真正使用该对象时才创建
public final class Singleton {
private Singleton() {}
private static Singleton INSTANCE = null;
public static Singleton getInstance() { //线程不安全
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
// 分析这里的线程安全, 并说明有什么缺点
线程安全,但是每一次都需要竞争重锁,时间开销大
public final class Singleton {
private Singleton() {}
private static Singleton INSTANCE = null;
public static synchronized Singleton getInstance() {
if( INSTANCE != null ){
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
双重检查锁
// 问题3:解释为什么要加 volatile ?
在synchronized代码块内部有可能发生指令重排,先返回对象,然后再调用构造方法生成实例,会使得线程拿到的对象是不完整的,造成运行异常,加上volatile防止指令重排
// 问题1:对比实现3, 说出这样做的意义
第一次为空检查是为了防止当对象生成之后线程仍然去竞争锁,第二次为空检查是为了避免重复性的生成实例对象
// 问题2:为什么还要在这里加为空判断, 之前不是判断过了吗
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE != null) {
return INSTANCE;
}
synchronized (Singleton.class) {
if (INSTANCE != null) {
return INSTANCE;
}
INSTANCE = new Singleton();
return INSTANCE;
}
}
}
枚举方式
// 问题1:枚举单例是如何限制实例个数的
INSTANCE是final static类型的
// 问题2:枚举单例在创建时是否有并发问题
没有
// 问题3:枚举单例能否被反射破坏单例
不能被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
可以避免反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
可以写构造方法把初始化的逻辑放入
enum Singleton {
INSTANCE;
}
静态内部类方式
// 问题1:属于懒汉式还是饿汉式
懒汉式,类加载机制也是懒汉式的,用到的时候才进行加载,因此只调用Singleton的其他方法或属性时是不会加载该内部类的,只有调用getInstance方法时才会加载内部类
// 问题2:在创建时是否有并发问题
加载该静态内部类的时候,对于static变量在初始化的时候执行赋值语句,由jvm保证并发安全,
public final class Singleton {
private Singleton() { }
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
除此之外克隆会破坏单例,要么单例类不实现Cloneable接口,要么实现Cloneable接口重写clone()方法时仍然返回单例对象,不再调用Object的clone()方法
数据库的索引为什么就选择了B+树,而没有选择其他的结构?
最小生成树
最短路径
拓扑排序
关键路径
插入排序
直接插入
折半插入
希尔
选择排序
简单选择
树形排序
堆排序
交换排序
冒泡排序
快速排序
在常见版本中,我们都是选择最左最右的元素作为枢轴,当数据基本有序的时候,此时就退化为一棵倾斜严重的二叉排序树,时间复杂度趋近于O(n^2),为了避免这种情况,我们可以使用随机化手段打乱这种有序性,[left, right]随机的选择枢轴
归并排序
二路归并
基数排序