一篇很长的Java复习笔记(想到什么写什么)

188 阅读19分钟

多线程

  1. 并发与并行:

    • 并行:单位时间多个程序同时执行
    • 并发:一段时间内的多个任务交替执行
  2. 线程与进程:

    • 何为进程:进程是程序的一次运行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序既是一个进程 创建-->运行-->消亡 的过程
    • 何为线程:线程是进程划分成的更小的运行单位。
    • 区别:进程与线程最大的区别在于基本上各进程之间是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但是不利于资源的管理和保护。
  3. 为什么要使用多线程:

    1. 从底层来看:线程可以比作轻量级的进程,线程间的切换和调度成本远远小于进程。多核CPU时代意味着多个线程可以同时运行,减少了线程上下文切换的开销。
    2. 单核时代:提高CPU和I/O设备的综合利用率。
    3. 多核时代: 提高CPU的利用率。
  4. 使用多线程可能带来什么问题:死锁 ,上下文切换

    • 死锁:多个线程为争夺对方所拥有的资源而阻塞导致的无法推进的状态
      • 死锁的四个必要条件:
        1. 互斥:同一个资源同一时刻只能由一个线程占有、
        2. 请求与保持:在请求其他资源时自身占有的资源不释放
        3. 不可剥夺:线程拥有的资源在执行完之前不释放
        4. 循环等待:多个线程形成首尾相接的等待资源的情况
      • 如何避免死锁:破坏四个必要条件之一
        1. 互斥:无法破坏,使用锁就是为了临界资源互斥
        2. 请求与保持:一次性申请所有需要的资源
        3. 不可剥夺:在占有部分资源而请求不到其他资源时释放占有的资源
        4. 循环等待:按序申请,反序释放
    • 线程上下文切换:当前线程在执行完CPU时间片切换到另一个线程之前会先保存自己的执行状态,以便下次再切换回这个线程时,可以再加载这个线程的状态。线程从保存到再加载的过程就是一次上下文切换
  5. 线程的生命周期:

    • NEW:初始状态
    • RUNNABLE:运行状态
    • BLOCKED:阻塞状态
    • WATTING:等待状态
    • TIME-WATTING:超时等待状态
    • TERMINATED:终止状态
  6. 实现多线程的几种方式:

    • 继承Thread类,重写run()方法
    • 实现Runnable接口,重写run()方法
    • 实现Callable接口,重写call()方法(此种方式可以获得线程执行结果)
    • 线程池
  7. sleep()方法和wait()方法的区别:

    • sleep()方法执行后不释放锁,wait()方法执行后释放锁
    • 两者都可以暂停线程的执行

synchronized关键字

相关概念

  • synchronized关键字用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被他修饰的方法或代码块在任意时刻只能有一个线程执行(访问)

使用方式

  • 主要分为三种
    • 修饰实例方法:用于给当前对象实例加锁,进入同步代码前需要先获取当前对象的锁。
    • 修饰静态方法:相当于给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员。
    • 修饰代码块:指定需要加锁的对象,进入同步代码块前要获得给定对象的锁。
    • **总结:synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是给Class上锁,加到实例方法上是给对象实例上锁 **
  • synchronized 应用场景:单例模式(双重检验锁)
    public class Singleton{
        private voletile static Singleton uniqueInstance;
        private Singleton(){
        }
        public synchronized static Singleton getUniqueInstance(){
            //先判断对象是否实例过,没有实例化过才进入加锁代码
            if(uniqueInstance == null){
            //类对象加锁
            synchronized (Singleton.class){
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
            }
            return uniqueInstance;
        }
    }
    
    • volatile作用:防止多线程下指令重排带来问题: 比如:uniqueInstance = new Singleton();这段代码是分为三步执行的:
    1. 为uniqueIntance分配内存空间
    2. 初始化uniqueInstance
    3. 将uniqueInstance 指向分配的内存地址 但是由于JVM具有指令重排的特性,执行顺序可能变为1-3-2.指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获取还没有初始化的实例。例如:线程T1执行了1和3,此时线程T2调用getUniqueInstance后发现uniqueInstance不为空,因此返回uniqueInstance ,但是此时uniqueInstance还未初始化。

底层原理

synchronized关键字底层原理属于JVM层面。

  1. synchronized同步代码块的情况下查看编译后的class文件可以看到加了同步的代码之间有monitorentermonitorexit两个指令,用于监控各线程对同步代码块的访问情况,并且设置了一个计数器,当计数器为0时则可以成功获取锁,同时计数器加一,此时其他线程访问此代码时阻塞,当此线程执行到monitorexit指令时计数器减一,表明锁被释放,允许其他线程访问。

JDK1.6之后synchronized的锁升级机制

锁主要存在四种状态:无锁,偏向锁,轻量锁,重量级锁,他们会伴随着竞争的激烈程度而逐渐升级。锁可以升级但不能降级,这种策略是为了提高获得锁和释放锁的效率。

  • 偏向锁: 当一段加锁代码在很长一段时间内只有一个线程访问时,每次访问都需要进行获取锁和释放锁的操作,而这个操作又非常消耗性能,所以就采用了偏向锁来解决这种竞争较小的同步代码;当一个线程访问同步代码时,将该线程的线程id记录下来,当该线程下次访问时同步代码块时,只需要查询一下偏向锁是否有该id,如果有则无需进行获取锁的操作了。
  • 轻量级锁: 如果一个线程访问同步代码时时没有存在该线程的id,则尝试获取偏向锁,获取成功则将该线程的id替换掉之前存在的线程id,当前锁还为偏向锁,而当获取失败时则表示存在竞争,偏向锁升级为轻量级锁,当前线程尝试自旋获取锁,如果达到一定时间(JVM决定)还是获取锁失败则升级为重量级锁。(轻量级锁适用于多个线程短时间交替执行的情况)
  • 重量级锁:monitor指令控制,每次访问都需要获取和释放锁(用于线程竞争激烈且执行时间较长的情况)

synchronized和ReentrantLock(Lock接口的实现类)的区别

  1. 两者都是可重入锁:一个对象获取锁之后在还未释放锁之前再次获取该锁时是可以获取的(虽然计数器不为0),如果synchronized不可重入的话则会造成死锁。
  2. synchronized依赖于JVM实现(JVM层面)而ReentrantLock依赖于API;表示synchronized实现线程同步是在JVM层面实现的,并没有直接暴露给我们(自动锁),而Lock机制(手动锁)是在JDK层面实现的,比如需要lock()/unlock()方法手动获取和释放锁,所以可以查看源代码来看到他是怎么实现的的。
  3. Lock比synchronized多了一些高级功能:①等待可中断 ②可以实现公平锁(AQS机制实现的) ③可实现选择性通知(锁可以绑定多个条件)

volatile

  • 作用:(不保证原子性,synchronized保证原子性)
  1. 内存可见性
  2. 防止指令重排
  • 怎么保证的内存可见性:(原理类似于缓存一致性协议MESI) 在JDK1.2之前,java 的内存模型实现总是从内存(即共享内存)读取变量,是不需要特别注意的。而在当前的Java内存模型中,线程可以把变量保存在本地内存中(比如寄存器),而不是直接在内存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在本地内存中的变量值的拷贝,造成数据的不一致。 此时就可以使用volatile声明变量,这就相当于告诉JVM这个变量是不稳定的,每次使用它都需要去主存中读取。

  • MESI:缓存一致性协议:当CPU写入数据时,如果发现操作的变量是共享变量,即在对他进行操作后,通知其他使用该变量的CPU将缓存中的变量副本设置为无效,因此当其他CPU从缓存中读取该变量时发现是无效的,那么就会从内存中重新读取,以保证数据的一致性。

  • 并发编程的三个特性:

  1. 原子性:一个操作或多个操作,要么所有的操作都执行完成,要么都不执行。synchronized就可以保证代码片段的原子性。
  2. 内存可见性:当一个操作对共享变量进行了修改,那么另外的线程都可以立即看到修改后的最新值。volatile即可保证共享变量的内存可见性
  3. 有序性: 代码在执行过程中的先后顺序,由于Java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候得到顺序。volatile就能保证有序

ThreadLocal

  1. 简介:通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类比作存放数据的盒子,盒子中可以存储每个线程的私有数据。

线程池

  1. 为什么要用线程池?(池化思想:线程池,数据库连接池,Http连接池)
    • 当我们每使用一个线程时就创建一个线程,用完就销毁,实现起来非常方便,但是当线程数量很多时,并且每一个线程只执行一个时间很短的任务就销毁了,那这样频繁的创建和销毁线程对系统来说就造成了很大的开销;所以就采用线程池的思想来达到线程复用的功能,就是执行完一个任务,线程不需要销毁就可以执行其他任务!
  2. 概念:线程池就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建和销毁线程的操作,减少了资源的消耗。
  3. 好处:
    • 降低资源消耗。
    • 提高响应速度。当任务到达时,任务无需等待线程创建就能立即执行
    • 提高线程的可管理性。使用线程池可以对线程进行统一的分配,调优和监控。
  4. 如何创建线程池:
  • 方式一:利用内置API Executor创建线程池(不推荐)

    1. 使用Executor类提供的静态方法newFixedThreadPool(int nThreads)生产一个指定长度(参数nThreads)的线程池;
    2. 创建一个类实现Runnable接口,重写run方法,设置线程任务;
    3. 调用ExecutorService中的submit(Runnable task)方法传递线程任务(实现类),开启线程,执行run方法
    4. 调用ExecutorService中的shutdown方法销毁线程池(一般不执行此步骤)
  • 方法二:手动通过参数来配置ThreadPoolExecutor(Executor的本质),实例化一个线程池实例

    1. 创建线程的API:
        ThreadPoolExecutor(int corePoolSize, 
                   int maximumPoolSize, 
                   long keepAliveTime, 
                   TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue, 
                   RejectedExecutionHandler handler)
    
    

    核心参数:7大核心参数

    • corePoolSize:核心线程数(最小可以同时运行的线程数量)

    • maximumPoolSize:最大线程数 (当池中存放的任务达到最大容量的时候,当前可以同时运行的线程数量变为最大线程数)

    • workQueue:任务队列 ,用于保存executor提交的Runnable任务(当新任务来的时候先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会放到队列中)

    • keepAliveTime:当线程池中的线程数量大于核心线程数的时候,如果没有新的任务提交,核心线程外的线程不会被立即销毁,而是会等待,直到等待时间超过了KeepAliveTime才会被回收销毁

    • unit:keepAliveTime 参数的时间单位

    • threadFactory: executor创建新线程时使用的工厂

    • handler:饱和策略(当前同时运行的线程数量达到最大线程数量并且队列也已经放满了任务时执行的处理程序)

JVM

  1. 内存区域:分为5个部分,分别为:堆,方法区(1.8之后元空间),虚拟机栈,本地方法栈,程序计数器
    • 线程共享的:
      • 堆:存放对象实例,几乎所有的对象实例以及数组都在此分配内存,又叫GC堆,分为新生代和老年代
      • 方法区(元空间):存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等,又称为永久代(元空间使用的是直接内存)
    • 线程私有的:
      • 虚拟机栈:方法执行的区域
      • 本地方法栈
      • 程序计数器
    • 运行时常量池:方法区的一部分,用于存放字面量和符号引用(1.7后移出常量池,在堆中新开辟一块区域存放运行时常量池)
      • 字面量
        1. 文本字符串
        2. final常量值
        3. 基本数据类型的值
      • 符号引用
        1. 类和结构的全限定类名
        2. 字段名称和描述符
        3. 方法名称和描述符
  2. Java对象的创建过程

image.png

  1. 堆内存中对象的分配策略
    • 堆空间基本结构

image.png * 其中eden,s0,s1为新生代区,tentired为老年代,大部分情况(大对象直接进入老年代),对象会先在eden区分配,在一次新生代垃圾回收后,还存活的对象会进入s0或s1,同时年龄加1,当年龄增加到一定程度(默认为15)时,会升级为老年代,年龄阈值可以通过参数XX:MaxlenuringIhreshold来设置 * 大对象直接进入老年代 4.Minor Gc(新生代Gc)和Full Gc * 新生代Gc(Minor Gc):当eden没有足够空间时发生的Gc * 老年代Gc(Major Gc/Full Gc):发生在老年代的Gc,速度一般比新生代Gc慢10倍以上 5. 如何判断对象是否死亡?(两种方法) * 引用计数法:给对象添加一个引用计数器,每当一个地方引用它,计数器加1;当引用失效,计数器减1,计数器为0的对象就是可回收对象 * 可达性分析算法:思路是以“GC Roots”的对象为起点,从这些节点开始向下搜索,节点走过的路径为引用链,当一个对象到GC Roots没有任何引用链相连的话,证明可回收。

image.png

  1. 四种引用(强、软、弱、虚)

    • 无论通过引用计数还是可达性分析算法判断对象对象是否存活都与引用有关
    1. 强引用:Gc绝不会回收的对象,当内存空 间不⾜,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终⽌,也不会靠随意回收具有强引⽤的对象来解决内存不⾜问题。
    2. 软引用:空间足够就不会回收,空间不足时就会回收这些对象的内存
    3. 弱引用:弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。
    4. 虚引用:"虚引⽤"顾名思义,就是形同虚设,与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期。如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被垃圾回收。
  2. 垃圾回收算法:

    • 标记-清除法:先标记所有需要回收的对象,标记完后统一回收所有被标记对象。会带来两个问题:
      1. 效率问题
      2. 空间问题(标记清除后产生大量不连续的内存碎片)

image.png

* 复制算法:为了解决效率问题而产生的;将内存分为大小相等的两块,每次使用其中一块,当这一块使用完后,就将存活的对象复制到另一块,然后把使用的空间一次性清理掉。

image.png

* 标记-整理算法:根据老年代的特点产生的算法;标记过程与“标记-清除法”一致,后续则不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。

image.png

* 分代收集算法:当前虚拟机的垃圾收集都采用分代收集算法;根据对象存活周期的不同将内存分为几块。一般将堆分为新生代和老年代,这样就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要少量对象的复制成本就可以完成每次的垃圾回收。而老年代的对象存活几率是比较高的,而且没有额外的空间对他进行分配担保,所以就必须使用“标记-清除”或者“标记-整理”算法进行垃圾回收。

类加载器

  1. JVM内置的三个ClassLoader,除了BootstarpClassLoader另两个类加载器均由Java实现且继承自java.lang.ClassLoader
    • BootstarpClassLoader:启动类加载器,由C++实现
    • ExsesionClassLoader:扩展类加载器
    • AppClassLoader:应用程序类加载器
  2. 双亲委派模型:每个类都有一个对应他的类加载器。系统中的ClassLoader在协同工作时会默认使用双亲委派模型。
    • 即在类加载的时候,系统会先判断当前类是否加载过,如果加载过会直接返回,否则才会尝试加载
    • 加载的时候,首先会把请求委派其父类加载器处理,因此所有的请求最终都应该传送至顶层的启动类加载器 BootstapClassLoader中。当父类加载器无法处理时(自定义类加载器),才会由自己来处理。当父类加载器为null时,会启动类加载器 BootstrapClassLoader为父类加载器开始加载。

image.png

  1. 双亲委派模型的好处:
    • 保证了Java程序的稳定运行
    • 可以避免类的重复加载(JVM区分不同类不仅仅根据类名,相同的类文件不同的类加载器加载产生的是两个不同的类)
    • 保证了Java的核心API不被篡改
  2. 不想使用双亲委派机制怎么办:可以自定义一个类加载器,然后重载loadClass()方法即可

计算机网络

  1. OSI和TCP/IP体系结构

image.png

  1. TCP三次握手和四次挥手
    • 三次握手:

image.png 简单示意图:

image.png

  1. 客户端发送带有SYN标志的数据包
  2. 服务端发送带有SYN/ACK标志的数据包
  3. 客户端发送带有ACK标志的数据包
  • 为什么要三次握手:目的是建立可靠的通信通道,通信简单来说就是数据的发送和接收
    1. 第一次握手:客户端什么都不能确定;服务端确定了对方发送正常,自己接受正常
    2. 第二次握手:客户端确认了:自己发送、接收正常,对方发送接收正常;服务端确认了:对方发送正常,自己接收正常
    3. 第三次握手:客户端确认了自己发送、接收正常,对方发送、接收正常;服务端确认了自己发送、接收正常,对方发送、接收正常
    • 三次握手就能确认双方发送接收功能是否正常,缺一不可

    • 四次挥手:断开一个TCP链接的过程

      1. 客户端发送一个FIN,用来关闭客户端到服务端的数据传递
      2. 服务端收到这个FIN,它发回一个ACK,确认序号为收到的序号加1
      3. 服务端关闭与客户端的连接,发送一个FIN给客户端
      4. 客户端发回ACK报文确认,并将确认序号设置为收到序号加1

image.png

  1. TCP,UDP的区别:

image.png

MySQL

  1. 一种开源的,稳定的关系型数据库
  2. 存储引擎:
    • MyISAM 5.5之前的默认引擎
    • InnoDB 5.5之后的默认引擎
    • 区别:
      1. 是否支持事务和崩溃后的安全恢复:InnoDB支持,MySAM不支持
      2. 是否支持行级锁:InnoDB支持,MySAM只有表级锁
      3. 是否支持外键: MySAM不支持,InnoDB支持
  3. 事务: 事务是逻辑上的一组操作,要么都执行,要么都不执行
  4. 事务的四大特性(ACID):
    • 原子性
    • 一致性:执行事务前后,数据保持一致
    • 隔离性:并发访问数据库时,一个事务不被其他事务干扰
    • 持久性:一个事务提交之后,他对数据的改变是持久的,即使是数据库发生故障也不应该有影响
  5. 事务隔离级别
    • 读未提交:会导致脏读,幻读或不可重复读
    • 读提交:解决了脏读的问题
    • 可重复读:InnoDB默认隔离级别,解决了脏读和不可重复读得到问题
    • 串行化:最高的隔离级别,解决了脏读,幻读,不可重复读