JAVA面试总结 查漏补缺

Java基础

四种访问权限

public:所有类可见。
private:只有同一类内部的方法可见,内部类也可以访问到。
default(friendly):类自身以及同一个包的类可以访问。如果不特意说明访问控制权限,则默认default。——因为默认不用申明,容易忘记
protected:类对自身、同一个包的类、以及自己的子类访问。继承可见(extends)

抽象类和接口的区别

java类继承是单继承,可以写非抽象方法,非抽象方法不能;接口可以多继承,只能有抽象方法,没有静态方法和常量的属性,只有接口的实现类才能重写接口中的方法。

  1. 都是上层的抽象层。
  2. 都不能被实例化
  3. 都能包含抽象的方法,这些抽象的方法用于描述类具备的功能,但是不比提供具体的实现。

HashMap和HashTable的区别

(1)线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;

(2)插入null:HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null;

(3)容量:HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;

(4)Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行散射运算;

// HashMap
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 下标index运算
int index = (table.length - 1) & hash(key)

// HashTable
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

(5)扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;

(6)结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;

(7)继承关系:HashTable继承自Dictionary类;而HashMap继承自AbstractMap类;

(8)迭代器:HashMap是fail-fast(查看之前HashMap相关文章);而HashTable不是。

String

不同方式String初始化的区别

  1. string str1 = “abc”;String str2 = new String(“abc”);
    前者会在常量字符池Stringpool中创建一个对象,如果常量字符池Stringpool中已经有了,则直接引用。
    后者会在常量字符池Stringpool和堆中分别创建一个对象,如果常量字符池StringPool中已经存在abc对象,则只在堆中创建一个abc对象。

  2. 当调用str2.integer()方法时,可以将字符串放入常量字符池StringPool中,如果已经存在则直接返回这个对象。
    在这里插入图片描述

== / equals / hashcode的关系

  1. ==是比较两个对象的地址是否相等
  2. equals默认和 == 一样也是比较地址,也可以根据自己的需求来重写什么叫做相等
  3. hashcode 是根据对象的地址来返回一串int型数字,如果对象不一样的话返回的值也不一样

instanceof

用来判断一个对象是否是一个类的实例
在这里插入图片描述

Java底层如何实现排序的?

Java中Arrays.sort使用了两种排序方法,快速排序和优化归并排序。
快速排序主要针对基本的数据类型(int short long)排序,而归并排序用于对象类型的排序
如果数据量小于60会使用插入排序,插入排序是稳定的

java中的堆和栈

栈:主要用于存储局部变量和对象的引用变量,每个线程都有一个独立的栈空间,所以线程之间是不共享数据的;
栈的特点是存取速度快,但所存数据的大小和生存期必须是确定的(编译后就已经确定大小)
堆:堆中主要存储实例化的对象和数组。线程共享。堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,但缺点是存取速度较慢

hash冲突的解决方法

1.开放地址法(线性探测法,二次探测法)
2.在散列函数法
3.链地址法(hashMap底层实现)

继承

几种修饰符的访问权限范围

在这里插入图片描述

static关键字的作用

一.修饰变量:
1.静态变量在类加载的时候被创建并初始化,只被创建一次(类加载只进行一次),可以修改
2.静态变量属于整个类而不属于某个对象。

二.修饰方法
1.可以通过类名来访问,不需要创建对象来访问,可以用来实现单例模式
2.静态方法只能调用静态方法和静态变量

三.修饰静态代码块
在类加载的时候执行,且只执行一次

this与super关键字

this
表示当前对象,指向成员变量和成员方法
指向某个构造方法,通过this调用其他构造方法,this() 表示无参构造方法

super
在子类构造器中显示调用父类构造器,super必须在子类构造器的第一行
可以在子类中充当临时父类对象,super.父类方法名。

类方法与this调用本类的类方法

类(静态)方法是指用static修饰的方法,普通方法叫对象(实例)方法

类方法中不能引用实例变量(非static)、不能通过this和super调用,可以直接调用,包括本类和其他类;
实例方法(非static)可以通过super.方法名,对象名.方法名调用父类的实例方法、可以通过类名.方法名,super.方法名调用父类的静态方法、可以通过this.方法名调用本类方法。

本类的类方法(static)可以通过类名.方法名调用

接口实现

非抽象类实现接口,必须实现接口中所有方法;
抽象类实现接口,则可以不实现接口中的所有方法,因为抽象类中允许有抽象方法的存在!

方法头指:修饰符+返回类型 +方法名(形参列表)
接口的访问权限:public
接口实现的方法头遵循两同两小一大原则
子类与父类方法名相同,参数类型和列表相同,
子类方法返回类型小于等于父类方法返回类型,
子类方法抛出异常小于等于父类方法抛出异常,
子类方法访问权限大于等于父类方法访问权限。

JVM

Java异常体系

请添加图片描述
Error:主要是虚拟机产生,与代码编写者无关,例如:outOfMemoryError
Exception:主要有运行时异常和io异常
运行时异常:数组越界,空指针异常
Io异常:找不到文件

发生oom的可能区域:
除了程序计数器都有可能发生

虚拟机栈:当jvm尝试去扩展栈空间失败时可能会抛出oom异常

堆:堆中对象堆积起来无法释放

方法区:

四种引用类型:

主要体现在对象不可达性和对垃圾回收的影响:
强引用:只要有强引用指向一个对象,就表面对象还活着,垃圾回收期不会碰这种对象
软引用:只有当jvm认为内存不足的时候才会去试图回收这些对象
弱引用:不能使对象豁免垃圾回收,只是提供一种对象的访问途径
虚引用:不能提供他访问对象,仅仅提供一种确保对象呗finalize之后做某些事情的机制

JAVA虚拟机是如果加载类的

1.加载:主要是用类加载器(启动类加载器,扩展类加载器,应用类加载器)来查找对应类的字节流
双亲委派的好处:由于类随着类加载器一起有一种优先级的层级关系,从而使基础类得到统一

2.链接
验证:确保字节流的安全性,不会对虚拟机造成危害
准备:为类的静态字段分配内存
解析:将符号引用解析成实际引用

3.初始化:调用方法,为静态变量赋与实际的值,执行静态代码块

Clinit方法

1.当类有字段赋值操作或者静态代码块时,编译后会为这个类生成clinit方法来执行这些语句。
2.clinit方法是被加锁的,同于一时刻只能有一个线程执行某个类的clinit方法。
https://blog.csdn.net/qq_33014341/article/details/122155179

运行时数据区域:

运行时内存区域:

1.程序计数器:记录正在执行虚拟机字节码的指令地址
2.java虚拟机栈:主要存储局部变量表
3.本地方法栈
4.堆:存储对象的地方,有新生代和老年代
5.方法区:存储类信息,常量,静态变量等信息

内存分配策略:

1.对象优先在eden区域分配
2.大对象直接进入老年代
3.长期存活的对象进入老年代:会有一个年龄计数器,达到指定的阈值就会进入老年代

FullGC触发条件:

1.调用system.gc()
2.老年代空间不足
3.minor GC时老年代空间分配担保失败

为什么重写equals的同时也要重写hashcode

首先必须满足如果equals相同,那么hashcode也必须相同,如果hashcode不相同,那么equals必须不相同的原则,否则会导致该类无法与基于散列值的集合类(hashmap , hashset ,hashtable)结合正常运行

因为对于这些集合在进行重复性判断时,是先用hashcode判断,如果相同在用equals判断

java中的多态

分为两种:

  1. 编译时多态:体现在重载(方法名相同而参数不同),在编译时期就根据传入的参数确定好调用哪个方法;
  2. 运行时多态:体现在方法的重写。在运行时期判断引用类型的实际类型根据实际的类型调用其相应的方法;
    当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。
    参考链接:https://www.runoob.com/w3cnote/java-polymorphism.html

接口类和抽象类的异同

区别:
1.抽象类可以有非抽象方法,但接口只能有抽象方法
2.接口中的成员变量默认修饰为public static final(高度抽象的模版,所以这些都是提取出来的不变特征),方法默认修饰为public abstract。抽象类中的成员变量可以被不同的修饰符来修饰
3.类可以实现多个接口但只能继承一个抽象类

相同:
1.不能被实例化
2.派生类都必须实现未实现的方法

java BIO NIO AIO 序列化

字节操作:使用inputStream和outPutSream实现,字节是传输和存储的最小单位
字符操作:inputStreamReader:将字节流解码成字符流
OutPutStramWriter:将字符流编码成字节流
对象操作:序列化就是将对象转换成字节序列,方便存储和运输,两个用途:

  1. 把对象的字节序列永久地保存到硬盘中,通常存放在一个文件里
  2. 在网络上传送对象的字节序列

在很多应用中,需要对某些对象进行序列化,让他们离开内存空间,入住到物理磁盘方便长期保存,比如session对象,当有大量用户并发访问的时候可能会出现10万个session对象,内存吃不消,此时就需要将这些session先序列化到硬盘中,等要用的时候在把对象还原到内存中。

当两个进程在进行远程通信的时候,彼此可以发送各种类型的数据,无论是何种类型的数据,都会以二进制的序列在网络上传送。发送方需要把这个java对象转换成字节序列,才能在网络上传送;接收方则需要把字节序列恢复成java对象。

Io与NIO的区别:

  1. NIO是非阻塞的
  2. NIO是面向缓冲区的,IO是面向流的

AIO:异步非阻塞

集合

hashtable与hashmap的区别

1.hashmap线程不安全,hashtable线程安全
2.hashmap最多允许一个键为null,而hashtable不允许
3.hashtable中默认的hash数组大小为11,增加的方式是old*2+1,而hashmap默认是16,肯定是2的指数
4.计算hash的方法不一样,hashtable是直接用hashcode,而hashmap使用了key的高16位和低16位做异或运算

Linkedhashmap继承于hashmap,可以按顺序读取,底层使用的是双向链表
TreeMap实现了sortmap接口,可以对元素进行排序,底层使用的红黑树

设计模式

单例模式

/**
 * 单例模式的特点:
 * 首先,单例模式使类在程序生命周期的任何时刻都只有一个实例,
 * 然后,单例的构造函数是私有的,外部程序如果想要访问这个单例类的话,
 * 必须通过getInstance()来请求(注意是请求)得到这个单例类的实例。
 */
public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {
    }

    private static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
  1. 为什么要用判断双重?
    因为可能有两个线程都执行完了第一个if语句,如果没有第二重判断,那么当其中有个线程执行完synchronized里面的语句之后,另外一个线程跟着也会执行,这样就达不到单例模式的效果

  2. 第一重判断去掉也可以实现,为什么不去掉?
    考虑这样一种情况,就是有两个线程同时到达,即同时调用getInstance(),
    此时由于singleton == null,所以很明显,两个线程都可以通过第一重的singleton == null,进入第一重 if 语句后,由于存在锁机制,所以会有一个线程进入 lock 语句并进入第二重 singleton == null ,而另外的一个线程则会在lock语句的外面等待。而当第一个线程执行完 new Singleton()语句后,便会退出锁定区域,此时,第二个线程便可以进入lock语句块,此时,如果没有第二重singleton == null的话,那么第二个线程还是可以调用new Singleton()语句,这样第二个线程也会创建一个Singleton实例,这样也还是违背了单例模式的初衷的,所以这里必须要使用双重检查锁定。
    细心的朋友一定会发现,如果我去掉第一重singleton == null,程序还是可以在多线程下完好的运行的,考虑在没有第一重singleton == null的情况下,当有两个线程同时到达,此时,由于lock机制的存在,第一个线程会进入lock语句块,并且可以顺利执行new Singleton(),当第一个线程退出lock语句块时, singleton这个静态变量已不为null了,所以当第二个线程进入lock时,还是会被第二重singleton == null挡在外面,而无法执行new Singleton().
    所以在没有第一重singleton == null的情况下,也是可以实现单例模式的。

  3. 那么为什么需要第一重singleton == null呢?
    这里就涉及一个性能问题了,因为对于单例模式的话,new Singleton()只需要执行一次就 OK 了,而如果没有第一重singleton == null的话,每一次有线程进入getInstance()时,均会执行锁定操作来实现线程同步,这是非常耗费性能的,而如果我加上第一重singleton == null 的话,那么就只有在第一次,也就是singleton ==null成立时的情况下执行一次锁定以实现线程同步,而以后的话,便只要直接返回Singleton实例就 OK 了而根本无需再进入lock语句块了,这样就可以解决由线程同步带来的性能问题了。

参考:单例模式双重检查锁机制 https://qinjiangbo.com/mechanism-of-double-locking-check.html

多线程

Synchronized关键字

(1)synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

(2)Synchronized的作用主要有三个:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。

(3)Synchronized 原理
在编译的字节码中加入了两条指令来进行代码的同步。
**monitorenter :**每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

**monitorexit:**执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

(4)Synchronized的优化

(5)synchronized与Lock的区别

  1. lock是一个类,主要有以下几个方法:
  2. lock():获取锁,如果锁被暂用则一直等待
  3. unlock():释放锁
  4. tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true
  5. tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间
    请添加图片描述(1)Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。
    (2)synchronized能锁住类、方法和代码块,而Lock是块范围内的。
    (3)Lock能提高多个线程读操作的效率;
    (4)Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。如果面试问起,你就说底层主要靠volatile和CAS操作实现的。

volatile关键字

  1. 禁止指令重排
  2. 变量可见性,保证该变量对所有线程可见。这里的可见性指的是当一个线程修改了变量的值,那么新的值对于其他线程是可以立即获取的。

内存语义:读这个变量的时候,JMM会把本地内存中的变量设置为无效,从主内存中取;
写这个变量的时候,JMM会把本地内存变量刷新到主内存当中。

实现:添加Volatile关键字会在汇编代码中多处一个lock前指令,也就是内存屏障。https://blog.csdn.net/haihui_yang/article/details/103789384
Volatile关键字
可以看到 volatile 修饰的变量确实会多一个 lock 指令。对比下图的普通变量。

0x000000010fea8c14: lock addl $0x0,(%rsp)  ;*putfield volatileCount
                                           ; - com.yhh.example.VolatileTest::increase@7 (line 22)

普通变量

volatile禁止指令重排操作

1.字节码层面:当代码中使用volatile修饰变量时,会在编译后的字节码中为变量添加ACC_VOLATILE标记,
2.JVM层面:JVM规范中有4个内存屏障,LoadLoad/StoreStore/LoadStore/StoreLoad,在读到ACC_VOLATILE标记时会在内存区读写之前都加屏障。
3.操作系统和硬件层面:操作系统执行该程序时,查到有内存屏障,使用lock指令,在指令前后都加lock(屏障),保证前后不乱序。

为什么存在指令重排

一条汇编指令的执行步骤

  1. 取指 IF
  2. 译码和取寄存器操作 ID
  3. 执行或者有效地址计算 EX
  4. 存储器访问 MEM
  5. 写回 WB

在CPU工作中汇编指令分多步完成,比如,取指时会使用PC寄存器和存储器,译码会用到指令寄存器组,执行时会使用ALU,写回时需要寄存器组。
每一步涉及到的硬件可能不同,于是有了流水线技术来执行指令。

指令1: IF->ID-> EX ->MEM ->WB
指令2: IF->ID-> EX ->MEM ->WB
如果我们假设,指令2需要等待指令1完全完成后,假若指令1每一个操作都需要1毫秒,那么指令2需要等待5毫秒,如果这样,实际上存在很大的资源浪费,并没有把设备资源完全发挥出来。所以,而使用流水线,指令2只需要等指令1IF的操作完成,即可入厂,这时指令2只需要等待1毫秒,这么大的性能提升是非常明显的。所以这时候指令1和指令2谁先完成,就不得而知了。

因为流水线满载后一旦中断,所有的硬件设备会进入一个停顿期,再次满载,需要几个周期,对性能损失比较大。指令重排就是一种减少流水线中断的技术。

流水线技术并不是说让多个指令并行执行,可能还是需要等他其他指令执行完才可以执行,那么这个时候等待就有一个停顿,我们可以让和这个指令不相干的指令继续执行,这就是指令重排。——目的同流水线技术一样:提高CPU资源利用率

指令重排有可能带来一个问题——乱序
乱序也必须保证程序上下文的因果关系不发生改变,如果无法保证,那么就应该遵守hapen-before原则,不能指令重排。
https://blog.csdn.net/qq_26400953/article/details/107967270
https://blog.csdn.net/weixin_43982927/article/details/107969119

进程、线程区别

进程间通信的方式

线程池

Executor

为什么使用线程池:手动创建线程耗费性能,不利于管理。
首先创建线程池有两种方式:使用Executors工厂来创建ThreadPoolExecutor这类自定义线程池。

使用Executors工厂来创建

Executors是一个类,用来创建线程池,常用的有四种线程池

  1. newFixedThreadPool 创建一个可重复固定的线程数的线程池
  2. newCachedThreadPool 创建一个可缓存的线程池,调用execute将重复用以前构造的线程(如果当前线程可用)。如果没有可用线程则创建新的线程并加入到池中。终止并从缓存中移除那些已有60s未被使用的线程。
  3. newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
  4. newScheduledThreadPool 创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

使用示例:请添加图片描述

使用ThreadPoolExecutor这个类自定义创建线程池

请添加图片描述
请添加图片描述

ThreadLocal

请添加图片描述
请添加图片描述

中断线程

  1. 线程执行完毕会自动结束
  2. 在线程处于阻塞,期限等待火无期限等待时,调用线程的interrupt()会抛出interruptedException异常从而提前终止线程
  3. 若没有处于阻塞状态,调用interrupte()将线程标记为中断,此时在调用interrupted()判断线程是否处于中断状态,来提前终止线程。
  4. 线程池调用shutdown()等待所有线程执行完毕之后关闭

CAS机制(Compare And Swap)

CAS操作是一个原子操作,由一条CPU指令完成,使用了3个基本操作数,内存地址V,旧的预期值A ,要修改的新值B。
需要更新一个变量的时候,只有当内存地址V中的值和预期值A相等的时候才会修改为新值B ,如果失败则自旋,重新尝试。

问题:

  1. ABA 解决办法,加一个版本号,只有当版本号和预期值都符合的时候才修改
  2. 不能保证代码块的原子性,CAS机制知识保证一个变量的原子操作

JMM的理解

JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。 JMM可以理解为是一个规范,一个抽象概念,并不真实存在。
请添加图片描述
JMM的核心是找到一个平衡点,在保证内存可见性的前提下,尽量的放松对编译器和处理器重排序的限制。
可能会产生缓存一致性的问题:
1.总线锁定
2.缓存一致性协议

三大特性:原子性,可见性,有序性

Java的并发采用的是共享内存模型
  • 线程之间的共享变量存储在主内存(Main Memory)中。
    - 共享变量 <—>主内存
  • 每个线程都有一个私有的本地内存(Local Memory)。
  • 本地内存中存储了该线程以读/写共享变量的副本。
    - 共享变量的副本<—>本地内存
JMM工作流程

在这里插入图片描述

JMM规定

  • 按顺序执行read–load,且不可单独出现。
  • 按顺序执行store-- write,且不可单独出现。
  • 有assign操作后,变量改变后,需要同步到主内存中。
  • 新变量必须诞生在主内存中,不可使用未load过的变量。
  • 一个变量同一时刻只允许一个线程lock它。且一个lock就要一个unlock解开。
  • 对变量lock操作,将会清空所有本地内存中此变量。
  • unlock一个变量前必须有store 和 write操作。

JVM的锁优化

1.自旋锁和自适应自旋锁
线程挂起和恢复都需要很大的性能开销,很多共享数据的锁定状态只持续很短的时间,为这段时间挂起恢复不值得,所以可以让现场进入一个自旋状态,但仍占用cpu的时间,默认是10次,超过10次则采用传统的方式挂起线程。自适应自旋锁会根据上次在这个锁上自旋的时间调整自选次数

2.锁消除
检测到对局部变量加锁,会自动将锁消除,如在一个方法里面有sb = s1 + s2 + s3以前会转化为StringBuffer(线程安全,有加锁)的append操作,优化之后会转换成StringBuilder的sppend操作

3.锁粗化
对一个对象反复加锁解锁,如上述的append方法,编译器会优化,只在最外层加一个锁

4.轻量级锁
对象有一个对象头,对象头分两部分区域,一部分存储子树的hashcode,GC分代年龄,锁标志位等,官方成为mark word,另一部分用于存储指向方法区对象类型的指针;

轻量级锁的执行过程是代码进入同步块的时候,如果当前对象没有被锁定(锁标志为是01)则先在线程的栈帧中建立一个锁记录空间(lock record)将对象的mark word拷贝过来,然后利用cas操作尝试将对象的markword更新为指向lock record,如果成功则当前线程拥有该对象锁,并且将锁标志位从01改为00,如果更新失败则会检查当前对象markword是否指向当前线程的栈帧,如果是则直接进入同步块执行否则说明当前的对象锁已经被其他线程占有。如果有两条以上的线程竞争这个锁,那就会膨胀成重量级锁,锁标志为变成10;

5.偏向锁
锁对象第一次被线程获取的时候,会将当前的锁标志设置为偏向锁01状态,并使用cas操作将当前线程的id记录在markword当中,如果成功那么在以后的操作当中不需进行任何操作就可以进入同步代码块,当有另外一个线程获取当前锁的时候,偏向模式就宣告结束,撤销偏向锁之后恢复到未锁定或者轻量级锁状态

计算机网络

tcp和udp的区别:

Udp:无连接,尽最大可能交付,没有拥塞控制流量控制
Tcp:面向连接,可靠交付,有拥塞控制和流量控制

浏览器输入URL的全过程

1.DNS解析,获取ip地址(本机,本地域名服务器,根域名服务器,顶级域名服务器,权限域名服务器)
2.建立TCP连接——三次握手
3.浏览器发出http请求
4.服务器进行响应
5.TCP连接释放——四次挥手
6.浏览器渲染

为什么是三次握手,四次挥手

三次握手:防止之前滞留的连接请求再次到达服务端
四次挥手:因为tcp是全双工模式,客户端停止发送请求之后,服务端也要停止发送请求

time_wait存在的原因,时间是多少(两倍的报文最大存活时间)

1.确保客户端发送的最后一个报文能被收到,服务端可以正常关闭。
2.让所有报文都在网络中消失,时间是两倍的最大报文存活时间。

tcp的可靠传输靠什么

超时重传:如果已经发送的报文在超过时间内没有被确认,那么就重新发送这个报文

Tcp的滑动窗口

发送方和接收方都有一个滑动窗口——发送窗口、接收窗口

TCP流量控制

流量控制是为了控制发送方的发送速率,保证接收方来得及接收
通过滑动窗口来控制,根据报文知道对方窗口的大小,然后根据窗口大小来控制发送速率

TCP拥塞控制

如报文过多,会导致超时重传,又会导致网络更加阻塞
请添加图片描述

  1. 慢开始和拥塞避免:
    慢开始:设置初始的报文数量为1;——指数增长(只在TCP连接建立时和网络出现超时时才使用)
    拥塞避免:设置一个阈值,当报文数超过这个阈值的时候每次,报文每次加一,如果出现超时领阈值等于当前报文的一半,重新执行慢开始——加法增大

  2. 快重传:发送方如果收到3个重复确认,则重传丢失的那个报文而不必继续等待设置的重传计时器时间到期。

  3. 快恢复:当发送方连续收到三个重复确认时,令阈值ssthresh等于当前报文的一半,并令当前发送的报文数等于阈值(cwnd阻塞窗口 = ssthresh阈值)。考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以判断没有出现网络阻塞,执行拥塞避免算法——乘法减小

区别:
发送窗口swnd = min(接受窗口rwnd,拥塞窗口cwnd)
rwnd>cwnd 接收方的接收能力限制发送方窗口的最大值
rwnd<cwnd 网络的拥塞限制发送方窗口的最大值

拥塞窗口 cwnd 变化的规则:
只要网络中没有出现拥塞,cwnd 就会增大;
但网络中出现了拥塞,cwnd 就减少;

TCP的拥塞控制机制是什么?

我们知道TCP通过一个定时器(timer)采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,然而重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这就导致了恶性循环,最终形成“网络风暴” —— TCP的拥塞控制机制就是用于应对这种情况。
首先需要了解一个概念,为了在发送端调节所要发送的数据量,定义了一个“拥塞窗口”(Congestion Window),在发送数据时,将拥塞窗口的大小与接收端ack的窗口大小做比较,取较小者作为发送数据量的上限。
拥塞控制主要是四个算法:
**1.慢启动:**意思是刚刚加入网络的连接,一点一点地提速,不要一上来就把路占满。
连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
每当收到一个ACK,cwnd++; 呈线性上升
每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”
**2.拥塞避免:**当拥塞窗口 cwnd 达到一个阈值时,窗口大小不再呈指数上升,而是以线性上升,避免增长过快导致网络拥塞。
每当收到一个ACK,cwnd = cwnd + 1/cwnd
每当过了一个RTT,cwnd = cwnd + 1
拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:
等到RTO超时,重传数据包
sshthresh = cwnd /2
cwnd 重置为 1
3.快重传——进入慢启动过程
在收到3个duplicate ACK时就开启重传,而不用等到RTO超时
sshthresh = cwnd = cwnd /2
进入快速恢复算法——Fast Recovery
4.快恢复——进入拥塞避免过程:至少收到了3个重复确认Duplicated Acks,说明网络也不那么糟糕,可以快速恢复。
cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
如果再收到 duplicated Acks,那么cwnd = cwnd +1
如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。

http各个版本的区别

http0.9 : 仅支持GET请求,仅能访问html资源
http 1.0 :增加了post和head请求
http1.1 : 增加了长连接,一个tcp连接可以发送多个http请求,新增了put,patch,delete请求
http2.0 : 增加了双工模式,不仅客户端能发送多个请求,服务端也能处理多个请求

数据库MySQL

索引

B树,B+树,以及两者的区别

B树是一种多路平衡查找树,其每一个节点都存储Key和data
B+树是B树的一个变种,叶子节点存储data,非叶子节点只存储key,B+树的叶子节点增加了顺序访问指针,每一个叶子节点都可以访问到他的下一个叶子节点

区别:

  1. B+树种只有叶子节点会带有全部信息,非叶子节点只起到索引的作用,二B树的所有节点都带有全部信息,B+树的每一层节点都会再次出现在下一层节点上
  2. B+树种所有叶子节点都是通过指针连在一起,B树则没有

索引的优点和缺点

优点:可以加大检索速度
缺点:创建和维护索引需要耗费时间

Mysql为什么选择B+树

Mysql数据本质上是放在外部存储的,B+树是为了加快读取速度二设计的一种数据结构

  1. 可以减少i/o次数,只有叶子节点才存储数据,非叶子节点存储索引,这样一次读取到内存的关键字增多,相对i/o次数也就减少(根据区别一)
  2. 能够提供稳定高效的范围扫描,因为所有的叶子节点都互相连接(根据区别二)

索引越多越好吗?

索引可以提高select的效率,但是也降低了insert和updata的效率,因为插入和更新的时候可能会重建索引,索引怎么建索引要慎重考虑。

索引分类

1.B+树索引:以b+树作为数据结构的索引
2.hash索引:能以O(1)的时间复杂度查找,但失去了有序性,innodb有一个自适应哈希索引,当这个索引值被频繁使用时会在b+树上创建一个哈希索引
3.全文索引:用于查找文本的关键词,中文需要由中文分词插件

MySQL优化

MySQL的优化,主要分为索引的的优化,sql语句的优化,表的优化。同时可以使用缓存增加效率

索引的优化

只要列中含有null,最好不要再此列设置索引
对于经常在where语句中使用的列,最好设置一个索引
对于like语句,以%或者-开头的不会使用索引,以%结尾会使用索引

sql语句的优化

查询优化要尽量避免全表扫描
查询时能不用就不用,尽量写字段名

MySQL常问问题

数据库如何应对大规模的写入和读取

(1)使用NoSQL,通过降低数据的安全性,减少对事物的支持,减少复杂查询的支持来获取性能的提升;但有些场合NoSQL无法满足要求

(2)分库分表:
水平切分:不修改数据库的表结构,通过对表中的数据拆分而达到分片的目的,一般水平切分在查询的时候可能会用到union操作(多个结果并)
可以根据hash或者日期来进行分表

垂直切分:修改表结构,按照访问的差异将某些列拆分出去,一般查询数据的时候可能会用到join操作;把常用的字段放在一个表中,不常用的放在一个表中;把字段比较大的比如text字段拆出来放在一个表中。

分库:分表能够解决数据量过大带来的查询效率下降问题,但是却无法给数据库的并发处理能力带来质的提升;分库可以对关键字取模的方式来对数据访问进行路由
在这里插入图片描述
(3)读写分离:
读写分离是在主服务器上修改数据,数据也会同步到从服务器上,从服务器只能提供读取,不能写入,实现备份的同时也实现了数据库的性能优化
在这里插入图片描述
如何保证数据一致性:
(1)主节点
保证事务每次提交之后,要确保binlog都能刷新到磁盘中,只要有了binlog,innoDB就有方法恢复数据,不至于导致主从复制的数据丢失(具体为两阶段提交,解决redo log与binlog逻辑一致性问题)

(2)从节点
开启 relay log 自动修复机制,发生 crash 时,会自动判断哪些 relay log 需要重新从master 上抓取回来再次应用,以此避免部分数据丢失的可能性。

数据库事务及其隔离级别

事务的特性:ACID(原子性、一致性,隔离性、持久性)

事务在并发的时候,隔离性很难保证主要可能出现下面这些问题:
脏读:一个事务读了另外一个事务未提交的数据,如果另外一个事务回滚则会发生脏读
不可重复读:一个事务前后读取同一行数据,如果在这个过程中有其他事务修改了此数据,则导致事务两次读取数据不一致——不可重复读
幻读:一个事务前后读取范围的时候,如果在这个过程中,有其他事务在这个数据范围新增或删除了数据,则导致事务两次读取的的数据范围不一致——幻读

事务隔离级别:
请添加图片描述
MySQL实现事务是基于undo/redo日志实现的:
undo日志记录修改前的状态,ROLLBACK基于undo日志实现——保证事务的原子性

redo日志记录修改后的状态,事务的持久性基于redo日志实现——保证事务的持久化

MVCC——保证事务的隔离性

事务的一致性由其他三个特性保证,是数据库最终的目标。

两种解决脏读、不可重复读、幻读的方案:

MVCC——多版本并发控制(性能较高,但读的可能是历史版本)——在RC,RR隔离级别下
由事务的两个隐藏字段(rtx_id:事务id,roll_point:回滚指针)、redo log、read view实现。

1.版本链:对于每一行的数据,在undo日志中,总会记录每个版本记录以及对应的事务id,
2.readView:

核心问题:当前版本链中哪个版本对当前事务可见
Readview包含的内容:{当前活跃的事务id,下一个应该分配的事务id,当前自己的事务id},根据当前读的版本事务id和这个readview对比,如果是活跃的或者大于下一个应该分配的事务id则说明当前版本对此事务不可见,应该前读一个版本,依次类推直到找到可见的版本

提交读:每次读取数据前都会生成一个readview

可重复读:在第一次读取时句时生成一个readview

锁(性能不高,但读的是最新版本)
InnoDB存储引擎一共有四种锁:共享锁(读锁,S锁)、排他锁(写锁,X锁)、意向共享锁(IS锁)和意向排他锁(IX锁)
其中共享锁与排他锁属于行级锁,另外两个意向锁属于表级锁

共享锁(读锁,S锁):若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放S锁。
排他锁(写锁,X锁):若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他事务不能再对A加作何类型的锁,直到T释放A上的X锁。
意向共享锁(IS锁):事务T在对表中数据对象加S锁前,首先需要对该表加IS(或更强的IX)锁。
意向排他锁(IX锁):事务T在对表中的数据对象加X锁前,首先需要对该表加IX锁。

意向锁的出现是为了更好细化锁的层级粒度,并且能提高锁处理性能:在数据库处理事务A请求的行级锁之前,会先申请该资源对应根目录资源的意向锁,以便能在其他事务B中再次请求表锁时做到阻塞。
请添加图片描述

MyISAM和innoDB的区别

1.innoDB支持行锁,MyISAM不支持行锁
2.innoDB支持事务,MyISAM不支持事务
3.innoDB支持回滚和安全回复,MyISAM不支持
4.innoDB的索引文件同时保存索引与数据——聚集索引,MyISAM的索引文件只存储了主键和行号,还需要根据行号去查找相应的记录——非聚集索引
5.innoDB更适合写密集的表,MyISAM更适合读密集的表
6.innoDB表中没有保存行总数,MyISAM用一个变量记录行总数

数据库redis

redis的数据淘汰策略

当redis内存数据大小达到一定的大小时,就会施行数据淘汰策略,主要有六种策略
请添加图片描述

数据库和缓存的数据一致性

mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据

根据数据淘汰策略,先算一下这20W的数据大概占多少内存,然后设置redis的内存,启用从所有数据集中挑选最近最少使用的淘汰策略

redis缓存和mysql数据库同步

Redis持久化

1.RDB持久化——Redis DataBase(redis默认方式)
将某个时间点的所有数据都存在硬盘中,如果发生故障将丢失最后一次创建快照的数据
触发RDB快照的条件:在指定的时间间隔内,执行指定次数的写操作

2.AOF持久化——Append Only File
所执行的每一条指令,都会记录到appendonly.aof文件中,redis会按照策略将指令存入硬盘中。当redis重启的时候会根据日志文件的内容将写指令从前到后执行一次完成数据恢复的功能

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鳄鱼儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值